PageSourceSearch

https://usrtoscana.vercel.app/widget-inline.js

js usrtoscana.vercel.app collected 2026-10-03 07:57:12 UTC 1,022,799 bytes, 15,131 lines download raw bytes

1window.__CAF_PRIVACY__ = false;
2window.__CAF_DOMANDEFISSE__ = true;
3window.__CAF_FIREBASE_CONFIG__ = {"apiKey":"","authDomain":"","projectId":"","storageBucket":"","messagingSenderId":"","appId":""};
4window.__CAF_ZONA__ = "";
5window.__CAF_DEMOPROMO__ = false;
6window.__CAF_DEMO_PROFILE__ = "";
7window.__CAF_PIEMONTE__ = false;
8window.__CAF_NEUTRAL__ = true;
9window.__CAF_CHATBOT_NAME__ = "CAF CISL";
10window.__CAF_PRIVACY_INFO__ = {"controller":"","address":"","email":"","phone":"","vat":"","dpo":""};
11window.__CAF_CONSENSO__ = true;
12window.__CAF_AI_DISCLOSURE__ = true;
13window.__CAF_PRIVACY_URL__ = "";
14window.__CAF_HELPLINE__ = "055 43921";
15window.__CAF_WIDGET_BUILD__ = "fa23cdd7802e@2026-10-03T07:25:44.501Z";
16/**
17 * CAF CISL Toscana – Inline Chat Widget (no iframe)
18 *
19 * Drop this script on any page. It injects a floating chat bubble + panel
20 * entirely via the DOM using Shadow DOM so it won't clash with the host page's
21 * CSS. All styles are self-contained.
22 *
23 * Usage:
24 *   <script
25 *     src="https://YOUR_DOMAIN/widget-inline.js"
26 *     data-base-url="https://YOUR_DOMAIN"
27 *     data-color="#059669"
28 *     data-name="CAF CISL - Toscana"
29 *     data-logo="https://indesignstudio.b-cdn.net/logoCAFCISL.png"
30 *     data-welcome="Ciao, come posso aiutarti?"
31 *     data-quick-replies="Che spese si possono detrarre?|Vorrei ricevere più informazioni|Qual è il centro CAF più vicino a me?"
32 *     data-voice="si"
33 *   ></script>
34 *
35 * Host-page triggers (no JS needed): add data-caf-action to any element.
36 *   <button data-caf-action="open">Chat</button>     <!-- opens the chat -->
37 *   <button data-caf-action="book">Prenota</button>  <!-- opens the booking wizard -->
38 *   Works on any element (button, a, div…), even if added to the DOM later
39 *   (event delegation). Programmatic equivalent of the two actions:
40 *     CafChatInline.open()         // open chat with the welcome message
41 *     CafChatInline.openBooking()  // open the booking wizard directly
42 *
43 * Or initialise manually:
44 *   CafChatInline.init({ baseUrl: "https://YOUR_DOMAIN" });
45 */
46(function () {
47  "use strict";
48
49  if (window.CafChatInline) return;
50
51  /* ── Storage namespace: isolamento per-LINK (origine) ──────────────────
52   * BUGFIX (leak cross-sito): tutte le chiavi localStorage/sessionStorage
53   * erano costanti GLOBALI identiche ovunque. localStorage è già per-origine,
54   * ma due tenant serviti dalla STESSA origine — pagina demo che cambia
55   * <script src>, localhost tra build di progetti diversi, preview, dominio
56   * condiviso multi-tenant — finivano nello STESSO bucket: la conversazione
57   * (e i dati cliente + il consenso) "passava" da un sito all'altro.
58   *
59   * REQUISITO: ogni LINK deve avere uno storico COMPLETAMENTE indipendente,
60   * senza alcuna persistenza tra link diversi. Quindi la chiave è legata
61   * ESPLICITAMENTE all'origine (location.origin = schema://host:porta), così
62   * l'isolamento è dimostrabile e non dipende da nessuna assunzione. In più
63   * incorporiamo l'identità tenant iniettata dalla route (__CAF_ZONA__ /
64   * __CAF_CHATBOT_NAME__ / flag) per separare anche più tenant sulla STESSA
65   * origine. Due contesti condividono lo storico SOLO se hanno STESSO link E
66   * STESSO tenant — cioè sono lo stesso identico sito. La parte leggibile
67   * (host + zona) è solo per ispezione DevTools; l'unicità è garantita
68   * dall'hash calcolato sull'origine RAW + tenant (immune a collisioni dei
69   * caratteri persi nella sanitizzazione). */
70  var CAF_NS = (function () {
71    /* ISOLAMENTO CONVERSAZIONI — SEMPRE, IN QUALSIASI CIRCOSTANZA.
72     * Ogni (origine + tenant) ha le proprie chiavi storage: due chatbot di
73     * tenant diversi NON condividono mai la conversazione, nemmeno se serviti
74     * dalla STESSA origine (pagina che cambia config, localhost, dominio
75     * condiviso). L'identita' tenant arriva dai globali iniettati dalla route
76     * (__CAF_ZONA__/__CAF_CHATBOT_NAME__/flag) + location.origin. Suffisso
77     * deterministico e stabile per tenant: basis iniettiva (JSON.stringify) +
78     * hash a 64 bit -> nessuna collisione. Effetto una-tantum: gli utenti
79     * esistenti ripartono da bucket pulito (purga lo stato gia' contaminato);
80     * il FLUSSO del prenotatore resta identico. */
81    try {
82      var w = (typeof window !== "undefined") ? window : {};
83      var loc = (typeof location !== "undefined" && location) || {};
84      /* Il LINK: origine completa (schema://host:porta). Fallback host/href e
85       * infine costante: mai stringa vuota, mai chiave globale condivisa. */
86      var link = (loc.origin || loc.host || loc.href || "nolink").toString();
87      var zona = (w.__CAF_ZONA__ || "").toString().trim().toLowerCase();
88      var name = (w.__CAF_CHATBOT_NAME__ || "").toString().trim().toLowerCase();
89      var demo = (w.__CAF_DEMOPROMO__ === true) ? "1" : "0";
90      var pie  = (w.__CAF_PIEMONTE__ === true) ? "1" : "0";
91      /* basis iniettiva al 100%: JSON.stringify codifica i confini dei campi
92       * senza separatori ambigui (niente NUL, niente spazi). */
93      var basis = JSON.stringify([link, zona, name, demo, pie]);
94      /* due hash indipendenti (djb2-xor + sdbm) -> 64 bit effettivi. */
95      var h1 = 5381, h2 = 0;
96      for (var i = 0; i < basis.length; i++) {
97        var c = basis.charCodeAt(i);
98        h1 = ((((h1 << 5) + h1) ^ c) >>> 0);
99        h2 = ((h2 * 65599 + c) >>> 0);
100      }
101      var hash = ("0000000" + h1.toString(36)).slice(-7) + ("0000000" + h2.toString(36)).slice(-7);
102      /* tag leggibili: includiamo anche il NOME, cosi' due tenant con stesso
103       * host+zona ma nome diverso restano distinti gia' nella parte leggibile. */
104      var hostTag = (loc.host || loc.hostname || "").toString().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 24);
105      var zonaTag = zona.replace(/[^a-z0-9]+/g, "").slice(0, 12);
106      var nameTag = name.replace(/[^a-z0-9]+/g, "").slice(0, 12);
107      return "::" + [hostTag, zonaTag, nameTag, hash].filter(Boolean).join("-");
108    } catch (_e) {
109      /* fallback non-vuoto: meglio un bucket isolato che la chiave globale. */
110      return "::isolated";
111    }
112  })();
113  function nsKey(base) { return base + CAF_NS; }
114  /* Chiavi un tempo inline, ora scopate per tenant (riusate in più punti). */
115  var BK_SID_KEY = nsKey("caf-bk-sid");
116  var BK_LANG_KEY = nsKey("caf-bk-lang");
117
118  /* ── Consenso privacy CHAT (gate all'apertura, vedi opts.consenso) ──
119   * Registro del consenso espresso dall'utente per usare la chat. Come il
120   * consenso del wizard prenotazione, il RECORD del consenso e' esso stesso
121   * "necessario" e viene salvato anche in privacy-mode. Chiave scopata per
122   * tenant via nsKey → mai condivisa cross-tenant. Attivo SOLO quando il
123   * deploy/host imposta opts.consenso (env CONSENSO=si / data-consenso).  */
124  var CHAT_CONSENT_KEY = nsKey("caf-chat-consent-v2");
125  function chatConsentLoad() {
126    try {
127      var r = localStorage.getItem(CHAT_CONSENT_KEY);
128      if (!r) return null;
129      var c = JSON.parse(r);
130      /* L. 132/2025 art. 4 c.4: un record senza dichiarazione di eta'
131         (scritto da versioni pre-age-gate) NON supera il gate: l'utente
132         deve riconfermare consenso E dichiarazione >=14 anni. */
133      return (c && c.v === 2 && c.ageDeclared === true) ? c : null;
134    } catch (_e) { return null; }
135  }
136  function chatConsentSave(url, ageOk) {
137    /* mini audit trail: SOLO origin+pathname (mai query/hash: potrebbero
138       contenere token o dati personali). ageOk = dichiarazione >=14 anni
139       (L. 132/2025 art. 4 c.4). */
140    var pg = "";
141    try { pg = location.origin + (location.pathname || ""); } catch (_e) {}
142    try {
143      localStorage.setItem(CHAT_CONSENT_KEY, JSON.stringify({
144        v: 2, ts: Date.now(), url: url || "", ageDeclared: !!ageOk,
145        page: pg
146      }));
147    } catch (_e) { /* storage negato/pieno: resta il flag in-memory */ }
148  }
149
150  /* ── helpers ───────────────────────────────────────────────── */
151  function getAttr(name) {
152    var scripts = document.getElementsByTagName("script");
153    var scriptSrcs = [];
154    for (var i = scripts.length - 1; i >= 0; i--) {
155      var s = scripts[i];
156      scriptSrcs.push(s.src || "(no src)");
157      if (s && s.src && s.src.indexOf("widget-inline") !== -1) {
158        return s.getAttribute("data-" + name) || "";
159      }
160    }
161    return "";
162  }
163
164  function escapeHtml(str) {
165    var d = document.createElement("div");
166    d.appendChild(document.createTextNode(str));
167    return d.innerHTML;
168  }
169
170  function quickId() {
171    try {
172      var _c = (typeof crypto !== "undefined") ? crypto : null;
173      if (_c && _c.getRandomValues) {
174        var _a = new Uint32Array(2);
175        _c.getRandomValues(_a);
176        return (_a[0].toString(36) + _a[1].toString(36)).slice(0, 8);
177      }
178    } catch (_e) {}
179    return Math.random().toString(36).slice(2, 10);
180  }
181
182  /* 1.4.13 reduced-motion: usa "auto" quando l'utente ha ridotto le animazioni */
183  function bkScrollBehavior() {
184    return (window.matchMedia && window.matchMedia("(prefers-reduced-motion:reduce)").matches) ? "auto" : "smooth";
185  }
186
187  /* ── pagine su cui il widget NON deve MAI essere mostrato ──────
188   *
189   * Richiesta esplicita: il widget inline non va MAI mostrato su
190   * queste pagine del sito cafcisl.it:
191   *   https://www.cafcisl.it/it-schede-11-prenota_appuntamento
192   *   https://www.cafcisl.it/it-schede-706-dichiarazione_dei_redditi
193   *   https://www.cafcisl.it/it-schede-437-dichiarazione_di_successione
194   *
195   * Il match avviene SOLO sul pathname (gli ID schede 11 / 706 / 437 sono
196   * specifici del CMS di cafcisl.it, quindi non serve vincolare l'host
197   * e così il blocco regge anche con/senza "www", http/https, ecc.).
198   * Si ignorano trailing slash, query string, hash e maiuscole.
199   * Su qualsiasi altra pagina il widget continua a essere mostrato.
200   */
201  var WIDGET_BLOCKED_PATHS = [
202    "/it-schede-11-prenota_appuntamento",
203    "/it-schede-706-dichiarazione_dei_redditi",
204    "/it-schede-437-dichiarazione_di_successione",
205  ];
206
207  function isWidgetBlockedPage() {
208    try {
209      var path = (location.pathname || "").toLowerCase().replace(/\/+$/, "");
210      for (var i = 0; i < WIDGET_BLOCKED_PATHS.length; i++) {
211        if (path === WIDGET_BLOCKED_PATHS[i]) return true;
212      }
213      return false;
214    } catch (e) {
215      return false;
216    }
217  }
218
219  /* ── Mantis sentinel parser ─────────────────────────────────
220   *
221   * Il backend `/api/chat` emette in streaming due linee sentinel:
222   *   <<<MANTIS_RETRIEVED>>>{...citations...}\n
223   *   <<<MANTIS_DONE>>>{...conversationId,confidence,...}\n
224   * Servono al widget React per esporre metadata, ma per questo
225   * widget vanilla NON devono essere mai visibili all'utente.
226   *
227   * Questa funzione rimuove i sentinel dal testo. Robusta su:
228   *   1. Sentinel completo
229   *   2. Sentinel con prefix completo ma JSON incompleto (manca \n)
230   *      → tronca dal prefix in poi
231   *   3. Suffix parziale del prefix (es. "<<<MAN") → tronca quel suffix
232   *
233   * Strip anche citazioni inline tipo `[F1, descrizione]` che il
234   * modello a volte inserisce nel testo della risposta.
235   */
236  var MANTIS_RETRIEVED_PREFIX = "<<<MANTIS_RETRIEVED>>>";
237  var MANTIS_DONE_PREFIX = "<<<MANTIS_DONE>>>";
238
239  function parseMantisSentinels(raw) {
240    var clean = raw;
241    var retrieved, done;
242    function stripPrefixedLine(prefix, onParsed) {
243      while (true) {
244        var idx = clean.indexOf(prefix);
245        if (idx === -1) return;
246        var nl = clean.indexOf("\n", idx + prefix.length);
247        if (nl === -1) {
248          /* payload non ancora completo: tronca dal prefix in avanti */
249          clean = clean.slice(0, idx);
250          return;
251        }
252        var jsonText = clean.slice(idx + prefix.length, nl);
253        try {
254          if (onParsed) onParsed(JSON.parse(jsonText));
255          clean = clean.slice(0, idx) + clean.slice(nl + 1);
256        } catch (e) {
257          /* JSON malformato: rimuovi la linea senza distruggere il resto */
258          clean = clean.slice(0, idx) + clean.slice(nl + 1);
259        }
260      }
261    }
262    stripPrefixedLine(MANTIS_RETRIEVED_PREFIX, function (p) { retrieved = p; });
263    stripPrefixedLine(MANTIS_DONE_PREFIX, function (p) { done = p; });
264
265    /* Tail check: suffix parziale di uno dei prefix → nascondi */
266    var prefixes = [MANTIS_RETRIEVED_PREFIX, MANTIS_DONE_PREFIX];
267    for (var pi = 0; pi < prefixes.length; pi++) {
268      var prefix = prefixes[pi];
269      var maxLen = Math.min(prefix.length - 1, clean.length);
270      for (var len = maxLen; len > 0; len--) {
271        var tail = clean.slice(clean.length - len);
272        if (prefix.indexOf(tail) === 0) {
273          clean = clean.slice(0, clean.length - len);
274          break;
275        }
276      }
277    }
278
279    /* Strip citazioni inline tipo [F1, descrizione] o [F2] */
280    clean = clean.replace(/\s*\[F\d+(?:[\s,]+[^\]]*)?\]/g, "");
281    /* Strip [F<n>... aperto a fine buffer (in arrivo durante stream) */
282    var openIdx = clean.lastIndexOf("[F");
283    if (openIdx !== -1 && clean.slice(openIdx).indexOf("]") === -1) {
284      var afterF = clean.slice(openIdx + 2);
285      if (afterF.length === 0 || /^\d/.test(afterF)) {
286        clean = clean.slice(0, openIdx).replace(/\s+$/, "");
287      }
288    }
289
290    return { clean: clean, retrieved: retrieved, done: done };
291  }
292
293  /* ── booking intent detection via Claude API ────────────── */
294
295  /** Ask Claude to analyze the user's message for booking intent.
296   *  Returns { isBooking, service, location } or null on error. */
297  async function analyzeBookingIntent(baseUrl, text) {
298    /* Quick pre-filter: skip Claude for very short or obviously non-booking messages */
299    var t = text.toLowerCase().trim();
300    if (t.length < 3) return null;
301    /* skip pure greetings/thanks */
302    if (/^(ciao|salve|buongiorno|buonasera|grazie|ok|va bene|perfetto|arrivederci|bye)\s*[.!?]*$/i.test(t)) return null;
303    try {
304      var ctrl = new AbortController();
305      var tid = setTimeout(function(){ ctrl.abort(); }, 4000);
306      var r = await fetch(baseUrl + "/api/booking-intent", {
307        method: "POST",
308        headers: { "Content-Type": "application/json" },
309        body: JSON.stringify({ message: text }),
310        signal: ctrl.signal,
311      });
312      clearTimeout(tid);
313      if (!r.ok) return null;
314      return await r.json();
315    } catch (e) { return null; }
316  }
317
318  /* Lightweight regex fallback — only used if Claude API is unavailable */
319  var BOOKING_KEYWORDS =
320    /\b(prenot\w*|appuntamento|fissare|booking|devo\s+fare|ho\s+bisogno\s+di|mi\s+serve|vorrei\s+fare|voglio\s+fare|voglio\s+prenotare|posso\s+prenotare|quando\s+posso)\b/i;
321
322  /* ── password reset intent detection via Claude API ──────────
323   * Dedicated endpoint: /api/password-reset-intent
324   * Returns { isPasswordReset, email } or null on error. */
325  async function analyzePasswordResetIntent(baseUrl, text) {
326    var t = text.toLowerCase().trim();
327    if (t.length < 3) return null;
328    if (/^(ciao|salve|buongiorno|buonasera|grazie|ok|va bene|perfetto|arrivederci|bye)\s*[.!?]*$/i.test(t)) return null;
329    try {
330      var ctrl = new AbortController();
331      var tid = setTimeout(function(){ ctrl.abort(); }, 4000);
332      var r = await fetch(baseUrl + "/api/password-reset-intent", {
333        method: "POST",
334        headers: { "Content-Type": "application/json" },
335        body: JSON.stringify({ message: text }),
336        signal: ctrl.signal,
337      });
338      clearTimeout(tid);
339      if (!r.ok) return null;
340      return await r.json();
341    } catch (e) { return null; }
342  }
343
344  /* Regex fallback for password reset (used if Claude unavailable) */
345  var PWD_RESET_KEYWORDS =
346    /\b(password|credenzial|accedere|accesso|login|loggare|entrare|recuper[aio]re|dimenticat[ao]|reimpost|reset)\b/i;
347  var PWD_RESET_CONTEXT =
348    /\b(personal\s*caf|area\s+(personale|riservata)|portale|profilo)\b/i;
349
350  /** Extract an email address from free text, if present. */
351  function extractEmail(text) {
352    var m = (text || "").match(/[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/);
353    return m ? m[0] : "";
354  }
355
356  /* Client-side extraction of service + location from the user's message.
357   * Used both as fallback when Claude is unavailable AND to fill in gaps
358   * when Claude misses one of the two hints. */
359  /* SOLO servizi gestiti dal CAF. NASPI, Assegno Unico Universale (INPS),
360   * pensioni e altri servizi di patronato NON appaiono qui — sono
361   * intercettati a monte dalla blocklist NON_CAF_SERVICE_RE.
362   * Pattern più specifici PRIMA dei più generici (primo match vince). */
363  var CAF_SERVICE_PATTERNS = [
364    {k: /\b730\b/i, name: "730"},
365    {k: /\bise[ue]\b/i, name: "ISEE"},
366    {k: /\bdsu\b/i, name: "DSU"},
367    {k: /\bcud\b/i, name: "CUD"},
368    {k: /\bsuccessi[oò]n\w*/i, name: "Successione"},
369    {k: /\b(colf|badant\w*)\b/i, name: "Colf e Badanti"},
370    {k: /\b(imu|tasi|tari)\b/i, name: "IMU/TASI/TARI"},
371    {k: /\bmodello\s+redditi\b|\bunico\b/i, name: "Modello REDDITI"},
372    {k: /\bmodello\s+red\b|\bred\b/i, name: "Modello RED"},
373    {k: /\binvciv\b/i, name: "INVCIV"},
374    {k: /\baccas\/?ps\b|\bassegno\s+sociale\b/i, name: "ACCAS/PS"},
375    {k: /\bcedolare\s+secca\b|\blocazion[ei]\b/i, name: "Locazione"},
376    {k: /\bdelega\s+unica\b/i, name: "Delega Unica"},
377    {k: /\bvisur[ae]\s+catastal[ei]\b|\bvisura\b/i, name: "Visura Catastale"},
378    {k: /\bpartita\s+iva\b|\bsapi\b/i, name: "Partita IVA"},
379    {k: /\bdichiarazion\w*/i, name: "dichiarazione"},
380    {k: /\bbonus\b/i, name: "bonus"}
381  ];
382  /* Italian provinces (major ones). Multi-word entries use capital-preserving name. */
383  var ITALIAN_PROVINCES = [
384    "Agrigento","Alessandria","Ancona","Aosta","Arezzo","Ascoli Piceno","Asti","Avellino",
385    "Bari","Barletta","Belluno","Benevento","Bergamo","Biella","Bologna","Bolzano","Brescia","Brindisi",
386    "Cagliari","Caltanissetta","Campobasso","Caserta","Catania","Catanzaro","Chieti","Como","Cosenza","Cremona","Crotone","Cuneo",
387    "Enna","Fermo","Ferrara","Firenze","Foggia","Forlì","Forli","Frosinone",
388    "Genova","Gorizia","Grosseto",
389    "Imperia","Isernia",
390    "La Spezia","Latina","Lecce","Lecco","Livorno","Lodi","Lucca",
391    "Macerata","Mantova","Massa","Matera","Messina","Milano","Modena","Monza",
392    "Napoli","Novara","Nuoro","Olbia","Oristano",
393    "Padova","Palermo","Parma","Pavia","Perugia","Pesaro","Pescara","Piacenza","Pisa","Pistoia","Pordenone","Potenza","Prato",
394    "Ragusa","Ravenna","Reggio Calabria","Reggio Emilia","Rieti","Rimini","Roma","Rovigo",
395    "Salerno","Sassari","Savona","Siena","Siracusa","Sondrio",
396    "Taranto","Teramo","Terni","Torino","Trapani","Trento","Treviso","Trieste",
397    "Udine","Urbino",
398    "Varese","Venezia","Verbania","Vercelli","Verona","Vibo Valentia","Vicenza","Viterbo"
399  ];
400  function clientExtractIntent(text) {
401    var t = (text || "").trim();
402    if (!t) return { isBooking: false, service: "", location: "" };
403    var lower = t.toLowerCase();
404    /* extract service */
405    var service = "";
406    for (var i = 0; i < CAF_SERVICE_PATTERNS.length; i++) {
407      if (CAF_SERVICE_PATTERNS[i].k.test(t)) { service = CAF_SERVICE_PATTERNS[i].name; break; }
408    }
409    /* extract location — sorted by longest first so "Reggio Calabria" wins over "Reggio" alone */
410    var location = "";
411    var sorted = ITALIAN_PROVINCES.slice().sort(function(a,b){return b.length-a.length;});
412    for (var j = 0; j < sorted.length; j++) {
413      var pv = sorted[j];
414      var re = new RegExp("\\b" + pv.replace(/[.*+?^${}()|[\]\\]/g,"\\$&") + "\\b", "i");
415      if (re.test(t)) { location = pv; break; }
416    }
417    var isBooking = BOOKING_KEYWORDS.test(lower) || (!!service && /\b(a|in|di|presso)\s+/i.test(t));
418    return { isBooking: isBooking, service: service, location: location };
419  }
420
421  /** Build a human-readable list of available province names.
422   *  If there are too many (national coverage), return a short summary. */
423  function formatProvinceList(provList) {
424    if (!provList || !provList.length) return "";
425    var names = provList.map(function(p) { return p.descrizione; });
426    if (names.length === 1) return names[0];
427    if (names.length <= 10) return names.slice(0, -1).join(", ") + " e " + names[names.length - 1];
428    /* too many — just say "tutta Italia" or show count */
429    return "tutte le province italiane (" + names.length + " province)";
430  }
431
432  /** Try to match a location string against the available provinces.
433   *  Returns the matched province or null. */
434  function matchProvince(locText, provList) {
435    if (!locText || !provList || !provList.length) return null;
436    var locLow = locText.toLowerCase()
437      .replace(/\b(sono|abito|mi trovo|sto|vivo|lavoro|vengo)\s+(a|di|da|in)\b/g, "")
438      .replace(/\bprovincia\s+di\b/g, "")
439      .replace(/\b(centro|sud|nord|est|ovest|zona)\b/g, "")
440      .replace(/^provincia\s+di\s+/i, "")
441      .replace(/[^a-z\u00e0-\u00fa\s]/g, "").replace(/\s+/g, " ").trim();
442    if (!locLow) return null;
443    var best = null, bestScore = 0;
444    provList.forEach(function(pv) {
445      var pn = (pv.descrizione || "").toLowerCase();
446      if (locLow === pn) { if (100 > bestScore) { bestScore = 100; best = pv; } }
447      else if (locLow.indexOf(pn) !== -1 || pn.indexOf(locLow) !== -1) {
448        if (80 > bestScore) { bestScore = 80; best = pv; }
449      }
450      locLow.split(/\s+/).forEach(function(w) {
451        if (w.length > 2 && pn.indexOf(w) !== -1 && 60 > bestScore) { bestScore = 60; best = pv; }
452      });
453    });
454    return best;
455  }
456
457  /** Score how well a service matches a search term (0 = no match) */
458  function matchService(srv, hint) {
459    if (!hint) return 0;
460    var h = hint.toLowerCase();
461    var name = ((srv.desonline || "") + " " + (srv.descrizione || "")).toLowerCase();
462    if (name.indexOf(h) !== -1) return 100;
463    var words = h.split(/\s+/).filter(function(w){ return w.length > 1; });
464    if (words.length > 0) {
465      var found = words.filter(function(w){ return name.indexOf(w) !== -1; });
466      if (found.length === words.length) return 80;
467      if (found.length > 0) return 40 * found.length / words.length;
468    }
469    return 0;
470  }
471
472  /* ── voice booking helpers ───────────────────────────────────── */
473
474  /** Parse spoken Italian numbers/ordinals to integer (1-based) */
475  function parseVoiceNumber(text) {
476    var t = text.toLowerCase().trim();
477    var map = {
478      "uno":1,"un":1,"primo":1,"prima":1,"il primo":1,"la prima":1,
479      "due":2,"secondo":2,"seconda":2,"il secondo":2,"la seconda":2,
480      "tre":3,"terzo":3,"terza":3,"il terzo":3,"la terza":3,
481      "quattro":4,"quarto":4,"quarta":4,
482      "cinque":5,"quinto":5,"quinta":5,
483      "sei":6,"sesto":6,"sesta":6,
484      "sette":7,"settimo":7,"settima":7,
485      "otto":8,"ottavo":8,"ottava":8,
486      "nove":9,"nono":9,"nona":9,
487      "dieci":10,"decimo":10,"decima":10
488    };
489    /* exact match first */
490    if (map[t] !== undefined) return map[t];
491    /* digit match */
492    var m = t.match(/\b(\d+)\b/);
493    if (m) return parseInt(m[1], 10);
494    /* substring match — prefer LAST match (handles "non il primo, il secondo") */
495    var lastVal = null, lastPos = -1;
496    for (var key in map) {
497      var idx = t.lastIndexOf(key);
498      if (idx !== -1 && idx > lastPos) { lastPos = idx; lastVal = map[key]; }
499    }
500    return lastVal;
501  }
502
503  /** Try to match a spoken day/time to a slot.
504   *  Handles: day names (lunedì), relative days (domani, oggi, dopodomani),
505   *  time of day (mattina, pomeriggio), spoken hours (alle dieci, alle 15),
506   *  and specific dates (il 15, il quindici). */
507  function matchSlotByVoice(text, spazi) {
508    if (!spazi || !spazi.length) return null;
509    var t = text.toLowerCase();
510
511    /* helper: parse slot data string to Date */
512    function slotDate(s) { return new Date(+s.data.slice(0,4), +s.data.slice(4,6)-1, +s.data.slice(6,8)); }
513    function slotHour(s) { return +(s.ora_inizio.slice(0,2)); }
514
515    /* helper: filter candidates by time-of-day or spoken hour */
516    function filterByTime(candidates) {
517      if (!candidates.length) return null;
518      var wantMorning = /\b(mattin[ao]|mattino)\b/.test(t);
519      var wantAfternoon = /\b(pomeriggi[oa]|sera)\b/.test(t);
520      /* try to extract a spoken hour: "alle dieci", "alle 15", "ore 9" */
521      var hourMatch = t.match(/\b(?:alle|ore)\s+(\d{1,2})/);
522      if (!hourMatch) {
523        var hourWords = {"una":1,"due":2,"tre":3,"quattro":4,"cinque":5,"sei":6,"sette":7,
524          "otto":8,"nove":9,"dieci":10,"undici":11,"dodici":12,"tredici":13,"quattordici":14,
525          "quindici":15,"sedici":16,"diciassette":17,"diciotto":18,"diciannove":19,"venti":20};
526        var hm = t.match(/\b(?:alle|ore)\s+([a-zà-ú]+)\b/);
527        if (hm && hourWords[hm[1]] !== undefined) hourMatch = [null, String(hourWords[hm[1]])];
528      }
529      if (hourMatch) {
530        var wantH = parseInt(hourMatch[1], 10);
531        /* find closest matching hour */
532        var best = null, bestDiff = 999;
533        candidates.forEach(function(s) {
534          var diff = Math.abs(slotHour(s) - wantH);
535          if (diff < bestDiff) { bestDiff = diff; best = s; }
536        });
537        return bestDiff <= 1 ? best : candidates[0]; /* within 1 hour tolerance */
538      }
539      if (wantMorning) {
540        var morn = candidates.filter(function(s) { return slotHour(s) < 13; });
541        return morn.length ? morn[0] : candidates[0];
542      }
543      if (wantAfternoon) {
544        var aft = candidates.filter(function(s) { return slotHour(s) >= 13; });
545        return aft.length ? aft[0] : candidates[0];
546      }
547      return candidates[0];
548    }
549
550    /* === relative day matching: oggi, domani, dopodomani === */
551    var now = new Date();
552    var todayStr = String(now.getFullYear()) + String(now.getMonth()+1).padStart(2,"0") + String(now.getDate()).padStart(2,"0");
553    var tom = new Date(now); tom.setDate(tom.getDate()+1);
554    var tomStr = String(tom.getFullYear()) + String(tom.getMonth()+1).padStart(2,"0") + String(tom.getDate()).padStart(2,"0");
555    var dot = new Date(now); dot.setDate(dot.getDate()+2);
556    var dotStr = String(dot.getFullYear()) + String(dot.getMonth()+1).padStart(2,"0") + String(dot.getDate()).padStart(2,"0");
557
558    if (/\boggi\b/.test(t)) {
559      var todaySl = spazi.filter(function(s) { return s.data === todayStr; });
560      if (todaySl.length) return filterByTime(todaySl);
561    }
562    if (/\bdomani\b/.test(t)) {
563      var tomSl = spazi.filter(function(s) { return s.data === tomStr; });
564      if (tomSl.length) return filterByTime(tomSl);
565    }
566    if (/\bdopodomani\b/.test(t)) {
567      var dotSl = spazi.filter(function(s) { return s.data === dotStr; });
568      if (dotSl.length) return filterByTime(dotSl);
569    }
570
571    /* === "il primo disponibile" / "il prossimo" === */
572    if (/\b(primo\s+disponibile|prossimo|prima\s+disponibile|prossima)\b/.test(t)) {
573      return filterByTime(spazi);
574    }
575
576    /* === day-of-week matching === */
577    var dayMap = {"luned\u00ec":1,"lunedi":1,"marted\u00ec":2,"martedi":2,
578      "mercoled\u00ec":3,"mercoledi":3,"gioved\u00ec":4,"giovedi":4,
579      "venerd\u00ec":5,"venerdi":5,"sabato":6,"domenica":0};
580    for (var dayName in dayMap) {
581      if (t.indexOf(dayName) !== -1) {
582        var dayCandidates = spazi.filter(function(s) { return slotDate(s).getDay() === dayMap[dayName]; });
583        if (dayCandidates.length) return filterByTime(dayCandidates);
584      }
585    }
586
587    /* === specific date: "il 15", "il quindici", "15 aprile" === */
588    var dateNum = null;
589    var dm = t.match(/\bil\s+(\d{1,2})\b/);
590    if (dm) dateNum = parseInt(dm[1], 10);
591    if (!dateNum) {
592      var dateWords = {"primo":1,"due":2,"tre":3,"quattro":4,"cinque":5,"sei":6,"sette":7,"otto":8,"nove":9,
593        "dieci":10,"undici":11,"dodici":12,"tredici":13,"quattordici":14,"quindici":15,"sedici":16,
594        "diciassette":17,"diciotto":18,"diciannove":19,"venti":20,"ventuno":21,"ventidue":22,
595        "ventitre":23,"ventiquattro":24,"venticinque":25,"ventisei":26,"ventisette":27,"ventotto":28,
596        "ventinove":29,"trenta":30,"trentuno":31};
597      var dwm = t.match(/\bil\s+([a-zà-ú]+)\b/);
598      if (dwm && dateWords[dwm[1]] !== undefined) dateNum = dateWords[dwm[1]];
599    }
600    if (!dateNum) { var dm2 = t.match(/\b(\d{1,2})\s+(gennaio|febbraio|marzo|aprile|maggio|giugno|luglio|agosto|settembre|ottobre|novembre|dicembre)\b/); if (dm2) dateNum = parseInt(dm2[1], 10); }
601    if (dateNum) {
602      var dateCandidates = spazi.filter(function(s) { return slotDate(s).getDate() === dateNum; });
603      if (dateCandidates.length) return filterByTime(dateCandidates);
604    }
605
606    /* === just time without day: "alle dieci", "pomeriggio" === */
607    var hasTimeOnly = /\b(alle|ore|mattin|pomeriggi|sera)\b/.test(t) && !dateNum;
608    if (hasTimeOnly) {
609      return filterByTime(spazi);
610    }
611
612    return null;
613  }
614
615  /** Match spoken text to a sede */
616  function matchSedeByVoice(text, sedi) {
617    if (!text || !sedi || !sedi.length) return null;
618    var t = text.toLowerCase().trim();
619    var best = null, bestScore = 0;
620    sedi.forEach(function(s) {
621      var name = ((s.descrizione || "") + " " + (s.citta || "")).toLowerCase();
622      if (name.indexOf(t) !== -1 || t.indexOf((s.descrizione || "").toLowerCase()) !== -1) {
623        if (100 > bestScore) { bestScore = 100; best = s; }
624      } else {
625        var words = t.split(/\s+/).filter(function(w) { return w.length > 2; });
626        var found = words.filter(function(w) { return name.indexOf(w) !== -1; });
627        if (found.length > 0) {
628          var sc = 60 * found.length / words.length;
629          if (sc > bestScore) { bestScore = sc; best = s; }
630        }
631      }
632    });
633    return bestScore >= 40 ? best : null;
634  }
635
636  function capitalizeFirst(str) {
637    if (!str) return str;
638    return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase();
639  }
640
641  /** Convert spoken Italian number words to digits for phone numbers.
642   *  Handles: single digits, tens, hundreds, compound numbers (trecentotrentotto),
643   *  "doppio"/"triplo" prefixes, and "più" for + prefix. */
644  function voiceToDigits(text) {
645    var t = " " + text.toLowerCase().replace(/['']/g, " ").replace(/\s+/g, " ").trim() + " ";
646
647    /* "doppio X" → X X, "triplo X" → X X X */
648    t = t.replace(/\b(doppio|doppia)\s+(\S+)/gi, function(_,__,w){ return w + " " + w; });
649    t = t.replace(/\b(triplo|tripla)\s+(\S+)/gi, function(_,__,w){ return w + " " + w + " " + w; });
650
651    /* "più" → + (for international prefix) */
652    t = t.replace(/\bpi[uù]\b/gi, "+");
653
654    /* compound hundreds: trecentotrentotto → 338 etc. */
655    var units = {"un":1,"uno":1,"due":2,"tre":3,"quattro":4,"cinque":5,"sei":6,"sette":7,"otto":8,"nove":9};
656    var teens = {"dieci":10,"undici":11,"dodici":12,"tredici":13,"quattordici":14,"quindici":15,"sedici":16,"diciassette":17,"diciotto":18,"diciannove":19};
657    var tens = {"venti":20,"trenta":30,"quaranta":40,"cinquanta":50,"sessanta":60,"settanta":70,"ottanta":80,"novanta":90};
658    var hundreds = {"cento":100,"duecento":200,"trecento":300,"quattrocento":400,"cinquecento":500,"seicento":600,"settecento":700,"ottocento":800,"novecento":900};
659
660    /* expand compound words like "trecentotrentotto" */
661    function expandCompound(word) {
662      var w = word.toLowerCase(), n = 0, found = false;
663      /* try hundreds prefix */
664      for (var hk in hundreds) { if (w.indexOf(hk) === 0) { n += hundreds[hk]; w = w.slice(hk.length); found = true; break; } }
665      if (!found && w === "cento") { return "100"; }
666      /* try teens */
667      for (var tk in teens) { if (w === tk || w.indexOf(tk) === 0) { n += teens[tk]; w = w.slice(tk.length); return String(n); } }
668      /* try tens */
669      for (var dk in tens) { if (w.indexOf(dk) === 0) { n += tens[dk]; w = w.slice(dk.length); break; } }
670      /* try units */
671      for (var uk in units) { if (w === uk || (w.length > 0 && w.indexOf(uk) === 0 && w.length <= uk.length + 1)) { n += units[uk]; w = ""; break; } }
672      if (n > 0 && w.length === 0) return String(n);
673      if (found && n > 0) return String(n); /* partial compound */
674      return null; /* not a compound number */
675    }
676
677    /* first pass: try to expand compound number words */
678    t = t.replace(/[a-zà-ú]+/gi, function(word) {
679      var exp = expandCompound(word);
680      return exp !== null ? exp : word;
681    });
682
683    /* second pass: simple digit word replacements for any remaining */
684    var simpleMap = {"zero":"0","uno":"1","un":"1","due":"2","tre":"3","quattro":"4",
685      "cinque":"5","sei":"6","sette":"7","otto":"8","nove":"9","dieci":"10"};
686    for (var sw in simpleMap) {
687      t = t.replace(new RegExp("\\b" + sw + "\\b", "gi"), simpleMap[sw]);
688    }
689
690    return t.replace(/\s+/g, "").replace(/[^0-9+]/g, "");
691  }
692
693  /** Clean spoken email: "chiocciola" → @, "punto" → ., "trattino" → -, etc. */
694  function voiceToEmail(text) {
695    /* Apply word-boundary patterns BEFORE stripping spaces */
696    return text.toLowerCase()
697      .replace(/\bchiocciola\b/g, "@").replace(/\bat\b/g, "@")
698      .replace(/\bpunto\b/g, ".").replace(/\bdot\b/g, ".")
699      .replace(/\btrattino\s*basso\b/g, "_").replace(/\bunderscore\b/g, "_")
700      .replace(/\btrattino\b/g, "-").replace(/\bmeno\b/g, "-")
701      .replace(/\bpi[uù]\b/g, "+")
702      .replace(/\s+/g, "");
703  }
704
705  /* ── planner date/time formatting ──────────────────────────── */
706  var B_DAY = ["Domenica","Luned\u00ec","Marted\u00ec","Mercoled\u00ec","Gioved\u00ec","Venerd\u00ec","Sabato"];
707  var B_MON = ["Gennaio","Febbraio","Marzo","Aprile","Maggio","Giugno","Luglio","Agosto","Settembre","Ottobre","Novembre","Dicembre"];
708  /* WCAG 3.1.2 (#C68): nomi giorno/mese localizzati via BK_I18N (cal_days_full/
709   * cal_months); B_DAY/B_MON restano come fallback se le chiavi mancassero. */
710  function fmtDate(s){if(!s||s.length<8)return s||"";var y=s.slice(0,4),m=s.slice(4,6),d=s.slice(6,8),dt=new Date(+y,+m-1,+d);if(isNaN(dt))return s;var dd=bkT("cal_days_full").split("|"),mm=bkT("cal_months").split("|");return (dd[dt.getDay()]||B_DAY[dt.getDay()])+" "+d+" "+(mm[+m-1]||B_MON[+m-1])+" "+y;}
711  function fmtTime(s){return(!s||s.length<4)?s||"":s.slice(0,2)+":"+s.slice(2,4);}
712
713  /* ── codice fiscale (CF) — validazione algoritmica con checksum ────────
714   * Algoritmo ufficiale Agenzia delle Entrate (DPR 605/1973 e s.m.i.).
715   * Restituisce { ok: bool, reason: string } per dare messaggi precisi. */
716  var CF_CHK_ODD={"0":1,"1":0,"2":5,"3":7,"4":9,"5":13,"6":15,"7":17,"8":19,"9":21,
717    "A":1,"B":0,"C":5,"D":7,"E":9,"F":13,"G":15,"H":17,"I":19,"J":21,"K":2,"L":4,"M":18,"N":20,
718    "O":11,"P":3,"Q":6,"R":8,"S":12,"T":14,"U":16,"V":10,"W":22,"X":25,"Y":24,"Z":23};
719  var CF_CHK_EVEN={"0":0,"1":1,"2":2,"3":3,"4":4,"5":5,"6":6,"7":7,"8":8,"9":9,
720    "A":0,"B":1,"C":2,"D":3,"E":4,"F":5,"G":6,"H":7,"I":8,"J":9,"K":10,"L":11,"M":12,"N":13,
721    "O":14,"P":15,"Q":16,"R":17,"S":18,"T":19,"U":20,"V":21,"W":22,"X":23,"Y":24,"Z":25};
722  var CF_CHK_LET="ABCDEFGHIJKLMNOPQRSTUVWXYZ";
723  function validateCodiceFiscale(raw){
724    if(!raw)return{ok:false,reason:bkT("err_cf_required")};
725    var cf=String(raw).toUpperCase().replace(/\s+/g,"");
726    if(cf.length<16)return{ok:false,reason:bkT("err_cf_short",(16-cf.length))};
727    if(cf.length>16)return{ok:false,reason:bkT("err_cf_long",(cf.length-16))};
728    if(!/^[A-Z0-9]{16}$/.test(cf))return{ok:false,reason:bkT("err_cf_chars")};
729    /* layout standard CF persona fisica: 6 lettere + 2 cifre + 1 lettera + 2 cifre + 1 lettera + 3 alfanum + 1 lettera */
730    if(!/^[A-Z]{6}[0-9LMNPQRSTUV]{2}[A-Z][0-9LMNPQRSTUV]{2}[A-Z][0-9LMNPQRSTUV]{3}[A-Z]$/.test(cf)){
731      return{ok:false,reason:bkT("err_cf_format")};
732    }
733    var sum=0;
734    for(var i=0;i<15;i++){var ch=cf.charAt(i);var v=(i%2===0)?CF_CHK_ODD[ch]:CF_CHK_EVEN[ch];if(v===undefined)return{ok:false,reason:bkT("err_cf_charpos",(i+1))};sum+=v;}
735    var expected=CF_CHK_LET.charAt(sum%26);
736    if(cf.charAt(15)!==expected)return{ok:false,reason:bkT("err_cf_checksum",expected,cf.charAt(15))};
737    return{ok:true,reason:""};
738  }
739
740  /* ── #42 GDPR consent gating multi-tier + #43 auto-clear + #45 obfuscation ──
741   *
742   * 3-TIER CONSENT MODEL (compliant con linee guida EDPB 03/2022):
743   *   - "necessary": funzionamento del widget (cache regioni/sedi, lingua,
744   *      lookup geo provincia approssimato dal browser). NO opt-in
745   *      richiesto perché senza non funziona.
746   *   - "functional": memorizzazione dati cliente (CF, nome, email, tel),
747   *      history prenotazioni, resume state. Opt-in obbligatorio.
748   *   - "analytics": sessionId persistente, UTM/gclid/fbclid, _bkLogEvent
749   *      verso /api/planner/log-event. Opt-in obbligatorio SEPARATO (non si
750   *      può vincolare il funzionale all'accettazione degli analytics).
751   *
752   * VERSIONING: se cambia il modello del consenso, invalidiamo i record esistenti
753   * incrementando BK_CONSENT_VERSION. */
754  var BK_CONSENT_VERSION=2;
755  var BK_CLIENT_KEY="caf-bk-client-v2"+CAF_NS;
756  var BK_CLIENT_FAMILY_KEY="caf-bk-client-family-v2"+CAF_NS;
757  var BK_CONSENT_KEY="caf-bk-consent-v2"+CAF_NS;
758  var BK_CLIENT_TTL_MS=30*24*60*60*1000; /* 30 giorni inattività */
759  /* legacy keys da migrare/cancellare al primo passaggio */
760  var BK_LEGACY_KEYS=["caf-bk-client-v1","caf-bk-client-family-v1","caf-bk-consent-v1"];
761  /* Chiavi scritte dalle versioni deployate PRIMA del namespacing per-tenant
762   * (CAF_NS): contengono PII/cronologia fuori dal gating attuale (GDPR
763   * artt. 5(1)(e), 17). Rimosse una tantum al bootstrap e incluse nel wipe. */
764  var BK_PRE_NS_KEYS=["caf-bk-client-v2","caf-bk-client-family-v2","caf-bk-consent-v2",
765    "caf-bk-history-v1","caf-bk-resume-v1","caf-bk-freq-v1","caf-bk-sid","caf-bk-lang",
766    "caf-chat-inline-history","caf-chat-inline-conv-id","caf-chat-inline-email","caf-chat-consent-v1"];
767  (function bkPurgePreNamespaceKeys(){
768    try{
769      /* con CAF_NS vuoto le chiavi "nude" SONO quelle attive: non toccarle */
770      if(!CAF_NS)return;
771      BK_PRE_NS_KEYS.forEach(function(k){try{localStorage.removeItem(k);}catch(_e){}});
772      for(var _i=localStorage.length-1;_i>=0;_i--){
773        var _k=localStorage.key(_i)||"";
774        if((/^caf-bk-cache-v\d+:/.test(_k)||/^caf-bk-slotcache-v\d+:/.test(_k))&&_k.indexOf(CAF_NS)===-1){
775          try{localStorage.removeItem(_k);}catch(_e){}
776        }
777      }
778    }catch(_e){}
779  })();
780
781  function bkConsentLoad(){
782    try{
783      var r=localStorage.getItem(BK_CONSENT_KEY);if(!r)return null;
784      var c=JSON.parse(r);if(!c||typeof c!=="object")return null;
785      /* invalidate se versione obsoleta: l'utente deve dare nuovo consenso */
786      if((+c.version||0)!==BK_CONSENT_VERSION)return null;
787      return c;
788    }catch(_e){return null;}
789  }
790  function bkConsentSave(consent){
791    try{
792      var payload={
793        version:BK_CONSENT_VERSION,
794        functional:!!(consent&&consent.functional),
795        analytics:!!(consent&&consent.analytics),
796        ts:Date.now(),
797        /* mini audit trail: page where consent was given */
798        page:(typeof location!=="undefined"&&location.pathname)||"",
799        ua:(typeof navigator!=="undefined"&&navigator.userAgent.
799slice(0,200))||""
800      };
801      localStorage.setItem(BK_CONSENT_KEY,JSON.stringify(payload));
802      return payload;
803    }catch(_e){return null;}
804  }
805  /* bkConsentHas(scope) — scope = "functional" | "analytics" | (undefined → any) */
806  function bkConsentHas(scope){
807    var c=bkConsentLoad();if(!c)return false;
808    if(scope==="functional")return!!c.functional;
809    if(scope==="analytics")return!!c.analytics;
810    /* legacy compat: chiamate senza scope = check se almeno qualcosa è acconsentito */
811    return!!(c.functional||c.analytics);
812  }
813  /* True se l'utente ha mai espresso una scelta esplicita (anche "rifiuta tutto").
814   * Usato per decidere se mostrare il banner all'apertura widget. */
815  function bkConsentExpressed(){return bkConsentLoad()!=null;}
816  function bkConsentDeny(){
817    bkConsentSave({functional:false,analytics:false});
818    /* Wipe immediato di TUTTI i dati personali e di tracking */
819    try{
820      [BK_CLIENT_KEY,BK_CLIENT_FAMILY_KEY,BK_HISTORY_KEY,BK_RESUME_KEY,BK_FREQ_KEY,BK_SID_KEY]
821        .concat(BK_LEGACY_KEYS).forEach(function(k){localStorage.removeItem(k);});
822    }catch(_e){}
823  }
824  /* Helper: revoca un singolo scope (functional o analytics) e cancella i dati
825   * collegati. Utile per i toggle nel pannello "preferenze". */
826  function bkConsentRevoke(scope){
827    var c=bkConsentLoad()||{functional:false,analytics:false};
828    var nc={functional:c.functional,analytics:c.analytics};
829    if(scope==="functional"){
830      nc.functional=false;
831      try{[BK_CLIENT_KEY,BK_CLIENT_FAMILY_KEY,BK_HISTORY_KEY,BK_RESUME_KEY,BK_FREQ_KEY].forEach(function(k){localStorage.removeItem(k);});}catch(_e){}
832    }
833    if(scope==="analytics"){
834      nc.analytics=false;
835      try{localStorage.removeItem(BK_SID_KEY);}catch(_e){}
836    }
837    bkConsentSave(nc);
838  }
839
840  /* Light XOR obfuscation con key derivata. NON è crittografia: è camouflage
841   * per evitare di vedere CF/email in chiaro nei DevTools. */
842  function _bkObfKey(){
843    var seed=(navigator.userAgent||"")+(navigator.language||"")+location.host;
844    var h=0;for(var i=0;i<seed.length;i++)h=((h<<5)-h+seed.charCodeAt(i))|0;
845    return Math.abs(h%251)+5;
846  }
847  function _bkXor(s,k){var out="";for(var i=0;i<s.length;i++)out+=String.fromCharCode(s.charCodeAt(i)^k);return out;}
848  function _bkObfuscate(obj){try{var json=JSON.stringify(obj);var k=_bkObfKey();return"o2:"+k+":"+btoa(unescape(encodeURIComponent(_bkXor(json,k))));}catch(_e){return JSON.stringify(obj);}}
849  function _bkDeobfuscate(raw){
850    if(!raw)return null;
851    try{
852      if(raw.indexOf("o2:")===0){
853        var parts=raw.split(":");var k=+parts[1];var b64=parts.slice(2).join(":");
854        var dec=decodeURIComponent(escape(atob(b64)));return JSON.parse(_bkXor(dec,k));
855      }
856      /* fallback compat con dati legacy in chiaro */
857      return JSON.parse(raw);
858    }catch(_e){return null;}
859  }
860
861  /* Migrazione one-shot da legacy keys → nuovo formato cifrato.
862   * Si avvia all'init e fa nulla se la nuova key esiste già. */
863  function bkMigrateLegacyClientKeys(){
864    try{
865      var legacyMap=[["caf-bk-client-v1",BK_CLIENT_KEY],["caf-bk-client-family-v1",BK_CLIENT_FAMILY_KEY]];
866      /* Senza consenso funzionale non ri-persistiamo PII legacy: la rimuoviamo
867       * (GDPR artt. 6, 17) invece di migrarla nel nuovo formato cifrato. */
868      var hasFn=bkConsentHas("functional");
869      legacyMap.forEach(function(pair){
870        var oldK=pair[0],newK=pair[1];
871        var oldRaw=localStorage.getItem(oldK);if(!oldRaw)return;
872        if(!hasFn){try{localStorage.removeItem(oldK);}catch(_e){}return;}
873        if(localStorage.getItem(newK)){try{localStorage.removeItem(oldK);}catch(_e){}return;} /* già migrato */
874        try{var obj=JSON.parse(oldRaw);if(obj&&typeof obj==="object"){localStorage.setItem(newK,_bkObfuscate(Object.assign({},obj,{ts:Date.now()})));}}catch(_e){}
875        localStorage.removeItem(oldK);
876      });
877    }catch(_e){}
878  }
879
880  /* #43 — Auto-clear: se il payload ha ts > 30gg di inattività, rimuove. */
881  function bkAutoClearStaleClient(){
882    try{
883      [BK_CLIENT_KEY,BK_CLIENT_FAMILY_KEY].forEach(function(k){
884        var raw=localStorage.getItem(k);if(!raw)return;
885        var obj=_bkDeobfuscate(raw);if(!obj)return;
886        var ts=+obj.ts||0;
887        if(!ts||(Date.now()-ts)>BK_CLIENT_TTL_MS){
888          localStorage.removeItem(k);
889        }
890      });
891    }catch(_e){}
892  }
893
894  function clientStorageKey(forWhom){return forWhom==="family"?BK_CLIENT_FAMILY_KEY:BK_CLIENT_KEY;}
895  function loadStoredClient(forWhom){
896    try{
897      /* #PII — niente lettura di dati personali senza consenso funzionale
898       * (prima solo la scrittura era gated → su device condiviso i dati di una
899       * persona precedente potevano essere letti anche senza consenso). */
900      if(!bkConsentHas("functional"))return null;
901      var raw=localStorage.getItem(clientStorageKey(forWhom));if(!raw)return null;
902      var p=_bkDeobfuscate(raw);if(!p||typeof p!=="object")return null;
903      /* Auto-clear inline: se stale, rimuovi e ritorna null */
904      if(p.ts&&(Date.now()-p.ts)>BK_CLIENT_TTL_MS){localStorage.removeItem(clientStorageKey(forWhom));return null;}
905      return{nome:p.nome||"",cognome:p.cognome||"",codfis:p.codfis||"",email:p.email||"",cellulare:p.cellulare||""};
906    }catch(e){return null;}
907  }
908  function saveStoredClient(c,forWhom){
909    try{
910      if(!c)return;
911      /* #42 — gating: niente persist di PII senza consenso FUNZIONALE (non basta
912       * l'analytics: i dati personali si salvano solo col consenso funzionale). */
913      if(!bkConsentHas("functional"))return;
914      var payload={nome:c.nome||"",cognome:c.cognome||"",codfis:c.codfis||"",email:c.email||"",cellulare:c.
914cellulare||"",ts:Date.now()};
915      localStorage.setItem(clientStorageKey(forWhom),_bkObfuscate(payload));
916    }catch(e){}
917  }
918
919  /* ── ICS calendar file generator ───────────────────────────────────── */
920  function pad2(n){n=+n;return n<10?"0"+n:""+n;}
921  function buildIcsForBooking(booking){
922    /* booking: {bookingId, sede:{descrizione,indirizzo,citta}, servizio:{descrizione}, slot:{data:"YYYYMMDD", ora_inizio:"HHMM", ora_fine:"HHMM"}} */
923    var slot=booking.slot||{};var data=slot.data||"";var oi=slot.ora_inizio||"0900";var of=slot.ora_fine||oi;
924    if(data.length<8)return null;
925    /* Costruisci datetime UTC approssimato: trattiamo l'orario come Europe/Rome.
926     * Per semplicità ICS uso TZID=Europe/Rome senza VTIMEZONE — la maggior parte
927     * dei client (Google/Apple/Outlook) lo gestisce correttamente. */
928    var dtStart=data+"T"+oi.padStart(4,"0").slice(0,4)+"00";
929    var dtEnd=data+"T"+of.padStart(4,"0").slice(0,4)+"00";
930    var dtStartFmt=dtStart.slice(0,4)+dtStart.slice(4,6)+dtStart.slice(6,8)+"T"+dtStart.slice(9,11)+dtStart.slice(11,13)+"00";
931    var dtEndFmt=dtEnd.slice(0,4)+dtEnd.slice(4,6)+dtEnd.slice(6,8)+"T"+dtEnd.slice(9,11)+dtEnd.slice(11,13)+"00";
932    var sede=booking.sede||{};var srv=booking.servizio||{};
933    var loc=[sede.descrizione,sede.indirizzo,sede.citta].filter(Boolean).join(", ");
934    var icsBrand=bkNeutralBrand("CAF CISL");
935    var summary=icsBrand+" — "+(srv.descrizione||"Appuntamento");
936    var desc="Prenotazione "+(booking.bookingId||"")+" — "+(srv.descrizione||"")+" presso "+loc;
937    var nowDt=new Date();var dtStamp=nowDt.getUTCFullYear()+pad2(nowDt.getUTCMonth()+1)+pad2(nowDt.getUTCDate())+"T"+pad2(nowDt.getUTCHours())+pad2(nowDt.getUTCMinutes())+pad2(nowDt.getUTCSeconds())+"Z";
938    var ics=[
939      "BEGIN:VCALENDAR","VERSION:2.0","PRODID:-//"+icsBrand.replace(/\/\//g,"/")+"//Booking Widget//IT","CALSCALE:GREGORIAN","METHOD:PUBLISH",
940      "BEGIN:VEVENT",
941      "UID:caf-cisl-"+(booking.bookingId||(""+Date.now()))+"@cafcisl.it",
942      "DTSTAMP:"+dtStamp,
943      "DTSTART;TZID=Europe/Rome:"+dtStartFmt,
944      "DTEND;TZID=Europe/Rome:"+dtEndFmt,
945      "SUMMARY:"+summary.replace(/[\r\n]/g," "),
946      "DESCRIPTION:"+desc.replace(/[\r\n]/g," "),
947      "LOCATION:"+loc.replace(/[\r\n]/g," "),
948      "END:VEVENT","END:VCALENDAR"
949    ].join("\r\n");
950    return ics;
951  }
952  function downloadIcs(ics,filename){
953    try{
954      var blob=new Blob([ics],{type:"text/calendar;charset=utf-8"});
955      var url=URL.createObjectURL(blob);
956      var a=document.createElement("a");a.href=url;a.download=filename||"appuntamento.ics";document.body.appendChild(a);a.click();
957      setTimeout(function(){URL.revokeObjectURL(url);if(a.parentNode)a.parentNode.removeChild(a);},500);
958    }catch(e){}
959  }
960  function whatsappShareUrl(text){
961    return "https://wa.me/?text="+encodeURIComponent(text);
962  }
963
964  /* ── Email typo detection (Bayesian distance su domini comuni) ───── */
965  var EMAIL_DOMAINS=["gmail.com","hotmail.com","hotmail.it","yahoo.it","yahoo.com","libero.it","outlook.com","outlook.it","icloud.com","tin.it","alice.it","virgilio.it","tiscali.it","fastwebnet.it","pec.it","email.it","aruba.it","gmail.it"];
966  function levenshtein(a,b){a=a||"";b=b||"";if(a===b)return 0;if(!a.length)return b.length;if(!b.length)return a.length;var m=[];for(var i=0;i<=b.length;i++)m[i]=[i];for(var j=0;j<=a.length;j++)m[0][j]=j;for(i=1;i<=b.length;i++){for(j=1;j<=a.length;j++){if(b.charAt(i-1)===a.charAt(j-1))m[i][j]=m[i-1][j-1];else m[i][j]=Math.min(m[i-1][j-1]+1,Math.min(m[i][j-1]+1,m[i-1][j]+1));}}return m[b.length][a.length];}
967  function suggestEmailDomain(email){
968    if(!email||email.indexOf("@")<0)return null;
969    var parts=email.toLowerCase().trim().split("@");if(parts.length!==2||!parts[1])return null;
970    var dom=parts[1];if(EMAIL_DOMAINS.indexOf(dom)>=0)return null; /* già corretto */
971    var best=null,bestDist=99;
972    for(var k=0;k<EMAIL_DOMAINS.length;k++){var d=levenshtein(dom,EMAIL_DOMAINS[k]);if(d<bestDist){bestDist=d;best=EMAIL_DOMAINS[k];}}
973    if(best&&bestDist>0&&bestDist<=2&&dom.length>=4)return parts[0]+"@"+best;
974    return null;
975  }
976
977  /* ── Phone format (italiano: 333 123 4567 / 02 1234 5678) ─────────── */
978  function formatPhoneIT(raw){
979    var d=String(raw||"").replace(/\D/g,"");
980    if(!d)return "";
981    /* mobile prefisso 3xx (3 + 3 + resto) */
982    if(d.charAt(0)==="3"&&d.length>=10){return d.slice(0,3)+" "+d.slice(3,6)+" "+d.slice(6,10);}
983    /* fisso 0X (2-4 cifre prefisso + 6-8 cifre numero) — formato semplice 02 + spazi ogni 4 */
984    if(d.charAt(0)==="0"&&d.length>=6){
985      var pl=d.length<=10?2:3;return d.slice(0,pl)+" "+d.slice(pl,pl+4)+" "+d.slice(pl+4);
986    }
987    /* fallback: gruppi di 3 */
988    return d.replace(/(\d{3})(?=\d)/g,"$1 ").trim();
989  }
990
991  /* ── Time-aware greeting ───────────────────────────────────────────── */
992  function timeAwareGreeting(){
993    var h=new Date().getHours();
994    if(h<5)return"Buonanotte";if(h<12)return"Buongiorno";if(h<18)return"Buon pomeriggio";return"Buonasera";
995  }
996
997  /* ── Mailto reminder generator ────────────────────────────────────── */
998  function mailtoReminder(booking){
999    var sp=booking.slot||{};var sd=booking.sede||{};var sv=booking.servizio||{};var to=booking.email||"";
1000    var mailBrand=bkNeutralBrand("CAF CISL");
1001    var subj="Promemoria appuntamento "+mailBrand+" — "+(fmtDate(sp.data||"")||"appuntamento");
1002    var body=[
1003      "Promemoria del tuo appuntamento presso "+mailBrand+":",
1004      "",
1005      "Data: "+(fmtDate(sp.data||"")+" alle "+fmtTime(sp.ora_inizio||"")),
1006      "Servizio: "+(sv.descrizione||""),
1007      "Sede: "+(sd.descrizione||""),
1008      "Indirizzo: "+(sd.indirizzo?sd.indirizzo+(sd.citta?", "+sd.citta:""):""),
1009      sd.telefono?"Telefono sede: "+sd.telefono:"",
1010      booking.bookingId?"ID prenotazione: "+booking.bookingId:"",
1011      "",
1012      "Ricordati di portare i documenti necessari.",
1013    ].filter(Boolean).join("\n");
1014    return "mailto:"+encodeURIComponent(to)+"?subject="+encodeURIComponent(subj)+"&body="+encodeURIComponent(body);
1015  }
1016
1017  /* ── UTM tracking ──────────────────────────────────────────────────────
1018   * GDPR/ePrivacy: UTM/gclid/fbclid sono identificatori marketing — leggerli
1019   * e inviarli al backend richiede consenso "analytics". Senza consenso,
1020   * ritorniamo oggetto vuoto. */
1021  function readUtmParams(){
1022    if(!bkConsentHas("analytics"))return{};
1023    try{
1024      if(typeof location==="undefined"||!location.search)return{};
1025      var u=new URLSearchParams(location.search);
1026      var out={};["utm_source","utm_medium","utm_campaign","utm_term","utm_content","gclid","fbclid"].forEach(function(k){var v=u.get(k);if(v)out[k]=v;});
1027      return out;
1028    }catch(_e){return{};}
1029  }
1030
1031  /* ── localStorage history per pattern detection (orario preferito) ──
1032   * GDPR: gated da consenso "functional". Senza consenso, no save (no-op). */
1033  var BK_HISTORY_KEY="caf-bk-history-v1"+CAF_NS;var BK_HISTORY_MAX=10;
1034  function pushBookingHistory(booking){
1035    if(!bkConsentHas("functional"))return; /* GDPR gating */
1036    try{
1037      var raw=localStorage.getItem(BK_HISTORY_KEY);var arr=[];if(raw){try{arr=JSON.parse(raw)||[];}catch(_e){arr=[];}}
1038      arr.unshift({when:Date.now(),slot:booking.slot,servizio:booking.servizio,sede:booking.sede,bookingId:booking.bookingId,provincia:booking.provincia||null,regione:booking.regione||null});
1039      if(arr.length>BK_HISTORY_MAX)arr=arr.slice(0,BK_HISTORY_MAX);
1040      localStorage.setItem(BK_HISTORY_KEY,JSON.stringify(arr));
1041    }catch(_e){}
1042  }
1043  function loadBookingHistory(){
1044    try{var raw=localStorage.getItem(BK_HISTORY_KEY);return raw?(JSON.parse(raw)||[]):[];}catch(_e){return[];}
1045  }
1046  /** Returns "morning" | "afternoon" | null based on past selections. */
1047  function preferredTimeOfDay(){
1048    var h=loadBookingHistory();if(h.length<2)return null;
1049    var morning=0,afternoon=0;
1050    h.forEach(function(b){var ora=(b&&b.slot&&b.slot.ora_inizio)||"";if(!ora)return;var hh=+ora.slice(0,2);if(isNaN(hh))return;if(hh<13)morning++;else afternoon++;});
1051    if(morning>=afternoon*2&&morning>=2)return"morning";
1052    if(afternoon>=morning*2&&afternoon>=2)return"afternoon";
1053    return null;
1054  }
1055
1056  /* ── Resume/abandonment state ─────────────────────────────────────────
1057   * GDPR: contiene client.codfis/email/cellulare → gated da "functional". */
1058  var BK_RESUME_KEY="caf-bk-resume-v1"+CAF_NS;var BK_RESUME_TTL_MS=24*60*60*1000;
1059  function saveResumeState(state){
1060    if(!bkConsentHas("functional"))return; /* GDPR gating */
1061    try{var payload={ts:Date.now(),state:state};localStorage.setItem(BK_RESUME_KEY,JSON.stringify(payload));}catch(_e){}
1062  }
1063  function loadResumeState(){
1064    try{var raw=localStorage.getItem(BK_RESUME_KEY);if(!raw)return null;var p=JSON.parse(raw);if(!p||!p.state)return null;if(Date.now()-(p.ts||0)>BK_RESUME_TTL_MS){localStorage.removeItem(BK_RESUME_KEY);return null;}return p.state;}catch(_e){return null;}
1065  }
1066  function clearResumeState(){try{localStorage.removeItem(BK_RESUME_KEY);}catch(_e){}}
1067
1068  /* ── Static catalog cache (regioni/province/servizi) — TTL 24h ─────
1069   * Riduce a 0ms il primo render dopo la prima visita evitando spinner.
1070   * IMPORTANTE: bump v2 per invalidare cache vecchie con "Toscana Regionale"
1071   * placeholder (ora il backend (PHP seeder) restituisce le 10 province reali). */
1072  var BK_CACHE_PREFIX="caf-bk-cache-v3"+CAF_NS+":";var BK_CACHE_TTL_MS=24*60*60*1000;
1073  function bkCacheGet(key){
1074    try{
1075      var raw=localStorage.getItem(BK_CACHE_PREFIX+key);if(!raw)return null;
1076      var p=JSON.parse(raw);if(!p||(Date.now()-(p.ts||0))>BK_CACHE_TTL_MS){
1077        localStorage.removeItem(BK_CACHE_PREFIX+key);return null;
1078      }
1079      return p.data;
1080    }catch(_e){return null;}
1081  }
1082  function bkCacheSet(key,data){
1083    try{localStorage.setItem(BK_CACHE_PREFIX+key,JSON.stringify({ts:Date.now(),data:data}));}catch(_e){}
1084  }
1085
1086  /* ── Haptic feedback (mobile only, no-op desktop) ─────────────── */
1087  function bkHaptic(ms){
1088    try{if(navigator&&navigator.vibrate)navigator.vibrate(ms||8);}catch(_e){}
1089  }
1090
1091  /* ── View Transitions API wrapper (Chrome/Safari TP) ──────────── */
1092  function bkViewTransition(fn){
1093    try{
1094      if(typeof document!=="undefined"&&document.startViewTransition){
1095        return document.startViewTransition(fn);
1096      }
1097    }catch(_e){}
1098    try{fn();}catch(_e){}
1099    return null;
1100  }
1101
1102  /* ── Normalize string: lower + remove accents + collapse spaces. ── */
1103  function bkNormalizeStr(s){
1104    if(s==null)return"";
1105    s=String(s).toLowerCase();
1106    try{s=s.normalize("NFD").replace(/[\u0300-\u036f]/g,"");}catch(_e){}
1107    return s.replace(/\s+/g," ").trim();
1108  }
1109
1110  /* ── Levenshtein distance (capped at maxDist for perf). ── */
1111  function bkLevenshtein(a,b,maxDist){
1112    if(a===b)return 0;
1113    var la=a.length,lb=b.length;
1114    if(!la)return lb;if(!lb)return la;
1115    if(maxDist!=null&&Math.abs(la-lb)>
1115maxDist)return maxDist+1;
1116    var prev=new Array(lb+1),curr=new Array(lb+1);
1117    for(var j=0;j<=lb;j++)prev[j]=j;
1118    for(var i=1;i<=la;i++){
1119      curr[0]=i;var rowMin=i;
1120      for(var k=1;k<=lb;k++){
1121        var cost=a.charCodeAt(i-1)===b.charCodeAt(k-1)?0:1;
1122        curr[k]=Math.min(curr[k-1]+1,prev[k]+1,prev[k-1]+cost);
1123        if(curr[k]<rowMin)rowMin=curr[k];
1124      }
1125      if(maxDist!=null&&rowMin>maxDist)return maxDist+1;
1126      var tmp=prev;prev=curr;curr=tmp;
1127    }
1128    return prev[lb];
1129  }
1130
1131  /* ── Sinonimi servizi: chiavi sono i match utente, valori sono token
1132   * canonici cercati nei nomi servizio. Tutto lowercased + senza accenti. ── */
1133  var BK_SINONIMI={
1134    "dichiarazione redditi":["730","unico","modello redditi"],
1135    "dichiarazione dei redditi":["730","unico","modello redditi"],
1136    "dichiarazione":["730","unico","successione"],
1137    "redditi":["730","unico"],
1138    "tasse":["730","f24","imu","tari"],
1139    "tasse casa":["imu","tari","locazioni"],
1140    "casa":["imu","tari","locazioni","mutuo","successione"],
1141    "affitto":["locazioni","rli","cedolare"],
1142    "affitti":["locazioni","rli","cedolare"],
1143    "locazione":["locazioni","rli","cedolare"],
1144    "contratto affitto":["locazioni","redazione","rli"],
1145    "isee":["isee","dsu"],
1146    "reddito di cittadinanza":["isee","reddito"],
1147    "assegno unico":["isee","assegno"],
1148    "bonus":["isee","bonus","assegno"],
1149    "successione":["successione","eredita"],
1150    "eredità":["successione"],
1151    "eredita":["successione"],
1152    "permessi soggiorno":["permesso","stranieri","soggiorno"],
1153    "cittadinanza":["cittadinanza","stranieri"],
1154    "trasporti":["atm","abbonamento"],
1155    "abbonamento":["atm","abbonamento","trasporti"],
1156    "patronato":["pensione","inps","disoccupazione","naspi"],
1157    "pensione":["pensione","inps"],
1158    "disoccupazione":["naspi","disoccupazione"],
1159    "naspi":["naspi","disoccupazione"],
1160    "colf":["colf","badanti","domestici"],
1161    "badanti":["badanti","colf","domestici"],
1162    "lavoratori domestici":["colf","badanti","domestici"],
1163    "f24":["f24","imu","tari"],
1164    "imu":["imu","f24"],
1165    "tari":["tari","f24"]
1166  };
1167
1168  /* ── Documenti richiesti per servizio (mapping client-side) ────────
1169   * La chiave matcha case-insensitive su sotto-stringa nel nome servizio. */
1170  var BK_DOC_REQUISITI=[
1171    {match:["730","unico","modello redditi","dichiarazione redditi"],docs:["Codice fiscale","Documento d'identità","Modello CU del datore","Spese detraibili (sanitarie, mutuo, scolastiche...)","IBAN per accredito rimborso"]},
1172    {match:["isee","dsu"],docs:["Codice fiscale di tutti i componenti del nucleo","Documento d'identità","Contratto affitto/atto di proprietà casa","Saldi e giacenza media conti correnti","Targhe veicoli posseduti"]},
1173    {match:["successione"],docs:["Codice fiscale del defunto e degli eredi","Certificato di morte","Atto notorio o dichiarazione sostitutiva","Visure catastali immobili","Estratti conto e dossier titoli"]},
1174    {match:["locazioni","rli","contratto affitto","cedolare"],docs:["Codice fiscale di locatore e conduttore","Dati catastali immobile","Copia contratto","Eventuale F24 per imposta di registro"]},
1175    {match:["imu","f24"],docs:["Codice fiscale","Visure catastali immobili","Aliquote comunali (di solito le recuperiamo noi)"]},
1176    {match:["pensione","inps"],docs:["Codice fiscale","Documento d'identità","Buste paga / estratti contributivi","Eventuale Cedolino Pensione"]},
1177    {match:["naspi","disoccupazione"],docs:["Codice fiscale","Documento d'identità","Lettera di licenziamento o cessazione","IBAN per accredito"]},
1178    {match:["colf","badanti","domestici"],docs:["Codice fiscale di datore e collaboratore","Documento d'identità del collaboratore (anche permesso soggiorno)","Dati del rapporto di lavoro (orario, retribuzione)"]},
1179    {match:["atm","abbonamento"],docs:["Codice fiscale","Documento d'identità","Eventuale ISEE per agevolazioni"]},
1180    {match:["permesso","cittadinanza","stranieri"],docs:["Documento d'identità","Codice fiscale","Permesso di soggiorno (se rinnovo)","Marca da bollo"]}
1181  ];
1182  function bkDocsForServizio(srv){
1183    if(!srv)return null;
1184    var name=bkNormalizeStr(srv.desonline||srv.descrizione||"");
1185    if(!name)return null;
1186    for(var i=0;i<BK_DOC_REQUISITI.length;i++){
1187      var entry=BK_DOC_REQUISITI[i];
1188      for(var j=0;j<entry.match.length;j++){
1189        if(name.indexOf(entry.match[j])!==-1)return entry.docs;
1190      }
1191    }
1192    return null;
1193  }
1194
1195  /* ── Mapping tipologia → categoria UI (icona + label + colore) ──── */
1196  var BK_TIPOLOGIE_CAT=[
1197    {match:["730","fiscal","red","cu","unico","f24","imu","tari","tasse"],id:"fiscale",label:"Fiscale",icon:'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="4" y="3" width="16" height="18" rx="2"/><path d="M8 7h8M8 11h8M8 15h5"/></svg>',color:"#0ea5e9"},
1198    /* "colf" PRIMA di "locazioni": il pattern "contratt" di locazioni
1199     * matchava "Colf-badanti: consegna nuovo contratto" (Belluno CBNEW)
1200     * mandando i servizi colf sotto l'icona Locazioni. Segnalato da
1201     * un operatore CAF il 21/05/26. Ora oltre al riordino abbiamo anche
1202     * la priorità per codtipologia (BK_CODTIPOLOGIA_TO_CAT) che è il
1203     * fix reale; questo riordino resta come safety net. */
1204    {match:["colf","badant","domestic"],id:"colf",label:"Colf / Badanti",icon:'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="8" r="4"/><path d="M4 21v-2a4 4 0 0 1 4-4h8a4 4 0 0 1 4 4v2"/></svg>',color:"#ec4899"},
1205    {match:["locazion","affitt","rli","cedolare","contratt"],id:"locazioni",label:"Locazioni",icon:'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 12 12 4l9 8"/><path d="M5 10v10h14V10"/></svg>',color:"#f59e0b"},
1206    {match:["success","eredit"],id:"successioni",label:"Successioni",icon:'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2v20M5 9h14M5 15h14"/></svg>
1206',color:"#a855f7"},
1207    {match:["isee","dsu","social","reddito","bonus","assegno"],id:"isee",label:"ISEE / Bonus",icon:'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9"/><path d="M9 12h6M12 9v6"/></svg>',color:"#10b981"},
1208    {match:["pension","inps","naspi","disoccup","patronato","contribut"],id:"patronato",label:"Patronato",icon:'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 11V7a3 3 0 0 1 6 0v4"/><rect x="5" y="11" width="14" height="10" rx="2"/></svg>',color:"#6366f1"},
1209    {match:["permesso","cittadinanz","stranier","immigraz","anolf"],id:"stranieri",label:"Stranieri",icon:'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9"/><path d="M3 12h18M12 3a14 14 0 0 1 0 18M12 3a14 14 0 0 0 0 18"/></svg>',color:"#0891b2"},
1210    {match:["atm","abbonament","trasport"],id:"trasporti",label:"Trasporti",icon:'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="4" y="4" width="16" height="14" rx="2"/><circle cx="8" cy="20" r="1.5"/><circle cx="16" cy="20" r="1.5"/><path d="M4 11h16"/></svg>',color:"#84cc16"}
1211  ];
1212
1213  /* Mappa codtipologia (codice CANONICO esposto dal Planner) → id categoria
1214   * UI. È il segnale PRIMARIO per scegliere l'icona, perché i Planner di
1215   * tutte le regioni usano gli stessi codici tipologia (FIS, LOC, CB, SNC,
1216   * ALTRO, …) anche quando le descrizioni dei singoli servizi variano.
1217   * Chiavi UPPERCASE → confronto case-insensitive (alcune connessioni
1218   * espongono "ANOLF" o "anolf", "COLF" o "CB"). Aliases (FISCALE/FISBAS/
1219   * FIS_01) derivano dalla probe live su tutte le 16 connessioni
1220   * (2026-05-21). Manca → si ricade sul match testuale. */
1221  var BK_CODTIPOLOGIA_TO_CAT={
1222    /* Fiscali: 730, IMU, ISE, REDDITI, …
1223     * NB: FIS_01 e FIS-INT NON sono qui: il primo è ambiguo (contiene
1224     * sia "Consulenze" fiscali sia "Dimissioni" sindacali), il secondo è
1225     * FISASCAT INTERNI (categoriale, non CAF). Lasciati cadere nel match
1226     * testuale o nel blocklist server-side. */
1227    FIS:"fiscale", FISCALE:"fiscale", FISBAS:"fiscale", FIS_NAZ:"fiscale", FISCALI:"fiscale", "PR_005":"fiscale",
1228    /* Locazioni */
1229    LOC:"locazioni",
1230    /* Colf e badanti */
1231    CB:"colf", COLF:"colf", COLF_NAZ:"colf",
1232    /* Successioni */
1233    SNC:"successioni", SUCCESSION:"successioni",
1234    /* Stranieri / ANOLF */
1235    ANOLF:"stranieri", SPORT_IMMI:"stranieri",
1236    /* Patronato: NASpI, disoccupazioni */
1237    DIS:"patronato"
1238  };
1239
1240  /* Regex sul `codice` del servizio. Valutata PRIMA di codtipologia perché
1241   * il prefisso codice è un segnale semantico più affidabile della
1242   * categorizzazione Planner: in Puglia il servizio "CBCHIU Colf-badanti…"
1243   * ha codtipologia=FIS (anomalia di catalogo), ma il codice "CB*" rivela
1244   * la natura reale del servizio e merita l'icona Colf. Per Veneto servizi
1245   * Anolf_* hanno codtipologia=ALTRO, ma il prefisso "Anolf" → Stranieri. */
1246  var BK_CODICE_PREFIX_RULES=[
1247    {re:/^cb/i,    id:"colf"},        /* CBCONS, CBNEW, CBCHIU, CBNOCL */
1248    {re:/^colf/i,  id:"colf"},
1249    {re:/^anolf/i, id:"stranieri"},   /* Anolf_2…_5, Anolf_RO, Anolf2_Ro… */
1250    {re:/^naspi/i, id:"patronato"},
1251    {re:/^loc/i,   id:"locazioni"},   /* LOCCONT, LOCRLI, LOCCOM, LOCCHI, LOCREG */
1252    {re:/^scn/i,   id:"successioni"}, /* SCNCONS, SCNFIRM, SCNUSU, SCNFINE */
1253    {re:/^ise/i,   id:"isee"}         /* ISE, ISESP, ISEN, ISEDIF, ISECOR, ISESOSP */
1254  ];
1255
1256  function bkLookupCat(id){
1257    for(var k=0;k<BK_TIPOLOGIE_CAT.length;k++){
1258      if(BK_TIPOLOGIE_CAT[k].id===id)return BK_TIPOLOGIE_CAT[k];
1259    }
1260    return null;
1261  }
1262
1263  function bkServizioCategoria(srv){
1264    var altri={id:"altri",label:"Altri",icon:'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9"/><path d="M12 8v4M12 16h.01"/></svg>',color:"#64748b"};
1265    if(!srv)return altri;
1266    /* 1) Match sul codice servizio (prefissi semantici come "CB*",
1267     *    "Anolf_*", "LOC*"). Valutato PRIMA della codtipologia perché il
1268     *    prefisso codice è univoco mentre la categorizzazione del Planner
1269     *    può avere anomalie regionali (vedi Puglia CBCHIU con tip=FIS). */
1270    var code=String(srv.codice||"").trim();
1271    if(code){
1272      for(var rp=0;rp<BK_CODICE_PREFIX_RULES.length;rp++){
1273        if(BK_CODICE_PREFIX_RULES[rp].re.test(code)){
1274          var pHit=bkLookupCat(BK_CODICE_PREFIX_RULES[rp].id);
1275          if(pHit)return pHit;
1276        }
1277      }
1278    }
1279    /* 2) Match canonico per codtipologia (Planner). Case-insensitive:
1280     *    upper-case la chiave perché alcune connessioni espongono "anolf"
1281     *    altre "ANOLF". */
1282    var codTip=String(srv.codtipologia||"").trim().toUpperCase();
1283    if(codTip&&Object.prototype.hasOwnProperty.call(BK_CODTIPOLOGIA_TO_CAT,codTip)){
1284      var hit=bkLookupCat(BK_CODTIPOLOGIA_TO_CAT[codTip]);
1285      if(hit)return hit;
1286    }
1287    /* 3) Fallback: match testuale (per codtipologia sconosciute o legacy).
1288     *    Pattern brevi (≤2 char come "cu","red") usano word-boundary per
1289     *    evitare falsi positivi tipo "scuola" → "cu" → fiscale. */
1290    var name=bkNormalizeStr((srv.desonline||"")+" "+(srv.descrizione||"")+" "+(srv.descriptionTipologia||"")+" "+(srv.destipologia||""));
1291    for(var i=0;i<BK_TIPOLOGIE_CAT.length;i++){
1292      var c=BK_TIPOLOGIE_CAT[i];
1293      for(var j=0;j<c.match.length;j++){
1294        var m=c.match[j];
1295        var found;
1296        if(m.length<=3){
1297          /* Word-boundary regex per token brevi (cu, red, f24, imu, atm). */
1298          found=new RegExp("\\b"+m.replace(/[.*+?^${}()|[\\]\\\\]/g,"\\$&")+"\\b").test(name);
1299        }else{
1300          found=name.indexOf(m)!==-1;
1301        }
1302        if(found)return c;
1303      }
1304    }
1305    return altri;
1306  }
1307
1308  /* ── Frequency tracker (province / servizi) per "più usati in alto" ── */
1309  var BK_FREQ_KEY="caf-bk-freq-v1"+CAF_NS;
1310  function bkFreqLoad(){try{var r=localStorage.getItem(BK_FREQ_KEY);return r?(JSON.parse(r)||{}):{};}catch(_e){return{};}}
1311  function bkFreqSave(o){try{localStorage.setItem(BK_FREQ_KEY,JSON.stringify(o));}catch(_e){}}
1312  function bkFreqInc(kind,id){if(!id)return;if(!bkConsentHas("functional"))return;var o=bkFreqLoad();var k=kind+":"+id;o[k]=(o[k]||0)+1;o[k+":ts"]=Date.now();bkFreqSave(o);}
1313  function bkFreqGet(kind,id){var o=bkFreqLoad();return o[kind+":"+id]||0;}
1314  function bkFreqSort(kind,items,getId){
1315    if(!Array.isArray(items)||items.length<2)return items.slice();
1316    var o=bkFreqLoad();var withFreq=items.map(function(it){var id=getId(it);return{it:it,f:o[kind+":"+id]||0,t:o[kind+":"+id+":ts"]||0};});
1317    withFreq.sort(function(a,b){if(b.f!==a.f)return b.f-a.f;return b.t-a.t;});
1318    return withFreq.map(function(x){return x.it;});
1319  }
1320
1321  /* ── Slot cache (TTL 5 min) per evitare refetch su back/forward. ── */
1322  var BK_SLOT_CACHE_PREFIX="caf-bk-slotcache-v1"+CAF_NS+":";var BK_SLOT_CACHE_TTL_MS=5*60*1000;
1323  function bkSlotCacheGet(key){
1324    try{var raw=localStorage.getItem(BK_SLOT_CACHE_PREFIX+key);if(!raw)return null;
1325      var p=JSON.parse(raw);if(!p||(Date.now()-(p.ts||0))>BK_SLOT_CACHE_TTL_MS){localStorage.removeItem(BK_SLOT_CACHE_PREFIX+key);return null;}return p.data;
1326    }catch(_e){return null;}
1327  }
1328  function bkSlotCacheSet(key,data){
1329    try{localStorage.setItem(BK_SLOT_CACHE_PREFIX+key,JSON.stringify({ts:Date.now(),data:data}));}catch(_e){}
1330  }
1331  function bkSlotCacheClear(){
1332    try{for(var i=localStorage.length-1;i>=0;i--){var k=localStorage.key(i);if(k&&k.indexOf(BK_SLOT_CACHE_PREFIX)===0)localStorage.removeItem(k);}}catch(_e){}
1333  }
1334
1335  /* ── Retry con exponential backoff per errori di rete/5xx. ──
1336   * Wrap esterno a _bkFetch: re-tenta solo errori transitori, mai 4xx/validation. */
1337  async function bkRetryWithBackoff(fn,opts){
1338    opts=opts||{};
1339    var maxAttempts=opts.maxAttempts||3;
1340    var baseMs=opts.baseMs||400;
1341    var maxMs=opts.maxMs||3000;
1342    var lastErr;
1343    for(var attempt=0;attempt<maxAttempts;attempt++){
1344      try{return await fn(attempt);}
1345      catch(err){
1346        lastErr=err;
1347        var code=err&&err.code;
1348        var transient=(code==="timeout"||code==="network"||code==="server"||(typeof code==="string"&&/^5\d\d$/.test(code)));
1349        if(!transient||attempt===maxAttempts-1)throw err;
1350        var delay=Math.min(maxMs,baseMs*Math.pow(2,attempt))+Math.random()*120;
1351        await new Promise(function(r){setTimeout(r,delay);});
1352      }
1353    }
1354    throw lastErr;
1355  }
1356
1357  /* UUID per idempotenza (es. clientRequestId della conferma prenotazione).
1358   * crypto.randomUUID dove disponibile, fallback compatibile altrove. */
1359  function bkUuid(){
1360    try{if(typeof crypto!=="undefined"&&crypto.randomUUID)return crypto.randomUUID();}catch(_e){}
1361    try{if(typeof crypto!=="undefined"&&crypto.getRandomValues){
1362      var b=crypto.getRandomValues(new Uint8Array(16));
1363      b[6]=(b[6]&0x0f)|0x40;b[8]=(b[8]&0x3f)|0x80;
1364      var h=[];for(var i=0;i<16;i++)h.push((b[i]+0x100).toString(16).slice(1));
1365      return h[0]+h[1]+h[2]+h[3]+"-"+h[4]+h[5]+"-"+h[6]+h[7]+"-"+h[8]+h[9]+"-"+h[10]+h[11]+h[12]+h[13]+h[14]+h[15];
1366    }}catch(_e){}
1367    return "rid-"+Date.now().toString(36)+"-"+Math.floor(Math.random()*1e9).toString(36);
1368  }
1369
1370  /* ── Network info: tipo connessione (4g/3g/2g) + online/offline ── */
1371  function bkConnectionType(){
1372    try{var c=navigator.connection||navigator.mozConnection||navigator.webkitConnection;return c&&c.effectiveType||"unknown";}catch(_e){return"unknown";}
1373  }
1374  function bkIsSlowNetwork(){var t=bkConnectionType();return t==="slow-2g"||t==="2g";}
1375  function bkIsOnline(){try{return navigator.onLine!==false;}catch(_e){return true;}}
1376
1377  /* NOCAF (env NOCAF=si → window.__CAF_NEUTRAL__): tenant NON-CAF.
1378   * I testi visibili all'utente non devono citare il CAF né le "pratiche":
1379   * si parla di operatore umano. Il brand negli artefatti (ICS, promemoria)
1380   * diventa il nome del chatbot del tenant. Senza flag: tutto IDENTICO. */
1381  function bkNeutralMode(){try{return typeof window!=="undefined"&&window.__CAF_NEUTRAL__===true;}catch(_e){return false;}}
1382  function bkNeutralBrand(dflt){
1383    if(!bkNeutralMode())return dflt;
1384    try{var n=(window.__CAF_CHATBOT_NAME__||"").trim();if(n&&!/caf/i.test(n))return n;}catch(_e){}
1385    return "Assistenza appuntamenti";
1386  }
1387
1388  /* ── Numero verde fallback (configurabile via attributo data-helpline). ── */
1389  function bkHelplineNumber(opts){
1390    if(opts&&opts.helpline)return String(opts.helpline);
1391    try{var s=document.currentScript||document.querySelector("script[data-helpline]");if(s){var v=s.getAttribute("data-helpline");if(v)return v;}}catch(_e){}
1392    /* Numero "Parla con un operatore" per-tenant: env OPERATORE_TEL sul
1393     * progetto Vercel, iniettata dalla route come __CAF_HELPLINE__.
1394     * Valore "no" = bottone operatore nascosto. */
1395    try{
1396      if(typeof window!=="undefined"&&typeof window.__CAF_HELPLINE__==="string"&&window.__CAF_HELPLINE__){
1397        var hv=String(window.__CAF_HELPLINE__).trim();
1398        if(hv.toLowerCase()==="no")return"";
1399        if(hv)return hv;
1400      }
1401    }catch(_e){}
1402    return"800-800-730"; /* fallback default CAF CISL */
1403  }
1404
1405  /* ── Prefissi telefonici internazionali (subset comune). ── */
1406  var BK_PHONE_COUNTRIES=[
1407    {code:"+39",flag:"🇮🇹",name:"Italia"},
1408    {code:"+1",flag:"🇺🇸",name:"USA/Canada"},
1409    {code:"+33",flag:"🇫🇷",name:"Francia"},
1410    {code:"+34",flag:"🇪🇸",name:"Spagna"},
1411    {code:"+44",flag:"🇬🇧",name:"Regno Unito"},
1412    {code:"+49",flag:"🇩🇪",name:"Germania"},
1413    {code:"+41",flag:"🇨🇭",name:"Svizzera"},
1414    {code:"+43",flag:"🇦🇹",name:"Austria"},
1415    {code:"+32",flag:"🇧🇪",name:"Belgio"},
1416    {code:"+31",flag:"🇳🇱",name:"Paesi Bassi"},
1417    {code:"+351",flag:"🇵🇹",name:"Portogallo"},
1418    {code:"+40",flag:"🇷🇴",name:"Romania"},
1419    {code:"+48",flag:"🇵🇱",name:"Polonia"},
1420    {code:"+30",flag:"🇬🇷",name:"Grecia"},
1421    {code:"+90",flag:"🇹🇷",name:"Turchia"},
1422    {code:"+212",flag:"🇲🇦",name:"Marocco"},
1423    {code:"+216",flag:"🇹🇳",name:"Tunisia"},
1424    {code:"+20",flag:"🇪🇬",name:"Egitto"},
1425    {code:"+62",flag:"🇮🇩",name:"Indonesia"},
1426    {code:"+86",flag:"🇨🇳",name:"Cina"},
1427    {code:"+91",flag:"🇮🇳",name:"India"},
1428    {code:"+63",flag:"🇵🇭",name:"Filippine"},
1429    {code:"+380",flag:"🇺🇦",name:"Ucraina"},
1430    {code:"+7",flag:"🇷🇺",name:"Russia"},
1431    {code:"+55",flag:"🇧🇷",name:"Brasile"},
1432    {code:"+54",flag:"🇦🇷",name:"Argentina"},
1433    {code:"+57",flag:"🇨🇴",name:"Colombia"},
1434    {code:"+51",flag:"🇵🇪",name:"Perù"},
1435    {code:"+593",flag:"🇪🇨",name:"Ecuador"},
1436    {code:"+221",flag:"🇸🇳",name:"Senegal"},
1437    {code:"+234",flag:"🇳🇬",name:"Nigeria"},
1438    {code:"+225",flag:"🇨🇮",name:"Costa d'Avorio"},
1439    {code:"+961",flag:"🇱🇧",name:"Libano"},
1440    {code:"+963",flag:"🇸🇾",name:"Siria"},
1441    {code:"+92",flag:"🇵🇰",name:"Pakistan"},
1442    {code:"+880",flag:"🇧🇩",name:"Bangladesh"}
1443  ];
1444
1445  /* ── Codifica codice fiscale italiano: estrae sesso, data nascita,
1446   * comune/codice catastale dalla CF (algoritmo deterministico). ── */
1447  function bkParseCodiceFiscale(cf){
1448    if(!cf||typeof cf!=="string")return null;
1449    cf=cf.toUpperCase().replace(/\s/g,"");
1450    if(!/^[A-Z]{6}\d{2}[A-EHLMPRST]\d{2}[A-Z]\d{3}[A-Z]$/.test(cf))return null;
1451    var monthMap={A:0,B:1,C:2,D:3,E:4,H:5,L:6,M:7,P:8,R:9,S:10,T:11};
1452    var year=parseInt(cf.slice(6,8),10);
1453    var monthCh=cf.charAt(8);if(!(monthCh in monthMap))return null;
1454    var month=monthMap[monthCh];
1455    var day=parseInt(cf.slice(9,11),10);
1456    var sesso=day>40?"F":"M";if(sesso==="F")day-=40;
1457    var fullYear=year<=(new Date().getFullYear()%100)?(2000+year):(1900+year);
1458    var d=new Date(fullYear,month,day);
1459    if(d.getFullYear()!==fullYear||d.getMonth()!==month||d.getDate()!==day)return null;
1460    return{sesso:sesso,giorno:day,mese:month+1,anno:fullYear,codiceCatastale:cf.slice(11,15),cf:cf};
1461  }
1462  function bkValidaCF(cf){return!!bkParseCodiceFiscale(cf);}
1463  function bkValidaEmail(em){return!!em&&/^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/.test(String(em).trim());}
1464  function bkValidaTelefono(tel){if(!tel)return false;var s=String(tel).replace(/[\s\-().]/g,"");if(/^\+?\d{6,15}$/.test(s))return true;return false;}
1465
1466  /* ── i18n: dizionari multilingua per il booking flow.
1467   * Lingue supportate: it (default), en, fr, es, de, ro. Per il resto si
1468   * fallback al testo italiano. La lingua è auto-rilevata da navigator.language
1469   * con override possibile via opts.lang / ?caf-lang= / data-lang / localStorage. */
1470  var BK_I18N={
1471    it:{
1472      /* a11y accessible names */ aria_open_chat:"Apri chat", aria_close_chat:"Chiudi chat", ph_write_message:"Scrivi un messaggio", aria_send:"Invia messaggio", aria_scroll_bottom:"Torna in fondo", aria_options:"Opzioni", aria_close:"Chiudi", aria_fullscreen:"Schermo intero", aria_exit_fullscreen:"Esci da schermo intero", aria_voice:"Riconoscimento vocale", aria_book:"Apri prenotazione", aria_open_book:"Apri chat e prenota un appuntamento", skip_to_input:"Vai al campo messaggio", aria_history:"Cronologia conversazione", sr_assistant:"Assistente", sr_you_said:"Tu hai scritto:", sr_said:"ha scritto:", assistant_typing:"L'assistente sta scrivendo…", label_error:"Errore", booking_confirmed:"Prenotazione confermata", menu_contact:"Parla con un operatore", aria_email:"Indirizzo email", aria_pwd_email:"Email per il reset della password", aria_view_on_map:"Vedi {0} sulla mappa", aria_copy:"Copia messaggio", aria_translate:"Traduci messaggio", aria_scroll_left:"Scorri a sinistra", aria_scroll_right:"Scorri a destra", aria_remove:"Rimuovi", aria_add:"Aggiungi", aria_speak:"Parla", aria_more_info:"Maggiori info", aria_hide:"Nascondi", aria_expand_booking:"Apri prenotazione a schermo intero", aria_book_appointment:"Prenota il tuo appuntamento", aria_booking:"Prenotazione", aria_search_office:"Cerca sede",
1473      hero_title:"Prenota il tuo appuntamento",
1474      hero_subtitle:"⏱ ~2 minuti · ",
1475      step_zone:"Zona", step_service:"Servizio", step_office:"Sede", step_time:"Orario", step_data:"Dati",
1476      h_region:"In quale regione desideri prenotare?", s_region:"Scegli la tua regione per vedere le sedi e i servizi disponibili.",
1477      h_province:"In quale provincia desideri prenotare?", s_province:"Usa la tua posizione per trovarla automaticamente oppure scegli manualmente.",
1478      h_service:"Scegli il servizio", s_service:"Seleziona il servizio che vuoi prenotare.",
1479      h_service_hint:"Quale servizio ti interessa?", s_service_hint:'Ho filtrato per "{0}
1479". Puoi affinare la ricerca qui sotto.',
1480      h_office:"Scegli la sede", s_office:"Ecco tutti i centri disponibili. Usa la geolocalizzazione per trovare la più vicina a te.",
1481      h_slot:"Scegli data e ora", s_slot:"Seleziona un giorno sul calendario per vedere gli orari disponibili.",
1482      h_data:"Inserisci i tuoi dati", s_data:"Quasi fatto! Ci servono i dati dell'intestatario per finalizzare la prenotazione.",
1483      h_confirm:"Conferma la tua prenotazione", s_confirm:"Controlla i dettagli. Se tutto è corretto, premi Conferma per salvare l'appuntamento.",
1484      btn_back:"Indietro", btn_cancel:"Annulla", btn_confirm:"Conferma prenotazione", btn_confirming:"Salvataggio in corso...",
1485      consent_before:"Ho letto l'", consent_link:"informativa sulla privacy", consent_after:" e acconsento al trattamento dei miei dati personali per la gestione della prenotazione.", consent_required:"Per procedere devi accettare il trattamento dei dati personali.",
1486      chatgate_h:"La tua privacy", chatgate_body:"Prima di iniziare la conversazione ti chiediamo di prendere visione dell'informativa sulla privacy e di acconsentire al trattamento dei dati personali.", chatgate_before:"Ho letto l'", chatgate_link:"informativa sulla privacy", chatgate_after:" e acconsento al trattamento dei miei dati personali.", chatgate_btn:"Accetta e inizia la chat",
1487      ai_disclosure:"Stai parlando con un assistente virtuale basato sull'intelligenza artificiale (IA), non con una persona. Le risposte sono generate automaticamente e possono contenere errori: verifica le informazioni importanti con un operatore. La tua pratica sarà seguita da operatori umani del CAF.",
1488      ai_badge:"Assistente IA · risposte generate automaticamente", ai_voice_notice:"Stai parlando con un assistente vocale automatico basato sull'intelligenza artificiale. Le risposte sono generate automaticamente e possono contenere errori.",
1489      email_gate_body:"Per iniziare la conversazione, inserisci il tuo indirizzo email.", email_gate_ph:"La tua email", email_gate_btn:"Inizia la chat", email_gate_note:"Useremo la tua email solo per gestire questa conversazione e ricontattarti sulla tua richiesta.", email_gate_privacy:"Informativa privacy", aria_prev_month:"Mese precedente", aria_next_month:"Mese successivo", hold_hint:"Ti consigliamo di completare entro {0}", hold_expired:"Gli orari potrebbero essere nel frattempo cambiati: verifica la disponibilità prima di confermare.",
1490      age_gate_label:"Dichiaro di avere almeno 14 anni. Per i minori di 14 anni è necessario il consenso di un genitore o di chi esercita la responsabilità genitoriale.",
1491      translate_ai_note:"Traduzione automatica generata da IA", demo_ribbon:"DEMO — i dati mostrati sono di esempio. Nessuna prenotazione reale viene effettuata.",
1492      btn_retry:"Riprova", btn_change:"Cambia selezione", btn_call:"Chiama", btn_restart:"Ricomincia", btn_close:"Chiudi", btn_edit:"Modifica",
1493      btn_use_location:"Usa la mia posizione", btn_use_location_sub:"Trovo automaticamente la tua provincia",
1494      btn_find_nearest:"Trova la sede più vicina", btn_find_nearest_again:"Ricerca di nuovo",
1495      btn_next:"Prosegui al riepilogo", btn_load_more:"Carica altri orari",
1496      label_or_choose:"oppure scegli manualmente",
1497      label_morning:"Mattina", label_afternoon:"Pomeriggio",
1498      label_today:"OGGI", label_tomorrow:"DOMANI", label_this_week:"QUESTA SETTIMANA",
1499      label_count_slots_one:"{0} orario", label_count_slots_other:"{0} orari",
1500      label_first_avail:"Prima disponibilità", label_quickpick_arrow:"→",
1501      label_when:"Quando", label_where:"Dove", label_what:"Cosa",
1502      label_owner:"Intestatario", label_contacts:"Contatti",
1503      label_summary_premium_h:"Tutto pronto, controlla e conferma",
1504      label_summary_premium_s:"Controlla i dettagli prima di salvare l'appuntamento.",
1505      label_summary_disclaimer:"Potrai modificare o cancellare la prenotazione fino a 24 ore prima dell'appuntamento.",
1506      label_form_for_self:"Per me", label_form_for_family:"Per un familiare",
1507      label_form_nome:"Nome", label_form_cognome:"Cognome", label_form_codfis:"Codice Fiscale", label_form_email:"Email", label_form_cellulare:"Cellulare",
1508      label_form_duration_min:"Durata ~{0} min",
1509      label_data_safe:"Dati protetti", label_data_caf:"Assistenza CAF", label_data_real:"Appuntamento con operatori reali del CAF",
1510      label_done_h:"Prenotazione confermata!", label_done_s:"Ti abbiamo inviato un'email di conferma con tutti i dettagli dell'appuntamento.",
1511      label_done_id:"ID prenotazione: ",
1512      label_add_calendar:"Aggiungi al calendario", label_share_whatsapp:"Condividi su WhatsApp", label_email_reminder:"Promemoria via email",
1513      label_done_survey:"Come è stata l'esperienza?", label_done_thanks:"Grazie per il feedback!",
1514      label_done_good:"Bene", label_done_meh:"Così così", label_done_bad:"Male",
1515      ph_search_service:"Cerca (es. 730, ISEE, casa, dichiarazione redditi...)",
1516      ph_search_office:"Cerca sede per nome, città o indirizzo (es. Milano, Garibaldi...)",
1517      ph_search_province:"Cerca città o provincia (es. Milano, Roma...)",
1518      ph_no_results_one:'Nessun servizio trovato per "{0}"',
1519      label_did_you_mean:"Forse intendevi:",
1520      label_popular:"🔥 Più richiesto", label_recent:"⏱ Recente", label_frequent:"⭐ Frequente",
1521      label_resume_h:"Riprendi da dove avevi lasciato",
1522      label_resume_btn:"Riprendi", label_resume_discard:"Ricomincia",
1523      label_offline_b:"Sei offline.", label_offline_msg:" Connettiti per completare la prenotazione. Lo stato è salvato e potrai riprendere.",
1524      label_slow:"Connessione lenta — il caricamento potrebbe richiedere qualche secondo in più.",
1525      err_h_default:"Si è verificato un errore", err_s_default:"Riprova oppure chiama il numero verde per assistenza.",
1526      err_h_timeout:"Connessione lenta", err_s_timeout:"Il server non risponde. Verifica la connessione e riprova.",
1527      err_h_network:"Niente connessione", err_s_network:"Sembra che tu sia offline. Controlla la rete e riprova.",
1528      err_h_no_services:"Nessun servizio disponibile", err_s_no_services:"Per la tua provincia al momento non sono disponibili servizi prenotabili online.",
1529      err_h_no_sedi:"Nessuna sede disponibile", err_s_no_sedi:"Non ci sono sedi configurate per questo servizio nella tua provincia.",
1530      err_h_no_spazi:"Nessuno slot disponibile", err_s_no_spazi:"Per ora non ci sono orari liberi. Riprova tra qualche giorno o cambia sede.",
1531      err_h_already_booked:"Hai già una prenotazione", err_s_already_booked:"Risulti già prenotato per un servizio in queste date.",
1532      err_h_slot_taken:"Orario appena occupato", err_s_slot_taken:"L'orario che hai scelto è stato appena prenotato da un'altra persona. Scegli un altro orario tra quelli aggiornati.",
1533      err_h_validation:"Dati non validi", err_s_validation:"Controlla i campi inseriti e riprova.",
1534      err_h_disabled:"Servizio non disponibile", err_s_disabled:"La prenotazione online è temporaneamente disabilitata.",
1535      err_h_server:"Errore del server", err_s_server:"Risposta non valida dal server. Riprova tra qualche istante.",
1536      toast_slow_title:"Connessione lenta", toast_slow_msg:"Sto riprovando ({0}/3)...",
1537      gdpr_h:"Privacy e protezione dei tuoi dati",
1538      gdpr_body:"Possiamo memorizzare su questo dispositivo i tuoi dati (nome, codice fiscale, email, telefono) per pre-compilarli la prossima volta. Questi dati di pre-compilazione restano nel tuo browser e si auto-cancellano dopo 30 giorni di inattività. Quando confermi una prenotazione o scrivi in chat, i dati necessari vengono inviati ai sistemi indicati nella sezione Destinatari.",
1539      gdpr_body_v2:"Per offrirti il servizio di prenotazione online, {0} utilizza alcune tecnologie di memorizzazione locale (localStorage) sul tuo dispositivo. Puoi scegliere quali categorie accettare. La tua scelta è registrata e revocabile in qualsiasi momento dal menu ⋯.",
1540      gdpr_more:"Dettagli completi", gdpr_accept:"Sì, ricorda i miei dati", gdpr_deny:"No, grazie",
1541      gdpr_accept_all:"Accetta tutti", gdpr_reject_all:"Rifiuta tutti", gdpr_customize:"Personalizza", gdpr_save_prefs:"Salva preferenze",
1542      gdpr_privacy_policy:"Informativa Privacy completa",
1543      gdpr_cat_necessary:"Necessari (sempre attivi)",
1544      gdpr_cat_necessary_d:"Tecnologie indispensabili al funzionamento del widget: cache delle regioni/sedi/servizi, lingua. Senza questi il widget non funziona. Base giuridica: strettamente necessarie al servizio richiesto (esenti da consenso).",
1545      gdpr_cat_functional:"Funzionali",
1546      gdpr_cat_functional_d:"Memorizzazione dei tuoi dati di prenotazione (nome, codice fiscale, email, telefono) per pre-compilarli la prossima volta. Lo stato di prenotazione in corso (resume) e la cronologia recente.",
1547      gdpr_cat_analytics:"Analytics e marketing",
1548      gdpr_cat_analytics_d:"Identificatore di sessione, parametri di campagna (UTM, gclid, fbclid) e raccolta degli eventi del flusso di prenotazione per migliorare il servizio. Dati pseudonimizzati.",
1549      gdpr_info_controller:"Titolare del trattamento:",
1550      gdpr_info_address:"Sede:", gdpr_info_email:"Email/PEC:", gdpr_info_phone:"Telefono:", gdpr_info_vat:"P.IVA / C.F.:", gdpr_info_dpo:"Resp. protezione dati (DPO):",
1551      gdpr_info_legal_basis:"Base giuridica:", gdpr_info_legal_basis_d:"Necessari = tecnologie strettamente necessarie al servizio richiesto (esenti da consenso, art. 122 Codice privacy); i dati di prenotazione sono trattati per dare esecuzione al servizio richiesto (art. 6.1.b GDPR). Funzionali e Analytics = consenso (art. 6.1.a GDPR).",
1552      gdpr_info_storage:"Dove sono conservati:", gdpr_info_storage_d:"I dati di pre-compilazione, la cronologia e le preferenze restano nel localStorage del tuo browser, su questo dispositivo (offuscati ma non cifrati con standard crittografici). Quando prenoti o scrivi in chat, i dati corrispondenti vengono inviati e conservati sui server dei responsabili del trattamento indicati in 'Destinatari'.",
1553      gdpr_info_retention:"Periodo di conservazione:", gdpr_info_retention_d:"Sul dispositivo: 30 giorni dall'ultima attività, poi auto-cancellazione. Sui server: le conversazioni sono cancellate e i log di prenotazione anonimizzati entro i termini stabiliti dal Titolare (a titolo indicativo: conversazioni circa 180 giorni; prenotazioni secondo gli obblighi contabili/fiscali applicabili). Il consenso resta valido fino a revoca.",
1554      gdpr_info_recipients:"Destinatari:", gdpr_info_recipients_d:"I dati di prenotazione sono inviati al sistema Planner CAF (in UE) per finalizzare l'appuntamento. Le conversazioni sono elaborate dalla piattaforma conversazionale Mantis e dai provider di IA collegati, anche extra-UE con clausole contrattuali tipo (SCC): assistenza conversazionale e classificazione degli intenti (Anthropic) e analisi di eventuali documenti caricati (OpenAI). Per individuare la sede più vicina si usano OpenStreetMap/Nominatim e servizi di geolocalizzazione via IP. Distribuzione tecnica: Vercel e Bunny CDN; conservazione su Google Firebase. Vedi l'Informativa Privacy del Titolare.",
1555      gdpr_info_rights:"I tuoi diritti:", gdpr_info_rights_d:"Accesso, rettifica, cancellazione, limitazione, portabilità, opposizione (artt. 15-22 GDPR). Reclamo al Garante Privacy.",
1556      gdpr_info_revoke:"Come revocare:", gdpr_info_revoke_d:"Apri la chat → menu (⋯) → Privacy. Puoi modificare le preferenze o cancellare tutti i dati.",
1557      menu_new:"Nuova conversazione",
1558      menu_revoke:"Revoca consenso e cancella dati",
1559      menu_privacy:"Privacy e dati personali",
1560      revoke_confirm:"Vuoi cancellare i dati salvati (nome, codice fiscale, email, telefono) e revocare il consenso?",
1561      revoke_done_h:"Dati cancellati", revoke_done_s:"I tuoi dati personali sono stati rimossi da questo dispositivo.",
1562      privacy_panel_h:"Privacy e i tuoi dati",
1563      privacy_panel_s:"Qui puoi modificare le preferenze di consenso, scaricare i dati che abbiamo memorizzato sul tuo dispositivo, o cancellarli completamente.",
1564      privacy_panel_consent_h:"Le tue preferenze di consenso",
1565      privacy_panel_data_h:"Dati attualmente memorizzati su questo dispositivo",
1566      privacy_panel_data_client:"Dati anagrafici (nome, CF, email, telefono)",
1567      privacy_panel_data_history:"Cronologia prenotazioni (numero)",
1568      privacy_panel_data_resume:"Prenotazione in corso (resume)",
1569      privacy_panel_data_sid:"Identificatore di sessione",
1570      privacy_panel_present:"presenti", privacy_panel_absent:"assenti",
1571      privacy_panel_export:"Scarica i miei dati", privacy_panel_delete_all:"Cancella tutto",
1572      privacy_panel_export_h:"Download avviato", privacy_panel_export_s:"I tuoi dati sono stati esportati in un file JSON.",
1573      privacy_panel_saved_h:"Preferenze salvate", privacy_panel_saved_s:"Le tue scelte di consenso sono state aggiornate.",
1574      cf_valid_ok:"CF valido.", cf_valid_recognized:"Riconosciuto: {0}, nato/a il {1}, comune (codice {2})",
1575      cf_invalid:"Codice fiscale non valido. Verifica i caratteri inseriti.",
1576      cf_male:"♂ Maschio", cf_female:"♀ Femmina",
1577      /* EAA/WCAG 2026-07-19: chiavi a11y + i18n wizard/voice */
1578      sr_booking_launch:"È disponibile un pulsante per aprire la prenotazione.", sr_results_count:"{0} risultati", sr_no_results:"Nessun risultato", aria_select_office:"Seleziona {0}",
1579      aria_stop_listening:"Interrompi l'ascolto", sr_listening:"Sto ascoltando", sr_processing:"Elaborazione in corso", aria_back_to_step:"Torna allo step {0}: {1}",
1580      step_of:"Passo {0} di {1}", cal_months:"Gennaio|Febbraio|Marzo|Aprile|Maggio|Giugno|Luglio|Agosto|Settembre|Ottobre|Novembre|Dicembre", cal_months_short:"gen|feb|mar|apr|mag|giu|lug|ago|set|ott|nov|dic", cal_days_full:"Domenica|Lunedì|Martedì|Mercoledì|Giovedì|Venerdì|Sabato",
1581      cal_days_short:"Lun|Mar|Mer|Gio|Ven|Sab|Dom", label_available:"Disponibile", label_selected:"Selezionato", label_nearest:"Più vicina",
1582      label_view_map:"Vedi sulla mappa", label_no_slots_h:"Nessun orario disponibile", label_no_slots_s:"Al momento non ci sono slot disponibili. Prova a cambiare sede o riprova più tardi.", label_no_dates_h:"Orari non disponibili",
1583      label_no_dates_s:"Non sono riuscito a leggere le date disponibili. Prova a cambiare sede.", label_pick_day:"Seleziona un giorno", label_no_slots_day:"Nessun orario in questo giorno. Prova un altro giorno disponibile.", ph_no_office_found:"Nessuna sede trovata per \"{0}\"",
1584      ph_no_results_generic:"Nessun risultato per \"{0}\"", label_reuse_data_h:"Riusa i tuoi dati salvati?", btn_its_me:"Sì, sono io", btn_not_me:"No, sono un'altra persona",
1585      label_saved_data:"Dati salvati", label_submitting:"Invio prenotazione...", label_submitting_sub:"Sto registrando il tuo appuntamento, ci vorrà qualche secondo.", label_loading_hint:"Cerco disponibilità per \"{0}\"...",
1586      label_loading_options:"Caricamento opzioni...", label_loading_short:"Caricamento…", label_error_code:"Codice: {0}", label_booking_inprogress:"Prenotazione in corso",
1587      label_expand:"Espandi", label_expand_sub:"Apri la vista a schermo intero per un'esperienza migliore", t_region:"Seleziona la regione", t_province:"Seleziona la provincia",
1588      t_data:"I tuoi dati", label_srv_hint_found:"Ho trovato questi servizi per \"{0}\". Quale ti interessa?", label_srv_hint:"Quale servizio vuoi prenotare?", label_sede_hint:"Perfetto! Ora scegli la sede più comoda per te.",
1589      btn_find_nearest_sub:"Usa la tua posizione GPS", btn_find_nearest_again_sub:"Usa la posizione aggiornata", label_slots_hint:"Ecco gli appuntamenti disponibili. Scegli quello che preferisci!", label_pref_time:"Di solito preferisci: {0}.",
1590      label_data_hint:"Quasi fatto! Inserisci i tuoi dati per completare la prenotazione.", label_date_at:"{0} alle {1}", label_appointment:"Appuntamento", btn_use:"Usa",
1591      btn_calendar:"Calendario", btn_email_reminder:"Inviami il promemoria via email", btn_choose_other:"Scegli un altro", btn_choose_other_slot:"Scegli un altro orario",
1592      label_done_email:"Prenotazione registrata per {0}", err_cf_required:"Inserisci il codice fiscale.", err_cf_short:"Il codice fiscale deve essere di 16 caratteri (mancano {0}
1592 caratteri).", err_cf_long:"Il codice fiscale deve essere di 16 caratteri (ne hai inserito {0} in più).",
1593      err_cf_chars:"Il codice fiscale può contenere solo lettere e cifre.", err_cf_format:"Il formato del codice fiscale non è valido. Verifica i caratteri inseriti.", err_cf_charpos:"Carattere non valido alla posizione {0}.", err_cf_checksum:"La cifra di controllo non torna (attesa '{0}' al posto di '{1}'). Ricontrolla.",
1594      err_cf_first:"Inserisci prima un codice fiscale valido", err_cf_proceed:"Inserisci un codice fiscale valido prima di proseguire.", err_required_fields:"Compila tutti i campi obbligatori.", err_email_invalid:"Inserisci un indirizzo email valido.",
1595      err_phone_invalid:"Inserisci un numero di cellulare valido (almeno 9 cifre).", err_uncertain:"Non abbiamo ricevuto conferma dal server: la prenotazione potrebbe essere comunque andata a buon fine. Controlla la tua email prima di riprovare.", err_unexpected:"Errore imprevisto. Riprova.", help_codfis:"Lo trovi sulla tua tessera sanitaria o tessera fiscale. È di 16 caratteri.",
1596      help_email:"Ti useremo solo per inviarti la conferma e il promemoria 24h prima.", help_cellulare:"Per eventuali comunicazioni urgenti dalla sede.", voice_tap_to_talk:"Tocca per parlare", voice_press_to_start:"Premi il microfono per iniziare",
1597      voice_speak_now:"Parla ora", voice_listening_hint:"Ti sto ascoltando... parla pure", voice_responding:"Sto rispondendo", voice_tap_interrupt:"Tocca il microfono per interrompere",
1598      voice_thinking:"Sto pensando...", voice_thinking_hint:"Un momento, sto preparando la risposta", voice_back_keyboard:"Torna alla tastiera", ph_your_city:"Scrivi la tua città...", ph_follow_steps:"Segui i passaggi sopra per prenotare..."
1599    },
1600    en:{
1601      /* a11y accessible names */ aria_open_chat:"Open chat", aria_close_chat:"Close chat", ph_write_message:"Write a message", aria_send:"Send message", aria_scroll_bottom:"Scroll to latest", aria_options:"Options", aria_close:"Close", aria_fullscreen:"Full screen", aria_exit_fullscreen:"Exit full screen", aria_voice:"Voice input", aria_book:"Open booking", aria_open_book:"Open chat and book an appointment", skip_to_input:"Skip to message field", aria_history:"Conversation history", sr_assistant:"Assistant", sr_you_said:"You said:", sr_said:"said:", assistant_typing:"The assistant is typing…", label_error:"Error", booking_confirmed:"Booking confirmed", menu_contact:"Talk to an operator", aria_email:"Email address", aria_pwd_email:"Password reset email", aria_view_on_map:"View {0} on the map", aria_copy:"Copy message", aria_translate:"Translate message", aria_scroll_left:"Scroll left", aria_scroll_right:"Scroll right", aria_remove:"Remove", aria_add:"Add", aria_speak:"Speak", aria_more_info:"More info", aria_hide:"Hide", aria_expand_booking:"Open booking full screen", aria_book_appointment:"Book your appointment", aria_booking:"Booking", aria_search_office:"Search office",
1602      hero_title:"Book your appointment",
1603      hero_subtitle:"⏱ ~2 minutes · ",
1604      step_zone:"Area", step_service:"Service", step_office:"Office", step_time:"Time", step_data:"Details",
1605      h_region:"Which region do you want to book in?", s_region:"Choose your region to see available offices and services.",
1606      h_province:"Which province do you want to book in?", s_province:"Use your location to find it automatically, or pick manually.",
1607      h_service:"Choose the service", s_service:"Select the service you want to book.",
1608      h_service_hint:"Which service do you need?", s_service_hint:'Filtered by "{0}". You can refine the search below.',
1609      h_office:"Choose the office", s_office:"Here are all the available offices. Use geolocation to find the nearest one.",
1610      h_slot:"Choose date and time", s_slot:"Select a day on the calendar to see available time slots.",
1611      h_data:"Enter your details", s_data:"Almost done! We need the booker's details to finalize.",
1612      h_confirm:"Confirm your booking", s_confirm:"Review the details. If everything is correct, press Confirm to save.",
1613      btn_back:"Back", btn_cancel:"Cancel", btn_confirm:"Confirm booking", btn_confirming:"Saving...",
1614      consent_before:"I have read the ", consent_link:"privacy policy", consent_after:" and I consent to the processing of my personal data to manage the booking.", consent_required:"To proceed you must accept the processing of your personal data.",
1615      chatgate_h:"Your privacy", chatgate_body:"Before starting the conversation, please read the privacy policy and consent to the processing of your personal data.", chatgate_before:"I have read the ", chatgate_link:"privacy policy", chatgate_after:" and I consent to the processing of my personal data.", chatgate_btn:"Accept and start chatting",
1616      ai_disclosure:"You are chatting with a virtual assistant powered by artificial intelligence (AI), not a human. Replies are generated automatically and may contain errors: check important information with an operator. Your case will be handled by human CAF staff.",
1617      ai_badge:"AI assistant · automatically generated replies", ai_voice_notice:"You are talking to an automated voice assistant powered by artificial intelligence. Replies are generated automatically and may contain errors.",
1618      email_gate_body:"To start the conversation, please enter your email address.", email_gate_ph:"Your email", email_gate_btn:"Start the chat", email_gate_note:"We only use your email to handle this conversation and follow up on your request.", email_gate_privacy:"Privacy policy", aria_prev_month:"Previous month", aria_next_month:"Next month", hold_hint:"We recommend completing within {0}", hold_expired:"Availability may have changed in the meantime: please double-check the time slots before confirming.",
1619      age_gate_label:"I declare that I am at least 14 years old. For children under 14, the consent of a parent or legal guardian is required.",
1620      translate_ai_note:"Automatic translation generated by AI", demo_ribbon:"DEMO — the data shown is sample data. No real booking is made.",
1621      btn_retry:"Retry", btn_change:"Change selection", btn_call:"Call", btn_restart:"Start over", btn_close:"Close", btn_edit:"Edit",
1622      btn_use_location:"Use my location", btn_use_location_sub:"I'll find your province automatically",
1623      btn_find_nearest:"Find the nearest office", btn_find_nearest_again:"Search again",
1624      btn_next:"Continue to summary", btn_load_more:"Load more times",
1625      label_or_choose:"or choose manually",
1626      label_morning:"Morning", label_afternoon:"Afternoon",
1627      label_today:"TODAY", label_tomorrow:"TOMORROW", label_this_week:"THIS WEEK",
1628      label_count_slots_one:"{0} slot", label_count_slots_other:"{0} slots",
1629      label_first_avail:"First available", label_quickpick_arrow:"→",
1630      label_when:"When", label_where:"Where", label_what:"What",
1631      label_owner:"Holder", label_contacts:"Contacts",
1632      label_summary_premium_h:"All set, review and confirm",
1633      label_summary_premium_s:"Check the details before saving the appointment.",
1634      label_summary_disclaimer:"You can edit or cancel the booking up to 24 hours before the appointment.",
1635      label_form_for_self:"For me", label_form_for_family:"For a family member",
1636      label_form_nome:"First name", label_form_cognome:"Last name", label_form_codfis:"Tax code", label_form_email:"Email", label_form_cellulare:"Mobile",
1637      label_form_duration_min:"Duration ~{0} min",
1638      label_data_safe:"Data protected", label_data_caf:"CAF support", label_data_real:"Appointment with real CAF operators",
1639      label_done_h:"Booking confirmed!", label_done_s:"We've sent you a confirmation email with all the appointment details.",
1640      label_done_id:"Booking ID: ",
1641      label_add_calendar:"Add to calendar", label_share_whatsapp:"Share on WhatsApp", label_email_reminder:"Email reminder",
1642      label_done_survey:"How was your experience?", label_done_thanks:"Thanks for the feedback!",
1643      label_done_good:"Good", label_done_meh:"So-so", label_done_bad:"Bad",
1644      ph_search_service:"Search (e.g. tax return, ISEE, home...)",
1645      ph_search_office:"Search office by name, city or address",
1646      ph_search_province:"Search city or province",
1647      ph_no_results_one:'No service found for "{0}"',
1648      label_did_you_mean:"Did you mean:",
1649      label_popular:"🔥 Most requested", label_recent:"⏱ Recent", label_frequent:"⭐ Frequent",
1650      label_resume_h:"Resume where you left off",
1651      label_resume_btn:"Resume", label_resume_discard:"Start over",
1652      label_offline_b:"You're offline.", label_offline_msg:" Connect to complete the booking. State is saved and you can resume.",
1653      label_slow:"Slow connection — loading may take a few extra seconds.",
1654      err_h_default:"An error occurred", err_s_default:"Retry or call the helpline for assistance.",
1655      err_h_timeout:"Slow connection", err_s_timeout:"The server is not responding. Check your connection and retry.",
1656      err_h_network:"No connection", err_s_network:"You appear to be offline. Check your network and retry.",
1657      err_h_no_services:"No services available", err_s_no_services:"There are currently no online-bookable services for your province.",
1658      err_h_no_sedi:"No offices available", err_s_no_sedi:"No offices configured for this service in your province.",
1659      err_h_no_spazi:"No slots available", err_s_no_spazi:"No free times right now. Try again in a few days or change office.",
1660      err_h_already_booked:"You already have a booking", err_s_already_booked:"You're already booked for a service on these dates.",
1661      err_h_slot_taken:"Time slot just taken", err_s_slot_taken:"The time you selected was just booked by someone else. Please pick another time from the updated list.",
1662      err_h_validation:"Invalid data", err_s_validation:"Check the entered fields and retry.",
1663      err_h_disabled:"Service unavailable", err_s_disabled:"Online booking is temporarily disabled.",
1664      err_h_server:"Server error", err_s_server:"Invalid response from server. Retry shortly.",
1665      toast_slow_title:"Slow connection", toast_slow_msg:"Retrying ({0}/3)...",
1666      gdpr_h:"Privacy and your data",
1667      gdpr_body:"We can store your details (name, tax code, email, phone) on this device to pre-fill them next time. This pre-fill data stays in your browser and auto-deletes after 30 days of inactivity. When you confirm a booking or write in chat, the necessary data is sent to the systems listed under Recipients.",
1668      gdpr_body_v2:"To offer the online booking service, {0} uses some local storage technologies (localStorage) on your device. You can choose which categories to accept. Your choice is recorded and revocable at any time from the ⋯ menu.",
1669      gdpr_more:"Full details", gdpr_accept:"Yes, remember my details", gdpr_deny:"No, thanks",
1670      gdpr_accept_all:"Accept all", gdpr_reject_all:"Reject all", gdpr_customize:"Customize", gdpr_save_prefs:"Save preferences",
1671      gdpr_privacy_policy:"Full Privacy Policy",
1672      gdpr_cat_necessary:"Necessary (always on)",
1673      gdpr_cat_necessary_d:"Technologies essential for the widget to work: regions/offices/services cac
1673he, language. Without these the widget cannot function. Legal basis: strictly necessary for the requested service (exempt from consent).",
1674      gdpr_cat_functional:"Functional",
1675      gdpr_cat_functional_d:"Storage of your booking details (name, tax code, email, phone) to pre-fill them next time. In-progress booking state (resume) and recent history.",
1676      gdpr_cat_analytics:"Analytics and marketing",
1677      gdpr_cat_analytics_d:"Session identifier, campaign parameters (UTM, gclid, fbclid) and collection of booking flow events to improve the service. Pseudonymized data.",
1678      gdpr_info_controller:"Data controller:",
1679      gdpr_info_address:"Registered office:", gdpr_info_email:"Email/PEC:", gdpr_info_phone:"Phone:", gdpr_info_vat:"VAT / Tax ID:", gdpr_info_dpo:"Data Protection Officer (DPO):",
1680      gdpr_info_legal_basis:"Legal basis:", gdpr_info_legal_basis_d:"Necessary = technologies strictly necessary for the requested service (exempt from consent, Art. 122 Italian Privacy Code); booking data is processed to perform the requested service (Art. 6.1.b GDPR). Functional and Analytics = consent (Art. 6.1.a GDPR).",
1681      gdpr_info_storage:"Where stored:", gdpr_info_storage_d:"Pre-fill data, history and preferences stay in your browser's localStorage, on this device (obfuscated but not encrypted with cryptographic standards). When you book or write in chat, the corresponding data is sent to and stored on the processors' servers listed under 'Recipients'.",
1682      gdpr_info_retention:"Retention period:", gdpr_info_retention_d:"On the device: 30 days from last activity, then auto-deletion. On the servers: conversations are deleted and booking logs anonymized within the periods set by the Controller (indicatively: conversations about 180 days; bookings according to applicable accounting/tax obligations). Consent remains valid until revoked.",
1683      gdpr_info_recipients:"Recipients:", gdpr_info_recipients_d:"Booking data is sent to the CAF Planner system (in EU) to finalize the appointment. Conversations are processed by the Mantis conversational platform and connected AI providers, including outside the EU under Standard Contractual Clauses (SCC): conversational assistance and intent classification (Anthropic) and analysis of any uploaded documents (OpenAI). OpenStreetMap/Nominatim and IP geolocation services are used to find the nearest office. Technical delivery: Vercel and Bunny CDN; storage on Google Firebase. See the Controller's Privacy Policy.",
1684      gdpr_info_rights:"Your rights:", gdpr_info_rights_d:"Access, rectification, erasure, restriction, portability, objection (Arts. 15-22 GDPR). Complaint to the Privacy Authority.",
1685      gdpr_info_revoke:"How to revoke:", gdpr_info_revoke_d:"Open the chat → menu (⋯) → Privacy. You can change preferences or delete all data.",
1686      menu_new:"New conversation",
1687      menu_revoke:"Revoke consent and delete data",
1688      menu_privacy:"Privacy and personal data",
1689      revoke_confirm:"Delete saved data (name, tax code, email, phone) and revoke consent?",
1690      revoke_done_h:"Data deleted", revoke_done_s:"Your personal data has been removed from this device.",
1691      privacy_panel_h:"Privacy and your data",
1692      privacy_panel_s:"Here you can change consent preferences, download the data we've stored on your device, or delete it completely.",
1693      privacy_panel_consent_h:"Your consent preferences",
1694      privacy_panel_data_h:"Data currently stored on this device",
1695      privacy_panel_data_client:"Personal data (name, tax code, email, phone)",
1696      privacy_panel_data_history:"Booking history (count)",
1697      privacy_panel_data_resume:"In-progress booking (resume)",
1698      privacy_panel_data_sid:"Session identifier",
1699      privacy_panel_present:"present", privacy_panel_absent:"absent",
1700      privacy_panel_export:"Download my data", privacy_panel_delete_all:"Delete everything",
1701      privacy_panel_export_h:"Download started", privacy_panel_export_s:"Your data has been exported to a JSON file.",
1702      privacy_panel_saved_h:"Preferences saved", privacy_panel_saved_s:"Your consent choices have been updated.",
1703      cf_valid_ok:"Valid tax code.", cf_valid_recognized:"Recognized: {0}, born {1}, municipality (code {2})",
1704      cf_invalid:"Invalid tax code. Check the entered characters.",
1705      cf_male:"♂ Male", cf_female:"♀ Female",
1706      /* EAA/WCAG 2026-07-19: chiavi a11y + i18n wizard/voice */
1707      sr_booking_launch:"A button is available to open the booking.", sr_results_count:"{0} results", sr_no_results:"No results", aria_select_office:"Select {0}",
1708      aria_stop_listening:"Stop listening", sr_listening:"Listening", sr_processing:"Processing", aria_back_to_step:"Back to step {0}: {1}",
1709      step_of:"Step {0} of {1}", cal_months:"January|February|March|April|May|June|July|August|September|October|November|December", cal_months_short:"Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec", cal_days_full:"Sunday|Monday|Tuesday|Wednesday|Thursday|Friday|Saturday",
1710      cal_days_short:"Mon|Tue|Wed|Thu|Fri|Sat|Sun", label_available:"Available", label_selected:"Selected", label_nearest:"Nearest",
1711      label_view_map:"View on map", label_no_slots_h:"No times available", label_no_slots_s:"There are no available slots at the moment. Try another office or try again later.", label_no_dates_h:"Times not available",
1712      label_no_dates_s:"I could not read the available dates. Try another office.", label_pick_day:"Select a day", label_no_slots_day:"No times on this day. Try another available day.", ph_no_office_found:"No office found for \"{0}\"",
1713      ph_no_results_generic:"No results for \"{0}\"", label_reuse_data_h:"Reuse your saved details?", btn_its_me:"Yes, it's me", btn_not_me:"No, I'm someone else",
1714      label_saved_data:"Saved details", label_submitting:"Sending booking...", label_submitting_sub:"I'm registering your appointment, it will take a few seconds.", label_loading_hint:"Looking for availability for \"{0}\"...",
1715      label_loading_options:"Loading options...", label_loading_short:"Loading…", label_error_code:"Code: {0}", label_booking_inprogress:"Booking in progress",
1716      label_expand:"Expand", label_expand_sub:"Open the full screen view for a better experience", t_region:"Select your region", t_province:"Select your province",
1717      t_data:"Your details", label_srv_hint_found:"I found these services for \"{0}\". Which one are you interested in?", label_srv_hint:"Which service would you like to book?", label_sede_hint:"Great! Now choose the most convenient office for you.",
1718      btn_find_nearest_sub:"Use your GPS position", btn_find_nearest_again_sub:"Use your updated position", label_slots_hint:"Here are the available appointments. Pick the one you prefer!", label_pref_time:"You usually prefer: {0}.",
1719      label_data_hint:"Almost done! Enter your details to complete the booking.", label_date_at:"{0} at {1}", label_appointment:"Appointment", btn_use:"Use",
1720      btn_calendar:"Calendar", btn_email_reminder:"Send me the reminder by email", btn_choose_other:"Choose another", btn_choose_other_slot:"Choose another time",
1721      label_done_email:"Booking registered for {0}", err_cf_required:"Enter your tax code.", err_cf_short:"The tax code must be 16 characters long ({0} characters missing).", err_cf_long:"The tax code must be 16 characters long ({0} too many).",
1722      err_cf_chars:"The tax code can only contain letters and digits.", err_cf_format:"The tax code format is not valid. Check the characters you entered.", err_cf_charpos:"Invalid character at position {0}.", err_cf_checksum:"The check digit does not match (expected '{0}' instead of '{1}'). Please check.",
1723      err_cf_first:"Enter a valid tax code first", err_cf_proceed:"Enter a valid tax code before proceeding.", err_required_fields:"Fill in all required fields.", err_email_invalid:"Enter a valid email address.",
1724      err_phone_invalid:"Enter a valid mobile number (at least 9 digits).", err_uncertain:"We did not receive a confirmation from the server: your booking may still have gone through. Check your email before trying again.", err_unexpected:"Unexpected error. Please try again.", help_codfis:"You can find it on your Italian health card. It is 16 characters long.",
1725      help_email:"We will only use it to send you the confirmation and the reminder 24h before.", help_cellulare:"For urgent communications from the office.", voice_tap_to_talk:"Tap to talk", voice_press_to_start:"Press the microphone to start",
1726      voice_speak_now:"Speak now", voice_listening_hint:"I'm listening... go ahead", voice_responding:"I'm answering", voice_tap_interrupt:"Tap the microphone to interrupt",
1727      voice_thinking:"Thinking...", voice_thinking_hint:"One moment, I'm preparing the answer", voice_back_keyboard:"Back to keyboard", ph_your_city:"Type your city...", ph_follow_steps:"Follow the steps above to book..."
1728    },
1729    fr:{
1730      /* --- GDPR / privacy (i18n completo, art. 12-13 GDPR) --- */
1731      gdpr_accept_all:"Tout accepter",
1732      gdpr_body_v2:"Pour vous proposer le service de réservation en ligne, {0} utilise certaines technologies de stockage local (localStorage) sur votre appareil. Vous pouvez choisir les catégories à accepter. Votre choix est enregistré et révocable à tout moment depuis le menu ⋯.",
1733      gdpr_cat_analytics:"Analytique et marketing",
1734      gdpr_cat_analytics_d:"Identifiant de session, paramètres de campagne (UTM, gclid, fbclid) et collecte des événements du parcours de réservation afin d'améliorer le service. Données pseudonymisées.",
1735      gdpr_cat_functional:"Fonctionnels",
1736      gdpr_cat_functional_d:"Mémorisation de vos données de réservation (nom, code fiscal, e-mail, téléphone) afin de les pré-remplir la prochaine fois. L'état de la réservation en cours (reprise) et l'historique récent.",
1737      gdpr_cat_necessary:"Nécessaires (toujours actifs)",
1738      gdpr_cat_necessary_d:"Technologies indispensables au fonctionnement du widget : cache des régions/agences/services, langue. Sans elles, le widget ne fonctionne pas. Base juridique : strictement nécessaires au service demandé (exemptées de consentement).",
1739      gdpr_customize:"Personnaliser",
1740      gdpr_info_address:"Siège :",
1741      gdpr_info_controller:"Responsable du traitement :",
1742      gdpr_info_dpo:"Délégué à la protection des données (DPO) :",
1743      gdpr_info_email:"E-mail/PEC :",
1744      gdpr_info_legal_basis:"Base juridique :",
1745      gdpr_info_legal_basis_d:"Nécessaires = technologies strictement nécessaires au service demandé (exemptées de consentement, art. 122 de la réglementation nationale applicable) ; les données de réservation sont traitées pour exécuter le service demandé (art. 6.1.b GDPR). Fonctionnels et Analytique = consentement (art. 6.1.a GDPR).",
1746      gdpr_info_phone:"Téléphone :",
1747      gdpr_info_recipients:"Destinataires :",
1748      gdpr_info_recipients_d:"Les données de réservation sont envoyées au système Planner CAF (dans l'UE) pour finaliser le rendez-vous. Les conversations sont traitées par la plateforme conversationnelle Mantis et les fournisseurs d'IA connectés, y compris hors UE au titre de clauses contractuelles types (SCC) : assistance conversationnelle et 
1748classification des intentions (Anthropic) et analyse des éventuels documents téléversés (OpenAI). Pour localiser l'agence la plus proche, on utilise OpenStreetMap/Nominatim et des services de géolocalisation par IP. Distribution technique : Vercel et Bunny CDN ; conservation sur Google Firebase. Consultez la Politique de confidentialité du Responsable du traitement.",
1749      gdpr_info_retention:"Durée de conservation :",
1750      gdpr_info_retention_d:"Sur l'appareil : 30 jours à compter de la dernière activité, puis suppression automatique. Sur les serveurs : les conversations sont supprimées et les journaux de réservation anonymisés dans les délais fixés par le Responsable du traitement (à titre indicatif : conversations environ 180 jours ; réservations selon les obligations comptables/fiscales applicables). Le consentement reste valable jusqu'à sa révocation.",
1751      gdpr_info_revoke:"Comment révoquer :",
1752      gdpr_info_revoke_d:"Ouvrez le chat → menu (⋯) → Confidentialité. Vous pouvez modifier les préférences ou supprimer toutes les données.",
1753      gdpr_info_rights:"Vos droits :",
1754      gdpr_info_rights_d:"Accès, rectification, effacement, limitation, portabilité, opposition (artt. 15-22 GDPR). Réclamation auprès de l'autorité de contrôle compétente.",
1755      gdpr_info_storage:"Où sont-elles conservées :",
1756      gdpr_info_storage_d:"Les données de pré-remplissage, l'historique et les préférences restent dans le localStorage de votre navigateur, sur cet appareil (masquées mais non chiffrées selon des standards cryptographiques). Lorsque vous réservez ou écrivez dans le chat, les données correspondantes sont envoyées et conservées sur les serveurs des sous-traitants indiqués dans 'Destinataires'.",
1757      gdpr_info_vat:"N° TVA / Code fiscal :",
1758      gdpr_privacy_policy:"Politique de confidentialité complète",
1759      gdpr_reject_all:"Tout refuser",
1760      gdpr_save_prefs:"Enregistrer les préférences",
1761      menu_privacy:"Confidentialité et données personnelles",
1762      privacy_panel_absent:"absentes",
1763      privacy_panel_consent_h:"Vos préférences de consentement",
1764      privacy_panel_data_client:"Données d'identité (nom, code fiscal, e-mail, téléphone)",
1765      privacy_panel_data_h:"Données actuellement mémorisées sur cet appareil",
1766      privacy_panel_data_history:"Historique des réservations (nombre)",
1767      privacy_panel_data_resume:"Réservation en cours (reprise)",
1768      privacy_panel_data_sid:"Identifiant de session",
1769      privacy_panel_delete_all:"Tout supprimer",
1770      privacy_panel_export:"Télécharger mes données",
1771      privacy_panel_export_h:"Téléchargement lancé",
1772      privacy_panel_export_s:"Vos données ont été exportées dans un fichier JSON.",
1773      privacy_panel_h:"Confidentialité et vos données",
1774      privacy_panel_present:"présentes",
1775      privacy_panel_s:"Ici, vous pouvez modifier les préférences de consentement, télécharger les données que nous avons mémorisées sur votre appareil, ou les supprimer complètement.",
1776      privacy_panel_saved_h:"Préférences enregistrées",
1777      privacy_panel_saved_s:"Vos choix de consentement ont été mis à jour.",
1778      /* a11y accessible names */ aria_open_chat:"Ouvrir le chat", aria_close_chat:"Fermer le chat", ph_write_message:"Écrire un message", aria_send:"Envoyer le message", aria_scroll_bottom:"Aller au dernier message", aria_options:"Options", aria_close:"Fermer", aria_fullscreen:"Plein écran", aria_exit_fullscreen:"Quitter le plein écran", aria_voice:"Reconnaissance vocale", aria_book:"Ouvrir la réservation", aria_open_book:"Ouvrir le chat et prendre rendez-vous", skip_to_input:"Aller au champ de message", aria_history:"Historique de la conversation", sr_assistant:"Assistant", sr_you_said:"Vous avez écrit :", sr_said:"a écrit :", assistant_typing:"L'assistant écrit…", label_error:"Erreur", booking_confirmed:"Réservation confirmée", menu_contact:"Parler à un opérateur", aria_email:"Adresse e-mail", aria_pwd_email:"E-mail de réinitialisation du mot de passe", aria_view_on_map:"Voir {0} sur la carte", aria_copy:"Copier le message", aria_translate:"Traduire le message", aria_scroll_left:"Défiler à gauche", aria_scroll_right:"Défiler à droite", aria_remove:"Retirer", aria_add:"Ajouter", aria_speak:"Parler", aria_more_info:"Plus d'infos", aria_hide:"Masquer", aria_expand_booking:"Ouvrir la réservation en plein écran", aria_book_appointment:"Prendre rendez-vous", aria_booking:"Réservation", aria_search_office:"Rechercher un bureau",
1779      hero_title:"Réservez votre rendez-vous",
1780      hero_subtitle:"⏱ ~2 minutes · ",
1781      step_zone:"Zone", step_service:"Service", step_office:"Bureau", step_time:"Horaire", step_data:"Données",
1782      h_region:"Dans quelle région voulez-vous réserver ?", s_region:"Choisissez votre région pour voir les bureaux et services disponibles.",
1783      h_province:"Dans quelle province voulez-vous réserver ?", s_province:"Utilisez votre position pour la trouver automatiquement ou choisissez manuellement.",
1784      h_service:"Choisissez le service", s_service:"Sélectionnez le service que vous souhaitez réserver.",
1785      h_service_hint:"Quel service vous intéresse ?", s_service_hint:'Filtré par "{0}". Vous pouvez affiner la recherche ci-dessous.',
1786      h_office:"Choisissez le bureau", s_office:"Voici tous les bureaux disponibles. Utilisez la géolocalisation pour trouver le plus proche.",
1787      h_slot:"Choisissez date et heure", s_slot:"Sélectionnez un jour sur le calendrier pour voir les créneaux disponibles.",
1788      h_data:"Entrez vos données", s_data:"Presque fini ! Nous avons besoin des données du titulaire.",
1789      h_confirm:"Confirmez votre réservation", s_confirm:"Vérifiez les détails. Si tout est correct, appuyez sur Confirmer.",
1790      btn_back:"Retour", btn_cancel:"Annuler", btn_confirm:"Confirmer la réservation", btn_confirming:"Enregistrement...",
1791      consent_before:"J'ai lu la ", consent_link:"politique de confidentialité", consent_after:" et je consens au traitement de mes données personnelles pour gérer le rendez-vous.", consent_required:"Pour continuer, vous devez accepter le traitement de vos données personnelles.",
1792      chatgate_h:"Votre vie privée", chatgate_body:"Avant de commencer la conversation, veuillez lire la politique de confidentialité et consentir au traitement de vos données personnelles.", chatgate_before:"J'ai lu la ", chatgate_link:"politique de confidentialité", chatgate_after:" et je consens au traitement de mes données personnelles.", chatgate_btn:"Accepter et commencer",
1793      ai_disclosure:"Vous discutez avec un assistant virtuel basé sur l'intelligence artificielle (IA), et non avec une personne. Les réponses sont générées automatiquement et peuvent contenir des erreurs : vérifiez les informations importantes auprès d'un opérateur. Votre dossier sera traité par le personnel humain du CAF.",
1794      ai_badge:"Assistant IA · réponses générées automatiquement", ai_voice_notice:"Vous parlez avec un assistant vocal automatique basé sur l'intelligence artificielle. Les réponses sont générées automatiquement et peuvent contenir des erreurs.",
1795      email_gate_body:"Pour commencer la conversation, saisissez votre adresse e-mail.", email_gate_ph:"Votre e-mail", email_gate_btn:"Démarrer le chat", email_gate_note:"Votre e-mail sert uniquement à gérer cette c
1795onversation et à donner suite à votre demande.", email_gate_privacy:"Politique de confidentialité", aria_prev_month:"Mois précédent", aria_next_month:"Mois suivant", hold_hint:"Nous vous conseillons de terminer dans un délai de {0}", hold_expired:"Les horaires ont pu changer entre-temps : vérifiez la disponibilité avant de confirmer.",
1796      age_gate_label:"Je déclare avoir au moins 14 ans. Pour les mineurs de moins de 14 ans, le consentement d'un parent ou du représentant légal est requis.",
1797      translate_ai_note:"Traduction automatique générée par IA", demo_ribbon:"DÉMO — les données affichées sont des exemples. Aucune réservation réelle n'est effectuée.",
1798      btn_retry:"Réessayer", btn_change:"Changer de sélection", btn_call:"Appeler", btn_restart:"Recommencer", btn_close:"Fermer", btn_edit:"Modifier",
1799      btn_use_location:"Utiliser ma position", btn_use_location_sub:"Je trouve votre province automatiquement",
1800      btn_find_nearest:"Trouver le bureau le plus proche", btn_find_nearest_again:"Rechercher à nouveau",
1801      btn_next:"Continuer vers le récapitulatif", btn_load_more:"Charger plus d'horaires",
1802      label_or_choose:"ou choisissez manuellement",
1803      label_morning:"Matin", label_afternoon:"Après-midi",
1804      label_today:"AUJOURD'HUI", label_tomorrow:"DEMAIN", label_this_week:"CETTE SEMAINE",
1805      label_count_slots_one:"{0} créneau", label_count_slots_other:"{0} créneaux",
1806      label_first_avail:"Première disponibilité", label_quickpick_arrow:"→",
1807      label_when:"Quand", label_where:"Où", label_what:"Quoi",
1808      label_owner:"Titulaire", label_contacts:"Contacts",
1809      label_summary_premium_h:"Tout est prêt, vérifiez et confirmez",
1810      label_summary_premium_s:"Vérifiez les détails avant d'enregistrer.",
1811      label_summary_disclaimer:"Vous pourrez modifier ou annuler jusqu'à 24h avant le rendez-vous.",
1812      label_form_for_self:"Pour moi", label_form_for_family:"Pour un proche",
1813      label_form_nome:"Prénom", label_form_cognome:"Nom", label_form_codfis:"Code fiscal", label_form_email:"E-mail", label_form_cellulare:"Mobile",
1814      label_form_duration_min:"Durée ~{0} min",
1815      label_data_safe:"Données protégées", label_data_caf:"Assistance CAF", label_data_real:"Rendez-vous avec de vrais opérateurs du CAF",
1816      label_done_h:"Réservation confirmée !", label_done_s:"Nous vous avons envoyé un e-mail de confirmation avec tous les détails.",
1817      label_done_id:"ID de réservation : ",
1818      label_add_calendar:"Ajouter au calendrier", label_share_whatsapp:"Partager sur WhatsApp", label_email_reminder:"Rappel par e-mail",
1819      label_done_survey:"Comment s'est passée l'expérience ?", label_done_thanks:"Merci pour votre retour !",
1820      label_done_good:"Bien", label_done_meh:"Bof", label_done_bad:"Mauvais",
1821      ph_search_service:"Rechercher (ex. impôts, ISEE, maison...)",
1822      ph_search_office:"Rechercher bureau par nom, ville ou adresse",
1823      ph_search_province:"Rechercher ville ou province",
1824      ph_no_results_one:'Aucun service trouvé pour "{0}"',
1825      label_did_you_mean:"Vouliez-vous dire :",
1826      label_popular:"🔥 Plus demandé", label_recent:"⏱ Récent", label_frequent:"⭐ Fréquent",
1827      label_resume_h:"Reprendre là où vous étiez",
1828      label_resume_btn:"Reprendre", label_resume_discard:"Recommencer",
1829      label_offline_b:"Vous êtes hors ligne.", label_offline_msg:" Connectez-vous pour terminer la réservation. L'état est sauvegardé.",
1830      label_slow:"Connexion lente — le chargement peut prendre quelques secondes de plus.",
1831      err_h_default:"Une erreur s'est produite", err_s_default:"Réessayez ou appelez le numéro vert pour assistance.",
1832      err_h_timeout:"Connexion lente", err_s_timeout:"Le serveur ne répond pas. Vérifiez la connexion et réessayez.",
1833      err_h_network:"Pas de connexion", err_s_network:"Vous semblez hors ligne. Vérifiez le réseau et réessayez.",
1834      err_h_no_services:"Aucun service disponible", err_s_no_services:"Aucun service réservable en ligne pour votre province en ce moment.",
1835      err_h_no_sedi:"Aucun bureau disponible", err_s_no_sedi:"Aucun bureau configuré pour ce service dans votre province.",
1836      err_h_no_spazi:"Aucun créneau disponible", err_s_no_spazi:"Aucun horaire libre pour le moment. Réessayez ou changez de bureau.",
1837      err_h_already_booked:"Vous avez déjà une réservation", err_s_already_booked:"Vous êtes déjà inscrit pour un service à ces dates.",
1838      err_h_slot_taken:"Créneau qui vient d'être pris", err_s_slot_taken:"L'horaire choisi vient d'être réservé par quelqu'un d'autre. Choisissez un autre horaire dans la liste mise à jour.",
1839      err_h_validation:"Données invalides", err_s_validation:"Vérifiez les champs saisis et réessayez.",
1840      err_h_disabled:"Service indisponible", err_s_disabled:"La réservation en ligne est temporairement désactivée.",
1841      err_h_server:"Erreur serveur", err_s_server:"Réponse invalide du serveur. Réessayez bientôt.",
1842      toast_slow_title:"Connexion lente", toast_slow_msg:"Nouvelle tentative ({0}/3)...",
1843      gdpr_h:"Voulez-vous accélérer vos prochaines réservations ?",
1844      gdpr_body:"Nous pouvons mémoriser vos données (nom, code fiscal, e-mail, téléphone) sur cet appareil pour les pré-remplir la prochaine fois. Les données restent dans votre navigateur et s'effacent après 30 jours d'inactivité.",
1845      gdpr_more:"Plus d'informations", gdpr_accept:"Oui, mémoriser mes données", gdpr_deny:"Non, merci",
1846      menu_new:"Nouvelle conversation",
1847      menu_revoke:"Révoquer le consentement et supprimer les données",
1848      revoke_confirm:"Supprimer les données enregistrées (nom, code fiscal, e-mail, téléphone) et révoquer le consentement ?",
1849      revoke_done_h:"Données supprimées", revoke_done_s:"Vos données personnelles ont été supprimées de cet appareil.",
1850      cf_valid_ok:"Code fiscal valide.", cf_valid_recognized:"Reconnu : {0}, né(e) le {1}, commune (code {2})",
1851      cf_invalid:"Code fiscal invalide. Vérifiez les caractères saisis.",
1852      cf_male:"♂ Homme", cf_female:"♀ Femme",
1853      /* EAA/WCAG 2026-07-19: chiavi a11y + i18n wizard/voice */
1854      sr_booking_launch:"Un bouton est disponible pour ouvrir la réservation.", sr_results_count:"{0} résultats", sr_no_results:"Aucun résultat", aria_select_office:"Sélectionner {0}",
1855      aria_stop_listening:"Arrêter l'écoute", sr_listening:"Écoute en cours", sr_processing:"Traitement en cours", aria_back_to_step:"Revenir à l'étape {0} : {1}",
1856      step_of:"Étape {0} sur {1}", cal_months:"Janvier|Février|Mars|Avril|Mai|Juin|Juillet|Août|Septembre|Octobre|Novembre|Décembre", cal_months_short:"janv|févr|mars|avr|mai|juin|juil|août|sept|oct|nov|déc", cal_days_full:"Dimanche|Lundi|Mardi|Mercredi|Jeudi|Vendredi|Samedi",
1857      cal_days_short:"Lun|Mar|Mer|Jeu|Ven|Sam|Dim", label_available:"Disponible", label_selected:"Sélectionné", label_nearest:"La plus proche",
1858      label_view_map:"Voir sur la carte", label_no_slots_h:"Aucun horaire disponible", label_no_slots_s:"Il n'y a pas de créneaux disponibles pour le moment. Essayez un autre bureau ou réessayez plus tard.", label_no_dates_h:"Horaires indisponibles",
1859      label_no_dates_s:"Je n'ai pas pu lire les dates disponibles. Essayez un autre bureau.", label_pick_day:"Sélectionnez un jour", label_no_slots_day:"Aucun horaire ce jour-là. Essayez un autre jour disponible.", ph_no_office_found:"Aucun bureau trouvé pour « {0} »",
1860      ph_no_results_generic:"Aucun résultat pour « {0} »", label_reuse_data_h:"Réutiliser vos données enregistrées ?", btn_its_me:"Oui, c'est moi", btn_not_me:"Non, je suis quelqu'un d'autre",
1861      label_saved_data:"Données enregistrées", label_submitting:"Envoi de la réservation...", label_submitting_sub:"J'enregistre votre rendez-vous, cela prendra quelques secondes.", label_loading_hint:"Je cherche des disponibilités pour « {0} »...",
1862      label_loading_options:"Chargement des options...", label_loading_short:"Chargement…", label_error_code:"Code : {0}", label_booking_inprogress:"Réservation en cours",
1863      label_expand:"Agrandir", label_expand_sub:"Ouvrez la vue plein écran pour une meilleure expérience", t_region:"Sélectionnez la région", t_province:"Sélectionnez la province",
1864      t_data:"Vos données", label_srv_hint_found:"J'ai trouvé ces services pour « {0} ». Lequel vous intéresse ?", label_srv_hint:"Quel service souhaitez-vous réserver ?", label_sede_hint:"Parfait ! Choisissez maintenant le bureau le plus pratique pour vous.",
1865      btn_find_nearest_sub:"Utiliser votre position GPS", btn_find_nearest_again_sub:"Utiliser la position mise à jour", label_slots_hint:"Voici les rendez-vous disponibles. Choisissez celui que vous préférez !", label_pref_time:"Vous préférez généralement : {0}.",
1866      label_data_hint:"Presque terminé ! Saisissez vos données pour finaliser la réservation.", label_date_at:"{0} à {1}", label_appointment:"Rendez-vous", btn_use:"Utiliser",
1867      btn_calendar:"Calendrier", btn_email_reminder:"M'envoyer le rappel par e-mail", btn_choose_other:"En choisir un autre", btn_choose_other_slot:"Choisir un autre horaire",
1868      label_done_email:"Réservation enregistrée pour {0}", err_cf_required:"Saisissez le code fiscal.", err_cf_short:"Le code fiscal doit comporter 16 caractères (il en manque {0}).", err_cf_long:"Le code fiscal doit comporter 16 caractères ({0} en trop).",
1869      err_cf_chars:"Le code fiscal ne peut contenir que des lettres et des chiffres.", err_cf_format:"Le format du code fiscal n'est pas valide. Vérifiez les caractères saisis.", err_cf_charpos:"Caractère non valide à la position {0}.", err_cf_checksum:"La clé de contrôle ne correspond pas ('{0}' attendu à la place de '{1}'). Vérifiez.",
1870      err_cf_first:"Saisissez d'abord un code fiscal valide", err_cf_proceed:"Saisissez un code fiscal valide avant de continuer.", err_required_fields:"Remplissez tous les champs obligatoires.", err_email_invalid:"Saisissez une adresse e-mail valide.",
1871      err_phone_invalid:"Saisissez un numéro de portable valide (au moins 9 chiffres).", err_uncertain:"Nous n'avons pas reçu de confirmation du serveur : votre réservation a peut-être quand même abouti. Vérifiez votre e-mail avant de réessayer.", err_unexpected:"Erreur inattendue. Réessayez.", help_codfis:"Vous le trouverez sur votre carte de santé. Il comporte 16 caractères.",
1872      help_email:"Utilisée uniquement pour la confirmation et le rappel 24 h avant.", help_cellulare:"Pour toute communication urgente du bureau.", voice_tap_to_talk:"Touchez pour parler", voice_press_to_start:"Appuyez sur le microphone pour commencer",
1873      voice_speak_now:"Parlez maintenant", voice_listening_hint:"Je vous écoute... parlez", voice_responding:"Je réponds", voice_tap_interrupt:"Touchez le microphone pour interrompre",
1874      voice_thinking:"Je réfléchis...", voice_thinking_hint:"Un instant, je prépare la réponse", voice_back_keyboard:"Retour au clavier", ph_your_city:"Écrivez votre ville...", ph_follow_steps:"Suivez les étapes ci-dessus pour réserver..."
1875    },
1876    es:{
1877      /* --- GDPR / privacy (i18n completo, art. 12-13 GDPR) --- */
1878      gdpr_accept_all:"Aceptar todo",
1879      gdpr_body_v2:"Para ofrecerte el servicio de reserva en línea, {0} utiliza algunas tecnologías de almacenamiento local (localStorage) en tu dispositivo. Puedes elegir qué categorías aceptar. Tu elección queda registrada y es revocable en cualquier momento desde el menú ⋯.",
1880      gdpr_cat_analytics:"Analítica y marketing",
1881      gdpr_cat_analytics_d:"Identificador de sesión, parámetros de campaña (UTM, gclid, fbclid) y recopilación de los eventos del flujo de reserva para mejorar el servicio. Datos seudonimizados.",
1882      gdpr_cat_functional:"Funcionales",
1883      gdpr_cat_functional_d:"Almacenamiento de tus datos de reserva (nombre, código fiscal, correo electrónico, teléfono) para rellenarlos previamente la próxima vez. El estado de la reserva en curso (reanudación) y el historial reciente.",
1884      gdpr_cat_necessary:"Necesarias (siempre activas)",
1885      gdpr_cat_necessary_d:"Tecnologías imprescindibles para el funcionamiento del widget: caché de regiones/sedes/servicios, idioma. Sin ellas el widget no funciona. Base jurídica: estrictamente necesarias para el servicio solicitado (exentas de consentimiento).",
1886      gdpr_customize:"Personalizar",
1887      gdpr_info_address:"Sede:",
1888      gdpr_info_controller:"Responsable del tratamiento:",
1889      gdpr_info_dpo:"Delegado de protección de datos (DPO):",
1890      gdpr_info_email:"Correo electrónico/PEC:",
1891      gdpr_info_legal_basis:"Base jurídica:",
1892      gdpr_info_legal_basis_d:"Necesarias = tecnologías estrictamente necesarias para el servicio solicitado (exentas de consentimiento, art. 122 de la normativa nacional aplicable); los datos de reserva se tratan para ejecutar el servicio solicitado (art. 6.1.b GDPR). Funcionales y Analítica = consentimiento (art. 6.1.a GDPR).",
1893      gdpr_info_phone:"Teléfono:",
1894      gdpr_info_recipients:"Destinatarios:",
1895      gdpr_info_recipients_d:"Los datos de reserva se envían al sistema Planner CAF (en la UE) para finalizar la cita. Las conversaciones son procesadas por la plataforma conversacional Mantis y los proveedores de IA conectados, incluso fuera de la UE mediante cláusulas contractuales tipo (SCC): asistencia conversacional y clasificación de intenciones (Anthropic) y análisis de los eventuales documentos cargados (OpenAI). Para localizar la sede más cercana se utilizan OpenStreetMap/Nominatim y servicios de geolocalización por IP. Distribución técnica: Vercel y Bunny CDN; conservación en Google Firebase. Consulta la Política de Privacidad del Responsable del tratamiento.",
1896      gdpr_info_retention:"Periodo de conservación:",
1897      gdpr_info_retention_d:"En el dispositivo: 30 días desde la última actividad, después eliminación automática. En los servidores: las conversaciones se eliminan y los registros de reserva se anonimizan dentro de los plazos establecidos por el Responsable del tratamiento (a título indicativo: conversaciones unos 180 días; reservas según las obligaciones contables/fiscales aplicables). El consentimiento sigue siendo válido hasta su revocación.",
1898      gdpr_info_revoke:"Cómo revocar:",
1899      gdpr_info_revoke_d:"Abre el chat → menú (⋯) → Privacidad. Puedes modificar las preferencias o eliminar todos los datos.",
1900      gdpr_info_rights:"Tus derechos:",
1901      gdpr_info_rights_d:"Acceso, rectificación, supresión, limitación, portabilidad, oposición (artt. 15-22 GDPR). Reclamación ante la autoridad de control competente.",
1902      gdpr_info_storage:"Dónde se conservan:",
1903      gdpr_info_storage_d:"Los datos de rellenado previo, el historial y las preferencias permanecen en el localStorage de tu navegador, en este dispositivo (ofuscados pero no cifrados con estándares criptográficos). Cuando reservas o escribes en el chat, los datos correspondientes se envían y se conservan en los servidores de los encargados del tratamiento indicados en 'Destinatarios'.",
1904      gdpr_info_vat:"N.º de IVA / Código fiscal:",
1905      gdpr_privacy_policy:"Política de Privacidad completa",
1906      gdpr_reject_all:"Rechazar todo",
1907      gdpr_save_prefs:"Guardar preferencias",
1908      menu_privacy:"Privacidad y datos personales",
1909      privacy_panel_absent:"ausentes",
1910      privacy_panel_consent_h:"Tus preferencias de consentimiento",
1911      privacy_panel_data_client:"Datos personales (nombre, código fiscal, correo electrónico, teléfono)",
1912      privacy_panel_data_h:"Datos actualmente almacenados en este dispositivo",
1913      privacy_panel_data_history:"Historial de reservas (número)",
1914      privacy_panel_data_resume:"Reserva en curso (reanudación)",
1915      privacy_panel_data_sid:"Identificador de sesión",
1916      privacy_panel_delete_all:"Eliminar todo",
1917      privacy_panel_export:"Descargar mis datos",
1918      privacy_panel_export_h:"Descarga iniciada",
1919      privacy_panel_export_s:"Tus datos se han exportado a un archivo JSON.",
1920      privacy_panel_h:"Privacidad y tus datos",
1921      privacy_panel_present:"presentes",
1922      privacy_panel_s:"Aquí puedes modificar las preferencias de consentimiento, descargar los datos que hemos almacenado en tu dispositivo o eliminarlos por completo.",
1923      privacy_panel_saved_h:"Preferencias guardadas",
1924      privacy_panel_saved_s:"Tus opciones de consentimiento se han actualizado.",
1925      /* a11y accessible names */ aria_open_chat:"Abrir el chat", aria_close_chat:"Cerrar el chat", ph_write_message:"Escribe un mensaje", aria_send:"Enviar mensaje", aria_scroll_bottom:"Ir al último mensaje", aria_options:"Opciones", aria_close:"Cerrar", aria_fullscreen:"Pantalla completa", aria_exit_fullscreen:"Salir de pantalla completa", aria_voice:"Reconocimiento de voz", aria_book:"Abrir la reserva", aria_open_book:"Abrir el chat y reservar una cita", skip_to_input:"Ir al campo de mensaje", aria_history:"Historial de conversación", sr_assistant:"Asistente", sr_you_said:"Tú dijiste:", sr_said:"dijo:", assistant_typing:"El asistente está escribiendo…", label_error:"Error", booking_confirmed:"Reserva confirmada", menu_contact:"Hablar con un operador", aria_email:"Dirección de correo electrónico", aria_pwd_email:"Correo para restablecer la contraseña", aria_view_on_map:"Ver {0} en el mapa", aria_copy:"Copiar mensaje", aria_translate:"Traducir mensaje", aria_scroll_left:"Desplazar a la izquierda", aria_scroll_right:"Desplazar a la derecha", aria_remove:"Quitar", aria_add:"Añadir", aria_speak:"Hablar", aria_more_info:"Más información", aria_hide:"Ocultar", aria_expand_booking:"Abrir la reserva en pantalla completa", aria_book_appointment:"Reserva tu cita", aria_booking:"Reserva", aria_search_office:"Buscar oficina",
1926      hero_title:"Reserva tu cita",
1927      hero_subtitle:"⏱ ~2 minutos · ",
1928      step_zone:"Zona", step_service:"Servicio", step_office:"Sede", step_time:"Horario", step_data:"Datos",
1929      h_region:"¿En qué región quieres reservar?", s_region:"Elige tu región para ver las sedes y servicios disponibles.",
1930      h_province:"¿En qué provincia quieres reservar?", s_province:"Usa tu ubicación para encontrarla automáticamente o elige manualmente.",
1931      h_service:"Elige el servicio", s_service:"Selecciona el servicio que quieres reservar.",
1932      h_service_hint:"¿Qué servicio te interesa?", s_service_hint:'Filtrado por "{0}". Puedes afinar la búsqueda abajo.',
1933      h_office:"Elige la sede", s_office:"Aquí están todas las sedes disponibles. Usa la geolocalización para encontrar la más cercana.",
1934      h_slot:"Elige fecha y hora", s_slot:"Selecciona un día en el calendario para ver los horarios disponibles.",
1935      h_data:"Introduce tus datos", s_data:"¡Casi listo! Necesitamos los datos del titular para finalizar.",
1936      h_confirm:"Confirma tu reserva", s_confirm:"Revisa los detalles. Si todo es correcto, pulsa Confirmar.",
1937      btn_back:"Atrás", btn_cancel:"Cancelar", btn_confirm:"Confirmar reserva", btn_confirming:"Guardando...",
1938      consent_before:"He leído la ", consent_link:"política de privacidad", consent_after:" y consiento el tratamiento de mis datos personales para gestionar la reserva.", consent_required:"Para continuar debes aceptar el tratamiento de tus datos personales.",
1939      chatgate_h:"Tu privacidad", chatgate_body:"Antes de iniciar la conversación, lee la política de privacidad y acepta el tratamiento de tus datos personales.", chatgate_before:"He leído la ", chatgate_link:"política de privacidad", chatgate_after:" y consiento el tratamiento de mis datos personales.", chatgate_btn:"Aceptar y empezar",
1940      ai_disclosure:"Estás hablando con un asistente virtual basado en inteligencia artificial (IA), no con una persona. Las respuestas se generan automáticamente y pueden contener errores: verifica la información importante con un operador. Tu gestión será atendida por personal humano del CAF.",
1941      ai_badge:"Asistente IA · respuestas generadas automáticamente", ai_voice_notice:"Estás hablando con un asistente de voz automático basado en inteligencia artificial. Las respuestas se generan automáticamente y pueden contener errores.",
1942      email_gate_body:"Para iniciar la conversación, introduce tu dirección de correo electrónico.", email_gate_ph:"Tu correo electrónico", email_gate_btn:"Iniciar el chat", email_gate_note:"Solo usamos tu correo para gestionar esta conversación y responder a tu solicitud.", email_gate_privacy:"Política de privacidad", aria_prev_month:"Mes anterior", aria_next_month:"Mes siguiente", hold_hint:"Te recomendamos completar en un plazo de {0}", hold_expired:"Los horarios pueden haber cambiado mientras tanto: comprueba la disponibilidad antes de confirmar.",
1943      age_gate_label:"Declaro que tengo al menos 14 años. Para los menores de 14 años se requiere el consentimiento de un progenitor o tutor legal.",
1944      translate_ai_note:"Traducción automática generada por IA", demo_ribbon:"DEMO — los datos mostrados son de ejemplo. No se realiza ninguna reserva real.",
1945      btn_retry:"Reintentar", btn_change:"Cambiar selección", btn_call:"Llamar", btn_restart:"Empezar de nuevo", btn_close:"Cerrar", btn_edit:"Modificar",
1946      btn_use_location:"Usar mi ubicación", btn_use_location_sub:"Encuentro tu provincia automáticamente",
1947      btn_find_nearest:"Encuentra la sede más cercana", btn_find_nearest_again:"Buscar de nuevo",
1948      btn_next:"Continuar al resumen", btn_load_more:"Cargar más horarios",
1949      label_or_choose:"o elige manualmente",
1950      label_morning:"Mañana", label_afternoon:"Tarde",
1951      label_today:"HOY", label_tomorrow:"MAÑANA", label_this_week:"ESTA SEMANA",
1952      label_count_slots_one:"{0} hora", label_count_slots_other:"{0} horas",
1953      label_first_avail:"Primera disponibilidad", label_quickpick_arrow:"→",
1954      label_when:"Cuándo", label_where:"Dónde", label_what:"Qué",
1955      label_owner:"Titular", label_contacts:"Contactos",
1956      label_summary_premium_h:"Todo listo, revisa y confirma",
1957      label_summary_premium_s:"Verifica los detalles antes de guardar.",
1958      label_summary_disclaimer:"Podrás modificar o cancelar la reserva hasta 24 horas antes de la cita.",
1959      label_form_for_self:"Para mí", label_form_for_family:"Para un familiar",
1960      label_form_nome:"Nombre", label_form_cognome:"Apellido", label_form_codfis:"Código fiscal", label_form_email:"Email", label_form_cellulare:"Móvil",
1961      label_form_duration_min:"Duración ~{0} min",
1962      label_data_safe:"Datos protegidos", label_data_caf:"Soporte CAF", label_data_real:"Cita con operadores reales del CAF",
1963      label_done_h:"¡Reserva confirmada!", label_done_s:"Te hemos enviado un correo de confirmación con todos los detalles.",
1964      label_done_id:"ID de reserva: ",
1965      label_add_calendar:"Añadir al calendario", label_share_whatsapp:"Compartir en WhatsApp", label_email_reminder:"Recordatorio por email",
1966      label_done_survey:"¿Cómo fue la experiencia?", label_done_thanks:"¡Gracias por el feedback!",
1967      label_done_good:"Bien", label_done_meh:"Regular", label_done_bad:"Mal",
1968      ph_search_service:"Buscar (ej. 730, ISEE, casa...)",
1969      ph_search_office:"Buscar sede por nombre, ciudad o dirección",
1970      ph_search_province:"Buscar ciudad o provincia",
1971      ph_no_results_one:'Ningún servicio encontrado para "{0}"',
1972      label_did_you_mean:"¿Quizás querías decir?:",
1973      label_popular:"🔥 Más solicitado", label_recent:"⏱ Reciente", label_frequent:"⭐ Frecuente",
1974      label_resume_h:"Reanudar donde lo dejaste",
1975      label_resume_btn:"Reanudar", label_resume_discard:"Empezar de nuevo",
1976      label_offline_b:"Estás desconectado.", label_offline_msg:" Conéctate para completar la reserva. El estado se ha guardado.",
1977      label_slow:"Conexión lenta — la carga puede tardar unos segundos más.",
1978      err_h_default:"Se ha producido un error", err_s_default:"Reintenta o llama al número gratuito para asistencia.",
1979      err_h_timeout:"Conexión lenta", err_s_timeout:"El servidor no responde. Verifica la conexión y reintenta.",
1980      err_h_network:"Sin conexión", err_s_network:"Pareces estar desconectado. Verifica la red y reintenta.",
1981      err_h_no_services:"Ningún servicio disponible", err_s_no_services:"Actualmente no hay servicios reservables online en tu provincia.",
1982      err_h_no_sedi:"Ninguna sede disponible", err_s_no_sedi:"No hay sedes configuradas para este servicio en tu provincia.",
1983      err_h_no_spazi:"Ningún horario disponible", err_s_no_spazi:"Sin horarios libres ahora. Reintenta o cambia de sede.",
1984      err_h_already_booked:"Ya tienes una reserva", err_s_already_booked:"Ya estás registrado para un servicio en estas fechas.",
1985      err_h_slot_taken:"Horario recién ocupado", err_s_slot_taken:"El horario elegido acaba de ser reservado por otra persona. Elige otro horario de la lista actualizada.",
1986      err_h_validation:"Datos inválidos", err_s_validation:"Verifica los campos y reintenta.",
1987      err_h_disabled:"Servicio no disponible", err_s_disabled:"La reserva online está temporalmente deshabilitada.",
1988      err_h_server:"Error del servidor", err_s_server:"Respuesta inválida del servidor. Reintenta en breve.",
1989      toast_slow_title:"Conexión lenta", toast_slow_msg:"Reintentando ({0}/3)...",
1990      gdpr_h:"¿Quieres acelerar tus próximas reservas?",
1991      gdpr_body:"Podemos guardar tus datos (nombre, código fiscal, email, teléfono) en este dispositivo para precompletarlos la próxima vez. Los datos permanecen en tu navegador y se autoeliminan tras 30 días de inactividad.",
1992      gdpr_more:"Más información", gdpr_accept:"Sí, recordar mis datos", gdpr_deny:"No, gracias",
1993      menu_new:"Nueva conversación",
1994      menu_revoke:"Revocar consentimiento y borrar datos",
1995      revoke_confirm:"¿Borrar los datos guardados (nombre, código fiscal, email, teléfono) y revocar el consentimiento?",
1996      revoke_done_h:"Datos borrados", revoke_done_s:"Tus datos personales se han eliminado de este dispositivo.",
1997      cf_valid_ok:"Código fiscal válido.", cf_valid_recognized:"Reconocido: {0}, nacido/a el {1}, municipio (código {2})",
1998      cf_invalid:"Código fiscal inválido. Verifica los caracteres.",
1999      cf_male:"♂ Hombre", cf_female:"♀ Mujer",
2000      /* EAA/WCAG 2026-07-19: chiavi a11y + i18n wizard/voice */
2001      sr_booking_launch:"Hay un botón disponible para abrir la reserva.", sr_results_count:"{0} resultados", sr_no_results:"Sin resultados", aria_select_office:"Seleccionar {0}",
2002      aria_stop_listening:"Detener la escucha", sr_listening:"Escuchando", sr_processing:"Procesando", aria_back_to_step:"Volver al paso {0}: {1}",
2003      step_of:"Paso {0} de {1}", cal_months:"Enero|Febrero|Marzo|Abril|Mayo|Junio|Julio|Agosto|Septiembre|Octubre|Noviembre|Diciembre", cal_months_short:"ene|feb|mar|abr|may|jun|jul|ago|sep|oct|nov|dic", cal_days_full:"Domingo|Lunes|Martes|Miércoles|Jueves|Viernes|Sábado",
2004      cal_days_short:"Lun|Mar|Mié|Jue|Vie|Sáb|Dom", label_available:"Disponible", label_selected:"Seleccionado", label_nearest:"La más cercana",
2005      label_view_map:"Ver en el mapa", label_no_slots_h:"No hay horarios disponibles", label_no_slots_s:"De momento no hay huecos disponibles. Prueba otra oficina o inténtalo más tarde.", label_no_dates_h:"Horarios no disponibles",
2006      label_no_dates_s:"No he podido leer las fechas disponibles. Prueba otra oficina.", label_pick_day:"Selecciona un día", label_no_slots_day:"No hay horarios ese día. Prueba otro día disponible.", ph_no_office_found:"Ninguna oficina encontrada para \"{0}\"",
2007      ph_no_results_generic:"Sin resultados para \"{0}\"", label_reuse_data_h:"¿Reutilizar tus datos guardados?", btn_its_me:"Sí, soy yo", btn_not_me:"No, soy otra persona",
2008      label_saved_data:"Datos guardados", label_submitting:"Enviando la reserva...", label_submitting_sub:"Estoy registrando tu cita, tardará unos segundos.", label_loading_hint:"Busco disponibilidad para \"{0}\"...",
2009      label_loading_options:"Cargando opciones...", label_loading_short:"Cargando…", label_error_code:"Código: {0}", label_booking_inprogress:"Reserva en curso",
2010      label_expand:"Ampliar", label_expand_sub:"Abre la vista a pantalla completa para una mejor experiencia", t_region:"Selecciona la región", t_province:"Selecciona la provincia",
2011      t_data:"Tus datos", label_srv_hint_found:"He encontrado estos servicios para \"{0}\". ¿Cuál te interesa?", label_srv_hint:"¿Qué servicio quieres reservar?", label_sede_hint:"¡Perfecto! Ahora elige la oficina más cómoda para ti.",
2012      btn_find_nearest_sub:"Usar tu posición GPS", btn_find_nearest_again_sub:"Usar la posición actualizada", label_slots_hint:"Aquí tienes las citas disponibles. ¡Elige la que prefieras!", label_pref_time:"Normalmente prefieres: {0}.",
2013      label_data_hint:"¡Casi listo! Introduce tus datos para completar la reserva.", label_date_at:"{0} a las {1}", label_appointment:"Cita", btn_use:"Usar",
2014      btn_calendar:"Calendario", btn_email_reminder:"Enviarme el recordatorio por correo", btn_choose_other:"Elegir otro", btn_choose_other_slot:"Elegir otro horario",
2015      label_done_email:"Reserva registrada para {0}", err_cf_required:"Introduce el código fiscal.", err_cf_short:"El código fiscal debe tener 16 caracteres (faltan {0}).", err_cf_long:"El código fiscal debe tener 16 caracteres ({0} de más).",
2016      err_cf_chars:"El código fiscal solo puede contener letras y cifras.", err_cf_format:"El formato del código fiscal no es válido. Revisa los caracteres introducidos.", err_cf_charpos:"Carácter no válido en la posición {0}.", err_cf_checksum:"El dígito de control no coincide (se esperaba '{0}' en lugar de '{1}'). Revísalo.",
2017      err_cf_first:"Introduce primero un código fiscal válido", err_cf_proceed:"Introduce un código fiscal válido antes de continuar.", err_required_fields:"Rellena todos los campos obligatorios.", err_email_invalid:"Introduce una dirección de correo válida.",
2018      err_phone_invalid:"Introduce un número de móvil válido (al menos 9 cifras).", err_uncertain:"No hemos recibido confirmación del servidor: tu reserva puede haberse completado igualmente. Comprueba tu correo antes de reintentar.", err_unexpected:"Error inesperado. Inténtalo de nuevo.", help_codfis:"Lo encuentras en tu tarjeta sanitaria. Tiene 16 caracteres.",
2019      help_email:"Solo la usaremos para enviarte la confirmación y el recordatorio 24 h antes.", help_cellulare:"Para comunicaciones urgentes de la oficina.", voice_tap_to_talk:"Toca para hablar", voice_press_to_start:"Pulsa el micrófono para empezar",
2020      voice_speak_now:"Habla ahora", voice_listening_hint:"Te estoy escuchando... habla", voice_responding:"Estoy respondiendo", voice_tap_interrupt:"Toca el micrófono para interrumpir",
2021      voice_thinking:"Estoy pensando...", voice_thinking_hint:"Un momento, estoy preparando la respuesta", voice_back_keyboard:"Volver al teclado", ph_your_city:"Escribe tu ciudad...", ph_follow_steps:"Sigue los pasos de arriba para reservar..."
2022    },
2023    de:{
2024      /* --- GDPR / privacy (i18n completo, art. 12-13 GDPR) --- */
2025      gdpr_accept_all:"Alle akzeptieren",
2026      gdpr_body_v2:"Um Ihnen den Online-Buchungsdienst anzubieten, verwendet {0} bestimmte Technologien zur lokalen Speicherung (localStorage) auf Ihrem Gerät. Sie können wählen, welche Kategorien Sie akzeptieren. Ihre Wahl wird gespeichert und ist jederzeit über das Menü ⋯ widerrufbar.",
2027      gdpr_cat_analytics:"Analyse und Marketing",
2028      gdpr_cat_analytics_d:"Sitzungskennung, Kampagnenparameter (UTM, gclid, fbclid) und Erfassung der Ereignisse des Buchungsablaufs zur Verbesserung des Dienstes. Pseudonymisierte Daten.",
2029      gdpr_cat_functional:"Funktionale",
2030      gdpr_cat_functional_d:"Speicherung Ihrer Buchungsdaten (Name, Steuernummer, E-Mail, Telefon), um sie beim nächsten Mal vorauszufüllen. Der Status der laufenden Buchung (Fortsetzung) und der jüngste Verlauf.",
2031      gdpr_cat_necessary:"Notwendige (immer aktiv)",
2032      gdpr_cat_necessary_d:"Für den Betrieb des Widgets unverzichtbare Technologien: Zwischenspeicher der Regionen/Standorte/Dienste, Sprache. Ohne diese funktioniert das Widget nicht. Rechtsgrundlage: unbedingt erforderlich für den angeforderten Dienst (einwilligungsfrei).",
2033      gdpr_customize:"Anpassen",
2034      gdpr_info_address:"Sitz:",
2035      gdpr_info_controller:"Verantwortlicher:",
2036      gdpr_info_dpo:"Datenschutzbeauftragter (DPO):",
2037      gdpr_info_email:"E-Mail/PEC:",
2038      gdpr_info_legal_basis:"Rechtsgrundlage:",
2039      gdpr_info_legal_basis_d:"Notwendige = für den angeforderten Dienst unbedingt erforderliche Technologien (einwilligungsfrei, art. 122 der geltenden nationalen Rechtsvorschriften); die Buchungsdaten werden zur Erfüllung des angeforderten Dienstes verarbeitet (art. 6.1.b GDPR). Funktionale und Analyse = Einwilligung (art. 6.1.a GDPR).",
2040      gdpr_info_phone:"Telefon:",
2041      gdpr_info_recipients:"Empfänger:",
2042      gdpr_info_recipients_d:"Die Buchungsdaten werden an das System Planner CAF (in der EU) übermittelt, um den Termin abzuschließen. Die Konversationen werden von der Konversationsplattform Mantis und den angebundenen KI-Anbietern verarbeitet, auch außerhalb der EU auf Grundlage von Standardvertragsklauseln (SCC): konversationelle Unterstützung und Klassifizierung der Absichten (Anthropic) sowie Analyse eventuell hochgeladener Dokumente (Ope
2042nAI). Zur Ermittlung des nächstgelegenen Standorts werden OpenStreetMap/Nominatim und IP-Geolokalisierungsdienste verwendet. Technische Bereitstellung: Vercel und Bunny CDN; Speicherung bei Google Firebase. Siehe die Datenschutzerklärung des Verantwortlichen.",
2043      gdpr_info_retention:"Speicherdauer:",
2044      gdpr_info_retention_d:"Auf dem Gerät: 30 Tage ab der letzten Aktivität, danach automatische Löschung. Auf den Servern: die Konversationen werden gelöscht und die Buchungsprotokolle innerhalb der vom Verantwortlichen festgelegten Fristen anonymisiert (als Richtwert: Konversationen etwa 180 Tage; Buchungen gemäß den geltenden buchhalterischen/steuerlichen Pflichten). Die Einwilligung bleibt bis zum Widerruf gültig.",
2045      gdpr_info_revoke:"Wie widerrufen:",
2046      gdpr_info_revoke_d:"Öffnen Sie den Chat → Menü (⋯) → Datenschutz. Sie können die Einstellungen ändern oder alle Daten löschen.",
2047      gdpr_info_rights:"Ihre Rechte:",
2048      gdpr_info_rights_d:"Auskunft, Berichtigung, Löschung, Einschränkung, Übertragbarkeit, Widerspruch (artt. 15-22 GDPR). Beschwerde bei der zuständigen Aufsichtsbehörde.",
2049      gdpr_info_storage:"Wo werden sie gespeichert:",
2050      gdpr_info_storage_d:"Die Vorausfülldaten, der Verlauf und die Einstellungen verbleiben im localStorage Ihres Browsers, auf diesem Gerät (verschleiert, aber nicht mit kryptografischen Standards verschlüsselt). Wenn Sie buchen oder im Chat schreiben, werden die entsprechenden Daten an die Server der unter 'Empfänger' angegebenen Auftragsverarbeiter übermittelt und dort gespeichert.",
2051      gdpr_info_vat:"USt-IdNr. / Steuernummer:",
2052      gdpr_privacy_policy:"Vollständige Datenschutzerklärung",
2053      gdpr_reject_all:"Alle ablehnen",
2054      gdpr_save_prefs:"Einstellungen speichern",
2055      menu_privacy:"Datenschutz und personenbezogene Daten",
2056      privacy_panel_absent:"nicht vorhanden",
2057      privacy_panel_consent_h:"Ihre Einwilligungseinstellungen",
2058      privacy_panel_data_client:"Stammdaten (Name, Steuernummer, E-Mail, Telefon)",
2059      privacy_panel_data_h:"Aktuell auf diesem Gerät gespeicherte Daten",
2060      privacy_panel_data_history:"Buchungsverlauf (Anzahl)",
2061      privacy_panel_data_resume:"Laufende Buchung (Fortsetzung)",
2062      privacy_panel_data_sid:"Sitzungskennung",
2063      privacy_panel_delete_all:"Alles löschen",
2064      privacy_panel_export:"Meine Daten herunterladen",
2065      privacy_panel_export_h:"Download gestartet",
2066      privacy_panel_export_s:"Ihre Daten wurden in eine JSON-Datei exportiert.",
2067      privacy_panel_h:"Datenschutz und Ihre Daten",
2068      privacy_panel_present:"vorhanden",
2069      privacy_panel_s:"Hier können Sie die Einwilligungseinstellungen ändern, die auf Ihrem Gerät gespeicherten Daten herunterladen oder sie vollständig löschen.",
2070      privacy_panel_saved_h:"Einstellungen gespeichert",
2071      privacy_panel_saved_s:"Ihre Einwilligungsentscheidungen wurden aktualisiert.",
2072      /* a11y accessible names */ aria_open_chat:"Chat öffnen", aria_close_chat:"Chat schließen", ph_write_message:"Nachricht schreiben", aria_send:"Nachricht senden", aria_scroll_bottom:"Zum letzten Beitrag", aria_options:"Optionen", aria_close:"Schließen", aria_fullscreen:"Vollbild", aria_exit_fullscreen:"Vollbild verlassen", aria_voice:"Spracheingabe", aria_book:"Buchung öffnen", aria_open_book:"Chat öffnen und Termin buchen", skip_to_input:"Zum Nachrichtenfeld springen", aria_history:"Gesprächsverlauf", sr_assistant:"Assistent", sr_you_said:"Sie schrieben:", sr_said:"schrieb:", assistant_typing:"Der Assistent schreibt…", label_error:"Fehler", booking_confirmed:"Buchung bestätigt", menu_contact:"Mit einem Mitarbeiter sprechen", aria_email:"E-Mail-Adresse", aria_pwd_email:"E-Mail zum Zurücksetzen des Passworts", aria_view_on_map:"{0} auf der Karte ansehen", aria_copy:"Nachricht kopieren", aria_translate:"Nachricht übersetzen", aria_scroll_left:"Nach links scrollen", aria_scroll_right:"Nach rechts scrollen", aria_remove:"Entfernen", aria_add:"Hinzufügen", aria_speak:"Sprechen", aria_more_info:"Mehr Infos", aria_hide:"Ausblenden", aria_expand_booking:"Buchung im Vollbild öffnen", aria_book_appointment:"Termin buchen", aria_booking:"Buchung", aria_search_office:"Büro suchen",
2073      hero_title:"Termin buchen",
2074      hero_subtitle:"⏱ ~2 Minuten · ",
2075      step_zone:"Region", step_service:"Service", step_office:"Filiale", step_time:"Zeit", step_data:"Daten",
2076      h_region:"In welcher Region möchten Sie buchen?", s_region:"Wählen Sie Ihre Region, um verfügbare Filialen und Services zu sehen.",
2077      h_province:"In welcher Provinz möchten Sie buchen?", s_province:"Nutzen Sie Ihren Standort oder wählen Sie manuell.",
2078      h_service:"Service auswählen", s_service:"Wählen Sie den Service, den Sie buchen möchten.",
2079      h_service_hint:"Welcher Service interessiert Sie?", s_service_hint:'Gefiltert nach "{0}". Verfeinern Sie die Suche unten.',
2080      h_office:"Filiale auswählen", s_office:"Hier alle verfügbaren Filialen. Nutzen Sie GPS für die nächste.",
2081      h_slot:"Datum und Uhrzeit wählen", s_slot:"Wählen Sie einen Tag im Kalender, um verfügbare Zeiten zu sehen.",
2082      h_data:"Daten eingeben", s_data:"Fast fertig! Wir benötigen die Daten des Termininhabers.",
2083      h_confirm:"Buchung bestätigen", s_confirm:"Überprüfen Sie die Details. Wenn alles stimmt, klicken Sie Bestätigen.",
2084      btn_back:"Zurück", btn_cancel:"Abbrechen", btn_confirm:"Buchung bestätigen", btn_confirming:"Speichere...",
2085      consent_before:"Ich habe die ", consent_link:"Datenschutzerklärung", consent_after:" gelesen und willige in die Verarbeitung meiner personenbezogenen Daten zur Verwaltung des Termins ein.", consent_required:"Um fortzufahren, müssen Sie der Verarbeitung Ihrer personenbezogenen Daten zustimmen.",
2086      chatgate_h:"Ihre Privatsphäre", chatgate_body:"Bevor Sie das Gespräch beginnen, lesen Sie bitte die Datenschutzerklärung und stimmen Sie der Verarbeitung Ihrer personenbezogenen Daten zu.", chatgate_before:"Ich habe die ", chatgate_link:"Datenschutzerklärung", chatgate_after:" gelesen und willige in die Verarbeitung meiner personenbezogenen Daten ein.", chatgate_btn:"Akzeptieren und starten",
2087      ai_disclosure:"Sie chatten mit einem virtuellen Assistenten auf Basis künstlicher Intelligenz (KI), nicht mit einer Person. Die Antworten werden automatisch generiert und können Fehler enthalten: Überprüfen Sie wichtige Informationen bei einem Mitarbeiter. Ihr Anliegen wird von menschlichen CAF-Mitarbeitern bearbeitet.",
2088      ai_badge:"KI-Assistent · automatisch generierte Antworten", ai_voice_notice:"Sie sprechen mit einem automatischen Sprachassistenten auf Basis künstlicher Intelligenz. Die Antworten werden automatisch generiert und können Fehler enthalten.",
2089      email_gate_body:"Um das Gespräch zu beginnen, geben Sie bitte Ihre E-Mail-Adresse ein.", email_gate_ph:"Ihre E-Mail", email_gate_btn:"Chat starten", email_gate_note:"Ihre E-Mail wird nur zur Bearbeitung dieses Gesprächs und Ihrer Anfrage verwendet.", email_gate_privacy:"Datenschutzerklärung", aria_prev_month:"Vormonat", aria_next_month:"Nächster Monat", hold_hint:"Wir empfehlen, innerhalb von {0} abzuschließen", hold_expired:"Die verfügbaren Zeiten können sich inzwischen geändert haben: Bitte prüfen Sie die Verfügbarkeit, bevor Sie bestätigen.",
2090      age_gate_label:"Ich erkläre, dass ich mindestens 14 Jahre alt bin. Für Kinder unter 14 Jahren ist die Zustimmung eines Elternteils oder Erziehungsberechtigten erforderlich.",
2091      translate_ai_note:"Automatische Übersetzung, generiert von KI", demo_ribbon:"DEMO — die angezeigten Daten sind Beispieldaten. Es wird keine echte Buchung vorgenommen.",
2092      btn_retry:"Erneut versuchen", btn_change:"Auswahl ändern", btn_call:"Anrufen", btn_restart:"Neu starten", btn_close:"Schließen", btn_edit:"Bearbeiten",
2093      btn_use_location:"Standort nutzen", btn_use_location_sub:"Ich finde Ihre Provinz automatisch",
2094      btn_find_nearest:"Nächste Filiale finden", btn_find_nearest_again:"Erneut suchen",
2095      btn_next:"Weiter zur Übersicht", btn_load_more:"Weitere Zeiten laden",
2096      label_or_choose:"oder manuell wählen",
2097      label_morning:"Vormittag", label_afternoon:"Nachmittag",
2098      label_today:"HEUTE", label_tomorrow:"MORGEN", label_this_week:"DIESE WOCHE",
2099      label_count_slots_one:"{0} Termin", label_count_slots_other:"{0} Termine",
2100      label_first_avail:"Frühester Termin", label_quickpick_arrow:"→",
2101      label_when:"Wann", label_where:"Wo", label_what:"Was",
2102      label_owner:"Inhaber", label_contacts:"Kontakte",
2103      label_summary_premium_h:"Alles bereit, prüfen und bestätigen",
2104      label_summary_premium_s:"Überprüfen Sie die Details vor dem Speichern.",
2105      label_summary_disclaimer:"Sie können bis 24 Stunden vor dem Termin ändern oder stornieren.",
2106      label_form_for_self:"Für mich", label_form_for_family:"Für ein Familienmitglied",
2107      label_form_nome:"Vorname", label_form_cognome:"Nachname", label_form_codfis:"Steuernummer", label_form_email:"E-Mail", label_form_cellulare:"Mobil",
2108      label_form_duration_min:"Dauer ~{0} Min",
2109      label_data_safe:"Daten geschützt", label_data_caf:"CAF-Support", label_data_real:"Termin mit echten CAF-Beratern",
2110      label_done_h:"Buchung bestätigt!", label_done_s:"Wir haben Ihnen eine Bestätigungs-E-Mail mit allen Details gesendet.",
2111      label_done_id:"Buchungs-ID: ",
2112      label_add_calendar:"Zum Kalender hinzufügen", label_share_whatsapp:"Auf WhatsApp teilen", label_email_reminder:"E-Mail-Erinnerung",
2113      label_done_survey:"Wie war die Erfahrung?", label_done_thanks:"Danke für das Feedback!",
2114      label_done_good:"Gut", label_done_meh:"Mittel", label_done_bad:"Schlecht",
2115      ph_search_service:"Suchen (z.B. Steuer, ISEE, Wohnung...)",
2116      ph_search_office:"Filiale nach Name, Stadt oder Adresse suchen",
2117      ph_search_province:"Stadt oder Provinz suchen",
2118      ph_no_results_one:'Kein Service gefunden für "{0}"',
2119      label_did_you_mean:"Meinten Sie:",
2120      label_popular:"🔥 Am gefragtesten", label_recent:"⏱ Kürzlich", label_frequent:"⭐ Häufig",
2121      label_resume_h:"Da fortfahren, wo Sie aufgehört haben",
2122      label_resume_btn:"Fortsetzen", label_resume_discard:"Neu starten",
2123      label_offline_b:"Sie sind offline.", label_offline_msg:" Verbinden Sie sich, um die Buchung abzuschließen. Status ist gespeichert.",
2124      label_slow:"Langsame Verbindung — das Laden kann etwas länger dauern.",
2125      err_h_default:"Ein Fehler ist aufgetreten", err_s_default:"Erneut versuchen oder die Hotline anrufen.",
2126      err_h_timeout:"Langsame Verbindung", err_s_timeout:"Der Server antwortet nicht. Verbindung prüfen und erneut versuchen.",
2127      err_h_network:"Keine Verbindung", err_s_network:"Sie scheinen offline zu sein. Netzwerk prüfen und erneut versuchen.",
2128      err_h_no_services:"Keine Services verfügbar", err_s_no_services:"Aktuell keine online buchbaren Services für Ihre Provinz.",
2129      err_h_no_sedi:"Keine Filialen verfügbar", err_s_no_sedi:"Keine Filialen für diesen Service in Ihrer Provinz konfiguriert.",
2130      err_h_no_spazi:"Keine Termine verfügbar", err_s_no_spazi:"Aktuell keine freien Zeiten. In ein paar Tagen erneut versuchen.",
2131      err_h_already_booked:"Sie haben bereits eine Buchung", err_s_already_booked:"Sie sind bereits für einen Service an diesen Daten gebucht.",
2132      err_h_slot_taken:"Termin gerade vergeben", err_s_slot_taken:"Der gewählte Termin wurde soeben von jemand anderem gebucht. Bitte wählen Sie einen anderen Termin aus der aktualisierten Liste.",
2133      err_h_validation:"Ungültige Daten", err_s_validation:"Felder prüfen und erneut versuchen.",
2134      err_h_disabled:"Service nicht verfügbar", err_s_disabled:"Online-Buchung ist vorübergehend deaktiviert.",
2135      err_h_server:"Server-Fehler", err_s_server:"Ungültige Antwort vom Server. Bitte gleich erneut versuchen.",
2136      toast_slow_title:"Langsame Verbindung", toast_slow_msg:"Erneuter Versuch ({0}/3)...",
2137      gdpr_h:"Möchten Sie zukünftige Buchungen beschleunigen?",
2138      gdpr_body:"Wir können Ihre Daten (Name, Steuernummer, E-Mail, Telefon) auf diesem Gerät speichern, um sie nächstes Mal vorauszufüllen. Daten bleiben im Browser und werden nach 30 Tagen Inaktivität gelöscht.",
2139      gdpr_more:"Mehr Infos", gdpr_accept:"Ja, Daten merken", gdpr_deny:"Nein, danke",
2140      menu_new:"Neue Konversation",
2141      menu_revoke:"Einwilligung widerrufen und Daten löschen",
2142      revoke_confirm:"Gespeicherte Daten (Name, Steuernummer, E-Mail, Telefon) löschen und Einwilligung widerrufen?",
2143      revoke_done_h:"Daten gelöscht", revoke_done_s:"Ihre persönlichen Daten wurden von diesem Gerät entfernt.",
2144      cf_valid_ok:"Gültige Steuernummer.", cf_valid_recognized:"Erkannt: {0}, geboren am {1}, Gemeinde (Code {2})",
2145      cf_invalid:"Ungültige Steuernummer. Zeichen prüfen.",
2146      cf_male:"♂ Männlich", cf_female:"♀ Weiblich",
2147      /* EAA/WCAG 2026-07-19: chiavi a11y + i18n wizard/voice */
2148      sr_booking_launch:"Eine Schaltfläche zum Öffnen der Buchung ist verfügbar.", sr_results_count:"{0} Ergebnisse", sr_no_results:"Keine Ergebnisse", aria_select_office:"{0} auswählen",
2149      aria_stop_listening:"Zuhören beenden", sr_listening:"Ich höre zu", sr_processing:"Verarbeitung läuft", aria_back_to_step:"Zurück zu Schritt {0}: {1}",
2150      step_of:"Schritt {0} von {1}", cal_months:"Januar|Februar|März|April|Mai|Juni|Juli|August|September|Oktober|November|Dezember", cal_months_short:"Jan|Feb|Mär|Apr|Mai|Jun|Jul|Aug|Sep|Okt|Nov|Dez", cal_days_full:"Sonntag|Montag|Dienstag|Mittwoch|Donnerstag|Freitag|Samstag",
2151      cal_days_short:"Mo|Di|Mi|Do|Fr|Sa|So", label_available:"Verfügbar", label_selected:"Ausgewählt", label_nearest:"Nächstgelegene",
2152      label_view_map:"Auf der Karte ansehen", label_no_slots_h:"Keine Uhrzeiten verfügbar", label_no_slots_s:"Derzeit sind keine Termine verfügbar. Versuchen Sie ein anderes Büro oder später erneut.", label_no_dates_h:"Uhrzeiten nicht verfügbar",
2153      label_no_dates_s:"Die verfügbaren Termine konnten nicht gelesen werden. Versuchen Sie ein anderes Büro.", label_pick_day:"Wählen Sie einen Tag", label_no_slots_day:"Keine Uhrzeiten an diesem Tag. Versuchen Sie einen anderen verfügbaren Tag.", ph_no_office_found:"Kein Büro gefunden für \"{0}\"",
2154      ph_no_results_generic:"Keine Ergebnisse für \"{0}\"", label_reuse_data_h:"Gespeicherte Daten wiederverwenden?", btn_its_me:"Ja, das bin ich", btn_not_me:"Nein, ich bin jemand anderes",
2155      label_saved_data:"Gespeicherte Daten", label_submitting:"Buchung wird gesendet...", label_submitting_sub:"Ihr Termin wird registriert, das dauert einige Sekunden.", label_loading_hint:"Ich suche Verfügbarkeiten für \"{0}\"...",
2156      label_loading_options:"Optionen werden geladen...", label_loading_short:"Wird geladen…", label_error_code:"Code: {0}", label_booking_inprogress:"Buchung läuft",
2157      label_expand:"Vergrößern", label_expand_sub:"Öffnen Sie die Vollbildansicht für ein besseres Erlebnis", t_region:"Region auswählen", t_province:"Provinz auswählen",
2158      t_data:"Ihre Daten", label_srv_hint_found:"Ich habe diese Leistungen für \"{0}\" gefunden. Welche interessiert Sie?", label_srv_hint:"Welche Leistung möchten Sie buchen?", label_sede_hint:"Perfekt! Wählen Sie jetzt das für Sie günstigste Büro.",
2159      btn_find_nearest_sub:"Ihre GPS-Position verwenden", btn_find_nearest_again_sub:"Aktualisierte Position verwenden", label_slots_hint:"Hier sind die verfügbaren Termine. Wählen Sie Ihren Favoriten!", label_pref_time:"Sie bevorzugen normalerweise: {0}.",
2160      label_data_hint:"Fast geschafft! Geben Sie Ihre Daten ein, um die Buchung abzuschließen.", label_date_at:"{0} um {1}", label_appointment:"Termin", btn_use:"Übernehmen",
2161      btn_calendar:"Kalender", btn_email_reminder:"Erinnerung per E-Mail senden", btn_choose_other:"Anderen wählen", btn_choose_other_slot:"Andere Uhrzeit wählen",
2162      label_done_email:"Buchung registriert für {0}
2162", err_cf_required:"Geben Sie die Steuernummer ein.", err_cf_short:"Die Steuernummer muss 16 Zeichen lang sein (es fehlen {0} Zeichen).", err_cf_long:"Die Steuernummer muss 16 Zeichen lang sein ({0} zu viel).",
2163      err_cf_chars:"Die Steuernummer darf nur Buchstaben und Ziffern enthalten.", err_cf_format:"Das Format der Steuernummer ist ungültig. Prüfen Sie die eingegebenen Zeichen.", err_cf_charpos:"Ungültiges Zeichen an Position {0}.", err_cf_checksum:"Die Prüfziffer stimmt nicht (erwartet '{0}' statt '{1}'). Bitte prüfen.",
2164      err_cf_first:"Geben Sie zuerst eine gültige Steuernummer ein", err_cf_proceed:"Geben Sie eine gültige Steuernummer ein, bevor Sie fortfahren.", err_required_fields:"Füllen Sie alle Pflichtfelder aus.", err_email_invalid:"Geben Sie eine gültige E-Mail-Adresse ein.",
2165      err_phone_invalid:"Geben Sie eine gültige Handynummer ein (mindestens 9 Ziffern).", err_uncertain:"Wir haben keine Bestätigung vom Server erhalten: Ihre Buchung kann trotzdem erfolgreich gewesen sein. Prüfen Sie Ihre E-Mail, bevor Sie es erneut versuchen.", err_unexpected:"Unerwarteter Fehler. Versuchen Sie es erneut.", help_codfis:"Sie finden sie auf Ihrer Gesundheitskarte. Sie ist 16 Zeichen lang.",
2166      help_email:"Nur für die Bestätigung und die Erinnerung 24 Std. vorher.", help_cellulare:"Für dringende Mitteilungen des Büros.", voice_tap_to_talk:"Zum Sprechen tippen", voice_press_to_start:"Drücken Sie das Mikrofon, um zu beginnen",
2167      voice_speak_now:"Sprechen Sie jetzt", voice_listening_hint:"Ich höre zu... sprechen Sie", voice_responding:"Ich antworte", voice_tap_interrupt:"Tippen Sie auf das Mikrofon, um zu unterbrechen",
2168      voice_thinking:"Ich denke nach...", voice_thinking_hint:"Einen Moment, ich bereite die Antwort vor", voice_back_keyboard:"Zurück zur Tastatur", ph_your_city:"Geben Sie Ihre Stadt ein...", ph_follow_steps:"Folgen Sie den Schritten oben, um zu buchen..."
2169    },
2170    ro:{
2171      /* --- GDPR / privacy (i18n completo, art. 12-13 GDPR) --- */
2172      gdpr_accept_all:"Acceptă toate",
2173      gdpr_body_v2:"Pentru a-ți oferi serviciul de rezervare online, {0} utilizează anumite tehnologii de stocare locală (localStorage) pe dispozitivul tău. Poți alege ce categorii să accepți. Alegerea ta este înregistrată și revocabilă în orice moment din meniul ⋯.",
2174      gdpr_cat_analytics:"Analiză și marketing",
2175      gdpr_cat_analytics_d:"Identificator de sesiune, parametri de campanie (UTM, gclid, fbclid) și colectarea evenimentelor din fluxul de rezervare pentru a îmbunătăți serviciul. Date pseudonimizate.",
2176      gdpr_cat_functional:"Funcționale",
2177      gdpr_cat_functional_d:"Memorarea datelor tale de rezervare (nume, cod fiscal, e-mail, telefon) pentru a le precompleta data viitoare. Starea rezervării în curs (reluare) și istoricul recent.",
2178      gdpr_cat_necessary:"Necesare (întotdeauna active)",
2179      gdpr_cat_necessary_d:"Tehnologii indispensabile funcționării widgetului: memoria cache a regiunilor/sediilor/serviciilor, limba. Fără acestea widgetul nu funcționează. Temei juridic: strict necesare serviciului solicitat (scutite de consimțământ).",
2180      gdpr_customize:"Personalizează",
2181      gdpr_info_address:"Sediu:",
2182      gdpr_info_controller:"Operator de date:",
2183      gdpr_info_dpo:"Responsabil cu protecția datelor (DPO):",
2184      gdpr_info_email:"E-mail/PEC:",
2185      gdpr_info_legal_basis:"Temei juridic:",
2186      gdpr_info_legal_basis_d:"Necesare = tehnologii strict necesare serviciului solicitat (scutite de consimțământ, art. 122 din reglementările naționale aplicabile); datele de rezervare sunt prelucrate pentru a executa serviciul solicitat (art. 6.1.b GDPR). Funcționale și Analiză = consimțământ (art. 6.1.a GDPR).",
2187      gdpr_info_phone:"Telefon:",
2188      gdpr_info_recipients:"Destinatari:",
2189      gdpr_info_recipients_d:"Datele de rezervare sunt trimise sistemului Planner CAF (în UE) pentru a finaliza programarea. Conversațiile sunt prelucrate de platforma conversațională Mantis și de furnizorii de IA conectați, inclusiv în afara UE în baza clauzelor contractuale standard (SCC): asistență conversațională și clasificarea intențiilor (Anthropic) și analiza eventualelor documente încărcate (Ope
2189nAI). Pentru a identifica cel mai apropiat sediu se utilizează OpenStreetMap/Nominatim și servicii de geolocalizare prin IP. Distribuție tehnică: Vercel și Bunny CDN; păstrare pe Google Firebase. Consultă Politica de confidențialitate a Operatorului de date.",
2190      gdpr_info_retention:"Perioada de păstrare:",
2191      gdpr_info_retention_d:"Pe dispozitiv: 30 de zile de la ultima activitate, apoi ștergere automată. Pe servere: conversațiile sunt șterse, iar jurnalele de rezervare sunt anonimizate în termenele stabilite de Operatorul de date (cu titlu indicativ: conversații aproximativ 180 de zile; rezervări conform obligațiilor contabile/fiscale aplicabile). Consimțământul rămâne valabil până la revocare.",
2192      gdpr_info_revoke:"Cum revoci:",
2193      gdpr_info_revoke_d:"Deschide chatul → meniu (⋯) → Confidențialitate. Poți modifica preferințele sau șterge toate datele.",
2194      gdpr_info_rights:"Drepturile tale:",
2195      gdpr_info_rights_d:"Acces, rectificare, ștergere, restricționare, portabilitate, opoziție (artt. 15-22 GDPR). Plângere la autoritatea de supraveghere competentă.",
2196      gdpr_info_storage:"Unde sunt păstrate:",
2197      gdpr_info_storage_d:"Datele de precompletare, istoricul și preferințele rămân în localStorage-ul browserului tău, pe acest dispozitiv (ofuscate, dar necriptate cu standarde criptografice). Când faci o rezervare sau scrii în chat, datele corespunzătoare sunt trimise și păstrate pe serverele persoanelor împuternicite indicate la 'Destinatari'.",
2198      gdpr_info_vat:"Cod TVA / Cod fiscal:",
2199      gdpr_privacy_policy:"Politica de confidențialitate completă",
2200      gdpr_reject_all:"Respinge toate",
2201      gdpr_save_prefs:"Salvează preferințele",
2202      menu_privacy:"Confidențialitate și date personale",
2203      privacy_panel_absent:"absente",
2204      privacy_panel_consent_h:"Preferințele tale de consimțământ",
2205      privacy_panel_data_client:"Date de identificare (nume, cod fiscal, e-mail, telefon)",
2206      privacy_panel_data_h:"Date stocate în prezent pe acest dispozitiv",
2207      privacy_panel_data_history:"Istoricul rezervărilor (număr)",
2208      privacy_panel_data_resume:"Rezervare în curs (reluare)",
2209      privacy_panel_data_sid:"Identificator de sesiune",
2210      privacy_panel_delete_all:"Șterge tot",
2211      privacy_panel_export:"Descarcă datele mele",
2212      privacy_panel_export_h:"Descărcare inițiată",
2213      privacy_panel_export_s:"Datele tale au fost exportate într-un fișier JSON.",
2214      privacy_panel_h:"Confidențialitate și datele tale",
2215      privacy_panel_present:"prezente",
2216      privacy_panel_s:"Aici poți modifica preferințele de consimțământ, descărca datele pe care le-am stocat pe dispozitivul tău sau le poți șterge complet.",
2217      privacy_panel_saved_h:"Preferințe salvate",
2218      privacy_panel_saved_s:"Alegerile tale privind consimțământul au fost actualizate.",
2219      /* a11y accessible names */ aria_open_chat:"Deschide chatul", aria_close_chat:"Închide chatul", ph_write_message:"Scrie un mesaj", aria_send:"Trimite mesajul", aria_scroll_bottom:"Mergi la ultimul mesaj", aria_options:"Opțiuni", aria_close:"Închide", aria_fullscreen:"Ecran complet", aria_exit_fullscreen:"Ieși din ecran complet", aria_voice:"Recunoaștere vocală", aria_book:"Deschide rezervarea", aria_open_book:"Deschide chatul și rezervă o programare", skip_to_input:"Sari la câmpul de mesaj", aria_history:"Istoricul conversației", sr_assistant:"Asistent", sr_you_said:"Ai scris:", sr_said:"a scris:", assistant_typing:"Asistentul scrie…", label_error:"Eroare", booking_confirmed:"Rezervare confirmată", menu_contact:"Vorbește cu un operator", aria_email:"Adresă de email", aria_pwd_email:"Email pentru resetarea parolei", aria_view_on_map:"Vezi {0} pe hartă", aria_copy:"Copiază mesajul", aria_translate:"Traduce mesajul", aria_scroll_left:"Derulează la stânga", aria_scroll_right:"Derulează la dreapta", aria_remove:"Elimină", aria_add:"Adaugă", aria_speak:"Vorbește", aria_more_info:"Mai multe informații", aria_hide:"Ascunde", aria_expand_booking:"Deschide rezervarea pe tot ecranul", aria_book_appointment:"Rezervă-ți programarea", aria_booking:"Rezervare", aria_search_office:"Caută sediu",
2220      hero_title:"Rezervă-ți programarea",
2221      hero_subtitle:"⏱ ~2 minute · ",
2222      step_zone:"Zonă", step_service:"Serviciu", step_office:"Sediu", step_time:"Oră", step_data:"Date",
2223      h_region:"În ce regiune vrei să rezervi?", s_region:"Alege regiunea pentru a vedea sediile și serviciile disponibile.",
2224      h_province:"În ce provincie vrei să rezervi?", s_province:"Folosește locația sau alege manual.",
2225      h_service:"Alege serviciul", s_service:"Selectează serviciul pe care vrei să-l rezervi.",
2226      h_service_hint:"Ce serviciu te interesează?", s_service_hint:'Filtrat după "{0}". Poți rafina căutarea mai jos.',
2227      h_office:"Alege sediul", s_office:"Iată toate sediile disponibile. Folosește geolocația pentru cel mai apropiat.",
2228      h_slot:"Alege data și ora", s_slot:"Selectează o zi din calendar pentru a vedea orele disponibile.",
2229      h_data:"Introdu datele", s_data:"Aproape gata! Avem nevoie de datele titularului.",
2230      h_confirm:"Confirmă rezervarea", s_confirm:"Verifică detaliile. Dacă totul e corect, apasă Confirmă.",
2231      btn_back:"Înapoi", btn_cancel:"Anulează", btn_confirm:"Confirmă rezervarea", btn_confirming:"Se salvează...",
2232      consent_before:"Am citit ", consent_link:"politica de confidențialitate", consent_after:" și sunt de acord cu prelucrarea datelor mele personale pentru gestionarea programării.", consent_required:"Pentru a continua trebuie să accepți prelucrarea datelor tale personale.",
2233      chatgate_h:"Confidențialitatea ta", chatgate_body:"Înainte de a începe conversația, te rugăm să citești politica de confidențialitate și să îți dai acordul pentru prelucrarea datelor personale.", chatgate_before:"Am citit ", chatgate_link:"politica de confidențialitate", chatgate_after:" și sunt de acord cu prelucrarea datelor mele personale.", chatgate_btn:"Acceptă și începe",
2234      ai_disclosure:"Vorbești cu un asistent virtual bazat pe inteligență artificială (IA), nu cu o persoană. Răspunsurile sunt generate automat și pot conține erori: verifică informațiile importante cu un operator. Dosarul tău va fi gestionat de personalul uman al CAF.",
2235      ai_badge:"Asistent IA · răspunsuri generate automat", ai_voice_notice:"Vorbești cu un asistent vocal automat bazat pe inteligență artificială. Răspunsurile sunt generate automat și pot conține erori.",
2236      email_gate_body:"Pentru a începe conversația, introdu adresa ta de e-mail.", email_gate_ph:"E-mailul tău", email_gate_btn:"Începe conversația", email_gate_note:"Folosim e-mailul tău doar pentru a gestiona această conversație și a răspunde solicitării tale.", email_gate_privacy:"Politica de confidențialitate", aria_prev_month:"Luna precedentă", aria_next_month:"Luna următoare", hold_hint:"Îți recomandăm să finalizezi în {0}", hold_expired:"Orarele s-ar putea să se fi schimbat între timp: verifică disponibilitatea înainte de a confirma.",
2237      age_gate_label:"Declar că am cel puțin 14 ani. Pentru minorii sub 14 ani este necesar consimțământul unui părinte sau al tutorelui legal.",
2238      translate_ai_note:"Traducere automată generată de IA", demo_ribbon:"DEMO — datele afișate sunt exemple. Nu se face nicio rezervare reală.",
2239      btn_retry:"Reîncearcă", btn_change:"Schimbă selecția", btn_call:"Sună", btn_restart:"Reîncepe", btn_close:"Închide", btn_edit:"Modifică",
2240      btn_use_location:"Folosește locația mea", btn_use_location_sub:"Găsesc provincia automat",
2241      btn_find_nearest:"Găsește sediul cel mai apropiat", btn_find_nearest_again:"Caută din nou",
2242      btn_next:"Continuă la rezumat", btn_load_more:"Încarcă mai multe ore",
2243      label_or_choose:"sau alege manual",
2244      label_morning:"Dimineața", label_afternoon:"După-amiaza",
2245      label_today:"AZI", label_tomorrow:"MÂINE", label_this_week:"SĂPTĂMÂNA ASTA",
2246      label_count_slots_one:"{0} oră", label_count_slots_other:"{0} ore",
2247      label_first_avail:"Prima disponibilitate", label_quickpick_arrow:"→",
2248      label_when:"Când", label_where:"Unde", label_what:"Ce",
2249      label_owner:"Titular", label_contacts:"Contacte",
2250      label_summary_premium_h:"Totul e gata, verifică și confirmă",
2251      label_summary_premium_s:"Verifică detaliile înainte de a salva.",
2252      label_summary_disclaimer:"Poți modifica sau anula rezervarea cu până la 24 de ore înainte.",
2253      label_form_for_self:"Pentru mine", label_form_for_family:"Pentru un membru al familiei",
2254      label_form_nome:"Prenume", label_form_cognome:"Nume", label_form_codfis:"Cod fiscal", label_form_email:"Email", label_form_cellulare:"Mobil",
2255      label_form_duration_min:"Durată ~{0} min",
2256      label_data_safe:"Date protejate", label_data_caf:"Suport CAF", label_data_real:"Programare cu operatori reali CAF",
2257      label_done_h:"Rezervare confirmată!", label_done_s:"Ți-am trimis un email de confirmare cu toate detaliile.",
2258      label_done_id:"ID rezervare: ",
2259      label_add_calendar:"Adaugă în calendar", label_share_whatsapp:"Distribuie pe WhatsApp", label_email_reminder:"Memento prin email",
2260      label_done_survey:"Cum a fost experiența?", label_done_thanks:"Mulțumim pentru feedback!",
2261      label_done_good:"Bine", label_done_meh:"Așa și așa", label_done_bad:"Rău",
2262      ph_search_service:"Caută (ex. 730, ISEE, casă...)",
2263      ph_search_office:"Caută sediu după nume, oraș sau adresă",
2264      ph_search_province:"Caută oraș sau provincie",
2265      ph_no_results_one:'Niciun serviciu găsit pentru "{0}"',
2266      label_did_you_mean:"Ai vrut să spui:",
2267      label_popular:"🔥 Cel mai cerut", label_recent:"⏱ Recent", label_frequent:"⭐ Frecvent",
2268      label_resume_h:"Reia de unde ai rămas",
2269      label_resume_btn:"Reia", label_resume_discard:"Reîncepe",
2270      label_offline_b:"Ești offline.", label_offline_msg:" Conectează-te pentru a finaliza rezervarea. Starea e salvată.",
2271      label_slow:"Conexiune lentă — încărcarea poate dura câteva secunde în plus.",
2272      err_h_default:"A apărut o eroare", err_s_default:"Reîncearcă sau sună la numărul verde pentru asistență.",
2273      err_h_timeout:"Conexiune lentă", err_s_timeout:"Serverul nu răspunde. Verifică conexiunea și reîncearcă.",
2274      err_h_network:"Fără conexiune", err_s_network:"Pari să fii offline. Verifică rețeaua și reîncearcă.",
2275      err_h_no_services:"Niciun serviciu disponibil", err_s_no_services:"Momentan nu există servicii rezervabile online în provincia ta.",
2276      err_h_no_sedi:"Niciun sediu disponibil", err_s_no_sedi:"Niciun sediu configurat pentru acest serviciu în provincia ta.",
2277      err_h_no_spazi:"Nicio oră disponibilă", err_s_no_spazi:"Momentan nu sunt ore libere. Reîncearcă sau schimbă sediul.",
2278      err_h_already_booked:"Ai deja o rezervare", err_s_already_booked:"Ești deja înregistrat pentru un serviciu în aceste date.",
2279      err_h_slot_taken:"Interval tocmai ocupat", err_s_slot_taken:"Ora aleasă tocmai a fost rezervată de altcineva. Alege altă oră din lista actualizată.",
2280      err_h_validation:"Date invalide", err_s_validation:"Verifică câmpurile și reîncearcă.",
2281      err_h_disabled:"Serviciu indisponibil", err_s_disabled:"Rezervarea online este temporar dezactivată.",
2282      err_h_server:"Eroare server", err_s_server:"Răspuns invali
2282d de la server. Reîncearcă în scurt timp.",
2283      toast_slow_title:"Conexiune lentă", toast_slow_msg:"Se reîncearcă ({0}/3)...",
2284      gdpr_h:"Vrei să accelerezi rezervările viitoare?",
2285      gdpr_body:"Putem memora datele tale (nume, cod fiscal, email, telefon) pe acest dispozitiv pentru a le precompleta data viitoare. Datele rămân în browser și se autoșterg după 30 de zile de inactivitate.",
2286      gdpr_more:"Mai multe informații", gdpr_accept:"Da, memorează datele mele", gdpr_deny:"Nu, mulțumesc",
2287      menu_new:"Conversație nouă",
2288      menu_revoke:"Revocă consimțământul și șterge datele",
2289      revoke_confirm:"Ștergi datele salvate (nume, cod fiscal, email, telefon) și revoci consimțământul?",
2290      revoke_done_h:"Date șterse", revoke_done_s:"Datele tale personale au fost eliminate de pe acest dispozitiv.",
2291      cf_valid_ok:"Cod fiscal valid.", cf_valid_recognized:"Recunoscut: {0}, născut/ă pe {1}, comună (cod {2})",
2292      cf_invalid:"Cod fiscal invalid. Verifică caracterele introduse.",
2293      cf_male:"♂ Bărbat", cf_female:"♀ Femeie",
2294      /* EAA/WCAG 2026-07-19: chiavi a11y + i18n wizard/voice */
2295      sr_booking_launch:"Este disponibil un buton pentru a deschide rezervarea.", sr_results_count:"{0} rezultate", sr_no_results:"Niciun rezultat", aria_select_office:"Selectează {0}",
2296      aria_stop_listening:"Oprește ascultarea", sr_listening:"Ascult", sr_processing:"Se procesează", aria_back_to_step:"Înapoi la pasul {0}: {1}",
2297      step_of:"Pasul {0} din {1}", cal_months:"Ianuarie|Februarie|Martie|Aprilie|Mai|Iunie|Iulie|August|Septembrie|Octombrie|Noiembrie|Decembrie", cal_months_short:"ian|feb|mar|apr|mai|iun|iul|aug|sep|oct|nov|dec", cal_days_full:"Duminică|Luni|Marți|Miercuri|Joi|Vineri|Sâmbătă",
2298      cal_days_short:"Lun|Mar|Mie|Joi|Vin|Sâm|Dum", label_available:"Disponibil", label_selected:"Selectat", label_nearest:"Cea mai apropiată",
2299      label_view_map:"Vezi pe hartă", label_no_slots_h:"Niciun orar disponibil", label_no_slots_s:"Momentan nu există intervale disponibile. Încearcă alt sediu sau revino mai târziu.", label_no_dates_h:"Orare indisponibile",
2300      label_no_dates_s:"Nu am putut citi datele disponibile. Încearcă alt sediu.", label_pick_day:"Selectează o zi", label_no_slots_day:"Niciun orar în această zi. Încearcă o altă zi disponibilă.", ph_no_office_found:"Niciun sediu găsit pentru \"{0}\"",
2301      ph_no_results_generic:"Niciun rezultat pentru \"{0}\"", label_reuse_data_h:"Refolosești datele salvate?", btn_its_me:"Da, sunt eu", btn_not_me:"Nu, sunt altă persoană",
2302      label_saved_data:"Date salvate", label_submitting:"Se trimite rezervarea...", label_submitting_sub:"Îți înregistrez programarea, durează câteva secunde.", label_loading_hint:"Caut disponibilitate pentru \"{0}\"...",
2303      label_loading_options:"Se încarcă opțiunile...", label_loading_short:"Se încarcă…", label_error_code:"Cod: {0}", label_booking_inprogress:"Rezervare în curs",
2304      label_expand:"Extinde", label_expand_sub:"Deschide vizualizarea pe tot ecranul pentru o experiență mai bună", t_region:"Selectează regiunea", t_province:"Selectează provincia",
2305      t_data:"Datele tale", label_srv_hint_found:"Am găsit aceste servicii pentru \"{0}\". Care te interesează?", label_srv_hint:"Ce serviciu vrei să rezervi?", label_sede_hint:"Perfect! Acum alege sediul cel mai convenabil pentru tine.",
2306      btn_find_nearest_sub:"Folosește poziția ta GPS", btn_find_nearest_again_sub:"Folosește poziția actualizată", label_slots_hint:"Iată programările disponibile. Alege-o pe cea preferată!", label_pref_time:"De obicei preferi: {0}.",
2307      label_data_hint:"Aproape gata! Introdu datele tale pentru a finaliza rezervarea.", label_date_at:"{0} la {1}", label_appointment:"Programare", btn_use:"Folosește",
2308      btn_calendar:"Calendar", btn_email_reminder:"Trimite-mi memento-ul pe email", btn_choose_other:"Alege altul", btn_choose_other_slot:"Alege alt orar",
2309      label_done_email:"Rezervare înregistrată pentru {0}", err_cf_required:"Introdu codul fiscal.", err_cf_short:"Codul fiscal trebuie să aibă 16 caractere (lipsesc {0}
2309).", err_cf_long:"Codul fiscal trebuie să aibă 16 caractere ({0} în plus).",
2310      err_cf_chars:"Codul fiscal poate conține doar litere și cifre.", err_cf_format:"Formatul codului fiscal nu este valid. Verifică caracterele introduse.", err_cf_charpos:"Caracter nevalid la poziția {0}.", err_cf_checksum:"Cifra de control nu corespunde (se aștepta '{0}' în loc de '{1}'). Verifică.",
2311      err_cf_first:"Introdu mai întâi un cod fiscal valid", err_cf_proceed:"Introdu un cod fiscal valid înainte de a continua.", err_required_fields:"Completează toate câmpurile obligatorii.", err_email_invalid:"Introdu o adresă de email validă.",
2312      err_phone_invalid:"Introdu un număr de mobil valid (cel puțin 9 cifre).", err_uncertain:"Nu am primit confirmare de la server: rezervarea ta poate totuși să fi reușit. Verifică emailul înainte de a încerca din nou.", err_unexpected:"Eroare neașteptată. Încearcă din nou.", help_codfis:"Îl găsești pe cardul tău de sănătate. Are 16 caractere.",
2313      help_email:"O folosim doar pentru confirmare și memento-ul cu 24 h înainte.", help_cellulare:"Pentru comunicări urgente de la sediu.", voice_tap_to_talk:"Atinge pentru a vorbi", voice_press_to_start:"Apasă microfonul pentru a începe",
2314      voice_speak_now:"Vorbește acum", voice_listening_hint:"Te ascult... vorbește", voice_responding:"Răspund", voice_tap_interrupt:"Atinge microfonul pentru a întrerupe",
2315      voice_thinking:"Mă gândesc...", voice_thinking_hint:"Un moment, pregătesc răspunsul", voice_back_keyboard:"Înapoi la tastatură", ph_your_city:"Scrie orașul tău...", ph_follow_steps:"Urmează pașii de mai sus pentru a rezerva..."
2316    }
2317  };
2318
2319  /* NOCAF: override chirurgico delle sole chiavi che citano il CAF o le
2320   * "pratiche". Attivo SOLO con window.__CAF_NEUTRAL__===true (env NOCAF=si):
2321   * senza flag il dizionario resta byte-per-byte identico (altri tenant intatti). */
2322  try{
2323    if(bkNeutralMode()){
2324      var BK_NEUTRAL_I18N={
2325      it:{ai_disclosure:"Stai parlando con un assistente virtuale basato sull'intelligenza artificiale (IA), non con una persona. Le risposte sono generate automaticamente e possono contenere errori: verifica le informazioni importanti con un operatore umano. La tua richiesta sarà seguita da un operatore umano.",label_data_caf:"Assistenza di operatori umani",label_data_real:"Appuntamento con operatori umani reali"},
2326      en:{ai_disclosure:"You are chatting with a virtual assistant powered by artificial intelligence (AI), not a human. Replies are generated automatically and may contain errors: check important information with a human operator. Your request will be handled by human operators.",label_data_caf:"Human operator support",label_data_real:"Appointment with real human operators"},
2327      fr:{ai_disclosure:"Vous discutez avec un assistant virtuel basé sur l'intelligence artificielle (IA), et non avec une personne. Les réponses sont générées automatiquement et peuvent contenir des erreurs : vérifiez les informations importantes auprès d'un opérateur humain. Votre demande sera traitée par des opérateurs humains.",label_data_caf:"Assistance d'opérateurs humains",label_data_real:"Rendez-vous avec de vrais opérateurs humains"},
2328      es:{ai_disclosure:"Estás hablando con un asistente virtual basado en inteligencia artificial (IA), no con una persona. Las respuestas se generan automáticamente y pueden contener errores: verifica la información importante con un operador humano. Tu solicitud será atendida por operadores humanos.",label_data_caf:"Asistencia de operadores humanos",label_data_real:"Cita con operadores humanos reales"},
2329      de:{ai_disclosure:"Sie chatten mit einem virtuellen Assistenten auf Basis künstlicher Intelligenz (KI), nicht mit einer Person. Die Antworten werden automatisch generiert und können Fehler enthalten: Überprüfen Sie wichtige Informationen bei einem menschlichen Mitarbeiter. Ihre Anfrage wird von menschlichen Mitarbeitern bearbeitet.",label_data_caf:"Betreuung durch menschliche Mitarbeiter",label_data_real:"Termin mit echten menschlichen Mitarbeitern"},
2330      ro:{ai_disclosure:"Vorbești cu un asistent virtual bazat pe inteligență artificială (IA), nu cu o persoană. Răspunsurile sunt generate automat și pot conține erori: verifică informațiile importante cu un operator uman. Solicitarea ta va fi gestionată de operatori umani.",label_data_caf:"Asistență de la operatori umani",label_data_real:"Programare cu operatori umani reali"}
2331      };
2332      for(var _nl in BK_NEUTRAL_I18N){if(BK_I18N[_nl]){for(var _nk in BK_NEUTRAL_I18N[_nl])BK_I18N[_nl][_nk]=BK_NEUTRAL_I18N[_nl][_nk];}}
2333      /* Destinatari GDPR: "Planner CAF" → "Planner" in tutte le lingue. */
2334      for(var _gl in BK_I18N){var _gd=BK_I18N[_gl]&&BK_I18N[_gl].gdpr_info_recipients_d;
2335        if(typeof _gd==="string")BK_I18N[_gl].gdpr_info_recipients_d=_gd.replace(/Planner CAF/g,"Planner").replace(/CAF Planner/g,"Planner");}
2336    }
2337  }catch(_e){}
2338
2339  /* Detect lingua: priorità a opts.lang/?caf-lang/data-lang/localStorage,
2340   * poi navigator.language (primi 2 char), fallback "it". */
2341  function bkDetectLang(opts){
2342    try{
2343      if(opts&&opts.lang){var forced=String(opts.lang).slice(0,2).toLowerCase();if(BK_I18N[forced])return forced;}
2344      var url="";try{var m=location.search.match(/[?&]caf-lang=([a-z]{2})/i);if(m)url=m[1].toLowerCase();}catch(_e){}
2345      if(url&&BK_I18N[url])return url;
2346      var sct=null;try{sct=document.querySelector("script[data-lang]");}catch(_e){}
2347      if(sct){var a=sct.getAttribute("data-lang");if(a&&BK_I18N[a.toLowerCase()])return a.toLowerCase();}
2348      var stored="";try{stored=localStorage.getItem(BK_LANG_KEY)||"";}catch(_e){}
2349      if(stored&&BK_I18N[stored])return stored;
2350      var nav=(navigator.language||navigator.userLanguage||"it").slice(0,2).toLowerCase();
2351      if(BK_I18N[nav])return nav;
2352    }catch(_e){}
2353    return"it";
2354  }
2355
2356  /* Module-scope active language; viene impostato in CafChat constructor. */
2357  var BK_LANG="it";
2358  function bkT(key,a,b,c){
2359    var dict=BK_I18N[BK_LANG]||BK_I18N.it;var s=dict[key];if(s==null)s=BK_I18N.it[key];if(s==null)return key;
2360    /* placeholder substitution: {0} {1} {2} */
2361    if(a!==undefined)s=s.replace("{0}",String(a));
2362    if(b!==undefined)s=s.replace("{1}",String(b));
2363    if(c!==undefined)s=s.replace("{2}",String(c));
2364    return s;
2365  }
2366
2367  /* ── Slot helpers: classifica fascia mattina/pomeriggio + density score ── */
2368  function bkSlotFascia(slot){
2369    if(!slot||!slot.ora_inizio)return"unknown";
2370    var hh=parseInt(String(slot.ora_inizio).slice(0,2),10);
2371    if(isNaN(hh))return"unknown";
2372    return hh<13?"morning":"afternoon";
2373  }
2374  /* density: 0..1 — più alto = più slot disponibili in quel range orario. */
2375  function bkSlotDensityMap(slots){
2376    var map={};if(!Array.isArray(slots))return map;
2377    var counts={};
2377slots.forEach(function(s){var hh=parseInt(String(s.ora_inizio||"").slice(0,2),10);if(isNaN(hh))return;counts[hh]=(counts[hh]||0)+1;});
2378    var max=0;Object.keys(counts).forEach(function(k){if(counts[k]>max)max=counts[k];});
2379    if(!max)return map;
2380    Object.keys(counts).forEach(function(k){map[k]=counts[k]/max;});
2381    return map;
2382  }
2383
2384  /* ── Lista hardcoded province italiane (mirror di GenerateProvincesForTenant.php).
2385   *  Usata come FALLBACK quando l'API restituisce solo entries placeholder
2386   *  tipo "Toscana Regionale" invece delle 10 province reali. external_id
2387   *  (sigla) viene usato come `codice` perché è ciò che il backend Laravel
2388   *  mappa a Province.external_id. */
2389  var BK_IT_PROVINCES=[
2390    {n:"Agrigento",s:"AG",r:"Sicilia"},{n:"Alessandria",s:"AL",r:"Piemonte"},{n:"Ancona",s:"AN",r:"Marche"},
2391    {n:"Arezzo",s:"AR",r:"Toscana"},{n:"Ascoli Piceno",s:"AP",r:"Marche"},{n:"Asti",s:"AT",r:"Piemonte"},
2392    {n:"Avellino",s:"AV",r:"Campania"},{n:"Bari",s:"BA",r:"Puglia"},{n:"Belluno",s:"BL",r:"Veneto"},
2393    {n:"Benevento",s:"BN",r:"Campania"},{n:"Bergamo",s:"BG",r:"Lombardia"},{n:"Biella",s:"BI",r:"Piemonte"},
2394    {n:"Bologna",s:"BO",r:"Emilia-Romagna"},{n:"Bolzano/Bozen",s:"BZ",r:"Trentino-Alto Adige/Südtirol"},
2395    {n:"Brescia",s:"BS",r:"Lombardia"},{n:"Brindisi",s:"BR",r:"Puglia"},{n:"Cagliari",s:"CA",r:"Sardegna"},
2396    {n:"Caltanissetta",s:"CL",r:"Sicilia"},{n:"Campobasso",s:"CB",r:"Molise"},{n:"Carbonia-Iglesias",s:"CI",r:"Sardegna"},
2397    {n:"Caserta",s:"CE",r:"Campania"},{n:"Catania",s:"CT",r:"Sicilia"},{n:"Catanzaro",s:"CZ",r:"Calabria"},
2398    {n:"Chieti",s:"CH",r:"Abruzzo"},{n:"Como",s:"CO",r:"Lombardia"},{n:"Cosenza",s:"CS",r:"Calabria"},
2399    {n:"Cremona",s:"CR",r:"Lombardia"},{n:"Crotone",s:"KR",r:"Calabria"},{n:"Cuneo",s:"CN",r:"Piemonte"},
2400    {n:"Enna",s:"EN",r:"Sicilia"},{n:"Fermo",s:"FM",r:"Marche"},{n:"Ferrara",s:"FE",r:"Emilia-Romagna"},
2401    {n:"Firenze",s:"FI",r:"Toscana"},{n:"Foggia",s:"FG",r:"Puglia"},{n:"Forlì-Cesena",s:"FC",r:"Emilia-Romagna"},
2402    {n:"Frosinone",s:"FR",r:"Lazio"},{n:"Genova",s:"GE",r:"Liguria"},{n:"Gorizia",s:"GO",r:"Friuli-Venezia Giulia"},
2403    {n:"Grosseto",s:"GR",r:"Toscana"},{n:"Imperia",s:"IM",r:"Liguria"},{n:"Isernia",s:"IS",r:"Molise"},
2404    {n:"L'Aquila",s:"AQ",r:"Abruzzo"},{n:"La Spezia",s:"SP",r:"Liguria"},{n:"Latina",s:"LT",r:"Lazio"},
2405    {n:"Lecce",s:"LE",r:"Puglia"},{n:"Lecco",s:"LC",r:"Lombardia"},{n:"Livorno",s:"LI",r:"Toscana"},
2406    {n:"Lodi",s:"LO",r:"Lombardia"},{n:"Lucca",s:"LU",r:"Toscana"},{n:"Macerata",s:"MC",r:"Marche"},
2407    {n:"Mantova",s:"MN",r:"Lombardia"},{n:"Massa-Carrara",s:"MS",r:"Toscana"},{n:"Matera",s:"MT",r:"Basilicata"},
2408    {n:"Medio Campidano",s:"VS",r:"Sardegna"},{n:"Messina",s:"ME",r:"Sicilia"},{n:"Milano",s:"MI",r:"Lombardia"},
2409    {n:"Modena",s:"MO",r:"Emilia-Romagna"},{n:"Monza e della Brianza",s:"MB",r:"Lombardia"},
2410    {n:"Napoli",s:"NA",r:"Campania"},{n:"Novara",s:"NO",r:"Piemonte"},{n:"Nuoro",s:"NU",r:"Sardegna"},
2411    {n:"Ogliastra",s:"OG",r:"Sardegna"},{n:"Olbia-Tempio",s:"OT",r:"Sardegna"},{n:"Oristano",s:"OR",r:"Sardegna"},
2412    {n:"Padova",s:"PD",r:"Veneto"},{n:"Palermo",s:"PA",r:"Sicilia"},{n:"Parma",s:"PR",r:"Emilia-Romagna"},
2413    {n:"Pavia",s:"PV",r:"Lombardia"},{n:"Perugia",s:"PG",r:"Umbria"},{n:"Pesaro e Urbino",s:"PU",r:"Marche"},
2414    {n:"Pescara",s:"PE",r:"Abruzzo"},{n:"Piacenza",s:"PC",r:"Emilia-Romagna"},{n:"Pisa",s:"PI",r:"Toscana"},
2415    {n:"Pistoia",s:"PT",r:"Toscana"},{n:"Pordenone",s:"PN",r:"Friuli-Venezia Giulia"},{n:"Potenza",s:"PZ",r:"Basilicata"},
2416    {n:"Prato",s:"PO",r:"Toscana"},{n:"Ragusa",s:"RG",r:"Sicilia"},{n:"Ravenna",s:"RA",r:"Emilia-Romagna"},
2417    {n:"Reggio di Calabria",s:"RC",r:"Calabria"},{n:"Reggio nell'Emilia",s:"RE",r:"Emilia-Romagna"},
2418    {n:"Rieti",s:"RI",r:"Lazio"},{n:"Rimini",s:"RN",r:"Emilia-Romagna"},{n:"Roma",s:"RM",r:"Lazio"},
2419    {n:"Rovigo",s:"RO",r:"Veneto"},{n:"Salerno",s:"SA",r:"Campania"},{n:"Sassari",s:"SS",r:"Sardegna"},
2420    {n:"Savona",s:"SV",r:"Liguria"},{n:"Siena",s:"SI",r:"Toscana"},{n:"Siracusa",s:"SR",r:"Sicilia"},
2421    {n:"Sondrio",s:"SO",r:"Lombardia"},{n:"Taranto",s:"TA",r:"Puglia"},{n:"Teramo",s:"TE",r:"Abruzzo"},
2422    {n:"Terni",s:"TR",r:"Umbria"},{n:"Torino",s:"TO",r:"Piemonte"},{n:"Trapani",s:"TP",r:"Sicilia"},
2423    {n:"Trento",s:"TN",r:"Trentino-Alto Adige/Südtirol"},{n:"Treviso",s:"TV",r:"Veneto"},
2424    {n:"Trieste",s:"TS",r:"Friuli-Venezia Giulia"},{n:"Udine",s:"UD",r:"Friuli-Venezia Giulia"},
2425    {n:"Valle d'Aosta/Vallée d'Aoste",s:"AO",r:"Valle d'Aosta/Vallée d'Aoste"},{n:"Varese",s:"VA",r:"Lombardia"},
2426    {n:"Venezia",s:"VE",r:"Veneto"},{n:"Verbano-Cusio-Ossola",s:"VB",r:"Piemonte"},{n:"Vercelli",s:"VC",r:"Piemonte"},
2427    {n:"Verona",s:"VR",r:"Veneto"},{n:"Vibo Valentia",s:"VV",r:"Calabria"},{n:"Vicenza",s:"VI",r:"Veneto"},
2428    {n:"Viterbo",s:"VT",r:"Lazio"}
2429  ];
2430
2431  /** Restituisce true se la lista provincie è "vuota di senso", cioè:
2432   *  - vuota
2433   *  - contiene solo entries placeholder ("regionale", "regione X", nome
2434   *    regione stesso, o nome regione con separator diverso es. "Abruzzo
2435   *    / Molise" vs "Abruzzo-Molise") */
2436  function bkIsPlaceholderProvinceList(provList,regionName){
2437    if(!provList||!provList.length)return true;
2438    var rn=(regionName||"").toLowerCase().trim();
2439    /* normalizza: rimuovi separator / - e collassa spazi → matcha
2440     * "Abruzzo / Molise" con "Abruzzo-Molise" / "Abruzzo Molise" */
2441    var rnNorm=rn.replace(/[\/\-]/g," ").replace(/\s+/g," ").trim();
2442    var allPlaceholder=true;
2443    for(var i=0;i<provList.length;i++){
2444      var d=((provList[i]&&provList[i].descrizione)||"").toLowerCase().trim();
2445      var dNorm=d.replace(/[\/\-]/g," ").replace(/\s+/g," ").trim();
2446      var isPh=/regional[ei]?\b/.test(d)
2447        || (rn&&d===rn)
2448        || (rn&&d===("regione "+rn))
2449        || (rn&&d.indexOf(rn)===0&&/regional/.test(d))
2450        || (rnNorm&&dNorm===rnNorm); // "abruzzo-molise" === "abruzzo molise"
2451      if(!isPh){allPlaceholder=false;break;}
2452    }
2453    return allPlaceholder;
2454  }
2455
2456  /** Normalizza una stringa: lowercase + rimuove punteggiatura e accenti
2457   *  e collassa whitespace. Permette match tolleranti tra "Emilia Romagna",
2458   *  "Emilia-Romagna", "EMILIA ROMAGNA", ecc. */
2459  function bkNormalizeRegion(s){
2460    if(!s)return "";
2461    return String(s).toLowerCase()
2462      .replace(/[\u00e0\u00e1\u00e2]/g,"a").replace(/[\u00e8\u00e9\u00ea]/g,"e")
2463      .replace(/[\u00ec\u00ed\u00ee]/g,"i").replace(/[\u00f2\u00f3\u00f4]/g,"o")
2464      .replace(/[\u00f9\u00fa\u00fb]/g,"u")
2465      .replace(/[\-\.\/']/g," ").replace(/\s+/g," ").trim();
2466  }
2467
2468  /** Mapping di sub-regioni / connessioni regionali noti che NON hanno il nome
2469   *  pieno della regione ma vi appartengono comunque. Esteso quando emergono
2470   *  nuovi naming sui tenant. */
2471  var BK_SUBREGION_MAP={
2472    "romagna":"Emilia-Romagna",
2473    "emilia centrale":"Emilia-Romagna",
2474    "emilia":"Emilia-Romagna"
2475  };
2476
2477  /** Estrae il nome regione da una descrizione placeholder.
2478   *  "TOSCANA REGIONALE" → "Toscana"
2479   *  "Lazio Regionale" → "Lazio"
2480   *  "Emilia Romagna" / "EMILIA-ROMAGNA" → "Emilia-Romagna"
2481   *  "Romagna" → "Emilia-Romagna" (sub-regione)
2482   *  Restituisce "" se nessuna regione italiana riconosciuta. */
2483  var BK_REGION_NAMES=null;
2484  function bkExtractRegionName(text){
2485    if(!text)return "";
2486    if(!BK_REGION_NAMES){
2487      var seen={};BK_REGION_NAMES=[];
2488      for(var i=0;i<BK_IT_PROVINCES.length;i++){
2489        var r=BK_IT_PROVINCES[i].r;if(!seen[r]){seen[r]=true;BK_REGION_NAMES.push(r);}
2490      }
2491    }
2492    var t=bkNormalizeRegion(text);
2493    if(!t)return "";
2494    /* Sub-region check (Romagna → Emilia-Romagna, ecc.) */
2495    var subKeys=Object.keys(BK_SUBREGION_MAP);
2496    /* Più lungo prima per evitare match parziali ("emilia" vs "emilia centrale") */
2497    subKeys.sort(function(a,b){return b.length-a.length;});
2498    for(var sk=0;sk<subKeys.length;sk++){
2499      if(t.indexOf(subKeys[sk])!==-1)return BK_SUBREGION_MAP[subKeys[sk]];
2500    }
2501    /* Match diretto contro nomi regione, normalizzati. Lungo prima. */
2502    var sorted=BK_REGION_NAMES.slice().sort(function(a,b){return b.length-a.length;});
2503    for(var j=0;j<sorted.length;j++){
2504      var keyN=bkNormalizeRegion(sorted[j]);
2505      var parts=keyN.split(" ");
2506      /* Prova match sull'intero, poi sui pezzi separati da / (es. "valle d aosta vallee d aoste"). */
2507      if(t.indexOf(keyN)!==-1)return sorted[j];
2508      /* Per regioni composite, prova anche solo la prima parte (Trentino, Friuli, Valle) */
2509      if(parts.length>1&&t.indexOf(parts[0])!==-1&&parts[0].length>=4)return sorted[j];
2510    }
2511    return "";
2512  }
2513
2514  /** Determina la regione italiana a cui appartiene un'entry "provincia".
2515   *  Usa BK_IT_PROVINCES (per nomi standard tipo Bologna, Bergamo)
2516   *  + bkExtractRegionName (per sub-regioni e placeholder).
2517   *
2518   *  Match TOLLERANTE per gestire varianti API:
2519   *  - "Monza Brianza" ↔ "Monza e della Brianza"
2520   *  - "Reggio Calabria" ↔ "Reggio di Calabria"
2521   *  - "Bolzano" ↔ "Bolzano/Bozen"
2522   *  Logica: esatto, poi inclusione bidirezionale, poi prima parola distintiva. */
2523  function bkRegionForProvinceEntry(entry){
2524    if(!entry||!entry.descrizione)return "";
2525    var d=bkNormalizeRegion(entry.descrizione);
2526    if(!d)return "";
2527    /* 1) Match esatto */
2528    for(var i=0;i<BK_IT_PROVINCES.length;i++){
2529      if(bkNormalizeRegion(BK_IT_PROVINCES[i].n)===d)return BK_IT_PROVINCES[i].r;
2530    }
2531    /* 2) Match per parole intere (NON substring per evitare "Romagna"
2532     *    che matcha "Roma"). Una entry matcha un province ufficiale se
2533     *    l'insieme di parole "significative" (≥4 char) coincide come set
2534     *    o uno è sottoinsieme dell'altro.
2535     *    - "Monza Brianza" → words=["monza","brianza"]
2536     *    - "Monza e della Brianza" → significant words=["monza","brianza"]
2537     *    → set uguale → match.
2538     *    - "Romagna" → words=["romagna"]
2539     *    - "Roma" → words=["roma"] → diversi → NO match. */
2540    var dWords=d.split(" ").filter(function(w){return w.length>=4;});
2541    if(dWords.length){
2542      for(var k=0;k<BK_IT_PROVINCES.length;k++){
2543        var pn=bkNormalizeRegion(BK_IT_PROVINCES[k].n);
2544        var pnWords=pn.split(" ").filter(function(w){return w.length>=4;});
2545        if(!pnWords.length)continue;
2546        /* Tutti dWords ⊆ pnWords? */
2547        var allIn=dWords.every(function(dw){return pnWords.indexOf(dw)!==-1;});
2548        /* Oppure tutti pnWords ⊆ dWords? */
2549        var revIn=pnWords.every(function(pw){return dWords.indexOf(pw)!==-1;});
2550        if(allIn||revIn)return BK_IT_PROVINCES[k].r;
2551      }
2552    }
2553    /* 3) Fallback: estrai regione dal nome (sub-regioni e placeholder). */
2554    return bkExtractRegionName(entry.descrizione);
2555  }
2556
2557  /** Filtra una lista di province per tenere solo quelle che geograficamente
2558   *  appartengono alla regione data. Se nessuna entry matcha, ritorna la lista
2559   *  originale (safety: meglio mostrare il picker pollutato che bloccare il flow). */
2560  function bkFilterProvincesByRegion(provList,regionName){
2561    if(!provList||!provList.length||!regionName)return provList||[];
2562    var target=bkNormalizeRegion(regionName);
2563    if(!target)return provList;
2564    var filtered=provList.filter(function(p){
2565      var prRegion=bkRegionForProvinceEntry(p);
2566      return prRegion&&bkNormalizeRegion(prRegion)===target;
2567    });
2568    /* Se il filtro NON trova nulla, NON degradiamo l'esperienza:
2569     * potrebbe essere un tenant atipico — mostriamo la lista intera. */
2570    return filtered.length>0?filtered:provList;
2571  }
2572
2573  /** Trova nella lista bkRegioni l'entry placeholder "X REGIONALE" che
2574   *  corrisponde a una data regione geografica (es. detected via GPS).
2575   *  Ritorna l'entry trovata o null. */
2576  function bkFindRegionalEntry(regioniList,regionName){
2577    if(!regioniList||!regioniList.length||!regionName)return null;
2578    var rn=String(regionName).toLowerCase();
2579    for(var i=0;i<regioniList.length;i++){
2580      var d=((regioniList[i]&&regioniList[i].descrizione)||"").toLowerCase();
2581      if(d.indexOf(rn)!==-1)return regioniList[i];
2582    }
2583    return null;
2584  }
2585
2586  /** Espande la lista placeholder con le province hardcoded della regi
2586one,
2587   *  formattate come l'API: {descrizione, codice}.
2588   *  Supporta nomi multi-regione (es. "Abruzzo / Molise") splittando su
2589   *  "/" e facendo union delle province di entrambe — perché il Planner
2590   *  abruzzomolise espone una sola "provincia" placeholder per entrambe
2591   *  le regioni e i codici reali (CH/AQ/PE/TE/CB/IS) vanno bene tutti. */
2592  function bkExpandProvinceFallback(regionName){
2593    if(!regionName)return [];
2594    var rl=String(regionName).toLowerCase().trim();
2595    var parts=rl.split("/").map(function(s){return s.trim();}).filter(Boolean);
2596    var seen={};
2597    var out=[];
2598    for(var i=0;i<parts.length;i++){
2599      var pn=parts[i];
2600      for(var j=0;j<BK_IT_PROVINCES.length;j++){
2601        var pr=BK_IT_PROVINCES[j];
2602        if(pr.r.toLowerCase()===pn&&!seen[pr.s]){
2603          seen[pr.s]=true;
2604          out.push({descrizione:pr.n,codice:pr.s,sigla:pr.s,_fallback:true});
2605        }
2606      }
2607    }
2608    return out;
2609  }
2610
2611  /* ── Timezone → default region (smart pre-select, no permission needed) */
2612  var BK_TZ_REGION_MAP={
2613    "Europe/Rome":["Veneto","Lombardia","Toscana","Emilia-Romagna","Lazio","Piemonte","Campania","Sicilia"]
2614  };
2615  function bkGuessRegionByTimezone(){
2616    try{
2617      var tz=Intl.DateTimeFormat().resolvedOptions().timeZone;
2618      return BK_TZ_REGION_MAP[tz]||null;
2619    }catch(_e){return null;}
2620  }
2621
2622  /* ── configuration ────────────────────────────────────────── */
2623  function resolveOpts(userOpts) {
2624    var o = userOpts || {};
2625    /* DEMOPROMO: il server (via /api/widget-inline) inietta
2626       window.__CAF_DEMOPROMO__ === true quando l'env ha DEMOPROMO=si. In quel
2627       caso il SOLO sistema di prenotazione gira su dati di esempio (vedi
2628       _bkDemoFetch) e il nome tenant viene forzato a __CAF_CHATBOT_NAME__
2629       (NEXT_PUBLIC_CHATBOT_NAME, default "CAF CISL Veneto"), sovrascrivendo
2630       eventuale data-name dell'host. Flag spento ⇒ nessuna variazione. */
2631    var demopromo = (typeof window !== "undefined" && window.__CAF_DEMOPROMO__ === true);
2632    var demoName = (typeof window !== "undefined" && window.__CAF_CHATBOT_NAME__) || "CAF CISL Veneto";
2633    /* PIEMONTE: iniettato da /api/widget-inline quando l'env ha PIEMONTE=si.
2634       Quando true, allo step DATA e ORA del wizard prenotazione il calendario
2635       carica TUTTA la disponibilità PRIMA di mostrarsi (drena la paginazione
2636       "Carica altri orari" fino in fondo, vedi _bkDrainAllSpazi): tutti i mesi
2637       appaiono già completi e navigabili, senza il bottone "Carica altri orari".
2638       Flag spento ⇒ nessuna variazione. */
2639    var piemonte = (typeof window !== "undefined" && window.__CAF_PIEMONTE__ === true);
2640    return {
2641      baseUrl: (o.baseUrl || getAttr("base-url") || "").replace(/\/$/, ""),
2642      color: o.color || getAttr("color") || "#059669",
2643      name: demopromo ? demoName : (o.name || getAttr("name") || "CAF CISL - Toscana"),
2644      logo:
2645        o.logo ||
2646        getAttr("logo") ||
2647        "https://indesignstudio.b-cdn.net/logoCAFCISL.png",
2648      chatLogo:
2649        o.chatLogo ||
2650        getAttr("chat-logo") ||
2651        "https://indesignstudio.b-cdn.net/logoChat.png",
2652      welcome:
2653        o.welcome ||
2654        getAttr("welcome") ||
2655        (demopromo
2656          ? "Ciao, sono l'assistente virtuale di " + demoName + ". Come posso esserti utile?"
2657          : "Ciao, sono l'assistente virtuale di CAF CISL - Toscana. Come posso esserti utile?"),
2658      /* DOMANDEFISSE: se il server (via /api/widget-inline) inietta
2659         window.__CAF_DOMANDEFISSE__ === false, le domande preimpostate
2660         sotto il welcome non vengono mai mostrate. Override forte: vale
2661         anche quando il consumer passa quickReplies o data-quick-replies. */
2662      quickReplies: (typeof window !== "undefined" && window.__CAF_DOMANDEFISSE__ === false)
2663        ? []
2664        : (o.quickReplies
2665          ? o.quickReplies
2666          : getAttr("quick-replies")
2667            ? getAttr("quick-replies").split("|").filter(Boolean)
2668            : [
2669                "Che spese si possono detrarre?",
2670                "Vorrei ricevere più informazioni",
2671                "Qual è il centro CAF più vicino a me?",
2672              ]),
2673      zIndex: o.zIndex || getAttr("z-index") || "9999",
2674      position: o.position || getAttr("position") || "right", // "right" | "left"
2675      /* a11y #C63: data-lang sul tag script (o opts.lang) FORZA la locale del
2676       * widget, con precedenza su navigator.language (vedi bkDetectLang). */
2677      lang: o.lang || getAttr("lang") || "",
2678      showBubble: o.showBubble !== undefined
2679        ? !!o.showBubble
2680        : getAttr("show-bubble") !== "false",
2681      voice: o.voice !== undefined
2682        ? !!o.voice
2683        : (getAttr("voice") || "").trim().toLowerCase() === "si",
2684      chiedomail: o.chiedomail !== undefined
2685        ? !!o.chiedomail
2686        : (getAttr("chiedomail") || "").trim().toLowerCase() === "si",
2687      privacy: o.privacy !== undefined
2688        ? !!o.privacy
2689        : (getAttr("privacy") || "").trim().toLowerCase() === "si"
2690          || (typeof window !== "undefined" && window.__CAF_PRIVACY__ === true),
2691      /* CONSENSO: gate di consenso privacy alla prima apertura della chat
2692         (checkbox + link all'informativa + bottone di approvazione). Attivo
2693         con opts.consenso, data-consenso="si" o window.__CAF_CONSENSO__
2694         iniettato da /api/widget-inline quando l'env ha CONSENSO=si.
2695         Default OFF ⇒ nessuna variazione per i tenant esistenti. */
2696      /* CONSENSO — default COMPLIANT-ON (gate consenso privacy + eta' >=14,
2697         L. 132/2025 art. 4 c.4). Attivo salvo opt-out esplicito:
2698         data-consenso="no", opts.consenso===false o __CAF_CONSENSO__===false. */
2699      consenso: o.consenso !== undefined
2700        ? !!o.consenso
2701        : (getAttr("consenso") || "").trim().toLowerCase() === "no"
2702          ? false
2703          : !(typeof window !== "undefined" && window.__CAF_CONSENSO__ === false),
2704      /* AI DISCLOSURE (AI Act art. 50) — default COMPLIANT-ON: la natura di
2705         IA e' sempre dichiarata e NON e' rimovibile dall'integrazione host
2706         (nessun attributo data-*): l'unico opt-out, riservato a usi fuori
2707         ambito AI Act, e' l'env server AI_DISCLOSURE=no (iniettata dalla
2708         route come __CAF_AI_DISCLOSURE__===false). */
2709      aiDisclosure: !(typeof window !== "undefined" && window.__CAF_AI_DISCLOSURE__ === false),
2710      /* URL dell'informativa privacy del sito ospite (opts / data-privacy-url
2711         / env NEXT_PUBLIC_PRIVACY_URL iniettata come __CAF_PRIVACY_URL__).
2712         Usato dal gate consenso chat e, gia' oggi, dai link privacy del
2713         wizard prenotazione che leggono opts.privacyUrl. */
2714      privacyUrl: o.privacyUrl
2715        || getAttr("privacy-url")
2716        || (typeof window !== "undefined" && typeof window.__CAF_PRIVACY_URL__ === "string" ? window.__CAF_PRIVACY_URL__ : "")
2717        || "",
2718      ecommerce: o.ecommerce !== undefined
2719        ? !!o.ecommerce
2720        : (getAttr("ecommerce") || "").trim().toLowerCase() === "si",
2721      productsUrl: o.productsUrl || getAttr("products-url") || "",
2722      demopromo: demopromo,
2723      piemonte: piemonte,
2724    };
2725  }
2726
2727  /* ── table renderer helper ─────────────────────────────────── */
2728  function renderTableBlock(rows, c) {
2729    var parsed = [];
2730    var sepIdx = -1;
2731    var maxCols = 0;
2732    for (var r = 0; r < rows.length; r++) {
2733      var row = rows[r];
2734      if (row.charAt(0) === "|") row = row.substring(1);
2735      if (row.charAt(row.length - 1) === "|") row = row.substring(0, row.length - 1);
2736      var cells = row.split("|").map(function (s) { return s.trim(); });
2737      parsed.push(cells);
2738      if (cells.length > maxCols) maxCols = cells.length;
2739      /* Riga separatrice: ogni cella è solo trattini (":" opzionali) e la
2740       * riga contiene almeno "--" o ":". Così una riga DATI di soli "-"
2741       * (es. "| - | - |") non viene scambiata per separatore e cancellata. */
2742      if (
2743        sepIdx < 0 &&
2744        cells.every(function (cl) { return cl === "" || /^:?-+:?$/.test(cl); }) &&
2745        /--|:/.test(row) &&
2746        cells.join("") !== ""
2747      ) {
2748        sepIdx = r;
2749      }
2750    }
2751    var h = '<div style="overflow-x:auto;margin:8px 0;border-radius:10px;border:1px solid #e2e8f0">';
2752    h += '<table style="width:100%;border-collapse:collapse;font-size:12.5px">';
2753    var dataIdx = 0;
2754    for (var i = 0; i < parsed.length; i++) {
2755      if (i === sepIdx) continue;
2756      var isHdr = sepIdx >= 0 && i < sepIdx;
2757      h += "<tr>";
2758      /* pad a maxCols: righe irregolari non producono tabelle sghembe */
2759      for (var j = 0; j < maxCols; j++) {
2760        var cell = j < parsed[i].length ? parsed[i][j] : "";
2761        var tag = isHdr ? "th" : "td";
2762        var st = isHdr
2763          ? "background:" + c + "1a;color:#065f46;padding:7px 10px;text-align:left;font-weight:600;font-size:11.5px;border-bottom:2px solid " + c + "40"
2764          : "padding:7px 10px;border-bottom:1px solid #f1f5f9;color:#334155" + (dataIdx % 2 === 1 ? ";background:#f8fafc" : "");
2765        h += "<" + tag + (isHdr ? ' scope="col"' : "") + ' style="' + st + '">' + cell + "</" + tag + ">";
2766      }
2767      h += "</tr>";
2768      if (!isHdr) dataIdx++;
2769    }
2770    h += "</table></div>";
2771    return h;
2772  }
2773
2774  /* ── markdown renderer ──────────────────────────────────────── */
2775  function renderMarkdown(text, c) {
2776    c = c || "#059669";
2777    var p = String(text == null ? "" : text)
2778      .replace(/\r\n?/g, "\n")
2779      .replace(/&/g, "&amp;")
2780      .replace(/</g, "&lt;")
2781      .replace(/>/g, "&gt;");
2782
2783    /* ── STEP 0: Extract code blocks (``` ... ```) before any processing ── */
2784    var codeBlocks = [];
2785    p = p.replace(/```[\w+#-]*[^\S\n]*\n?([\s\S]*?)```/g, function (_, co
2785de) {
2786      var idx = codeBlocks.length;
2787      codeBlocks.push(code.replace(/\n$/, ""));
2788      return "\n%%CODEBLOCK_" + idx + "%%\n";
2789    });
2790    /* Fence rimasta aperta (streaming troncato o LLM che dimentica la
2791     * chiusura): tutto ciò che segue diventa comunque un code block, così
2792     * non restano mai backtick tripli grezzi visibili. */
2793    p = p.replace(/```[\w+#-]*[^\S\n]*\n?([\s\S]*)$/, function (_, code) {
2794      var idx = codeBlocks.length;
2795      codeBlocks.push(code.replace(/\n$/, ""));
2796      return "\n%%CODEBLOCK_" + idx + "%%\n";
2797    });
2798
2799    /* ── STEP 0a: estrazione a placeholder di TUTTO ciò che non deve
2800     * essere toccato da bold/italic/euristiche: inline code, link
2801     * markdown, URL bare, URL www., marker prodotto. Questo è critico
2802     * per una linkificazione affidabile al 100%: se gli URL venissero
2803     * processati DOPO bold/italic, "**Header:**https://..." produrrebbe
2804     * "</strong>https://..." e i regex fallirebbero silenziosamente.
2805     * I placeholder sono pass-through per tutti gli step successivi e
2806     * vengono ripristinati alla fine (STEP 5). */
2807    var inlinePH = [];
2808    var pushPH = function (html) {
2809      var idx = inlinePH.length;
2810      inlinePH.push(html);
2811      return "%%CAFPH_" + idx + "%%";
2812    };
2813    var safeHref = function (url) { return url.replace(/"/g, "&quot;"); };
2814    /* Il testo è GIÀ HTML-escaped: per le label visibili niente secondo
2815     * escape (produrrebbe "&amp;amp;" → "amp;" visibile). decodeEnt serve
2816     * solo per troncare la label senza tagliare a metà una entity. */
2817    var decodeEnt = function (s) { return s.replace(/&amp;/g, "&"); };
2818    var escBase = function (s) {
2819      return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
2820    };
2821    /* Enfasi inline (usata anche dentro il testo dei link markdown).
2822     * L'ordine è: barrato → triplo → bold → italic. Il contenuto italic
2823     * esclude <> così non può attraversare un tag <strong> appena iniettato
2824     * dal bold (altrimenti "5*3=15***x**" produce <em>…<strong></em>). */
2825    var inlineEmph = function (s) {
2826      s = s.replace(/~~([^~\n]+)~~/g, "<s>$1</s>");
2827      s = s.replace(/\*\*\*([^\n*|]+)\*\*\*/g, '<strong class="caf-strong"><em>$1</em></strong>');
2828      s = s.replace(/\*\*([^\n|]+?)\*\*/g, '<strong class="caf-strong">$1</strong>');
2829      s = s.replace(/(?<!\*)\*([^*\n<>|]+)\*(?!\*)/g, "<em>$1</em>");
2830      /* Safety net: sequenze di 2+ asterischi rimaste orfane (bold non
2831       * bilanciato dallo streaming/LLM) non devono MAI restare visibili.
2832       * Le rimuovo, tranne quando racchiuse tra cifre su entrambi i lati
2833       * ("5**3" = potenza, non enfasi). Poi ritento l'italic: coppie di
2834       * "*" diventate adiacenti ("*a**b*" → "*ab*") vengono recuperate. */
2835      s = s.replace(/(?<!\d)\*{2,}|\*{2,}(?!\d)/g, "");
2836      s = s.replace(/(?<!\*)\*([^*\n<>|]+)\*(?!\*)/g, "<em>$1</em>");
2837      return s;
2838    };
2839
2840    /* 0a.1) Inline code `...` PRIMA di tutto: il contenuto resta letterale
2841     * (niente bold/URL/telefono dentro il codice). */
2842    p = p.replace(/`([^`\n]+)`/g, function (_, code) {
2843      return pushPH('<code class="caf-code-inline">' + code + "</code>");
2844    });
2845
2846    /* Whitelist of known official URLs used by the CAF CISL Veneto bot.
2847     * Sorted by length DESC so longest-prefix-match wins (prevents a shorter
2848     * URL from capturing when the captured text is actually a longer one). */
2849    var KNOWN_URLS = [
2850      "https://prenotazioni.cafcisl.it/env.php/prenotazione/provincia/VEN",
2851      "https://www.cafcislveneto.it/contatti/",
2852      "https://www.cislpadovarovigo.it/",
2853      "https://www.cislbellunotreviso.it/",
2854      "https://www.cislverona.it/",
2855      "https://www.cislvicenza.it/",
2856      "https://www.cislvenezia.it/"
2857    ];
2858
2859    /* 0a.2) Markdown link syntax [text](url) — must run before bare URL
2860     * match. Le parentesi bilanciate nell'URL (es. wikipedia "..._(fisco)")
2861     * sono ammesse un livello. Il testo del link può contenere enfasi. */
2862    p = p.replace(
2863      /\[([^\]\n]+)\]\((https?:\/\/(?:[^()\s]|\([^()\s]*\))+)\)/g,
2864      function (_, txt, url) {
2865        return pushPH(
2866          '<a href="' + safeHref(url) + '" target="_blank" rel="noopener noreferrer" style="color:#047857;text-decoration:underline;text-underline-offset:2px">' + inlineEmph(txt) + "</a>"
2867        );
2868      }
2869    );
2870
2871    /* 0a.3) Bare URLs anywhere in the text — whitelist + trim euristico. */
2872    p = p.replace(
2873      /(https?:\/\/[^\s<"*`]+)/g,
2874      function (url) {
2875        var trailing = "";
2876        /* a) Whitelist match: URL ufficiale → href bloccato su quello
2877         *    esatto, il resto è testo. Immune agli errori dell'LLM. */
2878        var matched = null;
2879        for (var i = 0; i < KNOWN_URLS.length; i++) {
2880          if (url.indexOf(KNOWN_URLS[i]) === 0) { matched = KNOWN_URLS[i]; break; }
2881        }
2882        if (matched) {
2883          trailing = url.slice(matched.length);
2884          url = matched;
2885        } else {
2886          /* b) trim punteggiatura/markdown in coda (inclusi ** e backtick) */
2887          var punctMatch = url.match(/[.,;:!?)\]}>'"*`]+$/);
2888          if (punctMatch) {
2889            trailing = punctMatch[0] + trailing;
2890            url = url.slice(0, -punctMatch[0].length);
2891          }
2892          /* le ")" di parentesi APERTE nell'URL tornano dentro l'href
2893           * (es. wikipedia .../CAF_(fisco)) */
2894          while (
2895            trailing.charAt(0) === ")" &&
2896            (url.split("(").length - 1) > (url.split(")").length - 1)
2897          ) {
2898            url += ")";
2899            trailing = trailing.slice(1);
2900          }
2901          /* c) parola TitleCase incollata SUBITO dopo il TLD (senza slash):
2902           *    "https://sito.itVaiSubito" → url sito.it + testo "VaiSubito".
2903           *    I path TitleCase legittimi (/Prenotazioni) restano intatti. */
2904          var tc = url.match(/^(https?:\/\/[a-z0-9.\-]+\.[a-z]{2,10})([A-ZÀ-Ü][a-zà-ü][\w\-À-Üà-ü]*)$/);
2905          if (tc) {
2906            trailing = tc[2] + trailing;
2907            url = tc[1];
2908          }
2909        }
2910        var phCut = url.indexOf("%%");
2911        if (phCut >= 0) { trailing = url.slice(phCut) + trailing; url = url.slice(0, phCut); }
2912        var label = decodeEnt(url).replace(/^https?:\/\/(www\.)?/, "").replace(/\/$/, "");
2913        if (label.length > 40) label = label.slice(0, 37) + "…";
2914        return pushPH(
2915          '<a href="' + safeHref(url) + '" target="_blank" rel="noopener noreferrer" class="caf-url-badge"><span aria-hidden="true">🔗</span>' + escBase(label) + "</a>"
2916        ) + trailing;
2917      }
2918    );
2919
2920    /* 0a.4) URL "www." senza protocollo → stesso badge, href https:// */
2921    p = p.replace(/(^|[\s(*_])(www\.[^\s<"*`]+)/gm, function (m, pre, url) {
2922      var trailing = "";
2923      var punctMatch = url.match(/[.,;:!?)\]}>'"*`]+$/);
2924      if (punctMatch) {
2925        trailing = punctMatch[0];
2926        url = url.slice(0, -punctMatch[0].length);
2927      }
2928      if (!/^www\.[a-z0-9\-]+(\.[a-z0-9\-]+)+/i.test(url)) return m;
2929      var phCutW = url.indexOf("%%");
2930      if (phCutW >= 0) { trailing = url.slice(phCutW) + trailing; url = url.slice(0, phCutW); }
2931      var label = decodeEnt(url).replace(/^www\./, "").replace(/\/$/, "");
2932      if (label.length > 40) label = label.slice(0, 37) + "…";
2933      return pre + pushPH(
2934        '<a href="https://' + safeHref(url) + '" target="_blank" rel="noopener noreferrer" class="caf-url-badge"><span aria-hidden="true">🔗</span>' + escBase(label) + "</a>"
2935      ) + trailing;
2936    });
2937
2938    /* ── STEP 0a-bis: Estrazione marker prodotti ecommerce
2939     * Sintassi accettata nel testo del bot:
2940     *   [[PROD:ID]]      → ID = ID prodotto (es. 2401)
2941     *   [[PROD:CODE]]    → ID = CODE prodotto (es. 3015000782)
2942     *   [[PROD:CODE_xxx]] o [[PROD:ID_xxx]] → forma esplicita
2943     * I marker vengono sostituiti con un anchor HTML che, in modalità
2944     * ecommerce, viene poi convertito in una card prodotto interattiva
2945     * dopo che innerHTML è stato applicato (vedi _renderEcommerceCards). */
2946    p = p.replace(/\[\[PROD:([A-Z0-9_]+)\]\]/gi, function (_, key) {
2947      var cleaned = String(key).trim();
2948      var dataKey;
2949      if (/^ID_/i.test(cleaned)) dataKey = "ID:" + cleaned.replace(/^ID_/i, "");
2950      else if (/^CODE_/i.test(cleaned)) dataKey = "CODE:" + cleaned.replace(/^CODE_/i, "");
2951      else if (/^\d{8,}$/.test(cleaned)) dataKey = "CODE:" + cleaned;
2952      else dataKey = "ID:" + cleaned;
2953      return pushPH(
2954        '<span class="caf-prod-anchor" data-prod-key="' + escapeHtml(dataKey) + '"></span>'
2955      );
2956    });
2957
2958    /* ── STEP 0b: Re-insert newlines that the streaming API strips ──
2959     * Ogni regola di "glue" è pipe-guarded: NON scatta se la riga corrente
2960     * contiene "|" (una cella di tabella non va mai spezzata). */
2961    var lineHasPipe = function (s, off) {
2962      var ls = s.lastIndexOf("\n", off);
2963      var le = s.indexOf("\n", off);
2964      return s.substring(ls + 1, le === -1 ? s.length : le).indexOf("|") !== -1;
2965    };
2966    // Normalise arrow-dashes (--->, ---->) to plain --- so HR detection
2967    // works. NB: la freccia "-->" (2 trattini) è testo legittimo e resta.
2968    p = p.replace(/-{3,}>/g, "---");
2969    // Table: split concatenated rows (streaming joins "| … || … |" into one
2970    // line). Solo su righe con abbastanza pipe da contenere DUE righe
2971    // (>= 6): una cella vuota "| a || b |" non viene spezzata in due righe.
2972    p = p.split("\n").map(function (line) {
2973      if (line.indexOf("||") === -1) return line;
2974      var pipes = line.match(/\|/g).length;
2975      return pipes >= 6 ? line.replace(/\|\|/g, "|\n|") : line;
2976    }).join("\n");
2977    // Table: split first row from preceding non-table text (e.g. "text:| Header | Value |")
2978    p = p.replace(/^([^|\n]+?)(\|.+\|)\s*$/gm, "$1\n$2");
2979    // Table: split trailing non-table content after last | in a table row
2980    p = p.replace(/^(\|.+\|)([^|\n]+)$/gm, "$1\n$2");
2981    // Heading glue: mai spezzare DENTRO una sequenza di # (prima
2982    // "## Titolo" a inizio riga diventava "#\n\n# Titolo": il primo #
2983    // faceva da prefisso [^\n] e il break cadeva in mezzo ai cancelletti).
2984    p = p.replace(/([^\n#])(#{2,3}\s)/g, function (m, pre, hd, off, s) {
2985      return lineHasPipe(s, off) ? m : pre + "\n\n" + hd;
2986    });
2987    // Il singolo # scatta solo dopo punteggiatura ("C# e F#" resta testo).
2988    p = p.replace(/([.!?;:)\]])[ \t]*(#\s)/g, function (m, pre, hd, off, s) {
2989      return lineHasPipe(s, off) ? m : pre + "\n\n" + hd;
2990    });
2991    p = p.replace(/([.!?;:)\]])\s*-\s(\*\*|[A-ZÀ-Ü])/g, function (m, pre, it, off, s) {
2992      return lineHasPipe(s, off) ? m : pre + "\n- " + it;
2993    });
2994    // Handle "- emoji" list items (e.g. "necessari:- 📋" or "determinato)- 📄")
2995    p = p.replace(/([.!?;:)\]\w])\s*-\s(\p{Extended_Pictographic})/gu, function (m, pre, it, off, s) {
2996      return lineHasPipe(s, off) ? m : pre + "\n- " + it;
2997    });
2998    // Handle "emoji- emoji" (e.g. list item after emoji)
2999    p = p.replace(/(\p{Extended_Pictographic})\s*-\s(\p{Extended_Pictographic}|\*\*|[A-ZÀ-Üa-z])/gu, function (m, pre, it, off, s) {
3000      return lineHasPipe(s, off) ? m : pre + "\n- " + it;
3001    });
3002    p = p.replace(/([^\n\s])(- \*\*)/g, function (m, pre, it, off, s) {
3003      return lineHasPipe(s, off) ? m : pre + "\n" + it;
3004    });
3005    // Glue "parola- Maiuscola": trattino attaccato alla parola precedente e
3006    // seguito da spazio + Maiuscola/bold/emoji = item di lista incollato
3007    // (es. "Documenti- CIE- Tessera"). La classe include "*" così anche
3008    // "**grassetto**- Voce" (dopo la chiusura del bold) spezza il bullet.
3009    p = p.replace(/([\p{L}\d*])- (?=\*\*|[A-ZÀ-Ü]|\p{Extended_Pictographic})/gu, function (m, pre, off, s) {
3010      return lineHasPipe(s, off) ? m : pre + "\n- ";
3011    });
3012    // Liste numerate incollate: "testo. 1. Vai" → newline prima di "1.".
3013    // (?<!\d) blocca gli orari ("ore 9.30. Vieni": cifra prima del punto);
3014    // i lookbehind su n./nr./art./num. bloccano "legge n. 104. Per".
3015    p = p.replace(/(?<!\d)(?<!\b[Nn])(?<!\b[Nn]r)(?<![Aa]rt)(?<![Nn]um)([.!?;:)\]])\s*(\d{1,3}[.)]\s)(?=[A-ZÀ-Ü*])/g, function (m, pre, it, off, s) {
3016      return lineHasPipe(s, off) ? m : pre + "\n" + it;
3017    });
3018    p = p.replace(/([^\n])(•\s)/g, function (m, pre, it, off, s) {
3019      return lineHasPipe(s, off) ? m : pre + "\n" + it;
3020    });
3021    p = p.replace(/(#{2,3}\s[^#\n]{3,}?[.!?)\]])([A-ZÀ-Ü*\-•\d])/g, function (m, pre, it, off, s) {
3022      return lineHasPipe(s, off) ? m : pre + "\n\n" + it;
3023    });
3024    // Heading incollato al corpo SENZA punteggiatura né spazio
3025    // ("## ScadenzeLa prima rata…"): in italiano una minuscola seguita SUBITO
3026    // da Maiuscola+minuscola (camelCase) è sempre un confine di riga perso
3027    // dallo streaming. Si separa il titolo (almeno 3 char) dal corpo al primo
3028    // di questi confini. Gli acronimi ("IMU", "ISEE") non hanno il pattern
3029    // minuscola→Maiuscola→minuscola e restano intatti.
3030    p = p.replace(/(#{1,6}\s+[^\n]{2,}?[a-zà-ÿ])([A-ZÀ-Þ][a-zà-ÿ]+[^\n]*)$/gm, function (m, head, body, off, s) {
3031      return lineHasPipe(s, off) ? m : head + "\n" + body;
3032    });
3033    // Emoji SUBITO dopo punteggiatura di fine frase (senza spazio) = testo
3034    // incollato dallo streaming → nuovo paragrafo. Con lo spazio in mezzo
3035    // ("Perfetto! 👍 Andiamo") la frase resta intera.
3036    p = p.replace(/([.!?])(\p{Extended_Pictographic})/gu, function (m, pre, emo, off, s) {
3037      return lineHasPipe(s, off) ? m : pre + "\n\n" + emo;
3038    });
3039    // HR: add newline before ---, but skip --- inside table separator rows
3040    p = p.replace(/([^\n])---/g, function (m, pre, off, s) {
3041      var ls = s.lastIndexOf("\n", off);
3042      if (s.substring(ls + 1, off + 1).indexOf("|") >= 0) return m;
3043      return pre + "\n---";
3044    });
3045    // HR: split content after --- onto new line, but skip table separators
3046    p = p.replace(/---[^\S\n]*([^\s-])/gu, function (m, after, off, s) {
3047      var ls = s.lastIndexOf("\n", off);
3048      if (s.substring(ls + 1, off).indexOf("|") >= 0) return m;
3049      return "---\n" + after;
3050    });
3051    // Bullet markdown "* item" → normalizzato a "- item" PRIMA dell'enfasi,
3052    // così l'asterisco del bullet non viene mangiato da bold/italic.
3053    p = p.replace(/(^|\n)\*[ \t]+(?=\S)/g, "$1- ");
3054
3055    /* ── STEP 1: Enfasi inline ──
3056     * ~~barrato~~, __bold__, _corsivo_ (con confini di parola: snake_case
3057     * resta intatto), poi ***`, **, * via inlineEmph. Il bold è vincolato
3058     * alla singola riga: un "**" orfano non trascina in grassetto tutto
3059     * il resto del messaggio. */
3060    p = p.replace(/(^|[\s(])__([^_\n]+)__(?=$|[\s).,:;!?])/gm, '$1<strong class="caf-strong">$2</strong>');
3061    p = p.replace(/(^|[\s(])_([^_\n]+)_(?=$|[\s).,:;!?])/gm, "$1<em>$2</em>");
3062    p = inlineEmph(p);
3063
3064    /* ── STEP 2: Split into lines and process block elements ── */
3065    var SPACER = '<div style="height:8px"></div>';
3066    var lines = p.split("\n");
3067    var html = [];
3068    var inList = false;
3069    var inBlockquote = false;
3070    var inTable = false;
3071    var tableRows = [];
3072
3073    for (var i = 0; i < lines.length; i++) {
3074      var line = lines[i];
3075      var trimmed = line.trim();
3076
3077      if (!trimmed) {
3078        if (inTable) { html.push(renderTableBlock(tableRows, c)); tableRows = []; inTable = false; }
3079        if (inList) { html.push("</div>"); inList = false; }
3080        if (inBlockquote) { html.push("</div>"); inBlockquote = false; }
3081        /* righe vuote consecutive = UN solo spacer (niente buchi giganti) */
3082        if (html[html.length - 1] !== SPACER) html.push(SPACER);
3083        continue;
3084      }
3085
3086      /* Marker di lista orfano su riga a sé ("* ", "- ", "•" senza testo):
3087       * bullet vuoto lasciato dallo streaming/LLM. Non mostrare il carattere
3088       * grezzo: la riga si ignora del tutto. */
3089      if (/^[-*•]$/.test(trimmed)) continue;
3090
3091      // Table row (lines starting and ending with |)
3092      if (/^\|.+\|$/.test(trimmed)) {
3093        if (inList) { html.push("</div>"); inList = false; }
3094        if (inBlockquote) { html.push("</div>"); inBlockquote = false; }
3095        tableRows.push(trimmed);
3096        inTable = true;
3097        continue;
3098      }
3099      if (inTable) { html.push(renderTableBlock(tableRows, c)); tableRows = []; inTable = false; }
3100
3101      // Code block placeholder (guardia: indice fuori range = testo utente)
3102      var cbMatch = trimmed.match(/^%%CODEBLOCK_(\d+)%%$/);
3103      if (cbMatch && parseInt(cbMatch[1], 10) < codeBlocks.length) {
3104        if (inList) { html.push("</div>"); inList = false; }
3105        if (inBlockquote) { html.push("</div>"); inBlockquote = false; }
3106        html.push('<pre class="caf-code-block"><code>' + codeBlocks[parseInt(cbMatch[1], 10)] + "</code></pre>");
3107        continue;
3108      }
3109
3110      // Blockquote (">", anche annidata ">>": appiattita su un livello)
3111      if (/^&gt;/.test(trimmed)) {
3112        if (inList) { html.push("</div>"); inList = false; }
3113        var bqMatch = trimmed.match(/^(?:&gt;\s*)+(.*)/);
3114        if (!inBlockquote) { html.push('<div class="caf-blockquote">'); inBlockquote = true; }
3115        html.push("<div>" + (bqMatch[1] || "") + "</div>");
3116        continue;
3117      }
3118      if (inBlockquote) { html.push("</div>"); inBlockquote = false; }
3119
3120      // Heading #..###### (con spazio) oppure ##..###### attaccato al testo
3121      // ("##Titolo"): livelli 1 / 2 / 3+ con gli stili originali.
3122      var hMatch = trimmed.match(/^(#{1,6})\s+(.+)/) || trimmed.match(/^(#{2,6})([^\s#].*)/);
3123      if (hMatch) {
3124        if (inList) { html.push("</div>"); inList = false; }
3125        var lvl = hMatch[1].length;
3126        var htxt = hMatch[2].replace(/^\s+/, "");
3127        if (lvl === 1) {
3128          html.push('<div role="heading" aria-level="3" style="display:flex;align-items:center;gap:7px;font-weight:700;font-size:1.15em;margin:12px 0 6px;color:#0f172a"><span style="display:inline-block;width:4px;min-height:18px;border-radius:999px;background:' + c + ';flex-shrink:0;align-self:stretch"></span><span style="flex:1;min-width:0">' + htxt + "</span></div>");
3129        } else if (lvl === 2) {
3130          html.push('<div role="heading" aria-level="4" style="display:flex;align-items:center;gap:7px;font-weight:700;font-size:1.05em;margin:10px 0 5px;color:#1e293b"><span style="display:inline-block;width:3px;min-height:16px;border-radius:999px;background:' + c + ';flex-shrink:0;align-self:stretch"></span><span style="flex:1;min-width:0">' + htxt + "</span></div>");
3131        } else {
3132          html.push('<div role="heading" aria-level="5" style="font-weight:700;font-size:0.95em;margin:10px 0 4px;color:#334155">' + htxt + "</div>");
3133        }
3134        continue;
3135      }
3136
3137      // Horizontal rule
3138      if (/^---+$/.test(trimmed)) {
3139        if (inList) { html.push("</div>"); inList = false; }
3140        html.push('<hr style="border:none;border-top:1px solid #e2e8f0;margin:10px 0">');
3141        continue;
3142      }
3143
3144      // Unordered list item
3145      if (/^[-•]\s+(.+)/.test(trimmed)) {
3146        if (!inList) { html.push('<div role="list" style="margin:6px 0;display:flex;flex-direction:column;gap:3px">'); inList = true; }
3147        var liMatch = trimmed.match(/^[-•]\s+(.+)/);
3148        html.push('<div role="listitem" style="display:flex;align-items:flex-start;gap:6px;animation:caf-li-in .25s ease-out both"><span aria-hidden="true" style="color:' + c + ';font-weight:700;font-size:10px;flex-shrink:0;margin-top:4px">
3148▸</span><span style="flex:1;min-width:0">' + liMatch[1] + "</span></div>");
3149        continue;
3150      }
3151
3152      // Numbered list item ("1." o "1)"; max 3 cifre: "2024. Testo" resta frase)
3153      if (/^(\d{1,3})[.)]\s+(.+)/.test(trimmed)) {
3154        if (!inList) { html.push('<div role="list" style="margin:6px 0;display:flex;flex-direction:column;gap:3px">'); inList = true; }
3155        var nlMatch = trimmed.match(/^(\d{1,3})[.)]\s+(.+)/);
3156        html.push('<div role="listitem" style="display:flex;align-items:flex-start;gap:6px;animation:caf-li-in .25s ease-out both"><span style="display:inline-flex;align-items:center;justify-content:center;width:16px;height:16px;border-radius:50%;background:' + c + '1a;color:#065f46;font-weight:700;font-size:10px;flex-shrink:0;margin-top:2px">' + nlMatch[1] + '</span><span style="flex:1;min-width:0">' + nlMatch[2] + "</span></div>");
3157        continue;
3158      }
3159
3160      // Regular paragraph
3161      if (inList) { html.push("</div>"); inList = false; }
3162      html.push('<div style="margin:4px 0">' + trimmed + "</div>");
3163    }
3164    if (inTable) html.push(renderTableBlock(tableRows, c));
3165    if (inList) html.push("</div>");
3166    if (inBlockquote) html.push("</div>");
3167
3168    var result = html.join("");
3169
3170    /* ── STEP 5: Restore placeholders ──
3171     * Guardie: un indice fuori range è testo scritto dall'utente
3172     * ("%%CAFPH_9%%" letterale) e resta com'è, mai "undefined". */
3173    result = result.replace(/%%CODEBLOCK_(\d+)%%/g, function (m, idx) {
3174      var k = parseInt(idx, 10);
3175      return k < codeBlocks.length
3176        ? '<code class="caf-code-inline">' + codeBlocks[k] + "</code>"
3177        : m;
3178    });
3179    result = result.replace(/%%CAFPH_(\d+)%%/g, function (m, idx) {
3180      var k = parseInt(idx, 10);
3181      return k < inlinePH.length ? inlinePH[k] : m;
3182    });
3183
3184    /* ── STEP 6: Phone numbers ──
3185     * Entrambe le pass saltano l'INTERNO dei tag (l'alternanza <[^>]*>
3186     * consuma i tag): niente <span> iniettati dentro href/style con
3187     * numeri tipo "https://sito.it/doc-123-4567". */
3188    // Pass A: "tel. XXXXX" / "telefono: XXXXX" → badge 📞 con numero nowrap.
3189    result = result.replace(
3190      /<[^>]*>|\btel(?:efono)?\.?\s*:?\s*(\+?[\d][\d\s\.\-]{5,16}[\d])/gi,
3191      function (m, num) {
3192        if (num === undefined) return m;
3193        return '<span class="caf-phone-badge">📞 <span class="caf-phone-lbl">Il telefono è</span>&nbsp;<span style="white-space:nowrap">' + num.trim() + "</span></span>";
3194      }
3195    );
3196    // Pass B: gruppi di cifre con spazi ("0583 21 64 84") → nowrap.
3197    result = result.replace(
3198      /<[^>]*>|\b\d{3,5}(?:[ \-]\d{2,6}){1,5}\b/g,
3199      function (m) {
3200        if (m.charAt(0) === "<") return m;
3201        return '<span style="white-space:nowrap">' + m + "</span>";
3202      }
3203    );
3204
3205    return result;
3206  }
3207
3208  /* ── CSS ───────────────────────────────────────────────────── */
3209  /* ── a11y: derive WCAG-safe foreground tokens from the tenant color ── */
3210  function cafHexNorm(h){h=String(h||"").trim().replace(/^#/,"");if(h.length===3)h=h[0]+h[0]+h[1]+h[1]+h[2]+h[2];return /^[0-9a-fA-F]{6}$/.test(h)?h.toLowerCase():"1e293b";}
3211  function cafHexLum(h){h=cafHexNorm(h);var r=parseInt(h.slice(0,2),16)/255,g=parseInt(h.slice(2,4),16)/255,b=parseInt(h.slice(4,6),16)/255;function f(x){return x<=0.03928?x/12.92:Math.pow((x+0.055)/1.055,2.4);}return 0.2126*f(r)+0.7152*f(g)+0.0722*f(b);}
3212  function cafContrast(a,b){var L1=cafHexLum(a),L2=cafHexLum(b),hi=Math.max(L1,L2),lo=Math.min(L1,L2);return (hi+0.05)/(lo+0.05);}
3213  /* Colore di testo/icone SUL brand (--caf-on-c). Preferisce il BIANCO sui
3214   * brand saturi (verde/blu/rosso: e' il look storico e voluto del widget,
3215   * ed e' AA per icone e testo grande — la soglia 3.0 e' il minimo WCAG per
3216   * componenti non testuali/testo grande, cioe' cio' che sta su questi
3217   * sfondi: send/close/calendario/CTA/bolla utente). Passa allo scuro SOLO
3218   * quando il bianco sarebbe illeggibile (tinte chiare: giallo, arancio,
3219   * verde pallido). Prima era >=4.5 e su #059669 (bianco 3.77) sceglieva
3220   * nero -> testo/icone neri sul verde, illeggibili di fatto. */
3221  function cafReadableOn(hex){if(cafContrast("#ffffff",hex)>=3.0)return "#ffffff";if(cafContrast("#0f172a",hex)>=4.5)return "#0f172a";return cafContrast("#000000",hex)>=cafContrast("#ffffff",hex)?"#000000":"#ffffff";}
3222  function cafDarken(hex,k){var h=cafHexNorm(hex);function h2(n){n=Math.max(0,Math.min(255,Math.round(n)));var s=n.toString(16);return s.length<2?"0"+s:s;}return "#"+h2(parseInt(h.slice(0,2),16)*k)+h2(parseInt(h.slice(2,4),16)*k)+h2(parseInt(h.slice(4,6),16)*k);}
3223  /* a11y #C41: target 4.9 (non 4.5 esatto) così ink regge >=4.5 anche sulle
3224   * tinte brand usate come sfondo (c+"14"/c+"0f"/c+"0c" su bianco, #f1f5f9):
3225   * verificato sui 6 brand critici (worst case #e30613: 4.62 su c14). */
3226  function cafInk(hex){var ink="#"+cafHexNorm(hex),k=1,guard=0;while(cafContrast(ink,"#ffffff")<4.9&&guard<40){k-=0.05;if(k<=0.05){ink=cafDarken(hex,0.05);break;}ink=cafDarken(hex,k);guard++;}return cafContrast(ink,"#ffffff")<4.9?"#0f172a":ink;}
3227  /* a11y: mix del brand verso il bianco (t=0 brand puro, t=1 bianco) */
3228  function cafLightenTo(hex,t){var h=cafHexNorm(hex);function ch(i){return parseInt(h.slice(i,i+2),16);}function h2(n){n=Math.max(0,Math.min(255,Math.round(n)));var s=n.toString(16);return s.length<2?"0"+s:s;}return "#"+h2(ch(0)+(255-ch(0))*t)+h2(ch(2)+(255-ch(2))*t)+h2(ch(4)+(255-ch(4))*t);}
3229  /* a11y dark mode #C5: variante del brand leggibile sui fondi scuri del
3230   * wizard. Calibrata sul fondo dark PIU' CHIARO usato (#1e293b) con margine
3231   * (>=4.6): garantisce >=4.5 anche su #0f172a e sui riquadri
3232   * rgba(30,41,59,.78). Speculare a cafInk (che lavora sul bianco). */
3233  function cafInkDark(hex){var t=0,cand="#"+cafHexNorm(hex);while(t<=1){cand=cafLightenTo(hex,t);if(cafContrast(cand,"#1e293b")>
3233=4.6)return cand;t+=0.05;}return "#f1f5f9";}
3234  /* a11y #C66: estremo chiaro dei gradienti CTA. Parte dall'equivalente
3235   * SOLIDO del vecchio c+"cc" composto su bianco (mix 20%) e riduce lo
3236   * schiarimento finche' il testo onC non regge >=4.6 anche sull'estremo
3237   * piu' chiaro (worst case verificato #e30613 -> #e51a26, 4.66). Colore
3238   * pieno = niente dipendenza dal backdrop (il vecchio alpha "cc" scendeva
3239   * a 3.48 composto sui fondi scuri). */
3240  function cafGradEnd(hex,onC){var t=0.2,cand;while(t>0){cand=cafLightenTo(hex,t);if(cafContrast(onC,cand)>=4.6)return cand;t-=0.02;}return "#"+cafHexNorm(hex);}
3241  function buildCSS(opts) {
3242    var c = opts.color;
3243    var onC = cafReadableOn(c);
3244    /* EAA/WCAG 1.4.3: quando testo e icone sul brand sono BIANCHI ma il bianco
3245     * non raggiunge 4.5:1 sul brand (es. #059669 = 3.77), scurisci il brand
3246     * usato come RIEMPIMENTO quel minimo che porta il bianco a >=4.5 — cosi'
3247     * anche il testo PICCOLO (bolle utente 13.5px, bottoni 11px) e' conforme,
3248     * non solo le icone. Solo se on-c e' bianco: sui colori chiari (on-c
3249     * scuro) il brand resta identico. Scurimento ~10%, impercettibile. */
3250    if(onC==="#ffffff"&&cafContrast("#ffffff",c)<4.5){
3251      for(var _kAA=0.99;_kAA>=0.5;_kAA-=0.01){var _dAA=cafDarken(c,_kAA);if(cafContrast("#ffffff",_dAA)>=4.5){c=_dAA;break;}}
3252    }
3253    var cInk = cafInk(c);
3254    var cInkDark = cafInkDark(c);
3255    var cGradEnd = cafGradEnd(c, onC);
3256    return (
3257      /* reset */
3258      ":host{all:initial;font-family:'Inter',-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,'Helvetica Neue',Arial,sans-serif;font-size:14px;color:#1e293b;line-height:1.5;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;--caf-c:" + c + ";--caf-on-c:" + onC + ";--caf-c-ink:" + cInk + ";--caf-c-ink-dark:" + cInkDark + ";}" +
3259      "*,*::before,*::after{box-sizing:border-box;margin:0;padding:0;}" +
3260
3261      /* ── keyframes ──────────────────────────────────── */
3262      "@keyframes caf-pop-in{0%{opacity:0;transform:translateY(14px) scale(.90)}55%{opacity:1;transform:translateY(-3px) scale(1.015)}100%{opacity:1;transform:translateY(0) scale(1)}}" +
3263      "@keyframes caf-logo-breathe{0%,100%{transform:translateY(0) scale(1);filter:brightness(1)}50%{transform:translateY(-3px) scale(1.07);filter:brightness(1.1)}}" +
3264      "@keyframes caf-pulse-ring{0%{transform:scale(1);opacity:.4}70%,100%{transform:scale(1.4);opacity:0}
3264}" +
3265      "@keyframes caf-pulse-ring-2{0%{transform:scale(1);opacity:.25}70%,100%{transform:scale(1.6);opacity:0}}" +
3266      "@keyframes caf-toggle-entrance{0%{opacity:0;transform:scale(.4) translateY(24px)}50%{opacity:1;transform:scale(1.1) translateY(-5px)}100%{opacity:1;transform:scale(1) translateY(0)}}" +
3267      "@keyframes caf-bubble-slide{0%{opacity:0;transform:translateX(20px) scale(.92)}100%{opacity:1;transform:translateX(0) scale(1)}}" +
3268      "@keyframes caf-wave{0%{transform:rotate(0deg)}20%{transform:rotate(14deg)}40%{transform:rotate(-8deg)}60%{transform:rotate(12deg)}80%{transform:rotate(-4deg)}100%{transform:rotate(0deg)}}" +
3269      "@keyframes caf-shimmer{0%{transform:translateX(-80%);opacity:.25}100%{transform:translateX(180%);opacity:0}}" +
3270      "@keyframes caf-caret{0%{opacity:.12}50%{opacity:.85}100%{opacity:.12}}" +
3271      "@keyframes caf-dots{0%,80%,100%{transform:scale(0)}40%{transform:scale(1)}}" +
3272      "@keyframes caf-online-pulse{0%,100%{box-shadow:0 0 0 0 " + c + "55}50%{box-shadow:0 0 0 4px " + c + "00}}" +
3273      "@keyframes caf-stagger{0%{opacity:0;transform:translateY(8px)}100%{opacity:1;transform:translateY(0)}}" +
3274      "@keyframes caf-highlight{0%{background:" + c + "4d}100%{background:" + c + "1a}}" +
3275      "@keyframes caf-badge-in{0%{opacity:0;transform:scale(.88) translateY(3px)}70%{opacity:1;transform:scale(1.03) translateY(-1px)}100%{opacity:1;transform:scale(1) translateY(0)}}" +
3276      "@keyframes caf-li-in{0%{opacity:0;transform:translateX(-8px)}100%{opacity:1;transform:translateX(0)}}" +
3277
3278      /* ── toggle button ──────────────────────────────── */
3279      ".caf-toggle{position:fixed;bottom:24px;z-index:" + opts.zIndex + ";width:64px;height:64px;border-radius:50%;border:none;cursor:pointer;display:flex;align-items:center;justify-content:center;background:" + c + ";color:var(--caf-on-c);" +
3280        "box-shadow:0 6px 24px -2px " + c + "66,0 2px 8px -2px rgba(0,0,0,.12);" +
3281        "transition:transform .3s cubic-bezier(.34,1.56,.64,1),box-shadow .3s ease;" +
3282        "animation:caf-toggle-entrance .7s cubic-bezier(.34,1.56,.64,1) both}" +
3283      ".caf-toggle:hover{transform:scale(1.06);box-shadow:0 10px 36px -2px " + c + "88,0 4px 12px -4px rgba(0,0,0,.15)}" +
3284      ".caf-toggle:active{transform:scale(.9)}" +
3285      ".caf-toggle img{width:30px;height:30px;object-fit:contain;animation:caf-logo-breathe 3.5s ease-in-out infinite;filter:drop-shadow(0 1px 3px rgba(0,0,0,.2))}" +
3286      ".caf-toggle .caf-close-icon{display:none;font-size:26px;font-weight:300;letter-spacing:-1px;transition:transform .5s cubic-bezier(.34,1.56,.64,1)}" +
3287      ".caf-toggle.open img{display:none}" +
3288      ".caf-toggle.open .caf-close-icon{display:block}" +
3289      ".caf-toggle .caf-pulse{position:absolute;inset:0;border-radius:50%;background:" + c + ";animation:caf-pulse-ring 2.6s cubic-bezier(.4,0,.6,1) infinite;pointer-events:none}" +
3290      ".caf-toggle .caf-pulse-2{position:absolute;inset:0;border-radius:50%;background:" + c + ";animation:caf-pulse-ring-2 2.6s cubic-bezier(.4,0,.6,1) .8s infinite;pointer-events:none}" +
3291      ".caf-toggle .caf-glow{position:absolute;inset:-8px;border-radius:50%;background:" + c + ";opacity:.45;filter:blur(20px);pointer-events:none;z-index:-1;transition:opacity .5s}" +
3292      ".caf-toggle.open .caf-pulse,.caf-toggle.open .caf-pulse-2{display:none}" +
3293      ".caf-toggle.open .caf-glow{opacity:0}" +
3294      /* booking enabled: pulse + glow più evidenti per attirare attenzione */
3295      ".caf-toggle.caf-bk-active .caf-glow{opacity:.75;filter:blur(28px)}" +
3296      ".caf-toggle.caf-bk-active .caf-pulse{animation-duration:1.8s}" +
3297      ".caf-toggle.caf-bk-active .caf-pulse-2{animation-duration:1.8s;animation-delay:.6s}" +
3298      ".caf-toggle.caf-bk-active.open .caf-glow{opacity:0}" +
3299      ".caf-pos-right{right:24px}" +
3300      ".caf-pos-left{left:24px}" +
3301
3302      /* ── bubble preview ─────────────────────────────── */
3303      ".caf-bubble{position:fixed;bottom:32px;z-index:" + opts.zIndex + ";display:flex;align-items:center;gap:10px;background:#fff;border-radius:20px;padding:12px 20px;font-size:13.5px;font-weight:500;color:#334155;box-shadow:0 6px 24px -6px rgba(0,0,0,.1),0 1px 3px rgba(0,0,0,.06);
3303cursor:pointer;transition:transform .3s cubic-bezier(.34,1.56,.64,1),box-shadow .3s ease;animation:caf-bubble-slide .5s cubic-bezier(.34,1.56,.64,1) .4s both}" +
3304      ".caf-bubble:hover{transform:translateY(-3px);box-shadow:0 10px 36px -6px rgba(0,0,0,.14),0 2px 6px rgba(0,0,0,.06)}" +
3305      ".caf-bubble.caf-pos-right{right:104px}" +
3306      ".caf-bubble.caf-pos-left{left:104px}" +
3307      /* booking enabled: bubble con look "promo" colorato e CTA più visibile */
3308      "@keyframes caf-bk-bubble-glow{0%,100%{box-shadow:0 6px 24px -6px " + c + "55,0 0 0 1px " + c + "30}50%{box-shadow:0 10px 32px -4px " + c + "88,0 0 0 2px " + c + "55}}" +
3309      "@keyframes caf-bk-arrow-bounce{0%,100%{transform:translateX(0)}50%{transform:translateX(4px)}}" +
3310      ".caf-bubble.caf-bk-active{background:linear-gradient(135deg," + c + " 0%,color-mix(in srgb," + c + " 80%,#1e293b) 100%);color:#fff;font-weight:600;font-size:14px;padding:13px 22px;animation:caf-bubble-slide .5s cubic-bezier(.34,1.56,.64,1) .4s both,caf-bk-bubble-glow 2.4s ease-in-out 1s infinite}" +
3311      ".caf-bubble.caf-bk-active:hover{transform:translateY(-3px) scale(1.02)}" +
3312      ".caf-bubble.caf-bk-active .caf-bk-arrow{display:inline-block;animation:caf-bk-arrow-bounce 1.2s ease-in-out infinite;font-weight:700}" +
3313      ".caf-bubble .caf-wave-emoji{display:inline-block;animation:caf-wave 1.4s ease-in-out infinite;transform-origin:70% 70%;font-size:16px}" +
3314
3315      /* ── panel ──────────────────────────────────────── */
3316      ".caf-panel{position:fixed;bottom:104px;z-index:" + opts.zIndex + ";width:400px;max-width:calc(100vw - 48px);max-height:min(640px,82vh);border-radius:24px;background:#fff;border:none;box-shadow:0 25px 60px -12px rgba(0,0,0,.2),0 0 0 1px rgba(0,0,0,.05);display:flex;flex-direction:column;overflow:hidden;" +
3317        "transition:transform .4s cubic-bezier(.34,1.56,.64,1),opacity .3s ease,visibility 0s .4s;" +
3318        "opacity:0;transform:translateY(16px) scale(.92);pointer-events:none;visibility:hidden}" +
3319      ".caf-panel.open{opacity:1;transform:translateY(0) scale(1);pointer-events:auto;visibility:visible;transition:transform .4s cubic-bezier(.34,1.56,.64,1),opacity .3s ease,visibility 0s 0s}" +
3320      ".caf-panel.caf-pos-right{right:24px}" +
3321      ".caf-panel.caf-pos-left{left:24px}" +
3322
3323      /* ── header ─────────────────────────────────────── */
3324      ".caf-header{display:flex;align-items:center;justify-content:space-between;padding:18px 20px;color:#fff;position:relative;border-bottom:none;flex-shrink:0;min-height:72px;overflow:visible}" +
3325      ".caf-header::before{content:'';position:absolute;inset:0;background:radial-gradient(circle at 20% 50%,rgba(255,255,255,.08) 0%,transparent 60%);pointer-events:none}" +
3326      ".caf-header::after{content:'';position:absolute;inset:0;background:rgba(255,255,255,.1);mask-image:linear-gradient(110deg,transparent 0%,rgba(255,255,255,.8) 18%,transparent 36%);-webkit-mask-image:linear-gradient(110deg,transparent 0%,rgba(255,255,255,.8) 18%,transparent 36%);animation:caf-shimmer 4s linear infinite;pointer-events:none}" +
3327      ".caf-header-left{display:flex;align-items:center;gap:14px;position:relative;z-index:1}" +
3328      ".caf-header-avatar{width:42px;height:42px;border-radius:50%;background:#fff;display:flex;align-items:center;justify-content:center;overflow:hidden;box-shadow:0 2px 8px rgba(0,0,0,.12);flex-shrink:0}" +
3329      ".caf-header-avatar img{width:80%;height:80%;object-fit:contain}" +
3330      ".caf-header-info{display:flex;flex-direction:column;gap:2px}" +
3331      ".caf-header-name{font-size:15px;font-weight:700;letter-spacing:-.01em;text-shadow:0 1px 2px rgba(0,0,0,.1)}" +
3332      ".caf-header-status{display:flex;align-items:center;gap:6px;font-size:12px;opacity:.9;font-weight:400}" +
3333      ".caf-online-dot{width:7px;height:7px;border-radius:50%;background:#86efac;flex-shrink:0;animation:caf-online-pulse 2s ease-in-out infinite}" +
3334      ".caf-header-actions{display:flex;gap:6px;align-items:center;position:relative;z-index:1}" +
3335      ".caf-header-btn{width:36px;height:36px;border-radius:12px;border:1px solid rgba(255,255,255,.2);background:rgba(255,255,255,.08);color:rgba(255,255,255,.95);
3335cursor:pointer;display:flex;align-items:center;justify-content:center;font-size:16px;font-weight:400;transition:all .2s;-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px)}" +
3336      ".caf-header-btn:hover{background:rgba(255,255,255,.2);transform:scale(1.05)}" +
3337      ".caf-header-btn:active{transform:scale(.92)}" +
3338
3339      /* ── menu ────────────────────────────────────────── */
3340      ".caf-menu{position:absolute;right:16px;top:62px;width:220px;background:#fff;border-radius:16px;border:none;box-shadow:0 12px 40px -8px rgba(0,0,0,.15),0 0 0 1px rgba(0,0,0,.04);z-index:10;overflow:hidden;transition:transform .25s cubic-bezier(.34,1.56,.64,1),opacity .2s ease,visibility 0s .25s;opacity:0;transform:translateY(-8px) scale(.95);pointer-events:none;visibility:hidden}" +
3341      ".caf-menu.open{opacity:1;transform:translateY(0) scale(1);pointer-events:auto;visibility:visible;transition:transform .25s cubic-bezier(.34,1.56,.64,1),opacity .2s ease,visibility 0s 0s}" +
3342      ".caf-menu-item{display:flex;align-items:center;gap:10px;width:100%;padding:13px 16px;border:none;background:transparent;cursor:pointer;font-size:13.5px;color:#334155;text-decoration:none;text-align:left;transition:background .15s;font-weight:500}" +
3343      ".caf-menu-item:hover{background:" + c + "0a}" +
3344      ".caf-menu-item .caf-icon{width:30px;height:30px;border-radius:10px;background:" + c + "14;color:" + c + ";display:flex;align-items:center;justify-content:center;font-size:15px;flex-shrink:0}" +
3345      ".caf-menu-divider{height:1px;background:#f1f5f9;margin:4px 0}" +
3346      ".caf-menu-label{padding:10px 16px 6px;font-size:11px;font-weight:600;color:#5b6573;text-transform:uppercase;letter-spacing:.06em}" +
3347      ".caf-rating-row{display:flex;gap:8px;padding:8px 16px 12px}" +
3348      ".caf-rate-btn{flex:1;display:flex;align-items:center;justify-content:center;gap:6px;padding:8px 0;border-radius:12px;border:1.5px solid #e2e8f0;background:#fff;color:#475569;font-size:13px;font-weight:500;cursor:pointer;transition:all .2s cubic-bezier(.34,1.56,.64,1)}" +
3349      ".caf-rate-btn:hover{border-color:" + c + "40;background:" + c + "06}" +
3350      ".caf-rate-btn:active{transform:scale(.95)}" +
3351      ".caf-rate-btn.selected-good{border-color:#10b981;background:#ecfdf5;color:#047857}" +
3352      ".caf-rate-btn.selected-bad{border-color:#f43f5e;background:#fff1f2;color:#be123c}" +
3353      ".caf-rate-btn .caf-rate-icon{font-size:16px;line-height:1}" +
3354
3355      /* ── chat area ──────────────────────────────────── */
3356      ".caf-messages{flex:1;min-height:0;overflow-y:auto;-webkit-overflow-scrolling:touch;overscroll-behavior:contain;padding:24px 18px;background:linear-gradient(180deg,#f8fafc 0%,#f1f5f9 100%);display:flex;flex-direction:column;gap:16px}" +
3357
3358      /* ── messages ───────────────────────────────────── */
3359      ".caf-msg-row{display:flex;gap:10px}" +
3360      ".caf-msg-row.caf-new{animation:caf-pop-in .45s cubic-bezier(.34,1.56,.64,1)}" +
3361      ".caf-msg-row.user{justify-content:flex-end}" +
3362      ".caf-msg-avatar{width:34px;height:34px;border-radius:50%;background:#fff;display:flex;align-items:center;justify-content:center;overflow:hidden;flex-shrink:0;box-shadow:0 1px 4px rgba(0,0,0,.08)}" +
3363      ".caf-msg-avatar img{width:75%;height:75%;object-fit:contain}" +
3364      ".caf-msg-bubble{padding:14px 18px;border-radius:20px;font-size:13.5px;line-height:1.7;word-break:break-word;transition:box-shadow .25s ease,transform .25s ease}" +
3365      ".caf-msg-bubble.assistant{background:#fff;color:#334155;border-bottom-left-radius:6px;box-shadow:0 1px 3px rgba(0,0,0,.04),0 0 0 1px rgba(0,0,0,.03)}" +
3366      ".caf-msg-bubble.assistant:hover{box-shadow:0 4px 16px rgba(0,0,0,.08),0 0 0 1px rgba(0,0,0,.03)}" +
3367      ".caf-msg-bubble.assistant p{margin:5px 0}" +
3368      ".caf-msg-bubble.assistant p:first-child{margin-top:0}" +
3369      ".caf-msg-bubble.assistant p:last-child{margin-bottom:0}" +
3370      ".caf-msg-bubble.user{color:#fff;border-bottom-right-radius:6px;white-space:pre-wrap;box-shadow:0 4px 16px -4px " + c + "40}" +
3371      ".caf-msg-bubble a{color:" + c + ";text-decoration:underline;text-underline-offset:2px}" +
3372      ".caf-msg-bubble.user a{color:#fff}" +
3373      ".caf-msg-bubble strong{font-weight:600}" +
3374      ".caf-strong{font-weight:700!important;background:" + c + "1a;border-radius:4px;padding:1px 5px;color:#065f46;animation:caf-highlight .7s ease-out forwards}" +
3375      ".caf-url-badge{display:inline-flex!important;align-items:center;gap:4px;background:" + c + "0d;border:1px solid " + c + "40;border-radius:20px;padding:2px 9px;font-weight:600;font-size:12px;text-decoration:none!important;color:var(--caf-c-ink)!important;animation:caf-badge-in .4s ease-out forwards}" +
3376      ".caf-url-badge:hover{opacity:.8}" +
3377      ".caf-phone-badge{display:inline-flex;align-items:center;gap:4px;white-space:nowrap;background:" + c + "0d;border:1px solid " + c + "33;border-radius:20px;padding:2px 10px;font-size:12.5px;font-weight:600;color:" + c + ";animation:caf-badge-in .4s ease-out forwards}" +
3378      ".caf-phone-lbl{font-weight:400;color:#5b6573;font-size:12px}" +
3379      ".caf-msg-bubble hr{border:none;border-top:1px solid #e2e8f0;margin:10px 0}" +
3380
3381      /* ── ecommerce: product card ───────────────────────── */
3382      ".caf-prod-card{--caf-prod-c:" + c + ";display:flex;gap:12px;background:linear-gradient(180deg,#fff,#fafbfc);border:1px solid #e5e7eb;border-radius:14px;padding:12px;margin:10px 0;box-shadow:0 1px 2px rgba(15,23,42,.04),0 4px 12px rgba(15,23,42,.06);transition:transform .2s ease,box-shadow .2s ease,border-color .2s ease;animation:caf-prod-in .35s cubic-bezier(.2,.9,.3,1.2) both}" +
3383      ".caf-prod-card:hover{transform:translateY(-2px);box-shadow:0 4px 12px rgba(15,23,42,.08),0 12px 28px rgba(15,23,42,.10);border-color:" + c + "55}" +
3384      ".caf-prod-img-wrap{position:relative;flex:0 0 92px;width:92px;height:92px;border-radius:10px;overflow:hidden;background:#f1f5f9;display:flex;align-items:center;justify-content:center}" +
3385      ".caf-prod-img{width:100%;height:100%;object-fit:contain;display:block;transition:transform .35s ease}" +
3386      ".caf-prod-card:hover .caf-prod-img{transform:scale(1.05)}" +
3387      ".caf-prod-promo-badge{position:absolute;top:5px;left:5px;background:linear-gradient(135deg,#ef4444,#dc2626);color:#fff;font-weight:800;font-size:10.5px;padding:2px 7px;border-radius:6px;letter-spacing:.3px;box-shadow:0 2px 6px rgba(220,38,38,.4);animation:caf-prod-pulse 2.4s ease-in-out infinite;z-index:2}" +
3388      ".caf-prod-stock-badge{position:absolute;bottom:5px;left:5px;font-weight:700;font-size:9.5px;padding:2px 6px;border-radius:5px;letter-spacing:.2px;text-transform:uppercase;z-index:2}" +
3389      ".caf-prod-stock-badge.low{background:#fef3c7;color:#92400e;border:1px solid #fbbf24}" +
3390      ".caf-prod-stock-badge.out{background:#fee2e2;color:#991b1b;border:1px solid #fca5a5}" +
3391      ".caf-prod-body{flex:1;min-width:0;display:flex;flex-direction:column;gap:5px}" +
3392      ".caf-prod-cat{font-size:9.5px;font-weight:700;text-transform:uppercase;letter-spacing:.6px;color:#5b6573}" +
3393      ".caf-prod-title{font-size:13px;font-weight:700;color:#0f172a;line-height:1.3;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;user-select:none}" +
3394      ".caf-prod-card.expanded .caf-prod-title{-webkit-line-clamp:99;display:block}" +
3395      ".caf-prod-meta{display:flex;align-items:center;gap:10px;font-size:11px;color:#5b6573;flex-wrap:wrap}" +
3396      ".caf-prod-pcs{display:inline-flex;align-items:center;gap:3px;background:#f1f5f9;color:#475569;padding:2px 7px;border-radius:6px;font-weight:600}" +
3397      ".caf-prod-pcs svg{width:11px;height:11px}" +
3398      ".caf-prod-perpiece{font-style:italic}" +
3399      ".caf-prod-prow{display:flex;align-items:baseline;gap:8px;margin-top:2px}" +
3400      ".caf-prod-price{font-size:18px;font-weight:800;color:" + c + ";letter-spacing:-.5px}" +
3401      ".caf-prod-price-old{font-size:12px;color:#5b6573;text-decoration:line-through;font-weight:500}" +
3402      ".caf-prod-desc{font-size:11.5px;color:#5b6573;line-height:1.45;display:none}" +
3403      ".caf-prod-card.expanded .caf-prod-desc{display:block;animation:caf-prod-fadein .25s ease-out}" +
3404      ".caf-prod-cta{display:flex;align-items:center;gap:8px;margin-top:8px;flex-wrap:wrap}" +
3405      ".caf-prod-qty{display:inline-flex;align-items:center;border:1px solid #e2e8f0;border-radius:8px;background:#fff;overflow:hidden}" +
3406      ".caf-prod-qty-btn{width:26px;height:28px;border:none;background:transparent;font-size:16px;font-weight:700;color:" + c + ";cursor:pointer;transition:background .15s ease;line-height:1}" +
3407      ".caf-prod-qty-btn:hover{background:" + c + "14}" +
3408      ".caf-prod-qty-btn:active{background:" + c + "26}" +
3409      ".caf-prod-qty-inp{width:36px;height:28px;border:none;border-left:1px solid #e2e8f0;border-right:1px solid #e2e8f0;text-align:center;font-size:13px;font-weight:700;color:#0f172a;outline:none;background:transparent}" +
3410      ".caf-prod-add{flex:1;min-width:110px;display:inline-flex;align-items:center;justify-content:center;gap:6px;background:linear-gradient(135deg," + c + "," + c + "d6);color:#fff;border:none;border-radius:8px;padding:7px 12px;font-size:12.5px;font-weight:700;cursor:pointer;letter-spacing:.2px;box-shadow:0 2px 6px " + c + "40;transition:transform .15s ease,box-shadow .2s ease,background .2s ease}" +
3411      ".caf-prod-add:hover{transform:translateY(-1px);box-shadow:0 4px 10px " + c + "55}" +
3412      ".caf-prod-add:active{transform:translateY(0)}" +
3413      ".caf-prod-add:disabled{background:#cbd5e1;cursor:not-allowed;box-shadow:none;opacity:.7}" +
3414      ".caf-prod-add svg{width:14px;
3414height:14px}" +
3415      ".caf-prod-add.added{background:linear-gradient(135deg,#10b981,#059669);animation:caf-prod-added .5s cubic-bezier(.2,.9,.3,1.2)}" +
3416      "@keyframes caf-prod-in{0%{opacity:0;transform:translateY(8px) scale(.98)}100%{opacity:1;transform:translateY(0) scale(1)}}" +
3417      "@keyframes caf-prod-fadein{0%{opacity:0;transform:translateY(-3px)}100%{opacity:1;transform:translateY(0)}}" +
3418      "@keyframes caf-prod-pulse{0%,100%{transform:scale(1)}50%{transform:scale(1.06)}}" +
3419      "@keyframes caf-prod-added{0%{transform:scale(1)}40%{transform:scale(1.06)}100%{transform:scale(1)}}" +
3420      "@media(max-width:480px){.caf-prod-card{flex-direction:column}.caf-prod-img-wrap{flex:0 0 auto;width:100%;height:140px}.caf-prod-img{object-fit:contain}}" +
3421      ".caf-prod-anchor{display:none}" +
3422
3423      /* ── carousel ───────────────────────────────────── */
3424      ".caf-prod-carousel{margin:14px -4px 4px;padding:10px 4px 4px;border-top:1px dashed #e2e8f0;animation:caf-prod-in .4s ease-out both}" +
3425      ".caf-prod-carousel-hdr{display:flex;align-items:center;justify-content:space-between;margin:0 4px 8px;font-size:12.5px}" +
3426      ".caf-prod-carousel-hdr-l{display:inline-flex;align-items:center;gap:6px;font-weight:700;color:#0f172a}" +
3427      ".caf-prod-carousel-hdr-l svg{width:14px;height:14px;color:" + c + "}" +
3428      ".caf-prod-carousel-count{color:#5b6573;font-size:11px;font-weight:600;background:#f1f5f9;padding:2px 8px;border-radius:10px}" +
3429      ".caf-prod-carousel-sw{position:relative}" +
3430      ".caf-prod-carousel-scroll{display:flex;gap:10px;overflow-x:auto;overflow-y:hidden;scroll-snap-type:x mandatory;scroll-behavior:smooth;-webkit-overflow-scrolling:touch;padding:6px 4px 14px;scrollbar-width:thin;scrollbar-color:#cbd5e1 transparent}" +
3431      ".caf-prod-carousel-scroll::-webkit-scrollbar{height:6px}" +
3432      ".caf-prod-carousel-scroll::-webkit-scrollbar-track{background:transparent}" +
3433      ".caf-prod-carousel-scroll::-webkit-scrollbar-thumb{background:#cbd5e1;border-radius:3px}" +
3434      ".caf-prod-carousel-scroll::-webkit-scrollbar-thumb:hover{background:#94a3b8}" +
3435      ".caf-prod-carousel-arrow{position:absolute;top:50%;transform:translateY(-50%);z-index:5;width:30px;height:30px;border-radius:50%;background:#fff;border:1px solid #e2e8f0;color:#0f172a;cursor:pointer;display:flex;align-items:center;justify-content:center;box-shadow:0 2px 8px rgba(15,23,42,.12);transition:opacity .25s ease,transform .15s ease,box-shadow .2s ease;opacity:0;pointer-events:none}" +
3436      ".caf-prod-carousel-arrow:hover{transform:translateY(-50%) scale(1.08);box-shadow:0 4px 14px rgba(15,23,42,.18)}" +
3437      ".caf-prod-carousel-arrow.l{left:-6px}" +
3438      ".caf-prod-carousel-arrow.r{right:-6px}" +
3439      ".caf-prod-carousel-arrow svg{width:14px;height:14px}" +
3440      /* Mini card: design ELEGANTE con foto dominante, no CTA, scrolling orizzontale. */
3441      ".caf-prod-mini{flex:0 0 190px;width:190px;background:#ffffff;border:1px solid #eef2f7;border-radius:16px;padding:0;display:flex;flex-direction:column;scroll-snap-align:start;box-shadow:0 1px 2px rgba(15,23,42,.04),0 4px 14px rgba(15,23,42,.04);transition:transform .3s cubic-bezier(.2,.9,.3,1.1),box-shadow .3s ease,border-color .25s ease;animation:caf-prod-in .4s cubic-bezier(.2,.9,.3,1.2) both;overflow:hidden;cursor:pointer}" +
3442      ".caf-prod-mini:hover{transform:translateY(-5px);border-color:" + c + "55;box-shadow:0 14px 32px rgba(15,23,42,.12),0 6px 12px rgba(15,23,42,.06)}" +
3443      ".caf-prod-mini-img-wrap{position:relative;width:100%;aspect-ratio:1/1;background:linear-gradient(135deg,#fafbfc 0%,#f1f5f9 100%);display:flex;align-items:center;justify-content:center;overflow:hidden}" +
3444      ".caf-prod-mini-img{width:88%;height:88%;object-fit:contain;display:block;transition:transform .45s cubic-bezier(.2,.9,.3,1.1);filter:drop-shadow(0 4px 8px rgba(15,23,42,.08))}" +
3445      ".caf-prod-mini:hover .caf-prod-mini-img{transform:scale(1.12) rotate(-1deg)}" +
3446      ".caf-prod-mini-promo{position:absolute;top:8px;left:8px;background:linear-gradient(135deg,#ef4444,#dc2626);color:#fff;font-weight:800;font-size:11px;padding:3px 8px;border-radius:8px;box-shadow:0 3px 8px rgba(220,38,38,.4);z-index:2;letter-spacing:.4px}" +
3447      ".caf-prod-mini-stock{position:absolute;bottom:8px;right:8px;font-weight:700;font-size:9px;padding:3px 7px;border-radius:6px;letter-spacing:.4px;text-transform:uppercase;z-index:2;backdrop-filter:blur(6px);-webkit-backdrop-filter:blur(6px)}" +
3448      ".caf-prod-mini-stock.low{background:rgba(251,191,36,.95);color:#7c2d12;border:1px solid rgba(245,158,11,.4)}" +
3449      ".caf-prod-mini-stock.out{background:rgba(185,28,28,.95);color:#fff;border:1px solid rgba(220,38,38,.4)}" +
3450      ".caf-prod-mini-body{padding:12px 13px 14px;display:flex;flex-direction:column;gap:8px;flex:1}" +
3451      ".caf-prod-mini-title{font-size:12.5px;font-weight:700;color:#0f172a;line-height:1.4;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;min-height:2.8em;letter-spacing:-.15px}" +
3452      ".caf-prod-mini-prow{display:flex;align-items:baseline;gap:7px;flex-wrap:wrap;margin-top:auto}" +
3453      ".caf-prod-mini-price{font-size:18px;font-weight:800;color:" + c + ";letter-spacing:-.6px;line-height:1}" +
3454      ".caf-prod-mini-price-old{font-size:12px;color:#5b6573;text-decoration:line-through;font-weight:600}" +
3455      "@media(max-width:480px){.caf-prod-mini{flex:0 0 160px;width:160px}.caf-prod-mini-body{padding:10px 11px 12px}.caf-prod-mini-price{font-size:16px}}" +
3456
3457
3458
3459      /* ── typing dots ────────────────────────────────── */
3460      ".caf-typing{display:flex;align-items:center;gap:5px;padding:4px 0}" +
3461      ".caf-typing span{width:7px;height:7px;border-radius:50%;background:" + c + "66;display:inline-block;animation:caf-dots 1.4s infinite ease-in-out both}" +
3462      ".caf-typing span:nth-child(1){animation-delay:-.32s}" +
3463      ".caf-typing span:nth-child(2){animation-delay:-.16s}" +
3464
3465      /* ── streaming caret ────────────────────────────── */
3466      ".caf-caret{display:inline-block;width:2.5px;height:1.15em;margin-left:3px;border-radius:999px;vertical-align:text-bottom;animation:caf-caret 1s ease-in-out infinite}" +
3467
3468      /* ── quick replies ──────────────────────────────── */
3469      /* ── QUICK REPLIES: premium cards ── */
3470      "@keyframes caf-quick-in{0%{opacity:0;transform:translateY(8px) scale(.96)}100%{opacity:1;transform:translateY(0) scale(1)}}" +
3471      "@keyframes caf-quick-shine{0%{transform:translateX(-100%) skewX(-20deg)}60%,100%{transform:translateX(260%) skewX(-20deg)}}" +
3472      ".caf-quick{display:flex;flex-direction:column;gap:8px;padding:4px 0 2px;margin-left:44px}" +
3473      ".caf-quick-label{font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.6px;color:#5b6573;margin-bottom:2px;display:flex;align-items:center;gap:6px}" +
3474      ".caf-quick-label::before,.caf-quick-label::after{content:'';flex:0 0 18px;height:1px;background:linear-gradient(90deg,transparent," + c + "30,transparent)}" +
3475      ".caf-quick-label::before{background:linear-gradient(90deg," + c + "30,transparent)}" +
3476      ".caf-quick button{position:relative;display:flex;align-items:center;gap:10px;padding:11px 14px 11px 13px;border-radius:14px;border:1.5px solid " + c + "20;background:linear-gradient(135deg,#fff," + c + "05);color:#1e293b;font-size:12.5px;cursor:pointer;text-align:left;font-weight:600;line-height:1.35;font-family:inherit;overflow:hidden;transition:transform .25s cubic-bezier(.34,1.56,.64,1),box-shadow .3s,border-color .25s,background .25s;animation:caf-quick-in .4s cubic-bezier(.34,1.56,.64,1) both;-webkit-tap-highlight-color:transparent;box-shadow:0 1px 2px rgba(15,23,42,.04)}" +
3477      ".caf-quick button::after{content:'';position:absolute;top:0;bottom:0;width:40%;background:linear-gradient(100deg,transparent 0%,rgba(255,255,255,.45) 50%,transparent 100%);transform:translateX(-100%) skewX(-20deg);pointer-events:none;transition:opacity .3s;opacity:0}" +
3478      ".caf-quick button:hover::after{opacity:1;animation:caf-quick-shine 1s ease-out}" +
3479      ".caf-quick button:nth-child(2){animation-delay:.08s}" +
3480      ".caf-quick button:nth-child(3){animation-delay:.16s}" +
3481      ".caf-quick button:nth-child(4){animation-delay:.24s}" +
3482      ".caf-quick button:nth-child(5){animation-delay:.32s}" +
3483      ".caf-quick button:hover{border-color:" + c + ";background:linear-gradient(135deg," + c + "," + cGradEnd + ");color:var(--caf-on-c);transform:translateY(-2px);box-shadow:0 10px 24px " + c + "38}" +
3484      ".caf-quick button:hover .caf-quick-ic{background:rgba(255,255,255,.25);color:#fff;transform:rotate(-6deg) scale(1.08)}" +
3485      ".caf-quick button:hover .caf-quick-ar{opacity:1;transform:translateX(2px);color:#fff}" +
3486      ".caf-quick button:active{transform:scale(.98)}" +
3487      ".caf-quick-ic{width:30px;height:30px;border-radius:10px;background:" + c + "15;color:" + c + ";display:flex;align-items:center;justify-content:center;flex-shrink:0;transition:background .25s,color .25s,transform .3s cubic-bezier(.34,1.56,.64,1);position:relative;z-index:1}" +
3488      ".caf-quick-ic svg{width:15px;height:15px}" +
3489      ".caf-quick-txt{flex:1;min-width:0;position:relative;z-index:1}" +
3490      ".caf-quick-ar{flex-shrink:0;opacity:.4;color:#5b6573;transition:opacity .25s,transform .25s,color .25s;position:relative;z-index:1}" +
3491      ".caf-quick-ar svg{width:14px;height:14px}" +
3492
3493      /* ── timestamps ──────────────────────────────────── */
3494      ".caf-msg-time{font-size:10px;color:#5b6573;margin-top:4px;padding:0 2px;user-select:none}" +
3495      ".caf-msg-row.user .caf-msg-time{text-align:right}" +
3496
3497      /* ── copy button ──────────────────────────────────── */
3498      ".caf-msg-wrap{position:relative;max-width:80%}" +
3499      ".caf-copy-btn{position:absolute;top:6px;right:6px;width:28px;height:28px;border-radius:8px;border:none;background:rgba(0,0,0,.06);color:#5b6573;cursor:pointer;display:flex;align-items:center;justify-content:center;font-size:13px;opacity:0;transition:opacity .2s,background .15s;z-index:1;pointer-events:none}" +
3500      ".caf-msg-wrap:hover .caf-copy-btn{opacity:1;pointer-events:auto}" +
3501      ".caf-copy-btn:hover{background:rgba(0,0,0,.1)}" +
3502      ".caf-copy-btn.copied{background:" + c + "18;color:" + c + "}" +
3503      ".caf-copy-toast{position:absolute;top:-28px;right:0;background:#1e293b;color:#fff;font-size:11px;font-weight:500;padding:4px 10px;border-radius:8px;pointer-events:none;animation:caf-pop-in .25s cubic-bezier(.34,1.56,.64,1);white-space:nowrap}" +
3504
3505      /* ── translate button & picker ─────────────────────── */
3506      ".caf-translate-btn{position:absolute;top:6px;right:38px;width:28px;height:28px;border-radius:8px;border:none;background:rgba(0,0,0,.06);color:#5b6573;cursor:pointer;display:flex;align-items:center;justify-content:center;font-size:14px;opacity:0;transition:opacity .2s,background .15s;z-index:1;pointer-events:none}" +
3507      ".caf-msg-wrap:hover .caf-translate-btn{opacity:1;pointer-events:auto}" +
3508      ".caf-translate-btn:hover{background:rgba(0,0,0,.1)}" +
3509
3510      /* ── mobile action bar (replaces absolute overlay) ── */
3511      ".caf-msg-actions{display:none}" +
3512      ".caf-lang-picker{position:absolute;top:38px;right:0;background:#fff;border-radius:14px;box-shadow:0 10px 36px -8px rgba(0,0,0,.16),0 0 0 1px rgba(0,0,0,.04);z-index:20;padding:8px;display:grid;grid-template-columns:repeat(4,1fr);gap:4px;animation:caf-pop-in .2s cubic-bezier(.34,1.56,.64,1);width:220px;max-height:260px;overflow-y:auto;overscroll-behavior:contain;-webkit-overflow-scrolling:touch}" +
3513      ".caf-lang-btn{display:flex;flex-direction:column;align-items:center;gap:2px;padding:8px 4px;border:none;background:transparent;border-radius:10px;cursor:pointer;transition:background .15s;font-size:11px;color:#475569;font-weight:500;font-family:inherit}" +
3514      ".caf-lang-btn:hover{background:" + c + "0a}" +
3515      ".caf-lang-btn:active{transform:scale(.94)}" +
3516      ".caf-lang-btn .caf-flag{font-size:20px;line-height:1}" +
3517      ".caf-translating{display:inline-flex;align-items:center;gap:6px;font-size:12px;color:#5b6573;padding:4px 0}" +
3518
3519      /* ── date separator ────────────────────────────────── */
3520      ".caf-date-sep{display:flex;align-items:center;gap:12px;padding:8px 0;user-select:none}" +
3521      ".caf-date-sep::before,.caf-date-sep::after{content:'';flex:1;height:1px;background:#e2e8f0}" +
3522      ".caf-date-sep span{font-size:10.5px;font-weight:600;color:#5b6573;text-transform:uppercase;letter-spacing:.05em;white-space:nowrap}" +
3523
3524      /* ── unread badge ──────────────────────────────────── */
3525      ".caf-badge{position:absolute;top:-4px;right:-4px;min-width:20px;height:20px;border-radius:10px;background:#dc2626;color:#fff;font-size:11px;font-weight:700;display:flex;align-items:center;justify-content:center;padding:0 5px;pointer-events:none;animation:caf-pop-in .3s cubic-bezier(.34,1.56,.64,1);box-shadow:0 2px 6px rgba(239,68,68,.4);line-height:1}" +
3526
3527      /* ── code rendering ────────────────────────────────── */
3528      ".caf-code-inline{background:#f1f5f9;padding:1px 6px;border-radius:4px;font-family:'SF Mono',SFMono-Regular,Consolas,'Liberation Mono',Menlo,monospace;font-size:.88em;color:#334155;border:1px solid #e2e8f0}" +
3529      ".caf-code-block{background:#1e293b;color:#e2e8f0;padding:14px 16px;border-radius:12px;font-family:'SF Mono',SFMono-Regular,Consolas,'Liberation Mono',Menlo,monospace;font-size:12px;line-height:1.6;overflow-x:auto;white-space:pre;margin:8px 0;-webkit-overflow-scrolling:touch}" +
3530      ".caf-blockquote{border-left:3px solid " + c + ";padding:4px 0 4px 14px;color:#5b6573;margin:8px 0;font-style:italic}" +
3531
3532      /* ── error ──────────────────────────────────────── */
3533      ".caf-error{background:#fef2f2;color:#b91c1c;font-size:12.5px;padding:10px 18px;border-radius:14px;text-align:center;animation:caf-pop-in .3s cubic-bezier(.34,1.56,.64,1);margin:0 18px;font-weight:500}" +
3534
3535      /* ── scroll-to-bottom ───────────────────────────── */
3536      ".caf-scroll-btn{position:absolute;bottom:10px;left:50%;transform:translateX(-50%);width:34px;height:34px;border-radius:50%;border:none;background:#fff;cursor:pointer;display:flex;align-items:center;justify-content:center;box-shadow:0 2px 10px rgba(0,0,0,.1),0 0 0 1px rgba(0,0,0,.04);transition:all .2s;font-size:15px;color:" + c + ";z-index:2}" +
3537      ".caf-scroll-btn:hover{box-shadow:0 4px 16px rgba(0,0,0,.14);transform:translateX(-50%) scale(1.08)}" +
3538
3539      /* ── input area ─────────────────────────────────── */
3540      ".caf-input-area{display:flex;align-items:center;gap:10px;padding:14px 18px 16px;border-top:none;background:#fff;flex-shrink:0;box-shadow:0 -1px 0 rgba(0,0,0,.04)}" +
3541      ".caf-input{flex:1;border:1.5px solid #818c9c;border-radius:16px;padding:12px 18px;font-size:13.5px;outline:none;color:#1e293b;font-family:inherit;transition:border-color .2s,box-shadow .2s;background:#f8fafc;font-weight:400}" +
3542      ".caf-input:focus{border-color:" + c + ";box-shadow:0 0 0 3px " + c + "15;background:#fff}" +
3543      ".caf-input::placeholder{color:#5b6573;font-weight:400}" +
3544      ".caf-send{width:44px;height:44px;border-radius:14px;border:none;cursor:pointer;display:flex;align-items:center;justify-content:center;transition:all .2s cubic-bezier(.34,1.56,.64,1);flex-shrink:0}" +
3545      ".caf-send:disabled{opacity:.35;cursor:not-allowed}" +
3546      ".caf-send:not(:disabled):hover{transform:scale(1.06);box-shadow:0 4px 14px " + c + "33}" +
3547      ".caf-send:not(:disabled):active{transform:scale(.92)}" +
3548      ".caf-send svg{width:18px;height:18px}" +
3549
3550      /* ── mic button (matches iframe ChatWidget style) ── */
3551      ".caf-mic{width:40px;height:40px;border-radius:50%;border:none;cursor:pointer;display:flex;align-items:center;justify-content:center;transition:all .3s;flex-shrink:0;position:relative;color:#fff;box-shadow:0 2px 12px -2px " + c + "73}" +
3552      ".caf-mic:disabled{opacity:.4;cursor:not-allowed;transform:none!important}" +
3553      ".caf-mic:not(:disabled):hover{transform:scale(1.10);box-shadow:0 4px 20px -2px " + c + "99}" +
3554      ".caf-mic:not(:disabled):active{transform:scale(.95)}" +
3555      ".caf-mic svg{width:18px;height:18px;position:relative;z-index:1;filter:drop-shadow(0 1px 1px rgba(0,0,0,.1))}" +
3556      ".caf-mic .caf-mic-pulse{position:absolute;inset:0;border-radius:50%;background:" + c + ";animation:caf-pulse-ring 2.8s ease-in-out infinite;pointer-events:none}" +
3557      ".caf-mic.listening .caf-mic-pulse{animation:caf-pulse-ring 1.2s ease-in-out infinite}" +
3558      ".caf-mic.listening{animation:caf-mic-glow 1.5s ease-in-out infinite}" +
3559      "@keyframes caf-mic-glow{0%,100%{box-shadow:0 0 0 0 " + c + "55}50%{box-shadow:0 0 0 8px " + c + "00}}" +
3560
3561      /* ── voice mode panel ─────────────────────────────── */
3562      ".caf-voice-panel{display:none;flex-direction:column;flex:1;min-height:0;overflow:hidden}" +
3563      ".caf-voice-panel.active{display:flex;animation:caf-voice-enter .45s cubic-bezier(.34,1.56,.64,1) both}" +
3564
3565      ".caf-voice-transcript{flex:1;min-height:0;overflow-y:auto;-webkit-overflow-scrolling:touch;overscroll-behavior:contain;padding:16px 14px;background:linear-gradient(180deg,#f8fafc,#f1f5f9);display:flex;flex-direction:column;gap:10px}" +
3566      ".caf-vt-row{display:flex;gap:8px;animation:caf-pop-in .3s ease-out}" +
3567      ".caf-vt-row.user{justify-content:flex-end}" +
3568      ".caf-vt-avatar{width:28px;height:28px;border-radius:50%;background:#fff;display:flex;align-items:center;justify-content:center;overflow:hidden;flex-shrink:0;box-shadow:0 1px 3px rgba(0,0,0,.06)}" +
3569      ".caf-vt-avatar img{width:75%;height:75%;object-fit:contain}" +
3570      ".caf-vt-bubble{max-width:85%;padding:10px 14px;border-radius:16px;font-size:13px;line-height:1.6;word-break:break-word}" +
3571      ".caf-vt-bubble.assistant{background:#fff;color:#334155;border-bottom-left-radius:4px;box-shadow:0 1px 2px rgba(0,0,0,.04)}" +
3572      ".caf-vt-bubble.user{color:#fff;border-bottom-right-radius:4px;box-shadow:0 2px 8px " + c + "30}" +
3573      ".caf-vt-ghost{max-width:85%;padding:10px 14px;border-radius:16px;font-size:13px;line-height:1.6;border-bottom-right-radius:4px;color:rgba(255,255,255,.9);animation:caf-pop-in .2s ease-out}" +
3574      ".caf-vt-dots{display:flex;align-items:center;gap:4px;padding:2px 0}" +
3575      ".caf-vt-dots span{width:6px;height:6px;border-radius:50%;display:inline-block;animation:caf-dots 1.4s infinite ease-in-out both}" +
3576      ".caf-vt-dots span:nth-child(1){animation-delay:-.32s}" +
3577      ".caf-vt-dots span:nth-child(2){animation-delay:-.16s}" +
3578
3579      ".caf-voice-controls{position:relative;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:20px;padding:24px 24px 28px;min-height:260px;border-top:1px solid rgba(0,0,0,.04)}" +
3580      ".caf-voice-glow{position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);border-radius:50%;filter:blur(40px);pointer-events:none;transition:all .8s ease}" +
3581      ".caf-voice-status{font-size:15px;font-weight:700;letter-spacing:.02em;transition:color .3s,opacity .3s;text-align:center;min-height:20px}" +
3582      ".caf-voice-hint{font-size:11.5px;font-weight:400;text-align:center;margin-top:-12px;transition:color .3s,opacity .3s;opacity:1;min-height:16px}" +
3583
3584      ".caf-voice-mic-wrap{position:relative;display:flex;align-items:center;justify-content:center}" +
3585      ".caf-voice-ring{position:absolute;border-radius:50%;border:2px solid;transition:all .7s}" +
3586      ".caf-voice-ripple{position:absolute;border-radius:50%;pointer-events:none}" +
3587      ".caf-voice-bars{position:absolute;display:flex;align-items:center;justify-content:center;width:130px;height:130px;pointer-events:none}" +
3588      ".caf-voice-bar{position:absolute;width:3px;border-radius:3px;transform-origin:center center}" +
3589
3590      ".caf-voice-mic-btn{position:relative;z-index:1;width:96px;height:96px;border-radius:50%;border:none;cursor:pointer;display:flex;align-items:center;justify-content:center;color:#fff;transition:background .3s,box-shadow .4s,transform .3s}" +
3591      ".caf-voice-mic-btn:active{transform:scale(.95)}" +
3592      ".caf-voice-mic-btn svg{width:40px;height:40px;fill:currentColor;filter:drop-shadow(0 1px 2px rgba(0,0,0,.15))}" +
3593
3594      ".caf-voice-exit{margin-top:4px;display:inline-flex;align-items:center;gap:8px;border-radius:999px;border:1px solid rgba(0,0,0,.08);background:rgba(255,255,255,.7);padding:10px 20px;font-size:12px;font-weight:500;color:#5b6573;cursor:pointer;transition:all .2s;-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px)}" +
3595      ".caf-voice-exit:hover{border-color:" + c + "40;color:" + c + ";transform:translateY(-2px);box-shadow:0 2px 8px rgba(0,0,0,.06)}" +
3596      ".caf-voice-exit:active{transform:scale(.95)}" +
3597      ".caf-voice-exit svg{width:16px;height:16px;fill:currentColor}" +
3598
3599      "@keyframes caf-voice-enter{0%{opacity:0;transform:translateY(40px) scale(.96)}60%{opacity:1;transform:translateY(-4px) scale(1.01)}100%{opacity:1;transform:translateY(0) scale(1)}}" +
3600      "@keyframes caf-voice-ripple{0%{transform:scale(1);opacity:.45}100%{transform:scale(2.6);opacity:0}}" +
3601      "@keyframes caf-voice-bar{0%,100%{transform:scaleY(.25)}50%{transform:scaleY(1)}}" +
3602      "@keyframes caf-mic-breathe{0%,100%{transform:scale(1);box-shadow:0 0 0 0 " + c + "40,0 0 40px 4px " + c + "1a}50%{transform:scale(1.06);box-shadow:0 0 0 8px " + c + "14,0 0 60px 8px " + c + "2e}}" +
3603      "@keyframes caf-speaking-glow{0%,100%{box-shadow:0 0 0 0 rgba(16,185,129,.2),0 0 30px 4px rgba(16,185,129,.08)}50%{box-shadow:0 0 0 12px rgba(16,185,129,.1),0 0 50px 12px rgba(16,185,129,.2)}}" +
3604
3605      /* ── footer ─────────────────────────────────────── */
3606      ".caf-footer{text-align:center;padding:7px 8px;font-size:11px;line-height:1.3;color:#475569;background:#f8fafc;border-top:1px solid #e2e8f0;flex-shrink:0}" +
3607
3608      /* ── desktop/tablet fullscreen ───────────────────── */
3609      ".caf-panel.open.caf-desktop-fs{position:fixed;inset:0;width:100vw;max-width:100vw;height:100vh;height:100dvh;max-height:100vh;border-radius:0;bottom:0;right:0;left:0;box-shadow:none;transition:transform .35s cubic-bezier(.4,0,.2,1),opacity .25s ease,border-radius .35s ease,width .35s ease,height .35s ease,inset .35s ease,visibility 0s 0s}" +
3610      ".caf-panel.open.caf-desktop-fs .caf-header{min-height:64px}" +
3611      ".caf-toggle.caf-fs-hide-desk{display:none!important}" +
3612      ".caf-bubble.caf-fs-hide-desk{display:none!important}" +
3613
3614      /* ── mobile fullscreen ──────────────────────────── */
3615      "@media(max-width:640px){" +
3616        ".caf-panel.open.caf-fullscreen{position:fixed;inset:0;width:100vw;max-width:100vw;height:100vh;height:100dvh;max-height:100vh;border-radius:0;bottom:0;box-shadow:none;touch-action:pan-y}" +
3617        ".caf-panel.open.caf-fullscreen .caf-header{padding-top:calc(env(safe-area-inset-top,0px) + 18px)}" +
3618        ".caf-panel.open.caf-fullscreen .caf-input-area{padding-bottom:calc(env(safe-area-inset-bottom,0px) + 16px)}" +
3619        /* CRITICAL iOS: prevent input auto-zoom by forcing >=16px on all inputs */
3620        ".caf-input{font-size:16px!important}" +
3621        ".caf-email-input{font-size:16px!important}" +
3622        ".caf-bk-fs-search input{font-size:16px!important;padding:13px 16px 13px 42px!important}" +
3623        ".caf-bk-fs-form .caf-bk-inp{font-size:16px!important;padding:14px 40px 14px 40px!important}" +
3624        /* bigger tap targets (WCAG ≥44px) */
3625        ".caf-header-btn{width:40px;height:40px}" +
3626        ".caf-mic{width:44px;height:44px}" +
3627        ".caf-send{width:46px;height:46px}" +
3628        /* prevent 300ms tap delay and double-tap zoom on interactive elements */
3629        ".caf-bk-card,.caf-bk-fs-tile,.caf-bk-fs-cal-chip,.caf-bk-fs-monthcal-day,.caf-bk-fs-btn,.caf-bk-pri,.caf-bk-act,.caf-bk-act2,.caf-quick button,.caf-bk-cta,.caf-bk-cta-btn,.caf-bk-locate,.caf-bk-expand-banner,.caf-bk-nearby-btn{touch-action:manipulation}" +
3630        ".caf-toggle.caf-fs-hide{display:none!important}" +
3631        ".caf-bubble.caf-fs-hide{display:none!important}" +
3632        ".caf-fs-btn{display:none!important}" +
3633        /* hide absolute-positioned buttons, show action bar instead */
3634        ".caf-copy-btn,.caf-translate-btn{display:none!important}" +
3635        ".caf-msg-actions{display:flex!important;align-items:center;gap:6px;margin-top:4px;padding:2px 0}" +
3636        ".caf-msg-actions button{display:inline-flex;align-items:center;gap:4px;border:none;background:transparent;color:#5b6573;font-size:11px;font-weight:500;font-family:inherit;padding:4px 8px;border-radius:8px;
3636cursor:pointer;-webkit-tap-highlight-color:transparent;touch-action:manipulation;transition:color .2s,background .15s}" +
3637        ".caf-msg-actions button:active{background:rgba(0,0,0,.06);color:#5b6573}" +
3638        ".caf-msg-actions button svg{width:13px;height:13px;flex-shrink:0}" +
3639        ".caf-msg-actions button.copied{color:var(--caf-c-ink)}" +
3640        ".caf-msg-actions .caf-lang-picker{position:absolute;bottom:28px;left:0;top:auto;right:auto}" +
3641
3642        /* ── mobile booking optimizations ── */
3643        /* grid collapses to single column on very small screens */
3644        ".caf-bk-grid{grid-template-columns:1fr!important;gap:6px}" +
3645        /* bigger touch targets for cards */
3646        ".caf-bk-card{padding:14px 14px 14px 42px;font-size:13px;min-height:48px;-webkit-tap-highlight-color:transparent}" +
3647        ".caf-bk-card-icon{left:12px;top:14px}" +
3648        /* bigger slot chips for fat fingers */
3649        ".caf-bk-slot-chip{padding:10px 16px;font-size:13px;min-height:44px;-webkit-tap-highlight-color:transparent}" +
3650        ".caf-bk-slot-time{font-size:13.5px}" +
3651        ".caf-bk-slot-period{font-size:10px;padding:3px 8px}" +
3652        /* day slots wrap nicely */
3653        ".caf-bk-day-slots{gap:8px}" +
3654        ".caf-bk-day-group{margin-bottom:14px}" +
3655        /* bigger form inputs for easy tapping */
3656        ".caf-bk-inp{padding:13px 38px 13px 38px;font-size:16px;border-radius:14px}" +
3657        ".caf-bk-inp-icon{left:14px}" +
3658        ".caf-bk-inp-icon svg{width:16px;height:16px}" +
3659        ".caf-bk-inp-check{right:14px}" +
3660        ".caf-bk-inp-label{font-size:11px;margin-left:6px}" +
3661        ".caf-bk-fg{gap:10px}" +
3662        /* search input bigger */
3663        ".caf-bk-search input{padding:12px 14px 12px 38px;font-size:16px;border-radius:14px}" +
3664        ".caf-bk-search-icon{left:14px}" +
3665        /* bigger primary button */
3666        ".caf-bk-pri{padding:15px;font-size:14px;border-radius:16px;min-height:50px}" +
3667        /* Sticky CTA: tiene il bottone sopra la tastiera + safe-area iOS */
3668        ".caf-bk-cta-sticky{position:sticky;bottom:8px;bottom:calc(8px + env(safe-area-inset-bottom,0px));z-index:5;backdrop-filter:saturate(1.4)}" +
3669        /* gender selector bigger touch */
3670        ".caf-bk-rb{padding:8px 16px;font-size:12px;min-height:40px}" +
3671        ".caf-bk-rr{gap:10px;padding:10px 0}" +
3672        /* step dots slightly bigger */
3673        ".caf-bk-step-dot{width:28px;height:28px;font-size:11px}" +
3674        ".caf-bk-step-lbl{font-size:10px;width:28px}" +
3675        /* crumb with more padding */
3676        ".caf-bk-crumb{padding:6px 14px;font-size:11px}" +
3677        /* back/cancel bigger tap areas */
3678        ".caf-bk-back,.caf-bk-cancel{padding:8px 4px;font-size:12px;min-height:36px;display:inline-flex;align-items:center}" +
3679        ".caf-bk-bot{padding-top:10px;margin-top:14px}" +
3680        /* more button bigger */
3681        ".caf-bk-more{padding:12px;font-size:12px;min-height:44px}" +
3682        /* summary card more spacious */
3683        ".caf-bk-sum{padding:16px;font-size:12px;border-radius:18px}" +
3684        ".caf-bk-sr{padding:5px 0}" +
3685        /* confirmation buttons */
3686        ".caf-bk-act,.caf-bk-act2{padding:10px 16px;font-size:12px;min-height:44px}" +
3687        /* error state bigger touch */
3688        ".caf-bk-acts{gap:8px}" +
3689        /* hint text bigger */
3690        ".caf-bk-title{font-size:15px}" +
3691        /* progress bar thicker */
3692        ".caf-bk-progress{height:4px;margin-top:8px}" +
3693        ".caf-bk-step-counter{font-size:11px;margin-top:6px}" +
3694        /* done page spacing */
3695        ".caf-bk-done-ic{width:64px;height:64px;margin-bottom:14px}" +
3696        ".caf-bk-done-t{font-size:17px}" +
3697        ".caf-bk-done-id{font-size:11px;padding:6px 16px}" +
3698        ".caf-bk-email{padding:12px 16px;font-size:11.5px;border-radius:14px}" +
3699      "}" +
3700
3701      /* ── extra small screens (< 380px, e.g. iPhone SE) ── */
3702      "@media(max-width:380px){" +
3703        ".caf-bk-steps-wrap{margin-bottom:8px}" +
3704        ".caf-bk-step-dot{width:24px;height:24px;font-size:9px}" +
3705        ".caf-bk-step-lbl{font-size:8px;width:24px}" +
3706        ".caf-bk-step-labels{padding:0}" +
3707        ".caf-bk-title{font-size:14px;gap:4px}" +
3708        ".caf-bk-card{padding:12px 12px 12px 38px;font-size:12.5px}" +
3709        ".caf-bk-slot-chip{padding:9px 12px;font-size:12.5px}" +
3710        ".caf-bk-sum{padding:12px}" +
3711      "}" +
3712      "@media(prefers-reduced-motion:reduce){" +
3713        "*,*::before,*::after{animation-duration:0.01ms!important;animation-iteration-count:1!important;transition-duration:0.01ms!important}" +
3714        ".caf-typing span,.caf-vt-dots span{animation:none!important;transform:scale(1)!important;opacity:.6!important}" +
3715      "}" +
3716
3717      /* ── booking UI ─────────────────────────────────── */
3718      "@keyframes caf-spin{0%{transform:rotate(0)}100%{transform:rotate(360deg)}}" +
3719      "@keyframes caf-confetti{0%{opacity:1;transform:translateY(0) rotate(0)}100%{opacity:0;transform:translateY(-60px) rotate(720deg)}}" +
3720      "@keyframes caf-check-draw{0%{stroke-dashoffset:24}100%{stroke-dashoffset:0}}" +
3721      "@keyframes caf-pulse-soft{0%,100%{opacity:.6}50%{opacity:1}}" +
3722      "@keyframes caf-shimmer-bar{0%{transform:translateX(-100%)}100%{transform:translateX(200%)}}" +
3723      "@keyframes caf-slide-in{0%{opacity:0;transform:translateX(30px)}100%{opacity:1;transform:translateX(0)}}" +
3724      "@keyframes caf-slide-in-back{0%{opacity:0;transform:translateX(-30px)}100%{opacity:1;transform:translateX(0)}}" +
3725      "@keyframes caf-fade-up{0%{opacity:0;transform:translateY(12px)}100%{opacity:1;transform:translateY(0)}}" +
3726      "@keyframes caf-ripple{0%{transform:scale(0);opacity:.4}100%{transform:scale(2.5);opacity:0}
3726}" +
3727      "@keyframes caf-progress-fill{0%{width:0}100%{width:var(--caf-prog,0%)}}" +
3728      ".caf-bk{animation:caf-pop-in .35s ease-out}" +
3729      ".caf-bk-slide-fwd{animation:caf-slide-in .35s ease-out}" +
3730      ".caf-bk-slide-back{animation:caf-slide-in-back .35s ease-out}" +
3731
3732      /* step indicator — checkmarks on completed */
3733      ".caf-bk-steps-wrap{margin-bottom:12px}" +
3734      ".caf-bk-steps{display:flex;align-items:center;gap:4px;margin-bottom:4px}" +
3735      ".caf-bk-step-dot{width:26px;height:26px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:10px;font-weight:700;transition:all .4s cubic-bezier(.34,1.56,.64,1);flex-shrink:0}" +
3736      ".caf-bk-step-dot.done{color:#fff;transform:scale(1)}" +
3737      ".caf-bk-step-dot.active{border:2.5px solid " + c + ";color:" + c + ";background:" + c + "0a;box-shadow:0 0 0 4px " + c + "12}" +
3738      ".caf-bk-step-dot.todo{border:2px solid #e2e8f0;color:#5b6573;background:#fff}" +
3739      ".caf-bk-step-line{flex:1;height:2px;border-radius:1px;transition:background .4s}" +
3740      ".caf-bk-step-labels{display:flex;justify-content:space-between;padding:0 2px}" +
3741      ".caf-bk-step-lbl{font-size:9px;color:#5b6573;text-align:center;width:26px;flex-shrink:0;transition:color .3s}" +
3742      ".caf-bk-step-lbl.active{color:" + c + ";font-weight:600}" +
3743      ".caf-bk-step-lbl.done{color:" + c + "90}" +
3744      ".caf-bk-progress{width:100%;height:3px;border-radius:2px;background:#e2e8f0;margin-top:6px;overflow:hidden}" +
3745      ".caf-bk-progress-bar{height:100%;border-radius:2px;background:linear-gradient(90deg," + c + "," + c + "cc);transition:width .5s cubic-bezier(.34,1.56,.64,1)}" +
3746      ".caf-bk-step-counter{font-size:10px;color:#5b6573;text-align:right;margin-top:4px}" +
3747      ".caf-bk-title{font-size:14px;font-weight:700;color:#1e293b;margin-bottom:8px;display:flex;align-items:center;gap:6px}" +
3748
3749      /* crumb with icon */
3750      ".caf-bk-crumb{display:inline-flex;align-items:center;gap:5px;border-radius:99px;background:" + c + "0a;border:1px solid " + c + "20;padding:4px 12px;font-size:10px;color:" + c + ";font-weight:500;margin-bottom:8px}" +
3751      ".caf-bk-crumb svg{flex-shrink:0;opacity:.7}" +
3752
3753      /* cards with icon stripe */
3754      ".caf-bk-grid{display:grid;grid-template-columns:1fr 1fr;gap:8px}" +
3755      ".caf-bk-list{display:flex;flex-direction:column;gap:8px}" +
3756      ".caf-bk-card{position:relative;overflow:hidden;border-radius:14px;border:1.5px solid #e2e8f0;background:#fff;padding:12px 14px 12px 40px;text-align:left;cursor:pointer;transition:all .25s cubic-bezier(.34,1.56,.64,1);font-family:inherit;font-size:12.5px;font-weight:600;color:#334155;-webkit-tap-highlight-color:transparent;touch-action:manipulation}" +
3757      ".caf-bk-card::before{content:'';position:absolute;left:0;top:0;bottom:0;width:4px;background:" + c + "30;border-radius:14px 0 0 14px;transition:width .3s,background .3s}" +
3758      ".caf-bk-card:hover{border-color:" + c + "50;box-shadow:0 6px 24px " + c + "18;transform:translateY(-2px)}" +
3759      ".caf-bk-card:hover::before{width:6px;background:" + c + "}" +
3760      ".caf-bk-card:active{transform:scale(.97)}" +
3761      ".caf-bk-card .caf-ripple{position:absolute;border-radius:50%;background:" + c + "20;animation:caf-ripple .5s ease-out forwards;pointer-events:none}" +
3762      ".caf-bk-card-icon{position:absolute;left:10px;top:12px;width:22px;height:22px;border-radius:7px;background:" + c + "12;color:" + c + ";display:flex;align-items:center;justify-content:center}" +
3763      ".caf-bk-card-icon svg{width:13px;height:13px}" +
3764      ".caf-bk-sub{display:flex;align-items:center;gap:4px;margin-top:3px;font-size:10px;font-weight:400;color:#5b6573}" +
3765      ".caf-bk-sub svg{flex-shrink:0;opacity:.6}" +
3766
3767      /* slot badge */
3768      ".caf-bk-badge{display:inline-flex;align-items:center;gap:4px;border-radius:99px;padding:3px 8px;font-size:10px;font-weight:600;margin-top:5px}" +
3769      ".caf-bk-badge svg{width:10px;height:10px}" +
3770
3771      /* spinner — shimmer bar + animated text */
3772      ".caf-bk-spin{display:flex;flex-direction:column;align-items:center;gap:12px;padding:24px 0}" +
3773      ".caf-bk-spin-wrap{position:relative;width:48px;height:48px}" +
3774      ".caf-bk-ring{width:48px;height:48px;border-radius:50%;border:3px solid #e2e8f0;border-top-color:" + c + ";animation:caf-spin .7s linear infinite}" +
3775      ".caf-bk-spin-icon{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;font-size:18px;animation:caf-pulse-soft 1.5s ease-in-out infinite}" +
3776      ".caf-bk-spin-lbl{font-size:12.5px;color:#475569;font-weight:500;text-align:center;
3776max-width:200px}" +
3777      ".caf-bk-spin-bar{width:120px;height:3px;border-radius:2px;background:#e2e8f0;overflow:hidden;margin-top:2px}" +
3778      ".caf-bk-spin-bar-inner{width:40%;height:100%;border-radius:2px;background:linear-gradient(90deg," + c + "," + c + "80);animation:caf-shimmer-bar 1.2s ease-in-out infinite}" +
3779
3780      /* slot day group headers */
3781      ".caf-bk-day-group{margin-bottom:10px}" +
3782      ".caf-bk-day-hdr{display:flex;align-items:center;gap:8px;margin-bottom:6px;padding:0 2px}" +
3783      ".caf-bk-day-name{font-size:12px;font-weight:700;color:#334155;text-transform:capitalize}" +
3784      ".caf-bk-day-date{font-size:10px;color:#5b6573;font-weight:500}" +
3785      ".caf-bk-day-line{flex:1;height:1px;background:#e2e8f0}" +
3786      ".caf-bk-day-slots{display:flex;flex-wrap:wrap;gap:6px}" +
3787      ".caf-bk-slot-chip{position:relative;overflow:hidden;display:inline-flex;align-items:center;gap:5px;border-radius:12px;border:1.5px solid #e2e8f0;background:#fff;padding:8px 14px;font-size:12px;font-weight:600;color:#334155;cursor:pointer;transition:all .25s cubic-bezier(.34,1.56,.64,1);font-family:inherit;-webkit-tap-highlight-color:transparent;touch-action:manipulation}" +
3788      ".caf-bk-slot-chip:hover{border-color:" + c + "50;background:" + c + "06;box-shadow:0 4px 16px " + c + "14;transform:translateY(-1px)}" +
3789      ".caf-bk-slot-chip:active{transform:scale(.96)}" +
3790      ".caf-bk-slot-chip .caf-ripple{position:absolute;border-radius:50%;background:" + c + "20;animation:caf-ripple .5s ease-out forwards;pointer-events:none}" +
3791      ".caf-bk-slot-time{font-weight:700;color:#1e293b}" +
3792      ".caf-bk-slot-period{font-size:9px;font-weight:600;border-radius:6px;padding:2px 6px}" +
3793      ".caf-bk-slot-period.matt{background:#eff6ff;color:#2563eb}" +
3794      ".caf-bk-slot-period.pom{background:#fff7ed;color:#c2410c}" +
3795
3796      /* search filter */
3797      ".caf-bk-search{position:relative;margin-bottom:10px}" +
3798      ".caf-bk-search-icon{position:absolute;left:12px;top:50%;transform:translateY(-50%);color:#5b6573;pointer-events:none;display:flex}" +
3799      ".caf-bk-search-icon svg{width:14px;height:14px}" +
3800      ".caf-bk-search input{width:100%;border:1.5px solid #818c9c;border-radius:12px;padding:9px 14px 9px 36px;font-size:12px;color:#1e293b;font-family:inherit;outline:none;background:#f8fafc;transition:border-color .2s,box-shadow .2s}" +
3801      ".caf-bk-search input:focus{border-color:" + c + ";box-shadow:0 0 0 3px " + c + "15;background:#fff}" +
3802      ".caf-bk-search input::placeholder{color:#5b6573}" +
3803      ".caf-bk-no-results{text-align:center;padding:16px;font-size:12px;color:#5b6573;animation:caf-fade-up .3s ease-out}" +
3804
3805      /* form inputs with icon */
3806      ".caf-bk-fg{display:flex;flex-direction:column;gap:8px}" +
3807      ".caf-bk-inp-wrap{position:relative}" +
3808      ".caf-bk-inp-icon{position:absolute;left:12px;top:50%;transform:translateY(-50%);color:#5b6573;pointer-events:none;display:flex;transition:color .2s}" +
3809      ".caf-bk-inp-icon svg{width:14px;height:14px}" +
3810      ".caf-bk-inp-valid .caf-bk-inp-icon{color:#047857}" +
3811      ".caf-bk-inp-check{position:absolute;right:12px;top:50%;transform:translateY(-50%) scale(0);color:#047857;display:flex;transition:transform .3s cubic-bezier(.34,1.56,.64,1)}" +
3812      ".caf-bk-inp-check svg{width:14px;height:14px}" +
3813      ".caf-bk-inp-valid .caf-bk-inp-check{transform:translateY(-50%) scale(1)}" +
3814      ".caf-bk-inp{width:100%;border:1.5px solid #818c9c;border-radius:12px;padding:10px 36px 10px 36px;font-size:12.5px;color:#1e293b;font-family:inherit;outline:none;background:#f8fafc;transition:border-color .25s,box-shadow .25s,background .25s}" +
3815      ".caf-bk-inp:focus{border-color:" + c + ";box-shadow:0 0 0 3px " + c + "15;background:#fff}" +
3816      ".caf-bk-inp::placeholder{color:#5b6573}" +
3817      ".caf-bk-inp-valid .caf-bk-inp{border-color:#10b981;background:#f0fdf4}" +
3818      ".caf-bk-inp-valid .caf-bk-inp:focus{border-color:" + c + ";box-shadow:0 0 0 3px " + c + "15;background:#fff}" +
3819      ".caf-bk-inp-label{font-size:10px;font-weight:600;color:#5b6573;margin-bottom:-4px;margin-left:4px}" +
3820      ".caf-bk-rr{display:flex;align-items:center;gap:8px;padding:8px 0}" +
3821      ".caf-bk-rl{font-size:11px;color:#5b6573;margin-right:4px}" +
3822      ".caf-bk-rb{display:inline-flex;align-items:center;gap:4px;border-radius:99px;padding:5px 12px;font-size:11px;font-weight:500;cursor:pointer;transition:all .2s;border:none}" +
3823      ".caf-bk-rb.on{color:#fff}" +
3824      ".caf-bk-rb.off{background:#f1f5f9;color:#5b6573}" +
3825      ".caf-bk-rb.off:hover{background:#e2e8f0}" +
3826      ".caf-bk-ferr{display:flex;align-items:center;gap:6px;border-radius:10px;background:#fff1f2;padding:8px 12px;font-size:11px;color:#be123c;margin-top:6px;animation:caf-pop-in .2s ease-out}" +
3827      ".caf-bk-pri{width:100%;display:flex;align-items:center;justify-content:center;gap:6px;border-radius:14px;border:none;padding:13px;font-size:13px;font-weight:700;color:#fff;cursor:pointer;transition:all .25s;font-family:inherit;margin-top:10px;letter-spacing:.2px;-webkit-tap-highlight-color:transparent;touch-action:manipulation}" +
3828      ".caf-bk-pri:hover{transform:translateY(-2px);filter:brightness(1.08);box-shadow:0 8px 24px " + c + "40}" +
3829      ".caf-bk-pri:active{transform:scale(.96);filter:brightness(.95)}" +
3830      ".caf-bk-pri:disabled{opacity:.5;cursor:not-allowed;transform:none;filter:none;box-shadow:none}" +
3831      ".caf-bk-sum{border-radius:16px;border:1px solid " + c + "20;background:linear-gradient(135deg," + c + "06," + c + "02,#fff);padding:14px;font-size:11.5px;color:#475569;backdrop-filter:blur(8px)}" +
3832      ".caf-bk-sr{display:flex;align-items:flex-start;gap:8px;padding:4px 0}" +
3833      ".caf-bk-si{flex-shrink:0;color:" + c + ";margin-top:1px}" +
3834      ".caf-bk-sv{font-weight:600;color:#334155}" +
3835      ".caf-bk-ss{font-size:10px;color:#5b6573;display:block}" +
3836      ".caf-bk-sum hr{border:none;border-top:1px solid " + c + "15;margin:6px 0}" +
3837      ".caf-bk-done-ic{position:relative;display:flex;align-items:center;justify-content:center;width:56px;height:56px;border-radius:50%;margin:0 auto 10px;color:#fff}" +
3838      ".caf-bk-done-ic svg{animation:caf-check-draw .6s ease-out .3s both}" +
3839      ".caf-bk-done-ic svg path{stroke-dasharray:24;stroke-dashoffset:24}" +
3840      ".caf-bk-confetti{position:absolute;width:6px;height:6px;border-radius:2px;animation:caf-confetti 1s ease-out forwards}" +
3841      ".caf-bk-done-t{font-size:15px;font-weight:700;color:#1e293b;text-align:center}" +
3842      ".caf-bk-done-id{display:inline-flex;align-items:center;gap:4px;border-radius:99px;padding:4px 14px;font-size:10px;font-weight:600;margin:8px auto;text-align:center}" +
3843      ".caf-bk-email{display:flex;align-items:center;gap:8px;border-radius:12px;background:linear-gradient(135deg,#eff6ff,#f0fdf4);border:1px solid #bfdbfe;padding:10px 14px;font-size:10.5px;color:#1d4ed8;margin-top:10px}" +
3844      ".caf-bk-ew{display:flex;align-items:flex-start;gap:10px;margin-bottom:10px}" +
3845      ".caf-bk-ei{flex-shrink:0;width:32px;height:32px;border-radius:50%;display:flex;align-items:center;justify-content:center}" +
3846      ".caf-bk-et{font-size:13px;font-weight:700}" +
3847      ".caf-bk-em{font-size:11px;color:#5b6573;margin-top:3px;line-height:1.5}" +
3848      ".caf-bk-acts{display:flex;flex-wrap:wrap;gap:6px}" +
3849      ".caf-bk-act{display:inline-flex;align-items:center;gap:4px;border-radius:12px;border:1px solid " + c + "25;background:" + c + "08;padding:8px 14px;font-size:11px;font-weight:600;color:" + c + ";cursor:pointer;transition:all .25s;font-family:inherit}" +
3850      ".caf-bk-act:hover{transform:translateY(-1px);box-shadow:0 4px 12px " + c + "18}" +
3851      ".caf-bk-act2{display:inline-flex;align-items:center;gap:4px;border-radius:12px;border:1px solid #e2e8f0;background:transparent;padding:8px 14px;font-size:11px;color:#5b6573;cursor:pointer;transition:all .2s;font-family:inherit}" +
3852      ".caf-bk-act2:hover{border-color:#cbd5e1;color:#5b6573}" +
3853      ".caf-bk-bot{display:flex;align-items:center;justify-content:space-between;padding-top:8px;border-top:1px solid #f1f5f9;margin-top:10px}" +
3854      ".caf-bk-back{display:inline-flex;align-items:center;gap:3px;font-size:11px;color:#5b6573;cursor:pointer;border:none;background:none;font-family:inherit;padding:0;transition:color .2s}" +
3855      ".caf-bk-back:hover{color:var(--caf-c-ink)}" +
3856      ".caf-bk-cancel{display:inline-flex;align-items:center;gap:3px;font-size:11px;color:#5b6573;cursor:pointer;border:none;background:none;font-family:inherit;padding:0;transition:color .2s}" +
3857      ".caf-bk-cancel:hover{color:#be123c}" +
3858      ".caf-bk-more{width:100%;display:flex;align-items:center;justify-content:center;gap:6px;border-radius:12px;border:1px dashed " + c + "40;background:" + c + "06;padding:8px;font-size:11px;font-weight:600;color:" + c + ";cursor:pointer;transition:all .25s;font-family:inherit;margin-top:6px}" +
3859      ".caf-bk-more:hover{border-color:" + c + "80;background:" + c + "12}" +
3860
3861      /* ─── persistent booking CTA (above input) ─── */
3862      "@keyframes caf-pulse-ring{0%{opacity:.8;transform:scale(1)}100%{opacity:0;transform:scale(1.55)}}" +
3863      "@keyframes caf-cta-slide{0%{opacity:0;transform:translateY(8px)}100%{opacity:1;transform:translateY(0)}}" +
3864      "@keyframes caf-cta-shimmer{0%{transform:translateX(-120%)}60%,100%{transform:translateX(220%)}}" +
3865      "@keyframes caf-dot-pulse{0%,100%{opacity:1;transform:scale(1)}50%{opacity:.5;transform:scale(1.25)}}" +
3866      "@keyframes caf-aurora{0%{background-position:0% 50%}50%{background-position:100% 50%}100%{background-position:0% 50%}}" +
3867      ".caf-bk-cta{position:relative;display:flex;align-items:center;gap:11px;padding:10px 12px 10px 14px;margin:0 14px 8px;border-radius:14px;background:linear-gradient(135deg," + c + "14," + c + "04 60%,#fff);border:1px solid " + c + "35;box-shadow:0 2px 10px " + c + "18;animation:caf-cta-slide .4s ease-out;cursor:pointer;transition:all .3s cubic-bezier(.34,1.56,.64,1);font-family:inherit;flex-shrink:0;overflow:hidden;transform-style:preserve-3d;perspective:900px}" +
3868      ".caf-bk-cta::after{content:'';position:absolute;top:0;bottom:0;width:40%;background:linear-gradient(100deg,transparent 0%,rgba(255,255,255,.6) 50%,transparent 100%);transform:translateX(-120%);animation:caf-cta-shimmer 7s ease-in-out infinite;animation-delay:2s;pointer-events:none}" +
3869      ".caf-bk-cta:hover{transform:translateY(-2px) rotateX(2deg);box-shadow:0 12px 30px " + c + "38;border-color:" + c + "70}" +
3870      ".caf-bk-cta:active{transform:scale(.99)}" +
3871      ".caf-bk-cta-ic{width:36px;height:36px;border-radius:11px;background:linear-gradient(135deg," + c + "," + cGradEnd + ");color:var(--caf-on-c);display:flex;align-items:center;justify-content:center;flex-shrink:0;position:relative;box-shadow:0 4px 14px " + c + "44;transition:transform .4s cubic-bezier(.34,1.56,.64,1)}" +
3872      ".caf-bk-cta:hover .caf-bk-cta-ic{transform:rotate(-8deg) 
3872scale(1.08)}" +
3873      ".caf-bk-cta-ic::before{content:'';position:absolute;inset:-2px;border-radius:13px;border:2px solid " + c + ";animation:caf-pulse-ring 2s ease-out infinite;pointer-events:none}" +
3874      ".caf-bk-cta-ic svg{width:17px;height:17px;position:relative;z-index:1;transition:transform .35s}" +
3875      ".caf-bk-cta-txt{flex:1;min-width:0;position:relative;z-index:1}" +
3876      ".caf-bk-cta-t{font-size:12.5px;font-weight:700;color:#0f172a;line-height:1.2;letter-spacing:-.1px;display:flex;align-items:center;gap:6px}" +
3877      ".caf-bk-cta-s{font-size:10.5px;color:#5b6573;margin-top:2px;line-height:1.35;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}" +
3878      ".caf-bk-cta-badge{display:inline-flex;align-items:center;gap:4px;font-size:9.5px;font-weight:700;color:#047857;background:#d1fae5;border-radius:99px;padding:2px 7px;letter-spacing:.2px;text-transform:uppercase;flex-shrink:0}" +
3879      ".caf-bk-cta-badge .d{width:6px;height:6px;border-radius:50%;background:#10b981;animation:caf-dot-pulse 1.4s ease-in-out infinite}" +
3880      ".caf-bk-cta-btn{position:relative;z-index:1;flex-shrink:0;border:none;background:linear-gradient(135deg," + c + "," + cGradEnd + ");color:#fff;border-radius:99px;padding:8px 14px;font-size:11px;font-weight:700;cursor:pointer;font-family:inherit;box-shadow:0 4px 12px " + c + "44;transition:all .2s;display:inline-flex;align-items:center;gap:4px;letter-spacing:.2px;white-space:nowrap;-webkit-tap-highlight-color:transparent}" +
3881      ".caf-bk-cta-btn:hover{transform:scale(1.05);box-shadow:0 6px 16px " + c + "55;filter:brightness(1.08)}" +
3882      ".caf-bk-cta-btn:active{transform:scale(.96)}" +
3883      ".caf-bk-cta-btn svg{width:11px;height:11px}" +
3884      ".caf-bk-cta-dismiss{position:absolute;top:4px;right:4px;width:20px;height:20px;border-radius:50%;border:none;background:transparent;color:#5b6573;cursor:pointer;display:flex;align-items:center;justify-content:center;z-index:2;transition:all .2s;font-family:inherit}" +
3885      ".caf-bk-cta-dismiss:hover{background:rgba(0,0,0,.05);color:#475569}" +
3886      ".caf-bk-cta-dismiss svg{width:12px;height:12px}" +
3887      /* proactive enlarged state */
3888      ".caf-bk-cta.proactive{padding:14px 14px 14px 16px;gap:14px;background:linear-gradient(135deg," + c + "22," + c + "08 55%,#fff);border-color:" + c + "55;box-shadow:0 8px 28px " + c + "28}" +
3889      ".caf-bk-cta.proactive .caf-bk-cta-ic{width:42px;height:42px;border-radius:12px}" +
3890      ".caf-bk-cta.proactive .caf-bk-cta-ic svg{width:20px;height:20px}" +
3891      ".caf-bk-cta.proactive .caf-bk-cta-t{font-size:13.5px}" +
3892      ".caf-bk-cta.proactive .caf-bk-cta-s{font-size:11px;white-space:normal;line-height:1.4}" +
3893      /* mini pill (dismissed) */
3894      ".caf-bk-cta-mini{position:absolute;right:14px;bottom:88px;z-index:6;border:none;background:linear-gradient(135deg," + c + "," + cGradEnd + ");color:#fff;border-radius:99px;padding:10px 14px 10px 10px;font-size:11.5px;font-weight:700;font-family:inherit;cursor:pointer;display:inline-flex;align-items:center;gap:6px;box-shadow:0 8px 22px " + c + "55;transition:all .25s;animation:caf-cta-slide .35s ease-out;-webkit-tap-highlight-color:transparent}" +
3895      ".caf-bk-cta-mini:hover{transform:translateY(-2px);box-shadow:0 12px 28px " + c + "66}" +
3896      ".caf-bk-cta-mini:active{transform:scale(.96)}" +
3897      ".caf-bk-cta-mini-ic{width:22px;height:22px;border-radius:50%;background:rgba(255,255,255,.25);display:flex;align-items:center;justify-content:center}" +
3898      ".caf-bk-cta-mini-ic svg{width:12px;height:12px}" +
3899      "@media(max-width:380px){.caf-bk-cta-s{display:none}.caf-bk-cta{gap:8px;padding:9px 10px}.caf-bk-cta-btn{padding:7px 11px;font-size:10.5px}.caf-bk-cta.proactive{padding:12px}}" +
3900
3901      /* ── ghost suggestion chips (idle input hints) ─── */
3902      "@keyframes caf-ghost-up{0%{opacity:0;transform:translateY(6px)}100%{opacity:1;transform:translateY(0)}}" +
3903      ".caf-ghost-sug{display:flex;gap:6px;padding:0 14px 6px;flex-wrap:wrap;animation:caf-ghost-up .35s ease-out;flex-shrink:0}" +
3904      ".caf-ghost-sug-chip{border:1px dashed " + c + "45;background:" + c + "06;color:" + c + ";border-radius:99px;padding:5px 11px;font-size:10.5px;font-weight:600;cursor:pointer;font-family:inherit;transition:all .2s;-webkit-tap-highlight-color:transparent;display:inline-flex;align-items:center;gap:4px}" +
3905      ".caf-ghost-sug-chip:hover{background:" + c + "14;border-style:solid;transform:translateY(-1px)}" +
3906      ".caf-ghost-sug-chip svg{width:10px;height:10px}" +
3907
3908      /* ── inline message chip (prenota su singolo servizio citato) ── */
3909      ".caf-inline-bk-chip{display:inline-flex;align-items:center;gap:5px;margin-top:8px;border:1px solid " + c + "40;background:linear-gradient(135deg," + c + "12," + c + "04);color:" + c + ";border-radius:99px;padding:6px 12px;font-size:11px;font-weight:700;cursor:pointer;font-family:inherit;transition:all .2s;-webkit-tap-highlight-color:transparent}" +
3910      ".caf-inline-bk-chip:hover{background:" + c + "20;transform:translateY(-1px);box-shadow:0 4px 12px " + c + "30}" +
3911      ".caf-inline-bk-chip svg{width:11px;height:11px}" +
3912
3913      /* ─── prominent expand BANNER (outside conversation, under header) ─── */
3914      "@keyframes caf-banner-slide{0%{opacity:0;transform:translateY(-14px)}100%{opacity:1;transform:translateY(0)}}" +
3915      ".caf-bk-expand-banner{display:none;align-items:center;gap:12px;width:100%;padding:11px 14px;background:linear-gradient(135deg," + c + "," + cGradEnd + ");color:#fff;border:none;cursor:pointer;font-family:inherit;flex-shrink:0;text-align:left;box-shadow:0 4px 14px " + c + "40;transition:filter .2s,box-shadow .2s;position:relative;overflow:hidden;animation:caf-banner-slide .4s cubic-bezier(.34,1.56,.64,1);z-index:4;-webkit-tap-highlight-color:transparent}" +
3916      ".caf-bk-expand-banner::after{content:'';position:absolute;top:0;bottom:0;width:40%;background:linear-gradient(100deg,transparent 0%,rgba(255,255,255,.35) 50%,transparent 100%);transform:translateX(-120%);animation:caf-cta-shimmer 5s ease-in-out infinite;animation-delay:1.5s;pointer-events:none}" +
3917      ".caf-bk-expand-banner.active{display:flex}" +
3918      ".caf-bk-expand-banner:hover{filter:brightness(1.08);box-shadow:0 8px 22px " + c + "55}" +
3919      ".caf-bk-expand-banner:active{transform:scale(.995)}" +
3920      ".caf-bk-expand-banner-ic{width:32px;height:32px;border-radius:10px;background:rgba(255,255,255,.22);backdrop-filter:blur(8px);display:flex;align-items:center;justify-content:center;flex-shrink:0;position:relative;z-index:1}" +
3921      ".caf-bk-expand-banner-ic svg{width:16px;height:16px}" +
3922      ".caf-bk-expand-banner-txt{flex:1;min-width:0;position:relative;z-index:1}" +
3923      ".caf-bk-expand-banner-t{font-size:13px;font-weight:700;letter-spacing:-.1px;line-height:1.2;display:flex;align-items:center;gap:6px}" +
3924      ".caf-bk-expand-banner-t .live-dot{width:7px;height:7px;border-radius:50%;background:#fff;animation:caf-dot-pulse 1.4s ease-in-out infinite;flex-shrink:0}" +
3925      ".caf-bk-expand-banner-s{font-size:10.5px;opacity:.9;margin-top:2px;line-height:1.3;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}" +
3926      ".caf-bk-expand-banner-arrow{flex-shrink:0;display:flex;align-items:center;gap:4px;font-size:11px;font-weight:700;padding:6px 10px;background:rgba(255,255,255,.2);border-radius:99px;transition:all .25s;position:relative;z-index:1}" +
3927      ".caf-bk-expand-banner:hover .caf-bk-expand-banner-arrow{background:rgba(255,255,255,.32);transform:translateX(3px)}" +
3928      ".caf-bk-expand-banner-arrow svg{width:11px;height:11px}" +
3929      "@media(max-width:380px){.caf-bk-expand-banner-s{display:none}.caf-bk-expand-banner{padding:10px 12px;gap:10px}.caf-bk-expand-banner-arrow{padding:5px 8px;font-size:10.5px}}" +
3930
3931      /* (legacy inline small button — kept hidden for backward compat) */
3932      ".caf-bk{position:relative}" +
3933      ".caf-bk-expand{display:none}" +
3934
3935      /* ─── FULLSCREEN BOOKING OVERLAY ─── */
3936      ".caf-bk-fs{position:absolute;inset:0;background:linear-gradient(180deg,#f8fafc 0%,#eef2f6 100%);display:flex;flex-direction:column;z-index:60;animation:caf-pop-in .35s ease-out;overflow:hidden;font-family:inherit;-webkit-overflow-scrolling:touch}" +
3937      ".caf-panel.open .caf-bk-fs{border-radius:inherit}" +
3938      "@media(max-width:640px){.caf-bk-fs{border-radius:0}}" +
3939
3940      /* hero with animated aurora gradient */
3941      ".caf-bk-fs-hero{position:relative;padding:18px 20px 16px;background:linear-gradient(135deg," + c + " 0%," + c + "cc 30%," + c + "d9 55%," + c + "99 80%," + c + " 100%);background-size:250% 250%;animation:caf-aurora 14s ease infinite;color:#fff;overflow:hidden;flex-shrink:0}" +
3942      "@media(max-width:640px){.caf-bk-fs-hero{padding:calc(env(safe-area-inset-top,0px) + 14px) 16px 14px}}" +
3943      ".caf-bk-fs-hero::before{content:'';position:absolute;top:-50%;right:-10%;width:300px;height:300px;background:radial-gradient(circle,rgba(255,255,255,.22),transparent 65%);pointer-events:none}" +
3944      ".caf-bk-fs-hero::after{content:'';position:absolute;bottom:-80%;left:-15%;width:340px;height:340px;background:radial-gradient(circle,rgba(255,255,255,.10),transparent 65%);pointer-events:none}" +
3945      ".caf-bk-fs-hrow{display:flex;align-items:center;gap:12px;position:relative;z-index:1}" +
3946      ".caf-bk-fs-hlogo{width:40px;height:40px;border-radius:12px;background:rgba(255,255,255,.2);backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);display:flex;align-items:center;justify-content:center;flex-shrink:0;border:1px solid rgba(255,255,255,.25)}" +
3947      ".caf-bk-fs-hlogo svg{width:20px;height:20px}" +
3948      ".caf-bk-fs-htxt{flex:1;min-width:0}" +
3949      ".caf-bk-fs-htitle{font-size:17px;font-weight:800;letter-spacing:-.3px;line-height:1.2}" +
3950      ".caf-bk-fs-hsub{font-size:11.5px;opacity:.88;margin-top:3px;font-weight:500}" +
3951      ".caf-bk-fs-hclose{border:none;background:rgba(255,255,255,.18);color:#fff;width:36px;height:36px;border-radius:50%;display:flex;align-items:center;justify-content:center;cursor:pointer;transition:all .2s;font-family:inherit;flex-shrink:0}" +
3952      ".caf-bk-fs-hclose:hover{background:rgba(255,255,255,.32);transform:rotate(90deg)}" +
3953      ".caf-bk-fs-hclose svg{width:16px;height:16px}" +
3954
3955      /* horizontal step rail inside hero */
3956      ".caf-bk-fs-rail{margin-top:14px;position:relative;z-index:1}" +
3957      ".caf-bk-fs-rail-track{display:flex;align-items:center;gap:4px;margin-bottom:5px}" +
3958      ".caf-bk-fs-rail-dot{width:26px;height:26px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:11px;font-weight:700;flex-shrink:0;transition:all .4s cubic-bezier(.34,1.56,.64,1);background:rgba(255,255,255,.18);color:rgba(255,255,255,.65);border:2px solid transparent}" +
3959      ".caf-bk-fs-rail-dot.done{background:#fff;color:" + c + "}" +
3960      ".caf-bk-fs-rail-dot.active{background:#fff;color:" + c + ";box-shadow:0 0 0 5px rgba(255,255,255,.25);transform:scale(1.08)}" +
3961      ".caf-bk-fs-rail-line{flex:1;height:2px;border-radius:1px;background:rgba(255,255,255,.22);transition:background .35s}" +
3962      ".caf-bk-fs-rail-line.done{background:#fff}" +
3963      ".caf-bk-fs-rail-labels{display:flex;justify-content:space-between;gap:4px;font-size:9.5px;color:rgba(255,255,255,.8);font-weight:600;letter-spacing:.2px}" +
3964      ".caf-bk-fs-rail-lbl{text-align:center;flex:1;min-width:0;text-transform:uppercase}" +
3965      ".caf-bk-fs-rail-lbl.active{color:#fff}" +
3966      "@media(max-width:480px){.caf-bk-fs-hlogo{width:34px;height:34px;border-radius:10px}.caf-bk-fs-hlogo svg{width:17px;height:17px}.caf-bk-fs-htitle{font-size:15px}.caf-bk-fs-hsub{font-size:10.5px}.caf-bk-fs-rail-dot{width:22px;height:22px;font-size:10px}.caf-bk-fs-rail-labels{font-size:8.5px}}" +
3967
3968      /* continuous progress bar (Apple-style) under rail */
3969      ".caf-bk-fs-progress-wrap{position:relative;z-index:1;height:4px;background:rgba(255,255,255,.2);border-radius:99px;margin-top:10px;overflow:hidden}" +
3970      ".caf-bk-fs-progress-bar{height:100%;border-radius:99px;background:linear-gradient(90deg,#fff,#fff,rgba(255,255,255,.85));box-shadow:0 0 10px rgba(255,255,255,.6);transition:width .6s cubic-bezier(.34,1.56,.64,1);position:relative}" +
3971      ".caf-bk-fs-progress-bar::after{content:'';position:absolute;right:0;top:50%;width:8px;height:8px;border-radius:50%;background:#fff;box-shadow:0 0 12px rgba(255,255,255,.9);transform:translate(50%,-50%)}" +
3972
3973      /* calendar month picker (slot step) */
3974      ".caf-bk-fs-monthcal{background:#fff;border:1px solid #e2e8f0;border-radius:18px;padding:16px;box-shadow:0 2px 14px rgba(15,23,42,.05);max-width:480px;animation:caf-fade-up .4s ease-out}" +
3975      ".caf-bk-fs-monthcal-hdr{display:flex;align-items:center;justify-content:space-between;margin-bottom:12px}" +
3976      ".caf-bk-fs-monthcal-title{font-size:15px;font-weight:800;color:#0f172a;text-transform:capitalize;letter-spacing:-.2px}" +
3977      ".caf-bk-fs-monthcal-nav{display:flex;gap:6px}" +
3978      ".caf-bk-fs-monthcal-nav button{width:30px;height:30px;border-radius:8px;border:1px solid #e2e8f0;background:#fff;color:#475569;cursor:pointer;display:flex;align-items:center;justify-content:center;transition:all .2s;font-family:inherit}" +
3979      ".caf-bk-fs-monthcal-nav button:hover:not(:disabled){border-color:" + c + ";color:" + c + ";background:" + c + "08}" +
3980      ".caf-bk-fs-monthcal-nav button:disabled{opacity:.4;cursor:not-allowed}" +
3981      ".caf-bk-fs-monthcal-nav button svg{width:14px;
3981height:14px}" +
3982      ".caf-bk-fs-monthcal-week{display:grid;grid-template-columns:repeat(7,1fr);gap:4px;margin-bottom:6px}" +
3983      ".caf-bk-fs-monthcal-wd{font-size:10px;font-weight:700;color:#5b6573;text-align:center;text-transform:uppercase;letter-spacing:.5px;padding:4px 0}" +
3984      ".caf-bk-fs-monthcal-grid{display:grid;grid-template-columns:repeat(7,1fr);gap:4px}" +
3985      ".caf-bk-fs-monthcal-day{position:relative;aspect-ratio:1;border:1px solid transparent;background:transparent;border-radius:10px;font-size:12.5px;font-weight:600;color:#334155;cursor:pointer;font-family:inherit;transition:all .2s;-webkit-tap-highlight-color:transparent;display:flex;align-items:center;justify-content:center}" +
3986      ".caf-bk-fs-monthcal-day.empty{cursor:default}" +
3987      ".caf-bk-fs-monthcal-day.past{color:#5b6573;cursor:not-allowed}" +
3988      ".caf-bk-fs-monthcal-day.available{color:" + c + ";background:" + c + "0c;border-color:" + c + "25;font-weight:700}" +
3989      ".caf-bk-fs-monthcal-day.available:hover{background:" + c + ";color:#fff;transform:scale(1.08);box-shadow:0 6px 16px " + c + "40}" +
3990      ".caf-bk-fs-monthcal-day.selected{background:" + c + ";color:#fff;border-color:" + c + ";box-shadow:0 6px 16px " + c + "50}" +
3991      ".caf-bk-fs-monthcal-day.today{font-weight:800}" +
3992      ".caf-bk-fs-monthcal-day.today::after{content:'';position:absolute;bottom:3px;left:50%;transform:translateX(-50%);width:3px;height:3px;border-radius:50%;background:currentColor}" +
3993      ".caf-bk-fs-monthcal-day.available .dot{position:absolute;top:3px;right:3px;width:5px;height:5px;border-radius:50%;background:#10b981}" +
3994      ".caf-bk-fs-monthcal-legend{display:flex;gap:12px;align-items:center;margin-top:12px;padding-top:10px;border-top:1px solid #eef2f6;font-size:10.5px;color:#5b6573}" +
3995      ".caf-bk-fs-monthcal-legend .lg{display:inline-flex;align-items:center;gap:5px}" +
3996      ".caf-bk-fs-monthcal-legend .lg .sq{width:12px;height:12px;border-radius:4px;border:1px solid " + c + "25;background:" + c + "0c}" +
3997      ".caf-bk-fs-monthcal-legend .lg .sq.sel{background:" + c + ";border-color:" + c + "}" +
3998
3999      /* time slots panel (beside or below calendar) */
4000      ".caf-bk-fs-slotpanel{background:#fff;border:1px solid #e2e8f0;border-radius:18px;padding:16px;box-shadow:0 2px 14px rgba(15,23,42,.05);animation:caf-fade-up .4s ease-out}" +
4001      ".caf-bk-fs-slotpanel-hdr{font-size:13px;font-weight:700;color:#0f172a;margin-bottom:10px;display:flex;align-items:center;gap:6px}" +
4002      ".caf-bk-fs-slotpanel-hdr svg{width:14px;height:14px;color:" + c + "}" +
4003      ".caf-bk-fs-slotpanel-empty{padding:20px;text-align:center;color:#5b6573;font-size:12.5px}" +
4004      ".caf-bk-fs-slotlayout{display:grid;grid-template-columns:1fr;gap:12px}" +
4005      "@media(min-width:720px){.caf-bk-fs-slotlayout{grid-template-columns:minmax(300px,360px) 1fr;gap:16px}}" +
4006      /* Mobile (≤640): tap target ≥44px, calendario più ariato, chip slot più
4007       * grandi e leggibili, header del giorno selezionato come banner colorato
4008       * sticky così l'utente sa sempre quale data sta guardando. */
4009      "@media(max-width:640px){" +
4010        ".caf-bk-fs-monthcal{padding:14px;border-radius:18px;box-shadow:0 4px 18px rgba(15,23,42,.06)}" +
4011        ".caf-bk-fs-monthcal-title{font-size:15px;font-weight:800}" +
4012        ".caf-bk-fs-monthcal-hdr{margin-bottom:14px}" +
4013        ".caf-bk-fs-monthcal-week{gap:2px;margin-bottom:4px}" +
4014        ".caf-bk-fs-monthcal-grid{gap:4px}" +
4015        ".caf-bk-fs-monthcal-day{font-size:14px;min-height:44px;border-radius:11px;font-weight:700}" +
4016        ".caf-bk-fs-monthcal-day.available{background:linear-gradient(135deg," + c + "0e," + c + "05);box-shadow:inset 0 0 0 1px " + c + "30}" +
4017        ".caf-bk-fs-monthcal-day.available:active{transform:scale(.94)}" +
4018        ".caf-bk-fs-monthcal-day.available .dot{top:5px;right:5px;width:6px;height:6px;box-shadow:0 0 0 2px #fff}" +
4019        ".caf-bk-fs-monthcal-day.selected{background:linear-gradient(135deg," + c + "," + c + "dd);color:#fff;box-shadow:0 8px 22px " + c + "55,inset 0 0 0 1px " + c + ";transform:scale(1.05)}" +
4020        ".caf-bk-fs-monthcal-day.today::after{width:4px;height:4px;bottom:5px}" +
4021        ".caf-bk-fs-monthcal-nav button{width:38px;height:38px;border-radius:10px}" +
4022        ".caf-bk-fs-monthcal-nav button svg{width:16px;height:16px}" +
4023        ".caf-bk-fs-monthcal-legend{display:none}" + /* superflua su mobile */
4024
4025        /* SLOT PANEL — hero header colorato che fa da "banner del giorno" */
4026        ".caf-bk-fs-slotpanel{padding:0;border-radius:18px;overflow:hidden;border:1px solid #e2e8f0;box-shadow:0 4px 18px rgba(15,23,42,.06)}" +
4027        ".caf-bk-fs-slotpanel-hdr{position:sticky;top:0;z-index:2;padding:14px 16px;margin:0;background:linear-gradient(135deg," + c + "f0," + c + "d0);color:#fff;font-size:15px;font-weight:800;border-bottom:none;letter-spacing:-.2px;display:flex;align-items:center;gap:10px;text-transform:capitalize}" +
4028        ".caf-bk-fs-slotpanel-hdr svg{width:18px;height:18px;color:#fff;opacity:.95;flex-shrink:0}" +
4029        ".caf-bk-fs-slotpanel-hdr > span:first-of-type{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}" +
4030        ".caf-bk-fs-slotpanel > .caf-bk-fs-slotgroup{padding:0 16px}" +
4031        ".caf-bk-fs-slotpanel > .caf-bk-fs-slotgroup:first-of-type{padding-top:14px}" +
4032        ".caf-bk-fs-slotpanel > .caf-bk-fs-slotgroup:last-child{padding-bottom:16px}" +
4033        ".caf-bk-fs-slotpanel-empty{padding:30px 20px}" +
4034
4035        /* SLOTGROUP HEADER — pill compatta invece di solo testo grigio */
4036        ".caf-bk-fs-slotgroup-hdr{display:inline-flex;align-items:center;gap:5px;padding:5px 10px;background:#f1f5f9;border-radius:99px;font-size:10.5px;font-weight:800;color:#475569;margin-bottom:10px}" +
4037
4038        /* CHIP ORARI — più grandi, più premibili, più tridimensionali */
4039        ".caf-bk-fs-cal-slots{grid-template-columns:repeat(auto-fill,minmax(92px,1fr));gap:9px}" +
4040        ".caf-bk-fs-cal-chip{padding:14px 6px;font-size:14.5px;min-height:50px;border-radius:13px;background:#fff;box-shadow:0 1px 2px rgba(15,23,42,.04),0 0 0 1px #e2e8f0;border:none;font-weight:700}" +
4041        ".caf-bk-fs-cal-chip:active{transform:scale(.94);background:" + c + "08;box-shadow:0 1px 2px rgba(15,23,42,.04),0 0 0 1.5px " + c + "}" +
4042        ".caf-bk-fs-cal-chip .d{width:7px;height:7px}" +
4043      "}" +
4044      /* Phone piccolo (≤380): griglia 3-col fissa per evitare chip troppo strette. */
4045      "@media(max-width:380px){" +
4046        ".caf-bk-fs-monthcal{padding:12px}" +
4047        ".caf-bk-fs-monthcal-day{font-size:13.5px;min-height:42px}" +
4048        ".caf-bk-fs-cal-slots{grid-template-columns:repeat(3,1fr);gap:8px}" +
4049        ".caf-bk-fs-cal-chip{padding:13px 4px;font-size:14px;min-height:48px}" +
4050      "}" +
4051      ".caf-bk-fs-slotgroup{margin-bottom:12px}" +
4052      ".caf-bk-fs-slotgroup:last-child{margin-bottom:0}" +
4053      ".caf-bk-fs-slotgroup-hdr{font-size:10.5px;font-weight:700;text-transform:uppercase;letter-spacing:.5px;color:#5b6573;margin-bottom:6px;display:flex;align-items:center;gap:4px}" +
4054
4055      /* "trova sede più vicina" button + distance badge */
4056      ".caf-bk-nearby{display:flex;flex-direction:column;gap:10px;margin-bottom:14px;animation:caf-fade-up .3s ease-out}" +
4057      ".caf-bk-nearby-btn{display:inline-flex;align-items:center;gap:8px;border:1.5px solid " + c + ";background:linear-gradient(135deg," + c + "10," + c + "04);color:" + c + ";border-radius:14px;padding:12px 16px;font-size:13px;font-weight:700;cursor:pointer;font-family:inherit;transition:all .2s;-webkit-tap-highlight-color:transparent;align-self:flex-start;position:relative;overflow:hidden;max-width:100%}" +
4058      ".caf-bk-nearby-btn:hover{background:" + c + ";color:#fff;transform:translateY(-1px);box-shadow:0 6px 18px " + c + "44}" +
4059      ".caf-bk-nearby-btn:active{transform:scale(.98)}" +
4060      ".caf-bk-nearby-btn:disabled{opacity:.6;cursor:not-allowed;transform:none}" +
4061      ".caf-bk-nearby-btn svg{width:15px;height:15px;flex-shrink:0}" +
4062      ".caf-bk-nearby-btn .spin{width:14px;
4062height:14px;border:2px solid currentColor;border-right-color:transparent;border-radius:50%;animation:caf-spin .7s linear infinite;flex-shrink:0}" +
4063      ".caf-bk-nearby-status{font-size:11.5px;color:#5b6573;display:flex;align-items:center;gap:6px;line-height:1.4;animation:caf-fade-up .25s ease-out}" +
4064      ".caf-bk-nearby-status.ok{color:#047857}" +
4065      ".caf-bk-nearby-status.err{color:#b91c1c}" +
4066      ".caf-bk-nearby-status svg{width:13px;height:13px;flex-shrink:0}" +
4067      /* INLINE prominent "use my location" button (filled gradient + pulse) */
4068      "@keyframes caf-locate-pulse{0%,100%{box-shadow:0 4px 16px " + c + "44}50%{box-shadow:0 6px 22px " + c + "66,0 0 0 4px " + c + "22}}" +
4069      ".caf-bk-locate{position:relative;display:flex;align-items:center;gap:10px;width:100%;padding:12px 14px;margin:2px 0 10px;border:none;background:linear-gradient(135deg," + c + "," + cGradEnd + ");color:#fff;border-radius:14px;font-size:13px;font-weight:700;cursor:pointer;font-family:inherit;box-shadow:0 4px 16px " + c + "44;transition:filter .2s,transform .2s;animation:caf-locate-pulse 2.8s ease-in-out infinite,caf-fade-up .35s ease-out;overflow:hidden;-webkit-tap-highlight-color:transparent;text-align:left}" +
4070      ".caf-bk-locate::after{content:'';position:absolute;top:0;bottom:0;width:40%;background:linear-gradient(100deg,transparent 0%,rgba(255,255,255,.32) 50%,transparent 100%);transform:translateX(-120%);animation:caf-cta-shimmer 4s ease-in-out infinite;animation-delay:1s;pointer-events:none}" +
4071      ".caf-bk-locate:hover{filter:brightness(1.08);transform:translateY(-1px)}" +
4072      ".caf-bk-locate:active{transform:scale(.99)}" +
4073      ".caf-bk-locate:disabled{opacity:.85;cursor:not-allowed;animation:none}" +
4074      ".caf-bk-locate-ic{width:30px;height:30px;border-radius:9px;background:rgba(255,255,255,.22);display:flex;align-items:center;justify-content:center;flex-shrink:0;position:relative;z-index:1}" +
4075      ".caf-bk-locate-ic svg{width:15px;height:15px}" +
4076      ".caf-bk-locate-ic .spin{width:14px;height:14px;border:2px solid rgba(255,255,255,.35);border-top-color:#fff;border-radius:50%;animation:caf-spin .7s linear infinite}" +
4077      ".caf-bk-locate-txt{flex:1;min-width:0;position:relative;z-index:1}" +
4078      ".caf-bk-locate-t{font-size:12.5px;font-weight:700;line-height:1.2;letter-spacing:-.1px}" +
4079      ".caf-bk-locate-s{font-size:10.5px;opacity:.9;margin-top:2px;line-height:1.3;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}" +
4080      ".caf-bk-locate-arrow{position:relative;z-index:1;flex-shrink:0;font-size:16px;line-height:1;opacity:.9}" +
4081      ".caf-bk-locate-status{font-size:11.5px;margin:0 0 10px;line-height:1.4;display:flex;align-items:flex-start;gap:6px;padding:8px 12px;border-radius:10px;animation:caf-fade-up .25s ease-out}" +
4082      ".caf-bk-locate-status.ok{background:#d1fae5;color:#047857}" +
4083      ".caf-bk-locate-status.err{background:#fee2e2;color:#b91c1c}" +
4084      ".caf-bk-locate-status.loading{background:" + c + "12;color:" + c + "}" +
4085      ".caf-bk-locate-status svg{width:13px;height:13px;flex-shrink:0;margin-top:1px}" +
4086      ".caf-bk-fs-tile-dist{position:absolute;top:10px;right:12px;display:inline-flex;align-items:center;gap:4px;font-size:10px;font-weight:800;color:" + c + ";background:" + c + "15;padding:3px 8px;border-radius:99px;border:1px solid " + c + "30;z-index:2;letter-spacing:.2px}" +
4087      ".caf-bk-fs-tile-dist svg{width:9px;height:9px}" +
4088      ".caf-bk-fs-tile.nearest{border-color:" + c + ";box-shadow:0 8px 24px " + c + "28}" +
4089      ".caf-bk-fs-tile.nearest::before{width:4px;background:linear-gradient(180deg," + c + "," + c + "aa)}" +
4090      ".caf-bk-fs-tile-nearest-ribbon{position:absolute;top:10px;left:-1px;background:linear-gradient(135deg," + c + "," + cGradEnd + ");color:var(--caf-on-c);font-size:9px;font-weight:800;padding:3px 10px 3px 8px;border-radius:0 8px 8px 0;letter-spacing:.5px;text-transform:uppercase;z-index:2;box-shadow:0 2px 8px " + c + "44;transform-origin:left center}" +
4091
4092      /* nearest reveal: shake + golden glow + ribbon pop */
4093      "@keyframes caf-nearest-shake{" +
4094        "0%,100%{transform:translate3d(0,0,0) rotate(0)}" +
4095        "12%{transform:translate3d(0,-8px,0) rotate(-1deg) scale(1.04)}" +
4096        "28%{transform:translate3d(0,0,0) rotate(1deg) scale(1.02)}" +
4097        "44%{transform:translate3d(0,-4px,0) rotate(-.5deg) scale(1.03)}" +
4098        "62%{transform:translate3d(0,0,0) rotate(.3deg) scale(1.02)}" +
4099        "80%{transform:translate3d(0,-1px,0) rotate(-.1deg) scale(1.01)}" +
4100      "}" +
4101      "@keyframes caf-nearest-glow{" +
4102        "0%,100%{box-shadow:0 8px 24px " + c + "30,0 0 0 0 " + c + "00}" +
4103        "50%{box-shadow:0 20px 60px " + c + "66,0 0 0 6px " + c + "22}" +
4104      "}" +
4105      "@keyframes caf-ribbon-pop{" +
4106        "0%{opacity:0;transform:scale(0) rotate(-25deg)}" +
4107        "55%{opacity:1;transform:scale(1.2) rotate(4deg)}" +
4108        "75%{transform:scale(.95) rotate(-2deg)}" +
4109        "100%{opacity:1;transform:scale(1) rotate(0)}" +
4110      "}" +
4111      "@keyframes caf-dist-pop{" +
4112        "0%{opacity:0;transform:scale(0) rotate(-90deg)}" +
4113        "60%{opacity:1;transform:scale(1.25) rotate(8deg)}" +
4114        "100%{opacity:1;transform:scale(1) rotate(0)}" +
4115      "}" +
4116      ".caf-bk-fs-tile.nearest-reveal{animation:caf-nearest-shake 1s cubic-bezier(.36,.07,.19,.97) both,caf-nearest-glow 2.4s ease-in-out 1s infinite;z-index:5}" +
4117      ".caf-bk-fs-tile.nearest-reveal .caf-bk-fs-tile-nearest-ribbon{animation:caf-ribbon-pop .7s cubic-bezier(.34,1.56,.64,1) .35s both}" +
4118      ".caf-bk-fs-tile-dist{animation:caf-dist-pop .5s cubic-bezier(.34,1.56,.64,1) .1s both}" +
4119
4120      /* sparkle particles (celebration) */
4121      "@keyframes caf-sparkle{" +
4122        "0%{opacity:0;transform:translate(0,0) scale(0) rotate(0)}" +
4123        "20%{opacity:1;transform:translate(calc(var(--sx,0)*.3),calc(var(--sy,0)*.3)) scale(1) rotate(90deg)}" +
4124        "70%{opacity:.9;transform:translate(calc(var(--sx,0)*.85),calc(var(--sy,0)*.85)) scale(.8) rotate(270deg)}" +
4125        "100%{opacity:0;transform:translate(var(--sx,0),var(--sy,0)) scale(.1) rotate(360deg)}" +
4126      "}" +
4127      ".caf-sparkle{position:absolute;left:50%;top:50%;margin:-7px 0 0 -7px;width:14px;height:14px;pointer-events:none;z-index:10;animation:caf-sparkle 1.2s cubic-bezier(.34,1.56,.64,1) both;will-change:transform,opacity}" +
4128      ".caf-sparkle svg{width:100%;height:100%;filter:drop-shadow(0 0 6px currentColor)}" +
4129
4130      /* ── smooth step stage transitions (fullscreen) with blur + scale + translate ── */
4131      "@keyframes caf-stage-fwd{" +
4132        "0%{opacity:0;transform:translate3d(30px,6px,0) scale(.96);filter:blur(4px)}" +
4133        "35%{opacity:.85;filter:blur(0)}" +
4134        "100%{opacity:1;transform:translate3d(0,0,0) scale(1);filter:blur(0)}" +
4135      "}" +
4136      "@keyframes caf-stage-back{" +
4137        "0%{opacity:0;transform:translate3d(-30px,6px,0) scale(.96);filter:blur(4px)}" +
4138        "35%{opacity:.85;filter:blur(0)}" +
4139        "100%{opacity:1;transform:translate3d(0,0,0) scale(1);filter:blur(0)}" +
4140      "}" +
4141      "@keyframes caf-stage-fade{0%{opacity:0;transform:scale(.985);filter:blur(3px)}40%{filter:blur(0)}100%{opacity:1;transform:scale(1);filter:blur(0)}}" +
4142      ".caf-bk-fs-stage{animation-duration:.55s;animation-timing-function:cubic-bezier(.22,.61,.36,1);animation-fill-mode:both;will-change:transform,opacity,filter}" +
4143      ".caf-bk-fs-stage.fwd{animation-name:caf-stage-fwd}" +
4144      ".caf-bk-fs-stage.back{animation-name:caf-stage-back}" +
4145      ".caf-bk-fs-stage.fade{animation-name:caf-stage-fade;animation-duration:.38s}" +
4146
4147      /* ── skeleton loaders (replace spinner with content shape) ── */
4148      "@keyframes caf-skel-shimmer{0%{background-position:-150% 0}100%{background-position:150% 0}}" +
4149      ".caf-bk-fs-skel{background:linear-gradient(90deg,#edf2f7 0%,#e2e8f0 50%,#edf2f7 100%);background-size:200% 100%;animation:caf-skel-shimmer 1.6s ease-in-out infinite;border-radius:16px;position:relative;overflow:hidden}" +
4150      ".caf-bk-fs-skel-tile{height:104px;animation-delay:var(--d,0ms)}" +
4151      ".caf-bk-fs-skel-row{height:14px;border-radius:7px;margin:0 0 8px}" +
4152      ".caf-bk-fs-skel-row.sm{height:10px;width:60%}" +
4153      ".caf-bk-fs-skel-row.md{height:14px;width:80%}" +
4154      ".caf-bk-fs-skel-row.lg{height:18px;width:90%}" +
4155      ".caf-bk-fs-skel-ping{position:absolute;bottom:0;left:0;height:2px;width:30%;background:linear-gradient(90deg,transparent," + c + ",transparent);animation:caf-shimmer-bar 1.2s ease-in-out infinite}" +
4156      ".caf-bk-fs-skel-caption{text-align:center;font-size:13px;color:#5b6573;margin-top:16px;display:flex;align-items:center;justify-content:center;gap:8px;animation:caf-pulse-soft 1.6s ease-in-out infinite}" +
4157      ".caf-bk-fs-skel-caption .sp{width:14px;
4157height:14px;border:2px solid " + c + "33;border-top-color:" + c + ";border-radius:50%;animation:caf-spin .7s linear infinite}" +
4158
4159      /* ── tile spotlight: when user picks one, dim the others ── */
4160      "@keyframes caf-tile-dim{to{opacity:.12;transform:scale(.9);filter:blur(2px)}}" +
4161      "@keyframes caf-tile-focus{" +
4162        "0%{transform:scale(1)}" +
4163        "35%{transform:scale(1.06);box-shadow:0 20px 50px " + c + "50}" +
4164        "100%{transform:scale(1.03);box-shadow:0 18px 45px " + c + "44}" +
4165      "}" +
4166      ".caf-bk-fs-tile-dim{animation:caf-tile-dim .35s cubic-bezier(.22,.61,.36,1) forwards;pointer-events:none}" +
4167      ".caf-bk-fs-tile-focus{animation:caf-tile-focus .4s cubic-bezier(.34,1.56,.64,1) forwards;z-index:5;pointer-events:none;border-color:" + c + "!important}" +
4168
4169      /* ── sidebar item completion (pending → filled with checkmark draw) ── */
4170      "@keyframes caf-side-complete{0%{transform:scale(1)}35%{transform:scale(1.2)}70%{transform:scale(.95)}100%{transform:scale(1)}}" +
4171      "@keyframes caf-side-check-draw{0%{stroke-dashoffset:16}100%{stroke-dashoffset:0}}" +
4172      ".caf-bk-fs-side-item.just-completed .caf-bk-fs-side-ic{animation:caf-side-complete .55s cubic-bezier(.34,1.56,.64,1)}" +
4173      ".caf-bk-fs-side-item.just-completed .caf-bk-fs-side-val{animation:caf-fade-up .5s cubic-bezier(.22,.61,.36,1)}" +
4174      ".caf-bk-fs-side-check{position:absolute;top:-4px;right:-4px;width:16px;height:16px;border-radius:50%;background:#10b981;display:flex;align-items:center;justify-content:center;box-shadow:0 2px 6px rgba(16,185,129,.45);animation:caf-pop-in .4s cubic-bezier(.34,1.56,.64,1)}" +
4175      ".caf-bk-fs-side-check svg{width:10px;height:10px;stroke-dasharray:16;animation:caf-side-check-draw .45s ease-out .15s both}" +
4176      ".caf-bk-fs-side-ic{position:relative}" +
4177
4178      /* ── rail active dot pulse (continuous, subtle) ── */
4179      "@keyframes caf-rail-pulse{0%,100%{box-shadow:0 0 0 5px rgba(255,255,255,.25)}50%{box-shadow:0 0 0 8px rgba(255,255,255,.12)}}" +
4180      ".caf-bk-fs-rail-dot.active{animation:caf-rail-pulse 2.2s ease-in-out infinite}" +
4181
4182      /* ── form field entry cascade ── */
4183      "@keyframes caf-field-in{0%{opacity:0;transform:translateY(8px) scale(.98)}100%{opacity:1;transform:translateY(0) scale(1)}}" +
4184      ".caf-bk-fs-form .caf-bk-inp-wrap{animation:caf-field-in .45s cubic-bezier(.22,.61,.36,1) both}" +
4185
4186      /* ── submit → loading → done morph ── */
4187      "@keyframes caf-submit-pulse{0%,100%{transform:scale(1);box-shadow:0 6px 20px " + c + "44}50%{transform:scale(1.03);box-shadow:0 10px 32px " + c + "66}}" +
4188      ".caf-bk-fs-btn-pri.submitting{animation:caf-submit-pulse 1s ease-in-out infinite;pointer-events:none}" +
4189      "@keyframes caf-done-grow{0%{opacity:0;transform:scale(.3) rotate(-25deg)}60%{opacity:1;transform:scale(1.15) rotate(8deg)}100%{opacity:1;transform:scale(1) rotate(0)}}" +
4190      ".caf-bk-fs-done-ic{animation:caf-done-grow .75s cubic-bezier(.34,1.56,.64,1) both!important}" +
4191
4192      /* smoother transitions on existing elements */
4193      ".caf-bk-fs-tile,.caf-bk-fs-tile-dim,.caf-bk-fs-tile-focus{will-change:transform,opacity}" +
4194
4195      /* better calendar day pop + chip feedback */
4196      "@keyframes caf-day-pop{0%{transform:scale(1)}40%{transform:scale(1.25)}70%{transform:scale(.94)}100%{transform:scale(1)}}" +
4197      ".caf-bk-fs-monthcal-day.selected{animation:caf-day-pop .4s cubic-bezier(.34,1.56,.64,1)}" +
4198      "@keyframes caf-chip-select{0%{transform:scale(1)}40%{transform:scale(1.15) rotate(-3deg)}70%{transform:scale(.95) rotate(1deg)}100%{transform:scale(1) rotate(0)}}" +
4199      ".caf-bk-fs-cal-chip.active{animation:caf-chip-select .4s cubic-bezier(.34,1.56,.64,1)}" +
4200
4201      /* polished tile hover/press */
4202      ".caf-bk-fs-tile{transition:transform .35s cubic-bezier(.34,1.56,.64,1),box-shadow .35s,border-color .25s,filter .25s}" +
4203      ".caf-bk-fs-tile:hover{filter:saturate(1.1)}" +
4204      ".caf-bk-fs-tile.picking{animation:caf-chip-select .35s cubic-bezier(.34,1.56,.64,1);pointer-events:none}" +
4205      ".caf-bk-fs-btn{transition:transform .18s cubic-bezier(.34,1.56,.64,1),box-shadow .25s,background .25s,filter .2s,color .2s,border-color .2s}" +
4206
4207      /* sede map iframe */
4208      ".caf-bk-fs-map{margin-top:12px;border-radius:14px;overflow:hidden;border:1px solid #e2e8f0;box-shadow:0 2px 10px rgba(15,23,42,.06);height:220px;background:#f1f5f9;position:relative}" +
4209      ".caf-bk-fs-map iframe{width:100%;height:100%;border:0;display:block}" +
4210      ".caf-bk-fs-map-fallback{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;flex-direction:column;gap:6px;color:#5b6573;font-size:12px;padding:20px;text-align:center}" +
4211      ".caf-bk-fs-map-fallback svg{width:30px;height:30px;opacity:.5}" +
4212
4213      /* mobile bottom sheet summary (fs) — lives between body and footer */
4214      ".caf-bk-fs-mbsum{display:none;background:rgba(255,255,255,.96);backdrop-filter:blur(12px);-webkit-backdrop-filter:blur(12px);border-top:1px solid #e2e8f0;padding:10px 14px;flex-shrink:0;box-shadow:0 -6px 20px rgba(15,23,42,.06);animation:caf-cta-slide .3s ease-out}" +
4215      "@media(max-width:820px){.caf-bk-fs-mbsum{display:block}}" +
4216      ".caf-bk-fs-mbsum-row{display:flex;align-items:center;gap:10px}" +
4217      ".caf-bk-fs-mbsum-handle{width:36px;height:4px;background:#cbd5e1;border-radius:2px;margin:0 auto 8px;display:block}" +
4218      ".caf-bk-fs-mbsum-count{flex-shrink:0;width:28px;height:28px;border-radius:8px;background:" + c + "15;color:" + c + ";font-size:12px;font-weight:800;display:flex;align-items:center;justify-content:center}" +
4219      ".caf-bk-fs-mbsum-txt{flex:1;min-width:0;font-size:11.5px;color:#475569;line-height:1.35;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-weight:600}" +
4220      ".caf-bk-fs-mbsum-txt b{color:#0f172a}" +
4221      ".caf-bk-fs-mbsum-toggle{flex-shrink:0;border:none;background:transparent;color:" + c + ";font-size:11px;font-weight:700;cursor:pointer;font-family:inherit;padding:4px 8px;border-radius:8px;transition:background .2s;display:inline-flex;align-items:center;gap:3px}" +
4222      ".caf-bk-fs-mbsum-toggle:hover{background:" + c + "12}" +
4223      ".caf-bk-fs-mbsum-exp{display:none;margin-top:10px;padding-top:10px;border-top:1px dashed #e2e8f0}" +
4224      ".caf-bk-fs-mbsum.expanded .caf-bk-fs-mbsum-exp{display:block;animation:caf-fade-up .25s ease-out}" +
4225      ".caf-bk-fs-mbsum-item{display:flex;align-items:flex-start;gap:8px;padding:5px 0;font-size:11.5px}" +
4226      ".caf-bk-fs-mbsum-item .lbl{color:#5b6573;min-width:70px;font-size:10.5px;font-weight:700;text-transform:uppercase;letter-spacing:.3px}" +
4227      ".caf-bk-fs-mbsum-item .val{color:#0f172a;font-weight:600;flex:1}" +
4228
4229      /* body layout: sidebar (desktop) + main */
4230      ".caf-bk-fs-body{flex:1;display:grid;grid-template-columns:1fr;overflow:hidden;min-height:0}" +
4231      "@media(min-width:820px){.caf-bk-fs-body{grid-template-columns:minmax(260px,300px) 1fr}}" +
4232
4233      /* sidebar summary */
4234      ".caf-bk-fs-side{display:none;overflow-y:auto;padding:20px 18px;background:rgba(255,255,255,.55);backdrop-filter:blur(6px);border-right:1px solid #e2e8f0}" +
4235      "@media(min-width:820px){.caf-bk-fs-side{display:block}}" +
4236      ".caf-bk-fs-side-title{font-size:10px;text-transform:uppercase;letter-spacing:1.2px;font-weight:800;color:#5b6573;margin-bottom:12px;display:flex;align-items:center;gap:6px}" +
4237      ".caf-bk-fs-side-title svg{width:12px;height:12px}" +
4238      ".caf-bk-fs-side-card{background:#fff;border:1px solid #e2e8f0;border-radius:14px;padding:14px;box-shadow:0 1px 3px rgba(0,0,0,.04);margin-bottom:12px}" +
4239      ".caf-bk-fs-side-item{display:flex;align-items:flex-start;gap:10px;padding:9px 0;border-bottom:1px dashed #eef2f6;transition:opacity .3s}" +
4240      ".caf-bk-fs-side-item:last-child{border-bottom:none}" +
4241      ".caf-bk-fs-side-item.pending{opacity:.45}" +
4242      ".caf-bk-fs-side-ic{width:30px;height:30px;border-radius:9px;background:" + c + "15;color:" + c + ";display:flex;align-items:center;justify-content:center;flex-shrink:0}" +
4243      ".caf-bk-fs-side-ic.pending{background:#f1f5f9;color:#5b6573}" +
4244      ".caf-bk-fs-side-ic svg{width:14px;
4244height:14px}" +
4245      ".caf-bk-fs-side-txt{flex:1;min-width:0}" +
4246      ".caf-bk-fs-side-lbl{font-size:9.5px;color:#5b6573;font-weight:700;text-transform:uppercase;letter-spacing:.4px}" +
4247      ".caf-bk-fs-side-val{font-size:12.5px;color:#0f172a;font-weight:600;margin-top:2px;word-wrap:break-word;line-height:1.3}" +
4248      ".caf-bk-fs-side-val.muted{color:#5b6573;font-weight:500;font-style:italic}" +
4249      ".caf-bk-fs-side-help{font-size:11px;color:#5b6573;line-height:1.5;padding:10px 12px;background:" + c + "06;border:1px solid " + c + "18;border-radius:10px;display:flex;gap:8px;align-items:flex-start}" +
4250      ".caf-bk-fs-side-help svg{width:13px;height:13px;color:" + c + ";flex-shrink:0;margin-top:2px}" +
4251
4252      /* main step area */
4253      ".caf-bk-fs-main{overflow-y:auto;padding:22px 26px 28px;display:flex;flex-direction:column;gap:18px;-webkit-overflow-scrolling:touch}" +
4254      "@media(max-width:820px){.caf-bk-fs-main{padding:16px 14px 20px;gap:14px}}" +
4255      "@media(max-width:480px){.caf-bk-fs-main{padding:14px 12px 18px}}" +
4256      ".caf-bk-fs-step-head{animation:caf-fade-up .35s ease-out}" +
4257      ".caf-bk-fs-step-num{display:inline-flex;align-items:center;gap:5px;font-size:10px;font-weight:800;letter-spacing:1.2px;text-transform:uppercase;color:" + c + ";background:" + c + "14;padding:5px 12px;border-radius:99px;margin-bottom:10px;border:1px solid " + c + "25}" +
4258      ".caf-bk-fs-step-h1{font-size:22px;font-weight:800;color:#0f172a;letter-spacing:-.4px;line-height:1.25}" +
4259      "@media(max-width:820px){.caf-bk-fs-step-h1{font-size:19px}}" +
4260      "@media(max-width:480px){.caf-bk-fs-step-h1{font-size:17px}}" +
4261      ".caf-bk-fs-step-h2{font-size:13.5px;color:#5b6573;margin-top:5px;line-height:1.55;max-width:640px}" +
4262      "@media(max-width:480px){.caf-bk-fs-step-h2{font-size:12.5px}}" +
4263
4264      /* large tile grid */
4265      ".caf-bk-fs-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:12px;animation:caf-fade-up .4s ease-out}" +
4266      "@media(max-width:520px){.caf-bk-fs-grid{grid-template-columns:1fr}}" +
4267      ".caf-bk-fs-tile{position:relative;background:#fff;border:1.5px solid #e2e8f0;border-radius:16px;padding:16px 40px 16px 18px;text-align:left;cursor:pointer;font-family:inherit;transition:all .25s cubic-bezier(.34,1.56,.64,1);overflow:hidden;-webkit-tap-highlight-color:transparent;min-height:96px;touch-action:manipulation}" +
4268      "@media(max-width:480px){.caf-bk-fs-tile{padding:14px 36px 14px 14px;min-height:80px}.caf-bk-fs-tile-ic{width:32px;height:32px;border-radius:9px;margin-bottom:8px}.caf-bk-fs-tile-ic svg{width:16px;height:16px}.caf-bk-fs-tile-name{font-size:13.5px}}" +
4269      ".caf-bk-fs-tile::before{content:'';position:absolute;left:0;top:0;bottom:0;width:0;background:linear-gradient(180deg," + c + "," + c + "aa);transition:width .3s}" +
4270      ".caf-bk-fs-tile:hover{border-color:" + c + ";transform:translateY(-3px);box-shadow:0 18px 44px " + c + "25}" +
4271      ".caf-bk-fs-tile:hover::before{width:4px}" +
4272      ".caf-bk-fs-tile:active{transform:scale(.98)}" +
4273      ".caf-bk-fs-tile-ic{width:38px;height:38px;border-radius:11px;background:linear-gradient(135deg," + c + "18," + c + "08);color:" + c + ";display:flex;align-items:center;justify-content:center;margin-bottom:10px}" +
4274      ".caf-bk-fs-tile-ic svg{width:18px;height:18px}" +
4275      ".caf-bk-fs-tile-name{font-size:14px;font-weight:700;color:#0f172a;letter-spacing:-.1px;line-height:1.35}" +
4276      ".caf-bk-fs-tile-sub{font-size:11.5px;color:#5b6573;margin-top:5px;display:flex;align-items:center;gap:5px;line-height:1.4}" +
4277      ".caf-bk-fs-tile-sub svg{width:11px;height:11px;flex-shrink:0;opacity:.7}" +
4278      ".caf-bk-fs-tile-arrow{position:absolute;right:14px;top:50%;transform:translateY(-50%);color:#5b6573;transition:all .25s;display:flex}" +
4279      ".caf-bk-fs-tile-arrow svg{width:16px;height:16px}" +
4280      ".caf-bk-fs-tile:hover .caf-bk-fs-tile-arrow{color:" + c + ";transform:translateY(-50%) translateX(4px)}" +
4281
4282      /* search in fs */
4283      ".caf-bk-fs-search{position:relative;margin-bottom:4px}" +
4284      ".caf-bk-fs-search-ic{position:absolute;
4284left:14px;top:50%;transform:translateY(-50%);color:#5b6573;pointer-events:none;display:flex}" +
4285      ".caf-bk-fs-search-ic svg{width:16px;height:16px}" +
4286      ".caf-bk-fs-search input{width:100%;border:1.5px solid #818c9c;border-radius:14px;padding:12px 16px 12px 42px;font-size:14px;color:#0f172a;font-family:inherit;outline:none;background:#fff;transition:border-color .2s,box-shadow .2s}" +
4287      ".caf-bk-fs-search input:focus{border-color:" + c + ";box-shadow:0 0 0 4px " + c + "18}" +
4288
4289      /* slot calendar */
4290      ".caf-bk-fs-cal{display:grid;grid-template-columns:repeat(auto-fit,minmax(260px,1fr));gap:14px;animation:caf-fade-up .4s ease-out}" +
4291      ".caf-bk-fs-cal-day{background:#fff;border:1px solid #e2e8f0;border-radius:16px;padding:14px 14px 12px;box-shadow:0 1px 3px rgba(0,0,0,.04)}" +
4292      ".caf-bk-fs-cal-day-hdr{display:flex;align-items:center;gap:10px;padding-bottom:10px;margin-bottom:10px;border-bottom:1px solid #eef2f6}" +
4293      ".caf-bk-fs-cal-day-badge{width:38px;height:38px;border-radius:10px;background:linear-gradient(135deg," + c + "18," + c + "08);color:" + c + ";display:flex;flex-direction:column;align-items:center;justify-content:center;flex-shrink:0;font-weight:800}" +
4294      ".caf-bk-fs-cal-day-badge .d{font-size:14px;line-height:1}" +
4295      ".caf-bk-fs-cal-day-badge .m{font-size:8px;text-transform:uppercase;letter-spacing:.5px;margin-top:1px;opacity:.85}" +
4296      ".caf-bk-fs-cal-day-txt{flex:1;min-width:0}" +
4297      ".caf-bk-fs-cal-day-n{font-size:13px;font-weight:700;color:#0f172a;text-transform:capitalize}" +
4298      ".caf-bk-fs-cal-day-d{font-size:11px;color:#5b6573;margin-top:1px}" +
4299      ".caf-bk-fs-cal-slots{display:grid;grid-template-columns:repeat(auto-fill,minmax(70px,1fr));gap:6px}" +
4300      ".caf-bk-fs-cal-chip{position:relative;border:1.5px solid #e2e8f0;background:#f8fafc;border-radius:10px;padding:9px 4px;font-size:12.5px;font-weight:700;color:#334155;cursor:pointer;font-family:inherit;transition:all .2s;-webkit-tap-highlight-color:transparent;display:flex;align-items:center;justify-content:center;gap:3px}" +
4301      ".caf-bk-fs-cal-chip:hover{border-color:" + c + ";background:#fff;color:" + c + ";transform:scale(1.04);box-shadow:0 4px 12px " + c + "22}" +
4302      ".caf-bk-fs-cal-chip:active{transform:scale(.96)}" +
4303      ".caf-bk-fs-cal-chip .d{width:6px;height:6px;border-radius:50%;background:#cbd5e1;flex-shrink:0}" +
4304      ".caf-bk-fs-cal-chip .d.pm{background:#fb923c}" +
4305      ".caf-bk-fs-cal-chip .d.am{background:#3b82f6}" +
4306
4307      /* form (fs) */
4308      ".caf-bk-fs-form{display:grid;grid-template-columns:1fr 1fr;gap:14px;max-width:680px;animation:caf-fade-up .4s ease-out}" +
4309      "@media(max-width:640px){.caf-bk-fs-form{grid-template-columns:1fr}}" +
4310      ".caf-bk-fs-form .caf-bk-inp-wrap{width:100%}" +
4311      ".caf-bk-fs-form .caf-bk-inp-label{font-size:11px;margin-bottom:-2px}" +
4312      ".caf-bk-fs-form .caf-bk-inp{padding:13px 40px 13px 40px;font-size:14px;border-radius:13px;background:#fff}" +
4313      ".caf-bk-fs-form .caf-bk-inp-icon{left:14px}" +
4314      ".caf-bk-fs-form .caf-bk-inp-icon svg{width:16px;height:16px}" +
4315      ".caf-bk-fs-form .caf-bk-inp-check{right:14px}" +
4316      ".caf-bk-fs-form-full{grid-column:1/-1}" +
4317      ".caf-bk-fs-form-full .caf-bk-rr{padding:0}" +
4318
4319      /* summary panel (confirm step) */
4320      ".caf-bk-fs-summary{background:#fff;border:1px solid #e2e8f0;border-radius:18px;padding:20px;box-shadow:0 4px 20px rgba(15,23,42,.05);max-width:680px;animation:caf-fade-up .4s ease-out}" +
4321      ".caf-bk-fs-summary-hdr{font-size:11px;font-weight:800;letter-spacing:1.2px;text-transform:uppercase;color:#5b6573;padding-bottom:10px;border-bottom:1px solid #eef2f6;margin-bottom:12px;display:flex;align-items:center;gap:6px}" +
4322      ".caf-bk-fs-summary-hdr svg{width:13px;height:13px;color:" + c + "}" +
4323      ".caf-bk-fs-summary-row{display:flex;align-items:flex-start;gap:12px;padding:10px 0;border-bottom:1px dashed #eef2f6}" +
4324      ".caf-bk-fs-summary-row:last-child{border-bottom:none}" +
4325      ".caf-bk-fs-summary-ic{width:34px;height:34px;border-radius:10px;background:" + c + "12;
4325color:" + c + ";display:flex;align-items:center;justify-content:center;flex-shrink:0}" +
4326      ".caf-bk-fs-summary-ic svg{width:15px;height:15px}" +
4327      ".caf-bk-fs-summary-body{flex:1;min-width:0}" +
4328      ".caf-bk-fs-summary-lbl{font-size:10.5px;color:#5b6573;font-weight:700;text-transform:uppercase;letter-spacing:.4px}" +
4329      ".caf-bk-fs-summary-val{font-size:14px;color:#0f172a;font-weight:600;margin-top:2px;line-height:1.35}" +
4330      ".caf-bk-fs-summary-sub{font-size:11.5px;color:#5b6573;margin-top:2px}" +
4331
4332      /* footer actions */
4333      ".caf-bk-fs-foot{border-top:1px solid #e2e8f0;background:rgba(255,255,255,.92);backdrop-filter:blur(12px);-webkit-backdrop-filter:blur(12px);padding:12px 20px;display:flex;align-items:center;justify-content:space-between;gap:10px;flex-shrink:0}" +
4334      "@media(max-width:640px){.caf-bk-fs-foot{padding:10px 14px calc(env(safe-area-inset-bottom,0px) + 10px)}}" +
4335      "@media(max-width:480px){.caf-bk-fs-foot{padding:9px 12px calc(env(safe-area-inset-bottom,0px) + 9px);gap:6px}}" +
4336      ".caf-bk-fs-foot-l,.caf-bk-fs-foot-r{display:flex;gap:8px;align-items:center}" +
4337      ".caf-bk-fs-btn{display:inline-flex;align-items:center;gap:6px;border-radius:12px;padding:11px 18px;font-size:12.5px;font-weight:700;cursor:pointer;font-family:inherit;border:1.5px solid transparent;transition:all .2s;-webkit-tap-highlight-color:transparent;letter-spacing:.2px}" +
4338      "@media(max-width:480px){.caf-bk-fs-btn{padding:10px 12px;font-size:12px}}" +
4339      ".caf-bk-fs-btn-ghost{background:transparent;color:#5b6573;border-color:#e2e8f0}" +
4340      ".caf-bk-fs-btn-ghost:hover{color:#0f172a;background:#f1f5f9;border-color:#cbd5e1}" +
4341      ".caf-bk-fs-btn-danger{background:transparent;color:#5b6573;border-color:transparent}" +
4342      ".caf-bk-fs-btn-danger:hover{color:#be123c;background:#fff1f2}" +
4343      ".caf-bk-fs-btn-pri{background:linear-gradient(135deg," + c + "," + cGradEnd + ");color:#fff;box-shadow:0 6px 20px " + c + "44;border-color:transparent}" +
4344      ".caf-bk-fs-btn-pri:hover{transform:translateY(-2px);box-shadow:0 10px 28px " + c + "55;filter:brightness(1.05)}" +
4345      ".caf-bk-fs-btn-pri:disabled{opacity:.5;cursor:not-allowed;transform:none;box-shadow:none}" +
4346      ".caf-bk-fs-btn svg{width:14px;height:14px}" +
4347
4348      /* loading / empty / error in fs */
4349      ".caf-bk-fs-center{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;padding:40px 20px;text-align:center;gap:14px}" +
4350      ".caf-bk-fs-loader{width:56px;height:56px;border:4px solid #e2e8f0;border-top-color:" + c + ";border-radius:50%;animation:caf-spin .8s linear infinite}" +
4351      ".caf-bk-fs-loader-txt{font-size:14px;color:#475569;font-weight:500;max-width:360px;line-height:1.5}" +
4352      ".caf-bk-fs-empty-ic{width:68px;height:68px;border-radius:50%;background:#fef3c7;color:#b45309;display:flex;align-items:center;justify-content:center}" +
4353      ".caf-bk-fs-empty-ic svg{width:30px;height:30px}" +
4354      ".caf-bk-fs-empty-t{font-size:18px;font-weight:800;color:#0f172a}" +
4355      ".caf-bk-fs-empty-s{font-size:13.5px;color:#5b6573;max-width:420px;line-height:1.5}" +
4356      ".caf-bk-fs-error-ic{width:68px;height:68px;border-radius:50%;background:#ffe4e6;color:#be123c;display:flex;align-items:center;justify-content:center}" +
4357
4358      /* done screen (fs) */
4359      ".caf-bk-fs-done{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:flex-start;padding:40px 24px;text-align:center;overflow-y:auto}" +
4360      ".caf-bk-fs-done-ic{width:88px;height:88px;border-radius:50%;background:linear-gradient(135deg," + c + "," + c + "b3);display:flex;align-items:center;justify-content:center;margin-bottom:18px;box-shadow:0 20px 50px " + c + "55;position:relative}" +
4361      ".caf-bk-fs-done-ic svg{width:44px;height:44px}" +
4362      ".caf-bk-fs-done-t{font-size:26px;font-weight:800;color:#0f172a;letter-spacing:-.5px;line-height:1.2}" +
4363      ".caf-bk-fs-done-s{font-size:14px;color:#5b6573;margin-top:8px;max-width:480px;line-height:1.55}" +
4364      ".caf-bk-fs-done-id{display:inline-flex;align-items:center;gap:6px;font-size:12px;font-weight:700;padding:7px 16px;border-radius:99px;margin-top:14px;background:" + c + "15;color:" + c + ";border:1px solid " + c + "30}" +
4365      ".caf-bk-fs-done-sum{margin-top:22px;max-width:520px;width:100%;text-align:left}" +
4366      ".caf-bk-fs-done-actions{display:flex;gap:10px;margin-top:20px;flex-wrap:wrap;justify-content:center}" +
4367
4368      /* ── #5 — content-visibility: skip layout/paint per griglie fuori viewport ── */
4369      ".caf-bk-fs-grid,.caf-bk-fs-cal,.caf-bk-fs-cal-slots{contain:layout style}" +
4370      ".caf-bk-fs-tile,.caf-bk-fs-cal-day{content-visibility:auto;contain-intrinsic-size:auto 96px}" +
4371
4372      /* ── #9 — glassmorphism coerente sulle card overlay ── */
4373      ".caf-bk-fs .caf-bk-fs-monthcal,.caf-bk-fs .caf-bk-fs-slotpanel,.caf-bk-fs .caf-bk-fs-summary,.caf-bk-fs .caf-bk-fs-side-card,.caf-bk-fs .caf-bk-fs-cal-day{background:rgba(255,255,255,.78);backdrop-filter:blur(10px) saturate(1.1);-webkit-backdrop-filter:blur(10px) saturate(1.1)}" +
4374      ".caf-bk-fs .caf-bk-fs-tile{background:rgba(255,255,255,.85);backdrop-filter:blur(8px);-webkit-backdrop-filter:blur(8px)}" +
4375
4376      /* ── #19 — progress bar interno shimmer (oltre alla width transition) ── */
4377      "@keyframes caf-progshim{0%{background-position:-100% 0}100%{background-position:200% 0}}" +
4378      ".caf-bk-fs-progress-bar{background:linear-gradient(90deg,#fff,rgba(255,255,255,.7),#fff,#fff);background-size:200% 100%;animation:caf-progshim 2.4s linear infinite}" +
4379
4380      /* ── #12 — empty-state illustrato (calendario stilizzato + ombra) ── */
4381      ".caf-bk-fs-empty-ic{position:relative;background:linear-gradient(135deg,#fef3c7,#fde68a);box-shadow:0 8px 28px rgba(217,119,6,.22);overflow:visible}" +
4382      ".caf-bk-fs-empty-ic::after{content:'';position:absolute;
4382left:50%;bottom:-10px;width:54px;height:6px;background:rgba(217,119,6,.18);border-radius:50%;filter:blur(4px);transform:translateX(-50%)}" +
4383      ".caf-bk-fs-empty-ic::before{content:'';position:absolute;top:-6px;right:-6px;width:18px;height:18px;border-radius:50%;background:linear-gradient(135deg,#fb923c,#f59e0b);box-shadow:0 4px 12px rgba(251,146,60,.45);animation:caf-dot-pulse 2s ease-in-out infinite}" +
4384
4385      /* ── #13 — dark mode auto via prefers-color-scheme ── */
4386      "@media(prefers-color-scheme:dark){" +
4387        ".caf-bk-fs{background:linear-gradient(180deg,#0f172a 0%,#1e293b 100%)}" +
4388        ".caf-bk-fs .caf-bk-fs-monthcal,.caf-bk-fs .caf-bk-fs-slotpanel,.caf-bk-fs .caf-bk-fs-summary,.caf-bk-fs .caf-bk-fs-side-card,.caf-bk-fs .caf-bk-fs-cal-day,.caf-bk-fs .caf-bk-fs-tile{background:rgba(30,41,59,.78);border-color:#334155;color:#e2e8f0}" +
4389        ".caf-bk-fs .caf-bk-fs-tile-name,.caf-bk-fs .caf-bk-fs-step-h1,.caf-bk-fs .caf-bk-fs-monthcal-title,.caf-bk-fs .caf-bk-fs-slotpanel-hdr,.caf-bk-fs .caf-bk-fs-cal-day-n,.caf-bk-fs .caf-bk-fs-summary-val,.caf-bk-fs .caf-bk-fs-side-val{color:#f1f5f9}" +
4390        ".caf-bk-fs .caf-bk-fs-step-h2,.caf-bk-fs .caf-bk-fs-tile-sub,.caf-bk-fs .caf-bk-fs-monthcal-day{color:#94a3b8}" +
4391        ".caf-bk-fs .caf-bk-fs-search input,.caf-bk-fs .caf-bk-inp{background:rgba(15,23,42,.6);color:#f1f5f9;border-color:#334155}" +
4392        ".caf-bk-fs .caf-bk-fs-search input::placeholder,.caf-bk-fs .caf-bk-inp::placeholder{color:#94a3b8}" +
4393        ".caf-bk-fs .caf-bk-fs-cal-chip{background:rgba(15,23,42,.5);color:#e2e8f0;border-color:#334155}" +
4394        ".caf-bk-fs .caf-bk-fs-monthcal-day{color:#94a3b8}" +
4395        ".caf-bk-fs .caf-bk-fs-foot{background:rgba(15,23,42,.92);border-color:#334155}" +
4396        ".caf-bk-fs .caf-bk-fs-side{background:rgba(15,23,42,.55);border-color:#334155}" +
4397        ".caf-bk-fs .caf-bk-fs-skel{background:linear-gradient(90deg,#1e293b 0%,#334155 50%,#1e293b 100%)}" +
4398        /* a11y dark #C6: testo secondario/bordi rimasti "da fondo chiaro" */
4399        ".caf-bk-fs .caf-bk-fs-btn-ghost{color:#cbd5e1;border-color:#334155}" +
4400        ".caf-bk-fs .caf-bk-fs-btn-danger{color:#94a3b8}" +
4401        ".caf-bk-fs .caf-bk-fs-side-lbl,.caf-bk-fs .caf-bk-fs-summary-lbl,.caf-bk-fs .caf-bk-fs-summary-sub,.caf-bk-fs .caf-bk-fs-summary-hdr,.caf-bk-fs .caf-bk-fs-side-title,.caf-bk-fs .caf-bk-fs-side-help,.caf-bk-fs .caf-bk-fs-monthcal-wd,.caf-bk-fs .caf-bk-fs-monthcal-legend,.caf-bk-fs .caf-bk-fs-slotpanel-empty,.caf-bk-fs .caf-bk-fs-slotgroup-hdr,.caf-bk-fs .caf-bk-fs-cal-day-d,.caf-bk-fs .caf-bk-fs-empty-s,.caf-bk-fs .caf-bk-fs-done-s,.caf-bk-fs .caf-bk-fs-skel-caption,.caf-bk-fs .caf-bk-fs-search-ic,.caf-bk-fs .caf-bk-fs-tile-arrow,.caf-bk-fs .caf-bk-fs-side-val.muted{color:#94a3b8}" +
4402        ".caf-bk-fs .caf-bk-fs-side-help{background:rgba(15,23,42,.5);border-color:#334155}" +
4403        ".caf-bk-fs .caf-bk-fs-monthcal-day.past{color:#64748b}" +
4404        /* a11y dark #C5: dentro il wizard dark il token ink (calibrato sul
4405         * bianco) diventa la variante chiara calibrata sui fondi scuri: tutte
4406         * le regole che usano var(--caf-c-ink) restano giuste in dark. Scope
4407         * su .caf-bk-fs (NON :host): il pannello chat resta a tema chiaro. */
4408        ".caf-bk-fs{--caf-c-ink:var(--caf-c-ink-dark)}" +
4409        /* ...tranne gli elementi del wizard che poggiano su card CHIARE anche
4410         * in dark (mbsum bianco, summary premium #fff): ink reale letterale. */
4411        ".caf-bk-fs .caf-bk-fs-mbsum-count,.caf-bk-fs .caf-bk-fs-mbsum-toggle,.caf-bk-fs .caf-bk-sum-premium-edit{color:" + cInk + "}" +
4412      "}" +
4413
4414      /* ── #4 — View Transitions: lascio il default cross-fade del browser
4415       * (durata predefinita ~250ms) per non interferire con le animazioni
4416       * caf-bk-fs-stage già scope-applicate a .caf-bk-fs-main. ── */
4417
4418      /* ── BOOKING-ACTIVE proactive cues (FAB wiggle/shake/tilt, mobile banner, tooltip) ── */
4419      "@keyframes caf-bk-wiggle{0%,100%{transform:rotate(0)}15%{transform:rotate(-8deg) scale(1.04)}30%{transform:rotate(7deg) scale(1.04)}45%{transform:rotate(-5deg) scale(1.02)}60%{transform:rotate(4deg) scale(1.02)}75%{transform:rotate(-2deg)}}" +
4420      "@keyframes caf-bk-shake{0%,100%{transform:translateX(0)}20%{transform:translateX(-6px)}40%{transform:translateX(6px)}60%{transform:translateX(-4px)}80%{transform:translateX(4px)}}" +
4421      ".caf-toggle.caf-bk-wiggle{animation:caf-bk-wiggle .9s ease-in-out 1}" +
4422      ".caf-toggle.caf-bk-shake{animation:caf-bk-shake .55s ease-in-out 2}" +
4423      ".caf-toggle.caf-bk-active:hover{transform:scale(1.08) rotate(-4deg)}" +
4424      /* badge tooltip "1 nuovo" */
4425      ".caf-toggle .caf-badge.caf-bk-newdot{display:flex!important;min-width:18px;height:18px;padding:0 5px;border-radius:9px;background:#dc2626;color:#fff;font-size:11px;font-weight:700;align-items:center;justify-content:center;position:absolute;top:-2px;right:-2px;box-shadow:0 2px 6px rgba(239,68,68,.5);animation:caf-pulse-soft 1.6s ease-in-out infinite}" +
4426      /* mobile: FAB più grande quando booking attivo + sticky banner */
4427      "@media(max-width:640px){" +
4428        ".caf-toggle.caf-bk-active{width:72px;height:72px}" +
4429        ".caf-toggle.caf-bk-active img{width:34px;height:34px}" +
4430      "}" +
4431      /* rispetto delle preferenze di motion ridotto: disabilita tutto ciò che attira l'occhio */
4432      "@media(prefers-reduced-motion:reduce){" +
4433        ".caf-toggle .caf-pulse,.caf-toggle .caf-pulse-2{animation:none!important;display:none}" +
4434        ".caf-toggle.caf-bk-active .caf-glow{opacity:.4!important;animation:none!important}" +
4435        ".caf-bubble.caf-bk-active{animation:none!important}" +
4436        ".caf-bubble.caf-bk-active .caf-bk-arrow{animation:none!important}" +
4437        ".caf-toggle.caf-bk-wiggle,.caf-toggle.caf-bk-shake{animation:none!important}" +
4438        ".caf-wave-emoji{animation:none!important}" +
4439      "}" +
4440      /* pausa animazioni quando il tab non è in focus per non sprecare CPU */
4441      ".caf-bk-pause-anim .caf-toggle .caf-pulse,.caf-bk-pause-anim .caf-toggle .caf-pulse-2,.caf-bk-pause-anim .caf-bubble.caf-bk-active,.caf-bk-pause-anim .caf-bubble.caf-bk-active .caf-bk-arrow{animation-play-state:paused!important}" +
4442
4443      /* ═══════════════════════════════════════════════════════════════
4444       * BOOKING UX 2.0 — categorie servizi, popular, docs tooltip,
4445       * slot fasce, prima disponibilità, error boundary, stepper
4446       * persistente, summary premium, skeleton realistici.
4447       * ═══════════════════════════════════════════════════════════════ */
4448
4449      /* ── #6/#8 categorie e card servizi più ricche ── */
4450      ".caf-bk-srv-cats{display:flex;flex-wrap:wrap;gap:8px;margin:0 0 14px;padding:0 2px}" +
4451      ".caf-bk-srv-cat{display:inline-flex;align-items:center;gap:6px;padding:7px 13px;border-radius:999px;border:1.5px solid #e2e8f0;background:#fff;color:#475569;font-size:12.5px;font-weight:600;cursor:pointer;transition:all .2s ease;white-space:nowrap}" +
4452      ".caf-bk-srv-cat svg{width:14px;
4452height:14px}" +
4453      ".caf-bk-srv-cat:hover{border-color:#cbd5e1;background:#f8fafc;transform:translateY(-1px)}" +
4454      ".caf-bk-srv-cat.active{background:" + c + ";color:#fff;border-color:" + c + ";box-shadow:0 4px 12px -3px " + c + "55}" +
4455      ".caf-bk-srv-cat-count{display:inline-flex;align-items:center;justify-content:center;min-width:18px;height:18px;padding:0 5px;border-radius:9px;background:rgba(0,0,0,.08);font-size:10.5px;font-weight:700}" +
4456      ".caf-bk-srv-cat.active .caf-bk-srv-cat-count{background:rgba(255,255,255,.25)}" +
4457      ".caf-bk-fs-tile.has-cat{padding-left:64px;position:relative}" +
4458      ".caf-bk-fs-tile-cat-badge{position:absolute;left:14px;top:14px;width:38px;height:38px;border-radius:11px;display:flex;align-items:center;justify-content:center;color:#fff;font-size:16px}" +
4459      ".caf-bk-fs-tile-cat-badge svg{width:18px;height:18px}" +
4460      ".caf-bk-fs-tile-popular{position:absolute;top:8px;right:36px;display:inline-flex;align-items:center;gap:4px;padding:3px 8px;border-radius:999px;background:linear-gradient(135deg,#f97316,#ef4444);color:#fff;font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.3px;box-shadow:0 2px 6px rgba(239,68,68,.4)}" +
4461      ".caf-bk-fs-tile-recent{position:absolute;top:8px;right:36px;display:inline-flex;align-items:center;gap:4px;padding:3px 8px;border-radius:999px;background:#0369a1;color:#fff;font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.3px}" +
4462      ".caf-bk-fs-tile-docs{display:inline-flex;align-items:center;gap:4px;margin-top:6px;padding:3px 8px;border-radius:6px;background:#f1f5f9;color:#475569;font-size:11px;font-weight:600;cursor:pointer;transition:background .15s}" +
4463      ".caf-bk-fs-tile-docs:hover{background:#e2e8f0;color:#1e293b}" +
4464      ".caf-bk-fs-tile-docs svg{width:11px;height:11px}" +
4465
4466      /* ── #9 documenti modal/popover ── */
4467      "@keyframes caf-bk-fade-in{0%{opacity:0;transform:translateY(8px)}100%{opacity:1;transform:translateY(0)}}" +
4468      ".caf-bk-docs-pop{position:absolute;left:14px;right:14px;top:100%;margin-top:6px;z-index:50;background:#fff;border:1px solid #e2e8f0;border-radius:12px;padding:14px 16px;box-shadow:0 12px 32px rgba(0,0,0,.12);animation:caf-bk-fade-in .25s ease-out}" +
4469      ".caf-bk-docs-pop-h{font-size:12px;font-weight:700;color:#0f172a;margin:0 0 8px;text-transform:uppercase;letter-spacing:.5px;display:flex;align-items:center;gap:6px}" +
4470      ".caf-bk-docs-pop-h svg{width:14px;height:14px;color:" + c + "}" +
4471      ".caf-bk-docs-pop-l{margin:0;padding:0;list-style:none}" +
4472      ".caf-bk-docs-pop-l li{position:relative;padding:4px 0 4px 20px;font-size:12.5px;line-height:1.5;color:#334155}" +
4473      ".caf-bk-docs-pop-l li::before{content:'';position:absolute;left:6px;top:11px;width:6px;height:6px;border-radius:50%;background:" + c + "}" +
4474      ".caf-bk-docs-pop-foot{margin-top:8px;padding-top:8px;border-top:1px solid #f1f5f9;font-size:11px;color:#5b6573}" +
4475
4476      /* ── #11 spelling tolerance suggestion ── */
4477      ".caf-bk-suggest{padding:14px 16px;background:linear-gradient(135deg,#fef3c7,#fde68a);border:1px solid #fcd34d;border-radius:12px;color:#78350f;font-size:13px;line-height:1.55;display:flex;gap:10px;align-items:flex-start}" +
4478      ".caf-bk-suggest svg{flex:0 0 auto;width:18px;height:18px;color:#b45309;margin-top:1px}" +
4479      ".caf-bk-suggest button{background:#fff;border:1px solid #f59e0b;color:#92400e;padding:5px 12px;border-radius:8px;font-size:12px;font-weight:600;cursor:pointer;margin-top:4px;display:inline-flex;align-items:center;gap:5px}" +
4480      ".caf-bk-suggest button:hover{background:#fef9c3}" +
4481
4482      /* ── #20/#21 slot fasce e prima disponibilità ── */
4483      "@keyframes caf-bk-shimmer{0%{background-position:-100% 0}100%{background-position:200% 0}}" +
4484      /* background: prima un solid color come fallback (iOS Safari <16.2 / Chrome <111
4485       * non supportano color-mix() — senza fallback il riquadro resta trasparente
4486       * e il testo bianco diventa illeggibile su sfondo bianco). Poi il gradient. */
4487      ".caf-bk-quickpick{margin:0 0 16px;padding:22px 22px;background:" + c + ";background:linear-gradient(135deg," + c + " 0%,color-mix(in srgb," + c + " 75%,#1e293b) 100%);border-radius:16px;color:#fff;display:flex;align-items:flex-start;gap:14px;cursor:pointer;box-shadow:0 8px 24px -6px " + c + "66;transition:transform .25s ease,box-shadow .25s ease;position:relative;overflow:hidden;width:100%;box-sizing:border-box;min-height:96px}" +
4488      ".caf-bk-quickpick::before{content:'';position:absolute;inset:0;background:linear-gradient(120deg,transparent 30%,rgba(255,255,255,.18) 50%,transparent 70%);background-size:200% 100%;animation:caf-bk-shimmer 3.5s ease-in-out infinite;pointer-events:none}" +
4489      ".caf-bk-quickpick:hover{transform:translateY(-2px);box-shadow:0 12px 32px -6px " + c + "88}" +
4490      ".caf-bk-quickpick-ic{flex:0 0 auto;width:48px;height:48px;border-radius:13px;background:rgba(255,255,255,.22);display:flex;align-items:center;justify-content:center;font-size:22px;backdrop-filter:blur(8px);align-self:flex-start;margin-top:2px}" +
4491      ".caf-bk-quickpick-body{flex:1 1 auto;min-width:0}" +
4492      ".caf-bk-quickpick-lbl{font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.6px;opacity:.85;margin-bottom:5px}" +
4493      ".caf-bk-quickpick-when{font-size:16px;font-weight:700;line-height:1.35;word-wrap:break-word;overflow-wrap:anywhere}" +
4494      ".caf-bk-quickpick-where{font-size:13px;opacity:.92;margin-top:6px;line-height:1.45;word-wrap:break-word;overflow-wrap:anywhere}" +
4495      ".caf-bk-quickpick-arrow{flex:0 0 auto;font-size:22px;font-weight:700;opacity:.9;align-self:center;padding-top:4px}" +
4496      /* Mobile: bumpiamo dimensioni testo, padding e min-height per assicurare
4497       * leggibilità del contenuto (la card è il CTA principale dello step). */
4498      "@media(max-width:480px){" +
4499        ".caf-bk-quickpick{padding:18px 16px;gap:14px;border-radius:14px;min-height:108px;align-items:center}" +
4500        ".caf-bk-quickpick-ic{width:48px;height:48px;border-radius:12px;font-size:22px;margin-top:0}" +
4501        ".caf-bk-quickpick-lbl{font-size:11px;letter-spacing:.7px;margin-bottom:6px;opacity:.9}" +
4502        ".caf-bk-quickpick-when{font-size:17px;font-weight:800;line-height:1.3;letter-spacing:-.1px}" +
4503        ".caf-bk-quickpick-where{font-size:13.5px;line-height:1.4;margin-top:5px;opacity:.95}" +
4504        ".caf-bk-quickpick-arrow{font-size:22px;align-self:center;padding-top:0}" +
4505      "}" +
4506      /* Mobile molto stretto (< 360px): ulteriore compattamento per evitare
4507       * troncamenti, ma testo ancora leggibile. */
4508      "@media(max-width:360px){" +
4509        ".caf-bk-quickpick{padding:16px 14px;gap:12px;min-height:100px}" +
4510        ".caf-bk-quickpick-ic{width:42px;height:42px;font-size:20px}" +
4511        ".caf-bk-quickpick-when{font-size:15.5px}" +
4512        ".caf-bk-quickpick-where{font-size:12.5px}" +
4513        ".caf-bk-quickpick-arrow{font-size:18px}" +
4514      "}" +
4515      ".caf-bk-slot-fasce{display:flex;gap:8px;margin:0 0 14px;padding:4px;background:#f1f5f9;border-radius:12px}" +
4516      ".caf-bk-slot-fascia{flex:1;padding:10px 12px;border:none;background:transparent;border-radius:9px;font-size:13px;font-weight:600;color:#5b6573;cursor:pointer;transition:all .2s ease;display:flex;align-items:center;justify-content:center;gap:6px}" +
4517      ".caf-bk-slot-fascia:hover{color:#334155}" +
4518      ".caf-bk-slot-fascia.active{background:#fff;color:" + c + ";box-shadow:0 1px 3px rgba(0,0,0,.08)}" +
4519      ".caf-bk-slot-fascia-count{font-size:11px;padding:1px 7px;border-radius:999px;background:#e2e8f0;color:#5b6573;font-weight:700}" +
4520      ".caf-bk-slot-fascia.active .caf-bk-slot-fascia-count{background:" + c + "22;color:" + c + "}" +
4521      /* slot density (caldi/freddi): tinta sottile sui chip via data-density */
4522      ".caf-bk-fs-cal-chip[data-density='hot']{background:linear-gradient(135deg,#dcfce7,#bbf7d0);border-color:#86efac;color:#166534}" +
4523      ".caf-bk-fs-cal-chip[data-density='hot']:hover{background:#bbf7d0;border-color:#22c55e;color:#15803d}" +
4524      ".caf-bk-fs-cal-chip[data-density='warm']{background:#f0fdf4;border-color:#bbf7d0;color:#15803d}" +
4525      ".caf-bk-fs-cal-chip[data-density='cool']{opacity:.85}" +
4526      ".caf-bk-fs-cal-chip[data-density] .d{display:none}" +
4527
4528      /* ── #35 summary premium ── */
4529      ".caf-bk-sum-premium{padding:0;border-radius:18px;overflow:hidden;background:#fff;box-shadow:0 8px 32px -8px rgba(0,0,0,.14);border:1px solid #e2e8f0;margin:0 0 14px}" +
4530      ".caf-bk-sum-premium-head{padding:20px 22px;background:linear-gradient(135deg," + c + ",color-mix(in srgb," + c + " 78%,#1e293b));color:#fff;display:flex;align-items:center;gap:12px}" +
4531      ".caf-bk-sum-premium-check{width:42px;height:42px;border-radius:50%;background:rgba(255,255,255,.22);display:flex;align-items:center;justify-content:center;backdrop-filter:blur(8px)}" +
4532      ".caf-bk-sum-premium-check svg{width:22px;height:22px}" +
4533      ".caf-bk-sum-premium-h{font-size:17px;font-weight:700;margin:0;line-height:1.3}" +
4534      ".caf-bk-sum-premium-s{font-size:12.5px;opacity:.85;margin-top:2px}" +
4535      ".caf-bk-sum-premium-cols{display:grid;grid-template-columns:repeat(3,1fr);gap:0;padding:0;background:#f8fafc}" +
4536      "@media(max-width:540px){.caf-bk-sum-premium-cols{grid-template-columns:1fr}}" +
4537      ".caf-bk-sum-premium-col{padding:18px;border-right:1px solid #e2e8f0;background:#fff}" +
4538      ".caf-bk-sum-premium-col:last-child{border-right:none}" +
4539      "@media(max-width:540px){.caf-bk-sum-premium-col{border-right:none;border-bottom:1px solid #e2e8f0}.caf-bk-sum-premium-col:last-child{border-bottom:none}}" +
4540      ".caf-bk-sum-premium-col-h{font-size:10.5px;font-weight:700;color:#5b6573;text-transform:uppercase;letter-spacing:.7px;margin:0 0 8px;display:flex;align-items:center;gap:6px}" +
4541      ".caf-bk-sum-premium-col-h svg{width:13px;height:13px;color:" + c + "}" +
4542      ".caf-bk-sum-premium-line{font-size:14px;color:#0f172a;font-weight:600;line-height:1.4;margin-bottom:3px}" +
4543      ".caf-bk-sum-premium-sub{font-size:12px;color:#5b6573;line-height:1.4;overflow-wrap:anywhere;min-width:0}" +
4544      ".caf-bk-sum-premium-edit{display:inline-flex;align-items:center;gap:4px;margin-top:8px;font-size:11px;color:" + c + ";background:transparent;border:none;cursor:pointer;font-weight:600;padding:0}" +
4545      ".caf-bk-sum-premium-edit:hover{text-decoration:underline}" +
4546      ".caf-bk-sum-premium-edit svg{width:11px;height:11px}" +
4547      ".caf-bk-sum-disclaimer{padding:11px 16px;font-size:12px;color:#5b6573;text-align:center;line-height:1.5;background:#f8fafc;border-top:1px solid #e2e8f0;display:flex;align-items:center;justify-content:center;gap:6px}" +
4548      ".caf-bk-sum-disclaimer svg{width:14px;
4548height:14px;color:#5b6573;flex:0 0 auto}" +
4549
4550      /* ── #37 conferma prominente con loader ── */
4551      "@keyframes caf-bk-confirm-pulse{0%,100%{box-shadow:0 8px 24px -6px " + c + "55,0 0 0 0 " + c + "33}50%{box-shadow:0 12px 32px -4px " + c + "88,0 0 0 8px " + c + "00}}" +
4552      ".caf-bk-confirm-mega{display:flex;align-items:center;justify-content:center;gap:10px;width:100%;padding:18px 24px;background:linear-gradient(135deg," + c + ",color-mix(in srgb," + c + " 80%,#1e293b));color:#fff;border:none;border-radius:14px;font-size:15.5px;font-weight:700;cursor:pointer;animation:caf-bk-confirm-pulse 2.4s ease-in-out infinite;transition:transform .15s ease}" +
4553      ".caf-bk-confirm-mega:hover{transform:translateY(-1px)}" +
4554      ".caf-bk-confirm-mega:active{transform:translateY(0) scale(.98)}" +
4555      ".caf-bk-confirm-mega:disabled{opacity:.7;cursor:wait;animation:none}" +
4556      ".caf-bk-confirm-mega svg{width:18px;height:18px}" +
4557      ".caf-bk-confirm-spinner{display:inline-block;width:18px;height:18px;border:2.5px solid rgba(255,255,255,.4);border-right-color:#fff;border-radius:50%;animation:caf-spin .8s linear infinite}" +
4558
4559      /* ── #38 toast errore strutturato ── */
4560      "@keyframes caf-bk-toast-in{0%{opacity:0;transform:translateY(-10px) scale(.96)}100%{opacity:1;transform:translateY(0) scale(1)}}" +
4561      ".caf-bk-toast{position:absolute;left:50%;top:14px;transform:translateX(-50%);z-index:100;max-width:520px;width:calc(100% - 24px);padding:14px 16px;background:#fef2f2;border:1px solid #fca5a5;border-radius:12px;color:#991b1b;display:flex;align-items:flex-start;gap:10px;box-shadow:0 12px 32px -6px rgba(239,68,68,.3);animation:caf-bk-toast-in .35s cubic-bezier(.34,1.56,.64,1) both}" +
4562      ".caf-bk-toast.warn{background:#fef3c7;border-color:#fbbf24;color:#92400e}" +
4563      ".caf-bk-toast.info{background:#dbeafe;border-color:#60a5fa;color:#1e40af}" +
4564      ".caf-bk-toast-ic{flex:0 0 auto;width:20px;height:20px}" +
4565      ".caf-bk-toast-body{flex:1;font-size:13px;line-height:1.5}" +
4566      ".caf-bk-toast-h{font-weight:700;font-size:13.5px;margin-bottom:2px}" +
4567      ".caf-bk-toast-x{flex:0 0 auto;background:transparent;border:none;color:inherit;opacity:.7;cursor:pointer;font-size:18px;line-height:1;padding:2px 4px}" +
4568      ".caf-bk-toast-x:hover{opacity:1}" +
4569
4570      /* ── #48 stepper visivo persistente ── */
4571      ".caf-bk-stepper{display:flex;align-items:center;gap:0;padding:14px 18px;background:#fff;border-bottom:1px solid #e2e8f0;position:sticky;top:0;z-index:10}" +
4572      ".caf-bk-stepper-item{flex:1;display:flex;flex-direction:column;align-items:center;gap:4px;position:relative;cursor:pointer;font-size:11px;color:#5b6573;font-weight:600;transition:color .2s}" +
4573      ".caf-bk-stepper-item.done{color:" + c + ";cursor:pointer}" +
4574      ".caf-bk-stepper-item.active{color:#0f172a;font-weight:700}" +
4575      ".caf-bk-stepper-item.locked{cursor:not-allowed}" +
4576      ".caf-bk-stepper-dot{width:26px;height:26px;border-radius:50%;background:#f1f5f9;color:#5b6573;border:2px solid #e2e8f0;display:flex;align-items:center;justify-content:center;font-size:12px;font-weight:700;transition:all .25s ease;z-index:1;position:relative}" +
4577      ".caf-bk-stepper-item.done .caf-bk-stepper-dot{background:" + c + ";color:#fff;border-color:" + c + "}" +
4578      ".caf-bk-stepper-item.active .caf-bk-stepper-dot{background:#fff;color:" + c + ";border-color:" + c + ";box-shadow:0 0 0 4px " + c + "22;transform:scale(1.08)}" +
4579      ".caf-bk-stepper-bar{position:absolute;top:13px;height:2px;background:#e2e8f0;width:100%;z-index:0}" +
4580      ".caf-bk-stepper-bar-fill{position:absolute;top:13px;left:0;height:2px;background:" + c + ";z-index:0;transition:width .4s cubic-bezier(.65,0,.35,1)}" +
4581      "@media(max-width:480px){.caf-bk-stepper{padding:12px 10px;gap:0}.caf-bk-stepper-item{font-size:9.5px}.caf-bk-stepper-dot{width:22px;height:22px;font-size:11px}.caf-bk-stepper-bar,.caf-bk-stepper-bar-fill{top:11px}}" +
4582
4583      /* ── #49 skeleton realistici per ogni step ── */
4584      "@keyframes caf-bk-skel-shimmer{0%{background-position:-100% 0}100%{background-position:200% 0}}" +
4585      ".caf-bk-skel{background:linear-gradient(90deg,#f1f5f9 0%,#e2e8f0 50%,#f1f5f9 100%);background-size:200% 100%;animation:caf-bk-skel-shimmer 1.4s ease-in-out infinite;border-radius:8px}" +
4586      ".caf-bk-skel-tile{height:74px;border-radius:14px;margin-bottom:10px}" +
4587      ".caf-bk-skel-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:10px}" +
4588      ".caf-bk-skel-slot{height:48px;border-radius:10px}" +
4589      ".caf-bk-skel-slots{display:grid;grid-template-columns:repeat(auto-fill,minmax(100px,1fr));gap:8px}" +
4590
4591      /* ── #57 error boundary visivo ── */
4592      ".caf-bk-errbox{padding:24px 22px;background:#fff;border-radius:16px;border:1px solid #fee2e2;box-shadow:0 8px 24px -8px rgba(239,68,68,.18);display:flex;flex-direction:column;align-items:center;text-align:center;gap:8px}" +
4593      ".caf-bk-errbox-ic{width:60px;height:60px;border-radius:50%;background:linear-gradient(135deg,#fee2e2,#fecaca);display:flex;align-items:center;justify-content:center;color:#b91c1c;margin-bottom:6px}" +
4594      ".caf-bk-errbox-ic svg{width:28px;height:28px}" +
4595      ".caf-bk-errbox-h{font-size:17px;font-weight:700;color:#0f172a;margin:0}" +
4596      ".caf-bk-errbox-msg{font-size:13.5px;color:#475569;line-height:1.55;margin:0;max-width:420px}" +
4597      ".caf-bk-errbox-actions{display:flex;flex-wrap:wrap;gap:8px;justify-content:center;margin-top:14px}" +
4598      ".caf-bk-errbox-btn{display:inline-flex;align-items:center;gap:6px;padding:10px 16px;border-radius:10px;font-size:13px;font-weight:600;cursor:pointer;border:1.5px solid transparent;transition:all .2s}" +
4599      ".caf-bk-errbox-btn.primary{background:" + c + ";color:#fff;border-color:" + c + "}" +
4600      ".caf-bk-errbox-btn.primary:hover{filter:brightness(1.08)}" +
4601      ".caf-bk-errbox-btn.ghost{background:#fff;color:#475569;border-color:#e2e8f0}" +
4602      ".caf-bk-errbox-btn.ghost:hover{background:#f8fafc;border-color:#cbd5e1}" +
4603      ".caf-bk-errbox-btn.helpline{background:#fef3c7;color:#92400e;border-color:#fbbf24}" +
4604      ".caf-bk-errbox-btn.helpline:hover{background:#fde68a}" +
4605      ".caf-bk-errbox-btn svg{width:14px;
4605height:14px}" +
4606      ".caf-bk-errbox-code{margin-top:10px;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:11px;color:#5b6573;letter-spacing:.3px}" +
4607
4608      /* ── #60 resume banner ── */
4609      ".caf-bk-resume{padding:14px 16px;background:linear-gradient(135deg,#dbeafe,#bfdbfe);border:1px solid #93c5fd;border-radius:12px;margin:0 0 14px;display:flex;align-items:center;gap:12px;color:#1e3a8a}" +
4610      ".caf-bk-resume-ic{flex:0 0 auto;width:36px;height:36px;border-radius:10px;background:rgba(255,255,255,.55);display:flex;align-items:center;justify-content:center;font-size:18px}" +
4611      ".caf-bk-resume-body{flex:1;min-width:0;font-size:13px;line-height:1.45}" +
4612      ".caf-bk-resume-h{font-weight:700;margin-bottom:2px}" +
4613      ".caf-bk-resume-actions{display:flex;gap:6px;flex-shrink:0}" +
4614      ".caf-bk-resume-btn{padding:7px 12px;border-radius:8px;font-size:12px;font-weight:600;cursor:pointer;border:none}" +
4615      ".caf-bk-resume-btn.primary{background:#1e40af;color:#fff}" +
4616      ".caf-bk-resume-btn.ghost{background:transparent;color:#1e3a8a}" +
4617
4618      /* ── #34 phone with country prefix ── */
4619      ".caf-bk-tel-wrap{display:flex;gap:6px;align-items:stretch}" +
4620      ".caf-bk-tel-cc{position:relative;flex:0 0 auto}" +
4621      ".caf-bk-tel-cc-btn{display:inline-flex;align-items:center;gap:5px;padding:0 12px;height:100%;border:1.5px solid #e2e8f0;border-radius:10px;background:#fff;font-size:13px;font-weight:600;color:#334155;cursor:pointer}" +
4622      ".caf-bk-tel-cc-btn:hover{border-color:#cbd5e1}" +
4623      ".caf-bk-tel-cc-pop{position:absolute;top:calc(100% + 4px);left:0;background:#fff;border:1px solid #e2e8f0;border-radius:10px;box-shadow:0 12px 32px rgba(0,0,0,.12);max-height:240px;overflow:auto;width:240px;z-index:50;animation:caf-bk-fade-in .2s ease-out}" +
4624      ".caf-bk-tel-cc-item{display:flex;align-items:center;gap:8px;padding:9px 12px;cursor:pointer;font-size:13px;color:#334155;border-bottom:1px solid #f1f5f9}" +
4625      ".caf-bk-tel-cc-item:hover{background:#f8fafc}" +
4626      ".caf-bk-tel-cc-item-flag{font-size:18px}" +
4627      ".caf-bk-tel-cc-item-code{flex:0 0 auto;color:#5b6573;font-weight:600;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12px}" +
4628      ".caf-bk-tel-input{flex:1;min-width:0}" +
4629
4630      /* ── #33 ricorda dati checkbox ── */
4631      ".caf-bk-remember{display:flex;align-items:flex-start;gap:9px;padding:10px 0;cursor:pointer;font-size:12.5px;line-height:1.5;color:#475569}" +
4632      ".caf-bk-remember input{margin-top:2px;flex:0 0 auto;accent-color:" + c + "}" +
4633      ".caf-bk-remember-sub{font-size:11px;color:#5b6573;margin-top:2px}" +
4634
4635      /* ── network/offline banner ── */
4636      ".caf-bk-net-banner{padding:9px 14px;font-size:12.5px;display:flex;align-items:center;gap:8px;line-height:1.45}" +
4637      ".caf-bk-net-banner.offline{background:#fef2f2;color:#991b1b;border-bottom:1px solid #fecaca}" +
4638      ".caf-bk-net-banner.slow{background:#fef3c7;color:#92400e;border-bottom:1px solid #fcd34d}" +
4639      ".caf-bk-net-banner svg{flex:0 0 auto;width:14px;height:14px}" +
4640
4641      /* ── #52 reduced motion esteso a tutto il booking ── */
4642      "@media(prefers-reduced-motion:reduce){" +
4643        ".caf-bk-fs-tile,.caf-bk-fs-stage,.caf-bk-quickpick,.caf-bk-toast,.caf-bk-skel,.caf-bk-confirm-mega{animation:none!important;transition:none!important}" +
4644        ".caf-bk-quickpick::before{animation:none!important}" +
4645        ".caf-bk-fs-tile-popular{animation:none!important}" +
4646      "}" +
4647
4648      /* ── #42 GDPR consent banner ───────────────────────────────────── */
4649      "@keyframes caf-bk-gdpr-in{0%{opacity:0;transform:translateY(20px)}100%{opacity:1;transform:translateY(0)}}" +
4650      /* Banner consenso CONFINATO al chat panel: backdrop assoluto interno + card.
4651       * Niente position:fixed sul viewport — non deve mai coprire la pagina host. */
4652      ".caf-bk-gdpr-backdrop{position:absolute;inset:0;z-index:100;background:rgba(15,23,42,.45);display:flex;align-items:flex-end;justify-content:center;padding:10px;border-radius:inherit;animation:caf-bk-fade-in .25s ease-out}" +
4653      ".caf-bk-gdpr{position:relative;width:100%;max-width:100%;background:#fff;border:1px solid #e2e8f0;border-radius:14px;box-shadow:0 10px 30px rgba(0,0,0,.2);padding:14px 16px;font-family:inherit;animation:caf-bk-gdpr-in .35s cubic-bezier(.34,1.56,.64,1) both;color:#0f172a;max-height:100%;overflow:auto}" +
4654      ".caf-bk-gdpr-row{display:flex;align-items:flex-start;gap:14px}" +
4655      ".caf-bk-gdpr-ic{flex:0 0 auto;width:38px;height:38px;border-radius:11px;background:linear-gradient(135deg," + c + "22," + c + "11);color:" + c + ";display:flex;align-items:center;justify-content:center}" +
4656      ".caf-bk-gdpr-ic svg{width:20px;height:20px}" +
4657      ".caf-bk-gdpr-body{flex:1;min-width:0;font-size:13.5px;line-height:1.55;color:#334155}" +
4658      ".caf-bk-gdpr-h{font-size:14.5px;font-weight:700;color:#0f172a;margin:0 0 4px}" +
4659      ".caf-bk-gdpr-actions{display:flex;flex-wrap:wrap;gap:8px;margin-top:12px}" +
4660      ".caf-bk-gdpr-btn{display:inline-flex;align-items:center;gap:6px;padding:9px 16px;border-radius:9px;font-size:13px;font-weight:600;cursor:pointer;border:1.5px solid transparent;font-family:inherit;transition:all .2s}" +
4661      ".caf-bk-gdpr-btn.primary{background:" + c + ";color:#fff;border-color:" + c + "}" +
4662      ".caf-bk-gdpr-btn.primary:hover{filter:brightness(1.08)}" +
4663      /* primary-outline = stessa enfasi di primary ma stile inverso (per "Rifiuta tutti")
4664       * GDPR EDPB 03/2022: accept e reject devono essere visualmente equivalenti */
4665      ".caf-bk-gdpr-btn.primary-outline{background:#fff;color:" + c + ";border-color:" + c + "}" +
4666      ".caf-bk-gdpr-btn.primary-outline:hover{background:" + c + "10}" +
4667      ".caf-bk-gdpr-btn.ghost{background:#fff;color:#475569;border-color:#e2e8f0}" +
4668      ".caf-bk-gdpr-btn.ghost:hover{background:#f8fafc;border-color:#cbd5e1}" +
4669      ".caf-bk-gdpr-link{color:" + c + ";text-decoration:underline;cursor:pointer;font-size:12px}" +
4670      ".caf-bk-gdpr-info{margin-top:8px;font-size:12px;color:#5b6573}" +
4671      ".caf-bk-gdpr-prefs{margin:14px 0 8px;display:flex;flex-direction:column;gap:8px;padding:12px;background:#f8fafc;border:1px solid #e2e8f0;border-radius:10px}" +
4672      ".caf-bk-gdpr-pref{display:flex;align-items:flex-start;gap:10px;font-size:12.5px;line-height:1.5;cursor:pointer;color:#334155}" +
4673      ".caf-bk-gdpr-pref input{margin-top:3px;flex:0 0 auto;accent-color:" + c + "}" +
4674      ".caf-bk-gdpr-pref input:disabled{opacity:.6;cursor:not-allowed}" +
4675      ".caf-bk-gdpr-pref small{display:block;color:#5b6573;margin-top:2px;font-size:11.5px;line-height:1.45}" +
4676      "@media(max-width:480px){.caf-bk-gdpr{padding:12px 14px}.caf-bk-gdpr-row{gap:10px}.caf-bk-gdpr-body{font-size:12.5px}}" +
4677
4678      /* ── #1 CF auto-fill feedback chip ─────────────────────────────── */
4679      ".caf-bk-cf-chip{display:flex;align-items:center;gap:8px;margin-top:6px;padding:8px 12px;background:#ecfdf5;border:1px solid #86efac;border-radius:8px;font-size:12.5px;color:#166534;animation:caf-bk-fade-in .3s ease-out}" +
4680      ".caf-bk-cf-chip.warn{background:#fef3c7;border-color:#fcd34d;color:#92400e}" +
4681      ".caf-bk-cf-chip svg{width:14px;
4681height:14px;flex:0 0 auto}" +
4682
4683      /* ── #2 smart re-booking card ──────────────────────────────────── */
4684      ".caf-bk-rebook{margin:0 0 14px;padding:16px 18px;background:linear-gradient(135deg,#f0f9ff,#e0f2fe);border:1px solid #bae6fd;border-radius:14px;display:flex;align-items:center;gap:14px;cursor:pointer;transition:all .25s ease;color:#0f172a}" +
4685      ".caf-bk-rebook:hover{transform:translateY(-2px);box-shadow:0 8px 20px rgba(14,165,233,.15);border-color:#7dd3fc}" +
4686      ".caf-bk-rebook-ic{flex:0 0 auto;width:42px;height:42px;border-radius:12px;background:#0369a1;color:#fff;display:flex;align-items:center;justify-content:center}" +
4687      ".caf-bk-rebook-ic svg{width:20px;height:20px}" +
4688      ".caf-bk-rebook-body{flex:1;min-width:0;line-height:1.45}" +
4689      ".caf-bk-rebook-lbl{font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.5px;color:#0369a1;margin-bottom:3px}" +
4690      ".caf-bk-rebook-h{font-size:14.5px;font-weight:700;color:#0f172a}" +
4691      ".caf-bk-rebook-s{font-size:12.5px;color:#475569;margin-top:2px;overflow-wrap:anywhere}" +
4692      ".caf-bk-rebook-arrow{flex:0 0 auto;font-size:18px;font-weight:700;color:#0369a1}" +
4693
4694      /* ── #13 mobile bottom-sheet con grab handle e snap ─────────────── */
4695      ".caf-bk-fs-mbsum{position:relative}" +
4696      ".caf-bk-fs-mbsum-grab{display:none;position:absolute;left:50%;top:6px;transform:translateX(-50%);width:36px;height:4px;background:#cbd5e1;border-radius:2px;cursor:grab}" +
4697      "@media(max-width:820px){.caf-bk-fs-mbsum-grab{display:block}.caf-bk-fs-mbsum{padding-top:18px;touch-action:pan-y}.caf-bk-fs-mbsum.dragging{transition:none}}" +
4698
4699      /* ── #14 form keyboard handling: padding extra quando input focused ── */
4700      ".caf-bk-fs-main.kb-padded{padding-bottom:280px!important;transition:padding-bottom .2s ease}" +
4701
4702      /* ── #31 SVG confetti più ricco ──────────────────────────────────── */
4703      "@keyframes caf-bk-confetti-fall{0%{transform:translate(0,-120%) rotate(0)}100%{transform:translate(var(--tx,0),120vh) rotate(var(--tr,720deg))}}" +
4704      ".caf-bk-confetti-svg{position:absolute;width:14px;height:14px;pointer-events:none;animation:caf-bk-confetti-fall var(--td,1.6s) cubic-bezier(.4,0,.6,1) forwards;animation-delay:var(--tdelay,0s);will-change:transform}" +
4705      "@media(prefers-reduced-motion:reduce){.caf-bk-confetti-svg{display:none}}" +
4706      /* ── a11y overrides (appended last to win the cascade) ── */
4707      ".caf-sr-only{position:absolute!important;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0 0 0 0);clip-path:inset(50%);white-space:nowrap;border:0}" +
4708      ".caf-skip{position:absolute;left:8px;top:8px;z-index:20;transform:translateY(-160%);transition:transform .15s ease;background:#fff;color:var(--caf-c-ink);border:2px solid var(--caf-c-ink);border-radius:8px;padding:8px 12px;font-size:13px;font-weight:600;cursor:pointer;font-family:inherit}" +
4709      ".caf-skip:focus,.caf-skip:focus-visible{transform:translateY(0)}" +
4710      /* Anello di focus: NIENTE border-radius forzato — l'outline segue la
4711       * forma REALE dell'elemento (sul pallino rotondo -> anello rotondo;
4712       * l'8px imposto lo faceva sembrare un riquadro squadrato). */
4713      ":host :focus-visible{outline:3px solid var(--caf-c-ink)!important;outline-offset:2px!important;box-shadow:0 0 0 2px #fff}"+
4714      /* scroll-margin SOLO nello scroller dei messaggi: applicato globalmente
4715       * faceva scrollare a Chrome anche il PANNELLO (overflow:hidden e' pur
4716       * sempre uno scroll container): contenuto shiftato in alto e fascia
4717       * bianca permanente sotto il footer (bug "spazio bianco" Chrome-only). */
4718      ".caf-messages :focus-visible{scroll-margin:96px 0}" +
4719      ".caf-input:focus-visible,.caf-bk-inp:focus-visible,.caf-bk-search input:focus-visible,.caf-bk-fs-search input:focus-visible{outline:2px solid var(--caf-c-ink)!important;outline-offset:1px!important;box-shadow:none}" +
4720      ".caf-msg-wrap:focus-within .caf-copy-btn,.caf-msg-wrap:focus-within .caf-translate-btn{opacity:1;pointer-events:auto}" +
4721      ".caf-msg-bubble.user,.caf-msg-bubble.user a,.caf-vt-bubble.user,.caf-mic,.caf-bk-pri,.caf-bk-fs-btn-pri,.caf-bk-cta-btn,.caf-bk-cta-mini,.caf-bk-locate,.caf-bk-expand-banner,.caf-bk-quickpick,.caf-bk-gdpr-btn.primary,.caf-bk-errbox-btn.primary,.caf-bk-srv-cat.active,.caf-bk-fs-monthcal-day.selected,.caf-bk-fs-monthcal-day.available:hover,.caf-bk-nearby-btn:hover,.caf-quick button:hover .caf-quick-ic,.caf-quick button:hover .caf-quick-ar,.caf-bk-step-dot.done,.caf-bk-rb.on{color:var(--caf-on-c)}" +
4722      ".caf-scroll-btn,.caf-msg-bubble a,.caf-url-badge,.caf-phone-badge,.caf-bk-crumb,.caf-bk-act,.caf-bk-more,.caf-inline-bk-chip,.caf-ghost-sug-chip,.caf-bk-fs-step-num,.caf-bk-step-lbl.active,.caf-bk-slot-fascia.active,.caf-bk-fs-mbsum-count,.caf-bk-fs-mbsum-toggle,.caf-bk-fs-side-ic,.caf-bk-fs-done-id,.caf-bk-sum-premium-edit,.caf-bk-fs-monthcal-day.available{color:var(--caf-c-ink)}" +
4723      ".caf-bk-confirm-mega{background:linear-gradient(135deg,var(--caf-c-ink),color-mix(in srgb,var(--caf-c-ink) 80%,#1e293b))}" +
4724      ".caf-bk-sum-premium-head{background:linear-gradient(135deg,var(--caf-c-ink),color-mix(in srgb,var(--caf-c-ink) 78%,#1e293b))}" +
4725      ".caf-bubble.caf-bk-active{background:linear-gradient(135deg,var(--caf-c-ink),color-mix(in srgb,var(--caf-c-ink) 80%,#1e293b))}" +
4726      /* a11y #C40: quickpick come confirm-mega: gradiente su base ink + testo
4727       * bianco (vince sull'override onC r.4602 perche' successivo in cascata) */
4728      ".caf-bk-quickpick{background:linear-gradient(135deg,var(--caf-c-ink),color-mix(in srgb,var(--caf-c-ink) 75%,#1e293b));color:#fff}" +
4729      ".caf-bk-cta-dismiss,.caf-bk-toast-x{min-width:24px;min-height:24px}" +
4730      ".caf-bk-back,.caf-bk-cancel,.caf-bk-sum-premium-edit,.caf-bk-fs-tile-docs,.caf-bk-fs-mbsum-toggle{min-height:24px}" +
4731      ""
4732    );
4733  }
4734
4735  /* ── WIDGET CLASS ─────────────────────────────────────────── */
4736  function CafChat(opts) {
4737    this.opts = opts;
4738    /* i18n: rileva lingua una sola volta all'avvio dell'istanza */
4739    BK_LANG = bkDetectLang(opts);
4740    this.isOpen = false;
4741    this.isLoading = false;
4742    this.messages = [];
4743    this.menuOpen = false;
4744    this.isAtBottom = true;
4745    this.unreadCount = 0;
4746    this.rating = null;
4747    this._saveTimeout = null;
4748    this._scrollRaf = null;
4749    this._voiceMode = false;
4750    this._isListening = false;
4751    this._isSpeaking = false;
4752    this._recognition = null;
4753    this._audioQueue = [];
4754    this._ttsAbortControllers = [];
4755    this._ttsEpoch = 0;
4756    this._isPlayingQueue = false;
4757    this._currentAudio = null;
4758    this._chatAbort = null;
4759    this._lastMessageWasVoice = false;
4760    this._listenTimer = null;
4761    this._voiceInterimText = "";
4762    this._vadInterval = null;
4763    this._vadStream = null;
4764    this._voiceStreamingBubble = null;
4765    this._voiceGhostEl = null;
4766    this._lastFinalIdx = 0;
4767    this._finalBuffer = "";
4768    this._sendDebounce = null;
4769    this.isFullscreen = false;
4770    /* booking state */
4771    this.bkStep = "idle"; this._bkDir = "fwd";
4772    this.bkRegioni = []; this.bkSelRegione = null;
4773    this.bkProvince = []; this.bkSelProv = null;
4774    this.bkServizi = []; this.bkSelServ = null;
4775    this.bkSedi = []; this.bkSelSede = null;
4776    this.bkSpazi = []; this.bkNextG = ""; this.bkSelSlot = null;
4777    /* Default: prenotazione "per me". */
4778    this.bkBookingFor = "self";
4779    /* #PII device-condiviso — NON pre-compiliamo ciecamente i dati salvati: li
4780     * teniamo come "candidato" e chiediamo conferma esplicita ("Sei tu?") allo
4781     * step collectData prima di adottarli. bkClient parte SEMPRE vuoto, così su
4782     * un PC condiviso (sportello) la persona successiva non invia per sbaglio i
4783     * dati di quella precedente. loadStoredClient è gated sul consenso functional. */
4784    this._bkStoredCandidate = loadStoredClient(this.bkBookingFor);
4785    this.bkClient = {nome:"",cognome:"",codfis:"",email:"",cellulare:"",sesso:"M"};
4786    this.bkId = null; this.bkErrMsg = ""; this.bkErrCode = null; this.bkErrReturn = null;
4787    this.bkFormErr = null; this._bkBusy = false; this._bkEl = null;
4788    this._bkOpGen = 0; this._bkActiveCtrl = null; this._bkLoadingReturnStep = null; this._bkLoadingMore = false;
4789    this._bkFsMode = false; this._bkFsEl = null;
4790    this._bkEnabled = undefined;
4791    this.bkHint = ""; /* extracted service name from user message */
4792    /* password reset card state */
4793    this.pwdStep = "idle"; /* idle | form | submitting | done | error */
4794    this.pwdEmail = "";
4795    this.pwdErr = "";
4796    this._pwdEl = null;
4797    this.STORAGE_KEY = "caf-chat-inline-history"+CAF_NS;
4798    this.CONV_ID_KEY = "caf-chat-inline-conv-id"+CAF_NS;
4799    this.EMAIL_KEY = "caf-chat-inline-email"+CAF_NS;
4800    this.conversationId = this._loadConversationId();
4801    this.userEmail = this._loadEmail();
4802    this._init();
4803  }
4804
4805  /* ── Ecommerce: rendering card prodotti dentro un bubble.
4806   * Attivato dopo che bubble.innerHTML è stato impostato dal markdown.
4807   * Trova tutti gli anchor "<span class='caf-prod-anchor' data-prod-key='…'>"
4808   * e li sostituisce con card DOM ricche con event listener.
4809   * Idempotente: anchor già processati hanno data-prod-rendered="1". */
4810  CafChat.prototype._renderEcommerceCards = function (bubble, userQuery) {
4811    if (!bubble || !this._ecommerceEnabled) return;
4812    if (!this._ecommerceProductIndex) return;
4813    var anchors = bubble.querySelectorAll(".caf-prod-anchor:not([data-prod-rendered])");
4814    var self = this;
4815    /* I marker [[PROD:ID]] del bot vengono raccolti in UN UNICO carosello
4816     * orizzontale (foto in evidenza + scroll). Niente più card grandi
4817     * inline: esperienza coerente e ordinata. */
4818    var collectedProducts = [];
4819    var seenKeys = {};
4820    for (var i = 0; i < anchors.length; i++) {
4821      var a = anchors[i];
4822      var key = a.getAttribute("data-prod-key") || "";
4823      var prod = self._ecommerceProductIndex[key];
4824      a.setAttribute("data-prod-rendered", "1");
4825      /* L'anchor è invisibile (display:none); lasciarlo nel DOM preserva
4826       * la posizione testuale del marker senza romperla. */
4827      if (prod && !seenKeys[key]) {
4828        seenKeys[key] = true;
4829        collectedProducts.push(prod);
4830      }
4831    }
4832    if (collectedProducts.length > 0) {
4833      /* Idempotenza: se il bubble ha già il carosello dei markers, skip. */
4834      if (!bubble.querySelector(".caf-prod-carousel[data-prod-source='markers']")) {
4835        var label = collectedProducts.length === 1 ? "Prodotto" : "Prodotti consigliati";
4836        var carousel = self._buildProductCarousel(collectedProducts, label);
4837        carousel.setAttribute("data-prod-source", "markers");
4838        bubble.appendChild(carousel);
4839      }
4840      /* Se ci sono markers espliciti, NON aggiungere il carosello
4841       * auto-detected (sarebbe ridondante). */
4842      return;
4843    }
4844    /* Nessun marker: prova intent detection su query utente. */
4845    self._maybeAppendAutoCarousel(bubble, userQuery);
4846  };
4847
4848  /** Appende un carosello orizzontale di prodotti se la query utente
4849   *  associata a questo bubble esprime un intento "lista prodotti".
4850   *
4851   *  Usa Claude (via /api/product-intent) per la classificazione semantica
4852   *  — capisce qualsiasi forma di domanda, anche colloquiale o creativa.
4853   *  Cache per-istanza delle risposte (stessa query = stesso risultato).
4854   *  Se l'API fallisce/è down, fallback al regex locale (best-effort). */
4855  CafChat.prototype._maybeAppendAutoCarousel = function (bubble, userQuery) {
4856    if (!bubble || !this._ecommerceEnabled || !this._ecommerceProducts) {
4857      console.debug("[CafChat][carousel] skip: ecommerce non pronto", {
4858        enabled: !!this._ecommerceEnabled,
4859        productsLoaded: !!this._ecommerceProducts
4860      });
4861      return;
4862    }
4863    if (bubble.querySelector(".caf-prod-carousel")) return;
4864    if (bubble.getAttribute("data-prod-carousel-checked") === "1") return;
4865    if (!bubble.classList || !bubble.classList.contains("assistant")) return;
4866    if (!userQuery || typeof userQuery !== "string") return;
4867    bubble.setAttribute("data-prod-carousel-checked", "1");
4868    var self = this;
4869    var queryKey = userQuery.trim().toLowerCase();
4870    self._intentCache = self._intentCache || {};
4871
4872    /* ── FAST-PATH locale ────────────────────────────────────────
4873     * Per query ovvie ("mostra card", "mostrami i prodotti", "voglio
4874     * vedere", "in offerta", "novità", ecc.) renderizziamo il carosello
4875     * IMMEDIATAMENTE senza aspettare Claude. Garantisce comportamento
4876     * istantaneo e robustezza anche se /api/product-intent è offline o
4877     * mal interpreta la query (es. "card" come parola inglese ambigua). */
4878    var fastIntent = self._fastLocalIntent(userQuery);
4879    if (fastIntent && fastIntent.show) {
4880      self._intentCache[queryKey] = fastIntent;
4881      self._renderCarouselFromIntent(bubble, fastIntent);
4882      return;
4883    }
4884
4885    /* Cache hit (Claude già consultato per questa query). */
4886    if (self._intentCache[queryKey] !== undefined) {
4887      self._renderCarouselFromIntent(bubble, self._intentCache[queryKey]);
4888      return;
4889    }
4890
4891    /* Query ambigua: chiediamo a Claude. */
4892    var url = (self.opts.baseUrl || "") + "/api/product-intent";
4893    var ctrl = new AbortController();
4894    var tid = setTimeout(function () { ctrl.abort(); }, 6000);
4895    fetch(url, {
4896      method: "POST",
4897      headers: { "Content-Type": "application/json" },
4898      body: JSON.stringify({
4899        message: userQuery,
4900        categories: self._ecommerceCategories || [],
4901      }),
4902      signal: ctrl.signal,
4903    }).then(function (r) {
4904      clearTimeout(tid);
4905      if (!r.ok) console.warn("[CafChat][carousel] /api/product-intent HTTP " + r.status);
4906      return r.ok ? r.json() : null;
4907    }).then(function (intent) {
4908      if (!intent) intent = self._detectProductIntentLocal(userQuery);
4909      console.debug("[CafChat][carousel] intent:", intent);
4910      self._intentCache[queryKey] = intent;
4911      /* Se il bubble è stato rimpiazzato da un re-render mentre attendevamo,
4912       * rifletti sull'ultimo bubble assistant invece di perdere il render. */
4913      if (!bubble.isConnected) {
4914        self._reapplyCarouselToLatest(intent);
4915        return;
4916      }
4917      self._renderCarouselFromIntent(bubble, intent);
4918    }).catch(function (err) {
4919      clearTimeout(tid);
4920      console.warn("[CafChat][carousel] product-intent error:", err && err.message ? err.message : err);
4921      var intent = self._detectProductIntentLocal(userQuery);
4922      self._intentCache[queryKey] = intent;
4923      if (!bubble.isConnected) {
4924        self._reapplyCarouselToLatest(intent);
4925        return;
4926      }
4927      self._renderCarouselFromIntent(bubble, intent);
4928    });
4929  };
4930
4931  /** Fast-path locale: cattura le query ovvie senza chiamare Claude.
4932   *  Più aggressivo del legacy _detectProductIntent perché è il primo
4933   *  filtro: vogliamo che "mostra card" / "mostrami i prodotti" / etc.
4934   *  triggerino IMMEDIATAMENTE il carosello.
4935   *  NOTA: NON usiamo \b dopo lettere accentate (à,è,é,ì,ò,ù) perché in
4936   *  JS regex \b funziona solo tra ASCII word chars. Usiamo invece
4937   *  lookahead/lookbehind di non-word ASCII oppure niente. */
4938  CafChat.prototype._fastLocalIntent = function (text) {
4939    var t = String(text || "").toLowerCase().trim();
4940    if (!t) return null;
4941    /* Promo / sconto / offerta */
4942    if (/(?:^|[^a-z0-9])(prom\w*|offert\w*|scont\w*|risparm\w*|sald\w*|svendit\w*|affar\w*)/i.test(t)) {
4943      return { show: true, type: "promo", category: "", label: "In offerta" };
4944    }
4945    /* Novità (gestione speciale per accento finale) */
4946    if (/(?:^|[^a-z0-9])(novit[aà]|novita|nuov[oaie]\w*|appena\s*arrivat\w*)/i.test(t)) {
4947      return { show: true, type: "new", category: "", label: "Novità" };
4948    }
4949    /* Popolari espliciti */
4950    if (/(?:^|[^a-z0-9])(popolar\w*|piu\s*vendut\w*|più\s*vendut\w*|best\s*sell\w*|miglior\w*|top\b|trend\w*|gettonat\w*|consigl\w*|raccoman\w*|suggeri\w*)/i.test(t)) {
4951      return { show: true, type: "popular", category: "", label: "I più popolari" };
4952    }
4953    /* Verbo "vedere" in qualsiasi forma + opzionali parole prima */
4954    if (/(?:^|[^a-z0-9])(vogli\w*|vorrei|fammi|fai|fatemi|fateci|posso|potete)\s+(veder\w+|vis\w+)/i.test(t)) {
4955      return { show: true, type: "popular", category: "", label: "Selezione per te" };
4956    }
4957    /* Verbi imperativi/azione che chiedono prodotti */
4958    if (/(?:^|[^a-z0-9])(most\w+|mostra|mostrami|elenc\w*|catalog\w*|lista|vetrin\w*|vendet\w*|dammi|date\w*|fammi\s+veder\w*|fai\s+veder\w*)/i.test(t)) {
4959      return { show: true, type: "popular", category: "", label: "Selezione per te" };
4960    }
4961    /* "cosa avete" / "cosa vendete" / "cosa c'è" / "che prodotti avete".
4962     * Restrittivo: NON matcha "che orari avete" o "che giorni siete aperti".
4963     * Richiede un sostantivo prodotto-correlato dopo "che". */
4964    if (/(?:^|[^a-z0-9])(cosa\s+(avet\w*|vendet\w*|c[èe])|che\s+(prodott\w*|articol\w*|robe?\b|cose\b|tipi)\s+(avet\w*|vendet\w*))/i.test(t)) {
4965      return { show: true, type: "popular", category: "", label: "Selezione per te" };
4966    }
4967    return null;
4968  };
4969
4970  /** Quando il bubble originale è stato rimpiazzato da un re-render,
4971   *  rendi il carosello sull'ultimo bubble assistente del DOM. */
4972  CafChat.prototype._reapplyCarouselToLatest = function (intent) {
4973    if (!intent || !intent.show || !this.messagesEl) return;
4974    var bubbles = this.messagesEl.querySelectorAll(".caf-msg-bubble.assistant");
4975    if (!bubbles.length) return;
4976    var last = bubbles[bubbles.length - 1];
4977    if (!last || last.querySelector(".caf-prod-carousel")) return;
4978    this._renderCarouselFromIntent(last, intent);
4979  };
4980
4981  /** Render carosello dato un intent oggetto {show, type, category, label}. */
4982  CafChat.prototype._renderCarouselFromIntent = function (bubble, intent) {
4983    if (!intent || !intent.show) return;
4984    if (bubble.querySelector(".caf-prod-carousel")) return;
4985    var products = this._selectProductsByIntent(
4986      { type: intent.type, value: intent.category, label: intent.label },
4987      15
4988    );
4989    if (!products.length) return;
4990    var carousel = this._buildProductCarousel(products, intent.label);
4991    bubble.appendChild(carousel);
4992  };
4993
4994  /** Fallback regex locale (usato solo se Claude API fallisce). */
4995  CafChat.prototype._detectProductIntentLocal = function (text) {
4996    var legacy = this._detectProductIntent(text);
4997    if (!legacy) return { show: false, type: "none", category: "", label: "" };
4998    return {
4999      show: true,
5000      type: legacy.type,
5001      category: legacy.value || "",
5002      label: legacy.label || "",
5003    };
5004  };
5005
5006  /** Riconosce l'intento "mostra lista prodotti" dal testo utente.
5007   *  Ritorna {type, value, label} oppure null.
5008   *  Categoria viene matchata solo se la query è breve O contiene un
5009   *  "verbo d'intento" — così "ieri ho bevuto una birra" non triggera. */
5010  CafChat.prototype._detectProductIntent = function (text) {
5011    var t = String(text || "").toLowerCase().trim();
5012    if (!t) return null;
5013    /* Popolari / più venduti / consigliati / top / best seller / migliori */
5014    if (/\b(popolar\w*|piu\s*vendut\w*|più\s*vendut\w*|best\s*sell\w*|consigl\w*|raccoman\w*|suggeri\w*|top\b|miglior\w*|preferit\w*|favorit\w*|trend\w*|gettonat\w*|gradit\w*)/i.test(t)) {
5015      return { type: "popular", label: "I più popolari" };
5016    }
5017    /* Promo / sconto / offerta */
5018    if (/\b(prom\w*|offert\w*|scont\w*|risparm\w*|sald\w*|svendit\w*|deal\b|affar\w*|conveni\w*)/i.test(t)) {
5019      return { type: "promo", label: "In offerta" };
5020    }
5021    /* Novità / nuovi */
5022    if (/\b(novit[aà]\w*|nuov[oaie]\w*|appena\s*arrivat\w*|ultim[oi]\s*arriv\w*|ultime\s*nov)/i.test(t)) {
5023      return { type: "new", label: "Novità" };
5024    }
5025    /* Verbo d'intento: usato per validare le categorie su query lunghe. */
5026    var hasIntentVerb = /\b(most\w*|vogli\w*|vorrei|cerc\w*|avet\w*|dammi|fatemi|fammi|elenc\w*|trov\w*|catalog|vendet\w*|qual[ie])\b/i.test(t);
5027    /* Categoria specifica: cerca matching su MASTER_CATEGORY/CATEGORY */
5028    var cats = this._ecommerceCategories || [];
5029    for (var i = 0; i < cats.length; i++) {
5030      var cat = cats[i];
5031      var catLower = cat.toLowerCase();
5032      if (catLower.length < 4) continue;
5033      var escaped = catLower.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
5034      var re = new RegExp("(^|[^a-z\u00e0-\u00ff])" + escaped + "([^a-z\u00e0-\u00ff]|$)", "i");
5035      if (re.test(t)) {
5036        /* Accetta categoria solo se: query corta OR ha verbo d'intento */
5037        if (t.length < 60 || hasIntentVerb) {
5038          return { type: "category", value: cat, label: "Categoria: " + cat };
5039        }
5040      }
5041    }
5042    /* "Tutti i prodotti" / "cosa avete" / "mostrami" / "elenco" / "lista" */
5043    if (/\b(tutti\s*i\s*prodott|cosa\s*avet|che\s*cosa\s*avet|mostr\w*|elenc\w*|catalog\w*|lista\s*prodott|vetrina|che\s*\w*\s*vendet)/i.test(t)) {
5044      return { type: "popular", label: "Selezione per te" };
5045    }
5046    return null;
5047  };
5048
5049  /** Seleziona N prodotti in base all'intento. */
5050  CafChat.prototype._selectProductsByIntent = function (intent, n) {
5051    var rows = (this._ecommerceProducts || []).slice();
5052    n = n || 15;
5053    function safeFloat(v){var x=parseFloat(v);return isFinite(x)?x:0;}
5054    function inStock(p){var s=safeFloat(p.WAREHOUSE_STOCK);return s>0;}
5055    function hasPromo(p){var pr=safeFloat(p.PRICE),pm=safeFloat(p.PRICE_PROMO);return pm>0&&pm<pr;}
5056    var pool;
5057    switch (intent.type) {
5058      case "promo":
5059        pool = rows.filter(function(p){return hasPromo(p)&&inStock(p)&&p.IMAGE_URL;});
5060        pool.sort(function(a,b){
5061          var sa=(safeFloat(a.PRICE)-safeFloat(a.PRICE_PROMO))/Math.max(1,safeFloat(a.PRICE));
5062          var sb=(safeFloat(b.PRICE)-safeFloat(b.PRICE_PROMO))/Math.max(1,safeFloat(b.PRICE));
5063          return sb-sa;
5064        });
5065        break;
5066      case "category":
5067        pool = rows.filter(function(p){
5068          var mc=String(p.MASTER_CATEGORY||"").toLowerCase();
5069          var cc=String(p.CATEGORY||"").toLowerCase();
5070          var v=intent.value.toLowerCase();
5071          return (mc===v||cc.indexOf(v)>=0||mc.indexOf(v)>=0)&&inStock(p)&&p.IMAGE_URL;
5072        });
5073        /* Promo prima, poi stock alto */
5074        pool.sort(function(a,b){
5075          var pa=hasPromo(a)?1:0,pb=hasPromo(b)?1:0;
5076          if(pa!==pb)return pb-pa;
5077          return safeFloat(b.WAREHOUSE_STOCK)-safeFloat(a.WAREHOUSE_STOCK);
5078        });
5079        break;
5080      case "new":
5081        /* Heuristic: ID più alto = aggiunto di recente */
5082        pool = rows.filter(function(p){return inStock(p)&&p.IMAGE_URL;});
5083        pool.sort(function(a,b){return parseInt(b.ID||"0",10)-parseInt(a.ID||"0",10);});
5084        break;
5085      case "popular":
5086      default:
5087        /* Popolare = ha promo + stock alto + immagine + categoria utile */
5088        pool = rows.filter(function(p){
5089          if(!inStock(p))return false;
5090          if(!p.IMAGE_URL)return false;
5091          if(String(p.MASTER_CATEGORY||"").toUpperCase()==="ALTRO")return false;
5092          return true;
5093        });
5094        pool.sort(function(a,b){
5095          var sa=(hasPromo(a)?2:0)+Math.min(10,safeFloat(a.WAREHOUSE_STOCK)/100);
5096          var sb=(hasPromo(b)?2:0)+Math.min(10,safeFloat(b.WAREHOUSE_STOCK)/100);
5097          return sb-sa;
5098        });
5099        break;
5100    }
5101    return pool.slice(0, n);
5102  };
5103
5104  /** Costruisce un carosello orizzontale scrollabile di card prodotto. */
5105  CafChat.prototype._buildProductCarousel = function (products, label) {
5106    var self = this;
5107    var c = this.opts.color;
5108    var carousel = document.createElement("div");
5109    carousel.className = "caf-prod-carousel";
5110
5111    var hdr = document.createElement("div");
5112    hdr.className = "caf-prod-carousel-hdr";
5113    var hLeft = document.createElement("div");
5114    hLeft.className = "caf-prod-carousel-hdr-l";
5115    hLeft.innerHTML = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/></svg><span>' + escapeHtml(label || "Prodotti") + '</span>';
5116    hdr.appendChild(hLeft);
5117    var hCount = document.createElement("span");
5118    hCount.className = "caf-prod-carousel-count";
5119    hCount.textContent = products.length + " prodott" + (products.length === 1 ? "o" : "i");
5120    hdr.appendChild(hCount);
5121    carousel.appendChild(hdr);
5122
5123    /* Scroller wrapper con frecce */
5124    var sw = document.createElement("div");
5125    sw.className = "caf-prod-carousel-sw";
5126    var btnL = document.createElement("button");
5127    btnL.type = "button";
5128    btnL.className = "caf-prod-carousel-arrow l";
5129    btnL.setAttribute("aria-label", bkT("aria_scroll_left"));
5130    btnL.innerHTML = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><polyline points="15 18 9 12 15 6"/></svg>';
5131    var btnR = document.createElement("button");
5132    btnR.type = "button";
5133    btnR.className = "caf-prod-carousel-arrow r";
5134    btnR.setAttribute("aria-label", bkT("aria_scroll_right"));
5135    btnR.innerHTML = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><polyline points="9 18 15 12 9 6"/></svg>';
5136    var scroller = document.createElement("div");
5137    scroller.className = "caf-prod-carousel-scroll";
5138
5139    products.forEach(function (p, i) {
5140      var miniCard = self._buildProductMiniCard(p, c);
5141      miniCard.style.animationDelay = (i * 40) + "ms";
5142      scroller.appendChild(miniCard);
5143    });
5144
5145    btnL.addEventListener("click", function () {
5146      scroller.scrollBy({ left: -240, behavior: bkScrollBehavior() });
5147    });
5148    btnR.addEventListener("click", function () {
5149      scroller.scrollBy({ left: 240, behavior: bkScrollBehavior() });
5150    });
5151    /* Update arrow visibility on scroll */
5152    function updateArrows() {
5153      btnL.style.opacity = scroller.scrollLeft > 4 ? "1" : "0";
5154      btnL.style.pointerEvents = scroller.scrollLeft > 4 ? "auto" : "none";
5155      var maxScroll = scroller.scrollWidth - scroller.clientWidth - 4;
5156      btnR.style.opacity = scroller.scrollLeft < maxScroll ? "1" : "0";
5157      btnR.style.pointerEvents = scroller.scrollLeft < maxScroll ? "auto" : "none";
5158    }
5159    scroller.addEventListener("scroll", updateArrows);
5160    setTimeout(updateArrows, 50);
5161
5162    sw.appendChild(btnL);
5163    sw.appendChild(scroller);
5164    sw.appendChild(btnR);
5165    carousel.appendChild(sw);
5166    return carousel;
5167  };
5168
5169  /** Mini card prodotto (più compatta, per uso nel carosello). */
5170  CafChat.prototype._buildProductMiniCard = function (p, c) {
5171    var self = this;
5172    function safeNum(v) { var n = parseFloat(v); return isFinite(n) && n >= 0 ? n : 0; }
5173    var price = safeNum(p.PRICE);
5174    var promo = safeNum(p.PRICE_PROMO);
5175    var hasPromo = promo > 0 && promo < price;
5176    var displayPrice = hasPromo ? promo : price;
5177    var savings = hasPromo ? Math.round(((price - promo) / price) * 100) : 0;
5178    var rawStock = parseFloat(p.WAREHOUSE_STOCK);
5179    var stockKnown = isFinite(rawStock);
5180    var outStock = stockKnown && rawStock <= 0;
5181    var lowStock = stockKnown && rawStock > 0 && rawStock < 20;
5182    var hasPrice = price > 0;
5183    var fmtPrice = function (n) { return "€" + (Math.round(n * 100) / 100).toFixed(2).replace(".", ","); };
5184
5185    var card = document.createElement("div");
5186    card.className = "caf-prod-mini";
5187
5188    var imgWrap = document.createElement("div");
5189    imgWrap.className = "caf-prod-mini-img-wrap";
5190    if (hasPromo) {
5191      var pb = document.createElement("div");
5192      pb.className = "caf-prod-mini-promo";
5193      pb.textContent = "-" + savings + "%";
5194      imgWrap.appendChild(pb);
5195    }
5196    if (outStock) {
5197      var ob = document.createElement("div");
5198      ob.className = "caf-prod-mini-stock out";
5199      ob.textContent = "Esaurito";
5200      imgWrap.appendChild(ob);
5201    } else if (lowStock) {
5202      var lb = document.createElement("div");
5203      lb.className = "caf-prod-mini-stock low";
5204      lb.textContent = "Ultimi";
5205      imgWrap.appendChild(lb);
5206    }
5207    var img = document.createElement("img");
5208    img.className = "caf-prod-mini-img";
5209    img.loading = "lazy";
5210    img.alt = p.ITEM || "";
5211    /* referrerpolicy=no-referrer: alcuni CDN bloccano via Referer header. */
5212    img.referrerPolicy = "no-referrer";
5213    /* Genera placeholder colorato + iniziali per fallback elegante. */
5214    function genFallback(item, category) {
5215      var name = String(item || "Prodotto");
5216      /* Iniziali: prime 2 parole significative (>2 char). */
5217      var words = name.split(/\s+/).filter(function (w) { return w.length > 2; });
5218      var initials = (words[0] ? words[0][0] : "?") + (words[1] ? words[1][0] : "");
5219      initials = initials.toUpperCase().slice(0, 2);
5220      /* Colore deterministico in base al nome — palette coordinata. */
5221      var palette = [
5222        ["#fef3c7", "#92400e"], ["#dbeafe", "#1e40af"], ["#fce7f3", "#9d174d"],
5223        ["#dcfce7", "#166534"], ["#fed7aa", "#9a3412"], ["#e0e7ff", "#3730a3"],
5224        ["#f3e8ff", "#6b21a8"], ["#cffafe", "#155e75"], ["#fee2e2", "#991b1b"],
5225        ["#ecfccb", "#3f6212"]
5226      ];
5227      var hash = 0;
5228      for (var i = 0; i < name.length; i++) hash = (hash * 31 + name.charCodeAt(i)) | 0;
5229      var colors = palette[Math.abs(hash) % palette.length];
5230      var bg = colors[0], fg = colors[1];
5231      var svg = "<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 200 200'>" +
5232        "<rect width='200' height='200' fill='" + bg + "'/>" +
5233        "<text x='100' y='100' text-anchor='middle' dominant-baseline='central' " +
5234        "font-family='-apple-system,Segoe UI,Roboto,sans-serif' font-weight='800' " +
5235        "font-size='84' fill='" + fg + "' letter-spacing='-2'>" + initials + "</text></svg>";
5236      return "data:image/svg+xml;utf8," + encodeURIComponent(svg);
5237    }
5238    var fallbackSvg = genFallback(p.ITEM, p.MASTER_CATEGORY);
5239    /* URL rewrite: il JSON ha www.pizzone.com (404) ma le foto sono su
5240     * shop.pizzone.com (200). Sostituiamo automaticamente. Normalizziamo
5241     * anche eventuali doppi slash come "/upload//images.jpg". */
5242    function rewriteImageUrl(u) {
5243      if (!u) return u;
5244      return String(u)
5245        .replace(/^https?:\/\/(?:www\.)?pizzone\.com\//i, "https://shop.pizzone.com/")
5246        .replace(/([^:])\/{2,}/g, "$1/");
5247    }
5248    var imageUrl = rewriteImageUrl(p.IMAGE_URL);
5249    img.src = imageUrl || fallbackSvg;
5250    /* Se anche shop.pizzone.com fallisce, mostra il placeholder colorato. */
5251    var retryDone = false;
5252    img.addEventListener("error", function () {
5253      if (!retryDone && imageUrl && img.src === imageUrl) {
5254        retryDone = true;
5255        img.removeAttribute("referrerpolicy");
5256        img.src = imageUrl + (imageUrl.indexOf("?") >= 0 ? "&" : "?") + "_=" + Date.now();
5257        return;
5258      }
5259      img.src = fallbackSvg;
5260    });
5261    imgWrap.appendChild(img);
5262    card.appendChild(imgWrap);
5263
5264    var body = document.createElement("div");
5265    body.className = "caf-prod-mini-body";
5266
5267    var title = document.createElement("div");
5268    title.className = "caf-prod-mini-title";
5269    title.textContent = p.ITEM || "Prodotto";
5270    body.appendChild(title);
5271
5272    if (hasPrice) {
5273      var prow = document.createElement("div");
5274      prow.className = "caf-prod-mini-prow";
5275      var pmain = document.createElement("span");
5276      pmain.className = "caf-prod-mini-price";
5277      pmain.textContent = fmtPrice(displayPrice);
5278      prow.appendChild(pmain);
5279      if (hasPromo) {
5280        var pold = document.createElement("span");
5281        pold.className = "caf-prod-mini-price-old";
5282        pold.textContent = fmtPrice(price);
5283        prow.appendChild(pold);
5284      }
5285      body.appendChild(prow);
5286    }
5287    card.appendChild(body);
5288
5289    return card;
5290  };
5291
5292  /** Costruisce la card DOM per un prodotto. */
5293  CafChat.prototype._buildProductCard = function (p, c) {
5294    var self = this;
5295    /* Safe numeric parsing: protezione da NaN/negativi/dati corrotti.
5296     * Se un valore è non-finito o negativo, lo trattiamo come 0 e
5297     * nascondiamo il relativo elemento in UI (no "€NaN" o prezzi negativi). */
5298    function safeNum(v) {
5299      var n = parseFloat(v);
5300      return isFinite(n) && n >= 0 ? n : 0;
5301    }
5302    function safeInt(v) {
5303      var n = parseInt(v, 10);
5304      return isFinite(n) && n >= 0 ? n : 0;
5305    }
5306    var price = safeNum(p.PRICE);
5307    var promo = safeNum(p.PRICE_PROMO);
5308    var hasPromo = promo > 0 && promo < price;
5309    var displayPrice = hasPromo ? promo : price;
5310    var savings = hasPromo ? Math.round(((price - promo) / price) * 100) : 0;
5311    var perPiece = safeNum(p.PRICE_FOR_SINGLE_PIECE);
5312    var perPack = safeInt(p.PIECES_PER_PACK);
5313    /* Stock: WAREHOUSE_STOCK assente o NaN ⇒ trattiamo come unknown (NON
5314     * disabilitiamo l'add). Esaurito SOLO se è esplicitamente <= 0. */
5315    var rawStock = parseFloat(p.WAREHOUSE_STOCK);
5316    var stockKnown = isFinite(rawStock);
5317    var stock = stockKnown ? rawStock : 1; /* unknown → consideriamo disponibile */
5318    var lowStock = stockKnown && stock > 0 && stock < 20;
5319    var outStock = stockKnown && stock <= 0;
5320    var hasPrice = price > 0;
5321    var fmtPrice = function (n) {
5322      return "€" + (Math.round(n * 100) / 100).toFixed(2).replace(".", ",");
5323    };
5324
5325    var card = document.createElement("div");
5326    card.className = "caf-prod-card";
5327    card.style.setProperty("--caf-prod-c", c);
5328
5329    /* image */
5330    var imgWrap = document.createElement("div");
5331    imgWrap.className = "caf-prod-img-wrap";
5332    if (hasPromo) {
5333      var badge = document.createElement("div");
5334      badge.className = "caf-prod-promo-badge";
5335      badge.textContent = "-" + savings + "%";
5336      imgWrap.appendChild(badge);
5337    }
5338    if (outStock) {
5339      var oo = document.createElement("div");
5340      oo.className = "caf-prod-stock-badge out";
5341      oo.textContent = "Esaurito";
5342      imgWrap.appendChild(oo);
5343    } else if (lowStock) {
5344      var ls = document.createElement("div");
5345      ls.className = "caf-prod-stock-badge low";
5346      ls.textContent = "Ultimi pezzi";
5347      imgWrap.appendChild(ls);
5348    }
5349    var img = document.createElement("img");
5350    img.className = "caf-prod-img";
5351    img.loading = "lazy";
5352    img.alt = p.ITEM || "";
5353    img.src = p.IMAGE_URL || "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Crect fill='%23f1f5f9' width='100' height='100'/%3E%3Ctext x='50' y='55' text-anchor='middle' fill='%2394a3b8' font-size='12'%3ENo image%3C/text%3E%3C/svg%3E";
5354    img.addEventListener("error", function () {
5355      img.src = "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Crect fill='%23f1f5f9' width='100' height='100'/%3E%3Cpath d='M30 35h40v30h-40z' fill='%23cbd5e1'/%3E%3Ccircle cx='42' cy='45' r='3' fill='%2394a3b8'/%3E%3Cpath d='M30 60l12-10 8 6 10-8 10 7v10h-40z' fill='%2394a3b8'/%3E%3C/svg%3E";
5356    });
5357    imgWrap.appendChild(img);
5358    card.appendChild(imgWrap);
5359
5360    /* body */
5361    var body = document.createElement("div");
5362    body.className = "caf-prod-body";
5363
5364    if (p.MASTER_CATEGORY) {
5365      var cat = document.createElement("div");
5366      cat.className = "caf-prod-cat";
5367      cat.textContent = p.MASTER_CATEGORY;
5368      body.appendChild(cat);
5369    }
5370
5371    var title = document.createElement("div");
5372    title.className = "caf-prod-title";
5373    title.textContent = p.ITEM || "Prodotto";
5374    body.appendChild(title);
5375
5376    /* meta row: pieces per pack + per-piece price */
5377    var meta = document.createElement("div");
5378    meta.className = "caf-prod-meta";
5379    if (perPack > 0) {
5380      var pcs = document.createElement("span");
5381      pcs.className = "caf-prod-pcs";
5382      pcs.innerHTML = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" 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> ' + perPack + " pz";
5383      meta.appendChild(pcs);
5384    }
5385    if (perPiece > 0) {
5386      var pp = document.createElement("span");
5387      pp.className = "caf-prod-perpiece";
5388      pp.textContent = fmtPrice(perPiece) + "/pz";
5389      meta.appendChild(pp);
5390    }
5391    if (meta.children.length) body.appendChild(meta);
5392
5393    /* prices row — solo se abbiamo un prezzo valido */
5394    if (hasPrice) {
5395      var prow = document.createElement("div");
5396      prow.className = "caf-prod-prow";
5397      var pmain = document.createElement("span");
5398      pmain.className = "caf-prod-price";
5399      pmain.textContent = fmtPrice(displayPrice);
5400      prow.appendChild(pmain);
5401      if (hasPromo) {
5402        var pold = document.createElement("span");
5403        pold.className = "caf-prod-price-old";
5404        pold.textContent = fmtPrice(price);
5405        prow.appendChild(pold);
5406      }
5407      body.appendChild(prow);
5408    }
5409
5410    /* description (collapsible) */
5411    if (p.DESCRIPTION) {
5412      var desc = document.createElement("div");
5413      desc.className = "caf-prod-desc";
5414      desc.textContent = String(p.DESCRIPTION).replace(/[\r\n]+/g, " ").replace(/\s+/g, " ").trim();
5415      body.appendChild(desc);
5416    }
5417
5418    /* CTA: + and qty + Aggiungi */
5419    var cta = document.createElement("div");
5420    cta.className = "caf-prod-cta";
5421    var qtyBox = document.createElement("div");
5422    qtyBox.className = "caf-prod-qty";
5423    var minus = document.createElement("button");
5424    minus.type = "button"; minus.className = "caf-prod-qty-btn"; minus.setAttribute("aria-label", bkT("aria_remove"));
5425    minus.textContent = "−";
5426    var qtyInp = document.createElement("input");
5427    qtyInp.type = "text"; qtyInp.inputMode = "numeric"; qtyInp.className = "caf-prod-qty-inp"; qtyInp.value = "1";
5428    var plus = document.createElement("button");
5429    plus.type = "button"; plus.className = "caf-prod-qty-btn"; plus.setAttribute("aria-label", bkT("aria_add"));
5430    plus.textContent = "+";
5431    qtyBox.appendChild(minus); qtyBox.appendChild(qtyInp); qtyBox.appendChild(plus);
5432    cta.appendChild(qtyBox);
5433
5434    var addBtn = document.createElement("button");
5435    addBtn.type = "button"; addBtn.className = "caf-prod-add";
5436    addBtn.disabled = outStock;
5437    addBtn.innerHTML = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.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.7 13.4a2 2 0 0 0 2 1.6h9.7a2 2 0 0 0 2-1.6L23 6H6"/></svg><span>' + (outStock ? "Esaurito" : "Aggiungi") + "</span>";
5438    cta.appendChild(addBtn);
5439
5440    body.appendChild(cta);
5441    card.appendChild(body);
5442
5443    /* interactions */
5444    minus.addEventListener("click", function () {
5445      var v = Math.max(1, (parseInt(qtyInp.value, 10) || 1) - 1);
5446      qtyInp.value = String(v);
5447    });
5448    plus.addEventListener("click", function () {
5449      var v = Math.max(1, (parseInt(qtyInp.value, 10) || 1) + 1);
5450      qtyInp.value = String(v);
5451    });
5452    qtyInp.addEventListener("input", function () {
5453      qtyInp.value = qtyInp.value.replace(/[^0-9]/g, "").slice(0, 4) || "1";
5454    });
5455    addBtn.addEventListener("click", function () {
5456      if (outStock) return;
5457      var qty = Math.max(1, parseInt(qtyInp.value, 10) || 1);
5458      self._ecommerceCart = self._ecommerceCart || [];
5459      self._ecommerceCart.push({ id: p.ID, code: p.CODE, name: p.ITEM, qty: qty, price: displayPrice });
5460      /* feedback visivo */
5461      addBtn.classList.add("added");
5462      var lbl = addBtn.querySelector("span");
5463      var prev = lbl ? lbl.textContent : "Aggiungi";
5464      if (lbl) lbl.textContent = "✓ Aggiunto";
5465      setTimeout(function () {
5466        addBtn.classList.remove("added");
5467        if (lbl) lbl.textContent = prev;
5468      }, 1400);
5469      /* hook esterno (opzionale): l'host può ascoltare l'evento */
5470      try {
5471        window.dispatchEvent(new CustomEvent("caf-product-add", {
5472          detail: { id: p.ID, code: p.CODE, name: p.ITEM, qty: qty, price: displayPrice, raw: p }
5473        }));
5474      } catch (_e) {}
5475    });
5476
5477    /* expand/collapse description on click */
5478    if (p.DESCRIPTION) {
5479      title.style.cursor = "pointer";
5480      title.setAttribute("role", "button");
5481      title.setAttribute("tabindex", "0");
5482      title.setAttribute("aria-expanded", "false");
5483      var _tglDesc = function () {
5484        var _ex = card.classList.toggle("expanded");
5485        title.setAttribute("aria-expanded", _ex ? "true" : "false");
5486      };
5487      title.addEventListener("click", _tglDesc);
5488      title.addEventListener("keydown", function (ev) {
5489        if (ev.key === "Enter" || ev.key === " " || ev.key === "Spacebar") { ev.preventDefault(); _tglDesc(); }
5490      });
5491    }
5492
5493    return card;
5494  };
5495
5496  CafChat.prototype._init = function () {
5497    /* host element */
5498    this.host = document.createElement("div");
5499    this.host.id = "caf-chat-inline-root";
5500    this.host.style.cssText = "position:fixed;z-index:" + this.opts.zIndex + ";top:0;left:0;width:0;height:0;pointer-events:none;";
5501    document.body.appendChild(this.host);
5502
5503    /* ── Ecommerce mode: AUTO-DETECT.
5504     * Tentiamo sempre il fetch del listino prodotti. Se il server lo
5505     * serve (HTTP 200) → ecommerce mode attivo automaticamente. Se
5506     * risponde 403 (env ECOMMERCE non impostato) → modalità chat
5507     * standard, nessun errore. Niente più dipendenza da
5508     * data-ecommerce sul tag <script>. */
5509    this._ecommerceProducts = null;
5510    this._ecommerceProductIndex = null;
5511    this._ecommerceEnabled = false;
5512    var _ecSelf = this;
5513    var _ecUrl = this.opts.productsUrl || (this.opts.baseUrl + "/api/products");
5514    fetch(_ecUrl, { headers: { Accept: "application/json" } })
5515      .then(function (r) {
5516        if (r.status === 403) {
5517          /* Server dice "ecommerce non attivo" — modalità chat standard. */
5518          return null;
5519        }
5520        if (!r.ok) {
5521          console.warn("[CafChat] /api/products HTTP " + r.status + " — modalità chat standard.");
5522          return null;
5523        }
5524        return r.json();
5525      })
5526      .then(function (data) {
5527        if (!data || !Array.isArray(data.rows) || !data.rows.length) return;
5528        _ecSelf._ecommerceProducts = data.rows;
5529        _ecSelf._ecommerceEnabled = true;
5530        var idx = {};
5531        var catSet = {};
5532        data.rows.forEach(function (p) {
5533          if (p.ID) idx["ID:" + String(p.ID).trim()] = p;
5534          if (p.CODE) idx["CODE:" + String(p.CODE).trim()] = p;
5535          if (p.MASTER_CATEGORY) catSet[String(p.MASTER_CATEGORY).trim()] = true;
5536          if (p.CATEGORY) catSet[String(p.CATEGORY).trim()] = true;
5537        });
5538        _ecSelf._ecommerceProductIndex = idx;
5539        _ecSelf._ecommerceCategories = Object.keys(catSet)
5540          .filter(function (k) { return k && k.length >= 4; })
5541          .sort(function (a, b) { return b.length - a.length; });
5542        console.info("[CafChat] ecommerce mode attivo: " + data.rows.length + " prodotti, " + _ecSelf._ecommerceCategories.length + " categorie.");
5543        /* Re-render: se ci sono già messaggi nel DOM, le card/caroselli appaiono. */
5544        if (_ecSelf._renderMessages) _ecSelf._renderMessages();
5545      })
5546      .catch(function (e) {
5547        console.warn("[CafChat] /api/products fetch failed:", e && e.message ? e.message : e);
5548      });
5549
5550    /* Abandonment recovery: avvisa l'utente prima di chiudere se ha
5551     * una prenotazione in corso; logga evento al server. */
5552    var _self=this;
5553    window.addEventListener("beforeunload",function(ev){
5554      var midFlow=_self.bkStep&&["selectProvincia","selectServizio","selectSede","selectSlot","collectData","confirm"].indexOf(_self.bkStep)>=0;
5555      if(midFlow){
5556        try{_self._bkLogEvent&&_self._bkLogEvent("abandonment_detected",{step:_self.bkStep});}catch(_e){}
5557        ev.preventDefault();ev.returnValue="";return"";
5558      }
5559    });
5560
5561    /* Annual 730 reminder: se utente ha prenotato 730 negli ultimi 14 mesi
5562     * e siamo nella stagione 730 (mar/apr/mag/giu), mostra prompt. */
5563    try{
5564      var hist=loadBookingHistory();var now=new Date();var month=now.getMonth(); /* 0-11 */
5565      var inSeason=month>=2&&month<=5; /* marzo-giugno */
5566      if(inSeason&&hist&&hist.length){
5567        for(var hi=0;hi<hist.length;hi++){
5568          var b=hist[hi];if(!b||!b.servizio)continue;
5569          var srvDesc=(b.servizio.descrizione||b.servizio.desonline||"").toLowerCase();
5570          if(srvDesc.indexOf("730")<0)continue;
5571          var ageMs=Date.now()-(b.when||0);var ageMonths=ageMs/(1000*60*60*24*30);
5572          if(ageMonths>=10&&ageMonths<=14){
5573            this._bkAnnualReminder=b;break;
5574          }
5575        }
5576      }
5577    }catch(_e){}
5578
5579    /* #43 — auto-clear dati cliente stale (>30gg) + migrazione legacy keys */
5580    try{bkMigrateLegacyClientKeys();bkAutoClearStaleClient();}catch(_e){}
5581
5582    /* shadow DOM */
5583    this.shadow = this.host.attachShadow({ mode: "open" });
5584
5585    /* styles */
5586    var style = document.createElement("style");
5587    style.textContent = buildCSS(this.opts);
5588    this.shadow.appendChild(style);
5589
5590    /* wrapper */
5591    this.wrapper = document.createElement("div");
5592    this.wrapper.setAttribute("lang", BK_LANG || "it");
5593    this.shadow.appendChild(this.wrapper);
5594
5595    /* load messages */
5596    this._loadMessages();
5597
5598    /* build UI */
5599    this._buildBubble();
5600    this._buildToggle();
5601    this._buildPanel();
5602    this._renderMessages();
5603
5604    /* events */
5605    var self = this;
5606    document.addEventListener("click", function () {
5607      if (self.menuOpen) {
5608        self.menuOpen = false;
5609        self._updateMenu();
5610      }
5611    });
5612
5613    /* Escape to close */
5614    document.addEventListener("keydown", function (e) {
5615      if (e.key === "Escape" && self.isOpen) {
5616        /* 1.4.13: ESC chiude PRIMA eventuali popover/picker transitori aperti
5617         * (selettore lingua, popover documenti, prefisso telefonico) senza
5618         * chiudere l'intero pannello. */
5619        if (self.shadow) {
5620          var _pops = self.shadow.querySelectorAll(".caf-lang-picker,.caf-bk-docs-pop,.caf-bk-tel-cc-pop");
5621          if (_pops && _pops.length) {
5622            for (var _pi = 0; _pi < _pops.length; _pi++) { try { if (_pops[_pi]._cafCleanup) _pops[_pi]._cafCleanup(); _pops[_pi].remove(); } catch (_e) {} }
5623            return;
5624          }
5625        }
5626        /* Durante il booking fullscreen Escape e' gestito da _bkFsEscHandler
5627         * (chiude SOLO l'overlay, con ripristino del focus): qui non dobbiamo
5628         * chiudere l'intero pannello — prima i due handler correvano entrambi
5629         * e l'utente perdeva chat e wizard insieme (WCAG 3.2.1). */
5630        if (self._bkFsMode) return;
5631        /* NON chiudere durante l'invio prenotazione (non idempotente, no
5632         * auto-retry): chiudere il pannello abbandonerebbe la POST "prenota" in
5633         * volo → prenotazione creata lato gestionale e persa lato client. */
5634        if (self.bkStep === "submitting") return;
5635        self.close();
5636      }
5637    });
5638
5639    /* visualViewport & TASTIERA — MODELLO "NESSUN PIN" (round 3).
5640     * Storia (ERROREMOLTOGRAVE_IPHONE, ERRORECEANCORA): le versioni
5641     * precedenti pinnavano il pannello alla quota del visual viewport
5642     * (height, poi top+occlusione) per tenerlo incollato all'area visibile
5643     * con la tastiera aperta. Su iOS non puo' essere affidabile: iOS 26.0 ha
5644     * un bug DI SISTEMA confermato da Apple (WebKit #297779: vv.height
5645     * stabilmente minore del reale, offsetTop bloccato a ~24px dopo la
5646     * chiusura, oscillazioni ~10px al cambio direzione di scroll) e gia'
5647     * prima il vv escludeva porzioni variabili di UI Safari (pillola URL,
5648     * barra accessori): OGNI pin basato su vv lasciava una striscia tra
5649     * input e tastiera (pagina host o vuoto).
5650     *
5651     * Modello attuale: NESSUN PIN. Il pannello fullscreen resta inset:0 /
5652     * 100dvh e copre SEMPRE l'intero layout viewport: qualunque pan della
5653     * tastiera puo' mostrare solo pannello, mai la pagina host (l'area
5654     * visibile e' per costruzione un sottoinsieme del layout viewport).
5655     * A portare l'input sopra la tastiera e' il reveal NATIVO del browser
5656     * sull'elemento focused: l'unico meccanismo che conosce la geometria
5657     * VERA della tastiera, indipendente dai valori visualViewport.
5658     * Qui resta solo: (a) chat ancorata all'ultimo messaggio all'apertura
5659     * tastiera, con guard scale!=1 per non scroll-jackare il pinch-zoom
5660     * (WCAG 1.4.4); (b) pulizia difensiva di quote inline residue di
5661     * versioni precedenti col pin. */
5662    if (window.visualViewport) {
5663      window.visualViewport.addEventListener("resize", function () {
5664        if (!self.isOpen) return;
5665        if (!self.panelEl.classList.contains("caf-fullscreen")) return;
5666        var vv = window.visualViewport;
5667        if (self.panelEl.style.height || self.panelEl.style.top || self.panelEl.style.borderBottom || self.panelEl.style.transition) {
5668          self.panelEl.style.transition = "";
5669          self.panelEl.style.height = "";
5670          self.panelEl.style.top = "";
5671          self.panelEl.style.borderBottom = "";
5672        }
5673        var scaled = Math.abs((vv.scale || 1) - 1) > 0.01;
5674        var kbOpen = !scaled && (window.innerHeight - vv.height) > 80;
5675        if (kbOpen) self._scrollToBottom();
5676      });
5677    }
5678
5679    /* Overlay diagnostico viewport (SOLO con flag esplicito: "cafvvdbg=1"
5680     * nell'URL della pagina host oppure window.__CAF_VV_DEBUG__ === true).
5681     * Mostra live innerHeight / visualViewport / rect del pannello con un
5682     * marcatore di versione: serve a MISURARE dal dispositivo reale i bug
5683     * di geometria iOS (WebKit #297779) da uno screenshot, senza devtools.
5684     * Nessun dato utente, nessuno storage, mai attivo di default. */
5685    var _vvDbgOn = false;
5686    try {
5687      _vvDbgOn = window.__CAF_VV_DEBUG__ === true || /[?#&]cafvvdbg=1/.test(String(location.href));
5688    } catch (_e) {}
5689    if (_vvDbgOn) {
5690      var _vvDbgEl = document.createElement("div");
5691      _vvDbgEl.className = "caf-vv-debug";
5692      _vvDbgEl.setAttribute("aria-hidden", "true");
5693      _vvDbgEl.style.cssText = "position:absolute;top:6px;right:6px;z-index:2147483647;background:rgba(0,0,0,.78);color:#7cfc00;font:11px/1.45 monospace;padding:6px 9px;border-radius:8px;pointer-events:none;white-space:pre;text-align:left";
5694      self.panelEl.appendChild(_vvDbgEl);
5695      setInterval(function () {
5696        try {
5697          var v = window.visualViewport;
5698          var r = self.panelEl.getBoundingClientRect();
5699          _vvDbgEl.textContent =
5700            "kb3-20260720" +
5701            "\niH  " + window.innerHeight +
5702            "\nvvH " + (v ? Math.round(v.height) : "-") +
5703            "\nvvT " + (v ? Math.round(v.offsetTop) : "-") +
5704            "\nsc  " + (v ? (v.scale || 1).toFixed(2) : "-") +
5705            "\nsY  " + Math.round(window.scrollY || 0) +
5706            "\npnl " + Math.round(r.top) + " / " + Math.round(r.bottom) +
5707            "\nae  " + ((self.shadow && self.shadow.activeElement && self.shadow.activeElement.tagName) || "-");
5708        } catch (_e) {}
5709      }, 300);
5710    }
5711
5712    /* #50/#51 — Online/offline + slow-network: re-render booking quando
5713     * cambia lo stato di rete così il banner appare/sparisce in tempo reale. */
5714    var refreshNet = function() {
5715      if (self._bkFsMode && self._bkRenderFs) self._bkRenderFs();
5716      else if (self.bkStep && self._bkRender) self._bkRender();
5717    };
5718    window.addEventListener("online", refreshNet);
5719    window.addEventListener("offline", refreshNet);
5720    try {
5721      var conn = navigator.connection || navigator.mozConnection || navigator.webkitConnection;
5722      if (conn && conn.addEventListener) conn.addEventListener("change", refreshNet);
5723    } catch (_e) { /* unsupported */ }
5724  };
5725
5726  /* ── persistence ──────────────────────────────────────────── */
5727  CafChat.prototype._loadMessages = function () {
5728    if (this.opts.privacy) {
5729      this.messages = [
5730        { id: "welcome", role: "assistant", content: timeAwareGreeting()+"! "+(this.opts.welcome||"")+((this.opts.aiDisclosure!==false)?("\n\n"+bkT("ai_disclosure")):"") },
5731      ];
5732      return;
5733    }
5734    try {
5735      var raw = localStorage.getItem(this.STORAGE_KEY);
5736      if (raw) {
5737        var stored = JSON.parse(raw);
5738        /* Retro-compat: legacy = array nudo; nuovo = {ts, msgs}. */
5739        var parsed = Array.isArray(stored)
5740          ? stored
5741          : (stored && Array.isArray(stored.msgs) ? stored.msgs : null);
5742        /* TTL cronologia chat (GDPR art. 5(1)(e)): scaduta -> scarta. */
5743        if (stored && !Array.isArray(stored) && typeof stored.ts === "number" &&
5744            (Date.now() - stored.ts) > CHAT_HISTORY_TTL_MS) {
5745          try { localStorage.removeItem(this.STORAGE_KEY); } catch (_e) {}
5746          parsed = null;
5747        }
5748        if (parsed && parsed.length > 0) {
5749          /* Defense in depth: utenti che hanno usato il widget prima del fix
5750           * sentinel-parser possono avere `<<<MANTIS_*>>>` salvato in
5751           * localStorage. Lo ripuliamo al caricamento così non viene mai
5752           * mostrato all'utente. */
5753          for (var pi = 0; pi < parsed.length; pi++) {
5754            var pm = parsed[pi];
5755            if (pm && typeof pm.content === "string" &&
5756                (pm.content.indexOf("<<<MANTIS_") !== -1 || /\[F\d/.test(pm.content))) {
5757              try { pm.content = parseMantisSentinels(pm.content).clean; }
5758              catch (_e) { /* leave as-is on error */ }
5759            }
5760          }
5761          /* AI Act art. 50(1): utenti di RITORNO con cronologia salvata da
5762           * versioni pre-disclosure devono ricevere la dichiarazione di
5763           * natura IA anche a livello di messaggi (non solo badge/footer). */
5764          if (this.opts.aiDisclosure !== false) {
5765            var _hasDiscl = false;
5766            for (var _di = 0; _di < parsed.length; _di++) {
5767              var _dmm = parsed[_di];
5768              if (_dmm && _dmm.role === "assistant" && (_dmm.id === "ai-disclosure" ||
5769                  (typeof _dmm.content === "string" && _dmm.content.indexOf(bkT("ai_disclosure")) !== -1))) {
5770                _hasDiscl = true; break;
5771              }
5772            }
5773            if (!_hasDiscl) {
5774              parsed.push({ id: "ai-disclosure", role: "assistant", content: bkT("ai_disclosure"), timestamp: Date.now() });
5775            }
5776          }
5777          this.messages = parsed;
5778          try { this._saveMessages(); } catch (_e) {}
5779          return;
5780        }
5781      }
5782    } catch (e) {
5783      localStorage.removeItem(this.STORAGE_KEY);
5784    }
5785    this.messages = [
5786      { id: "welcome", role: "assistant", content: timeAwareGreeting()+"! "+(this.opts.welcome||"")+((this.opts.aiDisclosure!==false)?("\n\n"+bkT("ai_disclosure")):"") },
5787    ];
5788    this._maybePushAnnualReminder();
5789  };
5790
5791  /** Pushes a re-engagement message in chat if user has booked 730 ~12 months ago. */
5792  CafChat.prototype._maybePushAnnualReminder = function(){
5793    if(!this._bkAnnualReminder)return;
5794    /* skip se già visto in questa sessione */
5795    try{var _ark=nsKey("caf-annual-reminder-shown");if(sessionStorage.getItem(_ark))return;sessionStorage.setItem(_ark,"1");}catch(_e){}
5796    var b=this._bkAnnualReminder;var lastDate=(b.slot&&b.slot.data)||"";
5797    var lastDateStr=fmtDate(lastDate);
5798    var msg="📅 L'anno scorso hai prenotato il **730**"+(lastDateStr?" il "+lastDateStr:"")+". Vuoi rifarlo anche quest'anno? Posso aiutarti subito.";
5799    this.messages.push({id:quickId(),role:"assistant",content:msg,timestamp:Date.now()});
5800    this._bkLogEvent&&this._bkLogEvent("annual_reminder_shown",{lastBookingId:b.bookingId||null});
5801  };
5802
5803  CafChat.prototype._saveMessages = function () {
5804    if (this.opts.privacy) return;
5805    var self = this;
5806    if (self._saveTimeout) clearTimeout(self._saveTimeout);
5807    self._saveTimeout = setTimeout(function () {
5808      try {
5809        /* @@BOOKING_LAUNCH@@ è salvato in localStorage di proposito: dopo
5810         * un refresh _renderMessages ricrea il bottone dal marker, così
5811         * la cronologia chat resta coerente. */
5812        var toSave = self.messages.filter(function(m){return m.content!=="@@BOOKING@@"&&m.content!=="@@PWDRESET@@";});
5813        if (toSave.length > 100) {
5814          var welcome = toSave[0] && toSave[0].id === "welcome" ? [toSave[0]] : [];
5815          toSave = welcome.concat(toSave.slice(-99));
5816        }
5817        localStorage.setItem(self.STORAGE_KEY, JSON.stringify({ ts: Date.now(), msgs: toSave }));
5818      } catch (e) { /* quota */ }
5819    }, 500);
5820  };
5821
5822  /* ── conversation id ─────────────────────────────────────── */
5823  /* Retention (GDPR art. 5(1)(e)): l'id conversazione ruota oltre questa eta'
5824   * assoluta cosi' il documento server smette di essere aggiornato e rientra
5825   * nella purge; la cronologia chat locale scade sull'inattivita'. */
5826  var CONV_ID_MAX_AGE_MS = 90 * 24 * 60 * 60 * 1000;
5827  var CHAT_HISTORY_TTL_MS = 90 * 24 * 60 * 60 * 1000;
5828  CafChat.prototype._loadConversationId = function () {
5829    if (this.opts.privacy) {
5830      /* ephemeral, in-memory only: no persistence, no cross-session tracking */
5831      return "anon_" + quickId() + quickId();
5832    }
5833    try {
5834      var id = localStorage.getItem(this.CONV_ID_KEY);
5835      if (id) {
5836        var m = /^conv_(\d+)_/.exec(id);
5837        var fresh = true;
5838        if (m) {
5839          var ageMs = Date.now() - parseInt(m[1], 10);
5840          if (isFinite(ageMs) && ageMs > CONV_ID_MAX_AGE_MS) fresh = false;
5841        }
5842        if (fresh) return id;
5843        try { localStorage.removeItem(this.CONV_ID_KEY); } catch (_e) {}
5844      }
5845    } catch (e) { /* ignore */ }
5846    /* NON persistere all'init (ePrivacy art. 5(3)): id in memoria; sara'
5847     * persistito da _persistConversationId() dopo il gate consenso. */
5848    return this._newConversationId(true);
5849  };
5850
5851  CafChat.prototype._newConversationId = function (ephemeral) {
5852    if (this.opts.privacy) {
5853      return "anon_" + quickId() + quickId();
5854    }
5855    var id = "conv_" + Date.now() + "_" + quickId();
5856    if (!ephemeral) {
5857      try {
5858        localStorage.setItem(this.CONV_ID_KEY, id);
5859      } catch (e) { /* ignore */ }
5860    }
5861    return id;
5862  };
5863
5864  CafChat.prototype._persistConversationId = function () {
5865    if (this.opts.privacy || !this.conversationId) return;
5866    try { localStorage.setItem(this.CONV_ID_KEY, this.conversationId); } catch (e) { /* ignore */ }
5867  };
5868
5869  /* ── email persistence ────────────────────────────────────── */
5870  CafChat.prototype._loadEmail = function () {
5871    if (this.opts.privacy) return "";
5872    try {
5873      return localStorage.getItem(this.EMAIL_KEY) || "";
5874    } catch (e) { return ""; }
5875  };
5876
5877  CafChat.prototype._saveEmail = function (email) {
5878    this.userEmail = email;
5879    if (this.opts.privacy) return;
5880    try { localStorage.setItem(this.EMAIL_KEY, email); } catch (e) { /* ignore */ }
5881  };
5882
5883  /* ── sync conversation: DUAL WRITE (fire-and-forget) ──────
5884   *
5885   * Scrive su Firestore tramite DUE percorsi indipendenti, idempotenti:
5886   *
5887   * 1. SERVER-SIDE: POST `<baseUrl>/api/conversations` — il backend Next.js
5888   *    scrive su Firestore via REST API. Vantaggi: nessuna restrizione di
5889   *    dominio sull'API key, controllo Security Rules server-side.
5890   *
5891   * 2. CLIENT-SIDE: PATCH diretto a Firestore REST API dal browser. Funziona
5892   *    anche se `/api/conversations` non è ancora deployato sul backend
5893   *    (es. solo modifiche locali non in produzione).
5894   *
5895   * Entrambi sono PATCH idempotenti con stesso conversationId: la doppia
5896   * scrittura non duplica nulla. Almeno una delle due quasi certamente
5897   * andrà a buon fine, garantendo persistenza affidabile.
5898   *
5899   * In privacy mode, NULLA viene inviato.
5900   * Schema: conversazioniPubbliche/{convId} + /messaggi/{msgId}
5901   */
5902  CafChat.prototype._syncConversation = function () {
5903    if (this.opts.privacy) return;
5904    /* Nessuna persistenza finche' il gate consenso non e' superato. */
5905    if (this.opts.consenso && this._needsConsent && this._needsConsent()) return;
5906    if (!this.conversationId) return;
5907    /* Consenso superato: ora e' lecito persistere l'id conversazione
5908     * (all'init resta solo in memoria per non scrivere pre-consenso). */
5909    this._persistConversationId();
5910
5911    var self = this;
5912    var msgs = this.messages.filter(function (m) {
5913      if (!m || !m.content || typeof m.content !== "string") return false;
5914      if (m.content === "@@BOOKING@@") return false;
5915      if (m.content === "@@PWDRESET@@") return false;
5916      if (m.content.indexOf("@@BOOKING_LAUNCH@@") === 0) return false;
5917      return m.role === "user" || m.role === "assistant";
5918    }).map(function (m) {
5919      var _sm = {
5920        id: m.id,
5921        role: m.role,
5922        content: m.content,
5923        timestamp: m.timestamp || Date.now()
5924      };
5925      /* AI Act art. 50(2): il modello viaggia con il messaggio assistant. */
5926      if (m.role === "assistant" && m.model) _sm.model = String(m.model);
5927      return _sm;
5928    });
5929    if (msgs.length === 0) return;
5930
5931    var nowIso = new Date().toISOString();
5932    var createdAtIso = nowIso;
5933    var ct = /^conv_(\d+)_/.exec(self.conversationId);
5934    if (ct) {
5935      var tMs = parseInt(ct[1], 10);
5936      if (isFinite(tMs) && tMs > 0) {
5937        try { createdAtIso = new Date(tMs).toISOString(); } catch (_e) {}
5938      }
5939    }
5940    /* GDPR minimizzazione: solo origin+pathname, mai query/hash (token/PII). */
5941    var pageUrl = "";
5942    try { pageUrl = (typeof location !== "undefined") ? (location.origin + (location.pathname || "")) : ""; } catch (_pe) { pageUrl = ""; }
5943
5944    /* ─── 1. SERVER-SIDE write (preferito) ─── */
5945    try {
5946      var apiUrl = (this.opts.baseUrl || "") + "/api/conversations";
5947      fetch(apiUrl, {
5948        method: "POST",
5949        headers: { "Content-Type": "application/json" },
5950        body: JSON.stringify({
5951          conversationId: self.conversationId,
5952          messages: msgs,
5953          metadata: {
5954            pageUrl: pageUrl,
5955            source: "widget-inline"
5956          }
5957        }),
5958        keepalive: true
5959      }).then(function (r) {
5960        if (!r.ok) console.warn("[widget] server-side /api/conversations HTTP " + r.status);
5961      }).catch(function (e) {
5962        console.warn("[widget] server-side /api/conversations failed:", e && e.message);
5963      });
5964    } catch (e1) {
5965      console.warn("[widget] server-side call exception:", e1 && e1.message);
5966    }
5967
5968    /* Ramo client-side diretto a Firestore RIMOSSO (2026-07-17) per conformita'
5969     * GDPR: esponeva la apiKey nel bundle e scriveva senza il percorso di
5970     * consenso server-side. La persistenza usa solo POST /api/conversations. */
5971  };
5972
5973  /* ── bubble ───────────────────────────────────────────────── */
5974  CafChat.prototype._BUBBLE_DEFAULT_HTML = '<span class="caf-wave-emoji">👋</span> Assistente IA · posso aiutarti?';
5975  CafChat.prototype._BUBBLE_BOOKING_HTML = '<span class="caf-wave-emoji">📅</span> Prenota un appuntamento <span class="caf-bk-arrow">→</span>';
5976
5977  CafChat.prototype._buildBubble = function () {
5978    var self = this;
5979    this.bubbleEl = document.createElement("div");
5980    this.bubbleEl.className = "caf-bubble caf-pos-" + this.opts.position;
5981    /* Lasciamo il contenuto vuoto fino al check prenotazione: se prenotazione
5982     * è abilitata il primo messaggio deve essere quello di prenotazione, non
5983     * il default. Manteniamo lo spazio invisibile per evitare layout shift. */
5984    this.bubbleEl.innerHTML = this._BUBBLE_DEFAULT_HTML;
5985    this.bubbleEl.style.pointerEvents = "auto";
5986    if (!this.opts.showBubble) {
5987      this.bubbleEl.style.display = "none";
5988    } else {
5989      /* nascondiamo finché il check booking non risolve, così l'utente
5990       * non vede mai prima "ciao" e poi "prenota". */
5991      this.bubbleEl.style.visibility = "hidden";
5992    }
5993    this.bubbleEl.addEventListener("click", function () {
5994      self.open();
5995    });
5996    this.wrapper.appendChild(this.bubbleEl);
5997
5998    /* Fallback: se il check booking non risponde entro 1.5s, mostra il
5999     * default per non lasciare la bubble nascosta a tempo indeterminato. */
6000    var revealedByFallback = false;
6001    var fallbackTid = setTimeout(function () {
6002      if (!self.bubbleEl) return;
6003      if (self.bubbleEl.style.visibility === "hidden") {
6004        self.bubbleEl.innerHTML = self._BUBBLE_DEFAULT_HTML;
6005        if (self.opts.showBubble) self.bubbleEl.style.visibility = "";
6006        revealedByFallback = true;
6007      }
6008    }, 1500);
6009
6010    /* Alternanza messaggio bubble (solo se prenotazione abilitata).
6011     * Il check `_bkCheckEnabled()` torna true quando il backend espone
6012     * /api/planner/province (cioè NEXT_PUBLIC_PRENOTAZIONE=SI lato server). */
6013    self._bkCheckEnabled().then(function (enabled) {
6014      clearTimeout(fallbackTid);
6015      if (!self.bubbleEl) return;
6016      if (enabled) {
6017        self.bubbleEl.classList.add("caf-bk-active");
6018        if (self.toggleEl) self.toggleEl.classList.add("caf-bk-active");
6019        self._bubbleMessages = self._buildBubbleMessages();
6020        /* _startBubbleAlternation imposta innerHTML = msgs[0] (BOOKING).
6021         * Se il fallback ha già rivelato "ciao", lo sovrascriviamo qui. */
6022        self._startBubbleAlternation();
6023        self._bkActivateProactiveCues();
6024      } else if (!revealedByFallback) {
6025        self.bubbleEl.innerHTML = self._BUBBLE_DEFAULT_HTML;
6026      }
6027      if (self.opts.showBubble) self.bubbleEl.style.visibility = "";
6028    }).catch(function () {
6029      clearTimeout(fallbackTid);
6030      if (!self.bubbleEl) return;
6031      if (!revealedByFallback) self.bubbleEl.innerHTML = self._BUBBLE_DEFAULT_HTML;
6032      if (self.opts.showBubble) self.bubbleEl.style.visibility = "";
6033    });
6034  };
6035
6036  /* Costruisce l'array di messaggi della bubble in base a contesto:
6037   * booking + default + (returning user / annual reminder / URL keyword).
6038   * BOOKING è in posizione 0 così quando la prenotazione è abilitata il
6039   * primo testo mostrato accanto all'icona è la CTA di prenotazione, non
6040   * il saluto "ciao posso aiutarti". */
6041  CafChat.prototype._buildBubbleMessages = function () {
6042    var msgs = [this._BUBBLE_BOOKING_HTML, this._BUBBLE_DEFAULT_HTML];
6043    /* URL/page keyword contextual */
6044    try {
6045      var hay = ((document.title || "") + " " + (location.pathname || "") + " " + (location.search || "")).toLowerCase();
6046      if (/\b730\b/.test(hay)) msgs.push('<span class="caf-wave-emoji">📋</span> Prenota per il 730 <span class="caf-bk-arrow">→</span>');
6047      else if (/\bisee\b/.test(hay)) msgs.push('<span class="caf-wave-emoji">📋</span> Prenota un appuntamento ISEE <span class="caf-bk-arrow">→</span>');
6048      else if (/\bf24\b/.test(hay)) msgs.push('<span class="caf-wave-emoji">💶</span> Aiuto con F24? Prenota <span class="caf-bk-arrow">→</span>');
6049    } catch (_e) { /* noop */ }
6050    /* Returning user */
6051    try {
6052      var hist = (typeof loadBookingHistory === "function") ? loadBookingHistory() : [];
6053      if (hist && hist.length) {
6054        msgs.push('<span class="caf-wave-emoji">👋</span> Bentornato, nuova prenotazione? <span class="caf-bk-arrow">→</span>');
6055      }
6056    } catch (_e) { /* noop */ }
6057    /* Annual reminder */
6058    if (this._bkAnnualReminder) {
6059      msgs.push('<span class="caf-wave-emoji">🗓️</span> È tempo del 730: prenota <span class="caf-bk-arrow">→</span>');
6060    }
6061    return msgs;
6062  };
6063
6064  /* Avvia (o riavvia) il loop di rotazione testo bubble (N messaggi).
6065   * Idempotente: chiamare due volte resetta il timer.
6066   * Si auto-ferma se il bubble viene nascosto o la chat aperta. */
6067  CafChat.prototype._startBubbleAlternation = function () {
6068    var self = this;
6069    if (window.matchMedia && window.matchMedia("(prefers-reduced-motion:reduce)").matches) {
6070      if (self.bubbleEl) { var _m = self._bubbleMessages && self._bubbleMessages.length ? self._bubbleMessages : [self._BUBBLE_BOOKING_HTML, self._BUBBLE_DEFAULT_HTML]; self.bubbleEl.innerHTML = _m[0]; }
6071      return;
6072    }
6073    if (!self.bubbleEl) return;
6074    if (self._bubbleAltTimer) { clearInterval(self._bubbleAltTimer); self._bubbleAltTimer = null; }
6075    var msgs = self._bubbleMessages && self._bubbleMessages.length
6076      ? self._bubbleMessages
6077      : [self._BUBBLE_BOOKING_HTML, self._BUBBLE_DEFAULT_HTML];
6078    /* a11y #C58 (WCAG 2.2.2): la rotazione automatica non e' infinita — si
6079     * ferma per sempre dopo max 5 cicli completi nella sessione o alla prima
6080     * apertura del pannello; resta il primo messaggio, statico. */
6081    if (self._bubbleAltDone || (self._bubbleAltTicks || 0) >= msgs.length * 5) {
6082      self.bubbleEl.innerHTML = msgs[0];
6083      return;
6084    }
6085    var idx = 0;
6086    self.bubbleEl.innerHTML = msgs[0];
6087    self._bubbleAltTimer = setInterval(function () {
6088      if (!self.bubbleEl || self.isOpen
6089          || self.bubbleEl.style.display === "none") {
6090        clearInterval(self._bubbleAltTimer); self._bubbleAltTimer = null; return;
6091      }
6092      idx = (idx + 1) % msgs.length;
6093      self.bubbleEl.innerHTML = msgs[idx];
6094      self._bubbleAltTicks = (self._bubbleAltTicks || 0) + 1;
6095      if (self._bubbleAltTicks >= msgs.length * 5) {
6096        clearInterval(self._bubbleAltTimer); self._bubbleAltTimer = null;
6097        self.bubbleEl.innerHTML = msgs[0];
6098      }
6099    }, 2400);
6100  };
6101
6102  CafChat.prototype._stopBubbleAlternation = function () {
6103    if (this._bubbleAltTimer) { clearInterval(this._bubbleAltTimer); this._bubbleAltTimer = null; }
6104  };
6105
6106  /* ── Booking proactive cues: wiggle FAB, inactivity shake, badge,
6107   * mobile sticky banner, exit-intent, scroll trigger, auto-open one-shot,
6108   * tab-visibility pause, aria-label aggiornato. Tutto si attiva solo
6109   * quando _bkEnabled === true (chiamato da _buildBubble). ── */
6110  CafChat.prototype._bkActivateProactiveCues = function () {
6111    var self = this;
6112    if (self._bkCuesActive) return;
6113    self._bkCuesActive = true;
6114
6115    /* aria-label più esplicito sulla FAB */
6116    if (self.toggleEl) { self.toggleEl.setAttribute("aria-label", bkT("aria_open_book")); self._toggleLabelClosed = bkT("aria_open_book"); }
6117
6118    /* badge "1 nuovo" finché non si apre la chat in questa sessione */
6119    try {
6120      var seen = sessionStorage.getItem(nsKey("caf-bk-seen")) === "1";
6121      if (!seen && self.badgeEl) {
6122        self.badgeEl.textContent = "1";
6123        self.badgeEl.classList.add("caf-bk-newdot");
6124        self.badgeEl.style.display = "flex";
6125      }
6126    } catch (_e) { /* noop */ }
6127
6128    /* tilt hover già via CSS; wiggle periodico ogni ~22s solo se chiusa */
6129    self._bkWiggleTimer = setInterval(function () {
6130      if (self.isOpen || !self.toggleEl) return;
6131      if (self._bkPaused) return;
6132      self.toggleEl.classList.remove("caf-bk-wiggle");
6133      /* reflow per ri-triggerare animazione */
6134      void self.toggleEl.offsetWidth;
6135      self.toggleEl.classList.add("caf-bk-wiggle");
6136    }, 22000);
6137
6138    /* shake su inattività (30s senza interazione utente) */
6139    var resetInactivity = function () {
6140      if (self._bkInactivityTimer) clearTimeout(self._bkInactivityTimer);
6141      if (self.isOpen) return;
6142      self._bkInactivityTimer = setTimeout(function () {
6143        if (self.isOpen || !self.toggleEl || self._bkPaused) return;
6144        self.toggleEl.classList.remove("caf-bk-shake");
6145        void self.toggleEl.offsetWidth;
6146        self.toggleEl.classList.add("caf-bk-shake");
6147      }, 30000);
6148    };
6149    ["mousemove", "keydown", "scroll", "touchstart"].forEach(function (ev) {
6150      window.addEventListener(ev, resetInactivity, { passive: true });
6151    });
6152    resetInactivity();
6153
6154    /* auto-apertura panel: una volta per sessione, dopo 18s di permanenza,
6155     * solo se l'utente non ha mai aperto la chat in questa sessione. */
6156    try {
6157      var autoFlag = sessionStorage.getItem(nsKey("caf-bk-autoopen")) === "1";
6158      if (!autoFlag) {
6159        self._bkAutoOpenTimer = setTimeout(function () {
6160          if (self.isOpen) return;
6161          /* a11y #15: niente auto-open su mobile (open() <640px fa takeover
6162           * fullscreen + scroll-lock inattesi) ne' con prefers-reduced-motion
6163           * attivo. L'apertura manuale resta identica. */
6164          if (window.innerWidth < 640) return;
6165          if (window.matchMedia && window.matchMedia("(prefers-reduced-motion:reduce)").matches) return;
6166          try { sessionStorage.setItem(nsKey("caf-bk-autoopen"), "1"); } catch (_e) {}
6167          /* apre solo se utente è ancora sulla pagina (visibile);
6168           * open(true) = apertura automatica senza furto di focus. */
6169          if (document.visibilityState === "visible") self.open(true);
6170        }, 18000);
6171      }
6172    } catch (_e) { /* noop */ }
6173
6174    /* exit-intent: mouse esce dal viewport in alto → mostra bubble pulsante */
6175    var exitFired = false;
6176    document.addEventListener("mouseleave", function (e) {
6177      if (exitFired || self.isOpen) return;
6178      if (e.clientY > 0) return; /* solo top */
6179      exitFired = true;
6180      try { sessionStorage.setItem(nsKey("caf-bk-exit"), "1"); } catch (_e) {}
6181      if (self.bubbleEl) {
6182        self.bubbleEl.style.display = "";
6183        self.bubbleEl.innerHTML = '<span class="caf-wave-emoji">⏳</span> Aspetta, prenoto in 1 minuto <span class="caf-bk-arrow">→</span>';
6184        self._stopBubbleAlternation();
6185        /* riprendi rotazione dopo 6s */
6186        setTimeout(function () { if (!self.isOpen) self._startBubbleAlternation(); }, 6000);
6187      }
6188      if (self.toggleEl) {
6189        self.toggleEl.classList.remove("caf-bk-shake");
6190        void self.toggleEl.offsetWidth;
6191        self.toggleEl.classList.add("caf-bk-shake");
6192      }
6193    });
6194
6195    /* scroll trigger: > 70% della pagina → forza visibilità bubble */
6196    var scrollFired = false;
6197    var onScroll = function () {
6198      if (scrollFired || self.isOpen) return;
6199      var doc = document.documentElement;
6200      var scrolled = (window.scrollY || doc.scrollTop) + window.innerHeight;
6201      var total = Math.max(doc.scrollHeight, document.body ? document.body.scrollHeight : 0);
6202      if (total > 0 && scrolled / total >= 0.7) {
6203        scrollFired = true;
6204        if (self.bubbleEl) {
6205          self.bubbleEl.style.display = "";
6206          if (!self._bubbleAltTimer) self._startBubbleAlternation();
6207        }
6208      }
6209    };
6210    window.addEventListener("scroll", onScroll, { passive: true });
6211
6212    /* visibility change: pausa animazioni quando il tab non è in focus */
6213    document.addEventListener("visibilitychange", function () {
6214      var hidden = document.visibilityState !== "visible";
6215      self._bkPaused = hidden;
6216      if (self.wrapper) self.wrapper.classList.toggle("caf-bk-pause-anim", hidden);
6217    });
6218
6219    /* quando l'utente apre la chat (anche manualmente) marca come "visto" */
6220    var origOpen = self.open;
6221    if (origOpen && !origOpen._bkWrapped) {
6222      var wrapped = function () {
6223        try { sessionStorage.setItem(nsKey("caf-bk-seen"), "1"); } catch (_e) {}
6224        if (self.badgeEl) {
6225          self.badgeEl.classList.remove("caf-bk-newdot");
6226          self.badgeEl.style.display = "none";
6227        }
6228        if (self._bkAutoOpenTimer) { clearTimeout(self._bkAutoOpenTimer); self._bkAutoOpenTimer = null; }
6229        return origOpen.apply(self, arguments);
6230      };
6231      wrapped._bkWrapped = true;
6232      self.open = wrapped;
6233    }
6234  };
6235
6236  /* A11y — focus trap centrale a stack (WCAG 2.1.2/2.4.3).
6237   * Il vecchio trap era un listener keydown sull'overlay stesso: agiva solo se
6238   * il focus era GIA' dentro l'overlay (wrap ai bordi first/last). Se il focus
6239   * finiva FUORI (autofocus dell'input chat, re-render, apertura del banner
6240   * con focus altrove) i Tab non transitavano mai per l'overlay e il "modale"
6241   * era attraversabile (bug verificato in CDP: 45 Tab non entravano mai nel
6242   * banner GDPR). Ora un unico listener document-level (capture, key composta)
6243   * governa l'overlay ATTIVO piu' in alto nello stack: se il focus e' fuori,
6244   * il primo Tab lo riporta dentro; se e' dentro, wrappa ai bordi. In piu',
6245   * quando il pannello copre l'intero viewport (mobile fullscreen o
6246   * desktop-fs) il pannello stesso fa da contenitore di ultima istanza: il
6247   * pannello flottante desktop resta volutamente non-modale. */
6248  CafChat.prototype._trapFocusables = function(ov){
6249    var _all=Array.prototype.slice.call(ov.querySelectorAll('a[href],button:not([disabled]),input:not([disabled]),textarea,select,[tabindex]:not([tabindex="-1"])'));
6250    var _notHidden=_all.filter(function(el){return !(el.hidden||(el.closest&&el.closest("[hidden]")));});
6251    var f=_notHidden.filter(function(el){return el.offsetWidth>0||el.offsetHeight>0||(el.getClientRects&&el.getClientRects().length>0);});
6252    if(!f.length)f=_notHidden;
6253    return f;
6254  };
6255  CafChat.prototype._trapIsActive = function(ov){
6256    if(!ov||!ov.isConnected||ov.hidden)return false;
6257    if(!(ov.offsetWidth>0||ov.offsetHeight>0||(ov.getClientRects&&ov.getClientRects().length>0)))return false;
6258    return true;
6259  };
6260  CafChat.prototype._trapTop = function(){
6261    var st=this._trapStack||[];
6262    for(var i=st.length-1;i>=0;i--){
6263      if(!st[i]||!st[i].isConnected){st.splice(i,1);continue;}
6264      if(this._trapIsActive(st[i]))return st[i];
6265    }
6266    var p=this.panelEl;
6267    if(p&&this.isOpen&&(p.classList.contains("caf-fullscreen")||p.classList.contains("caf-desktop-fs")))return p;
6268    return null;
6269  };
6270  CafChat.prototype._trapTab = function(ov){
6271    this._trapStack=this._trapStack||[];
6272    if(ov&&this._trapStack.indexOf(ov)===-1)this._trapStack.push(ov);
6273    if(this._trapHandlerBound)return;
6274    this._trapHandlerBound=true;
6275    var self=this;
6276    document.addEventListener("keydown",function(e){
6277      if(e.key!=="Tab")return;
6278      var top=self._trapTop();
6279      if(!top)return;
6280      var f=self._trapFocusables(top);
6281      if(!f.length)return;
6282      var first=f[0],last=f[f.length-1];
6283      var cur=(self.shadow&&self.shadow.activeElement)||document.activeElement;
6284      if(!cur||!top.contains(cur)){e.preventDefault();(e.shiftKey?last:first).focus();return;}
6285      if(e.shiftKey&&cur===first){e.preventDefault();last.focus();}
6286      else if(!e.shiftKey&&cur===last){e.preventDefault();first.focus();}
6287    },true);
6288  };
6289
6290  /* ── toggle button ────────────────────────────────────────── */
6291  CafChat.prototype._buildToggle = function () {
6292    var self = this;
6293    this.toggleEl = document.createElement("button");
6294    this.toggleEl.className = "caf-toggle caf-pos-" + this.opts.position;
6295    this.toggleEl.style.pointerEvents = "auto";
6296    this.toggleEl.setAttribute("aria-label", bkT("aria_open_chat"));
6297    this.toggleEl.setAttribute("aria-expanded", "false");
6298    this.toggleEl.setAttribute("aria-haspopup", "dialog");
6299    this._toggleLabelClosed = bkT("aria_open_chat");
6300    this.toggleEl.innerHTML =
6301      '<span class="caf-pulse"></span>' +
6302      '<span class="caf-pulse-2"></span>' +
6303      '<span class="caf-glow"></span>' +
6304      '<img src="' + escapeHtml(this.opts.chatLogo) + '" alt="">' +
6305      '<span class="caf-close-icon">&times;</span>';
6306    this.badgeEl = document.createElement("span");
6307    this.badgeEl.className = "caf-badge";
6308    this.badgeEl.style.display = "none";
6309    this.toggleEl.appendChild(this.badgeEl);
6310    this.toggleEl.addEventListener("click", function (e) {
6311      e.stopPropagation();
6312      self.isOpen ? self.close() : self.open();
6313    });
6314    this.wrapper.appendChild(this.toggleEl);
6315  };
6316
6317  /* ── chat panel ───────────────────────────────────────────── */
6318  CafChat.prototype._buildPanel = function () {
6319    var self = this;
6320    var o = this.opts;
6321
6322    this.panelEl = document.createElement("div");
6323    this.panelEl.className = "caf-panel caf-pos-" + o.position;
6324    /* Chrome scrolla anche i contenitori overflow:hidden per portare in vista
6325     * un elemento focalizzato: il pannello restava internamente scrollato
6326     * (scrollTop>0) senza scrollbar per rimediare -> contenuto shiftato e
6327     * fascia bianca sotto. Qui qualsiasi scroll del pannello viene annullato
6328     * appena accade (l'unico scroller legittimo e' l'area messaggi). */
6329    var _panelNoScroll = this.panelEl;
6330    _panelNoScroll.addEventListener("scroll", function () {
6331      if (_panelNoScroll.scrollTop) _panelNoScroll.scrollTop = 0;
6332      if (_panelNoScroll.scrollLeft) _panelNoScroll.scrollLeft = 0;
6333    });
6334    this.panelEl.style.pointerEvents = "auto";
6335    var hdrNameId = "caf-hdr-name-" + quickId();
6336    var menuId = "caf-menu-" + quickId();
6337    this._hdrNameId = hdrNameId;
6338    this.panelEl.setAttribute("role", "dialog");
6339    this.panelEl.setAttribute("aria-labelledby", hdrNameId);
6340    var skip = document.createElement("button");
6341    skip.type = "button";
6342    skip.className = "caf-skip";
6343    skip.textContent = bkT("skip_to_input");
6344    skip.addEventListener("click", function () { if (self.inputEl) { tr
6344y { self.inputEl.focus(); } catch (_e) {} } });
6345    this.panelEl.appendChild(skip);
6346
6347    /* header */
6348    var header = document.createElement("div");
6349    header.className = "caf-header";
6350    header.style.background = "linear-gradient(135deg, var(--caf-c-ink) 0%, color-mix(in srgb, var(--caf-c-ink) 78%, #1e293b) 100%)";
6351    header.innerHTML =
6352      '<div class="caf-header-left">' +
6353        '<div class="caf-header-avatar"><img src="' + escapeHtml(o.logo) + '" alt=""></div>' +
6354        '<div class="caf-header-info"><div class="caf-header-name" role="heading" aria-level="2" id="' + hdrNameId + '">' + escapeHtml(o.name) + '</div>' +
6355        '<div class="caf-header-status">' + (o.aiDisclosure!==false ? escapeHtml(bkT("ai_badge")) : '<span class="caf-online-dot"></span>Online') + '</div></div>' +
6356      '</div>' +
6357      '<div class="caf-header-actions">' +
6358        '<button class="caf-header-btn caf-fs-btn" aria-label="' + escapeHtml(bkT("aria_fullscreen")) + '">' +
6359          '<svg viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" style="width:16px;height:16px">' +
6360            '<polyline points="6 1 1 1 1 6"/><polyline points="14 1 19 1 19 6"/>
6360<polyline points="6 19 1 19 1 14"/><polyline points="14 19 19 19 19 14"/>' +
6361          '</svg>' +
6362        '</button>' +
6363        '<button class="caf-header-btn caf-menu-trigger" aria-label="' + escapeHtml(bkT("aria_options")) + '" aria-haspopup="true" aria-expanded="false" aria-controls="' + menuId + '">&#8943;</button>' +
6364        '<button class="caf-header-btn caf-close-btn" aria-label="' + escapeHtml(bkT("aria_close")) + '">&#215;</button>' +
6365      '</div>';
6366
6367    /* menu */
6368    this.menuEl = document.createElement("div");
6369    this.menuEl.className = "caf-menu";
6370    this.menuEl.id = menuId;
6371    var telNum = String(bkHelplineNumber(o) || "").replace(/[^0-9+]/g, "");
6372    this.menuEl.innerHTML =
6373      '<button class="caf-menu-item caf-new-conv">' +
6374        '<span class="caf-icon">+</span> ' + escapeHtml(bkT("menu_new")) +
6375      '</button>' +
6376      '<div class="caf-menu-divider"></div>' +
6377      '<div class="caf-menu-label">Valuta la conversazione</div>' +
6378      '<div class="caf-rating-row">' +
6379        '<button class="caf-rate-btn caf-rate-good" type="button" aria-pressed="false">' +
6380          '<span class="caf-rate-icon">👍</span> Positivo' +
6381        '</button>' +
6382        '<button class="caf-rate-btn caf-rate-bad" type="button" aria-pressed="false">' +
6383          '<span class="caf-rate-icon">👎</span> Negativo' +
6384        '</button>' +
6385      '</div>' +
6386      '<div class="caf-menu-divider"></div>' +
6387      (telNum ? ('<a class="caf-menu-item caf-operator" href="tel:' + telNum + '"><span class="caf-icon" aria-hidden="true">📞</span> ' + escapeHtml(bkT("menu_contact")) + '</a><div class="caf-menu-divider"></div>') : '') +
6388      '<button class="caf-menu-item caf-privacy-panel" type="button">' +
6389        '<span class="caf-icon">🔒</span> ' + escapeHtml(bkT("menu_privacy")) +
6390      '</button>';
6391    header.appendChild(this.menuEl);
6392
6393    header.querySelector(".caf-menu-trigger").addEventListener("click", function (e) {
6394      e.stopPropagation();
6395      self.menuOpen = !self.menuOpen;
6396      self._updateMenu();
6397    });
6398    header.querySelector(".caf-close-btn").addEventListener("click", function () {
6399      self.close();
6400    });
6401    this._fsBtnEl = header.querySelector(".caf-fs-btn");
6402    this._fsBtnEl.addEventListener("click", function (e) {
6403      e.stopPropagation();
6404      self._toggleFullscreen();
6405    });
6406    this.menuEl.querySelector(".caf-new-conv").addEventListener("click", function () {
6407      if (self._voiceMode) self._exitVoiceMode();
6408      self._interruptResponse();
6409      self.messages = [
6410        { id: "welcome", role: "assistant", content: timeAwareGreeting()+"! "+(self.opts.welcome||"")+((self.opts.aiDisclosure!==false)?("\n\n"+bkT("ai_disclosure")):"") },
6411      ];
6412      self.conversationId = self._newConversationId(true);
6413      self.rating = null;
6414      self._updateRatingButtons();
6415      self._bkReset(true);
6416      self._saveMessages();
6417      self._renderMessages();
6418      self._updateSendBtn();
6419      self.menuOpen = false;
6420      self._updateMenu();
6421    });
6422    this.menuEl.querySelector(".caf-rate-good").addEventListener("click", function (e) {
6423      e.stopPropagation();
6424      self.rating = self.rating === "good" ? null : "good";
6425      self._updateRatingButtons();
6426      self._syncRating();
6427    });
6428    this.menuEl.querySelector(".caf-rate-bad").addEventListener("click", function (e) {
6429      e.stopPropagation();
6430      self.rating = self.rating === "bad" ? null : "bad";
6431      self._updateRatingButtons();
6432      self._syncRating();
6433    });
6434
6435    /* #42 — Pannello Privacy: modifica preferenze + export + delete (Art. 15/17 GDPR). */
6436    this.menuEl.querySelector(".caf-privacy-panel").addEventListener("click", function (e) {
6437      e.stopPropagation();
6438      self.menuOpen = false; self._updateMenu();
6439      self._bkShowPrivacyPanel();
6440    });
6441
6442    this.panelEl.appendChild(header);
6443
6444    /* prominent "expand booking" banner — shown under the header while booking is active */
6445    var expandBanner = document.createElement("button");
6446    expandBanner.type = "button";
6447    expandBanner.className = "caf-bk-expand-banner";
6448    /* WCAG 2.5.3 (#C64): il nome accessibile deve CONTENERE le etichette
6449     * visibili del bottone ("Prenotazione in corso", "Espandi", sottotitolo). */
6450    expandBanner.setAttribute("aria-label", bkT("label_booking_inprogress")+" — "+bkT("label_expand")+". "+bkT("label_expand_sub"));
6451    expandBanner.innerHTML =
6452      '<div class="caf-bk-expand-banner-ic">' +
6453        '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round">' +
6454          '<polyline points="15 3 21 3 21 9"/>' +
6455          '<polyline points="9 21 3 21 3 15"/>' +
6456          '<line x1="21" y1="3" x2="14" y2="10"/>' +
6457          '<line x1="3" y1="21" x2="10" y2="14"/>' +
6458        '</svg>' +
6459      '</div>' +
6460      '<div class="caf-bk-expand-banner-txt">' +
6461        '<div class="caf-bk-expand-banner-t"><span class="live-dot"></span>' + escapeHtml(bkT("label_booking_inprogress")) + '</div>' +
6462        '<div class="caf-bk-expand-banner-s">' + escapeHtml(bkT("label_expand_sub")) + '</div>' +
6463      '</div>' +
6464      '<div class="caf-bk-expand-banner-arrow">' + escapeHtml(bkT("label_expand")) + ' ' +
6465        '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><polyline points="9 18 15 12 9 6"/></svg>' +
6466      '</div>';
6467    expandBanner.addEventListener("click", function () {
6468      if (self.bkStep === "idle" || self._bkFsMode) return;
6469      self._bkEnterFs();
6470    });
6471    this._bkExpandBannerEl = expandBanner;
6472    this.panelEl.appendChild(expandBanner);
6473
6474    /* messages container */
6475    this.messagesEl = document.createElement("div");
6476    this.messagesEl.className = "caf-messages";
6477    this.messagesEl.setAttribute("role", "log");
6478    this.messagesEl.setAttribute("aria-live", "off");
6479    this.messagesEl.setAttribute("aria-relevant", "additions");
6480    this.messagesEl.setAttribute("tabindex", "0");
6481    this.messagesEl.setAttribute("aria-label", bkT("aria_history"));
6482    this.messagesEl.addEventListener("scroll", function () {
6483      if (self._scrollRaf) return;
6484      self._scrollRaf = requestAnimationFrame(function () {
6485        self._scrollRaf = null;
6486        var el = self.messagesEl;
6487        self.isAtBottom = el.scrollTop + el.clientHeight >= el.scrollHeight - 60;
6488        self._updateScrollBtn();
6489      });
6490    }, { passive: true });
6491    this.panelEl.appendChild(this.messagesEl);
6492    this.srAnnouncer = document.createElement("div");
6493    this.srAnnouncer.className = "caf-sr-only";
6494    this.srAnnouncer.setAttribute("aria-live", "polite");
6495    this.srAnnouncer.setAttribute("aria-atomic", "true");
6496    this.panelEl.appendChild(this.srAnnouncer);
6497
6498    /* scroll-to-bottom button */
6499    var scrollWrap = document.createElement("div");
6500    scrollWrap.style.cssText = "position:relative;width:100%;height:0";
6501    this.scrollBtn = document.createElement("button");
6502    this.scrollBtn.className = "caf-scroll-btn";
6503    this.scrollBtn.type = "button";
6504    this.scrollBtn.setAttribute("aria-label", bkT("aria_scroll_bottom"));
6505    this.scrollBtn.innerHTML = '<span aria-hidden="true">↓</span>';
6506    this.scrollBtn.style.display = "none";
6507    this.scrollBtn.addEventListener("click", function () {
6508      self._scrollToBottom();
6509    });
6510    scrollWrap.appendChild(this.scrollBtn);
6511    this._scrollWrap = scrollWrap;
6512    this.panelEl.appendChild(scrollWrap);
6513
6514    /* error area */
6515    this.errorEl = document.createElement("div");
6516    this.errorEl.className = "caf-error";
6517    this.errorEl.setAttribute("role", "alert");
6518    this.errorEl.style.display = "none";
6519    this.panelEl.appendChild(this.errorEl);
6520
6521    /* input area */
6522    var inputArea = document.createElement("form");
6523    inputArea.className = "caf-input-area";
6524    inputArea.addEventListener("submit", function (e) {
6525      e.preventDefault();
6526      if (self._isListening) self._abortListening();
6527      self._send(self.inputEl.value);
6528    });
6529
6530    this.inputEl = document.createElement("input");
6531    this.inputEl.className = "caf-input";
6532    this.inputEl.type = "text";
6533    this.inputEl.placeholder = bkT("ph_write_message");
6534    this.inputEl.setAttribute("autocomplete", "off");
6535    this.inputEl.setAttribute("aria-label", bkT("ph_write_message"));
6536    this.inputEl.addEventListener("focus", function () { self._scheduleGhostSug(); });
6537    this.inputEl.addEventListener("input", function () { self._scheduleGhostSug(); });
6538    this.inputEl.addEventListener("blur", function () {
6539      setTimeout(function(){if(self._ghostSugTimer){clearTimeout(self._ghostSugTimer);self._ghostSugTimer=null;}}, 200);
6540    });
6541    /* Fix: alcuni temi/plugin WordPress agganciano listener globali su
6542       document/window che chiamano preventDefault() sulla barra spaziatrice
6543       (scroll-jacking, video player, lightbox, slideshow ecc.). Questo
6544       impedisce di digitare lo spazio nell'input. Fermiamo la propagazione
6545       degli eventi tastiera dall'input così i listener del sito host non
6546       possono cancellare il tasto. */
6547    var _stopHostKey = function (e) {
6548      /* Escape deve raggiungere il gestore document che chiude il pannello
6549       * (WCAG 2.1.2): il blocco anti-tema-host vale per i tasti di digitazione,
6550       * non per la chiusura del dialog. */
6551      if (e.key === "Escape" || e.key === "Esc") return;
6552      if (typeof e.stopPropagation === "function") e.stopPropagation();
6553      if (typeof e.stopImmediatePropagation === "function") e.stopImmediatePropagation();
6554    };
6555    this.inputEl.addEventListener("keydown", _stopHostKey);
6556    this.inputEl.addEventListener("keypress", _stopHostKey);
6557    this.inputEl.addEventListener("keyup", _stopHostKey);
6558
6559    this.sendBtn = document.createElement("button");
6560    this.sendBtn.className = "caf-send";
6561    this.sendBtn.type = "submit";
6562    this.sendBtn.style.background = "var(--caf-c)";
6563    this.sendBtn.style.color = "var(--caf-on-c)";
6564    this.sendBtn.setAttribute("aria-label", bkT("aria_send"));
6565    this.sendBtn.innerHTML = '<svg aria-hidden="true" viewBox="0 0 24 24" fill="currentColor" stroke="none"><path d="M3.478 2.405a.75.75 0 0 0-.926.94l2.432 7.905H13.5a.75.75 0 0 1 0 1.5H4.984l-2.432 7.905a.75.75 0 0 0 .926.94 60.519 60.519 
65650 0 0 18.445-8.986.75.75 0 0 0 0-1.218A60.517 60.517 0 0 0 3.478 2.405Z"/></svg>';
6566
6567    inputArea.appendChild(this.inputEl);
6568
6569    /* mic button (voice recognition) */
6570    if (this.opts.voice) {
6571      this.micBtn = document.createElement("button");
6572      this.micBtn.className = "caf-mic";
6573      this.micBtn.type = "button";
6574      this.micBtn.style.background = "var(--caf-c)";
6575      this.micBtn.setAttribute("aria-label", bkT("aria_voice"));
6576      this.micBtn.innerHTML =
6577        '<span class="caf-mic-pulse"></span>' +
6578        '<svg viewBox="0 0 24 24" fill="currentColor" stroke="none">' +
6579          '<path d="M12 14c1.66 0 3-1.34 3-3V5c0-1.66-1.34-3-3-3S9 3.34 9 5v6c0 1.66 1.34 3 3 3z"/>' +
6580          '<path d="M17 11c0 2.76-2.24 5-5 5s-5-2.24-5-5H5c0 3.53 2.61 6.43 6 6.92V21h2v-3.08c3.39-.49 6-3.39 6-6.92h-2z"/>' +
6581        '</svg>';
6582      var chatSelf = this;
6583      this.micBtn.addEventListener("click", function () {
6584        chatSelf._enterVoiceMode();
6585      });
6586      inputArea.appendChild(this.micBtn);
6587    }
6588
6589    inputArea.appendChild(this.sendBtn);
6590    this._inputAreaEl = inputArea;
6591
6592    /* persistent booking CTA (visible when PRENOTAZIONE=SI and booking not active) */
6593    var cta = document.createElement("div");
6594    cta.className = "caf-bk-cta";
6595    cta.style.display = "none";
6596    this._bkCtaEl = cta;
6597    this.panelEl.appendChild(cta);
6598
6599    /* ghost suggestions bar (idle hints) */
6600    var ghost = document.createElement("div");
6601    ghost.className = "caf-ghost-sug";
6602    ghost.style.display = "none";
6603    this._ghostSugEl = ghost;
6604    this.panelEl.appendChild(ghost);
6605
6606    this.panelEl.appendChild(inputArea);
6607    /* Footer persistente: dichiarazione di natura IA sempre visibile (AI Act art. 50). */
6608    if (o.aiDisclosure !== false) {
6609      var aiFooter = document.createElement("div");
6610      aiFooter.className = "caf-footer caf-ai-footer";
6611      aiFooter.setAttribute("role", "note");
6612      aiFooter.textContent = bkT("ai_badge");
6613      this.panelEl.appendChild(aiFooter);
6614      this._aiFooterEl = aiFooter;
6615    }
6616
6617    /* minimized CTA pill (appears when user dismisses) */
6618    var ctaMini = document.createElement("button");
6619    ctaMini.type = "button";
6620    ctaMini.className = "caf-bk-cta-mini";
6621    ctaMini.style.display = "none";
6622    ctaMini.setAttribute("aria-label", bkT("aria_book_appointment"));
6623    ctaMini.innerHTML =
6624      '<span class="caf-bk-cta-mini-ic">' +
6625        '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round">' +
6626          '<rect x="3" y="4" width="18" height="18" rx="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/>' +
6627        '</svg>' +
6628      '</span>' +
6629      '<span>Prenota</span>';
6630    ctaMini.addEventListener("click", function (e) {
6631      e.stopPropagation();
6632      self._bkCtaDismissed = false;
6633      self._updateBkCta();
6634      self._startBookingFromCta();
6635    });
6636    this._bkCtaMiniEl = ctaMini;
6637    this.panelEl.appendChild(ctaMini);
6638
6639    /* footer */
6640    /* footer rimosso su richiesta */
6641
6642    /* voice mode panel (hidden by default, shown when voice mode is active) */
6643    if (this.opts.voice) {
6644      this._buildVoicePanel();
6645    }
6646
6647    /* consent overlay (gate privacy: mostrato prima di TUTTO, email inclusa) */
6648    if (this.opts.consenso) {
6649      this._buildConsentOverlay();
6650      if (this._needsConsent()) {
6651        this.messagesEl.style.display = "none";
6652        this._scrollWrap.style.display = "none";
6653        this.errorEl.style.display = "none";
6654        this._inputAreaEl.style.display = "none";
6655      } else {
6656        this.consentOverlay.style.display = "none";
6657      }
6658    }
6659
6660    /* email overlay (shown before chat if chiedomail is enabled) */
6661    if (this.opts.chiedomail) {
6662      this._buildEmailOverlay();
6663      // If email not yet collected, hide chat UI and show overlay
6664      // (a meno che il gate consenso non venga prima: in quel caso l'email
6665      // overlay parte nascosto e viene mostrato all'approvazione del gate)
6666      if (this._needsEmail() && !this._needsConsent()) {
6667        this.messagesEl.style.display = "none";
6668        this._scrollWrap.style.display = "none";
6669        this.errorEl.style.display = "none";
6670        this._inputAreaEl.style.display = "none";
6671      } else {
6672        this.emailOverlay.style.display = "none";
6673      }
6674    }
6675
6676    this.wrapper.appendChild(this.panelEl);
6677  };
6678
6679  /* ── consent overlay (gate privacy all'apertura della chat) ───────
6680   * Attivo solo con opts.consenso (env CONSENSO=si / data-consenso="si").
6681   * L'utente deve SPUNTARE la checkbox e APPROVARE con il bottone prima di
6682   * poter usare la chat; il link apre l'informativa del sito ospite
6683   * (opts.privacyUrl). Il consenso e' ricordato per tenant
6684   * (CHAT_CONSENT_KEY); chiudere il widget resta sempre possibile
6685   * (il rifiuto = non usare la chat, nessun dato viene trattato). */
6686  CafChat.prototype._buildConsentOverlay = function () {
6687    var self = this;
6688    var o = this.opts;
6689    /* hardening: solo http(s) — una config errata non deve mai produrre
6690       un link javascript:/data: dentro l'overlay. */
6691    var url = (o.privacyUrl || "").trim();
6692    if (url && !/^https?:\/\//i.test(url)) url = "";
6693    /* escapeHtml NON escapa gli apici doppi: neutralizziamo eventuali '"'
6694       con l'URL-encoding cosi' l'attributo href non e' scomponibile. */
6695    if (url) url = url.replace(/"/g, "%22");
6696    this._consentUrl = url;
6697
6698    this.consentOverlay = document.createElement("div");
6699    this.consentOverlay.className = "caf-consent-overlay";
6700    this.consentOverlay.setAttribute("role", "dialog");
6701    this.consentOverlay.setAttribute("aria-modal", "true");
6702    this.consentOverlay.setAttribute("aria-label", bkT("chatgate_h"));
6703    /* BUGFIX (pannello in finestra): con age gate + disclosure il contenuto
6704     * del gate supera l'altezza del pannello; senza min-height:0 il flex-item
6705     * non si stringe e senza overflow-y il bottone "Accetta" finisce sotto il
6706     * bordo, irraggiungibile. Centraggio delegato al margin:auto del wrapper
6707     * interno: justify-content:center renderebbe irraggiungibile la parte
6708     * ALTA del contenuto quando trabocca. */
6709    this.consentOverlay.style.cssText =
6710      "flex:1;min-height:0;overflow-y:auto;z-index:100;background:#fff;display:flex;flex-direction:column;" +
6711      "align-items:center;justify-content:flex-start;padding:32px 24px;";
6712
6713    var linkHtml = url
6714      ? '<a class="caf-consent-link" href="' + escapeHtml(url) + '" target="_blank" rel="noopener noreferrer" style="color:var(--caf-c-ink);text-decoration:underline;font-weight:600">' + escapeHtml(bkT("chatgate_link")) + '</a>'
6715      : '<a class="caf-consent-link caf-consent-open-panel" href="#" role="button" style="color:var(--caf-c-ink);text-decoration:underline;font-weight:600;cursor:pointer">' + escapeHtml(bkT("chatgate_link")) + '</a>';
6716
6717    this.consentOverlay.innerHTML =
6718      '<div style="text-align:center;max-width:340px;width:100%;margin:auto 0">' +
6719        '<div style="margin-bottom:16px">' +
6720          '<img src="' + escapeHtml(o.logo) + '" alt="" style="display:block;width:60px;height:60px;border-radius:50%;object-fit:cover;margin:0 auto 12px">' +
6721          '<h3 style="margin:0 0 6px;font-size:18px;font-weight:700;color:#1e293b">' + escapeHtml(bkT("chatgate_h")) + '</h3>' +
6722          '<p style="margin:0;font-size:13.5px;color:#475569;line-height:1.5">' + escapeHtml(bkT("chatgate_body")) + '</p>' +
6723          (o.aiDisclosure!==false ? '<p style="margin:8px 0 0;font-size:12px;color:#475569;line-height:1.5;font-style:italic">' + escapeHtml(bkT("ai_disclosure")) + '</p>' : '') +
6724        '</div>' +
6725        '<label class="caf-consent-label" style="display:flex;align-items:flex-start;gap:10px;text-align:left;padding:12px 14px;background:#f8fafc;border:1.5px solid #e2e8f0;border-radius:12px;font-size:12.5px;line-height:1.5;color:#475569;cursor:pointer;margin-bottom:14px;transition:border-color .2s,background .2s">' +
6726          '<input type="checkbox" class="caf-consent-cb" aria-required="true" style="width:19px;height:19px;min-width:19px;margin-top:1px;accent-color:' + o.color + ';cursor:pointer">' +
6727          '<span>' + escapeHtml(bkT("chatgate_before")) + linkHtml + escapeHtml(bkT("chatgate_after")) + ' <span aria-hidden="true" style="color:#be123c;font-weight:700">*</span></span>' +
6728        '</label>' +
6729        '<label class="caf-consent-label caf-consent-age-label" style="display:flex;align-items:flex-start;gap:10px;text-align:left;padding:12px 14px;background:#f8fafc;border:1.5px solid #e2e8f0;border-radius:12px;font-size:12.5px;line-height:1.5;color:#475569;cursor:pointer;margin-bottom:14px;transition:border-color .2s,background .2s">' +
6730          '<input type="checkbox" class="caf-consent-age" aria-required="true" style="width:19px;height:19px;min-width:19px;margin-top:1px;accent-color:' + o.color + ';cursor:pointer">' +
6731          '<span>' + escapeHtml(bkT("age_gate_label")) + ' <span aria-hidden="true" style="color:#be123c;font-weight:700">*</span></span>' +
6732        '</label>' +
6733        '<button type="button" class="caf-consent-ok" disabled ' +
6734          'style="width:100%;padding:12px;border:none;border-radius:10px;background:var(--caf-c);color:var(--caf-on-c);' +
6735          'font-size:15px;font-weight:600;cursor:pointer;font-family:inherit;transition:opacity .2s;opacity:.5">' +
6736          escapeHtml(bkT("chatgate_btn")) +
6737        '</button>' +
6738      '</div>';
6739
6740    var cb = this.consentOverlay.querySelector(".caf-consent-cb");
6741    var ageCb = this.consentOverlay.querySelector(".caf-consent-age");
6742    var lbl = this.consentOverlay.querySelector(".caf-consent-label");
6743    var ageLbl = this.consentOverlay.querySelector(".caf-consent-age-label");
6744    var ok = this.consentOverlay.querySelector(".caf-consent-ok");
6745    /* Fallback informativa: se manca PRIVACY_URL il link apre il pannello
6746     * privacy in-widget, cosi' il consenso resta informato (GDPR artt. 7, 13). */
6747    var _openPanelLink = this.consentOverlay.querySelector(".caf-consent-open-panel");
6748    if (_openPanelLink) {
6749      _openPanelLink.addEventListener("click", function (e) {
6750        e.preventDefault();
6751        try { self._bkShowPrivacyPanel(); } catch (_e) {}
6752      });
6753    }
6754    function _syncConsentBtn() {
6755      var both = cb.checked && ageCb.checked;
6756      ok.disabled = !both;
6757      ok.style.opacity = both ? "1" : ".5";
6758      lbl.style.borderColor = cb.checked ? o.color : "#e2e8f0";
6759      lbl.style.background = cb.checked ? (o.color + "0f") : "#f8fafc";
6760      ageLbl.style.borderColor = ageCb.checked ? o.color : "#e2e8f0";
6761      ageLbl.style.background = ageCb.checked ? (o.color + "0f") : "#f8fafc";
6762    }
6763    cb.addEventListener("change", _syncConsentBtn);
6764    ageCb.addEventListener("change", _syncConsentBtn);
6765    ok.addEventListener("click", function () {
6766      if (!cb.checked || !ageCb.checked) return;
6767      /* flag in-memory PRIMA del save: se localStorage e' negato il gate
6768         resta comunque superato per questa sessione. */
6769      self._chatConsentGiven = true;
6770      chatConsentSave(self._consentUrl, ageCb.checked);
6771      self.consentOverlay.style.display = "none";
6772      if (self._syncAiFooter) setTimeout(function () { self._syncAiFooter(); }, 0);
6773      if (self._needsEmail() && self.emailOverlay) {
6774        self.emailOverlay.style.display = "flex";
6775        setTimeout(function () {
6776          var emailInput = self.emailOverlay.querySelector(".caf-email-input");
6777          if (emailInput) emailInput.focus();
6778        }, 100);
6779      } else {
6780        self.messagesEl.style.display = "";
6781        self._scrollWrap.style.display = "";
6782        self._inputAreaEl.style.display = "";
6783        self._scrollToBottom();
6784        setTimeout(function () { self.inputEl.focus(); }, 100);
6785      }
6786    });
6787
6788    this._trapTab(this.consentOverlay);
6789    this.panelEl.appendChild(this.consentOverlay);
6790  };
6791
6792  CafChat.prototype._needsConsent = function () {
6793    if (!this.opts.consenso) return false;
6794    if (this._chatConsentGiven) return false;
6795    return !chatConsentLoad();
6796  };
6797
6798  /* ── email overlay ──────────────────────────────────────────── */
6799  CafChat.prototype._buildEmailOverlay = function () {
6800    var self = this;
6801    var o = this.opts;
6802    var errId = "caf-email-err-" + quickId();
6803
6804    this.emailOverlay = document.createElement("div");
6805    this.emailOverlay.className = "caf-email-overlay";
6806    /* BUGFIX: stesso rimedio del gate consenso (scroll quando trabocca). */
6807    this.emailOverlay.style.cssText =
6808      "flex:1;min-height:0;overflow-y:auto;z-index:100;background:#fff;display:flex;flex-direction:column;" +
6809      "align-items:center;justify-content:flex-start;padding:32px 24px;";
6810
6811    this.emailOverlay.innerHTML =
6812      '<div style="text-align:center;max-width:320px;width:100%;margin:auto 0">' +
6813        '<div style="margin-bottom:20px">' +
6814          '<img src="' + escapeHtml(o.logo) + '" alt="" style="display:block;width:60px;height:60px;border-radius:50%;object-fit:cover;margin:0 auto 12px">' +
6815          '<h3 style="margin:0 0 6px;font-size:18px;font-weight:700;color:#1e293b">' + escapeHtml(o.name) + '</h3>' +
6816          '<p style="margin:0;font-size:14px;color:#5b6573;line-height:1.4">' + escapeHtml(bkT("email_gate_body")) + '</p>' +
6817        '</div>' +
6818        '<form class="caf-email-form" style="width:100%">' +
6819          '<input type="email" class="caf-email-input" placeholder="' + escapeHtml(bkT("email_gate_ph")) + '" required ' +
6820            'autocomplete="email" inputmode="email" aria-label="' + escapeHtml(bkT("aria_email")) + '" aria-describedby="' + errId + '" ' +
6821            'style="width:100%;padding:12px 14px;border:1.5px solid #e2e8f0;border-radius:10px;font-size:15px;' +
6822            'outline:none;transition:border-color .2s;box-sizing:border-box;margin-bottom:12px;font-family:inherit">' +
6823          '<div class="caf-email-err" id="' + errId + '" role="alert" style="display:none;text-align:left;font-size:13px;color:#b91c1c;margin:-4px 0 10px;line-height:1.35"></div>' +
6824          '<button type="submit" class="caf-email-submit" ' +
6825            'style="width:100%;padding:12px;border:none;border-radius:10px;background:var(--caf-c);color:var(--caf-on-c);' +
6826            'font-size:15px;font-weight:600;cursor:pointer;font-family:inherit;transition:opacity .2s">' +
6827            escapeHtml(bkT("email_gate_btn")) +
6828          '</button>' +
6829        '</form>' +
6830        '<p class="caf-email-privnote" style="margin:14px 0 0;font-size:11.5px;color:#475569;line-height:1.5;text-align:left">' +
6831          escapeHtml(bkT("email_gate_note")) + ' ' +
6832          ((o.privacyUrl && /^https?:\/\//i.test(o.privacyUrl))
6833            ? '<a href="' + escapeHtml(String(o.privacyUrl).replace(/"/g, "%22")) + '" target="_blank" rel="noopener noreferrer" style="color:var(--caf-c-ink);text-decoration:underline;font-weight:600">' + escapeHtml(bkT("email_gate_privacy")) + '</a>'
6834            : '<a href="#" class="caf-email-open-panel" role="button" style="color:var(--caf-c-ink);text-decoration:underline;font-weight:600;cursor:pointer">' + escapeHtml(bkT("email_gate_privacy")) + '</a>') +
6835        '</p>' +
6836      '</div>';
6837
6838    var form = this.emailOverlay.querySelector(".caf-email-form");
6839    var input = this.emailOverlay.querySelector(".caf-email-input");
6840    var errEl = this.emailOverlay.querySelector(".caf-email-err");
6841    var _eop = this.emailOverlay.querySelector(".caf-email-open-panel");
6842    if (_eop) {
6843      var _eopSelf = this;
6844      _eop.addEventListener("click", function (e) {
6845        e.preventDefault();
6846        try { _eopSelf._bkShowPrivacyPanel(); } catch (_e) {}
6847      });
6848    }
6849
6850    input.addEventListener("focus", function () {
6851      this.style.borderColor = o.color;
6852    });
6853    input.addEventListener("blur", function () {
6854      this.style.borderColor = "#e2e8f0";
6855    });
6856
6857    form.addEventListener("submit", function (e) {
6858      e.preventDefault();
6859      var email = input.value.trim();
6860      if (!bkValidaEmail(email)) {
6861        input.setAttribute("aria-invalid", "true");
6862        if (errEl) { errEl.textContent = "Inserisci un indirizzo email valido."; errEl.style.display = ""; }
6863        input.focus();
6864        return;
6865      }
6866      input.setAttribute("aria-invalid", "false");
6867      if (errEl) { errEl.textContent = ""; errEl.style.display = "none"; }
6868      self._saveEmail(email);
6869      // Hide overlay and show chat UI
6870      self.emailOverlay.style.display = "none";
6871      self.messagesEl.style.display = "";
6872      self._scrollWrap.style.display = "";
6873      self._inputAreaEl.style.display = "";
6874      if (self._syncAiFooter) self._syncAiFooter();
6875      self.inputEl.focus();
6876      self._scrollToBottom();
6877      // sync email to server immediately
6878      self._syncConversation();
6879    });
6880
6881    this.panelEl.appendChild(this.emailOverlay);
6882  };
6883
6884  CafChat.prototype._needsEmail = function () {
6885    return this.opts.chiedomail && !this.userEmail;
6886  };
6887
6888  /* ── voice mode panel ──────────────────────────────────────── */
6889  CafChat.prototype._buildVoicePanel = function () {
6890    var self = this;
6891    var o = this.opts;
6892
6893    this.voicePanelEl = document.createElement("div");
6894    this.voicePanelEl.className = "caf-voice-panel";
6895
6896    /* transcript area */
6897    this.voiceTranscriptEl = document.createElement("div");
6898    this.voiceTranscriptEl.className = "caf-voice-transcript";
6899    this.voicePanelEl.appendChild(this.voiceTranscriptEl);
6900
6901    /* controls area */
6902    var controls = document.createElement("div");
6903    controls.className = "caf-voice-controls";
6904    controls.style.background = "radial-gradient(ellipse at 50% 80%, " + o.color + "20 0%, rgba(240,253,244,.5) 50%, rgba(255,255,255,.8) 100%)";
6905
6906    /* ambient glow */
6907    this.voiceGlowEl = document.createElement("span");
6908    this.voiceGlowEl.className = "caf-voice-glow";
6909    this.voiceGlowEl.style.width = "120px";
6910    this.voiceGlowEl.style.height = "120px";
6911    this.voiceGlowEl.style.backgroundColor = o.color;
6912    this.voiceGlowEl.style.opacity = "0.08";
6913    controls.appendChild(this.voiceGlowEl);
6914
6915    /* status text */
6916    this.voiceStatusEl = document.createElement("p");
6917    this.voiceStatusEl.className = "caf-voice-status";
6918    this.voiceStatusEl.style.color = "var(--caf-c-ink)"; /* a11y #C54: colore pieno */
6919    this.voiceStatusEl.textContent = bkT("voice_tap_to_talk");
6920    controls.appendChild(this.voiceStatusEl);
6921
6922    /* hint text */
6923    this.voiceHintEl = document.createElement("p");
6924    this.voiceHintEl.className = "caf-voice-hint";
6925    this.voiceHintEl.style.color = "#5b6573";
6926    this.voiceHintEl.textContent = bkT("voice_press_to_start");
6927    controls.appendChild(this.voiceHintEl);
6928
6929    /* mic wrap */
6930    var micWrap = document.createElement("div");
6931    micWrap.className = "caf-voice-mic-wrap";
6932
6933    /* outer ring */
6934    this.voiceRingEl = document.createElement("span");
6935    this.voiceRingEl.className = "caf-voice-ring";
6936    this.voiceRingEl.style.width = "105px";
6937    this.voiceRingEl.style.height = "105px";
6938    this.voiceRingEl.style.borderColor = o.color + "30";
6939    micWrap.appendChild(this.voiceRingEl);
6940
6941    /* ripple container */
6942    this.voiceRipplesEl = document.createElement("span");
6943    this.voiceRipplesEl.style.cssText = "position:absolute;display:none";
6944    for (var r = 0; r < 3; r++) {
6945      var ripple = document.createElement("span");
6946      ripple.className = "caf-voice-ripple";
6947      ripple.style.cssText = "width:96px;height:96px;position:absolute;left:50%;top:50%;margin-left:-48px;margin-top:-48px;background:" + o.color;
6948      ripple.style.opacity = String(0.3 - r * 0.1);
6949      ripple.style.animation = "caf-voice-ripple 2s ease-out " + (r * 0.6) + "s infinite";
6950      this.voiceRipplesEl.appendChild(ripple);
6951    }
6952    micWrap.appendChild(this.voiceRipplesEl);
6953
6954    /* waveform bars container */
6955    this.voiceBarsEl = document.createElement("span");
6956    this.voiceBarsEl.className = "caf-voice-bars";
6957    this.voiceBarsEl.style.display = "none";
6958    for (var b = 0; b < 12; b++) {
6959      var bar = document.createElement("span");
6960      bar.className = "caf-voice-bar";
6961      bar.style.height = (10 + (b % 3) * 4) + "px";
6962      bar.style.transform = "rotate(" + (b * 30) + "deg) translateY(-58px)";
6963      bar.style.animation = "caf-voice-bar 0.5s ease-in-out " + (b * 0.06) + "s infinite";
6964      bar.style.background = o.color + "80";
6965      this.voiceBarsEl.appendChild(bar);
6966    }
6967    micWrap.appendChild(this.voiceBarsEl);
6968
6969    /* the big mic button */
6970    this.voiceMicBtn = document.createElement("button");
6971    this.voiceMicBtn.className = "caf-voice-mic-btn";
6972    this.voiceMicBtn.type = "button";
6973    this.voiceMicBtn.style.background = o.color;
6974    this.voiceMicBtn.style.boxShadow = "0 8px 30px -4px " + o.color + "80";
6975    this.voiceMicBtn.style.animation = "caf-mic-breathe 3s ease-in-out infinite";
6976    this.voiceMicBtn.setAttribute("aria-label", bkT("aria_speak"));
6977    this.voiceMicBtn.setAttribute("aria-pressed", "false");
6978    this._setVoiceMicIcon("mic");
6979    this.voiceMicBtn.addEventListener("click", function () {
6980      self._micRetryCount = 0; /* user gesture: reset retry counter */
6981      if (self._isSpeaking || self.isLoading) {
6982        self._interruptResponse();
6983        if (!self._isListening) self._scheduleListening(200);
6984      } else if (self._isListening) {
6985        self._stopListening();
6986      } else {
6987        self._startListening();
6988      }
6989    });
6990    micWrap.appendChild(this.voiceMicBtn);
6991    controls.appendChild(micWrap);
6992
6993    /* exit button */
6994    var exitBtn = document.createElement("button");
6995    exitBtn.className = "caf-voice-exit";
6996    exitBtn.type = "button";
6997    exitBtn.innerHTML =
6998      '<svg viewBox="0 0 20 20"><path fill-rule="evenodd" d="M2 4.75A.75.75 0 012.75 4h14.5a.75.75 0 010 1.5H2.75A.75.75 0 012 4.75zM2 10a.75.75 0 01.75-.75h14.5a.75.75 0 010 1.5H2.75A.75.75 0 012 10zm0 5.25a.75.75 0 01.75-.75h14.5a.75.75 0 010 1.5H2.75a.75.75 0 01-.75-.75z" clip-rule="evenodd"/></svg>
6998' +
6999      escapeHtml(bkT("voice_back_keyboard"));
7000    exitBtn.addEventListener("click", function () {
7001      self._exitVoiceMode();
7002    });
7003    controls.appendChild(exitBtn);
7004
7005    this.voicePanelEl.appendChild(controls);
7006
7007    /* insert after header, before messages */
7008    this.panelEl.insertBefore(this.voicePanelEl, this.messagesEl);
7009  };
7010
7011  CafChat.prototype._setVoiceMicIcon = function (mode) {
7012    var svg;
7013    if (mode === "stop") {
7014      svg = '<svg viewBox="0 0 24 24"><rect x="6" y="6" width="12" height="12" rx="3"/></svg>';
7015    } else if (mode === "speaker") {
7016      svg = '<svg viewBox="0 0 24 24"><path d="M3 9v6h4l5 5V4L7 9H3z"/><path d="M16.5 12A4.5 4.5 0 0014 8v8a4.47 4.47 0 002.5-4z"/><path d="M14 3.23v2.06a6.49 6.49 0 010 13.42v2.06A8.49 8.49 0 0022.5 12 8.49 8.49 0 0014 3.23z"/></svg>';
7017    } else if (mode === "dots") {
7018      svg = '<span style="display:flex;align-items:center;gap:6px">' +
7019        '<span style="width:10px;height:10px;border-radius:50%;background:#fff;animation:caf-dots 1.4s infinite ease-in-out both;animation-delay:-.3s"></span>' +
7020        '<span style="width:10px;height:10px;border-radius:50%;background:#fff;animation:caf-dots 1.4s infinite ease-in-out both;animation-delay:-.15s"></span>' +
7021        '<span style="width:10px;height:10px;border-radius:50%;background:#fff;animation:caf-dots 1.4s infinite ease-in-out both"></span>' +
7022      '</span>';
7023    } else {
7024      svg = '<svg viewBox="0 0 24 24"><path d="M12 14c1.66 0 3-1.34 3-3V5c0-1.66-1.34-3-3-3S9 3.34 9 5v6c0 1.66 1.34 3 3 3z"/><path d="M17 11c0 2.76-2.24 5-5 5s-5-2.24-5-5H5c0 3.53 2.61 6.43 6 6.92V21h2v-3.08c3.39-.49 6-3.39 6-6.92h-2z"/></svg>';
7025    }
7026    this.voiceMicBtn.innerHTML = svg;
7027  };
7028
7029  /* ── date helpers ─────────────────────────────────────────── */
7030  function formatDateLabel(ts) {
7031    var d = new Date(ts);
7032    var now = new Date();
7033    var today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
7034    var msgDay = new Date(d.getFullYear(), d.getMonth(), d.getDate());
7035    var diff = (today - msgDay) / 86400000;
7036    if (diff === 0) return "Oggi";
7037    if (diff === 1) return "Ieri";
7038    return d.toLocaleDateString("it-IT", { day: "numeric", month: "long" });
7039  }
7040
7041  function dayKey(ts) {
7042    if (!ts) return "";
7043    var d = new Date(ts);
7044    return d.getFullYear() + "-" + (d.getMonth() + 1) + "-" + d.getDate();
7045  }
7046
7047  /* ── translation languages ───────────────────────────────── */
7048  var TRANSLATE_LANGS = [
7049    { flag: "🇬🇧", code: "en", label: "EN" },
7050    { flag: "🇫🇷", code: "fr", label: "FR" },
7051    { flag: "🇩🇪", code: "de", label: "DE" },
7052    { flag: "🇪🇸", code: "es", label: "ES" },
7053    { flag: "🇵🇹", code: "pt", label: "PT" },
7054    { flag: "🇷🇴", code: "ro", label: "RO" },
7055    { flag: "🇨🇳", code: "zh", label: "ZH" },
7056    { flag: "🇸🇦", code: "ar", label: "AR" },
7057    { flag: "🇺🇦", code: "uk", label: "UK" },
7058    { flag: "🇷🇺", code: "ru", label: "RU" },
7059    { flag: "🇦🇱", code: "sq", label: "AL" },
7060    { flag: "🇮🇳", code: "hi", label: "HI" },
7061    { flag: "🇵🇱", code: "pl", label: "PL" },
7062    { flag: "🇳🇱", code: "nl", label: "NL" },
7063    { flag: "🇸🇪", code: "sv", label: "SV" },
7064    { flag: "🇩🇰", code: "da", label: "DA" },
7065    { flag: "🇳🇴", code: "no", label: "NO" },
7066    { flag: "🇫🇮", code: "fi", label: "FI" },
7067    { flag: "🇬🇷", code: "el", label: "EL" },
7068    { flag: "🇭🇺", code: "hu", label: "HU" },
7069    { flag: "🇨🇿", code: "cs", label: "CS" },
7070    { flag: "🇸🇰", code: "sk", label: "SK" },
7071    { flag: "🇧🇬", code: "bg", label: "BG" },
7072    { flag: "🇭🇷", code: "hr", label: "HR" },
7073    { flag: "🇷🇸", code: "sr", label: "SR" },
7074    { flag: "🇸🇮", code: "sl", label: "SL" },
7075    { flag: "🇱🇹", code: "lt", label: "LT" },
7076    { flag: "🇱🇻", code: "lv", label: "LV" },
7077    { flag: "🇪🇪", code: "et", label: "ET" },
7078    { flag: "🇹🇷", code: "tr", label: "TR" },
7079    { flag: "🇯🇵", code: "ja", label: "JA" },
7080    { flag: "🇰🇷", code: "ko", label: "KO" },
7081    { flag: "🇹🇭", code: "th", label: "TH" },
7082    { flag: "🇻🇳", code: "vi", label: "VI" },
7083    { flag: "🇮🇩", code: "id", label: "ID" },
7084    { flag: "🇲🇾", code: "ms", label: "MS" },
7085    { flag: "🇵🇭", code: "tl", label: "TL" },
7086    { flag: "🇮🇱", code: "he", label: "HE" },
7087    { flag: "🇮🇷", code: "fa", label: "FA" },
7088    { flag: "🇵🇰", code: "ur", label: "UR" },
7089    { flag: "🇧🇩", code: "bn", label: "BN" },
7090    { flag: "🇪🇹", code: "am", label: "AM" },
7091    { flag: "🇳🇬", code: "yo", label: "YO" },
7092    { flag: "🇰🇪", code: "sw", label: "SW" },
7093    { flag: "🇧🇷", code: "pt-BR", label: "PT-BR" },
7094    { flag: "🇲🇩", code: "mo", label: "MD" },
7095    { flag: "🇬🇪", code: "ka", label: "KA" },
7096    { flag: "🇦🇲", code: "hy", label: "HY" },
7097  ];
7098
7099  var LANG_NAMES = {
7100    en: "inglese", fr: "francese", de: "tedesco", es: "spagnolo",
7101    pt: "portoghese", ro: "rumeno", zh: "cinese", ar: "arabo",
7102    uk: "ucraino", ru: "russo", sq: "albanese", hi: "hindi",
7103    pl: "polacco", nl: "olandese", sv: "svedese", da: "danese",
7104    no: "norvegese", fi: "finlandese", el: "greco", hu: "ungherese",
7105    cs: "ceco", sk: "slovacco", bg: "bulgaro", hr: "croato",
7106    sr: "serbo", sl: "sloveno", lt: "lituano", lv: "lettone",
7107    et: "estone", tr: "turco", ja: "giapponese", ko: "coreano",
7108    th: "tailandese", vi: "vietnamita", id: "indonesiano", ms: "malese",
7109    tl: "filippino", he: "ebraico", fa: "persiano", ur: "urdu",
7110    bn: "bengalese", am: "amarico", yo: "yoruba", sw: "swahili",
7111    "pt-BR": "portoghese brasiliano", mo: "moldavo", ka: "georgiano", hy: "armeno"
7112  };
7113
7114  /* ── copy helper ────────────────────────────────────────── */
7115  function copyText(text, btn) {
7116    var done = function () {
7117      btn.classList.add("copied");
7118      btn.textContent = "✓";
7119      var toast = document.createElement("div");
7120      toast.className = "caf-copy-toast";
7121      toast.textContent = "Copiato!";
7122      btn.parentElement.appendChild(toast);
7123      setTimeout(function () {
7124        btn.classList.remove("copied");
7125        btn.textContent = "⧉";
7126        if (toast.parentElement) toast.parentElement.removeChild(toast);
7127      }, 1500);
7128    };
7129    if (navigator.clipboard && navigator.clipboard.writeText) {
7130      navigator.clipboard.writeText(text).then(done).catch(done);
7131    } else {
7132      var ta = document.createElement("textarea");
7133      ta.value = text;
7134      ta.style.cssText = "position:fixed;opacity:0";
7135      document.body.appendChild(ta);
7136      ta.select();
7137      try { document.execCommand("copy"); } catch (e) { /* ok */ }
7138      document.body.removeChild(ta);
7139      done();
7140    }
7141  }
7142
7143  /* ── render messages (full rebuild) ─────────────────────────── */
7144  CafChat.prototype._renderMessages = function () {
7145    var self = this;
7146    var o = this.opts;
7147    this._streamingBubble = null;
7148    var frag = document.createDocumentFragment();
7149
7150    var totalMsgs = this.messages.length;
7151    var lastDay = "";
7152
7153    for (var i = 0; i < totalMsgs; i++) {
7154      var msg = this.messages[i];
7155      var isLast = i === totalMsgs - 1;
7156      var isNew = i >= totalMsgs - 2 && this.isLoading;
7157
7158      /* ── date separator ── */
7159      var msgDay = dayKey(msg.timestamp);
7160      if (msg.timestamp && msgDay && msgDay !== lastDay && msg.id !== "welcome") {
7161        lastDay = msgDay;
7162        var sep = document.createElement("div");
7163        sep.className = "caf-date-sep";
7164        sep.innerHTML = "<span>" + formatDateLabel(msg.timestamp) + "</span>";
7165        frag.appendChild(sep);
7166      }
7167
7168      var row = document.createElement("div");
7169      row.className = "caf-msg-row" + (msg.role === "user" ? " user" : "") + (isNew ? " caf-new" : "");
7170      var srSender = document.createElement("span");
7171      srSender.className = "caf-sr-only";
7172      srSender.textContent = (msg.role === "user" ? bkT("sr_you_said") : ((o.name || bkT("sr_assistant")) + " " + bkT("sr_said")));
7173      row.appendChild(srSender);
7174
7175      if (msg.role === "assistant") {
7176        var avatar = document.createElement("div");
7177        avatar.className = "caf-msg-avatar";
7178        avatar.innerHTML = '<img src="' + escapeHtml(o.logo) + '" alt="">';
7179        row.appendChild(avatar);
7180      }
7181
7182      /* wrap bubble + timestamp + copy btn */
7183      var wrap = document.createElement("div");
7184      wrap.className = "caf-msg-wrap";
7185
7186      var bubble = document.createElement("div");
7187      bubble.className = "caf-msg-bubble " + msg.role;
7188      if (msg.transLang) { bubble.setAttribute("lang", msg.transLang); if (/^(ar|he|fa|ur)/.test(msg.transLang)) bubble.setAttribute("dir", "rtl"); }
7189      if (msg.role === "assistant") {
7190        bubble.setAttribute("data-ai-generated", "true");
7191        if (msg.model) bubble.setAttribute("data-model", String(msg.model));
7192      }
7193      if (msg.role === "user") {
7194        bubble.style.background = "var(--caf-c)"; /* brand AA-scurito: bianco >=4.5 sul testo bolla */
7195      }
7196
7197      if (msg.role === "assistant" && msg.content === "@@BOOKING@@") {
7198        /* La prenotazione ora è SEMPRE fullscreen: skip il bubble inline. */
7199        continue;
7200      } else if (msg.role === "assistant" && typeof msg.content === "string" && msg.content.indexOf("@@BOOKING_LAUNCH@@") === 0) {
7201        /* Booking launch: messaggio con bottone cliccabile che apre il wizard.
7202         * Format: "@@BOOKING_LAUNCH@@<optional service hint>" */
7203        var bklHint = msg.content.slice("@@BOOKING_LAUNCH@@".length).trim();
7204        var bklSelf = this;
7205        var bklIntro = document.createElement("div");
7206        bklIntro.className = "caf-bk-launch-intro";
7207        bklIntro.style.cssText = "margin-bottom:10px;font-weight:500;";
7208        bklIntro.textContent = bklHint
7209          ? "Perfetto! Clicca qui sotto per prenotare " + bklHint + " \u2192"
7210          : "Perfetto! Clicca qui sotto per aprire il wizard di prenotazione \u2192";
7211        bubble.appendChild(bklIntro);
7212        var bklBtn = document.createElement("button");
7213        bklBtn.type = "button";
7214        bklBtn.className = "caf-inline-bk-chip caf-bk-launch-btn";
7215        bklBtn.innerHTML = '<svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="4" width="18" height="18" rx="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/><path d="M9 16l2 2 4-4"/></svg>
7215' + escapeHtml(bklHint ? "Apri il wizard per " + bklHint : "Apri il wizard di prenotazione");
7216        bklBtn.addEventListener("click", function (e) {
7217          e.stopPropagation();
7218          /* Apri il wizard direttamente. NON usiamo _startBookingFromCta
7219           * perché aggiungerebbe un secondo user-msg fantasma "Vorrei
7220           * prenotare un appuntamento" — l'utente ha già scritto la sua
7221           * richiesta sopra. Chiamiamo _bkStart() direttamente. */
7222          if (bklSelf.bkStep !== "idle") return;
7223          if (bklHint) bklSelf.bkHint = bklHint;
7224          bklSelf._bkStart();
7225          if (bklSelf._updateBkCta) bklSelf._updateBkCta();
7226        });
7227        bubble.appendChild(bklBtn);
7228      } else if (msg.role === "assistant" && msg.content === "@@PWDRESET@@") {
7229        var pwC = document.createElement("div");
7230        pwC.className = "caf-pw";
7231        bubble.appendChild(pwC);
7232        this._pwdEl = pwC;
7233      } else if (msg.role === "assistant" && msg.content === "" && this.isLoading) {
7234        bubble.innerHTML =
7235          '<div class="caf-typing" aria-hidden="true"><span></span><span></span><span></span></div>';
7236        if (isLast) this._streamingBubble = bubble;
7237      } else if (msg.role === "assistant") {
7238        bubble.innerHTML = renderMarkdown(msg.content, o.color);
7239        /* Trova la query utente che ha PRECEDUTO questo specifico messaggio
7240         * assistente — non l'ultima della conversazione (sennò vecchi
7241         * bubble riceverebbero il carosello dell'ultima query). */
7242        var _ecQuery = "";
7243        for (var _qi = i - 1; _qi >= 0; _qi--) {
7244          if (this.messages[_qi].role === "user" && this.messages[_qi].content) {
7245            _ecQuery = this.messages[_qi].content; break;
7246          }
7247        }
7248        this._renderEcommerceCards && this._renderEcommerceCards(bubble, _ecQuery);
7249        if (this.isLoading && isLast) {
7250          var caret = document.createElement("span");
7251          caret.className = "caf-caret";
7252          caret.style.background = o.color + "e6";
7253          bubble.appendChild(caret);
7254          this._streamingBubble = bubble;
7255        }
7256        /* inline "Prenota questo servizio" chip when assistant mentions a bookable service */
7257        if (msg.content && msg.content.length > 0 && !this.isLoading && this._bkEnabled === true && this.bkStep === "idle" && msg.id !== "welcome") {
7258          /* SOLO servizi gestiti dal CAF. NASPI e Assegno Unico Universale
7259           * sono di competenza INCA/Patronato → non offriamo prenotazione.
7260           * Pattern più specifici PRIMA dei più generici (primo match vince).
7261           * Tutti i pattern hanno \b finale per evitare false positive
7262           * (es. "modello redditi" non deve matchare "modello red"). */
7263          var svcPatterns = [
7264            {k:/\b730\b/i, name:"730"},
7265            {k:/\bise[ue]\b/i, name:"ISEE"},
7266            {k:/\bcud\b/i, name:"CUD"},
7267            {k:/\bsuccessi[oò]n/i, name:"Successione"},
7268            {k:/\b(colf|badant\w*)\b/i, name:"Colf e Badanti"},
7269            {k:/\b(imu|tasi|tari)\b/i, name:"IMU/TASI/TARI"},
7270            {k:/\bmodello\s+redditi\b|\bunico\b/i, name:"Modello REDDITI"},
7271            {k:/\bmodello\s+red\b|\bred\b/i, name:"Modello RED"},
7272            {k:/\binvciv\b/i, name:"INVCIV"},
7273            {k:/\baccas\/?ps\b|\bassegno\s+sociale\b/i, name:"ACCAS/PS"},
7274            {k:/\bcedolare\s+secca\b|\blocazion[ei]\b/i, name:"Locazione"},
7275            {k:/\bdelega\s+unica\b/i, name:"Delega Unica"},
7276            {k:/\bvisur[ae]\s+catastal[ei]\b/i, name:"Visura Catastale"},
7277            {k:/\bpartita\s+iva\b|\bsapi\b/i, name:"Partita IVA"}
7278          ];
7279          var matched = null;
7280          for (var svcI = 0; svcI < svcPatterns.length; svcI++) {
7281            if (svcPatterns[svcI].k.test(msg.content)) { matched = svcPatterns[svcI].name; break; }
7282          }
7283          if (matched) {
7284            var chipSelf = this;
7285            var chipSvc = matched;
7286            var chip = document.createElement("button");
7287            chip.type = "button";
7288            chip.className = "caf-inline-bk-chip";
7289            chip.innerHTML = '<svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="4" width="18" height="18" rx="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/><path d="M9 16l2 2 4-4"/></svg>Prenota ' + escapeHtml(chipSvc);
7290            chip.addEventListener("click", function (e) {
7291              e.stopPropagation();
7292              chipSelf.bkHint = chipSvc;
7293              chipSelf._startBookingFromCta();
7294            });
7295            bubble.appendChild(chip);
7296          }
7297        }
7298        /* copy & translate buttons for assistant messages with content */
7299        if (msg.content && msg.content.length > 0 && !this.isLoading) {
7300          (function (content, wrapEl) {
7301            var copyBtn = document.createElement("button");
7302            copyBtn.className = "caf-copy-btn";
7303            copyBtn.type = "button";
7304            copyBtn.textContent = "⧉";
7305            copyBtn.setAttribute("aria-label", bkT("aria_copy"));
7306            copyBtn.addEventListener("click", function (e) {
7307              e.stopPropagation();
7308              copyText(content, copyBtn);
7309            });
7310            wrapEl.appendChild(copyBtn);
7311
7312            var transBtn = document.createElement("button");
7313            transBtn.className = "caf-translate-btn";
7314            transBtn.type = "button";
7315            transBtn.textContent = "🌐";
7316            transBtn.setAttribute("aria-label", bkT("aria_translate"));
7317            var pickerOpen = false;
7318            var pickerEl = null;
7319            transBtn.addEventListener("click", function (e) {
7320              e.stopPropagation();
7321              if (pickerOpen && pickerEl) {
7322                pickerEl.remove();
7323                pickerOpen = false;
7324                return;
7325              }
7326              pickerEl = document.createElement("div");
7327              pickerEl.className = "caf-lang-picker";
7328              TRANSLATE_LANGS.forEach(function (lang) {
7329                var btn = document.createElement("button");
7330                btn.className = "caf-lang-btn";
7331                btn.type = "button";
7332                btn.innerHTML = '<span class="caf-flag">' + lang.flag + '</span>' + lang.label;
7333                btn.addEventListener("click", function (ev) {
7334                  ev.stopPropagation();
7335                  pickerEl.remove();
7336                  pickerOpen = false;
7337                  self._translateMessage(content, lang.code);
7338                });
7339                pickerEl.appendChild(btn);
7340              });
7341              wrapEl.appendChild(pickerEl);
7342              pickerOpen = true;
7343              var closePicker = function (ev) {
7344                if (pickerEl && !pickerEl.contains(ev.target) && ev.target !== transBtn) {
7345                  pickerEl.remove();
7346                  pickerOpen = false;
7347                  document.removeEventListener("click", closePicker);
7348                }
7349              };
7350              /* BUGFIX: la chiusura con Esc rimuoveva il nodo ma lasciava
7351               * pickerOpen=true → servivano due attivazioni per riaprire. */
7352              pickerEl._cafCleanup = function () {
7353                pickerOpen = false;
7354                try { document.removeEventListener("click", closePicker); } catch (_e) {}
7355              };
7356              setTimeout(function () { document.addEventListener("click", closePicker); }, 0);
7357            });
7358            wrapEl.appendChild(transBtn);
7359
7360            /* mobile action bar — sits below the bubble, no overlap */
7361            var actBar = document.createElement("div");
7362            actBar.className = "caf-msg-actions";
7363            var mobCopy = document.createElement("button");
7364            mobCopy.type = "button";
7365            mobCopy.innerHTML = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg> Copia';
7366            mobCopy.addEventListener("click", function (e) {
7367              e.stopPropagation();
7368              copyText(content, mobCopy);
7369              mobCopy.classList.add("copied");
7370              var origHTML = mobCopy.innerHTML;
7371              mobCopy.innerHTML = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M20 6L9 17l-5-5"/></svg> Copiato';
7372              setTimeout(function () { mobCopy.classList.remove("copied"); mobCopy.innerHTML = origHTML; }, 1500);
7373            });
7374            actBar.appendChild(mobCopy);
7375            var mobTrans = document.createElement("button");
7376            mobTrans.type = "button";
7377            mobTrans.innerHTML = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><path d="M2 12h20M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></svg> Traduci';
7378            var mobPickerOpen = false, mobPickerEl = null;
7379            mobTrans.addEventListener("click", function (e) {
7380              e.stopPropagation();
7381              if (mobPickerOpen && mobPickerEl) { mobPickerEl.remove(); mobPickerOpen = false; return; }
7382              mobPickerEl = document.createElement("div");
7383              mobPickerEl.className = "caf-lang-picker";
7384              mobPickerEl.style.cssText = "position:absolute;bottom:32px;left:0;top:auto;right:auto";
7385              TRANSLATE_LANGS.forEach(function (lang) {
7386                var btn = document.createElement("button");
7387                btn.className = "caf-lang-btn"; btn.type = "button";
7388                btn.innerHTML = '<span class="caf-flag">' + lang.flag + '</span>' + lang.label;
7389                btn.addEventListener("click", function (ev) {
7390                  ev.stopPropagation(); mobPickerEl.remove(); mobPickerOpen = false;
7391                  self._translateMessage(content, lang.code);
7392                });
7393                mobPickerEl.appendChild(btn);
7394              });
7395              actBar.style.position = "relative";
7396              actBar.appendChild(mobPickerEl); mobPickerOpen = true;
7397              var closeMobPicker = function (ev) {
7398                if (mobPickerEl && !mobPickerEl.contains(ev.target) && ev.target !== mobTrans) {
7399                  mobPickerEl.remove(); mobPickerOpen = false; document.removeEventListener("click", closeMobPicker);
7400                }
7401              };
7402              setTimeout(function () { document.addEventListener("click", closeMobPicker); }, 0);
7403            });
7404            actBar.appendChild(mobTrans);
7405            wrapEl.appendChild(actBar);
7406          })(msg.content, wrap);
7407        }
7408      } else {
7409        bubble.textContent = msg.content;
7410      }
7411
7412      wrap.appendChild(bubble);
7413
7414      /* timestamp */
7415      if (msg.timestamp && msg.id !== "welcome") {
7416        var timeEl = document.createElement("div");
7417        timeEl.className = "caf-msg-time";
7418        timeEl.textContent = new Date(msg.timestamp).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
7419        wrap.appendChild(timeEl);
7420      }
7421
7422      row.appendChild(wrap);
7423      frag.appendChild(row);
7424    }
7425
7426    /* quick replies */
7427    if (
7428      this.messages.length === 1 &&
7429      this.messages[0].id === "welcome" &&
7430      o.quickReplies &&
7431      o.quickReplies.length > 0
7432    ) {
7433      var qr = document.createElement("div");
7434      qr.className = "caf-quick";
7435      /* small label over the quick replies */
7436      var qrLbl = document.createElement("div");
7437      qrLbl.className = "caf-quick-label";
7438      qrLbl.textContent = "Domande frequenti";
7439      qr.appendChild(qrLbl);
7440      /* pick an icon based on the text content — contextually meaningful */
7441      function pickQuickIcon(t) {
7442        var s = (t || "").toLowerCase();
7443        /* 1. Booking / appointment — calendar */
7444        if (/prenot|appuntamento|fissare\s+(un|l')/.test(s))
7445          return '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="4" width="18" height="18" rx="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/><path d="M8 14h.01M12 14h.01M16 14h.01"/></svg>';
7446        /* 2. Location / nearest / where — map pin */
7447        if (/vicin|dove|posizione|geoloc|indirizz|raggiun|tua\s+zona|a\s+me/.test(s))
7448          return '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"/><circle cx="12" cy="10" r="3"/></svg>';
7449        /* 3. CAF sede / centro — building */
7450        if (/\b(sede|sedi|centro|centri|caf|uffic)/.test(s)&&!(/servizi|elenco|list/.test(s)))
7451          return '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><rect x="4" y="2" width="16" height="20" rx="1"/><line x1="9" y1="6" x2="15" y2="6"/><line x1="9" y1="10" x2="15" y2="10"/><line x1="9" y1="14" x2="15" y2="14"/><path d="M10 22v-4h4v4"/></svg>';
7452        /* 4. Services / what we offer — grid of apps */
7453        if (/servizi|elenco|offerta|offrit[ae]|fate|che cosa\s+fa|assistenza/.test(s))
7454          return '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="7" height="7" rx="1.2"/><rect x="14" y="3" width="7" height="7" rx="1.2"/><rect x="14" y="14" width="7" height="7" rx="1.2"/><rect x="3" y="14" width="7" height="7" rx="1.2"/></svg>';
7455        /* 5. Deductions / expenses / money — Euro symbol */
7456        if (/detrarre|detrazion|detraibili|spes|rimborso|rimbors|bonus|agevolaz|sconto/.test(s))
7457          return '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M15.5 8.5A4.5 4.5 0 0 0 8 12a4.5 4.5 0 0 0 7.5 3.5"/><line x1="6" y1="10.5" x2="12" y2="10.5"/><line x1="6" y1="13.5" x2="12" y2="13.5"/></svg>';
7458        /* 6. Tax codes / 730 / ISEE / forms — document with signature lines */
7459        if (/\b(730|isee|cud|dichiarazion|modello|documento|successione|naspi|red|dsu)\b/.test(s))
7460          return '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8Z"/><polyline points="14 2 14 8 20 8"/>
7460<line x1="8" y1="13" x2="16" y2="13"/><line x1="8" y1="17" x2="13" y2="17"/></svg>';
7461        /* 7. IMU/TASI/TARI — house with small tag */
7462        if (/\b(imu|tasi|tari|tass|casa|immobil|prima\s+casa)\b/.test(s))
7463          return '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><polyline points="9 22 9 14 15 14 15 22"/></svg>';
7464        /* 8. Hours / opening / when — clock */
7465        if (/\b(orar|apertur|chius|quando|ora|che\s+ore)\b/.test(s))
7466          return '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>';
7467        /* 9. Contacts / phone / email — phone */
7468        if (/\b(contatt|telefono|numero|email|mail|scriv|chiam)\b/.test(s))
7469          return '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72c.13.96.37 1.9.72 2.81a2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45c.91.35 1.85.59 2.81.72A2 2 0 0 1 22 16.92z"/></svg>';
7470        /* 10. Documents needed / what to bring — folder or checklist */
7471        if (/document|serv\w*\s+servon|\bportar|cosa\s+mi\s+serve/.test(s))
7472          return '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 11l3 3L22 4"/><path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"/></svg>';
7473        /* 11. Pension — shield (security) */
7474        if (/\bpension|assegno\s+unico|inps\b/.test(s))
7475          return '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg>';
7476        /* 12. Info / details — info circle */
7477        if (/informazion|maggiori|più info|dettagl|approfond/.test(s))
7478          return '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg>';
7479        /* 13. Help / how-to — life buoy */
7480        if (/\b(aiuto|aiut|come\s+(faccio|si|fare|posso))\b/.test(s))
7481          return '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><circle cx="12" cy="12" r="4"/><line x1="4.93" y1="4.93" x2="9.17" y2="9.17"/><line x1="14.83" y1="14.83" x2="19.07" y2="19.07"/><line x1="14.83" y1="9.17" x2="19.07" y2="4.93"/><line x1="4.93" y1="19.07" x2="9.17" y2="14.83"/></svg>';
7482        /* 14. Generic question — question mark circle */
7483        if (/^(che|cosa|come|perché|quali|quale|quando|dove)\b/.test(s))
7484          return '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>';
7485        /* default: chat bubble */
7486        return '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>';
7487      }
7488      var arrowSvg = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><polyline points="9 18 15 12 9 6"/></svg>';
7489      for (var q = 0; q < o.quickReplies.length; q++) {
7490        (function (text) {
7491          var btn = document.createElement("button");
7492          btn.type = "button";
7493          btn.innerHTML =
7494            '<span class="caf-quick-ic" aria-hidden="true">' + pickQuickIcon(text) + '</span>' +
7495            '<span class="caf-quick-txt">' + escapeHtml(text) + '</span>' +
7496            '<span class="caf-quick-ar" aria-hidden="true">' + arrowSvg + '</span>';
7497          btn.addEventListener("click", function () {
7498            self._send(text);
7499          });
7500          qr.appendChild(btn);
7501        })(o.quickReplies[q]);
7502      }
7503      frag.appendChild(qr);
7504    }
7505
7506    var wasAtBottom = this.isAtBottom !== false;
7507    this.messagesEl.innerHTML = "";
7508    this.messagesEl.appendChild(frag);
7509    if (this._bkEl && this.bkStep !== "idle") this._bkRender();
7510    if (this._pwdEl && this.pwdStep !== "idle") this._pwdRender();
7511    if (wasAtBottom) { this._scrollToBottom(); } else { this._updateScrollBtn(); }
7512    /* keep voice transcript in sync when booking pushes messages */
7513    if (this._voiceMode) this._renderVoiceTranscript();
7514  };
7515
7516  /* ── update only the streaming bubble (no DOM rebuild) ────── */
7517  CafChat.prototype._updateStreamingBubble = function (content) {
7518    var bubble = this._streamingBubble;
7519    if (!bubble) return;
7520    var o = this.opts;
7521    bubble.innerHTML = renderMarkdown(content, o.color);
7522    /* NOTA: NON renderizziamo le card durante lo streaming.
7523     * Il bubble.innerHTML viene riscritto ad ogni chunk (~80ms): se
7524     * costruissimo le card qui, ogni interazione utente con la qty o il
7525     * bottone "Aggiungi" verrebbe persa al chunk successivo. Gli anchor
7526     * sono nascosti via CSS (display:none); il _renderMessages() finale
7527     * (chiamato a streaming completato) costruirà le card una sola volta
7528     * con stato pulito. */
7529    var caret = document.createElement("span");
7530    caret.className = "caf-caret";
7531    caret.style.background = o.color + "e6";
7532    bubble.appendChild(caret);
7533    var el = this.messagesEl;
7534    var scrollTop = el.scrollTop;
7535    var clientH = el.clientHeight;
7536    var scrollH = el.scrollHeight;
7537    var nearBottom = scrollTop + clientH >= scrollH - 150;
7538    if (nearBottom) {
7539      el.scrollTop = el.scrollHeight;
7540    }
7541  };
7542
7543  /* ══════════════════════════════════════════════════════════════
7544     ██  BOOKING SYSTEM – mirrors ChatWidget.tsx / useBookingFlow
7545     ══════════════════════════════════════════════════════════════ */
7546
7547  /* spawn celebratory sparkles around a DOM element (position:relative) */
7548  CafChat.prototype._bkSpawnSparkles = function(target,count){
7549    if(!target||!target.appendChild)return;
7550    count=count||10;
7551    var colors=["#fbbf24","#f59e0b","#10b981","#3b82f6",this.opts.color,"#ec4899"];
7552    var sparkleSvg='<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 2l2.4 7.2L22 12l-7.6 2.8L12 22l-2.4-7.2L2 12l7.6-2.8z"/></svg>';
7553    var host=target;
7554    for(var i=0;i<count;i++){
7555      var sp=document.createElement("span");
7556      sp.className="caf-sparkle";
7557      sp.style.color=colors[i%colors.length];
7558      sp.innerHTML=sparkleSvg;
7559      var angle=(Math.PI*2*i)/count+(Math.random()*0.4-0.2);
7560      var dist=50+Math.random()*35;
7561      sp.style.setProperty("--sx",(Math.cos(angle)*dist)+"px");
7562      sp.style.setProperty("--sy",(Math.sin(angle)*dist)+"px");
7563      sp.style.animationDelay=(i*35)+"ms";
7564      sp.style.animationDuration=(1000+Math.random()*400)+"ms";
7565      host.appendChild(sp);
7566      (function(el){setTimeout(function(){if(el.parentNode)el.parentNode.removeChild(el);},1500);})(sp);
7567    }
7568  };
7569
7570  /* ── auto-detect user's province via geolocation + reverse geocoding ── */
7571  CafChat.prototype._bkAutoDetectProvince = async function(renderFn){
7572    var self=this;
7573    /* DEMOPROMO: nessun GPS/Nominatim — simula il rilevamento e auto-seleziona
7574     * la prima provincia di esempio. */
7575    if(self.opts&&self.opts.demopromo){
7576      self._bkAutoProvStatus={type:"loading",msg:"Richiesta posizione (demo)..."};renderFn&&renderFn();
7577      setTimeout(function(){
7578        var p=(self.bkProvince&&self.bkProvince[0])||null;
7579        if(!p){self._bkAutoProvStatus={type:"err",msg:"Posizione non disponibile. Scegli manualmente qui sotto."};renderFn&&renderFn();return;}
7580        self._bkAutoProvStatus={type:"ok",msg:"Trovato: "+p.descrizione+" (demo). Ti porto ai servizi..."};renderFn&&renderFn();
7581        setTimeout(function(){if(self.bkStep==="selectProvincia")self._bkPickProv(p);},700);
7582      },700);
7583      return;
7584    }
7585    if(window.isSecureContext===false){self._bkAutoProvStatus={type:"err",msg:"La geolocalizzazione richiede HTTPS."};renderFn&&renderFn();return;}
7586    var provSnapshot=self.bkProvince;
7587    self._bkAutoProvStatus={type:"loading",msg:"Richiesta posizione..."};renderFn&&renderFn();
7588    /* Watchdog: alcuni browser (iOS Safari, Android+permissions glitch)
7589     * non chiamano mai i callback success/error di getCurrentPosition,
7590     * ignorando anche il timeout. Forziamo errore visibile dopo 18s — basta a
7591     * coprire il budget interno (GPS cityLevel 7s + IP parallelo ~3s +
7592     * Nominatim 6s) senza lasciare l'utente sul loading molto oltre la fine
7593     * reale del lavoro. */
7594    if(self._bkAutoProvWatchdog){clearTimeout(self._bkAutoProvWatchdog);}
7595    self._bkAutoProvWatchdog=setTimeout(function(){
7596      self._bkAutoProvWatchdog=null;
7597      if(self._bkAutoProvStatus&&self._bkAutoProvStatus.type==="loading"){
7598        self._bkAutoProvStatus={type:"err",msg:"Posizione non disponibile. Scegli manualmente qui sotto."};
7599        renderFn&&renderFn();
7600      }
7601    },18000);
7602
7603    /* Strategia identica a _bkStart: GPS browser city-level → IP geo fallback.
7604     * Risolve il caso più comune di failure: "permesso autorizzato ma il
7605     * browser non sa calcolare la posizione" (desktop senza GPS, Wi-Fi
7606     * positioning fallito, Linux senza geoclue, ecc.). cityLevel: qui serve
7607     * solo il match della PROVINCIA, l'alta precisione non aggiunge nulla. */
7608    var coords=null;var gpsErr=null;
7609    try{
7610      coords=await self._bkGetGeoPosition({cityLevel:true});
7611    }catch(e){
7612      gpsErr=e;
7613      /* Permesso negato esplicitamente: NON proviamo IP (rispetta scelta utente). */
7614      if(e&&e.code===1){
7615        if(self._bkAutoProvWatchdog){clearTimeout(self._bkAutoProvWatchdog);self._bkAutoProvWatchdog=null;}
7616        self._bkAutoProvStatus={type:"err",msg:"Permesso di geolocalizzazione negato dal browser."};
7617        renderFn&&renderFn();return;
7618      }
7619      /* Altri errori (timeout, position-unavailable): fallback IP. */
7620      self._bkAutoProvStatus={type:"loading",msg:"Provo a stimare la posizione..."};renderFn&&renderFn();
7621      try{
7622        coords=await self._bkGetIpGeoPosition();
7623      }catch(_e2){
7624        if(self._bkAutoProvWatchdog){clearTimeout(self._bkAutoProvWatchdog);self._bkAutoProvWatchdog=null;}
7625        var emsg="Posizione non disponibile.";
7626        if(gpsErr&&gpsErr.code===2)emsg="Posizione non disponibile (segnale GPS/Wi-Fi assente).";
7627        else if(gpsErr&&gpsErr.code===3)emsg="Richiesta posizione scaduta.";
7628        self._bkAutoProvStatus={type:"err",msg:emsg+" Scegli manualmente qui sotto."};
7629        renderFn&&renderFn();return;
7630      }
7631    }
7632    if(self._bkAutoProvWatchdog){clearTimeout(self._bkAutoProvWatchdog);self._bkAutoProvWatchdog=null;}
7633    if(self.bkProvince!==provSnapshot||self.bkStep!=="selectProvincia")return;
7634
7635    try{
7636      self._bkAutoProvStatus={type:"loading",msg:"Identifico la tua provincia..."};renderFn&&renderFn();
7637      /* reverse-geocode via Nominatim to get Italian province/city */
7638      var ctrl=new AbortController();var tid=setTimeout(function(){ctrl.abort();},6000);
7639      /* GDPR minimizzazione: 2 decimali (~1 km) bastano per la provincia (zoom=10). */
7640      var _glat=(typeof coords.lat==="number"?coords.lat:parseFloat(coords.lat)).toFixed(2);
7641      var _glon=(typeof coords.lon==="number"?coords.lon:parseFloat(coords.lon)).toFixed(2);
7642      var r=await fetch("https://nominatim.openstreetmap.org/reverse?format=json&zoom=10&addressdetails=1&lat="+_glat+"&lon="+_glon,{signal:ctrl.signal,headers:{"Accept":"application/json"}});
7643      clearTimeout(tid);
7644      if(self.bkProvince!==provSnapshot||self.bkStep!=="selectProvincia")return;
7645      if(!r.ok){self._bkAutoProvStatus={type:"err",msg:"Impossibile identificare la zona. Scegli manualmente."};renderFn&&renderFn();return;}
7646      var data=await r.json();
7647      var addr=(data&&data.address)||{};
7648      var candidates=[addr.county,addr.province,addr.state_district,addr.city,addr.town,addr.municipality].filter(Boolean);
7649      var matched=null;
7650      for(var i=0;i<candidates.length&&!matched;i++){
7651        matched=matchProvince(candidates[i],self.bkProvince);
7652      }
7653      if(!matched){
7654        /* build a human-readable location string from reverse-geocoded address */
7655        var locParts=[addr.city||addr.town||addr.village||addr.municipality,addr.county||addr.province||addr.state_district,addr.state].filter(Boolean);
7656        /* dedupe (city and county can be the same, e.g. "Roma, Roma") */
7657        var seen={},locUnique=[];
7658        for(var lp=0;lp<locParts.length;lp++){var k=locParts[lp].toLowerCase();if(!seen[k]){seen[k]=true;locUnique.push(locParts[lp]);}}
7659        var locStr=locUnique.join(", ")||(data&&data.display_name)||"posizione sconosciuta";
7660        self._bkAutoProvStatus={type:"err",msg:"Sei a "+locStr+", ma questa provincia non è tra quelle disponibili. Scegli manualmente."};
7661        renderFn&&renderFn();return;
7662      }
7663      self._bkAutoProvStatus={type:"ok",msg:"Trovato: "+matched.descrizione+". Ti porto ai servizi..."};
7664      renderFn&&renderFn();
7665      setTimeout(function(){if(self.bkStep==="selectProvincia")self._bkPickProv(matched);},900);
7666    }catch(e){
7667      self._bkAutoProvStatus={type:"err",msg:"Errore nella ricerca. Riprova."};renderFn&&renderFn();
7668    }
7669  };
7670
7671  /* ── geolocation + geocoding helpers ─────────────────────── */
7672  CafChat.prototype._bkHaversineKm = function(lat1,lon1,lat2,lon2){
7673    var R=6371,dLat=(lat2-lat1)*Math.PI/180,dLon=(lon2-lon1)*Math.PI/180;
7674    var a=Math.sin(dLat/2)*Math.sin(dLat/2)+Math.cos(lat1*Math.PI/180)*Math.cos(lat2*Math.PI/180)*Math.sin(dLon/2)*Math.sin(dLon/2);
7675    return R*2*Math.atan2(Math.sqrt(a),Math.sqrt(1-a));
7676  };
7677  CafChat.prototype._bkGeocodeSede = async function(s){
7678    if(s._lat!==undefined&&s._lon!==undefined)return{lat:s._lat,lon:s._lon};
7679    /* 1) USA COORDINATE DAL BACKEND se presenti (campi possibili). Evita
7680     *    completamente Nominatim e dà precisione massima. */
7681    var bLat=null,bLon=null;
7682    if(typeof s.lat==="number"&&typeof s.lon==="number"){bLat=s.lat;bLon=s.lon;}
7683    else if(typeof s.latitude==="number"&&typeof s.longitude==="number"){bLat=s.latitude;bLon=s.longitude;}
7684    else if(typeof s.latitudine==="number"&&typeof s.longitudine==="number"){bLat=s.latitudine;bLon=s.longitudine;}
7685    else if(s.coordinate&&typeof s.coordinate.lat==="number"&&typeof s.coordinate.lon==="number"){bLat=s.coordinate.lat;bLon=s.coordinate.lon;}
7686    else if(s.geo&&typeof s.geo.lat==="number"&&typeof s.geo.lon==="number"){bLat=s.geo.lat;bLon=s.geo.lon;}
7687    /* Anche stringhe valide (es. "43.7696") */
7688    else if(typeof s.lat==="string"&&typeof s.lon==="string"&&!isNaN(+s.lat)&&!isNaN(+s.lon)){bLat=+s.lat;bLon=+s.lon;}
7689    /* Sanity check rigoroso: dentro l'Italia. Scarta:
7690     *  - (0,0) "Null Island" (backend con campo non popolato)
7691     *  - Coords estere accidentali
7692     *  - NaN/Infinity */
7693    if(bLat!==null&&bLon!==null&&isFinite(bLat)&&isFinite(bLon)
7694       &&bLat>=35&&bLat<=48&&bLon>=6&&bLon<=19){
7695      s._lat=bLat;s._lon=bLon;return{lat:bLat,lon:bLon};
7696    }
7697    /* 2) FALLBACK: geocoding via Nominatim. Query strutturata per
7698     *    massimizzare precisione: street + city + Italia + viewbox Italia. */
7699    var street=(s.indirizzo||"").trim();
7700    var city=(s.citta||"").trim();
7701    if(!street&&!city)return null;
7702    /* Nominatim "structured" search ha più precisione del free-text
7703     * quando abbiamo street+city separati. */
7704    var url="https://nominatim.openstreetmap.org/search?format=json&limit=1&countrycodes=it&addressdetails=0";
7705    if(street)url+="&street="+encodeURIComponent(street);
7706    if(city)url+="&city="+encodeURIComponent(city);
7707    /* Bound dell'Italia (lon_min,lat_min,lon_max,lat_max): scarta risultati esteri */
7708    url+="&viewbox=6.6,47.1,18.5,35.5&bounded=1";
7709    try{
7710      /* Timeout 5s: leggermente più alto della versione precedente perché
7711       * la structured search può essere un filo più lenta. */
7712      var ctrl=new AbortController();var tid=setTimeout(function(){ctrl.abort();},5000);
7713      var r=await fetch(url,{signal:ctrl.signal,headers:{"Accept":"application/json"}});
7714      clearTimeout(tid);
7715      if(!r.ok)return null;
7716      var j=await r.json();
7717      if(j&&j[0]&&j[0].lat&&j[0].lon){
7718        var lat=+j[0].lat,lon=+j[0].lon;
7719        /* Sanity check: dentro l'Italia */
7720        if(lat>=35&&lat<=48&&lon>=6&&lon<=19){
7721          s._lat=lat;s._lon=lon;return{lat:lat,lon:lon};
7722        }
7723      }
7724      /* 3) FALLBACK al free-text (q=) se structured fallisce. Capita per
7725       *    indirizzi non standardizzati o numeri civici mancanti. */
7726      var q=[street,city,"Italia"].filter(Boolean).join(", ");
7727      if(!q)return null;
7728      var ctrl2=new AbortController();var tid2=setTimeout(function(){ctrl2.abort();},5000);
7729      var r2=await fetch("https://nominatim.openstreetmap.org/search?format=json&limit=1&countrycodes=it&q="+encodeURIComponent(q),{signal:ctrl2.signal,headers:{"Accept":"application/json"}});
7730      clearTimeout(tid2);
7731      if(!r2.ok)return null;
7732      var j2=await r2.json();
7733      if(j2&&j2[0]&&j2[0].lat&&j2[0].lon){
7734        var lat2=+j2[0].lat,lon2=+j2[0].lon;
7735        if(lat2>=35&&lat2<=48&&lon2>=6&&lon2<=19){
7736          s._lat=lat2;s._lon=lon2;return{lat:lat2,lon:lon2};
7737        }
7738      }
7739    }catch(e){}
7740    return null;
7741  };
7742  CafChat.prototype._bkFindNearestSede = async function(renderFn){
7743    var self=this;
7744    /* DEMOPROMO: nessun GPS/geocoding — assegna distanze fittizie crescenti e
7745     * marca la prima sede come la più vicina. */
7746    if(self.opts&&self.opts.demopromo){
7747      self._bkNearestStatus={type:"loading",msg:"Ricerca sede più vicina (demo)..."};renderFn&&renderFn();
7748      setTimeout(function(){
7749        var arr=self.bkSedi||[];var s=arr[0]||null;
7750        if(!s){self._bkNearestStatus={type:"err",msg:"Nessuna sede disponibile."};renderFn&&renderFn();return;}
7751        for(var i=0;i<arr.length;i++){arr[i]._distKm=+(0.8+i*1.7).toFixed(1);}
7752        self._bkNearestCode=s.codice;self._bkUserCoords={lat:45.49,lon:12.24};
7753        self._bkNearestStatus={type:"ok",msg:"Sede più vicina (demo): "+s.descrizione+" · "+s._distKm+" km"};
7754        renderFn&&renderFn();
7755      },700);
7756      return;
7757    }
7758    if(window.isSecureContext===false){self._bkNearestStatus={type:"err",msg:"La geolocalizzazione richiede una connessione sicura (HTTPS)."};renderFn&&renderFn();return;}
7759    /* snapshot bkSedi reference to avoid race conditions if user navigates mid-search */
7760    var sedeSnapshot=self.bkSedi;
7761    self._bkNearestStatus={type:"loading",msg:"Richiesta posizione precisa in corso..."};renderFn&&renderFn();
7762    /* GPS HIGH-PRECISION OBBLIGATORIO per il calcolo distanza:
7763     * il vecchio low-accuracy + cache 10min + fallback IP-geo davano
7764     * coords city-level che facevano scegliere "una sede a caso" come la
7765     * più vicina (errore tipico fino a 15 km in città). Ora pretendiamo
7766     * GPS fresco ad alta precisione e segnaliamo l'accuratezza all'utente. */
7767    var userCoords=null;
7768    try{
7769      userCoords=await self._bkGetGeoPositionPrecise();
7770    }catch(e){
7771      if(e&&e.code===1){
7772        self._bkNearestStatus={type:"err",msg:"Permesso di geolocalizzazione negato. Abilitalo nelle impostazioni del browser per trovare la sede più vicina."};
7773        renderFn&&renderFn();return;
7774      }
7775      var emsg="Posizione GPS non disponibile";
7776      if(e&&e.code===2)emsg="Posizione non disponibile (segnale GPS/Wi-Fi assente)";
7777      else if(e&&e.code===3)emsg="Richiesta posizione scaduta";
7778      /* Se il primo tentativo è già scaduto sul watchdog (code 3 = prompt
7779       * ignorato / callback GPS mai chiamati), un SECONDO GPS completo
7780       * sbatterebbe sullo stesso muro RADDOPPIANDO l'attesa (era il famigerato
7781       * caso ~44s: due watchdog impilati). Errore esplicito subito. Il retry
7782       * low-acc ha senso solo per code 2 (posizione momentaneamente non
7783       * disponibile), dove una stima cache/low-accuracy può comunque riuscire. */
7784      if(e&&e.code===3){
7785        self._bkNearestStatus={type:"err",msg:emsg+". Riprova all'aperto o scegli la sede manualmente."};
7786        renderFn&&renderFn();return;
7787      }
7788      /* Fallback al GPS standard (low-acc + cache) SE high-precision fallisce.
7789       * Avvisiamo che la precisione potrebbe essere ridotta. */
7790      try{
7791        self._bkNearestStatus={type:"loading",msg:emsg+". Provo con posizione approssimativa…"};renderFn&&renderFn();
7792        userCoords=await self._bkGetGeoPosition();
7793      }catch(_e2){
7794        /* Niente fallback IP-geo: con city-level accuracy, il "nearest"
7795         * sarebbe quasi sempre sbagliato e l'utente sceglierebbe una sede
7796         * a caso pensando sia la più vicina. Meglio errore esplicito. */
7797        self._bkNearestStatus={type:"err",msg:emsg+". Riprova all'aperto o scegli la sede manualmente."};
7798        renderFn&&renderFn();return;
7799      }
7800    }
7801    if(self.bkSedi!==sedeSnapshot||self.bkStep!=="selectSede")return;
7802    /* Salviamo le coords sull'istanza per usi futuri (background sort, ecc.) */
7803    self._bkUserCoords={lat:userCoords.lat,lon:userCoords.lon};
7804    /* Warning se l'accuracy è troppo bassa (>5km): probabile IP-geo o
7805     * Wi-Fi positioning impreciso. Ranking inaffidabile in città. */
7806    var accKm=(userCoords.accuracy&&isFinite(userCoords.accuracy))?(userCoords.accuracy/1000):0;
7807    if(accKm>5){
7808      self._bkLogEvent&&self._bkLogEvent("nearest_low_accuracy",{accuracy_km:accKm.toFixed(2)});
7809    }
7810    /* Esegue il resto della pipeline (geocoding sedi + sort) come prima. */
7811    (async function(){
7812      try{
7813        var uLat=userCoords.lat,uLon=userCoords.lon;
7814        /* PRECISIONE > VELOCITÀ: cap altissimo (200) per coprire qualunque
7815         * regione italiana (Toscana ha 70+ sedi, Lombardia 80+). L'early-stop
7816         * conservativo (≥15 geocoded E nearest<10km) garantisce comunque
7817         * tempi rapidi in città. Le sedi con coordinate backend (s.lat/lon)
7818         * sono istantanee → ordinarle prima accelera ulteriormente. */
7819        var MAX_SEDI=200;
7820        var sortedForGeo=sedeSnapshot.slice(0,MAX_SEDI).slice().sort(function(a,b){
7821          var ac=(a._lat!==undefined&&a._lon!==undefined)||(typeof a.lat==="number"&&typeof a.lon==="number")?0:1;
7822          var bc=(b._lat!==undefined&&b._lon!==undefined)||(typeof b.lat==="number"&&typeof b.lon==="number")?0:1;
7823          return ac-bc;
7824        });
7825        var total=sortedForGeo.length;
7826        self._bkNearestStatus={type:"loading",msg:"Ricerca sede più vicina... (0/"+total+")"};renderFn&&renderFn();
7827        var coords=[];
7828        var batchSize=4;
7829        /* DEADLINE globale: con molte sedi senza coords backend il geocoding
7830         * Nominatim (2 fetch/sede, batch) poteva durare MINUTI. Cap a 15s:
7831         * usiamo i risultati raccolti finora (l'utente può comunque scegliere
7832         * manualmente o l'esito parziale è già utile). */
7833        var nearestDeadline=Date.now()+15000;
7834        /* EARLY STOP CONDIZIONALE: ferma SOLO se abbiamo geocoded almeno
7835         * 15 sedi E la più vicina è < 10km (confidenza alta che le altre
7836         * non saranno più vicine). Altrimenti continua fino al 100%. */
7837        var EARLY_MIN=15;
7838        function nearestKmSoFar(){
7839          if(!coords.length)return Infinity;
7840          var min=Infinity;
7841          for(var z=0;z<coords.length;z++){
7842            var d=self._bkHaversineKm(uLat,uLon,coords[z].lat,coords[z].lon);
7843            if(d<min)min=d;
7844          }
7845          return min;
7846        }
7847        for(var i=0;i<sortedForGeo.length;i+=batchSize){
7848          /* bail out if user navigated away (bkSedi was replaced or step changed) */
7849          if(self.bkSedi!==sedeSnapshot||self.bkStep!=="selectSede")return;
7850          var batch=sortedForGeo.slice(i,i+batchSize);
7851          var res=await Promise.all(batch.map(function(s){return self._bkGeocodeSede(s);}));
7852          for(var k=0;k<res.length;k++){if(res[k])coords.push({sede:batch[k],lat:res[k].lat,lon:res[k].lon});}
7853          /* Aggiorna status con progresso */
7854          if(self.bkSedi===sedeSnapshot&&self.bkStep==="selectSede"){
7855            self._bkNearestStatus={type:"loading",msg:"Ricerca sede più vicina... ("+Math.min(i+batchSize,total)+"/"+total+")"};
7856            renderFn&&renderFn();
7857          }
7858          /* Early stop conservativo */
7859          if(coords.length>=EARLY_MIN&&nearestKmSoFar()<10)break;
7860          /* Cap temporale: oltre i 15s ci fermiamo con quanto raccolto. */
7861          if(Date.now()>nearestDeadline){self._bkLogEvent&&self._bkLogEvent("nearest_deadline_hit",{geocoded:coords.length,of:total});break;}
7862          if(i+batchSize<sortedForGeo.length)await new Promise(function(r){setTimeout(r,200);});
7863        }
7864        if(self.bkSedi!==sedeSnapshot||self.bkStep!=="selectSede")return;
7865        if(!coords.length){self._bkNearestStatus={type:"err",msg:"Non riesco a localizzare le sedi. Puoi sceglierne una manualmente."};renderFn&&renderFn();return;}
7866        coords.forEach(function(c){c.dist=self._bkHaversineKm(uLat,uLon,c.lat,c.lon);c.sede._distKm=c.dist;});
7867        coords.sort(function(a,b){return a.dist-b.dist;});
7868        /* sort sedi by distance (sedi without coords at the end) */
7869        self.bkSedi.sort(function(a,b){
7870          var da=a._distKm===undefined?Infinity:a._distKm;
7871          var db=b._distKm===undefined?Infinity:b._distKm;
7872          return da-db;
7873        });
7874        self._bkNearestCode=coords[0].sede.codice||coords[0].sede.descrizione;
7875        var km=coords[0].dist;
7876        var kmStr=km<1?(Math.round(km*1000)+" m"):(km.toFixed(1)+" km");
7877        /* Hint accuratezza: se la posizione utente era imprecisa (>5km),
7878         * avvisiamo che il ranking potrebbe non essere il più affidabile. */
7879        var accHint="";
7880        if(accKm>5)accHint=" — posizione approssimativa, verifica";
7881        self._bkNearestStatus={type:"ok",msg:"Sede più vicina trovata: "+coords[0].sede.descrizione+" (a "+kmStr+" da te"+accHint+")"};
7882        /* trigger the celebration animation on render */
7883        self._bkNearestJustFound=true;
7884        renderFn&&renderFn();
7885        /* spawn sparkles on the nearest tile AFTER render */
7886        requestAnimationFrame(function(){
7887          requestAnimationFrame(function(){
7888            var tile=self._bkFsEl?self._bkFsEl.querySelector(".caf-bk-fs-tile.nearest-reveal"):null;
7889            if(tile)self._bkSpawnSparkles(tile,14);
7890            self._bkNearestJustFound=false;
7891          });
7892        });
7893      }catch(e){
7894        self._bkNearestStatus={type:"err",msg:"Errore durante la ricerca. Riprova."};
7895        renderFn&&renderFn();
7896      }
7897    })();
7898  };
7899
7900  CafChat.prototype._bkFetchOnce = async function(action,body,opts){
7901    var self=this;opts=opts||{};
7902    var r;
7903    /* timeout configurabile (default 15s come plannerApi.ts; i fetch di catalogo
7904     * passano un valore più corto). `opts.signal` = cancellazione esterna
7905     * (bottone "Annulla"): la colleghiamo al nostro AbortController. */
7906    var to=opts.timeoutMs||15000;
7907    var ctrl=new AbortController();var tid=setTimeout(function(){ctrl.abort();},to);
7908    var ext=opts.signal,onExt=function(){try{ctrl.abort();}catch(_e){}};
7909    if(ext){if(ext.aborted)ctrl.abort();else ext.addEventListener("abort",onExt);}
7910    var cleanup=function(){clearTimeout(tid);if(ext)ext.removeEventListener("abort",onExt);};
7911    try{r=await fetch(this.opts.baseUrl+"/api/planner/"+action,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(body||{}),signal:ctrl.signal});}
7912    catch(netErr){cleanup();
7913      if(netErr instanceof DOMException&&netErr.name==="AbortError"){
7914        /* Annullamento utente: NON è transient → niente retry, niente popup. */
7915        if(ext&&ext.aborted){var ce={error:"Operazione annullata.",code:"cancelled"};self._bkLastErr=null;throw ce;}
7916        var te={error:"Il server non risponde. Riprova tra qualche istante.",code:"timeout",plannerMessage:"AbortError timeout after "+to+"ms on "+action};self._bkLastErr=te;throw te;}
7917      var ne={error:"Impossibile raggiungere il server. Verifica la connessione.",code:"network",plannerMessage:(netErr&&netErr.message)?netErr.message:String(netErr)};self._bkLastErr=ne;throw ne;}
7918    finally{cleanup();}
7919    if(!r.ok){var e={};try{e=await r.json();}catch(x){e={error:"Errore di comunicazione con il server.",code:"unknown"};}
7920      if(!e.plannerMessage)e.plannerMessage="HTTP "+r.status+" on /api/planner/"+action;
7921      /* tag 5xx come transient per retry */
7922      if(r.status>=500&&r.status<600&&!e.code)e.code=String(r.status);
7923      self._bkLastErr=e;throw e;}
7924    var json;try{json=await r.json();}catch(parseErr){var pe={error:"Risposta non valida dal server.",code:"server",plannerMessage:"JSON parse error on /api/planner/"+action};self._bkLastErr=pe;throw pe;}
7925    var data=json&&json.data;
7926    self._bkLastErr=null;
7927    return data||[];
7928  };
7929
7930  /* Wrapper con retry esponenziale (#58): re-tenta solo errori transient
7931   * (timeout/network/5xx). Mostra toast non bloccante al 2° tentativo
7932   * così l'utente sa che stiamo riprovando in background. */
7933  CafChat.prototype._bkFetch = async function(action,body,opts){
7934    var self=this;opts=opts||{};
7935    /* DEMOPROMO: dirotta OGNI chiamata dati (regioni/province/servizi/sedi/
7936     * spazi/prenota) su dati di esempio integrati. Nessuna richiesta esce mai
7937     * verso /api/planner. Unico choke-point: tutti i fetch del wizard passano
7938     * di qui. Flag spento ⇒ ramo standard sottostante, invariato. */
7939    if(self.opts&&self.opts.demopromo)return self._bkDemoFetch(action,body);
7940    /* AbortController condiviso da TUTTA la catena retry: il bottone "Annulla"
7941     * (_bkCancelLoading) lo abortisce e ferma sia l'attesa sia i retry.
7942     * I fetch background (es. prefetch slot) passano opts.background=true e NON
7943     * registrano _bkActiveCtrl, per non far puntare l'Annulla a una fetch
7944     * non-bloccante. */
7945    var ctrl=new AbortController();
7946    if(!opts.background)self._bkActiveCtrl=ctrl;
7947    var once=function(attempt){
7948      if(attempt>0&&self._bkShowToast){
7949        self._bkShowToast({type:"warn",title:bkT("toast_slow_title"),msg:bkT("toast_slow_msg",attempt+1)});
7950      }
7951      return self._bkFetchOnce(action,body,{timeoutMs:opts.timeoutMs,signal:ctrl.signal});
7952    };
7953    try{
7954      /* opts.noRetry: azioni NON idempotenti (es. "prenota") — un solo tentativo. */
7955      if(opts.noRetry)return await once(0);
7956      return await bkRetryWithBackoff(once,{maxAttempts:3,baseMs:500,maxMs:2500});
7957    }finally{ if(!opts.background&&self._bkActiveCtrl===ctrl)self._bkActiveCtrl=null; }
7958  };
7959
7960  /* ──────────────────────────────────────────────────────────────────────
7961   * DEMOPROMO — sorgente dati di ESEMPIO (Veneto). Attiva SOLO con
7962   * window.__CAF_DEMOPROMO__ === true (env DEMOPROMO=si). Sostituisce le
7963   * risposte di /api/planner/{regioni,province,servizi,sedi,spazi,prenota}
7964   * con dati finti ma plausibili, nelle STESSE shape del backend reale, così
7965   * il resto del wizard li tratta in modo identico. Nessuna chiamata di rete.
7966   * ────────────────────────────────────────────────────────────────────── */
7967  /* DEMOPROMO: il brand delle sedi/dati di esempio NON deve essere fisso
7968   * "CAF CISL": deve riportare il nome del chatbot (NEXT_PUBLIC_CHATBOT_NAME
7969   * -> window.__CAF_CHATBOT_NAME__), iniettato dalla route prima di questo
7970   * script. Fallback al default storico solo se assente. */
7971  var DEMO_BRAND = (typeof window !== "undefined" && window.__CAF_CHATBOT_NAME__) || "CAF CISL Veneto";
7972  /* Profilo del dataset demo. "cafacli" => sedi/regioni NAZIONALI CAF ACLI;
7973   * qualsiasi altro valore => dataset storico Veneto (CAF CISL). Selezione
7974   * esplicita via env DEMO_PROFILE (iniettata come window.__CAF_DEMO_PROFILE__),
7975   * con fallback inferito dal brand: un nome che contiene "ACLI" attiva il
7976   * profilo cafacli senza bisogno della env. */
7977  var DEMO_PROFILE = ((typeof window !== "undefined" && window.__CAF_DEMO_PROFILE__) || "").toString().trim().toLowerCase();
7978  if(!DEMO_PROFILE && /acli/i.test(DEMO_BRAND)) DEMO_PROFILE = "cafacli";
7979
7980  function demoPad2(n){return n<10?"0"+n:""+n;}
7981  function demoSlug(s){
7982    return String(s||"").toLowerCase()
7983      .replace(/[àáâ]/g,"a").replace(/[èéê]/g,"e")
7984      .replace(/[ìíî]/g,"i").replace(/[òóô]/g,"o")
7985      .replace(/[ùúû]/g,"u")
7986      .replace(/[^a-z0-9]+/g,".").replace(/^\.+/,"").replace(/\.+$/,"");
7987  }
7988  /* slot di esempio condivisi: prossimi ~8 giorni feriali, mattina+pomeriggio,
7989   * nelle shape compatte del Planner reale (data "YYYYMMDD", orari "HHMM"). */
7990  function demoSpazi(){
7991    var slots=[];var d=new Date();var businessDays=0;var guard=0;
7992    var times=[["0900","0930"],["0930","1000"],["1000","1030"],["1100","1130"],["1130","1200"],["1430","1500"],["1500","1530"],["1600","1630"]];
7993    while(businessDays<8&&guard<20){
7994      guard++;d.setDate(d.getDate()+1);
7995      var dow=d.getDay();if(dow===0||dow===6)continue;
7996      businessDays++;
7997      var ymd=""+d.getFullYear()+demoPad2(d.getMonth()+1)+demoPad2(d.getDate());
7998      for(var t=0;t<times.length;t++){
7999        slots.push({id_spazio:"DEMO-"+ymd+"-"+times[t][0],data:ymd,ora_inizio:times[t][0],ora_fine:times[t][1],sportello_id:"1"});
8000      }
8001    }
8002    return slots;
8003  }
8004
8005  /* ── Dataset VENETO (storico, CAF CISL) — invariato ─────────── */
8006  var VEN_REGIONI=[{codice:"VEN",descrizione:"Veneto",connessione:"demo",codiceRegione:"05"}];
8007  var VEN_PROVINCE=[
8008    {codice:"VE",descrizione:"Venezia"},
8009    {codice:"PD",descrizione:"Padova"},
8010    {codice:"TV",descrizione:"Treviso"},
8011    {codice:"VR",descrizione:"Verona"},
8012    {codice:"VI",descrizione:"Vicenza"},
8013    {codice:"RO",descrizione:"Rovigo"},
8014    {codice:"BL",descrizione:"Belluno"}
8015  ];
8016  var VEN_SERVIZI=[
8017    {codice:"730",descrizione:"Dichiarazione dei redditi – Modello 730/2026",desonline:"Modello 730/2026",tempo:"30",documenti:"CU, spese detraibili, eventuali deleghe",online:true,codtipologia:"FISCO",destipologia:"Servizi fiscali"},
8018    {codice:"ISEE",descrizione:"ISEE / DSU 2026",desonline:"ISEE 2026",tempo:"30",documenti:"Documenti redditi e patrimonio nucleo familiare",online:true,codtipologia:"FISCO",destipologia:"Servizi fiscali"},
8019    {codice:"IMU",descrizione:"Calcolo e versamento IMU",desonline:"IMU",tempo:"20",documenti:"Visure catastali immobili",online:true,codtipologia:"FISCO",destipologia:"Servizi fiscali"},
8020    {codice:"SUCC",descrizione:"Successioni",desonline:"Successioni",tempo:"45",documenti:"Atto di morte, visure, documenti eredi",online:true,codtipologia:"PATR",destipologia:"Patrimonio"},
8021    {codice:"COLF",descrizione:"Contratti Colf e Badanti",desonline:"Colf e Badanti",tempo:"30",documenti:"Documenti datore e lavoratore",online:true,codtipologia:"LAV",destipologia:"Lavoro domestico"},
8022    {codice:"RED",descrizione:"Modello RED / dichiarazioni reddituali",desonline:"Modello RED",tempo:"20",documenti:"Lettera INPS, documenti reddituali",online:true,codtipologia:"PREV",destipologia:"Previdenza"}
8023  ];
8024  var VEN_SEDI_BY_PROV={
8025    VE:[
8026      {codice:"SEDE-VE-01",descrizione:DEMO_BRAND+" Venezia – Mestre",indirizzo:"Via Cà Marcello 10",cap:"30172",prov:"VE",citta:"Venezia",telefono:"041 000 0000",email:"[email protected]"},
8027      {codice:"SEDE-VE-02",descrizione:DEMO_BRAND+" Venezia – Marghera",indirizzo:"Piazza Mercato 24",cap:"30175",prov:"VE",citta:"Venezia",telefono:"041 000 0001",email:"[email protected]"}
8028    ],
8029    PD:[
8030      {codice:"SEDE-PD-01",descrizione:DEMO_BRAND+" Padova – Centro",indirizzo:"Via Longhin 2",cap:"35129",prov:"PD",citta:"Padova",telefono:"049 000 0000",email:"[email protected]"}
8031    ],
8032    TV:[
8033      {codice:"SEDE-TV-01",descrizione:DEMO_BRAND+" Treviso",indirizzo:"Via Dandolo 4",cap:"31100",prov:"TV",citta:"Treviso",telefono:"0422 000 000",email:"[email protected]"}
8034    ]
8035  };
8036  function venProvinceFor(body){return VEN_PROVINCE.slice();}
8037  function venSediFor(body){
8038    var code=(body&&body.codiceProvincia)||"VE";
8039    if(VEN_SEDI_BY_PROV[code])return VEN_SEDI_BY_PROV[code].slice();
8040    var name=code;for(var i=0;i<VEN_PROVINCE.length;i++){if(VEN_PROVINCE[i].codice===code){name=VEN_PROVINCE[i].descrizione;break;}}
8041    return [{codice:"SEDE-"+code+"-01",descrizione:DEMO_BRAND+" "+name,indirizzo:"Via Roma 1",cap:"",prov:code,citta:name,telefono:"000 000 0000",email:"demo."+code.toLowerCase()+"@cafcislveneto.example"}];
8042  }
8043
8044  /* ── Dataset NAZIONALE CAF ACLI ─────────────────────────────── */
8045  var ACLI_EMAIL_DOMAIN="cafacli.example";
8046  var ACLI_SERVIZI=[
8047    {codice:"730",descrizione:"Dichiarazione dei redditi – Modello 730/2026",desonline:"Modello 730/2026",tempo:"30",documenti:"CU, spese detraibili, eventuali deleghe",online:true,codtipologia:"FISCO",destipologia:"Servizi fiscali"},
8048    {codice:"ISEE",descrizione:"ISEE / DSU 2026",desonline:"ISEE 2026",tempo:"30",documenti:"Documenti reddituali e patrimoniali del nucleo familiare",online:true,codtipologia:"FISCO",destipologia:"Servizi fiscali"},
8049    {codice:"IMU",descrizione:"Calcolo e versamento IMU",desonline:"IMU",tempo:"20",documenti:"Visure catastali degli immobili",online:true,codtipologia:"FISCO",destipologia:"Servizi fiscali"},
8050    {codice:"RED",descrizione:"Modello RED / dichiarazioni reddituali INPS",desonline:"Modello RED",tempo:"20",documenti:"Lettera INPS, documenti reddituali",online:true,codtipologia:"PREV",destipologia:"Previdenza"},
8051    {codice:"SUCC",descrizione:"Dichiarazione di successione",desonline:"Successioni",tempo:"45",documenti:"Atto di morte, visure catastali, documenti degli eredi",online:true,codtipologia:"PATR",destipologia:"Patrimonio"},
8052    {codice:"COLF",descrizione:"Contratti Colf, Badanti e lavoro domestico",desonline:"Colf e Badanti",tempo:"30",documenti:"Documenti del datore e del lavoratore",online:true,codtipologia:"LAV",destipologia:"Lavoro domestico"},
8053    {codice:"LOC",descrizione:"Contratti di locazione e cedolare secca",desonline:"Locazioni",tempo:"30",documenti:"Contratto, dati catastali, documenti delle parti",online:true,codtipologia:"FISCO",destipologia:"Servizi fiscali"},
8054    {codice:"AUU",descrizione:"Assegno Unico Universale e bonus sociali",desonline:"Assegno Unico",tempo:"20",documenti:"ISEE in corso di validità, IBAN",online:true,codtipologia:"PREV",destipologia:"Previdenza"}
8055  ];
8056  var ACLI_SPEC=[
8057    {reg:"Lombardia",cod:"LOM",prov:[
8058      {n:"Milano",s:"MI",sedi:[
8059        {label:"Milano Centro",via:"Via della Signora 3",cap:"20122",tel:"02 7700 1001"},
8060        {label:"Milano Niguarda",via:"Viale Suzzani 15",cap:"20162",tel:"02 7700 1002"}]},
8061      {n:"Monza e della Brianza",s:"MB",sedi:[{label:"Monza",via:"Via Vittorio Emanuele II 2",cap:"20900",tel:"039 200 2001"}]},
8062      {n:"Bergamo",s:"BG",sedi:[{label:"Bergamo",via:"Via Zelasco 1",cap:"24122",tel:"035 459 3001"}]},
8063      {n:"Brescia",s:"BS",sedi:[{label:"Brescia",via:"Via Cefalonia 70",cap:"25124",tel:"030 370 4001"}]}
8064    ]},
8065    {reg:"Lazio",cod:"LAZ",prov:[
8066      {n:"Roma",s:"RM",sedi:[
8067        {label:"Roma Eur",via:"Viale Marco Polo 18",cap:"00154",tel:"06 5840 1001"},
8068        {label:"Roma Tuscolano",via:"Via Tuscolana 235",cap:"00181",tel:"06 5840 1002"}]},
8069      {n:"Latina",s:"LT",sedi:[{label:"Latina",via:"Via Emanuele Filiberto 12",cap:"04100",tel:"0773 690 001"}]},
8070      {n:"Frosinone",s:"FR",sedi:[{label:"Frosinone",via:"Via Aldo Moro 466",cap:"03100",tel:"0775 210 001"}]}
8071    ]},
8072    {reg:"Piemonte",cod:"PIE",prov:[
8073      {n:"Torino",s:"TO",sedi:[
8074        {label:"Torino Centro",via:"Via Perrone 3 bis",cap:"10122",tel:"011 432 1001"},
8075        {label:"Torino Mirafiori",via:"Corso Unione Sovietica 220",cap:"10134",tel:"011 432 1002"}]},
8076      {n:"Cuneo",s:"CN",sedi:[{label:"Cuneo",via:"Corso Giolitti 21",cap:"12100",tel:"0171 290 001"}]},
8077      {n:"Novara",s:"NO",sedi:[{label:"Novara",via:"Via dei Caccia 7",cap:"28100",tel:"0321 390 001"}]}
8078    ]},
8079    {reg:"Veneto",cod:"VNT",prov:[
8080      {n:"Venezia",s:"VE",sedi:[{label:"Venezia Mestre",via:"Via Cà Marcello 10",cap:"30172",tel:"041 538 1001"}]},
8081      {n:"Padova",s:"PD",sedi:[{label:"Padova",via:"Via Tommaseo 60",cap:"35131",tel:"049 870 2001"}]},
8082      {n:"Verona",s:"VR",sedi:[{label:"Verona",via:"Via Città di Nimes 6",cap:"37123",tel:"045 800 3001"}]},
8083      {n:"Treviso",s:"TV",sedi:[{label:"Treviso",via:"Via Dandolo 4",cap:"31100",tel:"0422 540 001"}]}
8084    ]},
8085    {reg:"Emilia-Romagna",cod:"EMR",prov:[
8086      {n:"Bologna",s:"BO",sedi:[{label:"Bologna",via:"Via Marconi 69",cap:"40122",tel:"051 600 1001"}]},
8087      {n:"Modena",s:"MO",sedi:[{label:"Modena",via:"Viale Gramsci 75",cap:"41122",tel:"059 350 2001"}]},
8088      {n:"Parma",s:"PR",sedi:[{label:"Parma",via:"Via Verdi 35",cap:"43121",tel:"0521 270 001"}]},
8089      {n:"Reggio nell'Emilia",s:"RE",sedi:[{label:"Reggio Emilia",via:"Via Roma 18",cap:"42121",tel:"0522 430 001"}]}
8090    ]},
8091    {reg:"Toscana",cod:"TOS",prov:[
8092      {n:"Firenze",s:"FI",sedi:[{label:"Firenze",via:"Via de' Martelli 8",cap:"50129",tel:"055 270 1001"}]},
8093      {n:"Pisa",s:"PI",sedi:[{label:"Pisa",via:"Via San Lorenzo 4",cap:"56127",tel:"050 500 001"}]},
8094      {n:"Livorno",s:"LI",sedi:[{label:"Livorno",via:"Via Grande 92",cap:"57123",tel:"0586 880 001"}]}
8095    ]},
8096    {reg:"Campania",cod:"CAM",prov:[
8097      {n:"Napoli",s:"NA",sedi:[
8098        {label:"Napoli Centro",via:"Via Toledo 156",cap:"80132",tel:"081 551 1001"},
8099        {label:"Napoli Vomero",via:"Via Luca Giordano 60",cap:"80127",tel:"081 551 1002"}]},
8100      {n:"Salerno",s:"SA",sedi:[{label:"Salerno",via:"Corso Vittorio Emanuele 58",cap:"84122",tel:"089 250 001"}]},
8101      {n:"Caserta",s:"CE",sedi:[{label:"Caserta",via:"Via Roma 102",cap:"81100",tel:"0823 320 001"}]}
8102    ]},
8103    {reg:"Puglia",cod:"PUG",prov:[
8104      {n:"Bari",s:"BA",sedi:[{label:"Bari",via:"Via Napoli 5",cap:"70123",tel:"080 521 1001"}]},
8105      {n:"Lecce",s:"LE",sedi:[{label:"Lecce",via:"Viale Marconi 12",cap:"73100",tel:"0832 240 001"}]},
8106      {n:"Taranto",s:"TA",sedi:[{label:"Taranto",via:"Via Dante 45",cap:"74121",tel:"099 470 001"}]}
8107    ]}
8108  ];
8109  var ACLI_REGIONI=[];
8110  var ACLI_PROV_BY_REG={};
8111  var ACLI_SEDI_BY_PROV={};
8112  (function buildAcliDemo(){
8113    for(var i=0;i<ACLI_SPEC.length;i++){
8114      var R=ACLI_SPEC[i];
8115      var conn="acli-"+R.cod.toLowerCase();
8116      ACLI_REGIONI.push({codice:R.cod,descrizione:R.reg,connessione:conn,codiceRegione:R.cod});
8117      ACLI_PROV_BY_REG[R.cod]=[];
8118      for(var j=0;j<R.prov.length;j++){
8119        var P=R.prov[j];
8120        ACLI_PROV_BY_REG[R.cod].push({codice:P.s,descrizione:P.n});
8121        var list=[];
8122        for(var k=0;k<P.sedi.length;k++){
8123          var S=P.sedi[k];
8124          var lbl=S.label||P.n;
8125          list.push({
8126            codice:"SEDE-"+P.s+"-"+demoPad2(k+1),
8127            descrizione:DEMO_BRAND+" "+lbl,
8128            indirizzo:S.via,
8129            cap:S.cap,
8130            prov:P.s,
8131            citta:P.n,
8132            telefono:S.tel,
8133            email:"appuntamenti."+demoSlug(lbl)+"@"+ACLI_EMAIL_DOMAIN
8134          });
8135        }
8136        ACLI_SEDI_BY_PROV[P.s]=list;
8137      }
8138    }
8139  })();
8140  function acliProvinceFor(body){
8141    var rc=(body&&body.codiceRegione)||"";
8142    if(rc&&ACLI_PROV_BY_REG[rc])return ACLI_PROV_BY_REG[rc].slice();
8143    var all=[];for(var k in ACLI_PROV_BY_REG){if(ACLI_PROV_BY_REG.hasOwnProperty(k))all=all.concat(ACLI_PROV_BY_REG[k]);}return all;
8144  }
8145  function acliSediFor(body){
8146    var pc=(body&&body.codiceProvincia)||"";
8147    if(pc&&ACLI_SEDI_BY_PROV[pc])return ACLI_SEDI_BY_PROV[pc].slice();
8148    return [{codice:"SEDE-"+pc+"-01",descrizione:DEMO_BRAND+(pc?(" "+pc):""),indirizzo:"Via Roma 1",cap:"",prov:pc,citta:pc,telefono:"800 000 000",email:"appuntamenti@"+ACLI_EMAIL_DOMAIN}];
8149  }
8150
8151  /* dataset attivo in base al profilo selezionato */
8152  var DEMO_REGIONI, DEMO_SERVIZI, _demoProvinceFor, _demoSediFor;
8153  if(DEMO_PROFILE==="cafacli"){
8154    DEMO_REGIONI=ACLI_REGIONI; DEMO_SERVIZI=ACLI_SERVIZI;
8155    _demoProvinceFor=acliProvinceFor; _demoSediFor=acliSediFor;
8156  } else {
8157    DEMO_REGIONI=VEN_REGIONI; DEMO_SERVIZI=VEN_SERVIZI;
8158    _demoProvinceFor=venProvinceFor; _demoSediFor=venSediFor;
8159  }
8160
8161  CafChat.prototype._bkDemoData = function(action,body){
8162    switch(action){
8163      case "regioni": return DEMO_REGIONI.slice();
8164      case "province": return _demoProvinceFor(body);
8165      case "servizi": return DEMO_SERVIZI.slice();
8166      case "sedi": return _demoSediFor(body);
8167      case "spazi": return demoSpazi();
8168      case "prenota": return {id:"DEMO-"+bkUuid().replace(/-/g,"").slice(0,8).toUpperCase()};
8169      default: return [];
8170    }
8171  };
8172
8173  /* Risolve con i dati demo dopo una breve latenza simulata, così gli stati di
8174   * caricamento (spinner, "cerco disponibilità…") vengono comunque mostrati. */
8175  CafChat.prototype._bkDemoFetch = function(action,body){
8176    var payload=this._bkDemoData(action,body);
8177    var delay=(action==="prenota")?800:180;
8178    return new Promise(function(resolve){setTimeout(function(){resolve(payload);},delay);});
8179  };
8180
8181  /** Annulla il fetch di step in corso: aborta la catena, sblocca la UI e
8182   *  riporta l'utente allo step precedente (memorizzato in _bkLoadingReturnStep)
8183   *  invece di lasciarlo su uno spinner fino a ~47s. */
8184  CafChat.prototype._bkCancelLoading = function(){
8185    try{if(this._bkActiveCtrl)this._bkActiveCtrl.abort();}catch(_e){}
8186    /* Invalida ogni operazione in volo (anche prefetch già consumati): le
8187     * await stantie nei _bkPick* controllano _bkOpGen e abbandonano senza
8188     * saltare in avanti dopo che l'utente ha annullato. */
8189    this._bkOpGen=(this._bkOpGen||0)+1;
8190    this._bkActiveCtrl=null;this._bkBusy=false;
8191    this._bkAutoProvStatus=null;this._bkNearestStatus=null;this._bkResumeLoading=null;
8192    this.bkStep=this._bkLoadingReturnStep||"selectRegione";
8193    this._bkLogEvent&&this._bkLogEvent("step_cancelled",{back:this.bkStep});
8194    if(this._bkFsMode)this._bkRenderFs();else this._bkRender();
8195  };
8196
8197  /** Navigazione manuale (back / jump-to-step / go-to-step): invalida ogni
8198   *  _bkPick* ancora in volo. Senza questo bump dell'op-gen, una await stantia
8199   *  (es. il fetch "spazi" di una sede appena deselezionata) supera il guard
8200   *  `if(this._bkOpGen!==_g)return` e riscrive bkSpazi/bkSelSede/bkStep DOPO che
8201   *  la navigazione li ha resettati → uno slot/sede NON più selezionato poteva
8202   *  finire nel payload prenota (e nel record Firestore). Stesso identico
8203   *  contratto di _bkCancelLoading/_bkReset: bump op-gen + abort + sblocca busy. */
8204  CafChat.prototype._bkInvalidateInflight = function(){
8205    this._bkOpGen=(this._bkOpGen||0)+1;
8206    try{if(this._bkActiveCtrl)this._bkActiveCtrl.abort();}catch(_e){}
8207    this._bkActiveCtrl=null;this._bkBusy=false;
8208  };
8209
8210  /* Popup diagnostico Planner — iniettato in document.body (fuori shadow DOM) */
8211  CafChat.prototype._showPlannerPopup = function(diag){
8212    var self=this;
8213    try{
8214      /* Pulisci sia istanza legacy in body sia nuova istanza nel panel */
8215      var prev=document.getElementById("caf-planner-error-popup");
8216      if(prev&&prev.parentNode)prev.parentNode.removeChild(prev);
8217      if(self.panelEl){
8218        var prevP=self.panelEl.querySelector("#caf-planner-error-popup");
8219        if(prevP&&prevP.parentNode)prevP.parentNode.removeChild(prevP);
8220      }
8221    }catch(x){}
8222    var code=(diag&&diag.code?String(diag.code):"UNKNOWN").toUpperCase();
8223    var userMsg=(diag&&diag.userMessage)||(diag&&diag.error)||"Motivo non disponibile.";
8224    var raw=(diag&&diag.plannerMessage)||"";
8225    var esc=function(s){return String(s).replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;");};
8226    var overlay=document.createElement("div");
8227    overlay.id="caf-planner-error-popup";
8228    overlay.setAttribute("role","dialog");
8229    overlay.setAttribute("aria-modal","true");
8230    overlay.setAttribute("aria-labelledby","caf-planner-err-h");
8231    /* Confinato al chat panel — non copre il sito host. z-index:100 per
8232     * stare sopra .caf-bk-fs (z-index:60) e tutti gli altri elementi interni. */
8233    overlay.style.cssText="position:absolute;inset:0;z-index:100;background:rgba(0,0,0,0.55);display:flex;align-items:center;justify-content:center;padding:12px;font-family:system-ui,-apple-system,Segoe UI,Roboto,sans-serif;color:#0f172a;border-radius:inherit;";
8234    overlay.innerHTML=
8235      '<div style="width:min(520px,100%);background:#fff;border-radius:14px;box-shadow:0 20px 60px rgba(0,0,0,0.35);padding:22px 22px 18px;">'+
8236        '<div style="display:flex;align-items:center;gap:10px;margin-bottom:10px;">'+
8237          '<div style="width:36px;height:36px;border-radius:50%;background:#fee2e2;display:flex;align-items:center;justify-content:center;font-size:20px;">⚠️</div>'+
8238          '<h2 id="caf-planner-err-h" style="font-size:17px;font-weight:700;margin:0;color:#b91c1c;">Servizio prenotazione non disponibile</h2>'+
8239        '</div>'+
8240        '<div style="display:inline-block;background:#fef2f2;color:#b91c1c;border:1px solid #fecaca;border-radius:8px;padding:4px 10px;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12px;font-weight:600;letter-spacing:0.02em;margin-bottom:12px;">CODICE: '+esc(code)+'</div>'+
8241        '<p style="font-size:14px;line-height:1.5;margin:0 0 10px;color:#1e293b;">'+esc(userMsg)+'</p>'+
8242        (raw?'<pre style="background:#f1f5f9;border:1px solid #e2e8f0;border-radius:8px;padding:10px 12px;font-size:12px;line-height:1.45;color:#334155;white-space:pre-wrap;word-break:break-word;max-height:180px;overflow:auto;margin:0 0 14px;">'+esc(raw)+'</pre>':'')+
8243        '<div style="display:flex;justify-content:flex-end;gap:8px;">'+
8244          '<button type="button" data-caf-copy style="border:1px solid #cbd5e1;background:#fff;color:#334155;padding:8px 14px;border-radius:8px;font-size:13px;cursor:pointer;">Copia</button>'+
8245          '<button type="button" data-caf-close style="border:none;background:#b91c1c;color:#fff;padding:8px 16px;border-radius:8px;font-size:13px;font-weight:600;cursor:pointer;">Chiudi</button>'+
8246        '</div>'+
8247      '</div>';
8248    var _prevFocus=(self.shadow&&self.shadow.activeElement)||document.activeElement;
8249    var close=function(){
8250      try{overlay.parentNode.removeChild(overlay);}catch(x){}
8251      /* WCAG 2.4.3: il focus torna dov'era prima dell'apertura del popup */
8252      try{if(_prevFocus&&_prevFocus.isConnected&&_prevFocus.focus)_prevFocus.focus();}catch(_e){}
8253    };
8254    /* WCAG 2.1.2: Escape chiude il popup (solo il popup, non il pannello) */
8255    overlay.addEventListener("keydown",function(ev){if(ev.key==="Escape"||ev.key==="Esc"){ev.stopPropagation();close();}});
8256    overlay.addEventListener("click",function(ev){if(ev.target===overlay)close();});
8257    overlay.querySelector("[data-caf-close]").addEventListener("click",close);
8258    overlay.querySelector("[data-caf-copy]").addEventListener("click",function(){
8259      var txt="["+code+"] "+userMsg+(raw?"\n"+raw:"");
8260      try{if(navigator.clipboard&&navigator.clipboard.writeText)navigator.clipboard.writeText(txt);}catch(x){}
8261    });
8262    overlay.querySelector("div").addEventListener("click",function(ev){ev.stopPropagation();});
8263    /* Append nel chat panel — non in body — così l'overlay sta dentro il widget */
8264    if(self.panelEl)self.panelEl.appendChild(overlay);
8265    else document.body.appendChild(overlay);
8266    self._trapTab(overlay);
8267    try { overlay.querySelector("[data-caf-close]").focus(); } catch(_e){}
8268  };
8269
8270  /* Probe diagnostico: interroga /api/planner/health e mostra il popup.
8271     Se lastErr è già valorizzato (da un _bkFetch precedente) lo usa senza probe. */
8272  CafChat.prototype._showPlannerDiagPopup = async function(){
8273    var last=this._bkLastErr;
8274    if(last){this._showPlannerPopup(last);return;}
8275    var ctrl=new AbortController();var tid=setTimeout(function(){ctrl.abort();},8000);
8276    try{
8277      var r=await fetch(this.opts.baseUrl+"/api/planner/health",{method:"POST",headers:{"Content-Type":"application/json"},body:"{}",signal:ctrl.signal});
8278      clearTimeout(tid);
8279      var j={};try{j=await r.json();}catch(x){}
8280      this._showPlannerPopup({code:(j.code||"UNKNOWN"),userMessage:(j.userMessage||"Motivo non disponibile."),plannerMessage:(j.plannerMessage||("HTTP "+r.status))});
8281    }catch(e){clearTimeout(tid);
8282      this._showPlannerPopup({code:"PROBE-FAIL",userMessage:"Impossibile contattare /api/planner/health.",plannerMessage:(e&&e.message)?e.message:String(e)});
8283    }
8284  };
8285
8286  CafChat.prototype._bkCheckEnabled = async function(){
8287    /* DEMOPROMO: prenotazione sempre "abilitata", senza il probe di rete a
8288     * /api/planner/province. */
8289    if(this.opts&&this.opts.demopromo){this._bkEnabled=true;return true;}
8290    if(this._bkEnabled!==undefined)return this._bkEnabled;
8291    var ctrl=new AbortController();var tid=setTimeout(function(){ctrl.abort();},5000);
8292    try{var r=await fetch(this.opts.baseUrl+"/api/planner/province",{method:"POST",headers:{"Content-Type":"application/json"},body:"{}",signal:ctrl.signal});
8293      clearTimeout(tid);
8294      if(r.status===403){this._bkEnabled=false;return false;}this._bkEnabled=true;return true;
8295    }catch(e){clearTimeout(tid);return false;/* don't cache network/timeout errors */}
8296  };
8297
8298  /* ── password reset: render card into _pwdEl ─────────────── */
8299  CafChat.prototype._pwdRender = function(){
8300    var self = this;
8301    var el = this._pwdEl;
8302    if(!el) return;
8303    el.innerHTML = "";
8304    var c = this.opts.color;
8305
8306    var card = document.createElement("div");
8307    card.className = "caf-pw-card";
8308    card.style.cssText =
8309      "background:linear-gradient(135deg,#fff," + c + "05);" +
8310      "border:1.5px solid " + c + "30;border-radius:14px;padding:16px 16px 14px;" +
8311      "box-shadow:0 4px 16px " + c + "15;font-family:inherit;animation:caf-fade-up .35s ease-out";
8312
8313    /* Header */
8314    var hdr = document.createElement("div");
8315    hdr.style.cssText = "display:flex;align-items:center;gap:10px;margin-bottom:10px";
8316    var icon = document.createElement("div");
8317    icon.style.cssText = "width:32px;height:32px;border-radius:10px;background:var(--caf-c);color:var(--caf-on-c);display:flex;align-items:center;justify-content:center;flex-shrink:0;box-shadow:0 3px 10px " + c + "44";
8318    icon.innerHTML = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="11" width="18" height="11" rx="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg>';
8319    hdr.appendChild(icon);
8320    var htxt = document.createElement("div");
8321    htxt.style.cssText = "flex:1;min-width:0";
8322    var ht = document.createElement("div");
8323    ht.style.cssText = "font-size:13.5px;font-weight:700;color:#0f172a;letter-spacing:-.1px;line-height:1.25";
8324    ht.textContent = "Reimposta password PersonalCAF";
8325    htxt.appendChild(ht);
8326    var hs = document.createElement("div");
8327    hs.style.cssText = "font-size:11px;color:#5b6573;margin-top:1px;line-height:1.35";
8328    hs.textContent = "Inserisci la tua email, ti inviamo subito il link per il reset.";
8329    htxt.appendChild(hs);
8330    hdr.appendChild(htxt);
8331    card.appendChild(hdr);
8332
8333    if(this.pwdStep === "form" || this.pwdStep === "error"){
8334      var form = document.createElement("form");
8335      form.style.cssText = "display:flex;gap:8px;flex-wrap:wrap;margin-top:4px";
8336      form.addEventListener("submit", function(ev){ ev.preventDefault(); self._pwdSubmit(); });
8337
8338      var inpWrap = document.createElement("div");
8339      inpWrap.style.cssText = "flex:1;min-width:180px;position:relative";
8340      var inp = document.createElement("input");
8341      inp.type = "email"; inp.required = true; inp.autocomplete = "email";
8342      inp.placeholder = "[email protected]"; inp.setAttribute("aria-label", bkT("aria_pwd_email"));
8343      inp.value = this.pwdEmail || "";
8344      inp.style.cssText = "width:100%;padding:10px 12px 10px 36px;border:1.5px solid #e2e8f0;border-radius:10px;font-size:13px;font-family:inherit;outline:none;background:#fff;transition:border-color .2s,box-shadow .2s;box-sizing:border-box";
8345      inp.addEventListener("focus", function(){ inp.style.borderColor = c; inp.style.boxShadow = "0 0 0 3px " + c + "22"; });
8346      inp.addEventListener("blur", function(){ inp.style.borderColor = "#e2e8f0"; inp.style.boxShadow = "none"; });
8347      inp.addEventListener("input", function(){ self.pwdEmail = inp.value; });
8348      inpWrap.appendChild(inp);
8349      var mailIc = document.createElement("span");
8350      mailIc.style.cssText = "position:absolute;
8350left:11px;top:50%;transform:translateY(-50%);color:#5b6573;pointer-events:none;display:flex";
8351      mailIc.innerHTML = '<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="4" width="20" height="16" rx="2"/><path d="m22 7-10 5L2 7"/></svg>';
8352      inpWrap.appendChild(mailIc);
8353      form.appendChild(inpWrap);
8354
8355      var btn = document.createElement("button");
8356      btn.type = "submit";
8357      btn.style.cssText = "border:none;background:var(--caf-c);color:var(--caf-on-c);border-radius:10px;padding:10px 16px;font-size:12.5px;font-weight:700;cursor:pointer;font-family:inherit;box-shadow:0 4px 12px " + c + "44;display:inline-flex;align-items:center;gap:5px;transition:all .2s;-webkit-tap-highlight-color:transparent";
8358      btn.innerHTML = 'Invia link <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round"><polyline points="9 18 15 12 9 6"/></svg>';
8359      btn.addEventListener("mouseenter", function(){ btn.style.transform = "translateY(-1px)"; btn.style.boxShadow = "0 6px 16px " + c + "55"; });
8360      btn.addEventListener("mouseleave", function(){ btn.style.transform = "none"; btn.style.boxShadow = "0 4px 12px " + c + "44"; });
8361      form.appendChild(btn);
8362      card.appendChild(form);
8363
8364      if(this.pwdStep === "error" && this.pwdErr){
8365        var er = document.createElement("div");
8366        er.style.cssText = "margin-top:8px;font-size:11.5px;color:#b91c1c;background:#fef2f2;border:1px solid #fecaca;border-radius:8px;padding:7px 10px;line-height:1.4";
8367        er.textContent = this.pwdErr;
8368        card.appendChild(er);
8369      }
8370    } else if(this.pwdStep === "submitting"){
8371      var sub = document.createElement("div");
8372      sub.style.cssText = "display:flex;align-items:center;gap:10px;font-size:12.5px;color:#475569;padding:4px 0";
8373      sub.innerHTML = '<span style="width:16px;height:16px;border:2px solid #e2e8f0;border-top-color:' + c + ';border-radius:50%;animation:caf-spin .8s linear infinite"></span> Invio la richiesta...';
8374      card.appendChild(sub);
8375    } else if(this.pwdStep === "done"){
8376      var ok = document.createElement("div");
8377      ok.style.cssText = "display:flex;gap:10px;align-items:flex-start;margin-top:2px";
8378      var okIc = document.createElement("div");
8379      okIc.style.cssText = "width:26px;height:26px;border-radius:50%;background:#10b981;color:#fff;display:flex;align-items:center;justify-content:center;flex-shrink:0;box-shadow:0 3px 10px #10b98155";
8380      okIc.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>';
8381      ok.appendChild(okIc);
8382      var okTxt = document.createElement("div");
8383      okTxt.style.cssText = "flex:1;min-width:0;font-size:12.5px;color:#0f172a;line-height:1.5";
8384      okTxt.innerHTML =
8385        '<div style="font-weight:700;margin-bottom:3px">Richiesta inviata</div>' +
8386        'Riceverai a breve all\'indirizzo <strong>' + escapeHtml(self.pwdEmail) + '</strong> ' +
8387        'un\'email con il link per reimpostare la password. ' +
8388        'Se non la trovi entro pochi minuti, controlla la cartella <strong>Spam</strong> o <strong>Posta indesiderata</strong>.';
8389      ok.appendChild(okTxt);
8390      card.appendChild(ok);
8391    }
8392
8393    el.appendChild(card);
8394  };
8395
8396  /** Submit password reset request by writing the email to Firestore via REST. */
8397  CafChat.prototype._pwdSubmit = async function(){
8398    var self = this;
8399    var email = (this.pwdEmail || "").trim().toLowerCase();
8400    if(!/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/.test(email)){
8401      this.pwdStep = "error"; this.pwdErr = "Inserisci un indirizzo email valido.";
8402      this._pwdRender(); return;
8403    }
8404    this.pwdStep = "submitting"; this.pwdErr = ""; this._pwdRender();
8405
8406    /* GDPR minimizzazione (art. 5(1)(c)): niente URL con query/hash, userAgent troncato. */
8407    var _prUrl = ""; try { _prUrl = (typeof location !== "undefined") ? (location.origin + (location.pathname || "")) : ""; } catch (_e) { _prUrl = ""; }
8408
8409    /* Percorso preferito: endpoint SERVER (niente scrittura client-side
8410     * diretta su Firestore — GDPR artt. 25, 32). Il write REST diretto resta
8411     * SOLO come fallback transitorio per finestre di deploy misto
8412     * (widget nuovo in CDN + server senza la route). */
8413    var _pwdOk = false;
8414    try {
8415      var _srv = await fetch((this.opts.baseUrl || "") + "/api/password-reset-request", {
8416        method: "POST",
8417        headers: { "Content-Type": "application/json" },
8418        body: JSON.stringify({ email: email, pageUrl: _prUrl, source: "widget-inline" })
8419      });
8420      _pwdOk = !!(_srv && _srv.ok);
8421    } catch (_e) { _pwdOk = false; }
8422
8423    if (!_pwdOk) {
8424      var cfg = (typeof window !== "undefined" && window.__CAF_FIREBASE_CONFIG__) || {};
8425      if (!cfg.projectId || !cfg.apiKey) {
8426        this.pwdStep = "error"; this.pwdErr = "Impossibile inviare la richiesta ora. Riprova tra qualche istante.";
8427        this._pwdRender(); return;
8428      }
8429      var url = "https://firestore.googleapis.com/v1/projects/" + encodeURIComponent(cfg.projectId) +
8430                "/databases/(default)/documents/password_reset_requests?key=" + encodeURIComponent(cfg.apiKey);
8431      var body = {
8432        fields: {
8433          email:     { stringValue: email },
8434          createdAt: { timestampValue: new Date().toISOString() },
8435          pageUrl:   { stringValue: _prUrl },
8436          userAgent: { stringValue: (typeof navigator !== "undefined" ? (navigator.userAgent || "").slice(0,200) : "") },
8437          source:    { stringValue: "widget-inline" }
8438        }
8439      };
8440      try {
8441        var r = await fetch(url, {
8442          method: "POST",
8443          headers: { "Content-Type": "application/json" },
8444          body: JSON.stringify(body)
8445        });
8446        if (!r.ok) throw new Error("HTTP " + r.status);
8447      } catch (e) {
8448        self.pwdStep = "error";
8449        self.pwdErr = "Impossibile inviare la richiesta ora. Riprova tra qualche istante.";
8450        self._pwdRender();
8451        return;
8452      }
8453    }
8454
8455    self.pwdStep = "done";
8456    self._pwdRender();
8457    /* Add a follow-up assistant message summarising next steps */
8458    var m = {id:quickId(),role:"assistant",content:
8459      "Se hai altre domande, sono qui. Ricorda di controllare anche la cartella **Spam** o **Posta indesiderata** se non vedi l'email.",
8460      timestamp:Date.now()};
8461    self.messages.push(m);
8462    self._renderMessages(); self._saveMessages(); self._updateSendBtn();
8463  };
8464
8465  CafChat.prototype._pwdReset = function(silent){
8466    this.pwdStep = "idle"; this.pwdEmail = ""; this.pwdErr = ""; this._pwdEl = null;
8467    if(!silent){
8468      this.messages = this.messages.filter(function(m){return m.content!=="@@PWDRESET@@";});
8469      this._saveMessages(); this._renderMessages(); this._updateSendBtn();
8470    }
8471  };
8472
8473  CafChat.prototype._bkReset = function(silent){
8474    this.bkStep="idle";this.bkRegioni=[];this.bkSelRegione=null;this.bkProvince=[];this.bkSelProv=null;this.bkServizi=[];this.bkSelServ=null;
8475    this.bkSedi=[];this.bkSelSede=null;this.bkSpazi=[];this.bkNextG="";this.bkSelSlot=null;
8476    this.bkClient={nome:"",cognome:"",codfis:"",email:"",cellulare:"",sesso:"M"};
8477    /* #PII — ricarica il candidato dallo storage (gated sul consenso) per la
8478     * prossima prenotazione e ripulisci il resume: su device condiviso, chiudere/
8479     * abbandonare NON deve lasciare i dati personali pronti per il prossimo. */
8480    this._bkStoredCandidate=loadStoredClient(this.bkBookingFor);
8481    clearResumeState();
8482    this.bkId=null;this.bkErrMsg="";this.bkErrCode=null;this.bkErrReturn=null;this.bkFormErr=null;
8483    /* Consenso privacy: ogni nuova prenotazione richiede un nuovo opt-in esplicito. */
8484    this.bkPrivacyOk=false;
8485    this._bkBusy=false;this._bkEl=null;this.bkHint="";this._bkDir="fwd";
8486    this._bkVoiceField=null;this._bkVoiceAnnounced=null;
8487    this._bkCalMonth=null;this._bkCalSelected=null;
8488    this._bkNearestStatus=null;this._bkNearestCode=null;this._bkAutoProvStatus=null;
8489    this._bkSideFilled={};this._lastFsBkStep=null;
8490    this._bkPrevFsStep=undefined;this._bkSlotPrefetch={};
8491    this._bkSedeSearch="";
8492    /* Reset dello stato "loading annullabile": senza questo, alla prenotazione
8493     * SUCCESSIVA comparirebbe un bottone "Annulla" fantasma sul primo loading,
8494     * che salterebbe a uno step di ritorno stantio. Invalida anche un'eventuale
8495     * fetch ancora in volo (op-gen + abort) per non far ripartire await stantie. */
8496    this._bkLoadingReturnStep=null;this._bkLoadingMore=false;this._bkSubmitReqId=null;
8497    this._bkOpGen=(this._bkOpGen||0)+1;
8498    try{if(this._bkActiveCtrl)this._bkActiveCtrl.abort();}catch(_e){}
8499    this._bkActiveCtrl=null;
8500    if(this._bkAutoProvWatchdog){clearTimeout(this._bkAutoProvWatchdog);this._bkAutoProvWatchdog=null;}
8501    if(this._bkFsMode){this._bkExitFs(true);}
8502    if(!silent){
8503      /* Strip solo i placeholder @@BOOKING@@ (anchor wizard fullscreen).
8504       * Il bottone @@BOOKING_LAUNCH@@ resta nella cronologia: l'utente può
8505       * ri-cliccarlo per riaprire il wizard se ha cancellato. */
8506      this.messages=this.messages.filter(function(m){return m.content!=="@@BOOKING@@";});
8507      this._saveMessages();this._renderMessages();this._updateSendBtn();
8508    }
8509    if(this._updateBkCta)this._updateBkCta();
8510    if(this._updateBkExpandBanner)this._updateBkExpandBanner();
8511  };
8512
8513  CafChat.prototype._bkSetError = function(err,ret){
8514    /* Annullamento volontario dell'utente (bottone "Annulla"): NON è un errore,
8515     * la navigazione l'ha già gestita _bkCancelLoading → no-op silenzioso. */
8516    if(err&&err.code==="cancelled")return;
8517    var msg=bkT("err_unexpected"),code="unknown";
8518    if(err&&typeof err==="object"&&err.code){msg=err.error||msg;code=err.code;}
8519    else if(err instanceof Error){msg=err.message;}
8520    this.bkStep="error";this.bkErrMsg=msg;this.bkErrCode=code;this.bkErrReturn=ret||null;
8521    this._bkBusy=false;this._bkRender();
8522  };
8523
8524  /* #42 — Banner GDPR multi-tier (compliant EDPB 03/2022).
8525   *
8526   * Mostra il banner con 3 categorie chiaramente distinte:
8527   *   - NECESSARI (sempre on, non-toggleable): cache, lingua
8528   *   - FUNZIONALI (opt-in): pre-fill cliente, history, resume
8529   *   - ANALYTICS (opt-in separato): sessionId, UTM, log-event
8530   *
8531   * Tre azioni equivalenti per peso visivo (linee guida EDPB):
8532   *   - "Accetta tutti"
8533   *   - "Rifiuta tutti" (stesso peso)
8534   *   - "Personalizza" (toggle granulari)
8535   *
8536   * Risolve la promise con un oggetto {functional, analytics} così il
8537   * chiamante sa quali scope sono stati concessi. */
8538  CafChat.prototype._bkRequestConsent = function(opts){
8539    opts=opts||{};
8540    /* Se già espresso, ritorna il consenso esistente senza ri-prompt */
8541    if(bkConsentExpressed()){
8542      var c=bkConsentLoad();
8543      return Promise.resolve({functional:!!(c&&c.functional),analytics:!!(c&&c.analytics)});
8544    }
8545    var self=this;
8546    var privacyUrl=(self.opts&&self.opts.privacyUrl)||"";
8547    try{var sct=document.querySelector("script[data-privacy-url]");
8547if(sct&&!privacyUrl)privacyUrl=sct.getAttribute("data-privacy-url")||"";}catch(_e){}
8548    var _cafPi=(typeof window!=="undefined"&&window.__CAF_PRIVACY_INFO__)||{};
8549    var _piGet=function(k){var v=_cafPi&&_cafPi[k];return(typeof v==="string"&&v.trim())?v.trim():"";};
8550    var controller=(self.opts&&self.opts.dataController)||_piGet("controller")||"il titolare del trattamento (vedi Informativa Privacy del sito)";
8551    return new Promise(function(resolve){
8552      /* Pulisci eventuali residui (sia legacy in body sia nuova posizione in panel) */
8553      try{
8554        var prevLegacy=document.getElementById("caf-bk-gdpr-banner");
8555        if(prevLegacy&&prevLegacy.parentNode)prevLegacy.parentNode.removeChild(prevLegacy);
8556        if(self.panelEl){
8557          var prevPanel=self.panelEl.querySelector("#caf-bk-gdpr-backdrop");
8558          if(prevPanel&&prevPanel.parentNode)prevPanel.parentNode.removeChild(prevPanel);
8559          if(self.shadow){
8560            var shadowOld=self.shadow.querySelector("#caf-bk-gdpr-banner");
8561            if(shadowOld&&shadowOld.parentNode)shadowOld.parentNode.removeChild(shadowOld);
8562          }
8563        }
8564      }catch(_e){}
8565      var bd=document.createElement("div");bd.id="caf-bk-gdpr-backdrop";bd.className="caf-bk-gdpr-backdrop";
8566      var b=document.createElement("div");b.id="caf-bk-gdpr-banner";b.className="caf-bk-gdpr";b.setAttribute("role","dialog");b.setAttribute("aria-modal","true");b.setAttribute("aria-labelledby","caf-bk-gdpr-h");
8567      b.innerHTML=
8568        '<div class="caf-bk-gdpr-row">'
8569          +'<div class="caf-bk-gdpr-ic"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg></div>'
8570          +'<div class="caf-bk-gdpr-body">'
8571            +'<h3 class="caf-bk-gdpr-h" id="caf-bk-gdpr-h">'+escapeHtml(bkT("gdpr_h"))+'</h3>'
8572            +'<div>'+escapeHtml(bkT("gdpr_body_v2",controller))+'</div>'
8573            +'<div class="caf-bk-gdpr-prefs" data-prefs hidden>'
8574              +'<label class="caf-bk-gdpr-pref"><input type="checkbox" checked disabled> <span><b>'+escapeHtml(bkT("gdpr_cat_necessary"))+'</b><br><small>'+escapeHtml(bkT("gdpr_cat_necessary_d"))+'</small></span></label>'
8575              +'<label class="caf-bk-gdpr-pref"><input type="checkbox" data-cat="functional"> <span><b>'+escapeHtml(bkT("gdpr_cat_functional"))+'</b><br><small>'+escapeHtml(bkT("gdpr_cat_functional_d"))+'</small></span></label>'
8576              +'<label class="caf-bk-gdpr-pref"><input type="checkbox" data-cat="analytics"> <span><b>'+escapeHtml(bkT("gdpr_cat_analytics"))+'</b><br><small>'+escapeHtml(bkT("gdpr_cat_analytics_d"))+'</small></span></label>'
8577            +'</div>'
8578            +'<div class="caf-bk-gdpr-info">'
8579              +'<a class="caf-bk-gdpr-link" href="#" data-act="more">'+escapeHtml(bkT("gdpr_more"))+'</a>'
8580              +(privacyUrl?' · <a class="caf-bk-gdpr-link" href="'+escapeHtml(privacyUrl)+'" target="_blank" rel="noopener noreferrer">'+escapeHtml(bkT("gdpr_privacy_policy"))+'</a>':'')
8581            +'</div>'
8582            +'<div class="caf-bk-gdpr-actions">'
8583              +'<button type="button" class="caf-bk-gdpr-btn primary" data-act="accept-all">'+escapeHtml(bkT("gdpr_accept_all"))+'</button>'
8584              +'<button type="button" class="caf-bk-gdpr-btn primary-outline" data-act="reject-all">'+escapeHtml(bkT("gdpr_reject_all"))+'</button>'
8585              +'<button type="button" class="caf-bk-gdpr-btn ghost" data-act="customize">'+escapeHtml(bkT("gdpr_customize"))+'</button>'
8586              +'<button type="button" class="caf-bk-gdpr-btn primary" data-act="save-prefs" hidden>'+escapeHtml(bkT("gdpr_save_prefs"))+'</button>'
8587            +'</div>'
8588          +'</div>'
8589        +'</div>';
8590      bd.appendChild(b);
8591      /* Append DENTRO il chat panel (panelEl è position:fixed → containing block
8592       * per il backdrop assoluto). Fallback a body solo se il panel non esiste
8593       * ancora (es. consenso richiesto prima dell'init dell'UI). */
8594      if(self.panelEl)self.panelEl.appendChild(bd);
8595      else document.body.appendChild(bd);
8596      self._trapTab(b);
8597      try { (b.querySelector(".caf-bk-gdpr-btn.primary")||b.querySelector("button,[href],input")||b).focus(); } catch(_e){}
8598      var done=function(consent){
8599        try{bd.parentNode&&bd.parentNode.removeChild(bd);}catch(_e){}
8600        resolve(consent);
8601      };
8602      b.querySelector("[data-act=accept-all]").addEventListener("click",function(){
8603        bkConsentSave({functional:true,analytics:true});
8604        done({functional:true,analytics:true});
8605      });
8606      b.querySelector("[data-act=reject-all]").addEventListener("click",function(){
8607        bkConsentSave({functional:false,analytics:false});
8608        done({functional:false,analytics:false});
8609      });
8610      b.querySelector("[data-act=customize]").addEventListener("click",function(){
8611        var prefs=b.querySelector("[data-prefs]");prefs.hidden=false;
8612        b.querySelector("[data-act=customize]").hidden=true;
8613        b.querySelector("[data-act=save-prefs]").hidden=false;
8614      });
8615      b.querySelector("[data-act=save-prefs]").addEventListener("click",function(){
8616        var fn=!!b.querySelector("input[data-cat=functional]:checked");
8617        var an=!!b.querySelector("input[data-cat=analytics]:checked");
8618        bkConsentSave({functional:fn,analytics:an});
8619        done({functional:fn,analytics:an});
8620      });
8621      b.querySelector("[data-act=more]").addEventListener("click",function(ev){
8622        ev.preventDefault();
8623        var existingMore=b.querySelector(".caf-bk-gdpr-more");
8624        if(existingMore){existingMore.remove();return;}
8625        var more=document.createElement("div");more.className="caf-bk-gdpr-more";
8626        more.style.cssText="margin-top:10px;padding:12px;background:#f8fafc;border:1px solid #e2e8f0;border-radius:10px;font-size:12px;line-height:1.55;color:#475569";
8627        more.innerHTML=
8628          '<p style="margin:0 0 6px">'
8629          +'<b>'+escapeHtml(bkT("gdpr_info_controller"))+'</b> '+escapeHtml(controller)
8630          +[["address","gdpr_info_address"],["email","gdpr_info_email"],["phone","gdpr_info_phone"],["vat","gdpr_info_vat"],["dpo","gdpr_info_dpo"]].map(function(f){var v=_piGet(f[0]);return v?('<br><b>'+escapeHtml(bkT(f[1]))+'</b> '+escapeHtml(v)):"";}).join("")
8631          +'</p>'
8632          +'<p style="margin:0 0 6px"><b>'+escapeHtml(bkT("gdpr_info_legal_basis"))+'</b> '+escapeHtml(bkT("gdpr_info_legal_basis_d"))+'</p>'
8633          +'<p style="margin:0 0 6px"><b>'+escapeHtml(bkT("gdpr_info_storage"))+'</b> '+escapeHtml(bkT("gdpr_info_storage_d"))+'</p>'
8634          +'<p style="margin:0 0 6px"><b>'+escapeHtml(bkT("gdpr_info_retention"))+'</b> '+escapeHtml(bkT("gdpr_info_retention_d"))+'</p>'
8635          +'<p style="margin:0 0 6px"><b>'+escapeHtml(bkT("gdpr_info_recipients"))+'</b> '+escapeHtml(bkT("gdpr_info_recipients_d"))+'</p>'
8636          +'<p style="margin:0 0 6px"><b>'+escapeHtml(bkT("gdpr_info_rights"))+'</b> '+escapeHtml(bkT("gdpr_info_rights_d"))+'</p>'
8637          +'<p style="margin:0"><b>'+escapeHtml(bkT("gdpr_info_revoke"))+'</b> '+escapeHtml(bkT("gdpr_info_revoke_d"))+'</p>';
8638        var bd=b.querySelector(".caf-bk-gdpr-body");bd.insertBefore(more,bd.querySelector(".caf-bk-gdpr-actions"));
8639      });
8640    });
8641  };
8642
8643  /* #42 GDPR — pannello Privacy: visualizza dati salvati, modifica preferenze
8644   * di consenso (Art. 7 GDPR), esporta dati (Art. 15/20 portabilità), cancella
8645   * (Art. 17 right to erasure). Iniettato in document.body fuori shadow DOM. */
8646  CafChat.prototype._bkShowPrivacyPanel = function(){
8647    var self=this;
8648    /* Cleanup residui in entrambe le posizioni (body legacy e panel) */
8649    try{
8650      var prevLegacy=document.getElementById("caf-bk-privacy-overlay");
8651      if(prevLegacy&&prevLegacy.parentNode)prevLegacy.parentNode.removeChild(prevLegacy);
8652      if(self.panelEl){
8653        var prevPanel=self.panelEl.querySelector("#caf-bk-privacy-overlay");
8654        if(prevPanel&&prevPanel.parentNode)prevPanel.parentNode.removeChild(prevPanel);
8655      }
8656    }catch(_e){}
8657    var c=this.opts.color;
8658    var consent=bkConsentLoad()||{functional:false,analytics:false};
8659    /* Conta i dati salvati per dare visibilità all'utente di cosa abbiamo */
8660    var stored={};
8661    try{
8662      stored.client=!!localStorage.getItem(BK_CLIENT_KEY)||!!localStorage.getItem(BK_CLIENT_FAMILY_KEY);
8663      stored.history=loadBookingHistory().length;
8664      stored.resume=!!localStorage.getItem(BK_RESUME_KEY);
8665      stored.sid=!!localStorage.getItem(BK_SID_KEY);
8666      stored.freq=!!localStorage.getItem(BK_FREQ_KEY);
8667    }catch(_e){}
8668
8669    var ov=document.createElement("div");ov.id="caf-bk-privacy-overlay";
8670    /* Overlay CONFINATO al chat panel (no position:fixed sul viewport).
8671     * border-radius:inherit per seguire i bordi arrotondati del panel.
8672     * z-index:100 per stare sopra .caf-bk-fs (z-index:60). */
8673    ov.style.cssText="position:absolute;inset:0;z-index:100;background:rgba(15,23,42,.55);
8673display:flex;align-items:center;justify-content:center;padding:12px;font-family:system-ui,-apple-system,Segoe UI,Roboto,sans-serif;border-radius:inherit;animation:caf-bk-fade-in .25s ease-out";
8674    var modal=document.createElement("div");
8675    /* max-height:100% (NON 90vh): il modal vive DENTRO il chat panel, quindi
8676     * il limite giusto e' l'altezza dell'overlay (inset:0 nel panel), non il
8677     * viewport — con 90vh su un panel piu' basso del viewport i bottoni
8678     * finivano tagliati fuori senza scroll. border-box: il padding non deve
8679     * sommarsi a width/height oltre il contenitore. */
8680    modal.style.cssText="width:min(560px,100%);max-height:100%;box-sizing:border-box;overflow:auto;background:#fff;border-radius:18px;box-shadow:0 24px 60px rgba(0,0,0,.3);padding:24px";
8681    modal.innerHTML=
8682      '<div style="display:flex;align-items:center;gap:10px;margin-bottom:14px">'
8683        +'<div style="width:40px;height:40px;border-radius:11px;background:linear-gradient(135deg,'+c+'22,'+c+'11);color:'+c+';display:flex;align-items:center;justify-content:center"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" style="width:22px;height:22px"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg></div>'
8684        +'<h2 style="margin:0;font-size:18px;font-weight:700;color:#0f172a">'+escapeHtml(bkT("privacy_panel_h"))+'</h2>'
8685      +'</div>'
8686      +'<p style="margin:0 0 16px;font-size:13.5px;color:#475569;line-height:1.55">'+escapeHtml(bkT("privacy_panel_s"))+'</p>'
8687
8688      +'<h3 style="margin:18px 0 8px;font-size:14px;font-weight:700;color:#0f172a">'+escapeHtml(bkT("privacy_panel_consent_h"))+'</h3>'
8689      +'<div style="display:flex;flex-direction:column;gap:8px;padding:12px;background:#f8fafc;border:1px solid #e2e8f0;border-radius:10px">'
8690        +'<label style="display:flex;align-items:flex-start;gap:10px;font-size:12.5px;line-height:1.5;color:#334155"><input type="checkbox" checked disabled style="margin-top:3px"> <span><b>'+escapeHtml(bkT("gdpr_cat_necessary"))+'</b><br><small style="color:#5b6573">'+escapeHtml(bkT("gdpr_cat_necessary_d"))+'</small></span></label>'
8691        +'<label style="display:flex;align-items:flex-start;gap:10px;font-size:12.5px;line-height:1.5;cursor:pointer;color:#334155"><input type="checkbox" data-cat="functional" '+(consent.functional?"checked":"")+' style="margin-top:3px;accent-color:'+c+'"> <span><b>'+escapeHtml(bkT("gdpr_cat_functional"))+'</b><br><small style="color:#5b6573">'+escapeHtml(bkT("gdpr_cat_functional_d"))+'</small></span></label>'
8692        +'<label style="display:flex;align-items:flex-start;gap:10px;font-size:12.5px;line-height:1.5;cursor:pointer;color:#334155"><input type="checkbox" data-cat="analytics" '+(consent.analytics?"checked":"")+' style="margin-top:3px;accent-color:'+c+'"> <span><b>'+escapeHtml(bkT("gdpr_cat_analytics"))+'</b><br><small style="color:#5b6573">'+escapeHtml(bkT("gdpr_cat_analytics_d"))+'</small></span></label>'
8693      +'</div>'
8694
8695      +'<h3 style="margin:18px 0 8px;font-size:14px;font-weight:700;color:#0f172a">'+escapeHtml(bkT("privacy_panel_data_h"))+'</h3>'
8696      +'<div style="padding:12px;background:#f8fafc;border:1px solid #e2e8f0;border-radius:10px;font-size:12.5px;line-height:1.6;color:#475569">'
8697        +'<div>• '+escapeHtml(bkT("privacy_panel_data_client"))+': '+(stored.client?'<b style="color:#15803d">'+escapeHtml(bkT("privacy_panel_present"))+'</b>':'<span style="color:#5b6573">'+escapeHtml(bkT("privacy_panel_absent"))+'</span>')+'</div>'
8698        +'<div>• '+escapeHtml(bkT("privacy_panel_data_history"))+': '+(stored.history>0?'<b style="color:#15803d">'+stored.history+'</b>':'<span style="color:#5b6573">'+escapeHtml(bkT("privacy_panel_absent"))+'</span>')+'</div>'
8699        +'<div>• '+escapeHtml(bkT("privacy_panel_data_resume"))+': '+(stored.resume?'<b style="color:#15803d">'+escapeHtml(bkT("privacy_panel_present"))+'</b>':'<span style="color:#5b6573">'+escapeHtml(bkT("privacy_panel_absent"))+'</span>')+'</div>'
8700        +'<div>• '+escapeHtml(bkT("privacy_panel_data_sid"))+': '+(stored.sid?'<b style="color:#15803d">'+escapeHtml(bkT("privacy_panel_present"))+'</b>':'<span style="color:#5b6573">'+escapeHtml(bkT("privacy_panel_absent"))+'</span>')+'</div>'
8701      +'</div>'
8702
8703      +'<div style="display:flex;flex-wrap:wrap;gap:8px;margin-top:18px;justify-content:flex-end">'
8704        +'<button type="button" data-act="export" style="display:inline-flex;align-items:center;gap:6px;padding:9px 16px;border:1.5px solid #e2e8f0;background:#fff;color:#475569;border-radius:9px;font-size:13px;font-weight:600;cursor:pointer;font-family:inherit"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" style="width:14px;
8704height:14px"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg> '+escapeHtml(bkT("privacy_panel_export"))+'</button>'
8705        +'<button type="button" data-act="delete-all" style="display:inline-flex;align-items:center;gap:6px;padding:9px 16px;border:1.5px solid #fecaca;background:#fef2f2;color:#b91c1c;border-radius:9px;font-size:13px;font-weight:600;cursor:pointer;font-family:inherit"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" style="width:14px;height:14px"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6"/></svg> '+escapeHtml(bkT("privacy_panel_delete_all"))+'</button>'
8706        +'<button type="button" data-act="cancel" style="padding:9px 16px;border:1.5px solid #e2e8f0;background:#fff;color:#475569;border-radius:9px;font-size:13px;font-weight:600;cursor:pointer;font-family:inherit">'+escapeHtml(bkT("btn_close"))+'</button>'
8707        +'<button type="button" data-act="save" style="padding:9px 16px;border:none;background:var(--caf-c);color:var(--caf-on-c);border-radius:9px;font-size:13px;font-weight:600;cursor:pointer;font-family:inherit">'+escapeHtml(bkT("gdpr_save_prefs"))+'</button>'
8708      +'</div>';
8709
8710    ov.appendChild(modal);
8711    var close=function(){try{ov.parentNode&&ov.parentNode.removeChild(ov);}catch(_e){}};
8712    ov.addEventListener("click",function(ev){if(ev.target===ov)close();});
8713    modal.querySelector("[data-act=cancel]").addEventListener("click",close);
8714
8715    /* SAVE: aggiorna consenso + cancella dati delle categorie revocate */
8716    modal.querySelector("[data-act=save]").addEventListener("click",function(){
8717      var fn=!!modal.querySelector("input[data-cat=functional]:checked");
8718      var an=!!modal.querySelector("input[data-cat=analytics]:checked");
8719      var prev=bkConsentLoad()||{functional:false,analytics:false};
8720      bkConsentSave({functional:fn,analytics:an});
8721      /* Se l'utente ha REVOCATO un consenso (era true → false), cancella i dati */
8722      if(prev.functional&&!fn){bkConsentRevoke("functional");bkConsentSave({functional:false,analytics:an});}
8723      if(prev.analytics&&!an){bkConsentRevoke("analytics");bkConsentSave({functional:fn,analytics:false});}
8724      close();
8725      if(self._bkShowToast)self._bkShowToast({type:"info",title:bkT("privacy_panel_saved_h"),msg:bkT("privacy_panel_saved_s")});
8726    });
8727
8728    /* EXPORT: download JSON con tutto ciò che è memorizzato (Art. 15/20 GDPR) */
8729    modal.querySelector("[data-act=export]").addEventListener("click",function(){
8730      var data={
8731        exportedAt:new Date().toISOString(),
8732        consent:bkConsentLoad(),
8733        client_self:null,client_family:null,
8734        history:loadBookingHistory(),
8735        resume:null,
8736        sid:null,
8737        freq:null,
8738        language:BK_LANG,
8739        conversationId:self.conversationId||null,
8740        email:self.userEmail||(function(){try{return localStorage.getItem(self.EMAIL_KEY)||null;}catch(_e){return null;}})(),
8741        chatConsent:(typeof chatConsentLoad==="function"?chatConsentLoad():null),
8742        conversation:(self.messages||[]).map(function(m){return {role:m.role,content:m.content,timestamp:m.timestamp||null};})
8743      };
8744      try{var c1=localStorage.getItem(BK_CLIENT_KEY);if(c1)data.client_self=_bkDeobfuscate(c1);}catch(_e){}
8745      try{var c2=localStorage.getItem(BK_CLIENT_FAMILY_KEY);if(c2)data.client_family=_bkDeobfuscate(c2);}catch(_e){}
8746      try{var rsr=localStorage.getItem(BK_RESUME_KEY);if(rsr)data.resume=JSON.parse(rsr);}catch(_e){}
8747      try{data.sid=localStorage.getItem(BK_SID_KEY);}catch(_e){}
8748      try{var fr=localStorage.getItem(BK_FREQ_KEY);if(fr)data.freq=JSON.parse(fr);}catch(_e){}
8749      var blob=new Blob([JSON.stringify(data,null,2)],{type:"application/json;charset=utf-8"});
8750      var url=URL.createObjectURL(blob);
8751      var a=document.createElement("a");a.href=url;a.download="cafcisl-i-miei-dati-"+(new Date().toISOString().slice(0,10))+".json";
8752      document.body.appendChild(a);a.click();
8753      setTimeout(function(){URL.revokeObjectURL(url);try{a.parentNode.removeChild(a);}catch(_e){}},100);
8754      if(self._bkShowToast)self._bkShowToast({type:"info",title:bkT("privacy_panel_export_h"),msg:bkT("privacy_panel_export_s")});
8755    });
8756
8757    /* DELETE-ALL: wipe completo + revoca consenso (Art. 17 GDPR) */
8758    modal.querySelector("[data-act=delete-all]").addEventListener("click",function(){
8759      if(!confirm(bkT("revoke_confirm")))return;
8760      try{
8761        bkConsentDeny();
8762        /* Cancellazione completa (GDPR art. 17): oltre alle chiavi wizard,
8763         * rimuoviamo cronologia chat, id conversazione, email e le eventuali
8764         * chiavi legacy non namespaced rimaste da versioni precedenti. */
8765        [BK_CLIENT_KEY,BK_CLIENT_FAMILY_KEY,BK_HISTORY_KEY,BK_RESUME_KEY,BK_FREQ_KEY,BK_SID_KEY,BK_LANG_KEY,CHAT_CONSENT_KEY,
8766         self.STORAGE_KEY,self.CONV_ID_KEY,self.EMAIL_KEY]
8767          .concat(BK_LEGACY_KEYS).concat(BK_PRE_NS_KEYS)
8768          .forEach(function(k){try{localStorage.removeItem(k);}catch(_e){}});
8769        /* stato in-memory: azzera email, rating, id conversazione e messaggi. */
8770        self.userEmail = "";
8771        self.rating = null;
8772        self.conversationId = self.opts.privacy ? ("anon_"+quickId()+quickId()) : self._newConversationId(true);
8773        try {
8774          self.messages = [{ id: "welcome", role: "assistant", content: timeAwareGreeting()+"! "+(self.opts.welcome||"")+((self.opts.aiDisclosure!==false)?("\n\n"+bkT("ai_disclosure")):"") }];
8775          if (self._renderMessages) self._renderMessages();
8776        } catch(_e){}
8777        /* GDPR Art. 17: revocato anche il consenso CHAT → il gate si ri-arma
8778           subito (checkbox azzerata) se il tenant lo usa (opts.consenso). */
8779        self._chatConsentGiven = false;
8780        if (self.consentOverlay) {
8781          var _ccb = self.consentOverlay.querySelector(".caf-consent-cb");
8782          var _cok = self.consentOverlay.querySelector(".caf-consent-ok");
8783          var _clb = self.consentOverlay.querySelector(".caf-consent-label");
8784          var _cage = self.consentOverlay.querySelector(".caf-consent-age");
8785          var _calb = self.consentOverlay.querySelector(".caf-consent-age-label");
8786          if (_ccb) _ccb.checked = false;
8787          if (_cage) _cage.checked = false;
8788          if (_cok) { _cok.disabled = true; _cok.style.opacity = ".5"; }
8789          if (_clb) { _clb.style.borderColor = "#e2e8f0"; _clb.style.background = "#f8fafc"; }
8790          if (_calb) { _calb.style.borderColor = "#e2e8f0"; _calb.style.background = "#f8fafc"; }
8791          if (self._needsConsent()) {
8792            self.consentOverlay.style.display = "flex";
8793            self.messagesEl.style.display = "none";
8794            self._scrollWrap.style.display = "none";
8795            self._inputAreaEl.style.display = "none";
8796            if (self._syncAiFooter) self._syncAiFooter();
8797          }
8798        }
8799        /* svuota anche cache statiche e cache slot */
8800        for(var i=localStorage.length-1;i>=0;i--){
8801          var k=localStorage.key(i);
8802          if(k&&(k.indexOf(BK_CACHE_PREFIX)===0||k.indexOf(BK_SLOT_CACHE_PREFIX)===0)){
8803            try{localStorage.removeItem(k);}catch(_e){}
8804          }
8805        }
8806      }catch(_e){}
8807      close();
8808      if(self._bkShowToast)self._bkShowToast({type:"info",title:bkT("revoke_done_h"),msg:bkT("revoke_done_s")});
8809    });
8810
8811    /* Append DENTRO il chat panel così l'overlay è confinato al widget,
8812     * NON al viewport: il sito host non viene oscurato. Fallback a body
8813     * solo se panelEl non disponibile (caso patologico). */
8814    if(self.panelEl)self.panelEl.appendChild(ov);
8815    else document.body.appendChild(ov);
8816  };
8817
8818  /* #30 — wrapper che applica View Transitions ai cambi di step se l'utente
8819   * non ha reduced-motion. Fallback senza animazione su browser non supportati. */
8820  CafChat.prototype._bkRenderWithTransition = function(){
8821    var self=this;
8822    if(window.matchMedia&&window.matchMedia("(prefers-reduced-motion:reduce)").matches){
8823      this._bkRender();return;
8824    }
8825    if(typeof document!=="undefined"&&document.startViewTransition){
8826      document.startViewTransition(function(){self._bkRender();});
8827    }else{
8828      this._bkRender();
8829    }
8830  };
8831
8832  /* #31 — confetti SVG variegato (stelle, cerchi, cuori, diamanti). */
8833  CafChat.prototype._bkSpawnConfetti = function(container, opts){
8834    if(!container)return;
8835    if(window.matchMedia&&window.matchMedia("(prefers-reduced-motion:reduce)").matches)return;
8836    opts=opts||{};
8837    var count=opts.count||32;
8838    var colors=opts.colors||[this.opts.color,"#f59e0b","#8b5cf6","#ef4444","#06b6d4","#10b981","#ec4899","#f97316"];
8839    var shapes=[
8840      '<polygon points="10,2 12.4,7.6 18.5,8 13.7,12 15.3,18 10,14.7 4.7,18 6.3,12 1.5,8 7.6,7.6"/>', /* star */
8841      '<circle cx="10" cy="10" r="6"/>', /* circle */
8842      '<rect x="3" y="3" width="14" height="14" rx="2" transform="rotate(15 10 10)"/>', /* square */
8843      '<path d="M10 16C10 16 3 11 3 7C3 4.79 4.79 3 7 3C8.5 3 10 4 10 4C10 4 11.5 3 13 3C15.21 3 17 4.79 17 7C17 11 10 16 10 16Z"/>', /* heart */
8844      '<polygon points="10,2 16,10 10,18 4,10"/>' /* diamond */
8845    ];
8846    container.style.position=container.style.position||"relative";
8847    for(var i=0;i<count;i++){
8848      var el=document.createElement("div");el.className="caf-bk-confetti-svg";
8849      var color=colors[i%colors.length];
8850      var shape=shapes[i%shapes.length];
8851      var leftPct=Math.random()*100;
8852      var tx=(Math.random()*200-100)+"px";
8853      var tr=(Math.random()*1080-360)+"deg";
8854      var td=(1.2+Math.random()*1.6)+"s";
8855      var tdelay=(Math.random()*0.6)+"s";
8856      var size=(8+Math.random()*10);
8857      el.style.cssText="left:"+leftPct+"%;top:-20px;width:"+size+"px;
8857height:"+size+"px;color:"+color+";--tx:"+tx+";--tr:"+tr+";--td:"+td+";--tdelay:"+tdelay;
8858      el.innerHTML='<svg viewBox="0 0 20 20" fill="currentColor" xmlns="http://www.w3.org/2000/svg" style="width:100%;height:100%">'+shape+'</svg>';
8859      container.appendChild(el);
8860      /* cleanup dopo l'animazione */
8861      (function(node){setTimeout(function(){try{node.parentNode&&node.parentNode.removeChild(node);}catch(_e){}},3000);})(el);
8862    }
8863  };
8864
8865  /* #14 — scroll input in centro su mobile quando prende focus.
8866   * La tastiera virtuale copre fino al 50% dello schermo: scrollare al
8867   * centro garantisce che l'input resti visibile sopra la tastiera. */
8868  CafChat.prototype._bkAttachKeyboardScroll = function(rootEl){
8869    if(!rootEl)return;
8870    if(!window.matchMedia||!window.matchMedia("(max-width:820px)").matches)return;
8871    var inputs=rootEl.querySelectorAll(".caf-bk-inp,input[type=text],input[type=email],input[type=tel]");
8872    var scroller=rootEl.querySelector(".caf-bk-fs-main");
8873    inputs.forEach(function(inp){
8874      inp.addEventListener("focus",function(){
8875        if(scroller)scroller.classList.add("kb-padded");
8876        /* delay piccolo per attendere apertura tastiera */
8877        setTimeout(function(){
8878          try{inp.scrollIntoView({behavior:bkScrollBehavior(),block:"center",inline:"nearest"});}catch(_e){}
8879        },280);
8880      });
8881      inp.addEventListener("blur",function(){
8882        /* defer remove kb-padded fino a quando un altro input non prende focus */
8883        setTimeout(function(){
8884          if(scroller&&document.activeElement&&document.activeElement.matches&&!document.activeElement.matches(".caf-bk-inp,input,select,textarea"))
8885            scroller.classList.remove("kb-padded");
8886        },50);
8887      });
8888    });
8889  };
8890
8891  /* Toast non-bloccante (#38). Type: 'error' | 'warn' | 'info'. Auto-dismiss 4.5s. */
8892  CafChat.prototype._bkShowToast = function(opts){
8893    opts=opts||{};
8894    var container=this._bkFsMode?this._bkFsEl:this.panelEl;
8895    if(!container)return;
8896    /* rimuovi eventuali toast precedenti */
8897    var prev=container.querySelectorAll(".caf-bk-toast");prev.forEach(function(p){p.parentNode&&p.parentNode.removeChild(p);});
8898    var t=document.createElement("div");t.className="caf-bk-toast "+(opts.type==="warn"?"warn":opts.type==="info"?"info":"");
8899    /* WCAG 4.1.3 (#C37/#C47): toast esposto alle AT — errore=alert, stato=status */
8900    t.setAttribute("role",(opts.type==="warn"||opts.type==="info")?"status":"alert");
8901    var ic={
8902      error:'<svg class="caf-bk-toast-ic" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"><circle cx="12" cy="12" r="10"/><path d="M12 8v4m0 4h.01"/></svg>',
8903      warn:'<svg class="caf-bk-toast-ic" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M10.29 3.86 1.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"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>',
8904      info:'<svg class="caf-bk-toast-ic" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"><circle cx="12" cy="12" r="10"/><path d="M12 16v-4M12 8h.01"/></svg>'
8905    }[opts.type==="warn"?"warn":opts.type==="info"?"info":"error"];
8906    t.innerHTML=ic
8907      +'<div class="caf-bk-toast-body">'
8908        +(opts.title?'<div class="caf-bk-toast-h">'+escapeHtml(opts.title)+'</div>':'')
8909        +escapeHtml(opts.msg||"")
8910      +'</div>'
8911      +'<button type="button" class="caf-bk-toast-x" aria-label="' + escapeHtml(bkT("aria_close")) + '">×</button>';
8912    container.appendChild(t);
8913    var rm=function(){try{t.parentNode&&t.parentNode.removeChild(t);}catch(_e){}};
8914    t.querySelector(".caf-bk-toast-x").addEventListener("click",rm);
8915    var ttl=opts.duration||4500;setTimeout(rm,ttl);
8916  };
8917
8918  /* Mappa codice errore Planner → messaggio user-friendly + classe (#38, #57).
8919   * Ora i18n: usa bkT() per titolo + descrizione localizzati. */
8920  CafChat.prototype._bkFriendlyError = function(code,msg){
8921    var keyMap={
8922      timeout:["err_h_timeout","err_s_timeout"],
8923      network:["err_h_network","err_s_network"],
8924      no_services:["err_h_no_services","err_s_no_services"],
8925      no_sedi:["err_h_no_sedi","err_s_no_sedi"],
8926      no_spazi:["err_h_no_spazi","err_s_no_spazi"],
8927      already_booked:["err_h_already_booked","err_s_already_booked"],
8928      slot_taken:["err_h_slot_taken","err_s_slot_taken"],
8929      validation:["err_h_validation","err_s_validation"],
8930      disabled:["err_h_disabled","err_s_disabled"],
8931      server:["err_h_server","err_s_server"],
8932      unknown:["err_h_default","err_s_default"]
8933    };
8934    var keys=keyMap[code]||keyMap.unknown;
8935    return{title:bkT(keys[0]),msg:msg||bkT(keys[1])};
8936  };
8937
8938  /** Funnel event logger (fire-and-forget). Usa /api/planner/log-event.
8939   *  Non solleva mai eccezioni: il logging non deve mai rompere il flow.
8940   *  GDPR: gated da consenso "analytics". Senza consenso, no-op completo —
8941   *  niente sessionId persistente, niente UTM, niente fetch. */
8942  CafChat.prototype._bkLogEvent = function(eventType,extra){
8943    if(this.opts&&this.opts.demopromo)return; /* DEMOPROMO: nessuna telemetria verso /api/planner/log-event */
8944    if(!bkConsentHas("analytics"))return; /* GDPR gating: tracker = consenso */
8945    try{
8946      if(!this.opts||!this.opts.baseUrl)return;
8947      if(!this._sessionId){
8948        try{
8949          var k=BK_SID_KEY;var v=localStorage.getItem(k);
8950          if(!v){v=String(Date.now())+"-"+Math.random().toString(36).slice(2,9);localStorage.setItem(k,v);}
8951          this._sessionId=v;
8952        }catch(_e){this._sessionId="anon-"+Math.random().toString(36).slice(2,9);}
8953      }
8954      if(!this._bkUtm)this._bkUtm=readUtmParams();
8955      var body={
8956        event:eventType,
8957        sessionId:this._sessionId,
8958        source:"widget-inline",
8959        connessione:this.bkSelRegione&&this.bkSelRegione.connessione||null,
8960        regione:this.bkSelRegione||null,
8961        provincia:this.bkSelProv||null,
8962        servizio:this.bkSelServ?{codice:this.bkSelServ.codice,descrizione:this.bkSelServ.descrizione}:null,
8963        sede:this.bkSelSede?{codice:this.bkSelSede.codice,descrizione:this.bkSelSede.descrizione}:null,
8964        slot:this.bkSelSlot?{id_spazio:this.bkSelSlot.id_spazio,data:this.bkSelSlot.data,ora_inizio:this.bkSelSlot.ora_inizio}:null,
8965        extra:Object.assign({},this._bkUtm||{},extra||{}),
8966        context:{pageUrl:(typeof location!=="undefined"&&location.href)||"",userAgent:(typeof navigator!=="undefined"&&navigator.userAgent)||""}
8967      };
8968      fetch(this.opts.baseUrl+"/api/planner/log-event",{
8969        method:"POST",headers:{"Content-Type":"application/json"},
8970        body:JSON.stringify(body),keepalive:true
8971      }).catch(function(){});
8972    }catch(_e){}
8973  };
8974
8975  /** Promise wrapper around navigator.geolocation.getCurrentPosition.
8976   *  Resolves to {lat,lon} or rejects with the GeolocationPositionError.
8977   *
8978   *  Strategia a 2 stadi per massimizzare il successo anche quando il
8979   *  browser non ha una posizione cached:
8980   *   1) Tentativo veloce: cache permessa (maximumAge 10m), timeout breve.
8981   *      Se il browser ha già un fix recente risponde quasi subito.
8982   *   2) Se il primo fallisce SOLO per timeout/unavailable (NON per permesso
8983   *      negato), riprova senza cache con high-accuracy e timeout lungo.
8984   *      Questo recupera molti casi su desktop (Wi-Fi positioning lento)
8985   *      e su mobile a freddo (primo GPS-fix). */
8986  /** Fast-path "permesso già negato": legge lo stato via Permissions API SENZA
8987   *  far comparire alcun prompt. Risolve `true` SOLO se lo stato è 'denied', così
8988   *  un utente che ha già bloccato la geolocalizzazione fallisce subito (~0ms)
8989   *  invece di aspettare il watchdog. Affidabile su Chrome/Firefox/Edge.
8990   *  Safari<16 lancia (TypeError) e Safari>=16 riporta 'prompt' anche se negato:
8991   *  in entrambi i casi torniamo `false` e proseguiamo col GPS reale (il watchdog
8992   *  resta il backstop cross-browser). Mai usato come unica difesa. */
8993  CafChat.prototype._bkGeoPermissionDenied = function(){
8994    try{
8995      if(!navigator.permissions||!navigator.permissions.query)return Promise.resolve(false);
8996      return navigator.permissions.query({name:"geolocation"})
8997        .then(function(st){return !!(st&&st.state==="denied");})
8998        .catch(function(){return false;});
8999    }catch(_e){return Promise.resolve(false);}
9000  };
9001
9002  /** opts.cityLevel: il chiamante deve solo matchare la PROVINCIA (start /
9003   *  "Usa la mia posizione") → basta un fix city-level. Salta lo Stage 2
9004   *  high-accuracy (che non aggiunge nulla al match e ritarda solo il
9005   *  fallback IP-geo) e stringe il watchdog 10s→7s (Stage1 6s + margine). */
9006  CafChat.prototype._bkGetGeoPosition = function(opts){
9007    var self=this;
9008    var cityLevel=!!(opts&&opts.cityLevel);
9009    return new Promise(function(resolve,reject){
9010      if(!navigator.geolocation){reject({code:0,message:"Geolocalizzazione non supportata dal browser."});return;}
9011      if(window.isSecureContext===false){reject({code:0,message:"La geolocalizzazione richiede HTTPS."});return;}
9012      var done=false;
9013      /* Watchdog: il `timeout` di getCurrentPosition NON scatta mentre il
9014       * prompt è in attesa di risposta (W3C Geolocation §7.2) e alcuni browser
9015       * (iOS Safari) lo ignorano del tutto → il watchdog è l'UNICO bound reale.
9016       * Armato SUBITO (sincrono), prima della Permissions API, così il bound
9017       * esiste da t=0. Default 10s: copre Stage1(6s)+Stage2(8s) clampati e un
9018       * fix lento legittimo, senza punire chi non concede il permesso.
9019       * cityLevel 7s: solo Stage1(6s)+margine. */
9020      var watchdog=setTimeout(function(){
9021        if(done)return;done=true;
9022        reject({code:3,message:"GPS non risponde."});
9023      },cityLevel?7000:10000);
9024      var ok=function(pos){if(done)return;done=true;clearTimeout(watchdog);resolve({lat:pos.coords.latitude,lon:pos.coords.longitude,accuracy:pos.coords.accuracy});};
9025      var fail=function(err){if(done)return;done=true;clearTimeout(watchdog);reject(err||{code:0,message:"Errore di geolocalizzazione."});};
9026      self._bkGeoPermissionDenied().then(function(denied){
9027        if(done)return;
9028        if(denied){fail({code:1,message:"Permesso di geolocalizzazione negato."});return;}
9029        /* Stage 1 — veloce, cache 10min, low-accuracy */
9030        try{
9031          navigator.geolocation.getCurrentPosition(
9032            ok,
9033            function(err1){
9034              if(done)return;
9035              /* Permesso negato: niente retry, è inutile e peggiora UX. */
9036              if(err1&&err1.code===1){fail(err1);return;}
9037              /* cityLevel: niente Stage 2 — meglio passare subito al
9038               * fallback IP del chiamante che insistere sul GPS. */
9039              if(cityLevel){fail(err1);return;}
9040              /* Stage 2 — fresh, high-accuracy, timeout 8s. */
9041              try{
9042                navigator.geolocation.getCurrentPosition(
9043                  ok,
9044                  function(err2){fail(err2||err1);},
9045                  {enableHighAccuracy:true,timeout:8000,maximumAge:0}
9046                );
9047              }catch(_e){fail(err1);}
9048            },
9049            {enableHighAccuracy:false,timeout:6000,maximumAge:600000}
9050          );
9051        }catch(syncErr){
9052          fail({code:0,message:"Errore avvio GPS"});
9053        }
9054      });
9055    });
9056  };
9057
9058  /** Variante HIGH-PRECISION per il calcolo "sede più vicina".
9059   *  Richiede SEMPRE GPS fresco ad alta accuratezza (no cache), perché
9060   *  per il distance ranking ci serve precisione metrica, non city-level.
9061   *  Fallisce subito se l'utente ha negato il permesso.
9062   *  Watchdog 22s: alcuni browser (iOS Safari edge cases) ignorano il
9063   *  parametro `timeout` di getCurrentPosition e la Promise non si
9064   *  risolverebbe mai → bloccaremmo l'utente sul loading. */
9065  CafChat.prototype._bkGetGeoPositionPrecise = function(){
9066    var self=this;
9067    return new Promise(function(resolve,reject){
9068      if(!navigator.geolocation){reject({code:0,message:"Geolocalizzazione non supportata dal browser."});return;}
9069      if(window.isSecureContext===false){reject({code:0,message:"La geolocalizzazione richiede HTTPS."});return;}
9070      var done=false;
9071      /* Watchdog 16s (più del low-acc): l'alta precisione a freddo può
9072       * richiedere più tempo e qui serve un fix metrico per la "sede più
9073       * vicina", quindi non tagliamo troppo. Armato sincrono prima della
9074       * Permissions API così il bound esiste da t=0. */
9075      var watchdog=setTimeout(function(){
9076        if(done)return;done=true;
9077        reject({code:3,message:"GPS non risponde."});
9078      },16000);
9079      var ok=function(pos){if(done)return;done=true;clearTimeout(watchdog);resolve({lat:pos.coords.latitude,lon:pos.coords.longitude,accuracy:pos.coords.accuracy});};
9080      var fail=function(err){if(done)return;done=true;clearTimeout(watchdog);
9080reject(err||{code:0,message:"Errore di geolocalizzazione."});};
9081      self._bkGeoPermissionDenied().then(function(denied){
9082        if(done)return;
9083        if(denied){fail({code:1,message:"Permesso di geolocalizzazione negato."});return;}
9084        try{
9085          navigator.geolocation.getCurrentPosition(
9086            ok,fail,
9087            /* enableHighAccuracy:true → GPS hardware (mobile) o Wi-Fi triangulation
9088             *   ad alta precisione (desktop). maximumAge:0 → niente cache, sempre
9089             *   fix fresco. timeout:15s → GPS può richiedere 15+ s a freddo. */
9090            {enableHighAccuracy:true,timeout:15000,maximumAge:0}
9091          );
9092        }catch(syncErr){
9093          fail({code:0,message:"Errore avvio GPS: "+(syncErr&&syncErr.message||syncErr)});
9094        }
9095      });
9096    });
9097  };
9098
9099  /** Fallback: geolocalizzazione via IP quando il browser non riesce
9100   *  (desktop senza GPS, Wi-Fi positioning fallito, Linux senza geoclue,
9101   *  mobile in interno con segnale assente, ecc.). Precisione ~città — più
9102   *  che sufficiente per matchare la provincia.
9103   *
9104   *  Prova più endpoint HTTPS in cascata (no API key richiesta) finché uno
9105   *  risponde. Timeout per endpoint: 4s. */
9106  CafChat.prototype._bkGetIpGeoPosition = function(){
9107    var endpoints=[
9108      {url:"https://ipapi.co/json/",pick:function(j){return (j&&typeof j.latitude==="number"&&typeof j.longitude==="number")?{lat:j.latitude,lon:j.longitude}:null;}},
9109      {url:"https://get.geojs.io/v1/ip/geo.json",pick:function(j){var la=j&&parseFloat(j.latitude),lo=j&&parseFloat(j.longitude);return (isFinite(la)&&isFinite(lo))?{lat:la,lon:lo}:null;}},
9110      {url:"https://ipwho.is/",pick:function(j){return (j&&j.success!==false&&typeof j.latitude==="number"&&typeof j.longitude==="number")?{lat:j.latitude,lon:j.longitude}:null;}}
9111    ];
9112    /* Endpoint in PARALLELO: vince il primo che risponde con coords valide.
9113     * Era una cascata seriale (3 × 3s = fino a 9s) → ora ~3s (il più lento).
9114     * Promise.any è supportato su tutti i target (Chrome 85+/FF 79+/Safari 14+);
9115     * fallback con reduce per browser più vecchi. Normalizziamo l'errore alla
9116     * forma {code:0} che i chiamanti già si aspettano (mai un AggregateError). */
9117    var attempts=endpoints.map(function(ep){
9118      var ctrl=new AbortController();var tid=setTimeout(function(){ctrl.abort();},3000);
9119      return fetch(ep.url,{signal:ctrl.signal,headers:{"Accept":"application/json"}})
9120        .then(function(r){clearTimeout(tid);if(!r.ok)throw new Error("http "+r.status);return r.json();})
9121        .then(function(j){var c=ep.pick(j);if(!c)throw new Error("no coords");return c;})
9122        .catch(function(e){clearTimeout(tid);throw e;});
9123    });
9124    var race=Promise.any?Promise.any(attempts):attempts.reduce(function(p,n){return p.catch(function(){return n;});});
9125    return race.catch(function(){return Promise.reject({code:0,message:"IP geolocation failed"});});
9126  };
9127
9128  /** Solo il fetch Nominatim → ritorna l'oggetto `address` ({} se assente),
9129   *  o null su risposta non-ok. Separato dal match così può partire in
9130   *  PREFETCH appena il GPS risolve, in parallelo al fan-out province. */
9131  CafChat.prototype._bkReverseGeocodeFetch = async function(coords){
9132    var ctrl=new AbortController();var tid=setTimeout(function(){ctrl.abort();},6000);
9133    var resp;
9134    try{
9135      resp=await fetch("https://nominatim.openstreetmap.org/reverse?format=json&zoom=10&addressdetails=1&lat="+coords.lat+"&lon="+coords.lon,
9136        {signal:ctrl.signal,headers:{"Accept":"application/json"}});
9137    }finally{clearTimeout(tid);}
9138    if(!resp.ok)return null;
9139    var data=await resp.json();
9140    return (data&&data.address)||{};
9141  };
9142
9143  /** Reverse-geocode (Nominatim) + match against a flat all-Italy list.
9144   *  Returns {provincia, regione} or null.
9145   *  addrP (opzionale): promise di un _bkReverseGeocodeFetch già avviato. */
9146  CafChat.prototype._bkReverseGeocodeAndMatch = async function(coords,allData,addrP){
9147    if(!allData||!allData.items||!allData.items.length)return null;
9148    var addr=await (addrP||this._bkReverseGeocodeFetch(coords));
9149    if(addr==null)return null;
9150    var candidates=[addr.county,addr.province,addr.state_district,addr.city,addr.town,addr.municipality].filter(Boolean);
9151    var allProv=allData.items.map(function(x){return x.provincia;});
9152    var matched=null;
9153    for(var i=0;i<candidates.length&&!matched;i++){
9154      matched=matchProvince(candidates[i],allProv);
9155    }
9156    if(!matched){
9157      /* FALLBACK: nessuna provincia API matchata.
9158       * Se conosciamo la regione geografica (es. Nominatim addr.state="Toscana"),
9159       * cerchiamo nella lista regioni un entry placeholder "X REGIONALE" che
9160       * la corrisponde — così l'utente in Toscana viene auto-portato sulla
9161       * regione "TOSCANA REGIONALE" e da lì espandiamo alle province reali. */
9162      var stateName=addr.state||addr["ISO3166-2-lvl4"]||"";
9163      if(stateName){
9164        var regionalEntry=bkFindRegionalEntry(allData.regioni||[],stateName);
9165        if(regionalEntry){
9166          return {matched:null,address:addr,regionalEntry:regionalEntry};
9167        }
9168      }
9169      return {matched:null,address:addr};
9170    }
9171    var hit=null;
9172    for(var k=0;k<allData.items.length;k++){
9173      if(allData.items[k].provincia.codice===matched.codice){hit=allData.items[k];break;}
9174    }
9175    return {matched:matched,regione:hit?hit.regione:null,address:addr};
9176  };
9177
9178  CafChat.prototype._bkStart = async function(){
9179    if(this._bkBusy)return;this._bkBusy=true;
9180    /* Generazione di QUESTA apertura: chiudi+riapri rapido (con _bkBusy già
9181     * liberato dal picker early) fa partire una seconda _bkStart mentre le
9182     * promise della prima (fan-out, GPS, Nominatim) sono ancora in volo —
9183     * senza questo token i loro handler clobberebbero lo stato della nuova
9184     * apertura (bkRegioni/bkStep/banner) quando finalmente risolvono.
9185     * NON riusiamo _bkOpGen: viene incrementato da ogni _bkPickRegione/Prov
9186     * (anche manuali) e invaliderebbe l'auto-jump provincia legacy. */
9187    var _sg=this._bkStartGen=(this._bkStartGen||0)+1;
9188    if(this._updateBkCta)this._updateBkCta();
9189    /* La prenotazione avviene SEMPRE in modalità fullscreen — niente più
9190     * widget inline nella conversazione. */
9191    if(!this._bkFsMode){
9192      try{this._bkEnterFs();}catch(_e){}
9193    }
9194    /* #42 — GDPR: chiediamo consenso PRIMA di qualsiasi altra cosa che
9195     * possa scrivere/leggere dati personali. Il banner appare una sola
9196     * volta (poi è persistito). Se l'utente dà rifiuta-tutti, la
9197     * prenotazione funziona comunque ma niente pre-fill / resume / log. */
9198    if(!bkConsentExpressed()){
9199      this._bkBusy=false; /* libera busy: il banner è non bloccante per il flow */
9200      try{await this._bkRequestConsent();}catch(_e){}
9201      this._bkBusy=true;
9202    }
9203    /* #60 — se c'è uno stato di resume valido (ultime 24h) e non l'abbiamo
9204     * già proposto in questa sessione, chiediamo se vuole riprendere.
9205     * loadResumeState ritorna null senza consenso functional → no resume. */
9206    if(!this._bkResumeAsked){
9207      this._bkResumeAsked=true;
9208      var rs=loadResumeState();
9209      if(rs&&rs.step&&this._bkOfferResume){
9210        var didResume=await this._bkOfferResume(rs);
9211        if(didResume){this._bkBusy=false;return;}
9212      }
9213    }
9214    /* Dopo gli await di consenso/resume: se nel frattempo è partita una
9215     * _bkStart più recente, questa apertura è superata — non deve creare un
9216     * secondo prompt GPS né toccare bkStep/banner della nuova. */
9217    if(this._bkStartGen!==_sg)return;
9218    /* DEMOPROMO: percorso deterministico e OFFLINE — nessun GPS / IP-geo /
9219     * Nominatim. Mostra subito il picker delle regioni dei dati di esempio (o
9220     * auto-seleziona se il dataset ha una sola regione). Il rilevamento posizione
9221     * resta disponibile, simulato, dal bottone allo step provincia. */
9222    if(this.opts&&this.opts.demopromo){
9223      this._bkAutoProvStatus=null;
9224      var _regsD=[];
9225      try{_regsD=await this._bkFetch("regioni");}catch(_e){_regsD=[];}
9226      if(this._bkStartGen!==_sg)return;
9227      /* Le fetch demo non sono abortabili (~180ms): se nel frattempo l'utente ha
9228       * chiuso il wizard (_bkExitFs -> _bkReset, che azzera _bkFsMode e pulisce il
9229       * resume), NON scrivere step/render qui o si salverebbe un resume spurio. */
9230      if(!this._bkFsMode)return;
9231      if(!_regsD||!_regsD.length){this._bkSetError({error:"Nessuna regione disponibile (demo).",code:"no_services"});this._bkBusy=false;return;}
9232      this.bkRegioni=_regsD;
9233      this._bkProvinciaByConn={};
9234      if(_regsD.length===1){this._bkBusy=false;return this._bkPickRegione(_regsD[0]);}
9235      this.bkStep="selectRegione";
9236      this._bkBusy=false;
9237      this._bkRender();
9238      return;
9239    }
9240    /* STEP 0 — GPS NON bloccante. Il picker delle regioni viene mostrato
9241     * appena le province sono pronte; il GPS lavora in background e, se
9242     * risolve PRIMA che l'utente abbia interagito, fa auto-jump alla
9243     * provincia rilevata. Su fallimento (timeout/permesso negato), nessun
9244     * messaggio d'errore mostrato all'utente: sceglie manualmente come
9245     * sempre. Questo elimina il famigerato "Richiesta posizione scaduta". */
9246    this._bkAutoProvStatus={type:"loading",msg:"Caricamento..."};
9247    this.bkStep="loading";this._bkRender();
9248    var self=this;
9249    var earlyShown=false;
9250    /* Esito della geolocalizzazione condiviso fra i vari handler asincroni.
9251     * state: pending|ok|fail. addrP: prefetch Nominatim avviato appena il GPS
9252     * risolve (in parallelo al fan-out province). jumped: l'early-jump ha già
9253     * fatto avanzare la UI. allDataDone: fan-out province completato. */
9254    var geoOutcome={state:"pending",err:null,coords:null,addrP:null,jumped:false};
9255    var allDataDone=false;
9256    /* true quando una _bkStart più recente ha preso il controllo: gli handler
9257     * asincroni di QUESTA apertura devono diventare no-op. */
9258    function _bkStale(){return self._bkStartGen!==_sg;}
9259    function _bkGeoFailText(ge){
9260      /* Messaggio specifico in base al codice di errore così l'utente capisce
9261       * CHE problema c'è (permesso, segnale, timeout) invece di un generico
9262       * "impossibile rilevare". */
9263      if(ge.code===1)return "Permesso di geolocalizzazione negato dal browser. Scegli manualmente qui sotto.";
9264      if(ge.code===2)return "Posizione non disponibile (segnale GPS/Wi-Fi assente). Scegli manualmente qui sotto.";
9265      if(ge.code===3)return "Richiesta posizione scaduta. Scegli manualmente qui sotto.";
9266      return (ge.message||"Impossibile rilevare la posizione")+". Scegli manualmente qui sotto.";
9267    }
9268    /* Riflette geoOutcome nel banner di stato APPENA l'esito è noto — senza
9269     * aspettare il fan-out province. Prima il banner veniva aggiornato solo
9270     * dal consumer agganciato dopo `await _bkFetchAllProvince`: con cache
9271     * fredda (fan-out 6-18s) chi negava il permesso fissava "Sto cercando la
9272     * tua posizione…" per tutto quel tempo, finché il watchdog 12s non lo
9273     * nascondeva SENZA alcun messaggio (misurato via CDP: 12s di finto
9274     * loading dopo il rifiuto). Aggiorna solo se il banner è ancora "nostro"
9275     * (loading) e l'utente non ha già preso il controllo del picker. */
9276    function _bkGeoBannerNow(){
9277      if(_bkStale())return;
9278      if(self._bkUserOverride||self.bkSelProv)return;
9279      if(self.bkStep==="selectRegione"&&self.bkSelRegione)return;
9280      if(self.bkStep!=="selectRegione"&&self.bkStep!=="selectProvincia")return;
9281      if(geoOutcome.state==="fail"){
9282        if(self._bkAutoProvWatchdog){clearTimeout(self._bkAutoProvWatchdog);self._bkAutoProvWatchdog=null;}
9283        self._bkAutoProvStatus={type:"err",msg:_bkGeoFailText(geoOutcome.err||{})};
9284        self._bkRender();
9285      } else if(geoOutcome.state==="ok"&&!geoOutcome.jumped&&earlyShown){
9286        /* Solo nel flusso nazionale multi-regione (earlyShown): lì un consumer
9287         * (early-jump o post-fan-out) consumerà SEMPRE questo loading. Nei
9288         * percorsi ZONA/single-region che ritornano prima di agganciare il
9289         * consumer, il banner resterebbe appeso per sempre (nessun watchdog). */
9290        self._bkAutoProvStatus={type:"loading",msg:"Identifico la tua provincia…"};
9291        self._bkRender();
9292      }
9293    }
9294    /* Mostra il picker REGIONI appena arrivano le regioni — NON aspettare il
9295     * fan-out delle province (collo di bottiglia: N getProvince in parallelo, la
9296     * più lenta bloccava ~8s; in incognito/cookie bloccati la cache non salva mai
9297     * → ogni apertura ripagava il fan-out). Solo caso nazionale multi-regione:
9298     * single-region e ZONA hanno bisogno delle province e restano gestiti sotto.
9299     * Le province caricano in background; _bkPickRegione fa fetch on-demand se
9300     * l'utente sceglie una regione prima che il prefetch finisca. */
9301    function _bkShowRegioniEarly(regs){
9302      if(_bkStale())return;
9303      if(earlyShown||self.bkStep!=="loading")return;
9304      if(!regs||regs.length<=1)return;
9305      var z=(typeof window!=="undefined"&&window.__CAF_ZONA__||"").toString().trim().toLowerCase();
9306      if(z&&z!=="nazionale")return;
9307      earlyShown=true;
9308      self.bkRegioni=regs;
9309      self.bkStep="selectRegione";
9310      /* Il GPS potrebbe aver GIÀ un esito (es. permesso negato mentre le
9311       * regioni caricavano): mostra direttamente quello, non un finto
9312       * "Sto cercando…". */
9313      if(geoOutcome.state==="fail"){
9314        self._bkAutoProvStatus={type:"err",msg:_bkGeoFailText(geoOutcome.err||{})};
9315      } else {
9316        self._bkAutoProvStatus={type:"loading",
9317          msg:geoOutcome.state==="ok"?"Identifico la tua provincia…":"Sto cercando la tua posizione… (puoi anche scegliere manualmente)"};
9318      }
9319      self._bkRender();
9320      self._bkBusy=false;
9321      if(self._bkAutoProvWatchdog){clearTimeout(self._bkAutoProvWatchdog);self._bkAutoProvWatchdog=null;}
9322      if(geoOutcome.state!=="fail"){
9323        self._bkAutoProvWatchdog=setTimeout(function(){
9324          self._bkAutoProvWatchdog=null;
9325          if(self._bkAutoProvStatus&&self._bkAutoProvStatus.type==="loading"
9326             &&(self.bkStep==="selectRegione"||self.bkStep==="selectProvincia")){
9327            self._bkAutoProvStatus=null;self._bkRender();
9328          }
9329        },12000);
9330      }
9331      /* Il GPS potrebbe essere risolto PRIMA che le regioni fossero pronte:
9332       * ora che il picker c'è, l'early-jump può partire. */
9333      if(geoOutcome.state==="ok")_bkGeoEarlyJump().catch(function(){});
9334    }
9335    /* EARLY-JUMP: GPS + reverse-geocode pronti ma fan-out province ancora in
9336     * corso → salta SUBITO alla regione rilevata riusando _bkPickRegione (lo
9337     * stesso percorso del click manuale: fetch on-demand della sola regione,
9338     * geo-filtri e auto-pick placeholder inclusi), poi prova il match della
9339     * provincia sulla lista appena caricata. Con cache fredda il fan-out
9340     * costa 6-18s: senza questo salto anche chi CONCEDE subito il permesso
9341     * aspettava l'intero fan-out prima dell'auto-jump (misurato: ~12s).
9342     * Se qualcosa non matcha si ritorna senza toccare nulla: il consumer
9343     * post-fan-out (match flat su TUTTA Italia) resta il fallback. */
9344    async function _bkGeoEarlyJump(){
9345      if(_bkStale()||allDataDone||geoOutcome.jumped||geoOutcome.state!=="ok")return;
9346      if(self._bkUserOverride||self.bkSelProv||self.bkSelRegione)return;
9347      if(self.bkStep!=="selectRegione")return;
9348      if(!self.bkRegioni||self.bkRegioni.length<=1)return;
9349      var addr=null;
9350      try{addr=await geoOutcome.addrP;}catch(_e){return;}
9351      if(!addr)return;
9352      /* Ri-verifica DOPO l'await: l'utente o il fan-out potrebbero aver agito. */
9353      if(_bkStale()||allDataDone||geoOutcome.jumped)return;
9354      if(self._bkUserOverride||self.bkSelProv||self.bkSelRegione)return;
9355      if(self.bkStep!=="selectRegione")return;
9356      var stateName=addr.state||addr["ISO3166-2-lvl4"]||"";
9357      if(!stateName)return;
9358      var entry=bkFindRegionalEntry(self.bkRegioni,stateName);
9359      if(!entry)return;
9360      geoOutcome.jumped=true;
9361      self._bkUserCoords=geoOutcome.coords;
9362      if(self._bkAutoProvWatchdog){clearTimeout(self._bkAutoProvWatchdog);self._bkAutoProvWatchdog=null;}
9363      self._bkLogEvent&&self._bkLogEvent("regione_auto_picked_geo_early",{regione:entry.descrizione});
9364      self._bkAutoProvStatus={type:"ok",msg:"Trovato: "+entry.descrizione+". Carico le province..."};
9365      self._bkBusy=false;self._bkRender();
9366      await self._bkPickRegione(entry);
9367      /* Se siamo arrivati al picker province (niente auto-pick placeholder),
9368       * prova ad auto-selezionare la provincia rilevata da Nominatim. */
9369      if(_bkStale())return;
9370      if(self.bkStep!=="selectProvincia"||self.bkSelProv)return;
9371      var candidates=[addr.county,addr.province,addr.state_district,addr.city,addr.town,addr.municipality].filter(Boolean);
9372      var matched=null;
9373      for(var ci=0;ci<candidates.length&&!matched;ci++){matched=matchProvince(candidates[ci],self.bkProvince||[]);}
9374      if(matched){
9375        self._bkAutoProvStatus={type:"ok",msg:"Trovato: "+matched.descrizione+" ("+entry.descrizione+"). Cerco i servizi..."};
9376        self._bkBusy=false;self._bkRender();
9377        self._bkPickProv(matched);
9378      }
9379    }
9380    /* Catena: browser-GPS → IP-geo fallback. Se il browser fallisce per
9381     * qualunque motivo che NON sia "permesso negato" (code 1), proviamo a
9382     * dedurre la posizione via IP. Per matchare la provincia è più che
9383     * sufficiente, e risolve i casi più comuni di failure: desktop senza
9384     * GPS che dipende dal Wi-Fi positioning fallito, mobile su rete dati
9385     * con segnale GPS assente, Linux senza geoclue, ecc.
9386     * cityLevel: per il match provincia basta un fix city-level — lo stage
9387     * high-accuracy allungava solo l'attesa prima del fallback IP. */
9388    var posP=this._bkGetGeoPosition({cityLevel:true}).then(function(p){return {ok:true,coords:p};}).catch(function(err){
9389      /* Permesso esplicitamente negato: rispetta la scelta utente, niente IP. */
9390      if(err&&err.code===1)return {ok:false,err:err};
9391      return self._bkGetIpGeoPosition().then(function(p){return {ok:true,coords:p,viaIp:true};}).catch(function(){return {ok:false,err:err};});
9392    });
9393    /* Handler IMMEDIATO (sganciato dal fan-out province): banner aggiornato
9394     * appena l'esito geo è noto + prefetch Nominatim + eventuale early-jump. */
9395    posP.then(function(r){
9396      if(r.ok){
9397        geoOutcome.state="ok";geoOutcome.coords=r.coords;
9398        if(_bkStale())return; /* niente prefetch/banner per un'apertura superata */
9399        geoOutcome.addrP=self._bkReverseGeocodeFetch(r.coords);
9400        geoOutcome.addrP.catch(function(){}); /* consumata altrove; evita unhandledrejection */
9401        _bkGeoBannerNow();
9402        _bkGeoEarlyJump().catch(function(){});
9403      } else {
9404        geoOutcome.state="fail";geoOutcome.err=r.err||{};
9405        _bkGeoBannerNow();
9406      }
9407    });
9408    try{
9409      var allData=await this._bkFetchAllProvince({onRegioni:_bkShowRegioniEarly}).catch(function(){return {regioni:[],items:[]};});
9410      allDataDone=true; /* da qui l'early-jump non parte più: match flat sotto */
9411      if(_bkStale())return; /* un'apertura più recente ha preso il controllo */
9412      if(!allData.regioni||!allData.regioni.length){
9413        this._bkSetError({error:"Nessuna regione disponibile per la prenotazione.",code:"no_services"});return;
9414      }
9415      this.bkRegioni=allData.regioni;
9416      /* Cache province per connessione: evita re-fetch in _bkPickRegione */
9417      var _selfP=this;
9418      this._bkProvinciaByConn={};
9419      allData.items.forEach(function(it){
9420        var k=it.regione.connessione;if(!_selfP._bkProvinciaByConn[k])_selfP._bkProvinciaByConn[k]=[];
9421        _selfP._bkProvinciaByConn[k].push(it.provincia);
9422      });
9423      /* ZONA preselect (deploy multi-tenant): se window.__CAF_ZONA__ è
9424       * settato a "veneto" / "toscana" / ecc. e una regione con quella
9425       * descrizione esiste, la auto-pickiamo via _bkPickRegione (che gestisce
9426       * province, geo-filtri, placeholder auto-pick). "nazionale" o vuoto =
9427       * comportamento standard. */
9428      var zonaPre=(typeof window!=="undefined"&&window.__CAF_ZONA__||"").toString().trim().toLowerCase();
9429      if(zonaPre&&zonaPre!=="nazionale"){
9430        var zonaMatch=null;
9431        for(var zi=0;zi<this.bkRegioni.length;zi++){
9432          var zd=((this.bkRegioni[zi]&&this.bkRegioni[zi].descrizione)||"").trim().toLowerCase();
9433          if(zd===zonaPre){zonaMatch=this.bkRegioni[zi];break;}
9434        }
9435        if(zonaMatch){
9436          this._bkLogEvent&&this._bkLogEvent("regione_auto_picked_zona",{zona:zonaPre});
9437          this._bkBusy=false;
9438          return this._bkPickRegione(zonaMatch);
9439        }
9440        this._bkLogEvent&&this._bkLogEvent("regione_zona_no_match",{zona:zonaPre,available:this.bkRegioni.map(function(r){return r.descrizione;}).join(",")});
9441      }
9442      /* Mostra SUBITO il picker — niente attesa GPS. */
9443      if(this.bkRegioni.length===1){
9444        var only=this.bkRegioni[0];this.bkSelRegione=only;
9445        this.bkProvince=allData.items
9446          .filter(function(x){return x.regione.connessione===only.connessione;})
9447          .map(function(x){return x.provincia;});
9448        if(!this.bkProvince.length){this._bkSetError({error:"Nessuna provincia disponibile per "+only.descrizione+".",code:"no_services"});return;}
9449        /* FILTRO GEOGRAFICO: stessa logica di _bkPickRegione. */
9450        var detectedR=bkExtractRegionName(only.descrizione);
9451        if(detectedR){
9452          var beforeN0=this.bkProvince.length;
9453          this.bkProvince=bkFilterProvincesByRegion(this.bkProvince,detectedR);
9454          if(this.bkProvince.length<beforeN0){
9455            this._bkLogEvent&&this._bkLogEvent("province_geo_filtered",{regione:only.descrizione,detected:detectedR,before:beforeN0,after:this.bkProvince.length,trigger:"single_region_start"});
9456          }
9457        }
9458        /* AUTO-PICK PLACEHOLDER MATCH: stessa logica di _bkPickRegione. */
9459        var rDescS=(only.descrizione||"").trim().toLowerCase();
9460        var matchPS=null;
9461        if(rDescS){
9462          for(var miS=0;miS<this.bkProvince.length;miS++){
9463            var pdS=((this.bkProvince[miS]&&this.bkProvince[miS].descrizione)||"").trim().toLowerCase();
9464            if(pdS&&pdS===rDescS){matchPS=this.bkProvince[miS];break;}
9465          }
9466        }
9467        if(matchPS){
9468          this._bkLogEvent&&this._bkLogEvent("provincia_auto_picked_placeholder",{regione:only.descrizione,trigger:"single_region_start"});
9469          this._bkBusy=false;this._bkPickProv(matchPS);return;
9470        }
9471        if(this.bkProvince.length===1){this._bkBusy=false;this._bkPickProv(this.bkProvince[0]);return;}
9472        this.bkStep="selectProvincia";
9473      } else {
9474        if(!earlyShown)this.bkStep="selectRegione";
9475      }
9476      /* Indicatore non-bloccante mentre il GPS lavora in background.
9477       * L'utente vede che la posizione è in cerca, MA può comunque scegliere
9478       * manualmente — il picker è già interattivo. Quando il GPS risolve
9479       * (success o fail) lo status viene aggiornato/nascosto.
9480       * SALTATO se il picker è già stato mostrato in anticipo
9481       * (_bkShowRegioniEarly ha già impostato step/status/render/watchdog):
9482       * ri-renderizzare qui sovrascriverebbe un'eventuale navigazione già fatta
9483       * dall'utente nei secondi di attesa del fan-out province. */
9484      if(!earlyShown){
9485        /* Come in _bkShowRegioniEarly: se il GPS ha GIÀ un esito, il banner
9486         * lo riflette subito invece di un finto "Sto cercando…". */
9487        if(geoOutcome.state==="fail"){
9488          this._bkAutoProvStatus={type:"err",msg:_bkGeoFailText(geoOutcome.err||{})};
9489        } else {
9490          this._bkAutoProvStatus={type:"loading",
9491            msg:geoOutcome.state==="ok"?"Identifico la tua provincia…":"Sto cercando la tua posizione… (puoi anche scegliere manualmente)"};
9492        }
9493        this._bkRender();
9494        this._bkBusy=false;
9495        /* WATCHDOG: se entro 12s lo status loading non è cambiato
9496         * (es. GPS che non chiama mai callback, browser bug), lo
9497         * forziamo a null e l'utente vede solo il picker manuale. 12s copre
9498         * il budget GPS in background (watchdog interno ≤10s) + margine. */
9499        if(self._bkAutoProvWatchdog){clearTimeout(self._bkAutoProvWatchdog);self._bkAutoProvWatchdog=null;}
9500        if(geoOutcome.state!=="fail"){
9501          self._bkAutoProvWatchdog=setTimeout(function(){
9502            self._bkAutoProvWatchdog=null;
9503            if(self._bkAutoProvStatus&&self._bkAutoProvStatus.type==="loading"
9504               &&(self.bkStep==="selectRegione"||self.bkStep==="selectProvincia")){
9505              self._bkAutoProvStatus=null;self._bkRender();
9506            }
9507          },12000);
9508        }
9509      }
9510      /* GPS in background → auto-jump alla provincia rilevata SE l'utente
9511       * non ha ancora interagito col picker. Failure: status nascosto.
9512       * Agganciato SEMPRE (anche se il picker è già visibile): consuma il
9513       * risultato GPS — qui `allData` è risolto, quindi il match provincia
9514       * dispone della lista completa. */
9515      posP.then(async function(posResult){
9516        if(_bkStale())return;
9517        /* Fallimento: già gestito in tempo reale dall'handler agganciato alla
9518         * creazione di posP (banner err immediato, watchdog ripulito). Qui non
9519         * c'è nulla da fare — e NON dobbiamo azzerare il banner di errore. */
9520        if(!posResult.ok)return;
9521        if(self._bkAutoProvWatchdog){clearTimeout(self._bkAutoProvWatchdog);self._bkAutoProvWatchdog=null;}
9522        /* Early-jump già avvenuto (fan-out più lento di GPS+Nominatim): la UI
9523         * è già avanzata tramite _bkPickRegione, niente doppio salto. */
9524        if(geoOutcome.jumped)return;
9525        /* Se l'utente ha già scelto qualcosa, ignora il GPS senza disturbare.
9526         * IMPORTANTE: chiamiamo SEMPRE _bkRender() prima di return per evitare
9527         * che il banner "Sto cercando..." resti visibile. */
9528        /* Override esplicito (es. click sulla sidebar "Regione"): la scelta
9529         * manuale ha la precedenza assoluta, niente auto-jump da GPS. */
9530        if(self._bkUserOverride){self._bkAutoProvStatus=null;self._bkRender();return;}
9531        if(self.bkSelProv){self._bkAutoProvStatus=null;self._bkRender();return;}
9532        if(self.bkStep==="selectRegione"&&self.bkSelRegione){self._bkAutoProvStatus=null;self._bkRender();return;}
9533        if(self.bkStep!=="selectRegione"&&self.bkStep!=="selectProvincia"){self._bkAutoProvStatus=null;return;}
9534        self._bkUserCoords=posResult.coords;
9535        self._bkAutoProvStatus={type:"loading",msg:"Identifico la tua provincia…"};self._bkRender();
9536        try{
9537          var geo=await self._bkReverseGeocodeAndMatch(posResult.coords,allData,geoOutcome.addrP);
9538          /* Re-check dopo l'await: l'utente potrebbe aver interagito */
9539          if(_bkStale())return;
9540          if(self.bkSelProv){self._bkAutoProvStatus=null;self._bkRender();return;}
9541          if(self.bkStep==="selectRegione"&&self.bkSelRegione){self._bkAutoProvStatus=null;self._bkRender();return;}
9542          if(self.bkStep!=="selectRegione"&&self.bkStep!=="selectProvincia"){self._bkAutoProvStatus=null;return;}
9543          /* Cross-region guard: se l'utente ha già scelto una regione
9544           * diversa da quella rilevata dal GPS, NON sovrascrivere la
9545           * sua scelta. Questo si applica solo quando bkRegioni.length>1
9546           * (selezione multi-regione, non auto-set single-region). */
9547          if(geo&&geo.matched&&geo.regione&&self.bkStep==="selectProvincia"
9548              &&self.bkRegioni&&self.bkRegioni.length>1
9549              &&self.bkSelRegione&&self.bkSelRegione.connessione!==geo.regione.connessione){
9550            self._bkAutoProvStatus=null;self._bkRender();return;
9551          }
9552          if(geo&&geo.matched&&self._bkApplyMatchedProvincia(allData,geo.matched)){
9553            self._bkAutoProvStatus={type:"ok",msg:"Trovato: "+geo.matched.descrizione+" ("+(self.bkSelRegione&&self.bkSelRegione.descrizione||"")+"). Cerco i servizi..."};
9554            /* Forza il busy-flag a false: se il main flow di _bkStart per
9555             * qualche motivo ha lasciato il busy a true, _bkPickProv
9556             * uscirebbe in silenzio dal suo busy-guard e l'utente vedrebbe
9557             * solo "Trovato …" senza mai avanzare ai servizi. */
9558            self._bkBusy=false;
9559            self._bkRender();
9560            /* Fallback di sicurezza: se dopo 800ms siamo ancora su
9561             * selectRegione/selectProvincia (cioè _bkPickProv non è
9562             * partito o ha fallito senza errori visibili), lo invochiamo
9563             * di nuovo o portiamo l'utente a selectProvincia per dargli
9564             * controllo. */
9565            self._bkPickProv(geo.matched);
9566            setTimeout(function(){
9567              if(self.bkStep==="selectRegione"||self.bkStep==="selectProvincia"){
9568                /* _bkPickProv non è andato a buon fine. Mostra picker
9569                 * provincia (regione già impostata) così l'utente non
9570                 * resta bloccato sul picker regioni. */
9571                if(self.bkStep==="selectRegione"){
9572                  self.bkStep="selectProvincia";
9573                  self._bkAutoProvStatus={type:"ok",msg:"Sei in "+geo.matched.descrizione+" ("+(self.bkSelRegione&&self.bkSelRegione.descrizione||"")+"). Tocca per continuare."};
9574                  self._bkRender();
9575                }
9576              }
9577            },1500);
9578            return;
9579          }
9580          /* Nessuna provincia matchata, MA l'API ha un'entry "X REGIONALE"
9581           * per la regione geografica rilevata → auto-pick di quella entry.
9582           * _bkPickRegione poi espanderà alle 10 province reali (tramite
9583           * BK_IT_PROVINCES) grazie a bkExtractRegionName. */
9584          if(geo&&geo.regionalEntry){
9585            self._bkAutoProvStatus={type:"ok",msg:"Trovato: "+geo.regionalEntry.descrizione+". Carico le province..."};
9586            self._bkBusy=false;self._bkRender();
9587            self._bkPickRegione(geo.regionalEntry);
9588            return;
9589          }
9590          /* Nessun match in zona: messaggio chiaro invece di null silenzioso,
9591           * altrimenti l'utente si chiede perché "non trova nulla". */
9592          var _addr=geo&&geo.address?geo.address:{};
9593          var _loc=_addr.city||_addr.town||_addr.village||_addr.county||"";
9594          self._bkAutoProvStatus={type:"err",
9595            msg:_loc?("Sei a "+_loc+", ma questa zona non è tra le province disponibili. Scegli manualmente."):
9596                     "Posizione rilevata, ma non corrisponde a nessuna provincia disponibile. Scegli manualmente."};
9597          self._bkRender();
9598        }catch(_e){
9599          if(_bkStale())return;
9600          self._bkAutoProvStatus={type:"err",msg:"Errore nel rilevare la zona. Scegli manualmente qui sotto."};
9601          self._bkRender();
9602        }
9603      });
9604      return;
9605    }catch(e){if(!_bkStale())this._bkSetError(e);}
9606    /* Il busy-flag appartiene all'apertura più recente: una _bkStart superata
9607     * non deve azzerare il busy della nuova (ne romperebbe le guardie). */
9608    finally{if(!_bkStale()&&this._bkBusy)this._bkBusy=false;}
9609  };
9610
9611  /** Fetches ALL province across ALL regioni (parallel).
9612   *  Returns {regioni:[...], items:[{regione, provincia}, ...]}.
9613   *  Used by chat-driven flows (askLocation / intent detection) so the user
9614   *  can name any Italian province without picking the regione first. */
9615  CafChat.prototype._bkFetchAllProvince = async function(opts){
9616    var self=this;
9617    /* Cache hit: return immediately, then revalidate in background (SWR-style).
9618     * NB: su cache hit il picker si renderizza comunque subito (i dati completi
9619     * ci sono già), quindi `onRegioni` (early render) serve solo al cache MISS. */
9620    var cached=bkCacheGet("allProvince");
9621    if(cached&&cached.regioni&&cached.regioni.length){
9622      /* Debounce della revalidation: rapid open/close non deve schedulare
9623       * fetch multipli in parallelo. Una sola revalidation per sessione+2s. */
9624      if(!self._bkRevalidationScheduled){
9625        self._bkRevalidationScheduled=true;
9626        setTimeout(function(){
9627          self._bkRevalidateAllProvince().catch(function(){}).then(function(){
9628            self._bkRevalidationScheduled=false;
9629          });
9630        },2000);
9631      }
9632      return cached;
9633    }
9634    var fresh=await self._bkRevalidateAllProvince(opts);
9635    return fresh;
9636  };
9637  CafChat.prototype._bkRevalidateAllProvince = async function(opts){
9638    var self=this;
9639    var regs=await self._bkFetch("regioni");
9640    if(!regs||!regs.length)return {regioni:[],items:[]};
9641    /* ZONA preselect (deploy multi-tenant): se window.__CAF_ZONA__ punta a una
9642     * regione specifica, il backend (es. connessione "callcenter") può comunque
9643     * esporre TUTTE le regioni. Caricare le province di OGNUNA significa, per
9644     * ogni regione, una getProvince che a sua volta sonda i servizi di OGNI sua
9645     * provincia: decine→centinaia di chiamate Planner per regioni che l'utente
9646     * non vedrà mai (la ZONA è fissa). Quando la ZONA fa match con una regione,
9647     * un deploy preimpostato È un tenant mono-regione: restringiamo a QUELLA
9648     * sola regione SIA il fan-out province (cold-open 1+N → 1+1 getProvince) SIA
9649     * la lista `regioni` ritornata — così TUTTI i consumatori (picker, resume,
9650     * "vicino a me"/askLocation, messaggi) restano coerenti e non offrono mai
9651     * regioni le cui province non sono state caricate. Vuoto / "nazionale" /
9652     * nessun match = comportamento full (national + fallback misconfig sicuro).
9653     * Il match per descrizione è identico a quello in _bkStart, così le due
9654     * logiche ZONA restano sempre allineate.
9655     *
9656     * RITORNA: { regioni: regione ZONA preselezionata se match, altrimenti
9657     *           TUTTE; items: province della/e stessa/e regione/i }. */
9658    var zonaPre=(typeof window!=="undefined"&&window.__CAF_ZONA__||"").toString().trim().toLowerCase();
9659    var scopedRegs=regs;
9660    if(zonaPre&&zonaPre!=="nazionale"){
9661      var onlyZona=regs.filter(function(r){return ((r&&r.descrizione)||"").trim().toLowerCase()===zonaPre;});
9662      if(onlyZona.length)scopedRegs=onlyZona;
9663    }
9664    /* Early render: notifica le regioni appena pronte, PRIMA del fan-out delle
9665     * province (la chiamata getProvince più lenta bloccava il picker ~8s — e in
9666     * incognito/cookie bloccati la cache non persiste mai, quindi OGNI apertura
9667     * pagava il fan-out). _bkStart usa questo callback per mostrare subito il
9668     * picker regioni; le province continuano a caricare qui sotto in background. */
9669    if(opts&&typeof opts.onRegioni==="function"){try{opts.onRegioni(scopedRegs);}catch(_e){}}
9670    var lists=await Promise.all(scopedRegs.map(function(r){
9671      return self._bkFetch("province",{connessione:r.connessione,codiceRegione:r.codiceRegione})
9672        .then(function(rawP){
9673          var p=rawP||[];
9674          /* FALLBACK: se la lista API è placeholder (es. "Toscana Regionale"),
9675           * espandi alle province italiane reali per quella regi
9675one. */
9676          if(bkIsPlaceholderProvinceList(p,r.descrizione)){
9677            var fb=bkExpandProvinceFallback(r.descrizione);
9678            if(fb.length>0)p=fb;
9679          }
9680          return p.map(function(prov){return {regione:r,provincia:prov};});
9681        })
9682        .catch(function(){return [];});
9683    }));
9684    var flat=[];lists.forEach(function(l){l.forEach(function(x){flat.push(x);});});
9685    var out={regioni:scopedRegs,items:flat};
9686    /* Cache solo se il risultato è non vuoto, per non congelare un guasto API. */
9687    if(out.regioni.length>0)bkCacheSet("allProvince",out);
9688    return out;
9689  };
9690
9691  /** Given a matched provincia, set up state (regione+provincia+filtered list)
9692   *  from the result of _bkFetchAllProvince. */
9693  CafChat.prototype._bkApplyMatchedProvincia = function(allData,matched){
9694    var hit=null;
9695    for(var i=0;i<allData.items.length;i++){
9696      if(allData.items[i].provincia.codice===matched.codice){hit=allData.items[i];break;}
9697    }
9698    if(!hit)return false;
9699    this.bkRegioni=allData.regioni;
9700    this.bkSelRegione=hit.regione;
9701    this.bkProvince=allData.items
9702      .filter(function(x){return x.regione.connessione===hit.regione.connessione;})
9703      .map(function(x){return x.provincia;});
9704    this.bkSelProv=matched;
9705    return true;
9706  };
9707
9708  CafChat.prototype._bkPickRegione = async function(r){
9709    if(this._bkBusy)return;this._bkBusy=true;var _g=++this._bkOpGen;this.bkSelRegione=r;this._bkLoadingReturnStep="selectRegione";this.bkStep="loading";this._bkRender();bkHaptic(8);
9710    this._bkLogEvent&&this._bkLogEvent("regione_picked");
9711    try{
9712      /* Prefetch hit: usa cache se popolata (evita roundtrip) */
9713      var cached=this._bkProvinciaByConn&&this._bkProvinciaByConn[r.connessione];
9714      var p=(cached&&cached.length)?cached:await this._bkFetch("province",{connessione:r.connessione,codiceRegione:r.codiceRegione},{timeoutMs:15000});
9715      if(this._bkOpGen!==_g)return;
9716      if(!p||!p.length){this._bkSetError({error:"Nessuna provincia disponibile per "+r.descrizione+". Scegli un'altra regione.",code:"no_services"},"selectRegione");return;}
9717      /* FILTRO GEOGRAFICO: se la regione selezionata corrisponde ad una
9718       * regione italiana riconosciuta (es. "Emilia Romagna"), filtra la
9719       * lista per tenere solo le province appartenenti a quella regione.
9720       * Risolve il caso in cui l'API ritorna una lista miscellanea
9721       * cross-regione anche per regioni standard. */
9722      var detectedRegion=bkExtractRegionName(r.descrizione);
9723      if(detectedRegion){
9724        var beforeN=p.length;
9725        p=bkFilterProvincesByRegion(p,detectedRegion);
9726        if(p.length<beforeN){
9727          this._bkLogEvent&&this._bkLogEvent("province_geo_filtered",{regione:r.descrizione,detected:detectedRegion,before:beforeN,after:p.length});
9728        }
9729      }
9730      this.bkProvince=p;
9731      /* AUTO-PICK PLACEHOLDER MATCH:
9732       * Se la regione selezionata è del tipo "X REGIONALE" e fra le
9733       * province torna un'entry con la STESSA descrizione (es. anche
9734       * "TOSCANA REGIONALE"), quella è l'unica realmente utilizzabile dal
9735       * backend per questo tenant — anche se l'API ritorna ulteriori
9736       * province cross-regione. Auto-pickiamo il match e saltiamo
9737       * direttamente al passo Servizio. */
9738      var matchingPlaceholder=null;
9739      var rDesc=(r&&r.descrizione||"").trim().toLowerCase();
9740      if(rDesc){
9741        for(var mi=0;mi<p.length;mi++){
9742          var pd=((p[mi]&&p[mi].descrizione)||"").trim().toLowerCase();
9743          if(pd&&pd===rDesc){matchingPlaceholder=p[mi];break;}
9744        }
9745      }
9746      if(matchingPlaceholder){
9747        this._bkLogEvent&&this._bkLogEvent("provincia_auto_picked_placeholder",{regione:r.descrizione});
9748        this._bkBusy=false;this._bkPickProv(matchingPlaceholder);return;
9749      }
9750      /* DEFENSIVE FALLBACK (raro): se l'API torna SOLO placeholder
9751       * (es. solo "Toscana Regionale" senza match esatto col nome regione),
9752       * espande con la lista hardcoded delle province reali. */
9753      if(bkIsPlaceholderProvinceList(p,r.descrizione)){
9754        var fb=bkExpandProvinceFallback(r.descrizione);
9755        if(fb.length>0){
9756          p=fb;this.bkProvince=p;
9757          this._bkLogEvent&&this._bkLogEvent("province_fallback_used",{regione:r.descrizione,trigger:"api_placeholder",n:fb.length});
9758        }
9759      }
9760      if(p.length===1){this._bkBusy=false;this._bkPickProv(p[0]);return;}
9761      this.bkStep="selectProvincia";this._bkRender();
9762    }catch(e){this._bkSetError(e,"selectRegione");}finally{if(this._bkOpGen===_g)this._bkBusy=false;}
9763  };
9764
9765  CafChat.prototype._bkPickProv = async function(p){
9766    if(this._bkBusy)return;this._bkBusy=true;var _g=++this._bkOpGen;this.bkSelProv=p;this._bkLoadingReturnStep="selectProvincia";this.bkStep="loading";
9767    this._bkAutoProvStatus={type:"loading",msg:"Cerco i servizi per "+p.descrizione+"..."};
9768    this._bkRender();bkHaptic(8);
9769    this._bkLogEvent&&this._bkLogEvent("provincia_picked");
9770    var conn=this.bkSelRegione&&this.bkSelRegione.connessione;
9771    var srvCacheKey="servizi:"+conn+":"+p.codice;
9772    var cachedSrv=bkCacheGet(srvCacheKey);
9773    /* `[]` è truthy in JS — il check `cachedSrv||fetch` userebbe array vuoto
9774     * intrappolando l'utente in "nessun servizio" anche se l'API è guarita.
9775     * Trattiamo come miss anche cache vuota o non-array. */
9776    var hasValidCache=Array.isArray(cachedSrv)&&cachedSrv.length>0;
9777    try{var all=hasValidCache?cachedSrv:await this._bkFetch("servizi",{codiceProvincia:p.codice,connessione:conn},{timeoutMs:15000});
9778      if(this._bkOpGen!==_g)return;
9779      /* Cache solo risultati non vuoti per evitare di "congelare" un guasto temporaneo. */
9780      if(!hasValidCache&&Array.isArray(all)&&all.length>0)bkCacheSet(srvCacheKey,all);
9781      /* FILTRO PROCEDURA ONLINE — richiediamo solo il flag `online` truthy
9782       * (accetta "1", 1, true, "true", "SI", "Y"). La precedente richiesta
9783       * di `desonline` non vuoto nascondeva tutti i servizi senza
9784       * descrizione online configurata anche se prenotabili: ora la
9785       * descrizione fa fallback a `s.descrizione` lato render. Se `online`
9786       * è completamente assente (undefined/null), trattiamo il servizio
9787       * come prenotabile (default visibile) per non perdere voci dovute
9788       * a payload incompleti dal Planner. */
9789      function _bkIsOnlineSrv(s){
9790        if(!s)return false;
9791        var ol=s.online;
9792        if(ol==null||ol==="")return true; /* default: mostra */
9793        if(ol===0||ol==="0"||ol===false||ol==="false")return false;
9794        if(ol===1||ol==="1"||ol===true||ol==="true")return true;
9795        if(typeof ol==="string"){
9796          var t=ol.trim().toLowerCase();
9797          if(/^(si|sì|s|y|yes|true|on|attivo|abilitato)$/.test(t))return true;
9798          if(/^(no|n|false|off|disattivo|disabilitato)$/.test(t))return false;
9799        }
9800        return true; /* in dubbio: mostra (l'utente vedrà eventuali errori a step successivi) */
9801      }
9802      var srv=all.filter(_bkIsOnlineSrv);
9803      if(!srv.length){this._bkSetError({error:"Nessun servizio prenotabile online per "+p.descrizione+".",code:"no_services"},"selectProvincia");return;}
9804      this.bkServizi=srv;
9805      /* Smart: if user mentioned a specific service, try to auto-match */
9806      if(this.bkHint){
9807        var hint=this.bkHint;
9808        var scored=srv.map(function(s){return{s:s,score:matchService(s,hint)};}).filter(function(x){return x.score>0;});
9809        /* prefer shorter names on tie (usually the "base" variant, e.g. "730" over "730 integrativo") */
9810        scored.sort(function(a,b){
9811          if(b.score!==a.score)return b.score-a.score;
9812          var nA=((a.s.desonline||a.s.descrizione)||"").length;
9813          var nB=((b.s.desonline||b.s.descrizione)||"").length;
9814          return nA-nB;
9815        });
9816        /* AGGRESSIVE auto-pick: if top score >= 80, select it directly (user explicitly named the service) */
9817        if(scored.length>=1&&scored[0].score>=80){
9818          this._bkBusy=false;
9819          this._bkPickServ(scored[0].s);
9820          return;
9821        }
9822        /* weaker matches: filter the list to show only those with hint */
9823        if(scored.length>0&&scored.length<srv.length){
9824          this.bkServizi=scored.map(function(x){return x.s;});
9825        }
9826      }
9827      this.bkStep="selectServizio";this._bkRender();
9828    }catch(e){this._bkSetError(e,"selectProvincia");}finally{if(this._bkOpGen===_g)this._bkBusy=false;}
9829  };
9830
9831  CafChat.prototype._bkPickServ = async function(s){
9832    if(this._bkBusy)return;this._bkBusy=true;var _g=++this._bkOpGen;var p=this.bkSelProv;if(!p){this._bkBusy=false;return;}
9833    this.bkSelServ=s;this._bkLoadingReturnStep="selectServizio";this.bkStep="loading";
9834    this._bkAutoProvStatus={type:"loading",msg:"Cerco le sedi per "+(s.desonline||s.descrizione||"il servizio")+"..."};
9835    this._bkRender();bkHaptic(8);
9836    this._bkLogEvent&&this._bkLogEvent("servizio_picked");
9837    var conn=this.bkSelRegione&&this.bkSelRegione.connessione;
9838    /* Cache sedi per conn+provincia+servizio (mirror della cache servizi): una
9839     * riselezione dello stesso servizio è istantanea invece di ripagare il
9840     * round-trip (+ retry ×3) ad ogni click. Cache solo risultati non vuoti. */
9841    var sediCacheKey="sedi:"+conn+":"+p.codice+":"+s.codice;
9842    var cachedSedi=bkCacheGet(sediCacheKey);
9843    var hasSediCache=Array.isArray(cachedSedi)&&cachedSedi.length>0;
9844    try{var sedi=hasSediCache?cachedSedi:await this._bkFetch("sedi",{codiceProvincia:p.codice,codiceServizio:s.codice,connessione:conn},{timeoutMs:15000});
9845      if(this._bkOpGen!==_g)return;
9846      if(!sedi||!sedi.length){this._bkSetError({error:"Nessuna sede disponibile.",code:"no_sedi"},"selectServizio");return;}
9847      if(!hasSediCache)bkCacheSet(sediCacheKey,sedi);
9848      this.bkSedi=sedi;this.bkStep="selectSede";this._bkRender();
9849      /* Prefetch slot per le top 3 sedi: quando l'utente clicca, lo slot panel
9850       * appare istantaneamente (cache hit) invece di aspettare il fetch. */
9851      try{this._bkPrefetchTopSlots(3);}catch(_e){}
9852      /* #7 — lazy-load geocoding: difendi il main thread schedulando il batch
9853       * geocode quando il browser è idle (fallback setTimeout). Non blocca il render. */
9854      if(this._bkUserCoords){
9855        var _self2=this;var _runGeo=function(){_self2._bkSortSediByDistanceInBg();};
9856        if(typeof requestIdleCallback==="function")requestIdleCallback(_runGeo,{timeout:1500});
9857        else setTimeout(_runGeo,200);
9858      }
9859    }catch(e){this._bkSetError(e,"selectServizio");}finally{if(this._bkOpGen===_g)this._bkBusy=false;}
9860  };
9861
9862  /** Geocoda ogni sede in sequenza (per rispettare Nominatim ToS),
9863   *  calcola distanza haversine, ordina e ri-renderizza.
9864   *  Non blocca il render iniziale e gestisce silenziosamente gli errori. */
9865  CafChat.prototype._bkSortSediByDistanceInBg = async function(){
9866    if(!this._bkUserCoords||!this.bkSedi||!this.bkSedi.length)return;
9867    var self=this;var coords=this._bkUserCoords;var snap=this.bkSedi;
9868    /* Geocodiamo TUTTE le sedi (cap a 200 per coprire ogni regione IT).
9869     * Le sedi con coords backend (s.lat/lon) vanno SEMPRE prima e sono istantanee. */
9870    var sortedSnap=snap.slice(0,200).slice().sort(function(a,b){
9871      var ac=(a._lat!==undefined&&a._lon!==undefined)||(typeof a.lat==="number"&&typeof a.lon==="number")?0:1;
9872      var bc=(b._lat!==undefined&&b._lon!==undefined)||(typeof b.lat==="number"&&typeof b.lon==="number")?0:1;
9873      return ac-bc;
9874    });
9875    var maxToGeocode=sortedSnap.length;
9876    for(var i=0;i<maxToGeocode;i++){
9877      if(self.bkSedi!==snap||self.bkStep!=="selectSede")return; /* utente è andato avanti */
9878      var sede=sortedSnap[i];
9879      /* Salva lo stato PRIMA della call: se aveva già coords, _bkGeocodeSede
9880       * non hitta Nominatim → niente rate-limit necessario. */
9881      var hadCoords=(sede._lat!==undefined&&sede._lon!==undefined)
9882                   ||(typeof sede.lat==="number"&&typeof sede.lon==="number");
9883      try{
9884        var loc=await self._bkGeocodeSede(sede);
9885        if(loc&&typeof loc.lat==="number"&&typeof loc.lon==="number"){
9886          /* Riusa _distKm (nome usato anche da _bkFindNearestSede) per coerenza con render */
9887          sede._distKm=self._bkHaversineKm(coords.lat,coords.lon,loc.lat,loc.lon);
9888        }
9889      }catch(_e){}
9890      /* Rate-limit Nominatim ~1 req/sec. Skippato se backend ha già coords. */
9891      if(!hadCoords)await new Promise(function(r){setTimeout(r,150);});
9892    }
9893    if(self.bkSedi!==snap||self.bkStep!=="selectSede")return;
9894    /* #20 — NON ri-ordiniamo le tile sotto il dito (causava mis-tap: l'ordine
9895     * cambiava mentre l'utente stava per cliccare). Questo è il sort PASSIVO di
9896     * background (post-scelta servizio): calcoliamo solo la distanza minima per
9897     * marcare col badge la sede più vicina e mostrare le distanze in loco,
9898     * LASCIANDO INVARIATE le posizioni. Il sort esplicito resta in
9899     * _bkFindNearestSede, dove l'utente clicca "Trova la sede più vicina" e si
9900     * ASPETTA il riordino. */
9901    var minKm=Infinity,nearestCode=null;
9902    for(var si=0;si<self.bkSedi.length;si++){
9903      var dk=self.bkSedi[si]._distKm;
9904      if(typeof dk==="number"&&dk<minKm){minKm=dk;nearestCode=self.bkSedi[si].codice;}
9905    }
9906    if(nearestCode)self._bkNearestCode=nearestCode;
9907    if(self._bkFsMode)self._bkRenderFs();else self._bkRender();
9908  };
9909
9910  CafChat.prototype._bkPickSede = async function(sede,opts){
9911    if(this._bkBusy)return;this._bkBusy=true;var _g=++this._bkOpGen;var p=this.bkSelProv,s=this.bkSelServ;
9912    if(!p||!s){this._bkBusy=false;return;}this.bkSelSede=sede;this._bkLoadingReturnStep="selectSede";this.bkStep="loading";
9913    this._bkAutoProvStatus={type:"loading",msg:"Cerco gli orari disponibili presso "+sede.descrizione+"..."};
9914    this._bkRender();bkHaptic(8);
9915    /* opts.internalRefetch: riuso interno (es. refresh slot dopo slot_taken) —
9916     * l'utente NON ha ri-scelto la sede, niente doppio evento funnel. */
9917    if(!(opts&&opts.internalRefetch))this._bkLogEvent&&this._bkLogEvent("sede_picked");
9918    var conn=this.bkSelRegione&&this.bkSelRegione.connessione;
9919    /* Usa eventuale prefetch già in volo (avviato post _bkPickServ).
9920     * Consumalo (delete): un secondo click sulla stessa sede dopo go-back
9921     * deve re-fetchare slot freschi (gli orari potrebbero essere stati presi). */
9922    var pf=this._bkSlotPrefetch&&this._bkSlotPrefetch[sede.codice];
9923    if(pf&&this._bkSlotPrefetch)delete this._bkSlotPrefetch[sede.codice];
9924    var fetchP=pf||this._bkFetch("spazi",{codProv:p.codice,codSede:sede.codice,codServizio:s.codice,connessione:conn},{timeoutMs:15000});
9925    try{var r=await fetchP;
9926      /* Annullato (o superato da una nuova selezione) mentre attendevamo un
9927       * prefetch in background? Non saltare in avanti. */
9928      if(this._bkOpGen!==_g)return;
9929      /* Il prefetch slot è best-effort (budget ridotto) e la sua .catch risolve
9930       * a NULL se è fallito/scaduto. NON fidarci ciecamente del risultato del
9931       * prefetch: se è null facciamo un fetch FRESCO a 15s. Così una sede con
9932       * endpoint lento (8-15s) viene comunque servita — evita sia il TypeError
9933       * su null sia una regressione di affidabilità rispetto a prima. */
9934      if(r==null){
9935        r=await this._bkFetch("spazi",{codProv:p.codice,codSede:sede.codice,codServizio:s.codice,connessione:conn},{timeoutMs:15000});
9936        if(this._bkOpGen!==_g)return;
9937      }
9938      var sp=Array.isArray(r)?r:(r.spazi||[]);var g=(typeof r==="object"&&!Array.isArray(r))?(r.giorno||""):"";
9939      if(!sp.length){this._bkSetError({error:"Nessun appuntamento disponibile presso "+sede.descrizione+" per "+s.descrizione+". Prova un'altra sede o riprova più tardi.",code:"no_spazi"},"selectSede");return;}
9940      this.bkSpazi=sp;this.bkNextG=g;
9941      /* PIEMONTE: drena TUTTA la disponibilità PRIMA di mostrare il calendario
9942       * (resta visibile lo spinner "Cerco gli orari…"), così lo step DATA e ORA
9943       * appare con TUTTI i mesi disponibili già caricati e navigabili e SENZA il
9944       * bottone "Carica altri orari". Awaited dentro il flusso di _bkPickSede:
9945       * _bkBusy/_bkOpGen gestiscono la concorrenza; _g abortisce se l'utente
9946       * cambia selezione. Copre anche il resume (passa da _bkPickSede). */
9947      if(this.opts&&this.opts.piemonte&&this.bkNextG){
9948        await this._bkDrainAllSpazi(_g,p,s,sede,conn);
9949        if(this._bkOpGen!==_g)return;
9950      }
9951      this.bkStep="selectSlot";this._bkRender();
9952    }catch(e){this._bkSetError(e,"selectSede");}finally{if(this._bkOpGen===_g)this._bkBusy=false;}
9953  };
9954
9955  /** Prefetch slot per le top-N sedi (chiamato dopo _bkPickServ) — non blocca. */
9956  CafChat.prototype._bkPrefetchTopSlots = function(maxN){
9957    var p=this.bkSelProv,s=this.bkSelServ,conn=this.bkSelRegione&&this.bkSelRegione.connessione;
9958    if(!p||!s||!this.bkSedi||!this.bkSedi.length)return;
9959    var self=this;this._bkSlotPrefetch=this._bkSlotPrefetch||{};
9960    var n=Math.min(maxN||3,this.bkSedi.length);
9961    for(var i=0;i<n;i++){
9962      var sede=this.bkSedi[i];if(!sede||self._bkSlotPrefetch[sede.codice])continue;
9963      (function(sd){
9964        self._bkSlotPrefetch[sd.codice]=self._bkFetch("spazi",{codProv:p.codice,codSede:sd.codice,codServizio:s.codice,connessione:conn},{background:true,timeoutMs:8000})
9965          .catch(function(){delete self._bkSlotPrefetch[sd.codice];return null;});
9966      })(sede);
9967    }
9968  };
9969
9970  CafChat.prototype._bkLoadMore = async function(){
9971    var self=this,p=self.bkSelProv,s=self.bkSelServ,sede=self.bkSelSede;
9972    if(self._bkBusy||self._bkLoadingMore||!p||!s||!sede||!self.bkNextG)return;
9973    self._bkBusy=true;
9974    /* Feedback immediato sul bottone "Cerca altri orari": flag letto dal render
9975     * (spinner + disabled) così il click non sembra inerte durante il fetch. */
9976    self._bkLoadingMore=true;self._bkRender();
9977    var conn=self.bkSelRegione&&self.bkSelRegione.connessione;
9978    try{var r=await self._bkFetch("spazi",{codProv:p.codice,codSede:sede.codice,codServizio:s.codice,giornoInizio:self.bkNextG,connessione:conn},{timeoutMs:15000});
9979      var ns=Array.isArray(r)?r:(r.spazi||[]);var g=(typeof r==="object"&&!Array.isArray(r))?(r.giorno||""):"";
9980      if(!ns.length){self.bkNextG="";self._bkLoadingMore=false;self._bkRender();return;}
9981      self.bkSpazi=self.bkSpazi.concat(ns);self.bkNextG=g;self._bkLoadingMore=false;self._bkRender();
9982    }catch(e){self.bkNextG="";self._bkLoadingMore=false;self._bkRender();}finally{self._bkBusy=false;self._bkLoadingMore=false;}
9983  };
9984
9985  /** PIEMONTE only — drena TUTTA la paginazione degli spazi (getspazi è
9986   *  cursor-based: bkNextG = prossimo giorno da chiedere; finisce quando il
9987   *  Planner ritorna giorno/spazi vuoti). Accumula in bkSpazi finché il cursore
9988   *  non si esaurisce. È chiamata e ATTESA dentro _bkPickSede mentre è ancora
9989   *  visibile lo spinner "Cerco gli orari…", quindi quando appare lo step DATA e
9990   *  ORA TUTTI i mesi disponibili sono già caricati e navigabili (niente bottone
9991   *  "Carica altri orari"). È ciò che l'utente farebbe cliccando il bottone fino
9992   *  in fondo, automatizzato e front-loaded.
9993   *
9994   *  Concorrenza: nessuno stato proprio; gira dentro il flusso awaited di
9995   *  _bkPickSede (che tiene _bkBusy e possiede la generazione _g). Il controllo
9996   *  `_bkOpGen!==genAtStart` DOPO ogni await + la guardia `bkSelSede===sede`
9997   *  fermano il drain senza scrivere se l'utente cambia selezione. Su errore di
9998   *  rete interrompe lasciando bkNextG valorizzato → resta il fallback manuale. */
9999  CafChat.prototype._bkDrainAllSpazi = async function(genAtStart,p,s,sede,conn){
10000    var self=this;var guard=0,MAX=60; /* backstop anti-loop: orizzonte Planner limitato (~4 mesi, ~20-25 req) */
10001    while(self.bkNextG&&self._bkOpGen===genAtStart&&self.bkSelSede===sede&&guard++<MAX){
10002      var r;
10003      try{r=await self._bkFetch("spazi",{codProv:p.codice,codSede:sede.codice,codServizio:s.codice,giornoInizio:self.bkNextG,connessione:conn},{timeoutMs:15000});}
10004      catch(e){break;} /* errore di rete: tieni ciò che si ha + fallback manuale */
10005      if(self._bkOpGen!==genAtStart)return; /* superato da una nuova selezione: non scrivere */
10006      var ns=Array.isArray(r)?r:(r.spazi||[]);
10007      var g=(typeof r==="object"&&!Array.isArray(r))?(r.giorno||""):"";
10008      if(!ns.length){self.bkNextG="";break;}
10009      self.bkSpazi=self.bkSpazi.concat(ns);self.bkNextG=g;
10010    }
10011  };
10012
10013  CafChat.prototype._bkPickSlot = function(sp){
10014    /* Nuovo slot = nuova intenzione di prenotazione → chiave idempotenza fresca
10015     * (la dedup vale solo per i retry dello STESSO slot). */
10016    this._bkSubmitReqId=null;
10017    this.bkSelSlot=sp;this.bkStep="collectData";
10018    /* Avvia il promemoria ORIENTATIVO "completa entro" (UI-only, 5 min:
10019     * nessun hold reale lato server, quindi nessun claim di slot bloccato). */
10020    this._bkSlotLockedAt=Date.now();this._bkSlotLockMs=5*60*1000;
10021    this._bkRender();
10022    this._bkLogEvent&&this._bkLogEvent("slot_picked");
10023  };
10024
10025  /** Crea/aggiorna il banner countdown ORIENTATIVO "Ti consigliamo di
10026   *  completare entro X:YY" (a11y #C57: il vecchio "Slot bloccato"/"Tempo
10027   *  scaduto" era un claim falso — nessun hold server-side). Idempotente:
10028   *  pulisce eventuali interval precedenti tramite this._bkSlotTimerId. */
10029  CafChat.prototype._bkRenderSlotTimer = function(container){
10030    if(!this._bkSlotLockedAt||!this._bkSlotLockMs)return;
10031    var self=this;
10032    if(this._bkSlotTimerId){clearInterval(this._bkSlotTimerId);this._bkSlotTimerId=null;}
10033    var banner=document.createElement("div");
10034    banner.style.cssText="display:flex;align-items:center;gap:8px;background:linear-gradient(90deg,#fef3c7,#fde68a);border:1px solid #f59e0b;border-radius:10px;padding:8px 12px;margin-bottom:10px;font-size:12px;color:#92400e;font-weight:600";
10035    var ic=document.createElement("span");ic.style.cssText="display:inline-flex";ic.innerHTML='<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>';
10036    banner.appendChild(ic);
10037    var txt=document.createElement("span");txt.style.cssText="flex:1";banner.appendChild(txt);
10038    container.appendChild(banner);
10039    function tick(){
10040      var remaining=self._bkSlotLockMs-(Date.now()-self._bkSlotLockedAt);
10041      if(remaining<=0){
10042        txt.textContent=bkT("hold_expired");
10043        banner.style.background="#fee2e2";banner.style.borderColor="#ef4444";banner.style.color="#7f1d1d";
10044        if(self._bkSlotTimerId){clearInterval(self._bkSlotTimerId);self._bkSlotTimerId=null;}
10045        self._bkLogEvent&&self._bkLogEvent("slot_timer_expired");
10046        return;
10047      }
10048      var mm=Math.floor(remaining/60000);var ss=Math.floor((remaining%60000)/1000);
10049      txt.textContent=bkT("hold_hint",mm+":"+(ss<10?"0"+ss:ss));
10050    }
10051    tick();this._bkSlotTimerId=setInterval(tick,1000);
10052  };
10053
10054  CafChat.prototype._bkConfirm = function(){
10055    var d=this.bkClient;
10056    /* trim to avoid leading/trailing spaces being accepted silently */
10057    d.nome=(d.nome||"").trim();d.cognome=(d.cognome||"").trim();
10058    d.codfis=(d.codfis||"").trim().toUpperCase();
10059    d.email=(d.email||"").trim();d.cellulare=(d.cellulare||"").trim();
10060    if(!d.nome||!d.cognome||!d.codfis||!d.email||!d.cellulare){this.bkFormErr=bkT("err_required_fields");this._bkRender();return;}
10061    var cfChk=validateCodiceFiscale(d.codfis);
10062    if(!cfChk.ok){this.bkFormErr=cfChk.reason;this._bkRender();return;}
10063    if(!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(d.email)){this.bkFormErr=bkT("err_email_invalid");this._bkRender();return;}
10064    var digits=d.cellulare.replace(/\D/g,"");
10065    if(digits.length<9||digits.length>15){this.bkFormErr=bkT("err_phone_invalid");this._bkRender();return;}
10066    this.bkFormErr=null;this.bkStep="confirm";this._bkRender();
10067    /* Funnel event: dati raccolti correttamente */
10068    this._bkLogEvent&&this._bkLogEvent("data_collected");
10069  };
10070
10071  /* ── Privacy consent gate (checkbox obbligatoria a fine prenotazione) ──
10072   * Costruisce un blocco checkbox "molto chiaro" con link all'informativa,
10073   * condiviso fra la confirm compatta (_bkRender) e quella fullscreen
10074   * (_bkRenderFsImpl). Legge/scrive this.bkPrivacyOk e notifica il chiamante
10075   * via onToggle(ok) così che possa abilitare/disabilitare il bottone Conferma.
10076   * Lo stato è azzerato a ogni nuova prenotazione in _bkReset → consenso fresco. */
10077  CafChat.prototype._bkBuildConsentBox = function(onToggle){
10078    var self=this,c=this.opts.color;
10079    var privacyUrl=(this.opts&&this.opts.privacyUrl)||"";
10080    try{var sct=document.querySelector("script[data-privacy-url]");
10080if(sct&&!privacyUrl)privacyUrl=sct.getAttribute("data-privacy-url")||"";}catch(_e){}
10081    /* Niente fallback hardcoded cross-titolare: senza PRIVACY_URL il link
10082     * apre il pannello privacy in-widget (GDPR artt. 7, 13); hardening:
10083     * solo http(s). */
10084    if(privacyUrl&&!/^https?:\/\//i.test(privacyUrl))privacyUrl="";
10085    var on=!!this.bkPrivacyOk;
10086    var box=document.createElement("div");
10087    box.className="caf-bk-consent";box.setAttribute("data-caf-consent","1");
10088    box.style.cssText="display:flex;align-items:flex-start;gap:10px;margin:14px auto 6px;max-width:680px;padding:12px 14px;background:"+(on?(c+"0f"):"#f8fafc")+";border:1.5px solid "+(on?c:"#e2e8f0")+";border-radius:12px;font-size:12.5px;line-height:1.5;color:#475569;transition:border-color .2s,background .2s";
10089    var cb=document.createElement("input");cb.type="checkbox";cb.className="caf-bk-consent-cb";cb.checked=on;cb.setAttribute("aria-required","true");cb.setAttribute("aria-label",bkT("consent_before")+bkT("consent_link")+bkT("consent_after"));
10090    cb.style.cssText="width:19px;height:19px;min-width:19px;margin-top:1px;accent-color:"+c+";cursor:pointer";
10091    var txt=document.createElement("div");txt.style.cursor="pointer";
10092    txt.innerHTML=escapeHtml(bkT("consent_before"))
10093      +(privacyUrl
10094        ? '<a href="'+escapeHtml(privacyUrl.replace(/"/g,"%22"))+'" target="_blank" rel="noopener noreferrer" style="color:var(--caf-c-ink);text-decoration:underline;font-weight:600">'+escapeHtml(bkT("consent_link"))+'</a>'
10095        : '<a href="#" class="caf-bk-consent-open-panel" role="button" style="color:var(--caf-c-ink);text-decoration:underline;font-weight:600;cursor:pointer">'+escapeHtml(bkT("consent_link"))+'</a>')
10096      +escapeHtml(bkT("consent_after"))
10097      +' <span aria-hidden="true" style="color:#be123c;font-weight:700">*</span>';
10098    function fire(){
10099      self.bkPrivacyOk=!!cb.checked;
10100      box.style.borderColor=cb.checked?c:"#e2e8f0";
10101      box.style.background=cb.checked?(c+"0f"):"#f8fafc";
10102      self._bkLogEvent&&self._bkLogEvent("privacy_consent",{checked:!!cb.checked});
10103      if(typeof onToggle==="function")onToggle(!!cb.checked);
10104    }
10105    cb.addEventListener("change",fire);
10106    txt.addEventListener("click",function(e){if(e.target&&e.target.tagName==="A")return;cb.checked=!cb.checked;fire();});
10107    var _opl=txt.querySelector(".caf-bk-consent-open-panel");
10108    if(_opl){_opl.addEventListener("click",function(e){e.preventDefault();e.stopPropagation();try{self._bkShowPrivacyPanel();}catch(_e){}});}
10109    box.appendChild(cb);box.appendChild(txt);
10110    return box;
10111  };
10112
10113  CafChat.prototype._bkSubmit = async function(){
10114    if(this._bkBusy)return;
10115    /* Safety net: nessuna prenotazione senza consenso esplicito al trattamento
10116     * dati. La UI già disabilita il bottone, ma questo blocca anche un eventuale
10117     * trigger programmatico/forzato. */
10118    if(!this.bkPrivacyOk){this._bkShowToast&&this._bkShowToast({type:"error",msg:bkT("consent_required")});return;}
10119    this._bkBusy=true;var sp=this.bkSelSlot;if(!sp){this._bkBusy=false;return;}
10120    /* clientRequestId STABILE per questo tentativo di conferma, riusato se
10121     * l'utente riprova manualmente lo STESSO slot. ATTENZIONE: è inviato nel
10122     * body MA il server NON lo deduplica ancora (idempotenza server-side da
10123     * implementare). Oggi la protezione anti-doppione reale è: (a) niente
10124     * auto-retry su prenota (noRetry), (b) il gestionale che risponde
10125     * "già prenotato" sullo stesso slot. Azzerato a conferma riuscita / nuovo flusso. */
10126    if(!this._bkSubmitReqId)this._bkSubmitReqId=bkUuid();
10127    this.bkStep="submitting";this._bkRender();
10128    try{var d=this.bkClient;
10129      /* Client payload: 5 fields the chatbot collects. The Next.js route
10130         (saveBooking) appends update:"1" before forwarding to the Planner so
10131         that an existing codfis gets nome/cognome/email/cellulare refreshed
10132         (doc Rest Service rev 1.13).
10133         Additional fields (provincia/servizio/sede/slot/...) are NOT sent to
10134         the Planner — they are passed through to the server-side audit logger. */
10135      var pageUrl="";try{pageUrl=(typeof location!=="undefined"&&location.href)||"";}catch(_e){}
10136      var ua="";try{ua=(typeof navigator!=="undefined"&&navigator.userAgent)||"";}catch(_e){}
10137      var sid="";
10137try{sid=(this._sessionId||(this.opts&&this.opts.sessionId)||"");}catch(_e){}
10138      var r=await this._bkFetch("prenota",{
10139        idSpazio:sp.id_spazio,
10140        connessione:this.bkSelRegione&&this.bkSelRegione.connessione,
10141        clientData:{
10142          codfis:(d.codfis||"").toUpperCase(),
10143          nome:d.nome,
10144          cognome:d.cognome,
10145          email:d.email,
10146          cellulare:d.cellulare
10147        },
10148        source:"widget-inline",
10149        regione:this.bkSelRegione?{
10150          connessione:this.bkSelRegione.connessione,
10151          codiceRegione:this.bkSelRegione.codiceRegione,
10152          descrizione:this.bkSelRegione.descrizione
10153        }:null,
10154        provincia:this.bkSelProv?{codice:this.bkSelProv.codice,descrizione:this.bkSelProv.descrizione}:null,
10155        servizio:this.bkSelServ?{
10156          codice:this.bkSelServ.codice,descrizione:this.bkSelServ.descrizione,
10157          desonline:this.bkSelServ.desonline,tempo:this.bkSelServ.tempo,
10158          documenti:this.bkSelServ.documenti,online:this.bkSelServ.online
10159        }:null,
10160        sede:this.bkSelSede?{
10161          codice:this.bkSelSede.codice,descrizione:this.bkSelSede.descrizione,
10162          indirizzo:this.bkSelSede.indirizzo,cap:this.bkSelSede.cap,prov:this.bkSelSede.prov,
10163          citta:this.bkSelSede.citta,telefono:this.bkSelSede.telefono,email:this.bkSelSede.email
10164        }:null,
10165        slot:{id_spazio:sp.id_spazio,data:sp.data,ora_inizio:sp.ora_inizio,ora_fine:sp.ora_fine,sportello_id:sp.sportello_id},
10166        context:{pageUrl:pageUrl,sessionId:sid,userAgent:ua},
10167        clientRequestId:this._bkSubmitReqId
10168      },{noRetry:true});
10169      /* prenota NON è idempotente: nessun retry automatico (un timeout-poi-retry
10170       * creava una SECONDA prenotazione). Su timeout mostriamo "esito incerto". */
10171      this.bkId=(r&&r.id)?r.id:null;this.bkStep="done";this._bkSubmitReqId=null;this._bkRender();/* WCAG 4.1.3 (#C32): annuncio dentro il dialog aria-modal */this._bkFsAnnounce(bkT("booking_confirmed")+(this.bkId?(" "+bkT("label_done_id")+this.bkId):""));this._updateSendBtn();
10172      /* #42 — il consenso DEVE essere già stato espresso prima di arrivare
10173       * qui (chiediamo all'apertura widget). Se per qualche motivo non c'è,
10174       * proviamo un'ultima volta. saveStoredClient è interno-gated, fa no-op
10175       * senza consenso "functional". */
10176      var self=this;
10177      Promise.resolve().then(function(){return self._bkRequestConsent();}).then(function(consent){
10178        if(consent&&consent.functional){saveStoredClient(d,self.bkBookingFor);}
10179      });
10180      if(!this.opts.demopromo)pushBookingHistory({bookingId:this.bkId,slot:this.bkSelSlot,servizio:this.bkSelServ,sede:this.bkSelSede,provincia:this.bkSelProv,regione:this.bkSelRegione});
10181      clearResumeState(); /* prenotazione completata: niente resume */
10182      this._bkLogEvent&&this._bkLogEvent("submit_success",{bookingId:this.bkId,for:this.bkBookingFor});
10183    }catch(e){
10184      var code=(e&&e.code)||"unknown";
10185      this._bkLogEvent&&this._bkLogEvent("submit_failure",{errorCode:code,errorMessage:(e&&(e.error||e.message))||""});
10186      if(code==="timeout"||code==="network"||code==="server"||code==="unknown"||code==="500"||code==="502"||code==="503"||code==="504"){
10187        /* Esito INCERTO: la richiesta potrebbe aver raggiunto il gestionale.
10188         * Include "server"/5xx: il server li classifica come incerti (route.ts
10189         * salva bookingStatus:"uncertain") perché la prenotazione PUÒ esistere
10190         * (es. HTTP 200 con messaggio non riconosciuto). NON trattarli come
10191         * errore definitivo → eviterebbe un secondo invio e un possibile doppione.
10192         * NON resettiamo _bkSubmitReqId (riusabile da un'eventuale dedup server
10193         * futura). NB: la dedup server NON è ancora implementata → se l'utente
10194         * riprova manualmente e il primo invio era andato a buon fine, il
10195         * gestionale lo blocca con "già prenotato". Per questo invitiamo a
10196         * controllare l'email PRIMA di riprovare. */
10197        this._bkSetError({error:bkT("err_uncertain"),code:code},"confirm");
10198      }else{
10199        /* Errore definitivo (già prenotato / slot occupato / validazione / 4xx):
10200         * nessun doppione possibile → azzeriamo l'id così un nuovo flusso parte pulito. */
10201        this._bkSubmitReqId=null;
10202        if(code==="slot_taken"){
10203          /* cod 210: lo slot è stato preso da un altro cliente tra la scelta e il
10204           * save (il server ha già tentato il recupero per escludere il caso
10205           * "occupato da me stesso"). Svuotiamo gli spazi così il rientro su
10206           * DATA e ORA li ri-fetcha freschi invece di riproporre lo slot morto.
10207           * Azzeriamo anche lo stato calendario: la disponibilità fresca può
10208           * essere slittata a un altro giorno/mese — la vista si riallinea
10209           * alla prima disponibilità invece di mostrare un mese vuoto. */
10210          this.bkSelSlot=null;this.bkSpazi=[];this.bkNextG="";
10211          this._bkCalSelected=null;this._bkCalMonth=null;
10212        }
10213        this._bkSetError(e,(code==="already_booked"||code==="slot_taken")?"selectSlot":"confirm");
10214      }
10215    }
10216    finally{this._bkBusy=false;}
10217  };
10218
10219  /** Salta direttamente a uno step della prenotazione (usato dai click
10220   *  sulla sidebar). Resetta solo le selezioni dello step e dei successivi
10221   *  preservando quelle precedenti. Annulla anche l'auto-jump GPS in corso. */
10222  CafChat.prototype._bkJumpToStep = function(step){
10223    /* Cancella qualunque auto-jump GPS in arrivo: la scelta manuale dell'utente
10224     * deve avere la precedenza. */
10225    this._bkUserOverride=true;
10226    this._bkAutoProvStatus=null;
10227    if(this._bkAutoProvWatchdog){clearTimeout(this._bkAutoProvWatchdog);this._bkAutoProvWatchdog=null;}
10228    /* invalida ogni _bkPick* in volo prima di resettare le selezioni a valle */
10229    this._bkInvalidateInflight();
10230    /* Reset dello step puntato + tutti quelli a valle */
10231    this.bkFormErr=null;this._bkVoiceAnnounced=null;this._bkDir="back";
10232    /* track which sidebar items have already animated, so re-rendering after
10233     * jump non rifa l'animazione di "just-completed" su ciò che non cambia */
10234    switch(step){
10235      case "selectRegione":
10236        this.bkSelRegione=null;this.bkProvince=[];this.bkSelProv=null;
10237        this.bkServizi=[];this.bkSelServ=null;this.bkSedi=[];this.bkSelSede=null;
10238        this.bkSpazi=[];this.bkSelSlot=null;
10239        this.bkStep="selectRegione";
10240        if(this._bkSideFilled){this._bkSideFilled.reg=false;this._bkSideFilled.prov=false;this._bkSideFilled.serv=false;this._bkSideFilled.sede=false;this._bkSideFilled.slot=false;}
10241        break;
10242      case "selectProvincia":
10243        if(!this.bkSelRegione){this.bkStep="selectRegione";break;}
10244        this.bkSelProv=null;this.bkServizi=[];this.bkSelServ=null;
10245        this.bkSedi=[];this.bkSelSede=null;this.bkSpazi=[];this.bkSelSlot=null;
10246        this.bkStep="selectProvincia";
10247        if(this._bkSideFilled){this._bkSideFilled.prov=false;this._bkSideFilled.serv=false;this._bkSideFilled.sede=false;this._bkSideFilled.slot=false;}
10248        break;
10249      case "selectServizio":
10250        if(!this.bkSelProv){this.bkStep=this.bkSelRegione?"selectProvincia":"selectRegione";break;}
10251        this.bkSelServ=null;this.bkSedi=[];this.bkSelSede=null;
10252        this.bkSpazi=[];this.bkSelSlot=null;
10253        if(this._bkSideFilled){this._bkSideFilled.serv=false;this._bkSideFilled.sede=false;this._bkSideFilled.slot=false;}
10254        /* Se i servizi non sono già in cache (caso raro), ricarica via
10255         * _bkPickProv che li fetcha e popola bkServizi.
10256         * Cancelliamo bkHint per evitare l'auto-pick aggressivo. */
10257        if(!this.bkServizi||!this.bkServizi.length){
10258          this.bkHint=null;this._bkBusy=false;
10259          this._bkPickProv(this.bkSelProv);
10260          return;
10261        }
10262        this.bkStep="selectServizio";
10263        break;
10264      case "selectSede":
10265        if(!this.bkSelServ){this.bkStep=this.bkSelProv?"selectServizio":"selectProvincia";break;}
10266        this.bkSelSede=null;this.bkSpazi=[];this.bkSelSlot=null;
10267        this.bkStep="selectSede";
10268        if(this._bkSideFilled){this._bkSideFilled.sede=false;this._bkSideFilled.slot=false;}
10269        break;
10270      case "selectSlot":
10271        if(!this.bkSelSede){this.bkStep=this.bkSelServ?"selectSede":"selectServizio";break;}
10272        this.bkSelSlot=null;
10273        this.bkStep="selectSlot";
10274        if(this._bkSideFilled){this._bkSideFilled.slot=false;}
10275        break;
10276      case "collectData":
10277        if(!this.bkSelSlot){this.bkStep=this.bkSelSede?"selectSlot":"selectSede";break;}
10278        this.bkStep="collectData";
10279        break;
10280      default:return;
10281    }
10282    this._bkRender();
10283  };
10284
10285  /* #60 — propone un banner inline "Riprendi da dove hai lasciato?".
10286   * Ritorna Promise<bool> = true se l'utente ha scelto di riprendere
10287   * (in tal caso lo stato viene ripristinato e il render avviene). */
10288  CafChat.prototype._bkOfferResume = function(rs){
10289    var self=this;
10290    return new Promise(function(resolve){
10291      var container=self._bkFsEl;if(!container){resolve(false);return;}
10292      /* Pulisci eventuali precedenti */
10293      var prev=container.querySelector(".caf-bk-resume");if(prev)prev.parentNode.removeChild(prev);
10294      var stepLabels={selectRegione:bkT("step_zone"),selectProvincia:bkT("step_zone"),selectServizio:bkT("step_service"),selectSede:bkT("step_office"),selectSlot:bkT("step_time"),collectData:bkT("step_data"),confirm:bkT("h_confirm")};
10295      var lbl=stepLabels[rs.step]||bkT("label_resume_h");
10296      var detail=[];
10297      if(rs.provincia&&rs.provincia.descrizione)detail.push(rs.provincia.descrizione);
10298      if(rs.servizio&&(rs.servizio.desonline||rs.servizio.descrizione))detail.push(rs.servizio.desonline||rs.servizio.descrizione);
10299      var b=document.createElement("div");b.className="caf-bk-resume";
10300      b.innerHTML='<div class="caf-bk-resume-ic">▶</div>'
10301        +'<div class="caf-bk-resume-body"><div class="caf-bk-resume-h">'+escapeHtml(bkT("label_resume_h"))+'</div>'
10302        +'<div>'+escapeHtml(lbl)+(detail.length?" — "+escapeHtml(detail.join(" · ")):"")+'</
10302div></div>'
10303        +'<div class="caf-bk-resume-actions">'
10304          +'<button type="button" class="caf-bk-resume-btn primary" data-act="resume">'+escapeHtml(bkT("label_resume_btn"))+'</button>'
10305          +'<button type="button" class="caf-bk-resume-btn ghost" data-act="discard">'+escapeHtml(bkT("label_resume_discard"))+'</button>'
10306        +'</div>';
10307      container.appendChild(b);
10308      var done=function(answer){
10309        try{b.parentNode&&b.parentNode.removeChild(b);}catch(_e){}
10310        resolve(answer);
10311      };
10312      b.querySelector("[data-act=resume]").addEventListener("click",function(){
10313        /* 1) Rimuovi banner + risolvi promise SUBITO (cosi _bkStart esce e
10314         *    libera _bkBusy). Senza questo, _bkPickProv sotto fallirebbe il
10315         *    check `if(_bkBusy)return` perché _bkStart è ancora busy. */
10316        done(true);
10317        self._bkLogEvent&&self._bkLogEvent("resume_accepted",{step:rs.step});
10318        /* 2) Mostra subito un loading con messaggio dedicato così l'utente
10319         *    capisce che stiamo riprendendo (senza schermata vuota). */
10320        self.bkStep="loading";
10321        self._bkResumeLoading={
10322          msg:bkT("label_resume_h")+"..."
10323        };
10324        self._bkRender();
10325        /* 3) Esegui la cascata in modo asincrono. Tra ogni step lasciamo
10326         *    sliding via _bkPickXxx che gestiscono già bkBusy/render/error. */
10327        (async function(){
10328          /* Aspetta un microtask per essere sicuri che _bkStart abbia liberato
10329           * _bkBusy=false. Senza questo, _bkPickProv torna immediatamente. */
10330          await new Promise(function(r){setTimeout(r,0);});
10331          /* Cascata di resume ANNULLABILE: il bottone "Annulla" sullo step
10332           * loading (Group A) torna a selectRegione abortendo la fetch in corso,
10333           * invece di lasciare l'utente su uno spinner fino a decine di secondi. */
10334          self._bkLoadingReturnStep="selectRegione";
10335          try{
10336            /* Pre-popola i dati base PRIMA delle chiamate API: così se
10337             * qualcuna fallisce, la rete di sicurezza ha valori sensati. */
10338            /* #PII — resume cross-sessione su device condiviso: i dati cliente
10339             * salvati NON entrano direttamente in bkClient (eviterebbero il gate).
10340             * Diventano "candidato": il gate "Sei tu?" a collectData li adotta solo
10341             * su conferma esplicita. */
10342            if(rs.client){self._bkStoredCandidate=rs.client;self.bkClient={nome:"",cognome:"",codfis:"",email:"",cellulare:"",sesso:"M"};}
10343            if(rs.bookingFor){self.bkBookingFor=rs.bookingFor;}
10344            if(rs.regione){self.bkSelRegione=rs.regione;}
10345            /* Carica regioni se mancano. Necessario se l'utente apre la chat
10346             * da fresh load: bkRegioni è vuoto. ALSO populate _bkProvinciaByConn
10347             * per evitare un fetch in più dentro _bkPickRegione subito dopo. */
10348            if(!self.bkRegioni||!self.bkRegioni.length){
10349              var allFetched=null;
10350              try{allFetched=await self._bkFetchAllProvince();}catch(_e){allFetched=null;}
10351              if(!allFetched||!allFetched.regioni||!allFetched.regioni.length){
10352                /* Nessuna regione disponibile: errore esplicito invece di lista vuota silenziosa */
10353                throw new Error("regioni non disponibili dopo resume");
10354              }
10355              self.bkRegioni=allFetched.regioni;
10356              /* Popola _bkProvinciaByConn (come fa _bkStart) così _bkPickRegione
10357               * non rifa la stessa fetch dopo. Cache hit per la regione coperta
10358               * da allFetched.items (su deploy ZONA = la regione preselezionata);
10359               * un resume di una regione diversa (stato stale) fa miss e ricade
10360               * sulla fetch on-demand di _bkPickRegione poco sotto. */
10361              self._bkProvinciaByConn={};
10362              if(allFetched.items&&allFetched.items.length){
10363                allFetched.items.forEach(function(it){
10364                  var k=it.regione&&it.regione.connessione;if(!k)return;
10365                  if(!self._bkProvinciaByConn[k])self._bkProvinciaByConn[k]=[];
10366                  self._bkProvinciaByConn[k].push(it.provincia);
10367                });
10368              }
10369            }
10370            /* Regioni pronte: liberiamo la caption statica "Riprendi…" così le
10371             * caption PER-STEP dei _bkPickXxx ("Cerco i servizi…", "Cerco le
10372             * sedi…", "Cerco gli orari…") diventano visibili durante la cascata,
10373             * invece di uno spinner congelato (#6). */
10374            self._bkResumeLoading=null;
10375            /* Step "selectRegione": le regioni sono ora popolate sopra (o abbiamo
10376             * thrown). Setta solo lo step e renderizza. */
10377            if(rs.step==="selectRegione"){self.bkStep="selectRegione";self._bkRender();return;}
10378            if(rs.step==="selectProvincia"){
10379              if(rs.provincia){self.bkSelProv=rs.provincia;}
10380              /* Senza regione non possiamo caricare province → torna a selectRegione */
10381              if(!rs.regione){self.bkStep="selectRegione";self._bkRender();return;}
10382              /* Tentativo veloce: usa la cache _bkProvinciaByConn appena popolata.
10383               * Se hit, settiamo bkProvince direttamente senza un'altra chiamata
10384               * API. Se miss, deleghiamo a _bkPickRegione che gestisce fetch +
10385               * gestione errori + auto-pick + step transition. */
10386              var connKey=rs.regione&&rs.regione.connessione;
10387              var cachedProv=connKey&&self._bkProvinciaByConn&&self._bkProvinciaByConn[connKey];
10388              self.bkSelRegione=rs.regione;
10389              if(cachedProv&&cachedProv.length){
10390                self.bkProvince=cachedProv;
10391                self.bkStep="selectProvincia";self._bkRender();return;
10392              }
10393              /* Cache miss: lascia che _bkPickRegione fetchi e gestisca lo step
10394               * internamente. NON sovrascriviamo bkStep dopo l'await: se la
10395               * fetch fallisce _bkPickRegione setta bkStep="error" via
10396               * _bkSetError, ed è giusto mostrare l'errore — non una lista vuota. */
10397              self._bkBusy=false;
10398              await self._bkPickRegione(rs.regione);
10399              /* Se _bkPickRegione ha fallito (no province / network / regione
10400               * non più valida), il bkStep è "error". Lancia per far scattare
10401               * clearResumeState() nel catch outer: senza questo, al prossimo
10402               * refresh il banner "Riprendi" rifa il loop sullo stesso errore. */
10403              if(self.bkStep==="error"){
10404                throw new Error("resume: _bkPickRegione fallita su rs.regione (state stale o API down)");
10405              }
10406              return;
10407            }
10408            /* Step più avanzati: ri-cascata via _bkPickXxx che caricano i dati */
10409            if(!rs.provincia){throw new Error("missing provincia in resume state");}
10410            /* Reset busy: _bkPickProv lo riprende internamente */
10411            self._bkBusy=false;
10412            await self._bkPickProv(rs.provincia);
10413            if(rs.step==="selectServizio")return; /* _bkPickProv ha lasciato bkStep=selectServizio */
10414            if(!rs.servizio){throw new Error("missing servizio in resume state");}
10415            var fullSrv=(self.bkServizi||[]).filter(function(s){return s.codice===rs.servizio.codice;})[0]||rs.servizio;
10416            self._bkBusy=false;
10417            await self._bkPickServ(fullSrv);
10418            if(rs.step==="selectSede")return;
10419            if(!rs.sede){throw new Error("missing sede in resume state");}
10420            var fullSede=(self.bkSedi||[]).filter(function(s){return s.codice===rs.sede.codice;})[0]||rs.sede;
10421            self._bkBusy=false;
10422            await self._bkPickSede(fullSede);
10423            if(rs.step==="selectSlot")return;
10424            /* Step >= collectData: ricostruisci lo slot SOLO se esiste ancora tra
10425             * gli spazi appena ricaricati. Lo slot salvato a sessione precedente
10426             * può essere scaduto o già preso: NON ripristinarlo "as-is" (il vecchio
10427             * fallback `||rs.slot` lo faceva → uno slot stantio/passato finiva nel
10428             * payload prenota senza ri-validazione). Se non c'è più, riportiamo
10429             * l'utente alla scelta dello slot invece di prenotare dati errati. */
10430            var freshSlot=(rs.slot&&self.bkSpazi&&self.bkSpazi.length)
10431              ? (self.bkSpazi.filter(function(s){return s.id_spazio===rs.slot.id_spazio;})[0]||null)
10432              : null;
10433            if(freshSlot){
10434              self.bkSelSlot=freshSlot;
10435              /* #PII — se i dati cliente sono solo "candidati" (resume), non saltare
10436               * a "confirm" con dati altrui: atterra su collectData dove il gate
10437               * "Sei tu?" chiede conferma esplicita prima di prenotare. */
10438              self.bkStep=(rs.step==="confirm"&&self._bkStoredCandidate&&!(self.bkClient.codfis||self.bkClient.nome))?"collectData":rs.step;
10439            }else{
10440              self.bkSelSlot=null;
10441              self.bkStep="selectSlot"; /* slot non più disponibile → re-pick */
10442            }
10443            self._bkRender();
10444          }catch(err){
10445            /* In caso di errore, mostra error boundary con messaggio utile */
10446            try{
10447              self._bkSetError({
10448                error:bkT("err_s_default"),
10449                code:(err&&err.code)||"unknown",
10450                plannerMessage:(err&&err.message)||String(err)
10451              },null);
10452            }catch(_e){
10453              self.bkStep="selectRegione";self._bkRender();
10454            }
10455          }
10456        })();
10457      });
10458      b.querySelector("[data-act=discard]").addEventListener("click",function(){
10459        clearResumeState();
10460        self._bkLogEvent&&self._bkLogEvent("resume_discarded",{step:rs.step});
10461        done(false);
10462      });
10463    });
10464  };
10465
10466  /* Naviga direttamente a uno step precedente preservando lo stato a valle.
10467   * Usato dall'edit inline del summary (#36) e dal breadcrumb cliccabile (#47). */
10468  CafChat.prototype._bkGoToStep = function(targetStep){
10469    /* whitelist degli step navigabili a ritroso: */
10470    var seq=["selectRegione","selectProvincia","selectServizio","selectSede","selectSlot","collectData","confirm"];
10471    var ti=seq.indexOf(targetStep);if(ti<0)return;
10472    /* invalida ogni _bkPick* in volo prima di ripulire le selezioni a valle */
10473    this._bkInvalidateInflight();
10474    /* Pulisci selezioni a valle dello step target */
10475    if(ti<seq.indexOf("selectSlot")){this.bkSelSlot=null;}
10476    if(ti<seq.indexOf("selectSede")){this.bkSelSede=null;this.bkSpazi=[];}
10477    if(ti<seq.indexOf("selectServizio")){this.bkSelServ=null;this.bkServizi=[];}
10478    if(ti<seq.indexOf("selectProvincia")){this.bkSelProv=null;this.bkProvince=[];}
10479    if(ti<seq.indexOf("selectRegione")){this.bkSelRegione=null;this.bkRegioni=[];}
10480    this.bkStep=targetStep;this._bkDir="back";this._bkVoiceAnnounced=null;
10481    /* #30 — View Transitions: morph elegante tra step quando supportato */
10482    this._bkRenderWithTransition();
10483    bkHaptic&&bkHaptic(8);
10484    this._bkLogEvent&&this._bkLogEvent("step_jump",{to:targetStep});
10485  };
10486
10487  CafChat.prototype._bkBack = function(){
10488    /* invalida ogni _bkPick* in volo: la navigazione indietro scarta lo stato a
10489     * valle, quindi una fetch stantia non deve poterlo riscrivere dopo il reset */
10490    this._bkInvalidateInflight();
10491    this.bkFormErr=null;this._bkVoiceAnnounced=null;this._bkDir="back";
10492    switch(this.bkStep){
10493      case "selectRegione":
10494        /* go back to asking start (idle) */
10495        this._bkReset();
10496        return;
10497      case "selectProvincia":
10498        /* If multiple regioni, allow re-pick. Otherwise reset. */
10499        if(this.bkRegioni&&this.bkRegioni.length>1){
10500          this.bkStep="selectRegione";this.bkSelRegione=null;this.bkProvince=[];this.bkSelProv=null;break;
10501        }
10502        this._bkReset();
10503        return;
10504      case "selectServizio":
10505        /* if province was auto-selected (only 1 or from location match), go back to location */
10506        if(this.bkProvince.length<=1){this._bkReset();return;}
10507        this.bkStep="selectProvincia";this.bkSelProv=null;break;
10508      case "selectSede":this.bkStep="selectServizio";this.bkSelServ=null;
10509        /* restore full servizi list (in case it was filtered by hint) */
10510        this.bkServizi=[];break;
10511      case "selectSlot":this.bkStep="selectSede";this.bkSelSede=null;this.bkSpazi=[];break;
10512      case "collectData":this.bkStep="selectSlot";this.bkSelSlot=null;break;
10513      case "confirm":this.bkStep="collectData";break;
10514      case "error":
10515        var ret=this.bkErrReturn;
10516        this.bkErrCode=null;this.bkErrMsg="";this.bkErrReturn=null;
10517        if(ret){this.bkStep=ret;}
10518        else{this._bkReset();return;}
10519        break;
10520      default:this._bkReset();return;
10521    }
10522    /* if going back to selectServizio and servizi are empty, re-fetch */
10523    if(this.bkStep==="selectServizio"&&(!this.bkServizi||!this.bkServizi.length)&&this.bkSelProv){
10524      var self=this;
10525      self._bkBusy=true;self.bkStep="loading";self._bkRender();
10526      var connBack=self.bkSelRegione&&self.bkSelRegione.connessione;
10527      self._bkFetch("servizi",{codiceProvincia:self.bkSelProv.codice,connessione:connBack}).then(function(all){
10528        var srv=(all||[]).filter(function(s){return s.online==="1";});
10529        self.bkServizi=srv.length?srv:[];
10530        self.bkStep=srv.length?"selectServizio":"error";
10531        if(!srv.length){self.bkErrMsg=bkT("err_s_no_services");self.bkErrCode="no_services";self.bkErrReturn="selectProvincia";}
10532        self._bkBusy=false;self._bkRender();
10533      }).catch(function(e){self._bkBusy=false;self._bkSetError(e,"selectProvincia");});
10534      return;
10535    }
10536    /* slot_taken: il rientro su DATA e ORA trova bkSpazi svuotato di proposito
10537     * (il submit ha azzerato gli spazi) → ri-passa da _bkPickSede per ri-fetchare
10538     * gli orari FRESCHI (gestisce anche il drain Piemonte e il caso "nessuno
10539     * spazio rimasto" → errore no_spazi con ritorno a selectSede). */
10540    if(this.bkStep==="selectSlot"&&(!this.bkSpazi||!this.bkSpazi.length)&&this.bkSelSede&&this.bkSelProv&&this.bkSelServ){
10541      this._bkPickSede(this.bkSelSede,{internalRefetch:true});
10542      return;
10543    }
10544    this._bkRender();
10545  };
10546
10547  /* ── booking: render UI into _bkEl ─────────────────────────── */
10548  /* A11y (WCAG 2.4.3): il renderer inline azzera il container e distrugge
10549   * l'elemento con il focus. Il wrapper lo ri-posiziona dentro il wizard
10550   * inline (titolo o primo controllo) quando il focus era li' ed e' caduto
10551   * su body; non tocca nulla se l'utente stava scrivendo altrove. */
10552  CafChat.prototype._bkRender = function(){
10553    var hadFocus=false;
10554    try{
10555      var _a=(this.shadow&&this.shadow.activeElement)||document.activeElement;
10556      hadFocus=!!(_a&&this._bkEl&&this._bkEl.contains(_a));
10557    }catch(_e){}
10558    this._bkRenderInlineImpl();
10559    if(this._bkFsMode||!hadFocus)return;
10560    try{
10561      var el=this._bkEl;if(!el)return;
10562      var _b=(this.shadow&&this.shadow.activeElement)||document.activeElement;
10563      if(_b&&_b!==document.body&&el.contains(_b))return;
10564      var t=el.querySelector(".caf-bk-title");
10565      if(t&&!t.hasAttribute("tabindex"))t.setAttribute("tabindex","-1");
10566      if(!t)t=el.querySelector('button:not([disabled]),a[href],input:not([disabled]),[tabindex]:not
10566([tabindex="-1"])');
10567      if(t)t.focus();
10568    }catch(_e){}
10569  };
10570  CafChat.prototype._bkRenderInlineImpl = function(){
10571    /* clear resume loading hint quando usciamo dal loading state */
10572    if(this.bkStep!=="loading"&&this._bkResumeLoading){this._bkResumeLoading=null;}
10573    /* update the prominent expand banner outside the conversation */
10574    if(this._updateBkExpandBanner)this._updateBkExpandBanner();
10575    /* #60 — persisti stato per resume su refresh accidentale: salviamo solo
10576     * step "interattivi", non loading/error/done (quelli sono transitori). */
10577    try{
10578      var persistableSteps={selectRegione:1,selectProvincia:1,selectServizio:1,selectSede:1,selectSlot:1,collectData:1,confirm:1};
10579      if(persistableSteps[this.bkStep]){
10580        saveResumeState({
10581          step:this.bkStep,
10582          regione:this.bkSelRegione||null,
10583          provincia:this.bkSelProv||null,
10584          servizio:this.bkSelServ?{codice:this.bkSelServ.codice,descrizione:this.bkSelServ.descrizione,desonline:this.bkSelServ.desonline,tempo:this.bkSelServ.tempo}:null,
10585          sede:this.bkSelSede?{codice:this.bkSelSede.codice,descrizione:this.bkSelSede.descrizione,indirizzo:this.bkSelSede.indirizzo,citta:this.bkSelSede.citta}:null,
10586          slot:this.bkSelSlot||null,
10587          client:this.bkClient||null,
10588          bookingFor:this.bkBookingFor||"self"
10589        });
10590      }
10591    }catch(_e){}
10592    /* when fullscreen mode active, route to premium renderer */
10593    if(this._bkFsMode){this._bkRenderFs();return;}
10594    if(!this._bkEl)return;var el=this._bkEl,self=this,c=this.opts.color;el.innerHTML="";
10595    this._updateSendBtn();
10596    if(this.opts.demopromo){el.appendChild(this._bkDemoRibbon());}
10597    var TOTAL=5;
10598    var slideDir=self._bkDir||"fwd";self._bkDir="fwd";
10599
10600    var stepEmojis=["📍","📋","🏢","📅","✍️"];
10601    var stepLabels=[bkT("step_zone"),bkT("step_service"),bkT("step_office"),bkT("step_time"),bkT("step_data")];
10602    function mkStep(cur,label){
10603      var w=document.createElement("div");w.className="caf-bk-steps-wrap";
10604      /* step dots with checkmarks */
10605      var row=document.createElement("div");row.className="caf-bk-steps";
10606      for(var i=0;i<TOTAL;i++){
10607        if(i>0){var ln=document.createElement("div");ln.className="caf-bk-step-line";ln.style.background=i<=cur?c+"60":"#e2e8f0";row.appendChild(ln);}
10608        var dot=document.createElement("div");dot.className="caf-bk-step-dot "+(i<cur?"done":i===cur?"active":"todo");
10609        if(i<cur){dot.style.background=c;dot.innerHTML='<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="3" stroke-linecap="round"><path d="M20 6L9 17l-5-5"/></svg>';}
10610        else{dot.textContent=i+1;}
10611        row.appendChild(dot);
10612      }
10613      w.appendChild(row);
10614      /* step labels */
10615      var lblRow=document.createElement("div");lblRow.className="caf-bk-step-labels";
10616      for(var j=0;j<TOTAL;j++){
10617        if(j>0){var sp=document.createElement("div");sp.style.flex="1";lblRow.appendChild(sp);}
10618        var sl=document.createElement("div");sl.className="caf-bk-step-lbl"+(j===cur?" active":j<cur?" done":"");sl.textContent=stepLabels[j];lblRow.appendChild(sl);
10619      }
10620      w.appendChild(lblRow);
10621      /* progress bar */
10622      var prog=document.createElement("div");prog.className="caf-bk-progress";
10623      var progBar=document.createElement("div");progBar.className="caf-bk-progress-bar";progBar.style.width=Math.round(((cur+1)/TOTAL)*100)+"%";prog.appendChild(progBar);
10624      w.appendChild(prog);
10625      /* step counter */
10626      var cnt=document.createElement("div");cnt.className="caf-bk-step-counter";cnt.textContent=bkT("step_of",(cur+1),TOTAL);w.appendChild(cnt);
10627      /* title */
10628      var t=document.createElement("div");t.className="caf-bk-title";t.style.marginTop="8px";t.innerHTML=stepEmojis[cur]+" "+escapeHtml(label);w.appendChild(t);
10629      return w;
10630    }
10631    /* ripple effect helper */
10632    function addRipple(btn){
10633      btn.addEventListener("click",function(e){
10634        var rect=btn.getBoundingClientRect();var rip=document.createElement("span");rip.className="caf-ripple";
10635        var sz=Math.max(rect.width,rect.height);rip.style.width=rip.style.height=sz+"px";
10636        rip.style.left=(e.clientX-rect.left-sz/2)+"px";rip.style.top=(e.clientY-rect.top-sz/2)+"px";
10637        btn.appendChild(rip);setTimeout(function(){if(rip.parentNode)rip.parentNode.removeChild(rip);},600);
10638      });
10639    }
10640    function mkBot(back){
10641      var b=document.createElement("div");
10641b.className="caf-bk-bot";
10642      if(back!==false){var bk=document.createElement("button");bk.className="caf-bk-back";bk.textContent="\u2190 "+bkT("btn_back");bk.addEventListener("click",function(){self._bkBack();});b.appendChild(bk);}
10643      else b.appendChild(document.createElement("span"));
10644      var cn=document.createElement("button");cn.className="caf-bk-cancel";cn.textContent="\u2715 "+bkT("btn_cancel");cn.addEventListener("click",function(){self._bkReset();});b.appendChild(cn);return b;
10645    }
10646    function mkSpin(lbl,icon){var w=document.createElement("div");w.className="caf-bk-spin";
10647      var sw=document.createElement("div");sw.className="caf-bk-spin-wrap";
10648      var r=document.createElement("div");r.className="caf-bk-ring";sw.appendChild(r);
10649      var ic=document.createElement("div");ic.className="caf-bk-spin-icon";ic.textContent=icon||"\uD83D\uDCC5";sw.appendChild(ic);
10650      w.appendChild(sw);
10651      var l=document.createElement("div");l.className="caf-bk-spin-lbl";l.textContent=lbl;w.appendChild(l);
10652      var bar=document.createElement("div");bar.className="caf-bk-spin-bar";var bi=document.createElement("div");bi.className="caf-bk-spin-bar-inner";bar.appendChild(bi);w.appendChild(bar);
10653      return w;}
10654    function mkCrumb(t,svgIcon){var s=document.createElement("div");s.className="caf-bk-crumb";
10655      if(svgIcon)s.innerHTML=svgIcon;
10656      var sp=document.createElement("span");sp.textContent=t;s.appendChild(sp);return s;}
10657    function mkHint(txt){var d=document.createElement("div");d.style.cssText="font-size:12.5px;color:#5b6573;margin-bottom:10px;line-height:1.5;animation:caf-pop-in .3s ease-out";d.textContent=txt;return d;}
10658
10659    switch(this.bkStep){
10660    case "loading":{
10661      var loadMsg=(self._bkAutoProvStatus&&self._bkAutoProvStatus.type==="loading")
10662        ? self._bkAutoProvStatus.msg
10663        : (self.bkHint
10664            ? bkT("label_loading_hint",self.bkHint)
10665            : bkT("label_loading_options"));
10666      var loadIcon=(self._bkAutoProvStatus&&self._bkAutoProvStatus.type==="loading")?"\uD83D\uDCCD":"\uD83D\uDD0D";
10667      el.appendChild(mkSpin(loadMsg,loadIcon));
10668      /* "Annulla": sblocca l'attesa (fino a ~47s con i retry) e torna allo step
10669       * precedente. Solo se c'\u00E8 uno step di ritorno (non sul primissimo load). */
10670      if(self._bkLoadingReturnStep){
10671        var cbL=document.createElement("button");cbL.type="button";cbL.textContent=bkT("btn_cancel");
10672        cbL.style.cssText="margin:14px auto 0;display:block;background:none;border:none;color:#5b6573;font-size:13px;text-decoration:underline;cursor:pointer;padding:6px 10px";
10673        cbL.addEventListener("click",function(){self._bkCancelLoading();});
10674        el.appendChild(cbL);
10675      }
10676      break;
10677    }
10678    case "submitting": el.appendChild(mkSpin(bkT("label_submitting"),"\u2709\uFE0F")); break;
10679
10680    case "selectRegione":{
10681      var rw=document.createElement("div");rw.className=slideDir==="back"?"caf-bk-slide-back":"caf-bk-slide-fwd";
10682      rw.appendChild(mkStep(0,bkT("t_region")));
10683      rw.appendChild(mkHint(bkT("h_region")));
10684      /* Banner stato geolocalizzazione: visibile in TUTTI gli stati. */
10685      if(self._bkAutoProvStatus){
10686        var stTypeR=self._bkAutoProvStatus.type;
10687        var spR=document.createElement("div");spR.className="caf-bk-locate-status "+(stTypeR==="ok"?"ok":(stTypeR==="err"?"err":"loading"));
10688        var spIcR;
10689        if(stTypeR==="loading")spIcR='<span class="spin" style="display:inline-block;width:14px;height:14px;border:2px solid currentColor;border-right-color:transparent;border-radius:50%;animation:caf-spin .8s linear infinite;vertical-align:middle"></span>';
10690        else if(stTypeR==="ok")spIcR='<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round"><polyline points="20 6 9 17 4 12"/></svg>';
10691        else spIcR='<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"><circle cx="12" cy="12" r="10"/><path d="M12 8v4m0 4h.01"/></svg>';
10692        spR.innerHTML=spIcR+'<span>'+escapeHtml(self._bkAutoProvStatus.msg)+'</span>';
10693        rw.appendChild(spR);
10694      }
10695      var rg=document.createElement("div");rg.className="caf-bk-grid";
10696      self.bkRegioni.forEach(function(r,i){
10697        var b=document.createElement("button");b.className="caf-bk-card";b.style.animation="caf-fade-up .35s ease-out "+(i*40)+"ms backwards";b.style.paddingLeft="12px";
10698        var ic=document.createElement("span");ic.style.cssText="margin-right:4px;font-size:13px";ic.textContent="\uD83C\uDDEE\uD83C\uDDF9";b.appendChild(ic);
10699        b.appendChild(document.createTextNode(r.descrizione));
10700        addRipple(b);b.addEventListener("click",function(){self._bkPickRegione(r);});rg.appendChild(b);
10701      });
10702      rw.appendChild(rg);rw.appendChild(mkBot(false));el.appendChild(rw);break;
10703    }
10704    case "selectProvincia":{
10705      var pw=document.createElement("div");pw.className=slideDir==="back"?"caf-bk-slide-back":"caf-bk-slide-fwd";
10706      pw.appendChild(mkStep(0,bkT("t_province")));
10707      pw.appendChild(mkHint(bkT("h_province")));
10708      /* PROMINENT "use my location" button for auto-detecting province */
10709      var locProv=document.createElement("button");locProv.type="button";locProv.className="caf-bk-locate";
10710      var lpLoading=self._bkAutoProvStatus&&self._bkAutoProvStatus.type==="loading";
10711      if(lpLoading){locProv.disabled=true;
10712        locProv.innerHTML='<span class="caf-bk-locate-ic"><span class="spin"></span></span>'+
10713          '<div class="caf-bk-locate-txt"><div class="caf-bk-locate-t">'+escapeHtml(self._bkAutoProvStatus.msg)+'</div></div>';
10714      }else{
10715        locProv.innerHTML='<span class="caf-bk-locate-ic"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="10" r="3"/><path d="M20 10c0 6-8 12-8 12s-8-6-8-12a8 8 0 0 1 16 0Z"/></svg></span>'+
10716          '<div class="caf-bk-locate-txt"><div class="caf-bk-locate-t">'+escapeHtml(bkT("btn_use_location"))+'</div><div class="caf-bk-locate-s">'+escapeHtml(bkT("btn_use_location_sub"))+'</div></div>'+
10717          '<span class="caf-bk-locate-arrow">›</span>';
10718      }
10719      locProv.addEventListener("click",function(){self._bkAutoProvStatus=null;self._bkAutoDetectProvince(function(){self._bkRender();});});
10720      pw.appendChild(locProv);
10721      /* status row */
10722      if(self._bkAutoProvStatus&&self._bkAutoProvStatus.type!=="loading"){
10723        var sp=document.createElement("div");sp.className="caf-bk-locate-status "+(self._bkAutoProvStatus.type==="ok"?"ok":"err");
10724        var spIc=self._bkAutoProvStatus.type==="ok"
10725          ?'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round"><polyline points="20 6 9 17 4 12"/></svg>'
10726          :'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"><circle cx="12" cy="12" r="10"/><path d="M12 8v4m0 4h.01"/></svg>';
10727        sp.innerHTML=spIc+'<span>'+escapeHtml(self._bkAutoProvStatus.msg)+'</span>';
10728        pw.appendChild(sp);
10729      }
10730      /* divider label */
10731      var divL=document.createElement("div");divL.style.cssText="display:flex;align-items:center;gap:8px;margin:4px 0 8px;font-size:10px;color:#5b6573;font-weight:600;text-transform:uppercase;letter-spacing:.5px";
10732      divL.innerHTML='<span style="flex:1;height:1px;background:#e2e8f0"></span><span>'+escapeHtml(bkT("label_or_choose"))+'</span><span style="flex:1;height:1px;background:#e2e8f0"></span>';
10733      pw.appendChild(divL);
10734
10735      var g=document.createElement("div");g.className="caf-bk-grid";
10736      self.bkProvince.forEach(function(p,i){
10737        var b=document.createElement("button");b.className="caf-bk-card";b.style.animation="caf-fade-up .35s ease-out "+(i*50)+"ms backwards";b.style.paddingLeft="12px";
10738        var ic=document.createElement("span");ic.style.cssText="margin-right:4px;font-size:13px";ic.textContent="\uD83D\uDCCD";b.appendChild(ic);
10739        b.appendChild(document.createTextNode(p.descrizione));
10740        addRipple(b);b.addEventListener("click",function(){self._bkPickProv(p);});g.appendChild(b);});
10741      pw.appendChild(g);pw.appendChild(mkBot(false));el.appendChild(pw);break;
10742    }
10743    case "selectServizio":{
10744      var sw=document.createElement("div");sw.className=slideDir==="back"?"caf-bk-slide-back":"caf-bk-slide-fwd";
10745      sw.appendChild(mkStep(1,bkT("h_service")));
10746      sw.appendChild(mkCrumb(self.bkSelProv?self.bkSelProv.descrizione:"",'<svg width="10" height="10" viewB
10746ox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M20 10c0 6-8 12-8 12s-8-6-8-12a8 8 0 0 1 16 0Z"/><circle cx="12" cy="10" r="3"/></svg>'));
10747      var srvHintMsg=self.bkHint
10748        ? bkT("label_srv_hint_found",self.bkHint)
10749        : bkT("label_srv_hint");
10750      sw.appendChild(mkHint(srvHintMsg));
10751      /* search filter when many services */
10752      var srvListEl=document.createElement("div");srvListEl.className="caf-bk-list";
10753      var allServizi=self.bkServizi;
10754      function renderSrvList(filter){
10755        srvListEl.innerHTML="";
10756        var filtered=filter?allServizi.filter(function(s){return(s.desonline||s.descrizione).toLowerCase().indexOf(filter.toLowerCase())!==-1;}):allServizi;
10757        /* WCAG 4.1.3 (#C65): annuncia l'esito della ricerca (inline) */
10758        if(filter){self._bkFsAnnounce(filtered.length?bkT("sr_results_count",filtered.length):bkT("sr_no_results"));}
10759        if(!filtered.length){var nr=document.createElement("div");nr.className="caf-bk-no-results";nr.textContent=bkT("ph_no_results_one",filter);srvListEl.appendChild(nr);return;}
10760        filtered.forEach(function(s,i){var b=document.createElement("button");b.className="caf-bk-card";b.style.animation="caf-fade-up .3s ease-out "+(i*35)+"ms backwards";
10761          var ic=document.createElement("div");ic.className="caf-bk-card-icon";ic.innerHTML='<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8Z"/><path d="M14 2v6h6"/></svg>';b.appendChild(ic);
10762          var nm=document.createElement("span");nm.textContent=s.desonline||s.descrizione;b.appendChild(nm);
10763          if(+s.tempo>0){var sub=document.createElement("span");sub.className="caf-bk-sub";sub.innerHTML='<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><path d="M12 6v6l4 2"/></svg> '+s.tempo+" min";b.appendChild(sub);}
10764          addRipple(b);b.addEventListener("click",function(){self._bkPickServ(s);});srvListEl.appendChild(b);});
10765      }
10766      if(allServizi.length>5){
10767        var srch=document.createElement("div");srch.className="caf-bk-search";
10768        var srchIc=document.createElement("div");srchIc.className="caf-bk-search-icon";srchIc.innerHTML='<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/></svg>';srch.appendChild(srchIc);
10769        var srchInp=document.createElement("input");srchInp.type="text";srchInp.placeholder=bkT("ph_search_service");srchInp.setAttribute("aria-label",bkT("ph_search_service"));
10769srchInp.addEventListener("input",function(){renderSrvList(srchInp.value);});srch.appendChild(srchInp);sw.appendChild(srch);
10770      }
10771      renderSrvList("");sw.appendChild(srvListEl);sw.appendChild(mkBot(true));el.appendChild(sw);break;
10772    }
10773    case "selectSede":{
10774      var sdw=document.createElement("div");sdw.className=slideDir==="back"?"caf-bk-slide-back":"caf-bk-slide-fwd";
10775      sdw.appendChild(mkStep(2,bkT("h_office")));
10776      sdw.appendChild(mkCrumb(self.bkSelServ?(self.bkSelServ.desonline||self.bkSelServ.descrizione):"",'<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8Z"/><path d="M14 2v6h6"/></svg>'));
10777      sdw.appendChild(mkHint(bkT("label_sede_hint")));
10778      /* PROMINENT "find nearest" button (filled gradient + pulse) */
10779      var nb=document.createElement("button");nb.type="button";nb.className="caf-bk-locate";
10780      var nbLoading=self._bkNearestStatus&&self._bkNearestStatus.type==="loading";
10781      if(nbLoading){nb.disabled=true;
10782        nb.innerHTML='<span class="caf-bk-locate-ic"><span class="spin"></span></span>'+
10783          '<div class="caf-bk-locate-txt"><div class="caf-bk-locate-t">'+escapeHtml(self._bkNearestStatus.msg)+'</div></div>';
10784      }else{
10785        nb.innerHTML='<span class="caf-bk-locate-ic"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="10" r="3"/><path d="M20 10c0 6-8 12-8 12s-8-6-8-12a8 8 0 0 1 16 0Z"/></svg></span>'+
10786          '<div class="caf-bk-locate-txt"><div class="caf-bk-locate-t">'+escapeHtml(self._bkNearestCode?bkT("btn_find_nearest_again"):bkT("btn_find_nearest"))+'</div><div class="caf-bk-locate-s">'+escapeHtml(self._bkNearestCode?bkT("btn_find_nearest_again_sub"):bkT("btn_find_nearest_sub"))+'</div></div>'+
10787          '<span class="caf-bk-locate-arrow">›</span>';
10788      }
10789      nb.addEventListener("click",function(){self.bkSedi.forEach(function(s){delete s._distKm;});self._bkNearestCode=null;self._bkFindNearestSede(function(){self._bkRender();});});
10790      sdw.appendChild(nb);
10791      if(self._bkNearestStatus&&self._bkNearestStatus.type!=="loading"){
10792        var st=document.createElement("div");st.className="caf-bk-locate-status "+(self._bkNearestStatus.type==="ok"?"ok":"err");
10793        var stIc=self._bkNearestStatus.type==="ok"
10794          ?'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round"><polyline points="20 6 9 17 4 12"/></svg>'
10795          :'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"><circle cx="12" cy="12" r="10"/><path d="M12 8v4m0 4h.01"/></svg>';
10796        st.innerHTML=stIc+'<span>'+escapeHtml(self._bkNearestStatus.msg)+'</span>';
10797        sdw.appendChild(st);
10798      }
10799      var sl=document.createElement("div");sl.className="caf-bk-list";
10800      self.bkSedi.forEach(function(s,i){var b=document.createElement("button");b.className="caf-bk-card";b.style.animation="caf-fade-up .35s ease-out "+(i*50)+"ms backwards";
10801        var isNearest=self._bkNearestCode&&(s.codice===self._bkNearestCode||s.descrizione===self._bkNearestCode);
10802        if(isNearest){b.style.borderColor=c;b.style.boxShadow="0 6px 20px "+c+"28";}
10803        var ic=document.createElement("div");ic.className="caf-bk-card-icon";ic.innerHTML='<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/></svg>';b.appendChild(ic);
10804        var nm=document.createElement("span");nm.textContent=s.descrizione;b.appendChild(nm);
10805        if(s._distKm!==undefined){
10806          var d=document.createElement("span");d.style.cssText="display:inline-block;margin-left:6px;font-size:10px;font-weight:700;color:"+c+";background:"+c+"15;border:1px solid "+c+"30;padding:1px 7px;border-radius:99px;vertical-align:middle";
10807          var kmTxt=s._distKm<1?(Math.round(s._distKm*1000)+" m"):(s._distKm.toFixed(1)+" km");
10808          d.textContent=(isNearest?"★ ":"")+kmTxt;
10809          b.appendChild(d);
10810        }
10811        var ad=document.createElement("span");
10811ad.className="caf-bk-sub";ad.innerHTML='<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M20 10c0 6-8 12-8 12s-8-6-8-12a8 8 0 0 1 16 0Z"/><circle cx="12" cy="10" r="3"/></svg> '+escapeHtml(s.indirizzo+", "+s.citta);b.appendChild(ad);
10812        if(s.telefono){
10813          var tel=document.createElement("a");tel.className="caf-bk-sub";
10814          tel.href="tel:"+encodeURIComponent(String(s.telefono).replace(/\s+/g,""));
10815          tel.style.cssText="text-decoration:none;color:inherit;cursor:pointer";
10816          tel.addEventListener("click",function(ev){ev.stopPropagation();self._bkLogEvent&&self._bkLogEvent("sede_phone_clicked",{sede_codice:s.codice});});
10817          tel.innerHTML='<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3"/></svg> '+escapeHtml(s.telefono);
10818          b.appendChild(tel);
10819        }
10820        addRipple(b);b.addEventListener("click",function(){self._bkPickSede(s);});sl.appendChild(b);});
10821      sdw.appendChild(sl);sdw.appendChild(mkBot(true));el.appendChild(sdw);break;
10822    }
10823    case "selectSlot":{
10824      var slw=document.createElement("div");slw.className=slideDir==="back"?"caf-bk-slide-back":"caf-bk-slide-fwd";
10825      slw.appendChild(mkStep(3,bkT("h_slot")));
10826      slw.appendChild(mkCrumb(self.bkSelSede?self.bkSelSede.descrizione:"",'<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/></svg>'));
10827      slw.appendChild(mkHint(bkT("label_slots_hint")));
10828      /* Banner pattern preferenza orario in base alle prenotazioni passate */
10829      var prefTime=preferredTimeOfDay();
10830      if(prefTime){
10831        var ptB=document.createElement("div");ptB.style.cssText="display:flex;align-items:center;gap:8px;background:#eff6ff;border:1px solid #bfdbfe;border-radius:10px;padding:7px 10px;margin-bottom:10px;font-size:11.5px;color:#1e40af";
10832        ptB.innerHTML='<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg> '+bkT("label_pref_time",'<strong>'+escapeHtml(prefTime==="morning"?bkT("label_morning"):bkT("label_afternoon"))+'</strong>');
10833        slw.appendChild(ptB);
10834      }
10835      if(!self.bkSpazi.length){var emp=document.createElement("div");emp.style.cssText="border-radius:12px;border:1px solid #fde68a;background:#fffbeb;padding:14px;text-align:center;font-size:12px;color:#92400e;animation:caf-fade-up .3s ease-out";emp.innerHTML='<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#92400e" stroke-width="2" style="margin-bottom:4px"><circle cx="12" cy="12" r="10"/><path d="M12 8v4m0 4h.01"/></svg><br>'+escapeHtml(bkT("label_no_slots_h"));slw.appendChild(emp);}
10836      else{
10837        /* group slots by date */
10838        var dayGroups={},dayOrder=[];
10839        self.bkSpazi.forEach(function(s){
10840          var dk=s.data;
10841          if(!dayGroups[dk]){dayGroups[dk]=[];dayOrder.push(dk);}
10842          dayGroups[dk].push(s);
10843        });
10844        var gIdx=0;
10845        dayOrder.forEach(function(dk){
10846          var grp=document.createElement("div");grp.className="caf-bk-day-group";grp.style.animation="caf-fade-up .35s ease-out "+(gIdx*80)+"ms backwards";
10847          /* day header */
10848          var hdr=document.createElement("div");hdr.className="caf-bk-day-hdr";
10849          var dp=fmtDate(dk).split(" ");
10850          var dayN=document.createElement("span");dayN.className="caf-bk-day-name";dayN.textContent=dp.slice(0,2).join(" ");hdr.appendChild(dayN);
10851          var dayD=document.createElement("span");dayD.className="caf-bk-day-date";dayD.textContent=dp.slice(2).join(" ");hdr.appendChild(dayD);
10852          var dayL=document.createElement("div");dayL.className="caf-bk-day-line";hdr.appendChild(dayL);
10853          grp.appendChild(hdr);
10854          /* time chips */
10855          var chips=document.createElement("div");
10855chips.className="caf-bk-day-slots";
10856          dayGroups[dk].forEach(function(s,ci){
10857            var pm=+(s.ora_inizio.slice(0,2))>=13;
10858            var chip=document.createElement("button");chip.className="caf-bk-slot-chip";chip.style.animation="caf-fade-up .3s ease-out "+((gIdx*80)+(ci*30))+"ms backwards";
10859            var tm=document.createElement("span");tm.className="caf-bk-slot-time";tm.textContent=fmtTime(s.ora_inizio);chip.appendChild(tm);
10860            var period=document.createElement("span");period.className="caf-bk-slot-period "+(pm?"pom":"matt");period.textContent=pm?bkT("label_afternoon"):bkT("label_morning");chip.appendChild(period);
10861            addRipple(chip);chip.addEventListener("click",function(){self._bkPickSlot(s);});chips.appendChild(chip);
10862          });
10863          grp.appendChild(chips);slw.appendChild(grp);gIdx++;
10864        });
10865        if(self.bkNextG){var mr=document.createElement("button");mr.className="caf-bk-more";
10866          if(self._bkLoadingMore){mr.disabled=true;mr.style.opacity="0.6";mr.innerHTML='<span class="spin"></span> '+escapeHtml(bkT("label_loading_short"));}
10867          else{mr.innerHTML='<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 5v14m-7-7h14"/></svg> '+escapeHtml(bkT("btn_load_more"));mr.addEventListener("click",function(){self._bkLoadMore();});}
10868          slw.appendChild(mr);}
10869      }
10870      slw.appendChild(mkBot(true));el.appendChild(slw);break;
10871    }
10872    case "collectData":{
10873      var cdw=document.createElement("div");cdw.className=slideDir==="back"?"caf-bk-slide-back":"caf-bk-slide-fwd";
10874      cdw.appendChild(mkStep(4,bkT("t_data")));
10875      cdw.appendChild(mkCrumb(bkT("label_date_at",fmtDate(self.bkSelSlot?self.bkSelSlot.data:""),fmtTime(self.bkSelSlot?self.bkSelSlot.ora_inizio:"")),'<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="4" width="18" height="18" rx="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/></svg>'));
10876      cdw.appendChild(mkHint(bkT("label_data_hint")));
10877      /* Slot reservation timer (UI-only 5 min) */
10878      self._bkRenderSlotTimer(cdw);
10879      /* Toggle "Per chi prenoti?" — se famiglia, swap storage key + clear form */
10880      var forRow=document.createElement("div");forRow.style.cssText="display:flex;gap:6px;background:#f1f5f9;border-radius:10px;padding:3px;margin-bottom:10px";
10881      [["self",bkT("label_form_for_self")],["family",bkT("label_form_for_family")]].forEach(function(pair){
10882        var b=document.createElement("button");b.type="button";b.style.cssText="flex:1;border:none;padding:7px 10px;font-size:11.5px;font-weight:700;border-radius:8px;cursor:pointer;transition:all .2s;font-family:inherit;"+(self.bkBookingFor===pair[0]?"background:#fff;color:"+c+";box-shadow:0 1px 3px rgba(0,0,0,.06)":"background:transparent;color:#5b6573");
10883        b.textContent=pair[1];
10884        /* WCAG 4.1.2 (#C35): stato del toggle esposto alle AT */
10885        b.setAttribute("aria-pressed",self.bkBookingFor===pair[0]?"true":"false");
10886        b.addEventListener("click",function(){
10887          if(self.bkBookingFor===pair[0])return;
10888          self.bkBookingFor=pair[0];
10889          /* #PII — cambia intestatario: ricarica il candidato (non pre-fill cieco)
10890           * e svuota il form; il gate "Sei tu?" chiederà conferma. */
10891          self._bkStoredCandidate=loadStoredClient(pair[0]);
10892          self.bkClient={nome:"",cognome:"",codfis:"",email:"",cellulare:"",sesso:"M"};
10893          self._bkRender();
10894          self._bkLogEvent&&self._bkLogEvent("booking_for_changed",{for:pair[0]});
10895        });
10896        forRow.appendChild(b);
10897      });
10898      cdw.appendChild(forRow);
10899      /* TRUST SIGNALS strip — riduce ansia GDPR/dati personali nel momento di max attrito */
10900      var ts=document.createElement("div");ts.style.cssText="display:flex;gap:8px;align-items:center;justify-content:center;flex-wrap:wrap;font-size:11px;color:#475569;background:#f8fafc;border:1px solid #e2e8f0;border-radius:10px;padding:8px 10px;margin-bottom:12px";
10901      ts.innerHTML='<span style="display:inline-flex;align-items:center;gap:4px">
10901<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="#10b981" stroke-width="2.4" stroke-linecap="round"><rect x="3" y="11" width="18" height="11" rx="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg> '+escapeHtml(bkT("label_data_safe"))+'</span>'+
10902        '<span style="color:#5b6573">·</span>'+
10903        '<span style="display:inline-flex;align-items:center;gap:4px"><svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="#0ea5e9" stroke-width="2.4" stroke-linecap="round"><path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.86 19.86 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.86 19.86 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72c.13.96.37 1.9.7 2.81a2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45c.91.33 1.85.57 2.81.7A2 2 0 0 1 22 16.92Z"/></svg> '+escapeHtml(bkT("label_data_caf"))+'</span>'+
10904        '<span style="color:#5b6573">·</span>'+
10905        '<span style="display:inline-flex;align-items:center;gap:4px"><svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="#f59e0b" stroke-width="2.4" stroke-linecap="round"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/></svg> '+escapeHtml(bkT("label_data_real"))+'</span>';
10906      cdw.appendChild(ts);
10907      var fg=document.createElement("div");fg.className="caf-bk-fg";
10908      var ficons={
10909        nome:'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>',
10910        cognome:'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>',
10911        codfis:'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="2" y="5" width="20" height="14" rx="2"/><line x1="2" y1="10" x2="22" y2="10"/></svg>',
10912        email:'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2Z"/><path d="m22 6-10 7L2 6"/></svg>',
10913        cellulare:'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="5" y="2" width="14" height="20" rx="2"/><line x1="12" y1="18" x2="12.01" y2="18"/></svg>'
10914      };
10915      /* inline validation rules */
10916      var fvalidate={
10917        nome:function(v){return v.trim().length>=2;},
10918        cognome:function(v){return v.trim().length>=2;},
10919        codfis:function(v){return validateCodiceFiscale(v).ok;},
10920        email:function(v){return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v);},
10921        cellulare:function(v){return v.replace(/\D/g,"").length>=8;}
10922      };
10923      var flabels={nome:bkT("label_form_nome"),cognome:bkT("label_form_cognome"),codfis:bkT("label_form_codfis"),email:bkT("label_form_email"),cellulare:bkT("label_form_cellulare")};
10924      var checkSvg='<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><path d="M20 6L9 17l-5-5"/></svg>';
10925      /* inputmode + autocomplete mapping for mobile keyboards */
10926      var fInputMode={nome:"text",cognome:"text",codfis:"text",email:"email",cellulare:"tel"};
10927      var fAutoComp={nome:"given-name",cognome:"family-name",codfis:"off",email:"email",cellulare:"tel"};
10928      var fhelp={
10929        codfis:bkT("help_codfis"),
10930        email:bkT("help_email"),
10931        cellulare:bkT("help_cellulare")
10932      };
10933      /* CF MUST come first: until it's algorithmically valid (struttura + checksum)
10934       * the other fields are disabled e il bottone "Prosegui" non funziona. */
10935      var _wrapsByKey={};
10936      var _inputsByKey={};
10937      var _cfReasonBox=null;
10938      function _isCfOk(){return fvalidate.codfis(self.bkClient.codfis||"");}
10939      function _applyCfGate(){
10940        var ok=_isCfOk();
10941        ["nome","cognome","email","cellulare"].forEach(function(k){
10942          var inp=_inputsByKey[k];var wrap=_wrapsByKey[k];if(!inp||!wrap)return;
10943          inp.disabled=!ok;
10944          if(!ok){
10945            inp.setAttribute("aria-disabled","true");
10946            inp.setAttribute("title",bkT("err_cf_first"));
10947            wrap.style.opacity="0.45";wrap.style.pointerEvents="none";
10948          }else{
10949            inp.removeAttribute("aria-disabled");
10950            inp.removeAttribute("title");
10951            wrap.style.opacity="";wrap.style.pointerEvents="";
10952          }
10953        });
10954        if(_proceedBtn){
10955          /* CF valido E consenso privacy spuntato per proseguire al riepilogo */
10956          var pOk=ok&&!!self.bkPrivacyOk;
10957          _proceedBtn.disabled=!pOk;
10958          _proceedBtn.style.opacity=pOk?"":"0.45";
10959          _proceedBtn.style.cursor=pOk?"":"not-allowed";
10960          _proceedBtn.setAttribute("title",pOk?"":(!ok?bkT("err_cf_first"):bkT("consent_required")));
10961        }
10962      }
10963      var _proceedBtn=null;
10964      [{k:"codfis",p:flabels.codfis+" *",t:"text",mx:16,up:1},{k:"nome",p:flabels.nome+" *",t:"text"},{k:"cognome",p:flabels.cognome+" *",t:"text"},{k:"email",p:flabels.email+" *",t:"email"},{k:"cellulare",p:flabels.cellulare+" *",t:"tel"}].forEach(function(f,i){
10965        /* label con eventuale tooltip "?" inline */
10966        var lblRow=document.createElement("div");lblRow.style.cssText="display:flex;align-items:center;gap:4px";
10967        var lbl=document.createElement("div");lbl.className="caf-bk-inp-label";lbl.style.marginBottom="0";lbl.textContent=flabels[f.k];lblRow.appendChild(lbl);
10968        if(fhelp[f.k]){
10969          var hb=document.createElement("button");hb.type="button";hb.setAttribute("aria-label",bkT("aria_more_info"));
10970          hb.style.cssText="background:none;border:none;color:#5b6573;cursor:pointer;padding:2px;display:inline-flex;border-radius:50%";
10971          hb.innerHTML='<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round"><circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>';
10972          var tip=null;
10973          hb.addEventListener("click",function(){
10974            if(tip){tip.remove();tip=null;return;}
10975            tip=document.createElement("div");tip.style.cssText="font-size:11px;color:#475569;background:#f1f5f9;border-radius:6px;padding:6px 8px;margin-top:4px";tip.textContent=fhelp[f.k];
10976            wrap.parentNode.insertBefore(tip,wrap);
10977            self._bkLogEvent&&self._bkLogEvent("faq_tooltip_clicked",{field:f.k});
10978          });
10979          lblRow.appendChild(hb);
10980        }
10981        fg.appendChild(lblRow);
10982        var wrap=document.createElement("div");wrap.className="caf-bk-inp-wrap";wrap.style.animation="caf-fade-up .3s ease-out "+(i*60)+"ms backwards";
10983        var isValid=fvalidate[f.k]&&fvalidate[f.k](self.bkClient[f.k]);
10984        if(isValid&&self.bkClient[f.k])wrap.className+=" caf-bk-inp-valid";
10985        var icon=document.createElement("div");icon.className="caf-bk-inp-icon";icon.innerHTML=ficons[f.k];wrap.appendChild(icon);
10986        var inp=document.createElement("input");inp.className="caf-bk-inp";inp.type=f.t;inp.placeholder=f.p;inp.value=self.bkClient[f.k];
10987        inp.setAttribute("aria-label",flabels[f.k]);inp.setAttribute("aria-required","true");
10988        inp.setAttribute("autocomplete",fAutoComp[f.k]||"off");
10989        inp.setAttribute("inputmode",fInputMode[f.k]||"text");
10990        inp.setAttribute("enterkeyhint",f.k==="cellulare"?"done":"next");
10991        if(f.mx)inp.maxLength=f.mx;if(f.up)inp.style.textTransform="uppercase";
10992        /* check mark */
10993        var chk=document.createElement("div");chk.className="caf-bk-inp-check";chk.innerHTML=checkSvg;wrap.appendChild(chk);
10994        /* scroll field into view on mobile when keyboard opens */
10995        inp.addEventListener("focus",function(){
10996          setTimeout(function(){if(inp.scrollIntoViewIfNeeded)inp.scrollIntoViewIfNeeded(true);else inp.scrollIntoView({block:"center",behavior:bkScrollBehavior()});},300);
10997        });
10998        inp.addEventListener("input",function(){
10999          var val=f.up?inp.value.toUpperCase():inp.value;
11000          /* Phone real-time formatting: 333 123 4567 / 02 1234 5678 */
11001          if(f.k==="cellulare"){
11002            var formatted=formatPhoneIT(val);
11003            if(formatted!==val){inp.value=formatted;val=formatted;
11004              /* cursor at end (input lost cursor pos on programmatic value change) */
11005              try{inp.setSelectionRange(formatted.length,formatted.length);}catch(_e){}}
11006          }
11007          self.bkClient[f.k]=val;
11008          if(fvalidate[f.k]&&fvalidate[f.k](val)){wrap.classList.add("caf-bk-inp-valid");inp.setAttribute("aria-invalid","false");}
11009          else{wrap.classList.remove("caf-bk-inp-valid");inp.setAttribute("aria-invalid",val?"true":"false");}
11010          /* clear form error on edit */
11011          if(self.bkFormErr){self.bkFormErr=null;var errEl=cdw.querySelector(".caf-bk-ferr");if(errEl)errEl.remove();}
11012          /* clear email typo suggestion if user is editing email */
11013          if(f.k==="email"){var oldSugg=wrap.parentNode&&wrap.parentNode.querySelector(".caf-bk-email-sugg");if(oldSugg)oldSugg.remove();}
11014        });
11015        /* Email typo detection on blur */
11016        if(f.k==="email"){
11017          inp.addEventListener("blur",function(){
11018            var v=inp.value.trim();if(!v||v.indexOf("@")<0)return;
11019            var sugg=suggestEmailDomain(v);if(!sugg)return;
11020            var existing=wrap.parentNode&&wrap.parentNode.querySelector(".caf-bk-email-sugg");if(existing)existing.remove();
11021            var s=document.createElement("div");s.className="caf-bk-email-sugg";
11022            s.style.cssText="font-size:12px;color:#0369a1;background:#f0f9ff;border:1px solid #bae6fd;border-radius:8px;padding:6px 10px;margin-top:4px;cursor:pointer;display:flex;align-items:center;gap:6px";
11023            s.innerHTML='<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><circle cx="12" cy="12" r="10"/><path d="M12 16v-4M12 8h.01"/></svg> '+escapeHtml(bkT("label_did_you_mean"))+' <strong>'+escapeHtml(sugg)+'</strong> <span style="margin-left:auto;text-decoration:underline">'+escapeHtml(bkT("btn_use"))+'</span>';
11024            s.addEventListener("click",function(){inp.value=sugg;self.bkClient.email=sugg;wrap.classList.add("caf-bk-inp-valid");s.remove();self._bkLogEvent&&self._bkLogEvent("email_typo_corrected");});
11025            wrap.parentNode.insertBefore(s,wrap.nextSibling);
11026            self._bkLogEvent&&self._bkLogEvent("email_typo_suggested");
11027          });
11028        }
11029        /* move to next field on Enter (mobile keyboard "next" button) */
11030        inp.addEventListener("keydown",function(e){
11031          if(e.key==="Enter"){e.preventDefault();var allInps=fg.querySelectorAll(".caf-bk-inp");var idx=-1;for(var ai=0;ai<allInps.length;ai++){if(allInps[ai]===inp){idx=ai;break;}}if(idx>=0&&idx<allInps.length-1){allInps[idx+1].focus();}else{self._bkConfirm();}}
11032        });
11033        wrap.appendChild(inp);fg.appendChild(wrap);
11034        _wrapsByKey[f.k]=wrap;_inputsByKey[f.k]=inp;
11035        /* Per il CF: mostro inline il motivo specifico di invali
11035dità (struttura,
11036         * mese, giorno, checksum, ecc.) sotto al campo, e aggancio il gate. */
11037        if(f.k==="codfis"){
11038          _cfReasonBox=document.createElement("div");
11039          _cfReasonBox.className="caf-bk-cf-reason";
11040          var _cfReasonId=quickId();_cfReasonBox.id=_cfReasonId;_cfReasonBox.setAttribute("role","alert");inp.setAttribute("aria-describedby",_cfReasonId);
11041          _cfReasonBox.style.cssText="font-size:11px;line-height:1.35;margin:4px 2px 0;min-height:14px";
11042          fg.appendChild(_cfReasonBox);
11043          function _refreshCfReason(){
11044            var v=self.bkClient.codfis||"";
11045            if(!v){_cfReasonBox.textContent="";_cfReasonBox.style.color="";return;}
11046            var r=validateCodiceFiscale(v);
11047            if(r.ok){_cfReasonBox.textContent="✓ "+bkT("cf_valid_ok");_cfReasonBox.style.color="#047857";}
11048            else{_cfReasonBox.textContent=r.reason;_cfReasonBox.style.color="#be123c";}
11049          }
11050          inp.addEventListener("input",function(){_refreshCfReason();_applyCfGate();});
11051          inp.addEventListener("blur",function(){_refreshCfReason();});
11052          _refreshCfReason();
11053        }
11054      });
11055      _applyCfGate();
11056      cdw.appendChild(fg);
11057      if(self.bkFormErr){var fe=document.createElement("div");fe.className="caf-bk-ferr";fe.setAttribute("role","alert");fe.textContent=self.bkFormErr;cdw.appendChild(fe);}
11058      /* PRIVACY — consenso OBBLIGATORIO (checkbox) allo step Dati: opt-in
11059       * esplicito che gating "Prosegui al riepilogo" insieme al CF valido.
11060       * Sostituisce la vecchia microcopy passiva (GDPR Art. 6/7). */
11061      cdw.appendChild(self._bkBuildConsentBox(function(){_applyCfGate();}));
11062      var cb=document.createElement("button");cb.className="caf-bk-pri caf-bk-cta-sticky";cb.style.cssText="background:linear-gradient(135deg,"+c+","+c+"cc);box-shadow:0 6px 20px "+c+"33";cb.textContent=bkT("btn_next")+" \u203A";cb.addEventListener("click",function(){if(!_isCfOk()){self.bkFormErr=bkT("err_cf_proceed");self._bkRender();return;}if(!self.bkPrivacyOk){self._bkShowToast&&self._bkShowToast({type:"error",msg:bkT("consent_required")});return;}self._bkConfirm();});cdw.appendChild(cb);_proceedBtn=cb;_applyCfGate();
11063      cdw.appendChild(mkBot(true));el.appendChild(cdw);break;
11064    }
11065    case "confirm":{
11066      var cfw=document.createElement("div");cfw.className="caf-bk-slide-fwd";
11067      var tw=document.createElement("div");tw.style.cssText="display:flex;align-items:center;gap:10px;margin-bottom:12px";
11068      tw.innerHTML='<div style="width:32px;height:32px;border-radius:50%;background:linear-gradient(135deg,'+c+'20,'+c+'10);display:flex;align-items:center;justify-content:center"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="'+c+'" stroke-width="2.5"><path d="M9 12l2 2 4-4"/><circle cx="12" cy="12" r="10"/></svg></div>';
11069      var ct=document.createElement("span");ct.className="caf-bk-title";ct.style.cssText="margin-bottom:0;font-size:15px";ct.textContent=bkT("btn_confirm");tw.appendChild(ct);cfw.appendChild(tw);
11070      /* Slot reservation timer */
11071      self._bkRenderSlotTimer(cfw);
11072      var sum=document.createElement("div");sum.className="caf-bk-sum";sum.style.animation="caf-fade-up .35s ease-out";
11073      var sp=self.bkSelSlot||{},sd=self.bkSelSede||{},sv=self.bkSelServ||{},cd=self.bkClient;
11074      sum.innerHTML=
11075        '<div style="font-size:9px;text-transform:uppercase;letter-spacing:1px;color:#5b6573;font-weight:600;margin-bottom:6px">'+escapeHtml(bkT("label_appointment"))+'</div>'+
11076        '<div class="caf-bk-sr"><div class="caf-bk-si"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="4" width="18" height="18" rx="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/></svg></div><span class="caf-bk-sv">'+escapeHtml(fmtDate(sp.data||""))+"  "+escapeHtml(fmtTime(sp.ora_inizio||""))+" - "+escapeHtml(fmtTime(sp.ora_fine||""))+"</span></div>"+
11077        '<div class="caf-bk-sr"><div class="caf-bk-si"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M20 10c0 6-8 12-8 12s-8-6-8-12a8 8 0 0 1 16 0Z"/><circle cx="12" cy="10" r="3"/></svg></div><div><span class="caf-bk-sv">'+escapeHtml(sd.descrizione||"")+'</span><span class="caf-bk-ss">'+escapeHtml((sd.indirizzo||"")+", "+(sd.citta||""))+"</span></div></div>"+
11078        '<div class="caf-bk-sr"><div class="caf-bk-si"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8Z"/><path d="M14 2v6h6"/></svg></div><span class="caf-bk-sv">'+escapeHtml(sv.desonline||sv.descrizione||"")+"</span></div>"+
11079        "<hr>"+
11080        '<div style="font-size:9px;text-transform:uppercase;letter-spacing:1px;color:#5b6573;font-weight:600;margin-bottom:6px">'+escapeHtml(bkT("t_data"))+'</div>'+
11081        '<div class="caf-bk-sr"><div class="caf-bk-si"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg></div><div><span class="caf-bk-sv">'+escapeHtml(cd.nome+" "+cd.cognome)+'</span><span class="caf-bk-ss">'+escapeHtml(bkT("label_form_codfis"))+': '+escapeHtml(cd.codfis)+"</span></div></div>"+
11082        '<div class="caf-bk-sr"><div class="caf-bk-si"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2Z"/><path d="m22 6-10 7L2 6"/></svg></div><span class="caf-bk-sv">'+escapeHtml(cd.email)+"</span></div>"+
11083        '<div class="caf-bk-sr"><div class="caf-bk-si"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="5" y="2" width="14" height="20" rx="2"/>
11083<line x1="12" y1="18" x2="12.01" y2="18"/></svg></div><span class="caf-bk-sv">'+escapeHtml(cd.cellulare)+"</span></div>";
11084      cfw.appendChild(sum);
11085      var br=document.createElement("div");br.style.cssText="display:flex;gap:8px;margin-top:12px";
11086      var sb=document.createElement("button");sb.className="caf-bk-pri caf-bk-cta-sticky";sb.style.cssText="flex:1;margin-top:0;background:linear-gradient(135deg,"+c+","+c+"cc);box-shadow:0 6px 24px "+c+"33";sb.innerHTML='<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2.5" style="flex-shrink:0"><path d="M20 6L9 17l-5-5"/></svg> '+escapeHtml(bkT("btn_confirm"));sb.addEventListener("click",function(){if(!self.bkPrivacyOk){self._bkShowToast&&self._bkShowToast({type:"error",msg:bkT("consent_required")});return;}self._bkSubmit();});br.appendChild(sb);
11087      var eb=document.createElement("button");eb.className="caf-bk-act2";eb.style.padding="13px 16px";eb.innerHTML='<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/></svg> '+escapeHtml(bkT("btn_edit"));eb.addEventListener("click",function(){self._bkBack();});br.appendChild(eb);
11088      /* PRIVACY — checkbox di consenso obbligatoria: gate del bottone Conferma */
11089      var applyConsentGate=function(ok){sb.disabled=!ok;sb.style.opacity=ok?"":"0.5";sb.style.cursor=ok?"":"not-allowed";sb.style.filter=ok?"":"grayscale(0.35)";sb.title=ok?"":bkT("consent_required");};
11090      if(!self.bkPrivacyOk){cfw.appendChild(self._bkBuildConsentBox(applyConsentGate));}
11091      applyConsentGate(!!self.bkPrivacyOk);
11092      cfw.appendChild(br);
11093      cfw.appendChild(mkBot(false));el.appendChild(cfw);break;
11094    }
11095    case "done":{
11096      var di=document.createElement("div");di.className="caf-bk-done-ic";di.style.cssText="background:linear-gradient(135deg,"+c+","+c+"b3);box-shadow:0 8px 24px "+c+"44";
11097      di.innerHTML='<svg width="26" height="26" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2.5" stroke-linecap="round"><path d="M20 6L9 17l-5-5"/></svg>';
11098      /* confetti particles */
11099      var confColors=[c,"#f59e0b","#8b5cf6","#ef4444","#06b6d4","#10b981"];
11100      for(var ci=0;ci<12;ci++){var cf=document.createElement("div");cf.className="caf-bk-confetti";cf.style.cssText="background:"+confColors[ci%confColors.length]+";left:"+(20+Math.random()*60)+"%;"+"top:"+(20+Math.random()*60)+"%;"+"animation-delay:"+(ci*80)+"ms;"+"animation-duration:"+(800+Math.random()*400)+"ms;"+"width:"+(4+Math.random()*4)+"px;height:"+(4+Math.random()*4)+"px;"+"border-radius:"+(Math.random()>.5?"50%":"2px");di.appendChild(cf);}
11101      el.appendChild(di);
11102      var dt=document.createElement("div");dt.className="caf-bk-done-t";dt.textContent="\uD83C\uDF89 "+bkT("label_done_h");el.appendChild(dt);
11103      if(self.bkId){var bid=document.createElement("div");bid.className="caf-bk-done-id";bid.style.cssText="background:"+c+"15;color:"+c;bid.textContent=bkT("label_done_id")+self.bkId;el.appendChild(bid);}
11104      var ds=document.createElement("div");ds.className="caf-bk-sum";ds.style.marginTop="10px";var dsp=self.bkSelSlot||{};
11105      ds.innerHTML='<div class="caf-bk-sr"><div class="caf-bk-si">
11105<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="4" width="18" height="18" rx="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/></svg></div><span class="caf-bk-sv">'+escapeHtml(fmtDate(dsp.data||""))+" | "+escapeHtml(fmtTime(dsp.ora_inizio||""))+" - "+escapeHtml(fmtTime(dsp.ora_fine||""))+"</span></div>"+
11106        '<div class="caf-bk-sr"><div class="caf-bk-si"><svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M20 10c0 6-8 12-8 12s-8-6-8-12a8 8 0 0 1 16 0Z"/><circle cx="12" cy="10" r="3"/></svg></div><span>'+escapeHtml((self.bkSelSede||{}).descrizione||"")+"</span></div>"+
11107        '<div class="caf-bk-sr"><div class="caf-bk-si"><svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8Z"/><path d="M14 2v6h6"/></svg></div><span>'+escapeHtml((self.bkSelServ||{}).desonline||(self.bkSelServ||{}).descrizione||"")+"</span></div>";
11108      el.appendChild(ds);
11109      var en=document.createElement("div");en.className="caf-bk-email";en.innerHTML='<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="flex-shrink:0"><path d="M9 12l2 2 4-4"/><circle cx="12" cy="12" r="10"/></svg> '+bkT("label_done_email","<strong>"+escapeHtml(self.bkClient.email)+"</strong>");el.appendChild(en);
11110      /* Azioni post-conferma: ICS + WhatsApp share = riduce no-show */
11111      var actGrid=document.createElement("div");actGrid.style.cssText="display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-top:12px";
11112      var icsBtn=document.createElement("button");icsBtn.className="caf-bk-act2";icsBtn.style.cssText="justify-content:center";
11113      icsBtn.innerHTML='<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="4" width="18" height="18" rx="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/><path d="M12 14v4M10 16h4"/></svg> '+escapeHtml(bkT("btn_calendar"));
11114      icsBtn.addEventListener("click",function(){
11115        var ics=buildIcsForBooking({bookingId:self.bkId,sede:self.bkSelSede,servizio:self.bkSelServ,slot:self.bkSelSlot});
11116        if(ics)downloadIcs(ics,"caf-cisl-"+(self.bkId||"appuntamento")+".ics");
11117        self._bkLogEvent&&self._bkLogEvent("ics_downloaded");
11118      });
11119      actGrid.appendChild(icsBtn);
11120      var waBtn=document.createElement("button");waBtn.className="caf-bk-act2";waBtn.style.cssText="justify-content:center";
11121      waBtn.innerHTML='<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z"/></svg> WhatsApp';
11122      waBtn.addEventListener("click",function(){
11123        var sp=self.bkSelSlot||{};var sd=self.bkSelSede||{};var sv=self.bkSelServ||{};
11124        var msg="Ho prenotato un appuntamento al CAF CISL: "+(sv.descrizione||"")+" il "+fmtDate(sp.data||"")+" alle "+fmtTime(sp.ora_inizio||"")+" presso "+(sd.descrizione||"")+(sd.indirizzo?" ("+sd.indirizzo+(sd.citta?", "+sd.citta:"")+")":"")+(self.bkId?" — ID: "+self.bkId:"");
11125        window.open(whatsappShareUrl(msg),"_blank","noopener");
11126        self._bkLogEvent&&self._bkLogEvent("whatsapp_shared");
11127      });
11128      actGrid.appendChild(waBtn);
11129      el.appendChild(actGrid);
11130      /* Mailto reminder secondario: utile a chi non usa WhatsApp/calendario */
11131      var mlBtn=document.createElement("a");mlBtn.className="caf-bk-act2";
11132      mlBtn.style.cssText="margin-top:8px;width:100%;justify-content:center;display:flex;align-items:center;gap:6px;text-decoration:none;text-align:center;cursor:pointer";
11133      mlBtn.href=mailtoReminder({email:self.bkClient.email,bookingId:self.bkId,sede:self.bkSelSede,servizio:self.bkSelServ,slot:self.bkSelSlot});
11134      mlBtn.innerHTML='<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2Z"/><path d="m22 6-10 7L2 6"/></svg> '+escapeHtml(bkT("btn_email_reminder"));
11135      mlBtn.addEventListener("click",function(){self._bkLogEvent&&self._bkLogEvent("mailto_clicked");});
11136      el.appendChild(mlBtn);
11137      /* Mini-survey emoji: misura NPS post-prenotazione senza form */
11138      var srvWrap=document.createElement("div");srvWrap.style.cssText="margin-top:14px;padding:10px 12px;background:#f8fafc;border:1px solid #e2e8f0;border-radius:10px;text-align:center";
11139      var srvLbl=document.createElement("div");srvLbl.style.cssText="font-size:11.5px;color:#5b6573;margin-bottom:6px";srvLbl.textContent=bkT("label_done_survey");srvWrap.appendChild(srvLbl);
11140      var srvRow=document.createElement("div");srvRow.style.cssText="display:flex;justify-content:center;gap:14px";
11141      [["happy","\uD83D\uDE0A",bkT("label_done_good")],["neutral","\uD83D\uDE10",bkT("label_done_meh")],["sad","\uD83D\uDE1E",bkT("label_done_bad")]].forEac
11141h(function(item){
11142        var b=document.createElement("button");b.type="button";b.style.cssText="background:none;border:none;font-size:22px;cursor:pointer;padding:4px 6px;border-radius:8px;transition:background .2s;line-height:1";
11143        b.title=item[2];b.textContent=item[1];
11144        b.addEventListener("click",function(){
11145          self._bkLogEvent&&self._bkLogEvent("post_booking_survey",{rating:item[0]});
11146          srvRow.innerHTML='<div style="font-size:11.5px;color:#047857;font-weight:600;padding:6px 0">'+escapeHtml(bkT("label_done_thanks"))+'</div>';
11147        });
11148        b.addEventListener("mouseenter",function(){b.style.background="#e2e8f0";});
11149        b.addEventListener("mouseleave",function(){b.style.background="";});
11150        srvRow.appendChild(b);
11151      });
11152      srvWrap.appendChild(srvRow);
11153      el.appendChild(srvWrap);
11154      var dc=document.createElement("button");dc.className="caf-bk-act2";dc.style.cssText="margin-top:8px;width:100%;justify-content:center";dc.textContent=bkT("btn_close");dc.addEventListener("click",function(){self._bkReset();});el.appendChild(dc);
11155      break;
11156    }
11157    case "error":{
11158      var isRec=self.bkErrReturn!==null,isAB=self.bkErrCode==="already_booked",isST=self.bkErrCode==="slot_taken",isNR=["no_services","no_sedi","no_spazi"].indexOf(self.bkErrCode)!==-1,isCE=["network","timeout"].indexOf(self.bkErrCode)!==-1;
11159      var eCol=isNR?"#f59e0b":"#f43f5e",eBg=isNR?"#fef3c7":"#ffe4e6";
11160      var ew=document.createElement("div");
11160ew.className="caf-bk-ew";
11161      var ei=document.createElement("div");ei.className="caf-bk-ei";ei.style.background=eBg;ei.innerHTML='<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="'+eCol+'" stroke-width="2" stroke-linecap="round"><circle cx="12" cy="12" r="10"/><path d="M12 8v4m0 4h.01"/></svg>';ew.appendChild(ei);
11162      var etd=document.createElement("div");var ett=document.createElement("div");ett.className="caf-bk-et";ett.style.color=eCol;ett.textContent=isAB?bkT("err_h_already_booked"):isST?bkT("err_h_slot_taken"):isNR?bkT("sr_no_results"):isCE?bkT("err_h_network"):bkT("err_h_default");etd.appendChild(ett);
11163      var emm=document.createElement("div");emm.className="caf-bk-em";emm.textContent=self.bkErrMsg;etd.appendChild(emm);ew.appendChild(etd);el.appendChild(ew);
11164      var acts=document.createElement("div");acts.className="caf-bk-acts";
11165      if(isRec){var bb=document.createElement("button");bb.className="caf-bk-act";bb.textContent="\u2190 "+(isAB?bkT("btn_choose_other"):isST?bkT("btn_choose_other_slot"):isNR?bkT("btn_back"):bkT("btn_retry"));bb.addEventListener("click",function(){self._bkBack();});acts.appendChild(bb);}
11166      if(isCE||(!isRec&&!isCE)){var rb=document.createElement("button");rb.className="caf-bk-act";rb.textContent="\u21BB "+(isCE?bkT("btn_retry"):bkT("btn_restart"));rb.addEventListener("click",function(){self._bkStart();});acts.appendChild(rb);}
11167      var cb2=document.createElement("button");cb2.className="caf-bk-act2";cb2.textContent=bkT("btn_close");cb2.addEventListener("click",function(){self._bkReset();});acts.appendChild(cb2);el.appendChild(acts);
11168      /* WCAG 4.1.3 (#C3): errore annunciato anche nel wizard inline */
11169      self._bkFsAnnounce(bkT("label_error")+": "+ett.textContent+(self.bkErrMsg?". "+self.bkErrMsg:""));
11170      break;
11171    }}
11172    this._scrollToBottom();
11173    /* Voice mode: announce booking step via TTS so user can interact by voice */
11174    if (this._voiceMode) {
11175      var _voiceSteps = ["selectProvincia","selectServizio","selectSede","selectSlot","collectData","confirm","done","error"];
11176      if (_voiceSteps.indexOf(this.bkStep) !== -1) {
11177        this._bkVoiceAnnounce();
11178      }
11179    }
11180  };
11181
11182  /* ── prominent "Espandi prenotazione" banner (under header, outside conversation) ── */
11183  CafChat.prototype._updateBkExpandBanner = function () {
11184    if (!this._bkExpandBannerEl) return;
11185    var show = this.isOpen
11186      && this.bkStep && this.bkStep !== "idle"
11187      && this.bkStep !== "done"
11188      && this.bkStep !== "error"
11189      && !this._bkFsMode
11190      && !this._voiceMode;
11191    if (show) this._bkExpandBannerEl.classList.add("active");
11192    else this._bkExpandBannerEl.classList.remove("active");
11193  };
11194
11195  /* ── persistent booking CTA ─────────────────────────────── */
11196
11197  /* parse user messages + service keywords to craft dynamic CTA text */
11198  CafChat.prototype._getBkCtaContext = function () {
11199    /* SOLO servizi gestiti dal CAF. NASPI, Assegno Unico (INPS), pensioni
11200     * e altri servizi di patronato NON appaiono qui: il CTA banner / le
11201     * ghost suggestions non devono mai mostrare "Prenota NASPI" ecc.
11202     * Pattern più specifici PRIMA dei più generici (primo match vince). */
11203    var services = [
11204      {k:/\b730\b/i, name:"730"},
11205      {k:/\bise[ue]\b/i, name:"ISEE"},
11206      {k:/\bdsu\b/i, name:"DSU"},
11207      {k:/\bcud\b/i, name:"CUD"},
11208      {k:/\bsuccessi[oò]n/i, name:"Successione"},
11209      {k:/\b(colf|badant\w*)\b/i, name:"Colf e Badanti"},
11210      {k:/\b(imu|tasi|tari)\b/i, name:"IMU/TASI/TARI"},
11211      {k:/\bmodello\s+redditi\b|\bunico\b/i, name:"Modello REDDITI"},
11212      {k:/\bmodello\s+red\b|\bred\b/i, name:"Modello RED"},
11213      {k:/\binvciv\b/i, name:"INVCIV"},
11214      {k:/\baccas\/?ps\b|\bassegno\s+sociale\b/i, name:"ACCAS/PS"},
11215      {k:/\bcedolare\s+secca\b|\blocazion[ei]\b/i, name:"Locazione"},
11216      {k:/\bdelega\s+unica\b/i, name:"Delega Unica"},
11217      {k:/\bvisur[ae]\s+catastal[ei]\b|\bvisura\b/i, name:"Visura Catastale"},
11218      {k:/\bpartita\s+iva\b|\bsapi\b/i, name:"Partita IVA"},
11219      {k:/\bdichiarazion/i, name:"dichiarazione"},
11220      {k:/\bbonus\b/i, name:"bonus"}
11221    ];
11222    /* scan last few user messages */
11223    var recent = this.messages.slice(-6).filter(function(m){return m.role==="user";});
11224    var foundSvc = null, foundLoc = null;
11225    var provinceNames = ["firenze","prato","pistoia","lucca","pisa","livorno","siena","arezzo","grosseto","massa","carrara","roma","milano","bologna","torino","napoli","bari","palermo","catania","venezia","verona","padova","genova"];
11226    for (var i = recent.length - 1; i >= 0; i--) {
11227      var t = recent[i].content || "";
11228      if (!foundSvc) {
11229        for (var j = 0; j < services.length; j++) {
11230          if (services[j].k.test(t)) { foundSvc = services[j].name; break; }
11231        }
11232      }
11233      if (!foundLoc) {
11234        var tl = t.toLowerCase();
11235        for (var k2 = 0; k2 < provinceNames.length; k2++) {
11236          if (tl.indexOf(provinceNames[k2]) !== -1) { foundLoc = provinceNames[k2].charAt(0).toUpperCase()+provinceNames[k2].slice(1); break; }
11237        }
11238      }
11239      if (foundSvc && foundLoc) break;
11240    }
11241    /* count non-booking assistant replies to decide proactive state */
11242    var botReplies = this.messages.filter(function(m){return m.role==="assistant"&&m.id!=="welcome"&&m.content&&m.content!=="@@BOOKING@@"&&!(typeof m.content==="string"&&m.content.indexOf("@@BOOKING_LAUNCH@@")===0);}).length;
11243    return { service: foundSvc, location: foundLoc, botReplies: botReplies };
11244  };
11245
11246  CafChat.prototype._renderBkCtaContent = function () {
11247    if (!this._bkCtaEl) return;
11248    var self = this;
11249    var ctx = this._getBkCtaContext();
11250    var proactive = ctx.botReplies >= 3;
11251    var title = "Prenota il tuo appuntamento";
11252    var sub = "⏱ ~2 minuti · Veloce, online, in pochi click";
11253    var btnLbl = "Prenota ora";
11254    if (ctx.service && ctx.location) {
11255      title = "Prenota " + ctx.service + " a " + ctx.location;
11256      sub = "Ho trovato il servizio per te, ti porto alla scelta dell'orario";
11257      btnLbl = "Prenota";
11258    } else if (ctx.service) {
11259      title = "Prenota il servizio " + ctx.service;
11260      sub = "Scegli sede e orario in pochi secondi";
11261      btnLbl = "Prenota " + ctx.service;
11262    } else if (ctx.location) {
11263      title = "Prenota a " + ctx.location;
11264      sub = "Vedi gli slot disponibili nella tua zona";
11265      btnLbl = "Prenota ora";
11266    } else if (proactive) {
11267      title = "Hai bisogno di un appuntamento?";
11268      sub = "Posso aiutarti a prenotare il servizio CAF che ti serve";
11269      btnLbl = "Prenota adesso";
11270    }
11271    this._bkCtaEl.classList.toggle("proactive", proactive);
11272    var badge = '<span class="caf-bk-cta-badge"><span class="d"></span>Disponibili</span>';
11273    this._bkCtaEl.innerHTML =
11274      '<button type="button" class="caf-bk-cta-dismiss" aria-label="' + escapeHtml(bkT("aria_hide")) + '">' +
11275        '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>' +
11276      '</button>' +
11277      '<div class="caf-bk-cta-ic">' +
11278        '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round">' +
11279          '<rect x="3" y="4" width="18" height="18" rx="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/><path d="M9 16l2 2 4-4"/>' +
11280        '</svg>' +
11281      '</div>' +
11282      '<div class="caf-bk-cta-txt">' +
11283        '<div class="caf-bk-cta-t">' + escapeHtml(title) + badge + '</div>' +
11284        '<div class="caf-bk-cta-s">' + escapeHtml(sub) + '</div>' +
11285      '</div>' +
11286      '<button type="button" class="caf-bk-cta-btn">' + escapeHtml(btnLbl) +
11287        ' <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><polyline points="9 18 15 12 9 6"/></svg>' +
11288      '</button>';
11289    /* wire events (innerHTML wipes listeners) */
11290    var activate = function () {
11291      if (self._bkFsMode) return;
11292      if (self.bkStep !== "idle") return; /* CTA is hidden in this case anyway */
11293      self._startBookingFromCta();
11294    };
11295    this._bkCtaEl.onclick = activate;
11296    var primaryBtn = this._bkCtaEl.querySelector(".caf-bk-cta-btn");
11297    if (primaryBtn) {
11298      primaryBtn.addEventListener("click", function (e) {
11299        e.stopPropagation();
11300        activate();
11301      });
11302    }
11303    var dismissBtn = this._bkCtaEl.querySelector(".caf-bk-cta-dismiss");
11304    if (dismissBtn) {
11305      dismissBtn.addEventListener("click", function (e) {
11306        e.stopPropagation();
11307        self._bkCtaDismissed = true;
11308        self._updateBkCta();
11309      });
11310    }
11311  };
11312
11313  CafChat.prototype._updateBkCta = function () {
11314    if (!this._bkCtaEl) return;
11315    var baseOK = this._bkEnabled === true
11316      && this.bkStep === "idle"
11317      && !this._bkFsMode
11318      && !this._voiceMode
11319      && this.isOpen
11320      && !this._needsEmail()
11321      && !this._needsConsent();
11322    var showFull = baseOK && !this._bkCtaDismissed;
11323    var showMini = baseOK && this._bkCtaDismissed;
11324    if (showFull) this._renderBkCtaContent();
11325    this._bkCtaEl.style.display = showFull ? "flex" : "none";
11326    if (this._bkCtaMiniEl) this._bkCtaMiniEl.style.display = showMini ? "inline-flex" : "none";
11327    /* update ghost suggestions visibility */
11328    this._updateGhostSug();
11329  };
11330
11331  /* ── ghost suggestion chips (suggest a booking after idle) ─────── */
11332  CafChat.prototype._updateGhostSug = function () {
11333    if (!this._ghostSugEl) return;
11334    var show = this._bkEnabled === true
11335      && this.bkStep === "idle"
11336      && !this._bkFsMode
11337      && !this._voiceMode
11338      && this.isOpen
11339      && !this._needsEmail()
11340      && !this._needsConsent()
11341      && !!this._ghostSugShouldShow;
11342    if (!show) { this._ghostSugEl.style.display = "none"; return; }
11343    var self = this;
11344    var ctx = this._getBkCtaContext();
11345    var suggestions;
11346    if (ctx.service) {
11347      suggestions = [
11348        "Vorrei prenotare il " + ctx.service,
11349        "Quali documenti servono per " + ctx.service + "?",
11350        "Ho bisogno di un appuntamento"
11351      ];
11352    } else {
11353      suggestions = [
11354        "Vorrei prenotare un appuntamento",
11355        "Devo fare il 730",
11356        "Ho bisogno dell'ISEE"
11357      ];
11358    }
11359    this._ghostSugEl.innerHTML = "";
11360    suggestions.forEach(function (s) {
11361      var chip = document.createElement("button");
11362      chip.type = "button";
11363      chip.className = "caf-ghost-sug-chip";
11364      chip.innerHTML = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"><polyline points="9 18 15 12 9 6"/></svg>
11364' + escapeHtml(s);
11365      chip.addEventListener("click", function () {
11366        self.inputEl.value = s;
11367        self._ghostSugShouldShow = false;
11368        self._updateGhostSug();
11369        self._send(s);
11370      });
11371      self._ghostSugEl.appendChild(chip);
11372    });
11373    this._ghostSugEl.style.display = "flex";
11374  };
11375
11376  /* schedule ghost suggestions after input idle */
11377  CafChat.prototype._scheduleGhostSug = function () {
11378    var self = this;
11379    if (this._ghostSugTimer) { clearTimeout(this._ghostSugTimer); this._ghostSugTimer = null; }
11380    this._ghostSugShouldShow = false;
11381    this._updateGhostSug();
11382    this._ghostSugTimer = setTimeout(function () {
11383      if (!self.inputEl || self.inputEl.value.trim().length > 0) return;
11384      if (!self.isOpen || self._voiceMode || self._bkFsMode) return;
11385      if (self.bkStep !== "idle") return;
11386      self._ghostSugShouldShow = true;
11387      self._updateGhostSug();
11388    }, 15000);
11389  };
11390
11391  CafChat.prototype._startBookingFromCta = function () {
11392    var self = this;
11393    /* gate consenso privacy: nessuna prenotazione (e nessun messaggio
11394       persistito) prima dell'approvazione. open() mostra il gate. */
11395    if (self._needsConsent()) { self.open(); return; }
11396    if (self.bkStep !== "idle") return;
11397    /* inject placeholder booking message (same as intent-triggered flow) */
11398    var um = {id: quickId(), role: "user", content: "Vorrei prenotare un appuntamento", timestamp: Date.now()};
11399    self.messages.push(um);
11400    var bk = {id: quickId(), role: "assistant", content: "@@BOOKING@@", timestamp: Date.now()};
11401    self.messages.push(bk);
11402    self._renderMessages();
11403    self._saveMessages();
11404    /* start booking flow inline (user can click Espandi to go fullscreen manually) */
11405    self._bkStart();
11406    self._updateBkCta();
11407  };
11408
11409  /* ── Public entry: apri il pannello e porta dritto al wizard di
11410   * prenotazione. Se una prenotazione è già in corso (bkStep != "idle")
11411   * NON la resetta: riapre il pannello e rientra in fullscreen dove l'utente
11412   * era rimasto. Altrimenti avvia il flusso standard via _startBookingFromCta. */
11413  CafChat.prototype.openBooking = function () {
11414    this.open();
11415    /* gate consenso privacy pendente: open() ha appena mostrato il gate,
11416       il wizard NON deve partire (ne' visibile ne' nascosto sotto
11417       l'overlay). L'utente approva e poi riprova. */
11418    if (this._needsConsent()) return;
11419    if (this.bkStep && this.bkStep !== "idle") {
11420      if (!this._bkFsMode) { try { this._bkEnterFs(); } catch (_e) {} }
11421    } else {
11422      this._startBookingFromCta();
11423    }
11424  };
11425
11426  /* ══════════════════════════════════════════════════════════════
11427     ██  FULLSCREEN BOOKING – premium expanded layout
11428     ══════════════════════════════════════════════════════════════ */
11429
11430  /* A11y (WCAG 4.1.3): annuncia txt nella live region DENTRO l'overlay
11431   * fullscreen (con aria-modal le AT possono sopprimere gli annunci da nodi
11432   * fuori dal dialog). Fallback: srAnnouncer del pannello chat quando
11433   * l'overlay non esiste (wizard inline). Il piccolo delay lascia alle AT il
11434   * tempo di registrare la region appena ri-appesa dal re-render prima che
11435   * il testo cambi (altrimenti l'annuncio andrebbe perso). */
11436  CafChat.prototype._bkFsAnnounce = function(txt){
11437    var self=this;
11438    setTimeout(function(){
11439      try{
11440        var t=(self._bkFsMode&&self._bkFsAnnouncer)?self._bkFsAnnouncer:self.srAnnouncer;
11441        if(t)t.textContent=txt;
11442      }catch(_e){}
11443    },80);
11444  };
11445
11446  CafChat.prototype._bkEnterFs = function(){
11447    if(this._bkFsMode)return;
11448    this._bkFsMode=true;
11449    if(this._updateBkExpandBanner)this._updateBkExpandBanner();
11450    /* auto enter desktop fullscreen for best experience */
11451    this._bkFsWasFs=this.isFullscreen;
11452    if(!this.isFullscreen&&window.innerWidth>=640&&typeof this._enterFullscreen==="function"){
11453      try{this._enterFullscreen();}catch(e){}
11454    }
11455    /* create overlay element inside panel */
11456    var ov=document.createElement("div");ov.className="caf-bk-fs";ov.setAttribute("role","dialog");ov.setAttribute("aria-modal","true");ov.setAttribute("aria-label",bkT("aria_booking"));
11457    this._bkFsEl=ov;
11458    this.panelEl.appendChild(ov);
11459    /* A11y (WCAG 4.1.3): live region dentro il dialog aria-modal per gli
11460     * annunci del wizard (errori, conferma, esiti ricerca). */
11461    var fsAnn=document.createElement("div");fsAnn.className="caf-sr-only";
11462    fsAnn.setAttribute("aria-live","polite");fsAnn.setAttribute("aria-atomic","true");
11463    this._bkFsAnnouncer=fsAnn;ov.appendChild(fsAnn);
11464    /* ESC to close */
11465    var self=this;
11466    this._bkFsEscHandler=function(e){if(e.key==="Escape"){
11467      /* Durante l'invio della prenotazione (non idempotente, no auto-retry) NON
11468       * permettiamo l'uscita con Esc: interromperebbe lo stato mentre la POST
11469       * "prenota" è in volo → prenotazione creata e persa lato client + storico
11470       * incoerente. (Nello step submitting non c'è nemmeno il bottone Annulla.) */
11471      if(self.bkStep==="submitting")return;
11472      /* 1.4.13: se c'e' un popover transitorio aperto (docs/prefisso tel),
11473       * Escape chiude prima quello (lo fa il handler del pannello): non
11474       * uscire dal wizard nello stesso keypress. */
11475      var _fsPops=self.shadow&&self.shadow.querySelectorAll(".caf-lang-picker,.caf-bk-docs-pop,.caf-bk-tel-cc-pop");
11476      if(_fsPops&&_fsPops.length)return;
11477      e.preventDefault();self._bkExitFs();}};
11478    document.addEventListener("keydown",this._bkFsEscHandler);
11479    /* lock body scroll */
11480    this._bkFsPrevOverflow=document.body.style.overflow;
11481    document.body.style.overflow="hidden";
11482    this._bkFsPrevFocus=(this.shadow&&this.shadow.activeElement)||document.activeElement;
11483    this._bkRenderFs();
11484    try{var _fsClose=ov.querySelector(".caf-bk-fs-hclose");if(_fsClose)_fsClose.focus();}catch(_e){}
11485    this._trapTab(ov);
11486  };
11487
11488  CafChat.prototype._bkExitFs = function(silent){
11489    if(!this._bkFsMode)return;
11490    this._bkFsMode=false;
11491    if(this._updateBkExpandBanner)this._updateBkExpandBanner();
11492    if(this._bkFsEl&&this._bkFsEl.parentNode){this._bkFsEl.parentNode.removeChild(this._bkFsEl);}
11493    this._bkFsEl=null;this._bkFsAnnouncer=null;
11494    if(this._bkFsEscHandler){document.removeEventListener("keydown",this._bkFsEscHandler);this._bkFsEscHandler=null;}
11495    if(this._bkFsPrevOverflow!==undefined){document.body.style.overflow=this._bkFsPrevOverflow;this._bkFsPrevOverflow=undefined;}
11496    try{if(this._bkFsPrevFocus&&this._bkFsPrevFocus.focus){this._bkFsPrevFocus.focus();}else if(this.inputEl){this.inputEl.focus();}}catch(_e){}
11497    this._bkFsPrevFocus=null;
11498    /* if we auto-entered panel fullscreen, auto-exit as well */
11499    if(!this._bkFsWasFs&&this.isFullscreen&&typeof this._exitFullscreen==="function"){
11500      try{this._exitFullscreen();}catch(e){}
11501    }
11502    this._bkFsWasFs=false;
11503    /* La prenotazione è SEMPRE fullscreen: se l'utente esce a metà flusso
11504     * (non da done/error e non programmaticamente), resetta la prenotazione
11505     * così la chat resta pulita e si può ripartire da zero. */
11506    /* BUGFIX (input chat morto): "error" era escluso dal reset → uscendo da
11507     * un wizard in errore bkStep restava "error", _updateSendBtn continuava a
11508     * bloccare l'input e non si poteva piu' scrivere. Uscire da un errore =
11509     * abbandonare (nessuna POST in volo): si resetta come per gli altri step. */
11510    if(!silent&&this.bkStep&&this.bkStep!=="idle"&&this.bkStep!=="done"){
11511      try{this._bkLogEvent&&this._bkLogEvent("fullscreen_exit_aborted",{step:this.bkStep});}catch(_e){}
11512      this._bkReset(true);
11513    }
11514    /* Riallinea SEMPRE input/placeholder/bottoni allo stato post-uscita:
11515     * senza questa chiamata l'input restava congelato (disabled) sullo
11516     * stato del wizard anche dopo il reset. */
11517    try{this._updateSendBtn();}catch(_e){}
11518  };
11519
11520  /* render premium fullscreen booking layout.
11521   * Wrappa in View Transitions API SOLO sui cambi di step (non su intra-step
11522   * re-render come cambio gender/email-typo) per evitare overhead inutile e
11523   * conflitti con le animazioni `caf-bk-fs-stage` esistenti. Browser senza
11524   * supporto: fallback diretto. */
11525  /* A11y (WCAG 2.4.3): ogni render fullscreen fa innerHTML="" e distrugge
11526   * l'elemento con il focus → il browser lo spostava su body, FUORI dal
11527   * dialog aria-modal, a OGNI passaggio di step (verificato in CDP). Dopo il
11528   * render, se il focus era dentro l'overlay ed e' andato perso: su cambio
11529   * step va al titolo del nuovo step (tabindex=-1, annuncia il contesto allo
11530   * screen reader); su re-render intra-step prova a rifocalizzare l'elemento
11531   * equivalente (stesso tag+classe, es. l'input di ricerca sedi), poi ripiega
11532   * sul titolo. Se il focus NON era nell'overlay (render asincroni in
11533   * background) non tocca nulla. */
11534  CafChat.prototype._bkFsRefocus = function(prevRef,stepChanged){
11535    var ov=this._bkFsEl;if(!ov)return;
11536    if(!prevRef)return;
11537    try{
11538      var act=(this.shadow&&this.shadow.activeElement)||document.activeElement;
11539      if(act&&act!==document.body&&ov.contains(act))return;
11540      var t=null;
11541      if(!stepChanged&&prevRef.tag&&prevRef.cls){
11542        t=ov.querySelector(prevRef.tag.toLowerCase()+"."+prevRef.cls);
11543        if(t&&t.disabled)t=null;
11544      }
11545      if(!t){
11546        t=ov.querySelector(".caf-bk-fs-step-h1");
11547        if(t&&!t.hasAttribute("tabindex"))t.setAttribute("tabindex","-1");
11548      }
11549      if(!t){
11550        var f=this._trapFocusables(ov);
11551        t=f.length?f[0]:null;
11552      }
11553      if(t)t.focus();
11554    }catch(_e){}
11555  };
11556  CafChat.prototype._bkRenderFs = function(){
11557    var prevStep=this._bkPrevFsStep;
11558    var curStep=this.bkStep;
11559    var stepChanged=prevStep!==curStep&&prevStep!==undefined;
11560    this._bkPrevFsStep=curStep;
11561    var self=this;
11562    var prevRef=null;
11563    try{
11564      var _a=(this.shadow&&this.shadow.activeElement)||document.activeElement;
11565      if(_a&&this._bkFsEl&&this._bkFsEl.contains(_a)){
11566        var _c=(typeof _a.className==="string")?_a.className.split(/\s+/)[0]:"";
11567        prevRef={tag:_a.tagName||"",cls:_c};
11568      }
11569    }catch(_e){}
11570    if(stepChanged){
11571      bkViewTransition(function(){self._bkRenderFsImpl();self._bkFsRefocus(prevRef,true);});
11572    }else{
11573      this._bkRenderFsImpl();
11574      this._bkFsRefocus(prevRef,false);
11575    }
11576  };
11577  /* DEMOPROMO: fascia fissa che dichiara la natura di esempio della
11578   * prenotazione. Stile inline (nessuna dipendenza dal CSS string del widget),
11579   * non chiudibile, ricostruita ad ogni render → presente su tutti gli step. */
11580  CafChat.prototype._bkDemoRibbon = function(){
11581    var d=document.createElement("div");
11582    d.className="caf-bk-demo-ribbon";
11583    d.setAttribute("role","note");
11584    d.style.cssText="display:flex;align-items:center;gap:8px;justify-content:center;"
11585      +"background:linear-gradient(90deg,#fef3c7,#fde68a);color:#92400e;"
11586      +"font-size:12.5px;font-weight:600;line-height:1.35;padding:8px 14px;"
11587      +"border-bottom:1px solid #f59e0b;text-align:center;letter-spacing:.1px;";
11588    d.innerHTML='<span aria-hidden="true">🎬</span><span>'+escapeHtml(bkT("demo_ribbon"))+'</span>';
11589    return d;
11590  };
11591
11592  CafChat.prototype._bkRenderFsImpl = function(){
11593    if(!this._bkFsEl)return;
11594    var el=this._bkFsEl,self=this,c=this.opts.color,o=this.opts;
11595    el.innerHTML="";
11596    /* A11y: innerHTML="" ha appena staccato la live region (_bkFsAnnouncer).
11597     * Ri-attacchiamo LO STESSO nodo (ri-appenderlo la ri-registra presso le
11598     * AT); _bkFsAnnounce scrive con un piccolo delay proprio per coprire
11599     * questa finestra di re-render. */
11600    if(this._bkFsAnnouncer)el.appendChild(this._bkFsAnnouncer);
11601    this._updateSendBtn();
11602    var TOTAL=5;
11603    var stepLabels=[bkT("step_zone"),bkT("step_service"),bkT("step_office"),bkT("step_time"),bkT("step_data")];
11604    var stepIndex={selectRegione:0,selectProvincia:0,selectServizio:1,selectSede:2,selectSlot:3,collectData:4,confirm:4,submitting:4,done:4,loading:-1,error:-1};
11605    var cur=stepIndex[self.bkStep];if(cur===undefined)cur=0;
11606
11607    /* ── HERO ── */
11608    var hero=document.createElement("div");
11608hero.className="caf-bk-fs-hero";
11609    var hrow=document.createElement("div");hrow.className="caf-bk-fs-hrow";
11610    var hlogo=document.createElement("div");hlogo.className="caf-bk-fs-hlogo";
11611    hlogo.innerHTML='<svg viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="4" width="18" height="18" rx="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/><path d="M8 14h.01M12 14h.01M16 14h.01M8 18h.01M12 18h.01M16 18h.01"/></svg>';
11612    hrow.appendChild(hlogo);
11613    var htxt=document.createElement("div");htxt.className="caf-bk-fs-htxt";
11614    var ht=document.createElement("div");ht.className="caf-bk-fs-htitle";ht.textContent=bkT("hero_title");htxt.appendChild(ht);
11615    var hs=document.createElement("div");hs.className="caf-bk-fs-hsub";hs.textContent=bkT("hero_subtitle")+(o.name||"CAF CISL");htxt.appendChild(hs);
11616    hrow.appendChild(htxt);
11617    var hcl=document.createElement("button");hcl.className="caf-bk-fs-hclose";hcl.type="button";hcl.setAttribute("aria-label",bkT("aria_exit_fullscreen"));
11618    hcl.innerHTML='<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>';
11619    hcl.addEventListener("click",function(){self._bkExitFs();});
11620    hrow.appendChild(hcl);
11621    hero.appendChild(hrow);
11622
11623    /* step rail (#47: dot cliccabili per step già completati) */
11624    if(cur>=0&&self.bkStep!=="done"){
11625      var rail=document.createElement("div");rail.className="caf-bk-fs-rail";
11626      var track=document.createElement("div");track.className="caf-bk-fs-rail-track";
11627      var stepKeyByIndex=["selectProvincia","selectServizio","selectSede","selectSlot","collectData"];
11628      for(var i=0;i<TOTAL;i++){
11629        if(i>0){var ln=document.createElement("div");ln.className="caf-bk-fs-rail-line"+(i<=cur?" done":"");track.appendChild(ln);}
11630        var dot=document.createElement("div");dot.className="caf-bk-fs-rail-dot "+(i<cur?"done":i===cur?"active":"todo");
11631        if(i<cur)dot.innerHTML='<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" style="width:12px;height:12px"><path d="M20 6L9 17l-5-5"/></svg>';
11632        else dot.textContent=i+1;
11633        if(i<cur){
11634          (function(idx){
11635            dot.style.cursor="pointer";
11636            dot.setAttribute("role","button");
11637            dot.setAttribute("tabindex","0");
11638            dot.setAttribute("aria-label",bkT("aria_back_to_step",(idx+1),stepLabels[idx]));
11639            var go=function(){var k=stepKeyByIndex[idx];if(k)self._bkGoToStep(k);};
11640            dot.addEventListener("click",go);
11641            dot.addEventListener("keydown",function(ev){if(ev.key==="Enter"||ev.key===" "){ev.preventDefault();go();}});
11642          })(i);
11643        }
11644        track.appendChild(dot);
11645      }
11646      rail.appendChild(track);
11647      var lbls=document.createElement("div");lbls.className="caf-bk-fs-rail-labels";
11648      stepLabels.forEach(function(l,j){
11649        var sp=document.createElement("div");sp.className="caf-bk-fs-rail-lbl"+(j===cur?" active":"")+(j<cur?" clickable":"");sp.textContent=l;
11650        if(j<cur){sp.style.cursor="pointer";sp.addEventListener("click",function(){var k=stepKeyByIndex[j];if(k)self._bkGoToStep(k);});}
11651        lbls.appendChild(sp);
11652      });
11653      rail.appendChild(lbls);
11654      /* Apple-style continuous progress bar */
11655      var progWrap=document.createElement("div");progWrap.className="caf-bk-fs-progress-wrap";
11656      var progBar=document.createElement("div");progBar.className="caf-bk-fs-progress-bar";
11657      var progPct=Math.round(((cur+1)/TOTAL)*100);
11658      progBar.style.width=progPct+"%";
11659      progWrap.appendChild(progBar);rail.appendChild(progWrap);
11660      hero.appendChild(rail);
11661    }
11662    el.appendChild(hero);
11663    if(o.demopromo){el.appendChild(self._bkDemoRibbon());}
11664
11665    /* #50/#51 — banner di stato connessione: offline (bloccante info) o
11666     * "slow-2g/2g" (avviso non bloccante). Aggiornato in tempo reale via
11667     * listener registrati in _init (vedi _bkAttachNetworkListeners). */
11668    if(!bkIsOnline()){
11669      var nb=document.createElement("div");nb.className="caf-bk-net-banner offline";
11670      nb.innerHTML='<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M1 1l22 22"/><path d="M16.72 11.06A10.94 10.94 0 0 1 19 12.55"/>
11670<path d="M5 12.55a10.94 10.94 0 0 1 5.17-2.39"/><path d="M10.71 5.05A16 16 0 0 1 22.58 9"/><path d="M1.42 9a15.91 15.91 0 0 1 4.7-2.88"/><path d="M8.53 16.11a6 6 0 0 1 6.95 0"/><line x1="12" y1="20" x2="12.01" y2="20"/></svg>'
11671        +'<span><b>'+escapeHtml(bkT("label_offline_b"))+'</b>'+escapeHtml(bkT("label_offline_msg"))+'</span>';
11672      el.appendChild(nb);
11673    }else if(bkIsSlowNetwork()){
11674      var nb2=document.createElement("div");nb2.className="caf-bk-net-banner slow";
11675      nb2.innerHTML='<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"><circle cx="12" cy="12" r="10"/><path d="M12 6v6l4 2"/></svg>'
11676        +'<span>'+escapeHtml(bkT("label_slow"))+'</span>';
11677      el.appendChild(nb2);
11678    }
11679
11680    /* ── BODY ── */
11681    var body=document.createElement("div");body.className="caf-bk-fs-body";
11682
11683    /* sidebar (summary) */
11684    var side=document.createElement("div");side.className="caf-bk-fs-side";
11685    var sideTitle=document.createElement("div");sideTitle.className="caf-bk-fs-side-title";
11686    sideTitle.innerHTML='<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"><path d="M9 12l2 2 4-4"/><circle cx="12" cy="12" r="10"/></svg> Riepilogo';
11687    side.appendChild(sideTitle);
11688    var sideCard=document.createElement("div");sideCard.className="caf-bk-fs-side-card";
11689    /* track which sidebar items just became filled so we can animate them once */
11690    if(!self._bkSideFilled)self._bkSideFilled={};
11691    function mkSideItem(lbl,val,svg,pending,key,jumpStep){
11692      var it=document.createElement("div");it.className="caf-bk-fs-side-item"+(pending?" pending":"");
11693      /* Cliccando un item della sidebar, l'utente torna allo step
11694       * corrispondente (Regione→selectRegione, Provincia→selectProvincia, …).
11695       * Questo annulla anche l'auto-jump del GPS in corso così la scelta
11696       * manuale non viene sovrascritta. */
11697      if(jumpStep){
11698        it.style.cursor="pointer";
11699        it.setAttribute("role","button");
11700        it.setAttribute("tabindex","0");
11701        it.title="Modifica "+lbl.toLowerCase();
11702        it.addEventListener("click",function(){self._bkJumpToStep(jumpStep);});
11703        it.addEventListener("keydown",function(e){
11704          if(e.key==="Enter"||e.key===" "){e.preventDefault();self._bkJumpToStep(jumpStep);}
11705        });
11706      }
11707      var ic=document.createElement("div");ic.className="caf-bk-fs-side-ic"+(pending?" pending":"");ic.innerHTML=svg;
11708      /* just-completed: animate item + add checkmark overlay */
11709      if(!pending&&key&&!self._bkSideFilled[key]){
11710        it.classList.add("just-completed");
11711        self._bkSideFilled[key]=true;
11712        var chk=document.createElement("div");chk.className="caf-bk-fs-side-check";
11713        chk.innerHTML='<svg viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="3.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>';
11714        ic.appendChild(chk);
11715      }else if(!pending){
11716        /* persistent checkmark (smaller/static) for already-filled items */
11717        var chk2=document.createElement("div");chk2.className="caf-bk-fs-side-check";chk2.style.animation="none";
11718        chk2.innerHTML='<svg viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="3.5" stroke-linecap="round" stroke-linejoin="round" style="animation:none;stroke-dasharray:none"><polyline points="20 6 9 17 4 12"/></svg>';
11719        ic.appendChild(chk2);
11720      }
11721      it.appendChild(ic);
11722      var tx=document.createElement("div");tx.className="caf-bk-fs-side-txt";
11723      var l=document.createElement("div");l.className="caf-bk-fs-side-lbl";l.textContent=lbl;tx.appendChild(l);
11724      var v=document.createElement("div");v.className="caf-bk-fs-side-val"+(pending?" muted":"");v.textContent=val;tx.appendChild(v);
11725      it.appendChild(tx);return it;
11726    }
11727    var provSvg='<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M20 10c0 6-8 12-8 12s-8-6-8-12a8 8 0 0 1 16 0Z"/><circle cx="12" cy="10" r="3"/></svg>';
11728    var servSvg='<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8Z"/><path d="M14 2v6h6"/></svg>';
11729    var sedeSvg='<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/></svg>';
11730    var dateSvg='<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="4" width="18" height="18" rx="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/></svg>';
11731    var userSvg='<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>';
11732
11733    var regSvg='<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M2 12h20"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></svg>';
11734    /* Tutti gli item del riepilogo sono SEMPRE cliccabili. Se mancano
11735     * prerequisiti (es. click "Sede" senza Servizio scelto), _bkJumpToStep
11736     * fa fallback automatico allo step precedente più vicino disponibile. */
11737    sideCard.appendChild(mkSideItem("Regione",self.bkSelRegione?self.bkSelRegione.descrizione:"—",regSvg,!self.bkSelRegione,"reg","selectRegione"));
11738    sideCard.appendChild(mkSideItem("Provincia",self.bkSelProv?self.bkSelProv.descrizione:"—",provSvg,!self.bkSelProv,"prov","selectProvincia"));
11739    sideCard.appendChild(mkSideItem("Servizio",self.bkSelServ?(self.bkSelServ.desonline||self.bkSelServ.descrizione):"—",servSvg,!self.bkSelServ,"serv","selectServizio"));
11740    sideCard.appendChild(mkSideItem("Sede",self.bkSelSede?self.bkSelSede.descrizione:"—",sedeSvg,!self.bkSelSede,"sede","selectSede"));
11741    var slotTxt=self.bkSelSlot?(fmtDate(self.bkSelSlot.data)+" · "+fmtTime(self.bkSelSlot.ora_inizio)):"—";
11742    sideCard.appendChild(mkSideItem("Orario",slotTxt,dateSvg,!self.bkSelSlot,"slot","selectSlot"));
11743    var cd=self.bkClient;
11744    var hasClient=cd&&cd.nome&&cd.cognome;
11745    sideCard.appendChild(mkSideItem("Intestatario",hasClient?(cd.nome+" "+cd.cognome):"—",userSvg,!hasClient,"client","collectData"));
11746    side.appendChild(sideCard);
11747
11748    var help=document.createElement("div");
11748help.className="caf-bk-fs-side-help";
11749    help.innerHTML='<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg><span>Puoi tornare indietro in qualsiasi momento. Premi <b>ESC</b> per chiudere lo schermo intero.</span>';
11750    side.appendChild(help);
11751    body.appendChild(side);
11752
11753    /* main step area — wrap with stage class for smooth transitions on step change */
11754    var main=document.createElement("div");main.className="caf-bk-fs-main caf-bk-fs-stage";
11755    var stepChanged=self._lastFsBkStep!==self.bkStep;
11756    if(stepChanged){
11757      /* when transitioning to/from loading state, use a gentle fade (avoids double-slide
11758         when flow is selectX → loading → selectY which would feel jarring) */
11759      if(self._lastFsBkStep==="loading"||self.bkStep==="loading"||self.bkStep==="submitting"||self._lastFsBkStep==="submitting"){
11760        main.classList.add("fade");
11761      }else{
11762        main.classList.add(self._bkDir==="back"?"back":"fwd");
11763      }
11764    }
11765    self._lastFsBkStep=self.bkStep;
11766    self._bkDir="fwd";
11767
11768    function mkHead(num,title,sub){
11769      var h=document.createElement("div");h.className="caf-bk-fs-step-head";
11770      var n=document.createElement("div");n.className="caf-bk-fs-step-num";n.textContent=bkT("step_of",num,TOTAL);h.appendChild(n);
11771      var t=document.createElement("div");t.className="caf-bk-fs-step-h1";t.textContent=title;h.appendChild(t);
11772      if(sub){var s=document.createElement("div");s.className="caf-bk-fs-step-h2";s.textContent=sub;h.appendChild(s);}
11773      return h;
11774    }
11775    function mkLoader(txt){
11776      var w=document.createElement("div");w.className="caf-bk-fs-center";
11777      var ld=document.createElement("div");ld.className="caf-bk-fs-loader";w.appendChild(ld);
11778      var t=document.createElement("div");t.className="caf-bk-fs-loader-txt";t.textContent=txt||bkT("label_loading_short");w.appendChild(t);
11779      return w;
11780    }
11781
11782    /* skeleton loader builder — mirrors tile grid shape so user sees what's coming */
11783    function mkSkeletonGrid(caption){
11784      var wrap=document.createElement("div");
11785      var head=document.createElement("div");head.className="caf-bk-fs-step-head";
11786      var hSkel=document.createElement("div");hSkel.className="caf-bk-fs-skel caf-bk-fs-skel-row lg";hSkel.style.maxWidth="320px";head.appendChild(hSkel);
11787      var hSkel2=document.createElement("div");hSkel2.className="caf-bk-fs-skel caf-bk-fs-skel-row md";hSkel2.style.maxWidth="440px";hSkel2.style.marginTop="10px";head.appendChild(hSkel2);
11788      wrap.appendChild(head);
11789      var grid=document.createElement("div");grid.className="caf-bk-fs-grid";grid.style.marginTop="18px";
11790      for(var k=0;k<6;k++){
11791        var sk=document.createElement("div");sk.className="caf-bk-fs-skel caf-bk-fs-skel-tile";sk.style.setProperty("--d",(k*120)+"ms");
11792        var ping=document.createElement("span");ping.className="caf-bk-fs-skel-ping";sk.appendChild(ping);
11793        grid.appendChild(sk);
11794      }
11795      wrap.appendChild(grid);
11796      if(caption){
11797        var cap=document.createElement("div");cap.className="caf-bk-fs-skel-caption";
11798        cap.innerHTML='<span class="sp"></span>'+escapeHtml(caption);
11799        wrap.appendChild(cap);
11800      }
11801      return wrap;
11802    }
11803
11804    switch(self.bkStep){
11805    case "loading":{
11806      var lMsg=(self._bkResumeLoading&&self._bkResumeLoading.msg)
11807        ? self._bkResumeLoading.msg
11808        : (self._bkAutoProvStatus&&self._bkAutoProvStatus.type==="loading")
11809          ? self._bkAutoProvStatus.msg
11810          : (self.bkHint?bkT("label_loading_hint",self.bkHint):bkT("label_loading_options"));
11811      main.appendChild(mkSkeletonGrid(lMsg));
11812      /* "Annulla": evita lo spinner cieco fino a ~47s (retry ×3); torna allo
11813       * step precedente abortendo il fetch in corso. */
11814      if(self._bkLoadingReturnStep){
11815        var cancW=document.createElement("div");cancW.style.cssText="text-align:center;margin-top:16px";
11816        var cancB=document.createElement("button");cancB.type="button";cancB.textContent=bkT("btn_cancel");
11817        cancB.style.cssText="background:none;border:none;color:#5b6573;font-size:13.5px;text-decoration:underline;cursor:pointer;padding:8px 14px";
11818        cancB.addEventListener("click",function(){self._bkCancelLoading();});
11819        cancW.appendChild(cancB);main.appendChild(cancW);
11820      }
11821      break;
11822    }
11823    case "submitting":{
11824      /* keep the summary visible while submitting, show compact pulsing loader */
11825      var submitWrap=document.createElement("div");submitWrap.style.cssText="display:flex;flex-direction:column;align-items:center;justify-content:center;flex:1;gap:14px;padding:40px 20px";
11826      var sbIc=document.createElement("div");sbIc.style.cssText="width:72px;height:72px;border-radius:50%;background:linear-gradient(135deg,"+c+","+c+"cc);display:flex;align-items:center;justify-content:center;box-shadow:0 12px 32px "+c+"55;position:relative;animation:caf-submit-pulse 1.2s ease-in-out infinite";
11827      sbIc.innerHTML='<svg viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" style="width:30px;height:30px"><path d="M22 2L11 13"/><path d="M22 2l-7 20-4-9-9-4 20-7z"/></svg>';
11828      submitWrap.appendChild(sbIc);
11829      var sbT=document.createElement("div");sbT.style.cssText="font-size:18px;font-weight:800;color:#0f172a;text-align:center";sbT.textContent=bkT("label_submitting");submitWrap.appendChild(sbT);
11830      var sbS=document.createElement("div");sbS.style.cssText="font-size:13px;color:#5b6573;text-align:center;max-width:420px;line-height:1.5";sbS.textContent=bkT("label_submitting_sub");submitWrap.appendChild(sbS);
11831      main.appendChild(submitWrap);break;
11832    }
11833
11834    case "selectRegione":{
11835      main.appendChild(mkHead(1,bkT("h_region"),bkT("s_region")));
11836      /* #21 — Last booking shortcut chip: ripeti l'ultima prenotazione fatta */
11837      try{
11838        var _lastH=loadBookingHistory();
11839        var _lastB=_lastH&&_lastH[0];
11840        if(_lastB&&_lastB.regione&&_lastB.provincia&&_lastB.servizio&&_lastB.sede&&_lastB.servizio.codice&&_lastB.sede.codice){
11841          var lbWrap=document.createElement("button");lbWrap.type="button";
11842          lbWrap.style.cssText="display:flex;align-items:center;gap:12px;width:100%;max-width:520px;padding:12px 14px;margin:0 0 14px;border:1.5px solid "+c+"55;background:linear-gradient(135deg,"+c+"12,"+c+"04);border-radius:14px;cursor:pointer;font-family:inherit;text-align:left;transition:all .2s;-webkit-tap-highlight-color:transparent;animation:caf-fade-up .35s ease-out";
11843          lbWrap.innerHTML=
11844            '<span style="width:36px;height:36px;border-radius:10px;background:var(--caf-c);color:var(--caf-on-c);display:flex;align-items:center;justify-content:center;flex-shrink:0">'+
11845            '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" style="width:17px;height:17px"><path d="M3 12a9 9 0 1 0 3-6.7L3 8"/><path d="M3 3v5h5"/></svg>'+
11846            '</span>'+
11847            '<span style="flex:1;min-width:0">'+
11848              '<span style="display:block;font-size:12.5px;font-weight:800;color:#0f172a">Ripeti ultima prenotazione</span>'+
11849              '<span style="display:block;font-size:11px;color:#5b6573;margin-top:2px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">'+escapeHtml((_lastB.servizio.desonline||_lastB.servizio.descrizione||"")+" · "+(_lastB.sede.descrizione||""))+'</span>'+
11850            '</span>'+
11851            '<svg viewBox="0 0 24 24" fill="none" stroke="'+c+'" stroke-width="2.5" stroke-linecap="round" style="width:14px;height:14px;flex-shrink:0"><polyline points="9 18 15 12 9 6"/></svg>';
11852          lbWrap.addEventListener("mouseenter",function(){lbWrap.style.transform="translateY(-2px)";lbWrap.style.boxShadow="0 8px 22px "+c+"33";});
11853          lbWrap.addEventListener("mouseleave",function(){lbWrap.style.transform="";lbWrap.style.boxShadow="";});
11854          lbWrap.addEventListener("click",function(){
11855            bkHaptic(8);
11856            self._bkLogEvent&&self._bkLogEvent("last_booking_repeated");
11857            /* Pre-popola TUTTO il path (regione/prov/serv/sede) prima di chiamare
11858             * _bkPickSede, che richiede bkSelProv e bkSelServ già impostati. */
11859            self.bkSelRegione=_lastB.regione;
11860            self.bkSelProv=_lastB.provincia;
11861            self.bkSelServ=_lastB.servizio;
11862            self.bkSelSede=_lastB.sede;
11863            self.bkSedi=[_lastB.sede];
11864            self.bkStep="loading";self._bkRenderFs();
11865            /* Carica gli slot per la sede memorizzata. _bkPickSede gestisce
11866             * fallimenti (_bkSetError) se la sede non è più disponibile. */
11867            self._bkPickSede(_lastB.sede);
11868          });
11869          main.appendChild(lbWrap);
11870        }
11871      }catch(_e){}
11872      /* #22 — Smart default: ordina le regioni in base a timezone (Italia) e history */
11873      try{
11874        var _tzPref=bkGuessRegionByTimezone();
11875        var _histRegs={};_lastH=_lastH||loadBookingHistory();
11876        _lastH.forEach(function(b){if(b&&b.sede&&b.sede.regione){_histRegs[b.sede.regione]=(_histRegs[b.sede.regione]||0)+1;}});
11877        if(_tzPref&&self.bkRegioni&&self.bkRegioni.length>1){
11878          self.bkRegioni.sort(function(a,b){
11879            var ai=_tzPref.indexOf(a.descrizione),bi=_tzPref.indexOf(b.descrizione);
11880            var ah=_histRegs[a.descrizione]||0,bh=_histRegs[b.descrizione]||0;
11881            if(ah!==bh)return bh-ah;
11882            if(ai<0)ai=999;if(bi<0)bi=999;return ai-bi;
11883          });
11884        }
11885      }catch(_e){}
11886      /* Banner stato geolocalizzazione: visibile in TUTTI gli stati
11887       * (loading/ok/err) per dare feedback durante il GPS in background. */
11888      if(self._bkAutoProvStatus){
11889        var spFR=document.createElement("div");
11890        var stType=self._bkAutoProvStatus.type;
11891        spFR.className="caf-bk-locate-status "+(stType==="ok"?"ok":(stType==="err"?"err":"loading"));
11892        var spIcFR;
11893        if(stType==="loading")spIcFR='<span class="spin" style="display:inline-block;width:14px;
11893height:14px;border:2px solid currentColor;border-right-color:transparent;border-radius:50%;animation:caf-spin .8s linear infinite;vertical-align:middle"></span>';
11894        else if(stType==="ok")spIcFR='<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round"><polyline points="20 6 9 17 4 12"/></svg>';
11895        else spIcFR='<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"><circle cx="12" cy="12" r="10"/><path d="M12 8v4m0 4h.01"/></svg>';
11896        spFR.innerHTML=spIcFR+'<span>'+escapeHtml(self._bkAutoProvStatus.msg)+'</span>';
11897        main.appendChild(spFR);
11898      }
11899      var regGrid=document.createElement("div");regGrid.className="caf-bk-fs-grid";
11900      self.bkRegioni.forEach(function(r,i){
11901        var t=document.createElement("button");t.type="button";t.className="caf-bk-fs-tile";t.style.animation="caf-fade-up .35s ease-out "+(i*30)+"ms backwards";
11902        var ic=document.createElement("div");ic.className="caf-bk-fs-tile-ic";ic.innerHTML='<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M2 12h20"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/><circle cx="12" cy="12" r="10"/></svg>';t.appendChild(ic);
11903        var nm=document.createElement("div");nm.className="caf-bk-fs-tile-name";nm.textContent=r.descrizione;t.appendChild(nm);
11904        var ar=document.createElement("div");ar.className="caf-bk-fs-tile-arrow";ar.innerHTML='<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><polyline points="9 18 15 12 9 6"/></svg>';t.appendChild(ar);
11905        t.addEventListener("click",function(){
11906          var all=regGrid.querySelectorAll(".caf-bk-fs-tile");
11907          all.forEach(function(other){if(other!==t)other.classList.add("caf-bk-fs-tile-dim");});
11908          t.classList.add("caf-bk-fs-tile-focus");
11909          setTimeout(function(){self._bkPickRegione(r);},200);
11910        });
11911        regGrid.appendChild(t);
11912      });
11913      main.appendChild(regGrid);break;
11914    }
11915
11916    case "selectProvincia":{
11917      main.appendChild(mkHead(1,bkT("h_province"),bkT("s_province")));
11918      /* PROMINENT "use my location" to auto-detect province */
11919      var locProvBox=document.createElement("div");locProvBox.className="caf-bk-nearby";
11920      var locProvBtn=document.createElement("button");locProvBtn.type="button";locProvBtn.className="caf-bk-locate";
11921      var lpLoad=self._bkAutoProvStatus&&self._bkAutoProvStatus.type==="loading";
11922      if(lpLoad){locProvBtn.disabled=true;
11923        locProvBtn.innerHTML='<span class="caf-bk-locate-ic"><span class="spin"></span></span><div class="caf-bk-locate-txt"><div class="caf-bk-locate-t">'+escapeHtml(self._bkAutoProvStatus.msg)+'</div></div>';
11924      }else{
11925        locProvBtn.innerHTML='<span class="caf-bk-locate-ic"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="10" r="3"/><path d="M20 10c0 6-8 12-8 12s-8-6-8-12a8 8 0 0 1 16 0Z"/></svg></span><div class="caf-bk-locate-txt"><div class="caf-bk-locate-t">'+escapeHtml(bkT("btn_use_location"))+'</div><div class="caf-bk-locate-s">'+escapeHtml(bkT("btn_use_location_sub"))+'</div></div><span class="caf-bk-locate-arrow">›</span>';
11926      }
11927      locProvBtn.addEventListener("click",function(){self._bkAutoProvStatus=null;self._bkAutoDetectProvince(function(){self._bkRenderFs();});});
11928      locProvBox.appendChild(locProvBtn);
11929      if(self._bkAutoProvStatus&&self._bkAutoProvStatus.type!=="loading"){
11930        var spF=document.createElement("div");spF.className="caf-bk-locate-status "+(self._bkAutoProvStatus.type==="ok"?"ok":"err");
11931        var spIcF=self._bkAutoProvStatus.type==="ok"
11932          ?'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round"><polyline points="20 6 9 17 4 12"/></svg>'
11933          :'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"><circle cx="12" cy="12" r="10"/><path d="M12 8v4m0 4h.01"/></svg>';
11934        spF.innerHTML=spIcF+'<span>'+escapeHtml(self._bkAutoProvStatus.msg)+'</span>';
11935        locProvBox.appendChild(spF);
11936      }
11937      main.appendChild(locProvBox);
11938      var divLbl=document.createElement("div");divLbl.style.cssText="display:flex;align-items:center;gap:10px;margin:2px 0 8px;font-size:10.5px;color:#5b6573;font-weight:700;text-transform:uppercase;letter-spacing:.5px";
11939      divLbl.innerHTML='<span style="flex:1;height:1px;background:#e2e8f0"></span><span>'+escapeHtml(bkT("label_or_choose"))+'</span><span style="flex:1;height:1px;background:#e2e8f0"></span>';
11940      main.appendChild(divLbl);
11941
11942      /* #2 — sort province per frequenza d'uso (più usate in cima). Stable sort. */
11943      var sortedProvince=bkFreqSort("prov",self.bkProvince||[],function(p){return p.codice;});
11944
11945      /* #3 — search input nazionale: cerca tra tutte le province. Se l'utente
11946       * digita una città/provincia che NON è in self.bkProvince (cioè non è
11947       * della regione corrente), suggeriamo lo switch alla regione corretta. */
11948      if(self.bkProvince&&self.bkProvince.length>5){
11949        var sb=document.createElement("div");sb.className="caf-bk-fs-search";sb.style.marginBottom="12px";
11950        sb.innerHTML='<div class="caf-bk-fs-search-ic"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/></svg></div>';
11951        var sinp=document.createElement("input");sinp.type="text";sinp.placeholder=bkT("ph_search_province");sinp.setAttribute("aria-label",bkT("ph_search_province"));
11952        sb.appendChild(sinp);
11953        main.appendChild(sb);
11954        var _provDeb=null;
11955        sinp.addEventListener("input",function(){
11956          if(_provDeb)clearTimeout(_provDeb);
11957          _provDeb=setTimeout(function(){
11958            var q=bkNormalizeStr(sinp.value);
11959            grid.querySelectorAll(".caf-bk-fs-tile").forEach(function(t){
11960              var name=bkNormalizeStr(t.dataset.name||"");
11961              t.style.display=(!q||name.indexOf(q)!==-1)?"":"none";
11962            });
11963          },120);
11964        });
11965      }
11966
11967      var grid=document.createElement("div");grid.className="caf-bk-fs-grid";
11968      sortedProvince.forEach(function(p,i){
11969        var t=document.createElement("button");t.type="button";t.className="caf-bk-fs-tile";t.dataset.name=p.descrizione;t.style.animation="caf-fade-up .35s ease-out "+Math.min(i*30,400)+"ms backwards";
11970        var ic=document.createElement("div");ic.className="caf-bk-fs-tile-ic";ic.innerHTML=provSvg;t.appendChild(ic);
11971        var nm=document.createElement("div");nm.className="caf-bk-fs-tile-name";nm.textContent=p.descrizione;t.appendChild(nm);
11972        /* #2 — badge "frequente" se l'utente l'ha già usata almeno 2 volte */
11973        if(bkFreqGet("prov",p.codice)>=2){
11974          var fr=document.createElement("span");fr.className="caf-bk-fs-tile-recent";fr.textContent=bkT("label_frequent");t.appendChild(fr);
11975        }
11976        var ar=document.createElement("div");ar.className="caf-bk-fs-tile-arrow";ar.innerHTML='<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><polyline points="9 18 15 12 9 6"/></svg>';t.appendChild(ar);
11977        t.addEventListener("click",function(){
11978          var all=grid.querySelectorAll(".caf-bk-fs-tile");
11979          all.forEach(function(other){if(other!==t)other.classList.add("caf-bk-fs-tile-dim");});
11980          t.classList.add("caf-bk-fs-tile-focus");
11981          bkFreqInc("prov",p.codice);
11982          setTimeout(function(){self._bkPickProv(p);},200);
11983        });
11984        grid.appendChild(t);
11985      });
11986      main.appendChild(grid);break;
11987    }
11988    case "selectServizio":{
11989      main.appendChild(mkHead(2,self.bkHint?bkT("h_service_hint"):bkT("h_service"),self.bkHint?bkT("s_service_hint",self.bkHint):bkT("s_service")));
11990      var allSrv=self.bkServizi;
11991      var gridWrap=document.createElement("div");gridWrap.style.display="flex";gridWrap.style.flexDirection="column";gridWrap.style.gap="14px";
11992
11993      /* #6 — categorizza tutti i servizi per categoria UI (icona + label).
11994       * I servizi senza match vanno in "Altri". */
11995      var catsBuckets={};var catsOrder=[];
11996      allSrv.forEach(function(s){
11997        var cat=bkServizioCategoria(s);var bucket=catsBuckets[cat.id];
11998        if(!bucket){bucket={cat:cat,items:[]};catsBuckets[cat.id]=bucket;catsOrder.push(cat.id);}
11999        bucket.items.push(s);
12000      });
12001      /* #7 — flag popolari/recenti via frequency tracker */
12002      var popularIds={};
12003      var freq=allSrv.map(function(s){return{s:s,f:bkFreqGet("srv",s.codice)};}).filter(function(x){return x.f>0;});
12004      freq.sort(function(a,b){return b.f-a.f;});
12005      freq.slice(0,2).forEach(function(x){popularIds[x.s.codice]="popular";});
12006      /* recente = primo della history se non già popolare */
12007      try{var _hist=loadBookingHistory();if(_hist&&_hist.length){var lastSrvCode=_hist[0]&&_hist[0].servizio&&_hist[0].servizio.codice;if(lastSrvCode&&!popularIds[lastSrvCode])popularIds[lastSrvCode]="recent";}}catch(_e){}
12008
12009      var srvGrid=document.createElement("div");
12009srvGrid.className="caf-bk-fs-grid";
12010
12011      /* Categoria attualmente attiva (null = tutte). */
12012      var activeCat=null;
12013      /* Tab categorie (#6): mostrate solo se ci sono almeno 2 categorie diverse e abbastanza servizi. */
12014      var catsBar=null;
12015      if(catsOrder.length>=2&&allSrv.length>=6){
12016        catsBar=document.createElement("div");catsBar.className="caf-bk-srv-cats";
12017        var allBtn=document.createElement("button");allBtn.type="button";allBtn.className="caf-bk-srv-cat active";
12018        allBtn.innerHTML='<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><circle cx="12" cy="12" r="9"/></svg> Tutti <span class="caf-bk-srv-cat-count">'+allSrv.length+'</span>';
12019        allBtn.addEventListener("click",function(){activeCat=null;updateCatsUi();renderSrv(searchInputRef.value||"");});
12020        catsBar.appendChild(allBtn);
12021        catsOrder.forEach(function(catId){
12022          var b=catsBuckets[catId];
12023          var btn=document.createElement("button");btn.type="button";btn.className="caf-bk-srv-cat";btn.dataset.cat=catId;
12024          btn.innerHTML=b.cat.icon+' '+escapeHtml(b.cat.label)+' <span class="caf-bk-srv-cat-count">'+b.items.length+'</span>';
12025          btn.addEventListener("click",function(){activeCat=catId;updateCatsUi();renderSrv(searchInputRef.value||"");});
12026          catsBar.appendChild(btn);
12027        });
12028        gridWrap.appendChild(catsBar);
12029      }
12030      function updateCatsUi(){
12031        if(!catsBar)return;
12032        catsBar.querySelectorAll(".caf-bk-srv-cat").forEach(function(b){b.classList.remove("active");});
12033        if(!activeCat){catsBar.querySelector(".caf-bk-srv-cat").classList.add("active");}
12034        else{var sel=catsBar.querySelector('.caf-bk-srv-cat[data-cat="'+activeCat+'"]');if(sel)sel.classList.add("active");}
12035      }
12036
12037      /* Search input (#3-#10-#11): include sinonimi + Levenshtein "did you mean" */
12038      var searchInputRef={value:""};
12039      function renderSrv(filter){
12040        srvGrid.innerHTML="";
12041        var rawFilter=filter||"";
12042        var f=bkNormalizeStr(rawFilter);
12043        /* base list = tutti o filtrati per categoria */
12044        var base=activeCat?(catsBuckets[activeCat]?catsBuckets[activeCat].items:[]):allSrv;
12045        var flt;
12046        if(!f){flt=base;}
12047        else{
12048          /* tokens da cercare = filter normalizzato + sinonimi */
12049          var tokens=[f];
12050          if(BK_SINONIMI[f])BK_SINONIMI[f].forEach(function(t){tokens.push(bkNormalizeStr(t));});
12051          /* match: ogni servizio passa se almeno un token è substring del nome */
12052          flt=base.filter(function(s){
12053            var hay=bkNormalizeStr((s.desonline||"")+" "+(s.descrizione||"")+" "+(s.descriptionTipologia||""));
12054            for(var ti=0;ti<tokens.length;ti++){if(hay.indexOf(tokens[ti])!==-1)return true;}
12055            return false;
12056          });
12057        }
12058        /* Sort: popolari primi (se non c'è filtro), poi recenti, poi resto */
12059        if(!f){
12060          flt=flt.slice().sort(function(a,b){
12061            var ap=popularIds[a.codice]==="popular"?2:popularIds[a.codice]==="recent"?1:0;
12062            var bp=popularIds[b.codice]==="popular"?2:popularIds[b.codice]==="recent"?1:0;
12063            if(ap!==bp)return bp-ap;
12064            return 0;
12065          });
12066        }
12067        /* WCAG 4.1.3 (#C65): annuncia l'esito della ricerca servizi */
12068        if(f){self._bkFsAnnounce(flt.length?bkT("sr_results_count",flt.length):bkT("sr_no_results"));}
12069        if(!flt.length){
12070          /* #11 — spelling tolerance: cerca servizi con Levenshtein ≤ 3 */
12071          var suggestions=[];
12072          if(f){
12073            allSrv.forEach(function(s){
12074              var name=bkNormalizeStr(s.desonline||s.descrizione||"");
12075              if(!name)return;
12076              /* split nome in parole e calcola distanza minima */
12077              var words=name.split(/\s+/);var minD=99;
12078              words.forEach(function(w){var d=bkLevenshtein(f,w,3);if(d<minD)minD=d;});
12079              if(minD<=2&&minD<=Math.max(2,Math.floor(f.length/3))){suggestions.push({s:s,d:minD});}
12080            });
12081            suggestions.sort(function(a,b){return a.d-b.d;});suggestions=suggestions.slice(0,3);
12082          }
12083          if(suggestions.length){
12084            var sg=document.createElement("div");sg.className="caf-bk-suggest";sg.style.gridColumn="1/-1";
12085            sg.innerHTML='<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M12 16v-4M12 8h.01"/></svg>'
12086              +'<div><div style="font-weight:700;margin-bottom:4px">'+escapeHtml(bkT("ph_no_results_generic",rawFilter))+'</div>'
12087              +'<div>'+escapeHtml(bkT("label_did_you_mean"))+'</div><div style="display:flex;flex-wrap:wrap;gap:6px;margin-top:6px"></div></div>';
12088            var sgRow=sg.querySelector("div > div:last-child");
12089            suggestions.forEach(function(sg2){
12090              var b=document.createElement("button");b.type="button";b.textContent=sg2.s.desonline||sg2.s.descrizione;
12091              b.addEventListener("click",function(){searchInputRef.value=sg2.s.desonline||sg2.s.descrizione;searchInputRef._inputEl&&(searchInputRef._inputEl.value=searchInputRef.value);renderSrv(searchInputRef.value);});
12092              sgRow.appendChild(b);
12093            });
12094            srvGrid.appendChild(sg);
12095          }else{
12096            var nr=document.createElement("div");nr.style.cssText="grid-column:1/-1;padding:30px;text-align:center;color:#5b6573;font-size:13.5px";nr.textContent=bkT("ph_no_results_one",rawFilter);srvGrid.appendChild(nr);
12097          }
12098          return;
12099        }
12100        flt.forEach(function(s,i){
12101          var t=document.createElement("button");t.type="button";t.className="caf-bk-fs-tile has-cat";t.style.animation="caf-fade-up .3s ease-out "+Math.min(i*25,500)+"ms backwards";t.style.position="relative";
12102          var cat=bkServizioCategoria(s);
12103          /* Badge categoria sx */
12104          var badge=document.createElement("div");badge.className="caf-bk-fs-tile-cat-badge";badge.style.background=cat.color;badge.innerHTML=cat.icon;t.appendChild(badge);
12105          /* Popular/Recent badge */
12106          if(popularIds[s.codice]==="popular"){
12107            var pb=document.createElement("span");
12107pb.className="caf-bk-fs-tile-popular";pb.textContent=bkT("label_popular");t.appendChild(pb);
12108          }else if(popularIds[s.codice]==="recent"){
12109            var rb=document.createElement("span");rb.className="caf-bk-fs-tile-recent";rb.textContent=bkT("label_recent");t.appendChild(rb);
12110          }
12111          var nm=document.createElement("div");nm.className="caf-bk-fs-tile-name";nm.textContent=s.desonline||s.descrizione;t.appendChild(nm);
12112          if(+s.tempo>0){var su=document.createElement("div");su.className="caf-bk-fs-tile-sub";su.innerHTML='<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><path d="M12 6v6l4 2"/></svg> '+escapeHtml(bkT("label_form_duration_min",s.tempo));t.appendChild(su);}
12113          var ar=document.createElement("div");ar.className="caf-bk-fs-tile-arrow";ar.innerHTML='<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><polyline points="9 18 15 12 9 6"/></svg>';t.appendChild(ar);
12114          t.addEventListener("click",function(ev){
12115            var all=srvGrid.querySelectorAll(".caf-bk-fs-tile");
12116            all.forEach(function(other){if(other!==t)other.classList.add("caf-bk-fs-tile-dim");});
12117            t.classList.add("caf-bk-fs-tile-focus");
12118            bkFreqInc("srv",s.codice);
12119            setTimeout(function(){self._bkPickServ(s);},200);
12120          });
12121          srvGrid.appendChild(t);
12122        });
12123      }
12124      if(allSrv.length>5){
12125        var sb=document.createElement("div");sb.className="caf-bk-fs-search";
12126        var sic=document.createElement("div");sic.className="caf-bk-fs-search-ic";sic.innerHTML='<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/></svg>';sb.appendChild(sic);
12127        var sinp=document.createElement("input");sinp.type="text";sinp.placeholder=bkT("ph_search_service");sinp.setAttribute("aria-label",bkT("ph_search_service"));
12128        searchInputRef._inputEl=sinp;
12129        /* debounce 150ms per il fuzzy match */
12130        var _srvDeb=null;
12131        sinp.addEventListener("input",function(){
12132          searchInputRef.value=sinp.value;
12133          if(_srvDeb)clearTimeout(_srvDeb);
12134          _srvDeb=setTimeout(function(){renderSrv(sinp.value);},150);
12135        });
12136        sb.appendChild(sinp);
12137        gridWrap.appendChild(sb);
12138      }
12139      renderSrv("");gridWrap.appendChild(srvGrid);main.appendChild(gridWrap);break;
12140    }
12141    case "selectSede":{
12142      main.appendChild(mkHead(3,bkT("h_office"),bkT("s_office")));
12143      /* "find nearest" block */
12144      var nearby=document.createElement("div");nearby.className="caf-bk-nearby";
12145      var nb=document.createElement("button");nb.type="button";nb.className="caf-bk-nearby-btn";
12146      var nbLoading=self._bkNearestStatus&&self._bkNearestStatus.type==="loading";
12147      if(nbLoading){nb.disabled=true;nb.innerHTML='<span class="spin"></span> '+escapeHtml(self._bkNearestStatus.msg);}
12148      else{nb.innerHTML='<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="10" r="3"/><path d="M20 10c0 6-8 12-8 12s-8-6-8-12a8 8 0 0 1 16 0Z"/></svg> '+escapeHtml(self._bkNearestCode?bkT("btn_find_nearest_again"):bkT("btn_find_nearest"));}
12149      nb.addEventListener("click",function(){
12150        /* reset ranking before new search */
12151        self.bkSedi.forEach(function(s){delete s._distKm;});
12152        self._bkNearestCode=null;
12153        self._bkFindNearestSede(function(){self._bkRenderFs();});
12154      });
12155      nearby.appendChild(nb);
12156      /* status */
12157      if(self._bkNearestStatus&&self._bkNearestStatus.type!=="loading"){
12158        var st=document.createElement("div");st.className="caf-bk-nearby-status "+(self._bkNearestStatus.type==="ok"?"ok":"err");
12159        var stIc=self._bkNearestStatus.type==="ok"
12160          ?'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round"><polyline points="20 6 9 17 4 12"/></svg>'
12161          :'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"><circle cx="12" cy="12" r="10"/><path d="M12 8v4m0 4h.01"/></svg>';
12162        st.innerHTML=stIc+'<span>'+escapeHtml(self._bkNearestStatus.msg)+'</span>';
12163        nearby.appendChild(st);
12164      }
12165      main.appendChild(nearby);
12166
12167      /* SEARCH BAR — filtro live per nome sede / indirizzo / città / telefono.
12168       * Mostrato solo se ci sono > 5 sedi (sotto soglia non serve). */
12169      var allSedi=self.bkSedi;
12170      var sgridWrap=document.createElement("div");sgridWrap.style.display="flex";sgridWrap.style.flexDirection="column";sgridWrap.style.gap="14px";
12171      var sgrid=document.createElement("div");sgrid.className="caf-bk-fs-grid";
12172
12173      function renderSediTiles(filter){
12174        sgrid.innerHTML="";
12175        var f=(filter||"").trim().toLowerCase();
12176        var list=allSedi;
12177        if(f){
12178          list=allSedi.filter(function(s){
12179            var hay=((s.descrizione||"")+" "+(s.indirizzo||"")+" "+(s.citta||"")+" "+(s.telefono||"")).toLowerCase();
12180            /* Match a-token: ogni parola del filtro deve essere presente */
12181            var tokens=f.split(/\s+/).filter(Boolean);
12182            for(var ti=0;ti<tokens.length;ti++){if(hay.indexOf(tokens[ti])===-1)return false;}
12183            return true;
12184          });
12185        }
12186        /* WCAG 4.1.3 (#C65): annuncia l'esito del filtro (debounce già sull'input) */
12187        if(f){self._bkFsAnnounce(list.length?bkT("sr_results_count",list.length):bkT("sr_no_results"));}
12188        if(!list.length){
12189          var nr=document.createElement("div");
12190          nr.style.cssText="grid-column:1/-1;padding:30px;text-align:center;color:#5b6573;font-size:13.5px";
12191          nr.textContent=bkT("ph_no_office_found",filter);
12192          sgrid.appendChild(nr);return;
12193        }
12194        list.forEach(function(s,i){
12195          /* WCAG 4.1.2 (#C30/#C34): il tile contiene link tel/mappa → niente
12196           * role=button col rischio nested-interactive. Il click del mouse resta
12197           * sul contenitore; da tastiera si usa il bottone-freccia dedicato. */
12198          var t=document.createElement("div");t.className="caf-bk-fs-tile";t.style.animation="caf-fade-up .35s ease-out "+Math.min(i*40,800)+"ms backwards";
12199          var isNearest=self._bkNearestCode&&(s.codice===self._bkNearestCode||s.descrizione===self._bkNearestCode);
12200          if(isNearest){
12201            t.classList.add("nearest");
12202            if(self._bkNearestJustFound){
12203              t.classList.add("nearest-reveal");
12204              t.style.animation="none";
12205            }
12206            var ribbon=document.createElement("div");ribbon.className="caf-bk-fs-tile-nearest-ribbon";ribbon.textContent=bkT("label_nearest");t.appendChild(ribbon);
12207          }
12208          if(s._distKm!==undefined){
12209            var dst=document.createElement("span");dst.className="caf-bk-fs-tile-dist";
12210            var kmTxt=s._distKm<1?(Math.round(s._distKm*1000)+" m"):(s._distKm.toFixed(1)+" km");
12211            dst.innerHTML='<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round"><circle cx="12" cy="10" r="3"/><path d="M20 10c0 6-8 12-8 12s-8-6-8-12a8 8 0 0 1 16 0Z"/></svg>'+kmTxt;
12212            t.appendChild(dst);
12213          }
12214          var ic=document.createElement("div");ic.className="caf-bk-fs-tile-ic";ic.innerHTML=sedeSvg;t.appendChild(ic);
12215          var nm=document.createElement("div");nm.className="caf-bk-fs-tile-name";nm.textContent=s.descrizione;t.appendChild(nm);
12216          var sub=document.createElement("div");sub.className="caf-bk-fs-tile-sub";sub.innerHTML='<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M20 10c0 6-8 12-8 12s-8-6-8-12a8 8 0 0 1 16 0Z"/><circle cx="12" cy="10" r="3"/></svg> '+escapeHtml(s.indirizzo+", "+s.citta);t.appendChild(sub);
12217          if(s.telefono){
12218            var tel=document.createElement("a");tel.className="caf-bk-fs-tile-sub";
12219            tel.href="tel:"+encodeURIComponent(String(s.telefono).replace(/\s+/g,""));
12220            tel.style.cssText="text-decoration:none;color:inherit;cursor:pointer";
12221            tel.addEventListener("click",function(ev){ev.stopPropagation();self._bkLogEvent&&self._bkLogEvent("sede_phone_clicked",{sede_codice:s.codice});});
12222            tel.innerHTML='<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3"/></svg> '+escapeHtml(s.telefono);
12223            t.appendChild(tel);
12224          }
12225          var mapLink=document.createElement("a");mapLink.className="caf-bk-fs-tile-sub";mapLink.href="https://www.google.com/maps/search/?api=1&query="+encodeURIComponent((s.descrizione||"")+" "+(s.indirizzo||"")+" "+(s.citta||""));mapLink.target="_blank";mapLink.rel="noopener";mapLink.setAttribute("aria-label",bkT("aria_view_on_map",(s.descrizione||"")));mapLink.style.cursor="pointer";mapLink.style.color="var(--caf-c-ink)";mapLink.style.fontWeight="700";mapLink.style.textDecoration="none";
12226          mapLink.innerHTML='<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="1 6 1 22 8 18 16 22 23 18 23 2 16 6 8 2 1 6"/><line x1="8" y1="2" x2="8" y2="18"/><line x1="16" y1="6" x2="16" y2="22"/></svg> '+escapeHtml(bkT("label_view_map"));
12227          mapLink.addEventListener("click",function(ev){ev.stopPropagation();});
12228          t.appendChild(mapLink);
12229          var pickSede=function(){
12230            var allTiles=sgrid.querySelectorAll(".caf-bk-fs-tile");
12231            allTiles.forEach(function(other){if(other!==t)other.classList.add("caf-bk-fs-tile-dim");});
12232            t.classList.add("caf-bk-fs-tile-focus");
12233            setTimeout(function(){self._bkPickSede(s);},200);
12234          };
12235          /* WCAG 4.1.2 (#C30): la freccia è un VERO <button> — unico stop da
12236           * tastiera del tile — con nome "Seleziona {sede}"; stile inline per
12237           * non cambiare l'aspetto. stopPropagation evita il doppio trigger
12238           * (il click sul contenitore chiama già pickSede). */
12239          var ar=document.createElement("button");ar.type="button";ar.className="caf-bk-fs-tile-arrow";
12240          ar.setAttribute("aria-label",bkT("aria_select_office",(s.descrizione||"")+(s.citta?(" - "+s.citta):"")));
12241          ar.style.cssText="background:none;border:0;padding:0;color:inherit;cursor:pointer";
12242          ar.innerHTML='<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><polyline points="9 18 15 12 9 6"/></svg>';
12243          ar.addEventListener("click",function(ev){ev.stopPropagation();pickSede();});
12244          t.appendChild(ar);
12245          t.addEventListener("click",pickSede);
12246          sgrid.appendChild(t);
12247        });
12248      }
12249
12250      if(allSedi.length>5){
12251        var sb=document.createElement("div");sb.className="caf-bk-fs-search";
12252        var sic=document.createElement("div");sic.className="caf-bk-fs-search-ic";
12253        sic.innerHTML='<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/></svg>';
12254        sb.appendChild(sic);
12255        var sinp=document.createElement("input");
12256        sinp.type="text";
12257        sinp.placeholder=bkT("ph_search_office");
12258        sinp.setAttribute("aria-label",bkT("aria_search_office"));
12259        /* Persiste il filtro tra re-render (cambio status nearest, ecc.)
12260         * altrimenti l'utente lo perderebbe ad ogni evento. */
12261        if(self._bkSedeSearch)sinp.value=self._bkSedeSearch;
12262        var _sedeDeb=null;
12263        sinp.addEventListener("input",function(){
12264          if(_sedeDeb)clearTimeout(_sedeDeb);
12265          _sedeDeb=setTimeout(function(){
12266            self._bkSedeSearch=sinp.value;
12267            renderSediTiles(sinp.value);
12268          },120);
12269        });
12270        sb.appendChild(sinp);
12271        sgridWrap.appendChild(sb);
12272      }
12273      sgridWrap.appendChild(sgrid);
12274      main.appendChild(sgridWrap);
12275      renderSediTiles(self._bkSedeSearch||"");
12276      break;
12277    }
12278    case "selectSlot":{
12279      /* Costruisco l'header MA non lo appendo subito: vogliamo che il
12280       * quickpick "Prima disponibilità" venga PRIMA del titolo, così su
12281       * mobile è la prima cosa visibile senza scroll. Per gli stati di
12282       * errore/empty (no slots) appendiamo l'header normalmente in cima. */
12283      var slotHeadEl=mkHead(4,bkT("h_slot"),bkT("s_slot"));
12284      if(!self.bkSpazi.length){
12285        main.appendChild(slotHeadEl);
12286        var em=document.createElement("div");em.className="caf-bk-fs-center";
12287        var emIc=document.createElement("div");emIc.className="caf-bk-fs-empty-ic";emIc.innerHTML='<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="4" width="18" height="18" rx="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/></svg>';em.appendChild(emIc);
12288        var emT=document.createElement("div");emT.className="caf-bk-fs-empty-t";emT.textContent=bkT("label_no_slots_h");em.appendChild(emT);
12289        var emS=document.createElement("div");emS.className="caf-bk-fs-empty-s";emS.textContent=bkT("label_no_slots_s");em.appendChild(emS);
12290        main.appendChild(em);break;
12291      }
12292      /* convert various API date formats to ISO yyyy-mm-dd */
12293      function toIso(raw){
12294        if(!raw)return "";
12295        var r=String(raw);
12296        /* YYYYMMDD (8 digits, CAF API format) */
12297        if(/^\d{8}$/.test(r))return r.slice(0,4)+"-"+r.slice(4,6)+"-"+r.slice(6,8);
12298        /* YYYY-MM-DD or YYYY-MM-DDT... */
12299        if(/^\d{4}-\d{2}-\d{2}/.test(r))return r.substring(0,10);
12300        /* DD/MM/YYYY */
12301        if(/^\d{2}\/\d{2}\/\d{4}/.test(r)){var p=r.split("/");return p[2]+"-"+p[1]+"-"+p[0];}
12302        return "";
12303      }
12304      /* bucket slots by ISO yyyy-mm-dd */
12305      var dayGroups={},dayKeys=[];
12306      self.bkSpazi.forEach(function(s){
12307        var iso=toIso(s.data);
12308        if(!iso)return;
12309        if(!dayGroups[iso]){dayGroups[iso]=[];dayKeys.push(iso);}
12310        dayGroups[iso].push(s);
12311      });
12312      if(!dayKeys.length){
12313        main.appendChild(slotHeadEl);
12314        var em2=document.createElement("div");em2.className="caf-bk-fs-center";
12315        var em2Ic=document.createElement("div");em2Ic.className="caf-bk-fs-empty-ic";em2Ic.innerHTML='<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="4" width="18" height="18" rx="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/></svg>';em2.appendChild(em2Ic);
12316        var em2T=document.createElement("div");em2T.className="caf-bk-fs-empty-t";em2T.textContent=bkT("label_no_dates_h");em2.appendChild(em2T);
12317        var em2S=document.createElement("div");em2S.className="caf-bk-fs-empty-s";em2S.textContent=bkT("label_no_dates_s");em2.appendChild(em2S);
12318        main.appendChild(em2);break;
12319      }
12320      /* sort chronologically */
12321      dayKeys.sort();
12322      var firstDay=dayKeys[0]||"";
12323      function pad2(n){n=String(n);return n.length<2?"0"+n:n;}
12324      /* determine current month view (first available if not set) */
12325      if(!self._bkCalMonth||!/^\d{4}-\d{2}$/.test(self._bkCalMonth)){
12326        if(firstDay){self._bkCalMonth=firstDay.slice(0,7);}
12327        else{var tn=new Date();self._bkCalMonth=tn.getFullYear()+"-"+pad2(tn.getMonth()+1);}
12328      }
12329      if((!self._bkCalSelected||!dayGroups[self._bkCalSelected])&&firstDay){self._bkCalSelected=firstDay;}
12330
12331      /* #23 — density map globale: ore con più slot = "hot". */
12332      var slotDensityGlobal=bkSlotDensityMap(self.bkSpazi);
12333
12334      /* #21 — Quick-pick "Prima disponibilità": card prominente che, con un
12335       * solo click, seleziona il primo slot disponibile in assoluto. */
12336      if(firstDay&&dayGroups[firstDay]&&dayGroups[firstDay].length){
12337        var firstSlot=dayGroups[firstDay].slice().sort(function(a,b){return String(a.ora_inizio||"").localeCompare(String(b.ora_inizio||""));})[0];
12338        if(firstSlot){
12339          var qp=document.createElement("div");qp.className="caf-bk-quickpick";qp.setAttribute("role","button");qp.setAttribute("tabindex","0");
12340          var fdObj=new Date(firstDay+"T00:00:00");
12341          var dayNamesQ=bkT("cal_days_full").split("|");
12342          var monthNamesQ=bkT("cal_months_short").split("|");
12343          var whenStr=dayNamesQ[fdObj.getDay()]+" "+fdObj.getDate()+" "+monthNamesQ[fdObj.getMonth()]+" · "+fmtTime(firstSlot.ora_inizio);
12344          var sedeStr=(self.bkSelSede&&(self.bkSelSede.descrizione+(self.bkSelSede.citta?" — "+self.bkSelSede.citta:"")))||"";
12345          qp.innerHTML=
12346            '<div class="caf-bk-quickpick-ic">⚡</div>'
12347            +'<div class="caf-bk-quickpick-body">'
12348              +'<div class="caf-bk-quickpick-lbl">'+escapeHtml(bkT("label_first_avail"))+'</div>'
12349              +'<div class="caf-bk-quickpick-when">'+escapeHtml(whenStr)+'</div>'
12350              +(sedeStr?'<div class="caf-bk-quickpick-where">'+escapeHtml(sedeStr)+'</div>':'')
12351            +'</div>'
12352            +'<div class="caf-bk-quickpick-arrow">→</div>';
12353          var pickFirst=function(){bkHaptic(10);self._bkLogEvent&&self._bkLogEvent("quickpick_first_slot",{slot:firstSlot});self._bkPickSlot(firstSlot);};
12354          qp.addEventListener("click",pickFirst);
12355          qp.addEventListener("keydown",function(ev){if(ev.key==="Enter"||ev.key===" "){ev.preventDefault();pickFirst();}});
12356          main.appendChild(qp);
12357        }
12358      }
12359      /* Header del passo DOPO il quickpick: l'utente vede prima la card verde
12360       * "Prima disponibilità" (CTA primaria), poi il titolo del passo, poi il
12361       * calendario. Critico su mobile dove lo spazio sopra-fold è limitato. */
12362      main.appendChild(slotHeadEl);
12363
12364      var layout=document.createElement("div");layout.className="caf-bk-fs-slotlayout";
12365
12366      /* LEFT: month calendar */
12367      var cal=document.createElement("div");cal.className="caf-bk-fs-monthcal";
12368      function renderMonth(){
12369        cal.innerHTML="";
12370        var parts=self._bkCalMonth.split("-");
12371        var yr=+parts[0],mo=+parts[1]-1;
12372        var first=new Date(yr,mo,1);
12373        var daysInMonth=new Date(yr,mo+1,0).getDate();
12374        var startDow=(first.getDay()+6)%7; /* monday=0 */
12375        var monthNames=bkT("cal_months").split("|");
12376        var hdr=document.createElement("div");hdr.className="caf-bk-fs-monthcal-hdr";
12377        var title=document.createElement("div");title.className="caf-bk-fs-monthcal-title";title.textContent=monthNames[mo]+" "+yr;hdr.appendChild(title);
12378        var nav=document.createElement("div");nav.className="caf-bk-fs-monthcal-nav";
12379        /* determine if prev/next has available dates */
12380        var hasPrev=dayKeys.some(function(dk){var d=new Date(dk+"T00:00:00");return(d.getFullYear()<yr)||(d.getFullYear()===yr&&d.getMonth()<mo);});
12381        var hasNext=dayKeys.some(function(dk){var d=new Date(dk+"T00:00:00");return(d.getFullYear()>yr)||(d.getFullYear()===yr&&d.getMonth()>mo);});
12382        /* #20 — FLIP-style slide su cambio mese: il grid esce, il nuovo entra. */
12383        function _calNavigate(deltaMo){
12384          var nm=mo+deltaMo,ny=yr;if(nm<0){nm=11;ny--;}if(nm>11){nm=0;ny++;}
12385          self._bkCalMonth=ny+"-"+String(nm+1).padStart(2,"0");
12386          var oldGrid=cal.querySelector(".caf-bk-fs-monthcal-grid");
12387          if(oldGrid){
12388            oldGrid.style.transition="transform .22s ease, opacity .22s ease";
12389            oldGrid.style.transform="translateX("+(deltaMo>0?-24:24)+"px)";
12390            oldGrid.style.opacity="0";
12391          }
12392          setTimeout(function(){
12393            renderMonth();
12394            var newGrid=cal.querySelector(".caf-bk-fs-monthcal-grid");
12395            if(newGrid){
12396              newGrid.style.transition="none";newGrid.style.transform="translateX("+(deltaMo>0?24:-24)+"px)";newGrid.style.opacity="0";
12397              requestAnimationFrame(function(){
12398                newGrid.style.transition="transform .28s cubic-bezier(.34,1.56,.64,1), opacity .28s ease";
12399                newGrid.style.transform="translateX(0)";newGrid.style.opacity="1";
12400              });
12401            }
12402            renderSlots();
12403            /* refocus della freccia equivalente (o dell'altra se disabilitata):
12404             * il rebuild l'ha distrutta e il focus era caduto su body. */
12405            try{
12406              var _aeN=(self.shadow&&self.shadow.activeElement)||document.activeElement;
12407              if(!_aeN||_aeN===document.body){
12408                var _navB=cal.querySelectorAll(".caf-bk-fs-monthcal-nav button");
12409                var _same=_navB&&_navB[deltaMo>0?1:0];
12410                var _other=_navB&&_navB[deltaMo>0?0:1];
12411                if(_same&&!_same.disabled)_same.focus();
12412                else if(_other&&!_other.disabled)_other.focus();
12413              }
12414            }catch(_e){}
12415          },150);
12416        }
12417        var prevB=document.createElement("button");prevB.type="button";prevB.disabled=!hasPrev;prevB.setAttribute("aria-label",bkT("aria_prev_month"));prevB.innerHTML='<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"><polyline points="15 18 9 12 15 6"/></svg>';
12418        prevB.addEventListener("click",function(){_calNavigate(-1);});
12419        nav.appendChild(prevB);
12420        var nextB=document.createElement("button");nextB.type="button";nextB.disabled=!hasNext;nextB.setAttribute("aria-label",bkT("aria_next_month"));nextB.innerHTML='<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"><polyline points="9 18 15 12 9 6"/></svg>';
12421        nextB.addEventListener("click",function(){_calNavigate(1);});
12422        nav.appendChild(nextB);
12423        hdr.appendChild(nav);cal.appendChild(hdr);
12424        /* weekdays */
12425        var wk=document.createElement("div");wk.className="caf-bk-fs-monthcal-week";
12426        bkT("cal_days_short").split("|").forEach(function(d){var el=document.createElement("div");el.className="caf-bk-fs-monthcal-wd";el.textContent=d;wk.appendChild(el);});
12427        cal.appendChild(wk);
12428        /* grid */
12429        var grid=document.createElement("div");grid.className="caf-bk-fs-monthcal-grid";
12430        for(var e=0;e<startDow;e++){var emC=document.createElement("div");
12430emC.className="caf-bk-fs-monthcal-day empty";grid.appendChild(emC);}
12431        var todayStr=new Date().toISOString().substring(0,10);
12432        for(var d=1;d<=daysInMonth;d++){
12433          var iso=yr+"-"+String(mo+1).padStart(2,"0")+"-"+String(d).padStart(2,"0");
12434          var b=document.createElement("button");b.type="button";b.className="caf-bk-fs-monthcal-day";b.textContent=d;
12435          /* WCAG 4.1.2 (#C33/#C62): nome accessibile = data completa localizzata */
12436          b.setAttribute("aria-label",d+" "+monthNames[mo]+" "+yr);
12437          var hasSlot=!!dayGroups[iso];
12438          var isPast=iso<todayStr;
12439          if(hasSlot){
12440            b.classList.add("available");
12441            var dot=document.createElement("span");dot.className="dot";b.appendChild(dot);
12442            if(iso===self._bkCalSelected)b.classList.add("selected");
12443            b.setAttribute("aria-pressed",iso===self._bkCalSelected?"true":"false");
12444            (function(isoI){b.addEventListener("click",function(){
12445              self._bkCalSelected=isoI;renderMonth();renderSlots();
12446              /* refocus: il rebuild ha distrutto il bottone-giorno attivato
12447               * (WCAG 2.4.3); rifocalizza il giorno selezionato ricreato. */
12448              try{
12449                var _aeD=(self.shadow&&self.shadow.activeElement)||document.activeElement;
12450                if(!_aeD||_aeD===document.body){
12451                  var _selD=cal.querySelector(".caf-bk-fs-monthcal-day.selected");
12452                  if(_selD)_selD.focus();
12453                }
12454              }catch(_e){}
12455              bkHaptic&&bkHaptic(6);
12456              /* Mobile: scroll automatico agli orari. Trovo il container
12457               * scrollabile reale (.caf-bk-fs-main) e ci scrollo dentro
12458               * con offset preciso, invece di scrollIntoView che a volte
12459               * sposta la pagina intera invece del pannello. */
12460              if(window.matchMedia&&window.matchMedia("(max-width:719px)").matches){
12461                requestAnimationFrame(function(){
12462                  /* doppio rAF per essere sicuri che il layout post-render sia stabile */
12463                  requestAnimationFrame(function(){
12464                    var scroller=slotPanel.closest(".caf-bk-fs-main")||slotPanel.parentElement;
12465                    if(!scroller)return;
12466                    var sRect=scroller.getBoundingClientRect();
12467                    var pRect=slotPanel.getBoundingClientRect();
12468                    var offset=(pRect.top-sRect.top)+scroller.scrollTop-8; /* -8: piccolo respiro sopra */
12469                    try{scroller.scrollTo({top:offset,behavior:bkScrollBehavior()});}catch(_e){scroller.scrollTop=offset;}
12470                  });
12471                });
12472              }
12473            });})(iso);
12474          }else if(isPast){
12475            b.classList.add("past");b.disabled=true;
12476          }else{
12477            b.disabled=true;b.style.opacity=".4";b.style.cursor="default";
12478          }
12479          if(iso===todayStr)b.classList.add("today");
12480          grid.appendChild(b);
12481        }
12482        cal.appendChild(grid);
12483        /* legend */
12484        var lg=document.createElement("div");lg.className="caf-bk-fs-monthcal-legend";
12485        lg.innerHTML='<span class="lg"><span class="sq"></span> '+escapeHtml(bkT("label_available"))+'</span><span class="lg"><span class="sq sel"></span> '+escapeHtml(bkT("label_selected"))+'</span>';
12486        cal.appendChild(lg);
12487      }
12488      layout.appendChild(cal);
12489
12490      /* RIGHT: slot panel for selected day */
12491      var slotPanel=document.createElement("div");slotPanel.className="caf-bk-fs-slotpanel";
12492      function renderSlots(){
12493        slotPanel.innerHTML="";
12494        var hdr=document.createElement("div");hdr.className="caf-bk-fs-slotpanel-hdr";
12495        hdr.innerHTML='<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"><circle cx="12" cy="12" r="10"/><path d="M12 6v6l4 2"/></svg> ';
12496        var sel=self._bkCalSelected;
12497        if(sel&&dayGroups[sel]){
12498          var dObj=new Date(sel+"T00:00:00");
12499          var dayNames=bkT("cal_days_full").split("|");
12500          var monthNames=bkT("cal_months").split("|");
12501          var lbl=document.createElement("span");lbl.textContent=dayNames[dObj.getDay()]+" "+dObj.getDate()+" "+monthNames[dObj.getMonth()];hdr.appendChild(lbl);
12502          /* #11 — pill Oggi/Domani/Settimana + count degli orari disponibili */
12503          (function(){
12504            var today=new Date();today.setHours(0,0,0,0);
12505            var diffMs=dObj.getTime()-today.getTime();var diffD=Math.round(diffMs/86400000);
12506            var lbl2="";var bg=c+"15",col=c;
12507            if(diffD===0){lbl2=bkT("label_today");bg="#fef3c7";col="#d97706";}
12508            else if(diffD===1){lbl2=bkT("label_tomorrow");bg=c+"15";col=c;}
12509            else if(diffD>1&&diffD<=7){lbl2=bkT("label_this_week");bg="#e0f2fe";col="#0369a1";}
12510            /* count orari del giorno: utile soprattutto su mobile, dove il
12511             * banner è prominente e l'info "X orari disponibili" rassicura
12512             * subito l'utente. */
12513            var nSlots=(dayGroups[sel]||[]).length;
12514            var countPill=document.createElement("span");
12515            countPill.style.cssText="display:inline-flex;align-items:center;gap:4px;font-size:11px;font-weight:700;padding:3px 9px;border-radius:99px;background:rgba(255,255,255,.22);color:#fff;margin-left:auto;backdrop-filter:blur(4px);-webkit-backdrop-filter:blur(4px)";
12516            countPill.innerHTML='<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" style="width:11px;height:11px"><circle cx="12" cy="12" r="10"/><path d="M12 6v6l4 2"/></svg> '+escapeHtml(bkT(nSlots===1?"label_count_slots_one":"label_count_slots_other",nSlots));
12517            hdr.appendChild(countPill);
12518            if(lbl2){
12519              var pill=document.createElement("span");
12520              pill.style.cssText="display:inline-flex;align-items:center;gap:4px;font-size:9.5px;font-weight:800;letter-spacing:.6px;padding:3px 8px;border-radius:99px;background:"+bg+";color:"+col;
12521              pill.textContent=lbl2;hdr.appendChild(pill);
12522            }
12523          })();
12524          slotPanel.appendChild(hdr);
12525          /* split am/pm */
12526          var slots=dayGroups[sel];
12527          var amS=[],pmS=[];
12528          slots.forEach(function(s){var h=+(s.ora_inizio||"").slice(0,2);(h>=13?pmS:amS).push(s);});
12529          function grpSlots(label,list,period){
12530            if(!list.length)return;
12531            var g=document.createElement("div");g.className="caf-bk-fs-slotgroup";
12532            var gh=document.createElement("div");gh.className="caf-bk-fs-slotgroup-hdr";
12533            gh.innerHTML=period==="am"?'<svg viewBox="0 0 24 24" fill="none" stroke="#3b82f6" stroke-width="2.2" stroke-linecap="round" style="width:12px;height:12px"><circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M4.93 19.07l1.41-1.41M17.66 6.34l1.41-1.41"/></svg>':'<svg viewBox="0 0 24 24" fill="none" stroke="#fb923c" stroke-width="2.2" stroke-linecap="round" style="width:12px;height:12px"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg>';
12534            var t=document.createElement("span");t.textContent=label;gh.appendChild(t);g.appendChild(gh);
12535            var cs=document.createElement("div");cs.className="caf-bk-fs-cal-slots";
12536            list.forEach(function(s,si){
12537              var ch=document.createElement("button");ch.type="button";ch.className="caf-bk-fs-cal-chip";
12538              ch.style.animation="caf-fade-up .25s ease-out "+(si*22)+"ms backwards";
12539              /* #23 — density attribute basato su quanti slot esistono in quella fascia oraria */
12540              var hh=parseInt(String(s.ora_inizio||"").slice(0,2),10);
12541              var dens=slotDensityGlobal[hh];
12542              if(dens!=null){ch.setAttribute("data-density",dens>=0.7?"hot":dens>=0.4?"warm":"cool");}
12543              var dotEl=document.createElement("span");dotEl.className="d "+period;ch.appendChild(dotEl);
12544              var tn=document.createElement("span");tn.textContent=fmtTime(s.ora_inizio);ch.appendChild(tn);
12545              /* #10 — popularity dot: mostrato SOLO quando l'API fornisce
12546               * dati reali su disponibilità (s.disponibili + s.posti). Senza
12547               * dati veri, evitiamo segnali fuorvianti per ogni slot. */
12548              if(s.disponibili!==undefined&&s.posti!==undefined&&+s.posti>0){
12549                var avail=+s.disponibili/+s.posti;
12550                var pCol="#10b981",pTip="Molti posti disponibili";
12551                if(avail<0.25){pCol="#ef4444";pTip="Ultimi posti";}
12552                else if(avail<0.6){pCol="#f59e0b";pTip="Pochi posti";}
12553                var pop=document.createElement("span");
12554                pop.style.cssText="position:absolute;top:3px;right:3px;width:6px;height:6px;border-radius:50%;background:"+pCol+";box-shadow:0 0 0 1.5px #fff";
12555                pop.title=pTip;ch.style.position="relative";
12555ch.appendChild(pop);
12556              }
12557              ch.addEventListener("click",function(){bkHaptic(8);self._bkPickSlot(s);});
12558              cs.appendChild(ch);
12559            });
12560            g.appendChild(cs);slotPanel.appendChild(g);
12561          }
12562          grpSlots(bkT("label_morning"),amS,"am");
12563          grpSlots(bkT("label_afternoon"),pmS,"pm");
12564        }else{
12565          var noTxt=document.createElement("span");noTxt.textContent=bkT("label_pick_day");hdr.appendChild(noTxt);
12566          slotPanel.appendChild(hdr);
12567          var empty=document.createElement("div");empty.className="caf-bk-fs-slotpanel-empty";empty.textContent=bkT("label_no_slots_day");slotPanel.appendChild(empty);
12568        }
12569      }
12570      renderMonth();renderSlots();
12571      layout.appendChild(slotPanel);
12572      main.appendChild(layout);
12573      if(self.bkNextG){
12574        var mr=document.createElement("button");mr.type="button";mr.className="caf-bk-fs-btn caf-bk-fs-btn-ghost";mr.style.alignSelf="flex-start";mr.style.marginTop="12px";
12575        if(self._bkLoadingMore){mr.disabled=true;mr.style.opacity="0.6";mr.innerHTML='<span class="spin"></span> '+escapeHtml(bkT("btn_load_more"));}
12576        else{
12577          mr.innerHTML='<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"><path d="M12 5v14m-7-7h14"/></svg> '+escapeHtml(bkT("btn_load_more"));
12578          mr.addEventListener("click",function(){self._bkLoadMore();});
12579        }
12580        main.appendChild(mr);
12581      }
12582      break;
12583    }
12584    case "collectData":{
12585      main.appendChild(mkHead(5,bkT("h_data"),bkT("s_data")));
12586      /* Slot reservation timer (fullscreen) */
12587      var slotTmrFs=document.createElement("div");slotTmrFs.style.maxWidth="680px";main.appendChild(slotTmrFs);
12588      self._bkRenderSlotTimer(slotTmrFs);
12589      /* Toggle Per me / Per un familiare */
12590      var forRowFs=document.createElement("div");forRowFs.style.cssText="display:flex;gap:8px;background:#f1f5f9;border-radius:12px;padding:4px;margin:0 0 14px;max-width:420px";
12591      [["self",bkT("label_form_for_self")],["family",bkT("label_form_for_family")]].forEach(function(pair){
12592        var b=document.createElement("button");b.type="button";b.style.cssText="flex:1;border:none;padding:9px 12px;font-size:13px;font-weight:700;border-radius:10px;cursor:pointer;transition:all .2s;font-family:inherit;"+(self.bkBookingFor===pair[0]?"background:#fff;color:"+c+";box-shadow:0 1px 4px rgba(0,0,0,.08)":"background:transparent;color:#5b6573");
12593        b.textContent=pair[1];
12594        /* WCAG 4.1.2 (#C35): stato del toggle esposto alle AT */
12595        b.setAttribute("aria-pressed",self.bkBookingFor===pair[0]?"true":"false");
12596        b.addEventListener("click",function(){
12597          if(self.bkBookingFor===pair[0])return;
12598          self.bkBookingFor=pair[0];
12599          /* #PII — cambia intestatario: ricarica il candidato (non pre-fill cieco)
12600           * e svuota il form; il gate "Sei tu?" chiederà conferma. */
12601          self._bkStoredCandidate=loadStoredClient(pair[0]);
12602          self.bkClient={nome:"",cognome:"",codfis:"",email:"",cellulare:"",sesso:"M"};
12603          self._bkRenderFs();
12604          self._bkLogEvent&&self._bkLogEvent("booking_for_changed",{for:pair[0]});
12605        });
12606        forRowFs.appendChild(b);
12607      });
12608      main.appendChild(forRowFs);
12609      /* Trust signals strip (fullscreen) */
12610      var tsFs=document.createElement("div");tsFs.style.cssText="display:flex;gap:14px;align-items:center;flex-wrap:wrap;font-size:12.5px;color:#475569;background:#f8fafc;border:1px solid #e2e8f0;border-radius:12px;padding:10px 14px;max-width:680px;margin:0 0 14px";
12611      tsFs.innerHTML='<span style="display:inline-flex;align-items:center;gap:5px"><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="#10b981" stroke-width="2.4" stroke-linecap="round"><rect x="3" y="11" width="18" height="11" rx="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg> '+escapeHtml(bkT("label_data_safe"))+'</span>'+
12612        '<span style="color:#5b6573">·</span>'+
12613        '<span style="display:inline-flex;align-items:center;gap:5px"><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="#0ea5e9" stroke-width="2.4" stroke-linecap="round"><path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.86 19.86 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.86 19.86 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72c.13.96.37 1.9.7 2.81a2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45c.9
126131.33 1.85.57 2.81.7A2 2 0 0 1 22 16.92Z"/></svg> '+escapeHtml(bkT("label_data_caf"))+'</span>'+
12614        '<span style="color:#5b6573">·</span>'+
12615        '<span style="display:inline-flex;align-items:center;gap:5px"><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="#f59e0b" stroke-width="2.4" stroke-linecap="round"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/></svg> '+escapeHtml(bkT("label_data_real"))+'</span>';
12616      main.appendChild(tsFs);
12617      /* #PII — gate "Sei tu?": se ci sono dati salvati (candidato) e il form è
12618       * ancora vuoto, chiediamo conferma esplicita prima di adottarli. Su un PC
12619       * condiviso (sportello) questo impedisce alla persona successiva di inviare
12620       * per sbaglio i dati di quella precedente. */
12621      var _cand=self._bkStoredCandidate;
12622      var _formEmpty=!(self.bkClient.codfis||self.bkClient.nome||self.bkClient.email);
12623      if(_cand&&(_cand.codfis||_cand.nome)&&_formEmpty){
12624        var who=((_cand.nome||"")+" "+(_cand.cognome||"")).trim();
12625        var cfTail=(_cand.codfis||"").toUpperCase().slice(-4);
12626        var reuse=document.createElement("div");
12627        reuse.style.cssText="display:flex;flex-wrap:wrap;align-items:center;gap:10px;background:#eff6ff;border:1px solid #bfdbfe;border-radius:12px;padding:12px 14px;max-width:680px;margin:0 0 14px;font-size:13.5px;color:#1e3a5f";
12628        var rtxt=document.createElement("div");rtxt.style.cssText="flex:1;
12628min-width:200px;line-height:1.4";
12629        rtxt.innerHTML='<strong>'+escapeHtml(bkT("label_reuse_data_h"))+'</strong><br><span style="color:#475569">'+escapeHtml(who||bkT("label_saved_data"))+(cfTail?' · CF …'+escapeHtml(cfTail):'')+'</span>';
12630        reuse.appendChild(rtxt);
12631        var ryes=document.createElement("button");ryes.type="button";ryes.textContent=bkT("btn_its_me");
12632        ryes.setAttribute("aria-pressed","false");
12633        ryes.style.cssText="border:none;background:var(--caf-c);color:var(--caf-on-c);font-weight:700;font-size:13px;padding:8px 14px;border-radius:9px;cursor:pointer;font-family:inherit";
12634        ryes.addEventListener("click",function(){
12635          self.bkClient={nome:_cand.nome||"",cognome:_cand.cognome||"",codfis:_cand.codfis||"",email:_cand.email||"",cellulare:_cand.cellulare||"",sesso:self.bkClient.sesso||"M"};
12636          self._bkLogEvent&&self._bkLogEvent("prefill_reused",{for:self.bkBookingFor});
12637          self._bkRenderFs();
12638        });
12639        var rno=document.createElement("button");rno.type="button";rno.textContent=bkT("btn_not_me");
12640        rno.setAttribute("aria-pressed","false");
12641        rno.style.cssText="border:1px solid #cbd5e1;background:#fff;color:#475569;font-weight:600;font-size:13px;padding:8px 14px;border-radius:9px;cursor:pointer;font-family:inherit";
12642        rno.addEventListener("click",function(){
12643          self._bkStoredCandidate=null;
12644          try{localStorage.removeItem(clientStorageKey(self.bkBookingFor));}catch(_e){}
12645          self._bkLogEvent&&self._bkLogEvent("prefill_rejected",{for:self.bkBookingFor});
12646          self._bkRenderFs();
12647        });
12648        reuse.appendChild(ryes);reuse.appendChild(rno);
12649        main.appendChild(reuse);
12650      }
12651      var fg=document.createElement("div");fg.className="caf-bk-fs-form";
12652      var ficons={
12653        nome:'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>',
12654        cognome:'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>',
12655        codfis:'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="2" y="5" width="20" height="14" rx="2"/><line x1="2" y1="10" x2="22" y2="10"/></svg>',
12656        email:'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2Z"/><path d="m22 6-10 7L2 6"/></svg>',
12657        cellulare:'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="5" y="2" width="14" height="20" rx="2"/><line x1="12" y1="18" x2="12.01" y2="18"/></svg>'
12658      };
12659      var fvalidate={nome:function(v){return v.trim().length>=2;},cognome:function(v){return v.trim().length>=2;},codfis:function(v){return validateCodiceFiscale(v).ok;},email:function(v){return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v);},cellulare:function(v){return v.replace(/\D/g,"").length>=8;}};
12660      var flabels={nome:bkT("label_form_nome"),cognome:bkT("label_form_cognome"),codfis:bkT("label_form_codfis"),email:bkT("label_form_email"),cellulare:bkT("label_form_cellulare")};
12661      var checkSvg='<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><path d="M20 6L9 17l-5-5"/></svg>';
12662      /* Codice Fiscale come primo campo: con l'auto-fill #1 ne deriviamo
12663       * sesso/data nascita/comune e l'utente verifica subito di aver
12664       * digitato il proprio CF prima di procedere col resto. */
12665      var fFields=[{k:"codfis",p:"RSSMRA80A01H501U",t:"text",mx:16,up:1,full:true},{k:"nome",p:"Mario",t:"text",full:false},{k:"cognome",p:"Rossi",t:"text",full:false},{k:"email",p:"[email protected]",t:"email",full:false},{k:"cellulare",p:"333 1234567",t:"tel",full:false}];
12666      /* CF gate (fullscreen): finché CF non è algoritmicamente valido,
12667       * blocchiamo gli altri input e il pulsante "Prosegui" nel footer. */
12668      var _fsWrapsByKey={};var _fsInpsByKey={};var _fsCfReasonBox=null;
12669      self._bkCfFsApplyGate=function(){
12670        var ok=fvalidate.codfis(self.bkClient.codfis||"");
12671        ["nome","cognome","email","cellulare"].forEach(function(k){
12672          var inp=_fsInpsByKey[k];var w=_fsWrapsByKey[k];if(!inp||!w)return;
12673          inp.disabled=!ok;
12674          if(!ok){inp.setAttribute("aria-disabled","true");inp.setAttribute("title",bkT("err_cf_first"));w.style.opacity="0.45";w.style.pointerEvents="none";}
12675          else{inp.removeAttribute("aria-disabled");inp.removeAttribute("title");w.style.opacity="";w.style.pointerEvents="";}
12676        });
12677        if(self._bkCfFsNextBtn){
12678          /* "Prosegui al riepilogo": richiede CF valido E consenso privacy
12679           * spuntato. Senza spunta non si procede con la prenotazione. */
12680          var nextOk=ok&&!!self.bkPrivacyOk;
12681          self._bkCfFsNextBtn.disabled=!nextOk;
12682          self._bkCfFsNextBtn.style.opacity=nextOk?"":"0.45";
12683          self._bkCfFsNextBtn.style.cursor=nextOk?"":"not-allowed";
12684          self._bkCfFsNextBtn.setAttribute("title",nextOk?"":(!ok?bkT("err_cf_first"):bkT("consent_required")));
12685        }
12686      };
12687      fFields.forEach(function(f,i){
12688        var col=document.createElement("div");col.className="caf-bk-inp-wrap";if(f.full)col.classList.add("caf-bk-fs-form-full");
12689        col.style.animation="caf-fade-up .35s ease-out "+(i*50)+"ms backwards";
12690        /* label */
12691        var lbl=document.createElement("div");lbl.className="caf-bk-inp-label";lbl.textContent=flabels[f.k];col.appendChild(lbl);
12692        var wrap=document.createElement("div");wrap.className="caf-bk-inp-wrap";
12693        var isValid=fvalidate[f.k]&&fvalidate[f.k](self.bkClient[f.k]);
12694        if(isValid&&self.bkClient[f.k])wrap.classList.add("caf-bk-inp-valid");
12695        var icon=document.createElement("div");icon.className="caf-bk-inp-icon";icon.innerHTML=ficons[f.k];wrap.appendChild(icon);
12696        var inp=document.createElement("input");inp.className="caf-bk-inp";inp.type=f.t;inp.placeholder=f.p;inp.value=self.bkClient[f.k];
12697        inp.setAttribute("aria-label",flabels[f.k]);inp.setAttribute("aria-required","true");
12698        /* attributi mobile-friendly per tastiera nativa corretta */
12699        var fAutoFs={nome:"given-name",cognome:"family-name",codfis:"off",email:"email",cellulare:"tel"};
12700        var fImFs={nome:"text",cognome:"text",codfis:"text",email:"email",cellulare:"tel"};
12701        inp.setAttribute("autocomplete",fAutoFs[f.k]||"off");
12702        inp.setAttribute("inputmode",fImFs[f.k]||"text");
12703        inp.setAttribute("enterkeyhint",f.k==="cellulare"?"done":"next");
12704        if(f.up)inp.setAttribute("autocapitalize","characters");
12705        if(f.mx)inp.maxLength=f.mx;if(f.up)inp.style.textTransform="uppercase";
12706        var chk=document.createElement("div");chk.className="caf-bk-inp-check";chk.innerHTML=checkSvg;wrap.appendChild(chk);
12707        inp.addEventListener("input",function(){
12708          var val=f.up?inp.value.toUpperCase():inp.value;
12709          if(f.k==="cellulare"){var formatted=formatPhoneIT(val);if(formatted!==val){inp.value=formatted;val=formatted;try{inp.setSelectionRange(formatted.length,formatted.length);}catch(_e){}}}
12710          self.bkClient[f.k]=val;
12711          if(fvalidate[f.k]&&fvalidate[f.k](val)){wrap.classList.add("caf-bk-inp-valid");inp.setAttribute("aria-invalid","false");}else{wrap.classList.remove("caf-bk-inp-valid");inp.setAttribute("aria-invalid",val?"true":"false");}
12712          if(self.bkFormErr){self.bkFormErr=null;var errEl=main.querySelector(".caf-bk-ferr");if(errEl)errEl.remove();}
12713          if(f.k==="email"){var oldSugg2=col.querySelector(".caf-bk-email-sugg");if(oldSugg2)oldSugg2.remove();}
12714          /* #1 — Auto-fill da Codice Fiscale: quando l'utente raggiunge 16
12715           * caratteri, decodifichiamo data nascita + sesso + comune codice
12716           * catastale. Mostriamo un chip di feedback. Nome/cognome NON sono
12717           * deterministicamente derivabili dal CF (sono solo consonanti) —
12718           * quindi non li auto-compiliamo, ma usiamo i dati estratti per
12719           * popolare lo state e validare coerenza.
12720           * Il chip è informativo: aiuta l'utente a verificare di aver
12721           * digitato il proprio CF e non quello di un'altra persona. */
12722          if(f.k==="codfis"){
12723            var oldChip=col.querySelector(".caf-bk-cf-chip");if(oldChip)oldChip.remove();
12724            if(val.length===16){
12725              var parsed=bkParseCodiceFiscale(val);
12726              if(parsed){
12727                /* Salva nello state per uso nel payload (sesso era hardcoded "M") */
12728                self.bkClient.sesso=parsed.sesso;
12729                self.bkClient._cfBirthDate=parsed.giorno+"/"+parsed.mese+"/"+parsed.anno;
12730                self.bkClient._cfComune=parsed.codiceCatastale;
12731                var chip=document.createElement("div");chip.className="caf-bk-cf-chip";
12732                var sessoLbl=parsed.sesso==="F"?bkT("cf_female"):bkT("cf_male");
12733                var dateStr=parsed.giorno+"/"+parsed.mese+"/"+parsed.anno;
12734                chip.innerHTML='<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round"><polyline points="20 6 9 17 4 12"/></svg>'
12735                  +'<span><b>'+escapeHtml(bkT("cf_valid_ok"))+'</b> '+escapeHtml(bkT("cf_valid_recognized",sessoLbl,dateStr,parsed.codiceCatastale))+'</span>';
12736                col.appendChild(chip);
12737              }else{
12738                var chipW=document.createElement("div");chipW.className="caf-bk-cf-chip warn";
12739                chipW.innerHTML='<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"><circle cx="12" cy="12" r="10"/><path d="M12 8v4M12 16h.01"/></svg>'
12740                  +'<span>'+escapeHtml(bkT("cf_invalid"))+'</span>';
12741                col.appendChild(chipW);
12742              }
12743            }
12744          }
12745        });
12746        if(f.k==="email"){
12747          inp.addEventListener("blur",function(){
12748            var v=inp.value.trim();if(!v||v.indexOf("@")<0)return;
12749            var sugg=suggestEmailDomain(v);if(!sugg)return;
12750            var existing=col.querySelector(".caf-bk-email-sugg");if(existing)existing.remove();
12751            var s=document.createElement("div");s.className="caf-bk-email-sugg";
12752            s.style.cssText="font-size:13px;color:#0369a1;background:#f0f9ff;border:1px solid #bae6fd;border-radius:8px;padding:8px 12px;margin-top:6px;cursor:pointer;display:flex;align-items:center;gap:8px";
12753            s.innerHTML='<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><circle cx="12" cy="12" r="10"/><path d="M12 16v-4M12 8h.01"/></svg> '+escapeHtml(bkT("label_did_you_mean"))+' <strong>'+escapeHtml(sugg)+'</strong> <span style="margin-left:auto;text-decoration:underline">'+escapeHtml(bkT("btn_use"))+'</span>';
12754            s.addEventListener("click",function(){inp.value=sugg;self.bkClient.email=sugg;wrap.classList.add("caf-bk-inp-valid");s.remove();self._bkLogEvent&&self._bkLogEvent("email_typo_corrected");});
12755            col.appendChild(s);
12756            self._bkLogEvent&&self._bkLogEvent("email_typo_suggested");
12757          });
12758        }
12759        inp.addEventListener("keydown",function(e){
12760          if(e.key==="Enter"){e.preventDefault();var all=main.querySelectorAll(".caf-bk-inp");var idx=-1;for(var ai=0;ai<all.length;ai++){if(all[ai]===inp){idx=ai;break;}}if(idx>=0&&idx<all.length-1)all[idx+1].focus();else self._bkConfirm();}
12761        });
12762        wrap.appendChild(inp);col.appendChild(wrap);fg.appendChild(col);
12763        _fsWrapsByKey[f.k]=wrap;_fsInpsByKey[f.k]=inp;
12764        if(f.k==="codfis"){
12765          _fsCfReasonBox=document.createElement("div");
12766          _fsCfReasonBox.className="caf-bk-cf-reason";
12767          var _fsCfReasonId=quickId();_fsCfReasonBox.id=_fsCfReasonId;_fsCfReasonBox.setAttribute("role","alert");inp.setAttribute("aria-describedby",_fsCfReasonId);
12768          _fsCfReasonBox.style.cssText="font-size:12px;line-height:1.35;margin:4px 2px 0;min-height:14px";
12769          col.appendChild(_fsCfReasonBox);
12770          var _fsRefreshCfReason=function(){
12771            var v=self.bkClient.codfis||"";
12772            if(!v){_fsCfReasonBox.textContent="";_fsCfReasonBox.style.color="";return;}
12773            var r=validateCodiceFiscale(v);
12774            if(r.ok){_fsCfReasonBox.textContent="✓ "+bkT("cf_valid_ok");_fsCfReasonBox.style.color="#047857";}
12775            else{_fsCfReasonBox.textContent=r.reason;_fsCfReasonBox.style.color="#be123c";}
12776          };
12777          inp.addEventListener("input",function(){_fsRefreshCfReason();self._bkCfFsApplyGate();});
12778          inp.addEventListener("blur",function(){_fsRefreshCfReason();});
12779          _fsRefreshCfReason();
12780        }
12781      });
12782      self._bkCfFsApplyGate();
12783      /* gender row rimosso dalla UI per scelta UX. Il default sesso:"M"
12784       * viene mantenuto nello state (bkClient) per compatibilità col payload
12785       * API del backend Planner che richiede comunque il campo. */
12786      main.appendChild(fg);
12787      if(self.bkFormErr){var fe=document.createElement("div");fe.className="caf-bk-ferr";fe.setAttribute("role","alert");fe.style.maxWidth="680px";fe.textContent=self.bkFormErr;main.appendChild(fe);}
12788      /* PRIVACY — consenso OBBLIGATORIO allo step Dati: la checkbox gating il
12789       * pulsante "Prosegui al riepilogo" (insieme al CF valido). Senza spunta
12790       * non si procede con la prenotazione. onToggle ri-applica il gate. */
12791      main.appendChild(self._bkBuildConsentBox(function(){if(typeof self._bkCfFsApplyGate==="function")self._bkCfFsApplyGate();}));
12792      self._bkCfFsApplyGate();
12793      /* #14 — attiva scroll-on-focus per gli input del form (mobile only) */
12794      setTimeout(function(){self._bkAttachKeyboardScroll(self._bkFsEl);},0);
12795      break;
12796    }
12797    case "confirm":{
12798      main.appendChild(mkHead(5,bkT("h_confirm"),bkT("s_confirm")));
12799      var sp=self.bkSelSlot||{},sd=self.bkSelSede||{},sv=self.bkSelServ||{},cdx=self.bkClient;
12800      /* #35 — premium summary card a 3 colonne con edit inline (#36) */
12801      var card=document.createElement("div");card.className="caf-bk-sum-premium";
12802      /* head */
12803      var head=document.createElement("div");head.className="caf-bk-sum-premium-head";
12804      head.innerHTML='<div class="caf-bk-sum-premium-check"><svg viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6L9 17l-5-5"/></svg></div>'
12805        +'<div><h3 class="caf-bk-sum-premium-h">'+escapeHtml(bkT("label_summary_premium_h"))+'</h3>'
12806        +'<div class="caf-bk-sum-premium-s">'+escapeHtml(bkT("label_summary_premium_s"))+'</div></div>';
12807      card.appendChild(head);
12808      /* cols: Quando / Dove / Cosa */
12809      var cols=document.createElement("div");cols.className="caf-bk-sum-premium-cols";
12810      function mkCol(iconSvg,h,line,sub,goStep){
12811        var col=document.createElement("div");col.className="caf-bk-sum-premium-col";
12812        var hd=document.createElement("div");hd.className="caf-bk-sum-premium-col-h";hd.innerHTML=iconSvg+' '+escapeHtml(h);col.appendChild(hd);
12813        var ln=document.createElement("div");ln.className="caf-bk-sum-premium-line";ln.textContent=line;col.appendChild(ln);
12814        if(sub){var sb=document.createElement("div");sb.className="caf-bk-sum-premium-sub";sb.textContent=sub;col.appendChild(sb);}
12815        if(goStep){
12816          var ed=document.createElement("button");ed.type="button";ed.className="caf-bk-sum-premium-edit";
12817          ed.innerHTML='<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 20h9"/><path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4 12.5-12.5z"/></svg> '+escapeHtml(bkT("btn_edit"));
12818          ed.addEventListener("click",function(){self._bkGoToStep(goStep);});
12819          col.appendChild(ed);
12820        }
12821        return col;
12822      }
12823      cols.appendChild(mkCol(
12824        '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"><circle cx="12" cy="12" r="10"/><path d="M12 6v6l4 2"/></svg>',
12825        bkT("label_when"),
12826        fmtDate(sp.data||"")+" · "+fmtTime(sp.ora_inizio||""),
12827        fmtTime(sp.ora_inizio||"")+" — "+fmtTime(sp.ora_fine||""),
12828        "selectSlot"
12829      ));
12830      cols.appendChild(mkCol(
12831        '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="10" r="3"/><path d="M20 10c0 6-8 12-8 12s-8-6-8-12a8 8 0 0 1 16 0Z"/></svg>',
12832        bkT("label_where"),
12833        sd.descrizione||"",
12834        (sd.indirizzo||"")+(sd.citta?", "+sd.citta:""),
12835        "selectSede"
12836      ));
12837      cols.appendChild(mkCol(
12838        '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><path d="M14 2v6h6"/></svg>',
12839        bkT("label_what"),
12840        sv.desonline||sv.descrizione||"",
12841        (+sv.tempo>0?bkT("label_form_duration_min",sv.tempo):""),
12842        "selectServizio"
12843      ));
12844      card.appendChild(cols);
12845      /* intestatario row (ulteriore) */
12846      var ownerCols=document.createElement("div");
12846ownerCols.className="caf-bk-sum-premium-cols";
12847      /* a11y #C27 (WCAG 1.4.10): niente "1fr 1fr" inline (non collassava mai):
12848       * auto-fit -> 2 colonne solo quando lo spazio basta, 1 colonna a 320px. */
12849      ownerCols.style.gridTemplateColumns="repeat(auto-fit,minmax(min(100%,200px),1fr))";
12850      ownerCols.appendChild(mkCol(
12851        '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"><circle cx="12" cy="8" r="4"/><path d="M4 21v-2a4 4 0 0 1 4-4h8a4 4 0 0 1 4 4v2"/></svg>',
12852        bkT("label_owner"),
12853        ((cdx&&cdx.nome)||"")+" "+((cdx&&cdx.cognome)||""),
12854        bkT("label_form_codfis")+": "+((cdx&&cdx.codfis)||"—"),
12855        "collectData"
12856      ));
12857      ownerCols.appendChild(mkCol(
12858        '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2Z"/><path d="m22 6-10 7L2 6"/></svg>',
12859        bkT("label_contacts"),
12860        (cdx&&cdx.email)||"—",
12861        (cdx&&cdx.cellulare)||"",
12862        "collectData"
12863      ));
12864      card.appendChild(ownerCols);
12865      /* #39 — disclaimer cancellazione */
12866      var disc=document.createElement("div");disc.className="caf-bk-sum-disclaimer";
12867      disc.innerHTML='<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"><circle cx="12" cy="12" r="10"/><path d="M12 8v4M12 16h.01"/></svg> '+escapeHtml(bkT("label_summary_disclaimer"));
12868      card.appendChild(disc);
12869      main.appendChild(card);
12870      /* #37 — pulsante conferma "mega" prominente */
12871      var confirmWrap=document.createElement("div");confirmWrap.style.cssText="margin:18px 0 6px;display:flex;justify-content:center";
12872      var bigConfirm=document.createElement("button");bigConfirm.type="button";bigConfirm.className="caf-bk-confirm-mega";
12873      bigConfirm.innerHTML='<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round"><path d="M20 6L9 17l-5-5"/></svg> '+escapeHtml(bkT("btn_confirm"));
12874      bigConfirm.addEventListener("click",function(){
12875        if(bigConfirm.disabled)return;
12876        if(!self.bkPrivacyOk){self._bkShowToast&&self._bkShowToast({type:"error",msg:bkT("consent_required")});return;}
12877        bigConfirm.disabled=true;
12878        bigConfirm.innerHTML='<span class="caf-bk-confirm-spinner"></span> '+escapeHtml(bkT("btn_confirming"));
12879        bkHaptic(15);
12880        self._bkSubmit();
12881      });
12882      confirmWrap.appendChild(bigConfirm);
12883      /* PRIVACY — checkbox di consenso obbligatoria sopra il bottone Conferma.
12884       * Il gate disabilita SIA il bottone "mega" nel corpo SIA il bottone
12885       * Conferma nello sticky footer (self._bkFsFootConfirmBtn, creato dopo, nel
12886       * footer), così tutti i CTA di conferma del layout fullscreen riflettono
12887       * in modo coerente lo stato del consenso. Azzero la ref del footer: verrà
12888       * ri-agganciata dal footer di QUESTO render. */
12889      self._bkFsFootConfirmBtn=null;
12890      var applyFsConsentGate=function(ok){
12891        var _btns=[bigConfirm,self._bkFsFootConfirmBtn];
12892        for(var _i=0;_i<_btns.length;_i++){var _b=_btns[_i];if(!_b)continue;
12893          _b.disabled=!ok;_b.style.opacity=ok?"":"0.5";_b.style.cursor=ok?"":"not-allowed";_b.style.filter=ok?"":"grayscale(0.35)";_b.title=ok?"":bkT("consent_required");}
12894      };
12895      self._bkFsConsentApplyGate=applyFsConsentGate;
12896      /* Il consenso viene raccolto allo step Dati. Qui lo ri-mostriamo SOLO se
12897       * non ancora dato (es. ripresa diretta sul riepilogo), così l'utente può
12898       * comunque acconsentire; il gate sui bottoni resta sempre attivo. */
12899      if(!self.bkPrivacyOk){main.appendChild(self._bkBuildConsentBox(applyFsConsentGate));}
12900      applyFsConsentGate(!!self.bkPrivacyOk);
12901      main.appendChild(confirmWrap);
12902      break;
12903    }
12904    case "done":{
12905      body.style.gridTemplateColumns="1fr";side.style.display="none";
12906      var dn=document.createElement("div");dn.className="caf-bk-fs-done";
12907      var dIc=document.createElement("div");
12907dIc.className="caf-bk-fs-done-ic";
12908      dIc.innerHTML='<svg viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6L9 17l-5-5"/></svg>';
12909      dn.appendChild(dIc);
12910      /* #31 — confetti SVG ricco (stelle/cerchi/cuori/diamanti) sull'intero
12911       * pannello "done", non solo sull'icona, per un effetto più festoso. */
12912      self._bkSpawnConfetti(dn,{count:36});
12913      var dt=document.createElement("div");dt.className="caf-bk-fs-done-t";dt.textContent=bkT("label_done_h");dn.appendChild(dt);
12914      var ds=document.createElement("div");ds.className="caf-bk-fs-done-s";ds.textContent=bkT("label_done_s");dn.appendChild(ds);
12915      if(self.bkId){var bid=document.createElement("div");bid.className="caf-bk-fs-done-id";bid.innerHTML='<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" style="width:12px;height:12px"><path d="M20 12V8l-8-5-8 5v8a4 4 0 0 0 4 4h4"/><path d="M9 12h6"/><path d="M9 16h3"/></svg> '+escapeHtml(bkT("label_done_id"))+escapeHtml(self.bkId);dn.appendChild(bid);}
12916      /* summary mini */
12917      var dss=document.createElement("div");dss.className="caf-bk-fs-done-sum caf-bk-fs-summary";
12918      var dspSlot=self.bkSelSlot||{};
12919      var dsRows=[
12920        [dateSvg,"Data",fmtDate(dspSlot.data||"")+" · "+fmtTime(dspSlot.ora_inizio||"")],
12921        [sedeSvg,"Sede",(self.bkSelSede||{}).descrizione||""],
12922        [servSvg,"Servizio",(self.bkSelServ||{}).desonline||(self.bkSelServ||{}).descrizione||""],
12923      ];
12924      dsRows.forEach(function(r){
12925        var row=document.createElement("div");row.className="caf-bk-fs-summary-row";
12926        var ic=document.createElement("div");ic.className="caf-bk-fs-summary-ic";ic.innerHTML=r[0];row.appendChild(ic);
12927        var bd=document.createElement("div");bd.className="caf-bk-fs-summary-body";
12928        var l=document.createElement("div");l.className="caf-bk-fs-summary-lbl";l.textContent=r[1];bd.appendChild(l);
12929        var v=document.createElement("div");v.className="caf-bk-fs-summary-val";v.textContent=r[2];bd.appendChild(v);
12930        row.appendChild(bd);dss.appendChild(row);
12931      });
12932      dn.appendChild(dss);
12933      var dActs=document.createElement("div");dActs.className="caf-bk-fs-done-actions";
12934      /* ICS calendar download */
12935      var btIcs=document.createElement("button");btIcs.type="button";btIcs.className="caf-bk-fs-btn caf-bk-fs-btn-ghost";
12936      btIcs.innerHTML='<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width:14px;height:14px;margin-right:6px;vertical-align:-2px"><rect x="3" y="4" width="18" height="18" rx="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/><path d="M12 14v4M10 16h4"/></svg> '+escapeHtml(bkT("label_add_calendar"));
12937      btIcs.addEventListener("click",function(){
12938        var ics=buildIcsForBooking({bookingId:self.bkId,sede:self.bkSelSede,servizio:self.bkSelServ,slot:self.bkSelSlot});
12939        if(ics)downloadIcs(ics,"caf-cisl-"+(self.bkId||"appuntamento")+".ics");
12940        self._bkLogEvent&&self._bkLogEvent("ics_downloaded");
12941      });
12942      dActs.appendChild(btIcs);
12943      /* WhatsApp share */
12944      var btWa=document.createElement("button");btWa.type="button";btWa.className="caf-bk-fs-btn caf-bk-fs-btn-ghost";
12945      btWa.innerHTML='<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="width:14px;height:14px;margin-right:6px;vertical-align:-2px"><path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z"/></svg> '+escapeHtml(bkT("label_share_whatsapp"));
12946      btWa.addEventListener("click",function(){
12947        var sp=self.bkSelSlot||{};var sd=self.bkSelSede||{};var sv=self.bkSelServ||{};
12948        var msg="Ho prenotato un appuntamento al CAF CISL: "+(sv.descrizione||"")+" il "+fmtDate(sp.data||"")+" alle "+fmtTime(sp.ora_inizio||"")+" presso "+(sd.descrizione||"")+(sd.indirizzo?" ("+sd.indirizzo+(sd.citta?", "+sd.citta:"")+")":"")+(self.bkId?" — ID: "+self.bkId:"");
12949        window.open(whatsappShareUrl(msg),"_blank","noopener");
12950        self._bkLogEvent&&self._bkLogEvent("whatsapp_shared");
12951      });
12952      dActs.appendChild(btWa);
12953      /* Mailto reminder */
12954      var btMl=document.createElement("a");
12954btMl.className="caf-bk-fs-btn caf-bk-fs-btn-ghost";
12955      btMl.href=mailtoReminder({email:self.bkClient.email,bookingId:self.bkId,sede:self.bkSelSede,servizio:self.bkSelServ,slot:self.bkSelSlot});
12956      btMl.style.cssText="text-decoration:none;display:inline-flex;align-items:center;gap:6px;cursor:pointer";
12957      btMl.innerHTML='<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width:14px;height:14px"><path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2Z"/><path d="m22 6-10 7L2 6"/></svg> '+escapeHtml(bkT("label_email_reminder"));
12958      btMl.addEventListener("click",function(){self._bkLogEvent&&self._bkLogEvent("mailto_clicked");});
12959      dActs.appendChild(btMl);
12960      var btClose=document.createElement("button");btClose.type="button";btClose.className="caf-bk-fs-btn caf-bk-fs-btn-pri";btClose.textContent=bkT("btn_close");
12961      btClose.addEventListener("click",function(){self._bkReset();});
12962      dActs.appendChild(btClose);
12963      dn.appendChild(dActs);
12964      /* Mini-survey post-booking */
12965      var srvFs=document.createElement("div");srvFs.style.cssText="margin-top:18px;padding:14px 18px;background:#f8fafc;border:1px solid #e2e8f0;border-radius:12px;text-align:center;max-width:420px";
12966      var srvFsLbl=document.createElement("div");srvFsLbl.style.cssText="font-size:13px;color:#475569;margin-bottom:8px;font-weight:600";srvFsLbl.textContent=bkT("label_done_survey");srvFs.appendChild(srvFsLbl);
12967      var srvFsRow=document.createElement("div");srvFsRow.style.cssText="display:flex;justify-content:center;gap:18px";
12968      [["happy","\uD83D\uDE0A",bkT("label_done_good")],["neutral","\uD83D\uDE10",bkT("label_done_meh")],["sad","\uD83D\uDE1E",bkT("label_done_bad")]].forEach(function(item){
12969        var b=document.createElement("button");b.type="button";b.style.cssText="background:none;border:none;font-size:30px;cursor:pointer;padding:6px 8px;border-radius:10px;transition:transform .2s,background .2s;line-height:1";
12970        b.title=item[2];b.textContent=item[1];
12971        b.addEventListener("click",function(){
12972          self._bkLogEvent&&self._bkLogEvent("post_booking_survey",{rating:item[0]});
12973          srvFsRow.innerHTML='<div style="font-size:13px;color:#047857;font-weight:600;padding:8px 0">'+escapeHtml(bkT("label_done_thanks"))+'</div>';
12974        });
12975        b.addEventListener("mouseenter",function(){b.style.transform="scale(1.2)";b.style.background="#e2e8f0";});
12976        b.addEventListener("mouseleave",function(){b.style.transform="";b.style.background="";});
12977        srvFsRow.appendChild(b);
12978      });
12979      srvFs.appendChild(srvFsRow);dn.appendChild(srvFs);
12980      main.appendChild(dn);
12981      break;
12982    }
12983    case "error":{
12984      body.style.gridTemplateColumns="1fr";side.style.display="none";
12985      var isRec=self.bkErrReturn!==null;
12986      var fr=self._bkFriendlyError(self.bkErrCode,self.bkErrMsg);
12987      var helpline=bkHelplineNumber(self.opts);
12988      var er=document.createElement("div");er.className="caf-bk-fs-center";
12989      var box=document.createElement("div");box.className="caf-bk-errbox";
12990      box.innerHTML=
12991        '<div class="caf-bk-errbox-ic"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"><circle cx="12" cy="12" r="10"/><path d="M12 8v4m0 4h.01"/></svg></div>'
12992        +'<h3 class="caf-bk-errbox-h">'+escapeHtml(fr.title)+'</h3>'
12993        +'<p class="caf-bk-errbox-msg">'+escapeHtml(fr.msg)+'</p>';
12994      var actsEl=document.createElement("div");actsEl.className="caf-bk-errbox-actions";
12995      /* Riprova: ricarica lo step relativo se possibile, altrimenti restart */
12996      var retryBtn=document.createElement("button");retryBtn.type="button";retryBtn.className="caf-bk-errbox-btn primary";
12997      retryBtn.innerHTML='<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"><path d="M3 12a9 9 0 0 1 14.5-7.1L21 8M21 3v5h-5M21 12a9 9 0 0 1-14.5 7.1L3 16M3 21v-5h5"/></svg> '+escapeHtml(bkT("btn_retry"));
12998      retryBtn.addEventListener("click",function(){
12999        if(isRec)self._bkBack(); else self._bkStart();
13000      });
13001      actsEl.appendChild(retryBtn);
13002      /* Indietro */
13003      if(isRec){
13004        var bb=document.createElement("button");bb.type="button";
13004bb.className="caf-bk-errbox-btn ghost";
13005        bb.innerHTML='<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"><polyline points="15 18 9 12 15 6"/></svg> '+escapeHtml(bkT("btn_change"));
13006        bb.addEventListener("click",function(){self._bkBack();});
13007        actsEl.appendChild(bb);
13008      }
13009      /* Numero verde fallback (#59) */
13010      if(helpline){
13011        var hl=document.createElement("a");hl.className="caf-bk-errbox-btn helpline";
13012        hl.href="tel:"+helpline.replace(/[^0-9+]/g,"");
13013        hl.innerHTML='<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7A2 2 0 0 1 22 16.92z"/></svg> '+escapeHtml(bkT("btn_call"))+' '+escapeHtml(helpline);
13014        hl.style.textDecoration="none";
13015        hl.addEventListener("click",function(){self._bkLogEvent&&self._bkLogEvent("helpline_clicked",{from:"error",code:self.bkErrCode});});
13016        actsEl.appendChild(hl);
13017      }
13018      /* Ricomincia da capo */
13019      var rs=document.createElement("button");rs.type="button";rs.className="caf-bk-errbox-btn ghost";
13020      rs.innerHTML='<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg> '+escapeHtml(bkT("btn_restart"));
13021      rs.addEventListener("click",function(){self._bkReset();self._bkStart();});
13022      actsEl.appendChild(rs);
13023      box.appendChild(actsEl);
13024      if(self.bkErrCode){var codeEl=document.createElement("div");codeEl.className="caf-bk-errbox-code";codeEl.textContent=bkT("label_error_code",self.bkErrCode);box.appendChild(codeEl);}
13025      er.appendChild(box);main.appendChild(er);
13026      /* WCAG 4.1.3 (#C3): errore (incluso esito incerto) annunciato alle AT */
13027      self._bkFsAnnounce(bkT("label_error")+": "+fr.title+(fr.msg?". "+fr.msg:""));
13028      break;
13029    }}
13030
13031    body.appendChild(main);
13032    el.appendChild(body);
13033
13034    /* ── MOBILE SUMMARY (bottom sheet, <820px) ── */
13035    if(self.bkStep!=="done"&&self.bkStep!=="error"&&self.bkStep!=="submitting"){
13036      var selectionsCount=0;
13037      if(self.bkSelProv)selectionsCount++;
13038      if(self.bkSelServ)selectionsCount++;
13039      if(self.bkSelSede)selectionsCount++;
13040      if(self.bkSelSlot)selectionsCount++;
13041      if(selectionsCount>0){
13042        var mbsum=document.createElement("div");mbsum.className="caf-bk-fs-mbsum";
13043        /* #13 — grab handle visivo + drag-to-toggle */
13044        var grab=document.createElement("div");grab.className="caf-bk-fs-mbsum-grab";grab.setAttribute("aria-hidden","true");
13045        mbsum.appendChild(grab);
13046        var summary=[];
13047        if(self.bkSelProv)summary.push("<b>"+escapeHtml(self.bkSelProv.descrizione)+"</b>");
13048        if(self.bkSelServ)summary.push("<b>"+escapeHtml(self.bkSelServ.desonline||self.bkSelServ.descrizione)+"</b>");
13049        if(self.bkSelSede)summary.push(escapeHtml(self.bkSelSede.descrizione));
13050        if(self.bkSelSlot)summary.push(escapeHtml(fmtDate(self.bkSelSlot.data)+" "+fmtTime(self.bkSelSlot.ora_inizio)));
13051        var row=document.createElement("div");row.className="caf-bk-fs-mbsum-row";
13052        var cnt=document.createElement("div");cnt.className="caf-bk-fs-mbsum-count";cnt.textContent=selectionsCount;row.appendChild(cnt);
13053        var txt=document.createElement("div");txt.className="caf-bk-fs-mbsum-txt";txt.innerHTML=summary.join(" · ");row.appendChild(txt);
13054        var tog=document.createElement("button");tog.type="button";tog.className="caf-bk-fs-mbsum-toggle";
13055        tog.innerHTML='Vedi <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" style="width:12px;height:12px"><polyline points="6 9 12 15 18 9"/></svg>';
13056        tog.addEventListener("click",function(){
13057          mbsum.classList.toggle("expanded");
13058          tog.innerHTML=mbsum.classList.contains("expanded")
13059            ?'Chiudi <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" style="width:12px;height:12px"><polyline points="18 15 12 9 6 15"/></svg>'
13060            :'Vedi <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" style="width:12px;height:12px"><polyline points="6 9 12 15 18 9"/></svg>';
13061        });
13062        row.appendChild(tog);mbsum.appendChild(row);
13063        /* expanded items */
13064        var exp=document.createElement("div");
13064exp.className="caf-bk-fs-mbsum-exp";
13065        function addItem(lbl,val){if(!val)return;var it=document.createElement("div");it.className="caf-bk-fs-mbsum-item";var l=document.createElement("span");l.className="lbl";l.textContent=lbl;it.appendChild(l);var v=document.createElement("span");v.className="val";v.textContent=val;it.appendChild(v);exp.appendChild(it);}
13066        if(self.bkSelProv)addItem("Provincia",self.bkSelProv.descrizione);
13067        if(self.bkSelServ)addItem("Servizio",self.bkSelServ.desonline||self.bkSelServ.descrizione);
13068        if(self.bkSelSede)addItem("Sede",self.bkSelSede.descrizione+" — "+(self.bkSelSede.indirizzo||"")+", "+(self.bkSelSede.citta||""));
13069        if(self.bkSelSlot)addItem("Orario",fmtDate(self.bkSelSlot.data)+" · "+fmtTime(self.bkSelSlot.ora_inizio));
13070        mbsum.appendChild(exp);
13071
13072        /* #13 — drag-to-expand: pointer/touch sul handle. Threshold 30px. */
13073        (function(){
13074          var startY=null,curY=null,wasExpanded=false;
13075          var onStart=function(ev){
13076            startY=(ev.touches?ev.touches[0].clientY:ev.clientY);
13077            wasExpanded=mbsum.classList.contains("expanded");
13078            mbsum.classList.add("dragging");
13079          };
13080          var onMove=function(ev){
13081            if(startY===null)return;
13082            curY=(ev.touches?ev.touches[0].clientY:ev.clientY);
13083            var dy=curY-startY;
13084            /* swipe UP (dy<0) e non espanso → preview espansione; swipe DOWN
13085             * (dy>0) e espanso → preview chiusura. */
13086            if(!wasExpanded&&dy<-10)mbsum.classList.add("expanded");
13087            else if(wasExpanded&&dy>10)mbsum.classList.remove("expanded");
13088          };
13089          var onEnd=function(){
13090            startY=null;curY=null;mbsum.classList.remove("dragging");
13091            /* Sync icona toggle button */
13092            var nowExp=mbsum.classList.contains("expanded");
13093            tog.innerHTML=nowExp
13094              ?'Chiudi <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" style="width:12px;height:12px"><polyline points="18 15 12 9 6 15"/></svg>'
13095              :'Vedi <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" style="width:12px;height:12px"><polyline points="6 9 12 15 18 9"/></svg>';
13096          };
13097          grab.addEventListener("touchstart",onStart,{passive:true});
13098          grab.addEventListener("touchmove",onMove,{passive:true});
13099          grab.addEventListener("touchend",onEnd);
13100          /* BUGFIX: mousemove/mouseup su window venivano aggiunti a OGNI
13101           * render della summary e mai rimossi (accumulo). Ora vivono solo
13102           * per la durata del drag. */
13103          var _dragUp=function(ev){
13104            window.removeEventListener("mousemove",onMove);
13105            window.removeEventListener("mouseup",_dragUp);
13106            onEnd(ev);
13107          };
13108          grab.addEventListener("mousedown",function(ev){
13109            onStart(ev);
13110            window.addEventListener("mousemove",onMove);
13111            window.addEventListener("mouseup",_dragUp);
13112          });
13113          /* tap sull'handle = toggle */
13114          grab.addEventListener("click",function(){tog.click();});
13115        })();
13116
13117        el.appendChild(mbsum);
13118      }
13119    }
13120
13121    /* ── FOOTER ── (hidden on done/error) */
13122    if(self.bkStep!=="done"&&self.bkStep!=="error"&&self.bkStep!=="submitting"){
13123      var foot=document.createElement("div");foot.className="caf-bk-fs-foot";
13124      var fl=document.createElement("div");fl.className="caf-bk-fs-foot-l";
13125      if(self.bkStep!=="selectProvincia"&&self.bkStep!=="loading"){
13126        var back=document.createElement("button");back.type="button";back.className="caf-bk-fs-btn caf-bk-fs-btn-ghost";
13127        back.innerHTML='<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"><polyline points="15 18 9 12 15 6"/></svg> '+escapeHtml(bkT("btn_back"));
13128        back.addEventListener("click",function(){self._bkBack();});
13129        fl.appendChild(back);
13130      }
13131      var cancel=document.createElement("button");cancel.type="button";
13131cancel.className="caf-bk-fs-btn caf-bk-fs-btn-danger";cancel.textContent=bkT("btn_cancel");
13132      cancel.addEventListener("click",function(){self._bkReset();});
13133      fl.appendChild(cancel);
13134      foot.appendChild(fl);
13135
13136      var fr=document.createElement("div");fr.className="caf-bk-fs-foot-r";
13137      /* Pulisco la ref al bottone "next" tra render diversi per evitare di
13138       * mutare un nodo DOM staccato in step !== collectData. */
13139      if(self.bkStep!=="collectData"){self._bkCfFsNextBtn=null;}
13140      if(self.bkStep!=="confirm"){self._bkFsFootConfirmBtn=null;}
13141      if(self.bkStep==="collectData"){
13142        var next=document.createElement("button");next.type="button";next.className="caf-bk-fs-btn caf-bk-fs-btn-pri";
13143        next.innerHTML=escapeHtml(bkT("btn_next"))+' <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"><polyline points="9 18 15 12 9 6"/></svg>';
13144        next.addEventListener("click",function(){
13145          if(!validateCodiceFiscale(self.bkClient.codfis||"").ok){
13146            self.bkFormErr=bkT("err_cf_proceed");
13147            self._bkRender();return;
13148          }
13149          if(!self.bkPrivacyOk){self._bkShowToast&&self._bkShowToast({type:"error",msg:bkT("consent_required")});return;}
13150          self._bkConfirm();
13151        });
13152        fr.appendChild(next);
13153        /* aggancia al gate CF rendered dal collectData */
13154        self._bkCfFsNextBtn=next;
13155        if(typeof self._bkCfFsApplyGate==="function")self._bkCfFsApplyGate();
13156      }else if(self.bkStep==="confirm"){
13157        var confirmBtn=document.createElement("button");confirmBtn.type="button";confirmBtn.className="caf-bk-fs-btn caf-bk-fs-btn-pri";
13158        confirmBtn.innerHTML='<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><path d="M20 6L9 17l-5-5"/></svg> '+escapeHtml(bkT("btn_confirm"));
13159        confirmBtn.addEventListener("click",function(){if(!self.bkPrivacyOk){self._bkShowToast&&self._bkShowToast({type:"error",msg:bkT("consent_required")});return;}self._bkSubmit();});
13160        fr.appendChild(confirmBtn);
13161        /* PRIVACY — questo CTA del footer condivide il gate di consenso con il
13162         * bottone "mega" del corpo: lo registro e applico subito lo stato. */
13163        self._bkFsFootConfirmBtn=confirmBtn;
13164        if(typeof self._bkFsConsentApplyGate==="function")self._bkFsConsentApplyGate(!!self.bkPrivacyOk);
13165      }
13166      foot.appendChild(fr);
13167      el.appendChild(foot);
13168    }
13169
13170    /* Voice mode: announce booking step via TTS */
13171    if(this._voiceMode){
13172      var _voiceSteps=["selectProvincia","selectServizio","selectSede","selectSlot","collectData","confirm","done","error"];
13173      if(_voiceSteps.indexOf(this.bkStep)!==-1){this._bkVoiceAnnounce();}
13174    }
13175  };
13176
13177  /* ══════════════════════════════════════════════════════════════
13178     ██  VOICE BOOKING – spoken interaction for each booking step
13179     ══════════════════════════════════════════════════════════════ */
13180
13181  /** Announce the current booking step options via TTS */
13182  CafChat.prototype._bkVoiceAnnounce = function () {
13183    var self = this;
13184    var announceId = self.bkStep + "_" + (self._bkVoiceField || "");
13185    if (self._bkVoiceAnnounced === announceId) return;
13186    self._bkVoiceAnnounced = announceId;
13187
13188    switch (self.bkStep) {
13189      case "selectProvincia": {
13190        var names = self.bkProvince.map(function(p) { return p.descrizione; }).join(", ");
13191        self._queueTTSChunk("Seleziona la provincia. Le province disponibili sono: " + names + ". Quale preferisci?");
13192        break;
13193      }
13194      case "selectServizio": {
13195        var srvList = self.bkServizi.slice(0, 6);
13196        var srvNames = srvList.map(function(s, i) { return (i+1) + ", " + (s.desonline || s.descrizione); }).join(". ");
13197        var extra = self.bkServizi.length > 6 ? ". E altri " + (self.bkServizi.length - 6) + " servizi" : "";
13198        self._queueTTSChunk("Scegli il servizio. I servizi disponibili sono: " + srvNames + extra + ". Quale vuoi prenotare? Puoi dire il nome o il numero.");
13199        break;
13200      }
13201      case "selectSede": {
13202        var sedeNames = self.bkSedi.map(function(s, i) { return (i+1) + ", " + s.descrizione + " a " + s.citta; }).join(". ");
13203        self._queueTTSChunk("Scegli la sede. Le sedi disponibili sono: " + sedeNames + ". Quale preferisci?");
13204        break;
13205      }
13206      case "selectSlot": {
13207        var slotDescs = self.bkSpazi.slice(0, 5).map(function(s, i) {
13208          return (i+1) + ", " + fmtDate(s.data) + " alle " + fmtTime(s.ora_inizio);
13209        }).join(". ");
13210        var slotExtra = self.bkSpazi.length > 5 ? ". Ci sono altri " + (self.bkSpazi.length - 5) + " orari disponibili." : "";
13211        self._queueTTSChunk("Scegli data e ora. Gli appuntamenti disponibili sono: " + slotDescs + slotExtra + ". Quale preferisci? Puoi dire il numero, il giorno, o l'orario.");
13212        break;
13213      }
13214      case "collectData": {
13215        self._bkVoiceField = self._bkVoiceField || "nome";
13216        if (self._bkVoiceField === "nome") {
13217          self._queueTTSChunk("Quasi fatto! Ho bisogno dei tuoi dati per completare la prenotazione. Dimmi il tuo nome.");
13218        }
13219        break;
13220      }
13221      case "confirm": {
13222        var sp = self.bkSelSlot || {};
13223        var sd = self.bkSelSede || {};
13224        var sv = self.bkSelServ || {};
13225        var cd = self.bkClient;
13226        self._queueTTSChunk(
13227          "Ecco il riepilogo della tua prenotazione. " +
13228          "Servizio: " + (sv.desonline || sv.descrizione) + ". " +
13229          "Sede: " + sd.descrizione + " a " + sd.citta + ". " +
13230          "Data: " + fmtDate(sp.data) + " alle " + fmtTime(sp.ora_inizio) + ". " +
13231          "A nome di " + cd.nome + " " + cd.cognome + ", " +
13232          "email " + cd.email + ", " +
13233          "cellulare " + cd.cellulare.split("").join(" ") + ". " +
13234          "Vuoi confermare? Rispondi s\u00ec, no, oppure modifica se vuoi cambiare qualcosa."
13235        );
13236        break;
13237      }
13238      case "done": {
13239        var doneMsg = "Prenotazione confermata con successo!";
13240        if (self.bkId) doneMsg += " Il numero di prenotazione \u00e8 " + self.bkId + ".";
13241        self._queueTTSChunk(doneMsg);
13242        break;
13243      }
13244      case "error": {
13245        self._queueTTSChunk((self.bkErrMsg || "Si \u00e8 verificato un errore.") + " Vuoi riprovare? Dì s\u00ec o annulla.");
13246        break;
13247      }
13248    }
13249  };
13250
13251  /** Handle spoken text during an active booking step */
13252  CafChat.prototype._bkHandleVoiceInput = function (text) {
13253    var self = this;
13254    var t = text.toLowerCase().trim();
13255
13256    /* If a fetch is in progress, inform user and ignore input */
13257    if (self._bkBusy) {
13258      self._queueTTSChunk("Un momento, sto ancora caricando. Riprova tra poco.");
13259      return;
13260    }
13261
13262    /* Global voice commands: cancel / back / repeat */
13263    if (/\b(annulla|cancella|esci dalla prenotazione|stop prenotazione)\b/i.test(t)) {
13264      self._bkReset();
13265      self._queueTTSChunk("Va bene, ho annullato la prenotazione.");
13266      return;
13267    }
13268    if (/\b(indietro|torna indietro|passo precedente)\b/i.test(t)) {
13269      self._bkBack();
13270      return;
13271    }
13272    /* "ripeti" / "non ho capito" — re-announce current step */
13273    if (/\b(ripeti|ripetere|non ho capito|puoi ripetere|cosa hai detto|ripeterm[ie]|di nuovo)\b/i.test(t)) {
13274      self._bkVoiceAnnounced = null;
13275      self._bkVoiceAnnounce();
13276      return;
13277    }
13278
13279    switch (self.bkStep) {
13280      case "selectProvincia": {
13281        var matched = matchProvince(t, self.bkProvince);
13282        if (matched) {
13283          self._queueTTSChunk("Provincia di " + matched.descrizione + ". Cerco i servizi disponibili.");
13284          self._bkPickProv(matched);
13285        } else {
13286          var num = parseVoiceNumber(t);
13287          if (num !== null && num >= 1 && num <= self.bkProvince.length) {
13288            var prov = self.bkProvince[num - 1];
13289            self._queueTTSChunk("Provincia di " + prov.descrizione + ". Cerco i servizi disponibili.");
13290            self._bkPickProv(prov);
13291          } else {
13292            self._bkVoiceAnnounced = null; /* allow re-announce */
13293            var pNames = self.bkProvince.map(function(p) { return p.descrizione; }).join(", ");
13294            self._queueTTSChunk("Non ho capito. Le province disponibili sono: " + pNames + ". Quale preferisci?");
13295          }
13296        }
13297        break;
13298      }
13299      case "selectServizio": {
13300        var bestServ = null, bestScore = 0;
13301        self.bkServizi.forEach(function(s) {
13302          var score = matchService(s, t);
13303          if (score > bestScore) { bestScore = score; bestServ = s; }
13304        });
13305        if (bestServ && bestScore >= 40) {
13306          self._queueTTSChunk("Servizio selezionato: " + (bestServ.desonline || bestServ.descrizione) + ". Cerco le sedi.");
13307          self._bkPickServ(bestServ);
13308        } else {
13309          var sNum = parseVoiceNumber(t);
13310          if (sNum !== null && sNum >= 1 && sNum <= self.bkServizi.length) {
13311            var srv = self.bkServizi[sNum - 1];
13312            self._queueTTSChunk("Servizio selezionato: " + (srv.desonline || srv.descrizione) + ". Cerco le sedi.");
13313            self._bkPickServ(srv);
13314          } else {
13315            self._bkVoiceAnnounced = null;
13316            var sNames = self.bkServizi.slice(0, 5).map(function(s, i) { return (i+1) + ", " + (s.desonline || s.descrizione); }).join(". ");
13317            self._queueTTSChunk("Non ho trovato il servizio. I servizi sono: " + sNames + ". Puoi dire il nome o il numero.");
13318          }
13319        }
13320        break;
13321      }
13322      case "selectSede": {
13323        var bestSede = matchSedeByVoice(t, self.bkSedi);
13324        if (bestSede) {
13325          self._queueTTSChunk("Sede selezionata: " + bestSede.descrizione + ". Cerco gli appuntamenti.");
13326          self._bkPickSede(bestSede);
13327        } else {
13328          var sdNum = parseVoiceNumber(t);
13329          if (sdNum !== null && sdNum >= 1 && sdNum <= self.bkSedi.length) {
13330            var sede = self.bkSedi[sdNum - 1];
13331            self._queueTTSChunk("Sede selezionata: " + sede.descrizione + ". Cerco gli appuntamenti.");
13332            self._bkPickSede(sede);
13333          } else {
13334            self._bkVoiceAnnounced = null;
13335            var sdNames = self.bkSedi.map(function(s, i) { return (i+1) + ", " + s.descrizione; }).join(". ");
13336            self._queueTTSChunk("Non ho individuato la sede. Le sedi sono: " + sdNames + ". Puoi dire il nome o il numero.");
13337          }
13338        }
13339        break;
13340      }
13341      case "selectSlot": {
13342        /* "altri orari" / "carica altri" / "vedi altri" → load more */
13343        if (/\b(altri|altre|carica|vedi|mostra|prossim[ie])\s*(orar[ie]|disponibilit|appuntament|dat[ae])?\b/i.test(t) && self.bkNextG) {
13344          self._queueTTSChunk("Cerco altri orari disponibili.");
13345          self._bkLoadMore();
13346          break;
13347        }
13348        /* try natural language match first (domani, lunedì, alle 10, etc.) */
13349        var slotMatch = matchSlotByVoice(t, self.bkSpazi);
13350        if (slotMatch) {
13351          self._queueTTSChunk("Appuntamento selezionato: " + fmtDate(slotMatch.data) + " alle " + fmtTime(slotMatch.ora_inizio) + ".");
13352          self._bkPickSlot(slotMatch);
13353        } else {
13354          /* fallback: try ordinal/number index */
13355          var slNum = parseVoiceNumber(t);
13356          if (slNum !== null && slNum >= 1 && slNum <= self.bkSpazi.length) {
13357            var slot = self.bkSpazi[slNum - 1];
13358            self._queueTTSChunk("Appuntamento selezionato: " + fmtDate(slot.data) + " alle " + fmtTime(slot.ora_inizio) + ".");
13359            self._bkPickSlot(slot);
13360          } else {
13361            self._bkVoiceAnnounced = null;
13362            var slDescs = self.bkSpazi.slice(0, 4).map(function(s, i) {
13363              return (i+1) + ", " + fmtDate(s.data) + " alle " + fmtTime(s.ora_inizio);
13364            }).join(". ");
13365            var helpMsg = "Non ho capito. Gli appuntamenti sono: " + slDescs + ". Puoi dire il numero, il giorno come domani o luned\u00ec, oppure l'orario come alle dieci.";
13366            if (self.bkNextG) helpMsg += " Dì 'altri orari' per vederne altri.";
13367            self._queueTTSChunk(helpMsg);
13368          }
13369        }
13370        break;
13371      }
13372      case "collectData": {
13373        self._bkHandleVoiceCollectData(t);
13374        break;
13375      }
13376      case "confirm": {
13377        if (/\b(s[i\u00ec]|conferm[ao]?|ok|va bene|perfetto|certo|procedi|invia)\b/i.test(t)) {
13378          self._queueTTSChunk("Invio la prenotazione.");
13379          self._bkSubmit();
13380        } else if (/\b(no|modific|cambi|correggi|sbagliato)\b/i.test(t)) {
13381          /* check if user wants to edit a specific field */
13382          if (/\b(nome|cognome|codice|fiscale|email|cellulare|telefono|numero)\b/i.test(t)) {
13383            /* go back to collectData and set the right field */
13384            self.bkStep = "collectData";
13385            if (/\bnome\b/i.test(t) && !/\bcognome\b/i.test(t)) self._bkVoiceField = "nome";
13386            else if (/\bcognome\b/i.test(t)) self._bkVoiceField = "cognome";
13387            else if (/\b(codice|fiscale)\b/i.test(t)) self._bkVoiceField = "codfis";
13388            else if (/\bemail\b/i.test(t)) self._bkVoiceField = "email";
13389            else if (/\b(cellulare|telefono|numero)\b/i.test(t)) self._bkVoiceField = "cellulare";
13390            else self._bkVoiceField = "nome";
13391            self._bkVoiceAnnounced = null;
13392            self._bkRender();
13393            self._queueTTSChunk("Ok, modifichiamo. Dimmi il nuovo valore.");
13394          } else {
13395            self._bkBack();
13396            self._queueTTSChunk("Torno alla modifica dei dati.");
13397          }
13398        } else {
13399          self._bkVoiceAnnounced = null;
13400          self._queueTTSChunk("Vuoi confermare la prenotazione? Rispondi s\u00ec, no, oppure dì cosa vuoi modificare.");
13401        }
13402        break;
13403      }
13404      case "error": {
13405        if (/\b(s[i\u00ec]|riprov|ricominc|riprova|ok|certo|va bene)\b/i.test(t)) {
13406          self._queueTTSChunk("Va bene, riprovo.");
13407          if (self.bkErrReturn) self._bkBack();
13408          else self._bkStart();
13409        } else if (/\b(no|chiudi|basta|esci|annull)\b/i.test(t)) {
13410          self._bkReset();
13411          self._queueTTSChunk("Ho chiuso la prenotazione.");
13412        } else {
13413          self._bkVoiceAnnounced = null;
13414          self._queueTTSChunk("Vuoi riprovare? Rispondi s\u00ec o no.");
13415        }
13416        break;
13417      }
13418    }
13419  };
13420
13421  /** Handle voice input for collectData step — one field at a time */
13422  CafChat.prototype._bkHandleVoiceCollectData = function (text) {
13423    var self = this;
13424    var field = self._bkVoiceField || "nome";
13425    var t = text.trim();
13426
13427    /* helper: clean name input — strip filler words STT may prepe
13427nd */
13428    function cleanName(raw) {
13429      return capitalizeFirst(
13430        raw.replace(/^(mi chiamo|il mio nome è|sono|il mio cognome è|di cognome)\s+/i, "")
13431           .replace(/\s+/g, " ").trim()
13432      );
13433    }
13434
13435    /* helper: confirm + move to next field */
13436    function nextField(currentVal, nextF, nextAnnounce, ttsMsg) {
13437      self._bkVoiceField = nextF;
13438      self._bkVoiceAnnounced = nextAnnounce;
13439      self._queueTTSChunk(currentVal + ". " + ttsMsg);
13440      self._bkRender();
13441    }
13442
13443    /* helper: ask user to correct current field */
13444    function retryField(ttsMsg) {
13445      self._bkVoiceAnnounced = "collectData_" + field + "_retry";
13446      self._queueTTSChunk(ttsMsg);
13447    }
13448
13449    /* "salta" / "non lo so" → skip optional-ish fields or ask again */
13450    var wantSkip = /\b(salta|saltare|non (lo )?so|passo|dopo)\b/i.test(t);
13451
13452    switch (field) {
13453      case "nome": {
13454        var nome = cleanName(t);
13455        if (nome.length < 2) { retryField("Non ho capito il nome. Come ti chiami?"); break; }
13456        self.bkClient.nome = nome;
13457        nextField(nome, "cognome", "collectData_cognome", "Qual \u00e8 il tuo cognome?");
13458        break;
13459      }
13460      case "cognome": {
13461        var cognome = cleanName(t);
13462        if (cognome.length < 2) { retryField("Non ho capito il cognome. Qual \u00e8 il tuo cognome?"); break; }
13463        self.bkClient.cognome = cognome;
13464        nextField(cognome, "codfis", "collectData_codfis", "Dimmi il tuo codice fiscale. Puoi dettarlo lettera per lettera.");
13465        break;
13466      }
13467      case "codfis": {
13468        var cf = t.replace(/\s+/g, "").toUpperCase();
13469        if (cf.length === 16) {
13470          self.bkClient.codfis = cf;
13471          nextField(cf, "email", "collectData_email", "Qual \u00e8 il tuo indirizzo email? Di' chiocciola per la at e punto per il punto.");
13472        } else if (cf.length > 10 && cf.length < 16) {
13473          /* close but not quite — likely STT error */
13474          retryField("Ho capito " + cf.length + " caratteri su 16. Il codice fiscale \u00e8 " + cf + ". Prova a ripeterlo pi\u00f9 lentamente, lettera per lettera.");
13475        } else {
13476          retryField("Il codice fiscale deve essere di 16 caratteri. Dettalo lettera per lettera, ad esempio: A, B, C, D...");
13477        }
13478        break;
13479      }
13480      case "email": {
13481        var email = voiceToEmail(t);
13482        /* fix common STT artifacts in emails */
13483        email = email.replace(/\.\./g, ".").replace(/@+/g, "@");
13484        if (email.indexOf("@") !== -1 && email.indexOf(".") !== -1 && email.length >= 6) {
13485          self.bkClient.email = email;
13486          nextField(email, "cellulare", "collectData_cellulare", "Qual \u00e8 il tuo numero di cellulare?");
13487        } else {
13488          retryField("Non ho capito l'email. Dilla lentamente, ad esempio: mario punto rossi chiocciola gmail punto com.");
13489        }
13490        break;
13491      }
13492      case "cellulare": {
13493        var phone = voiceToDigits(t);
13494        if (phone.length >= 9 && phone.length <= 15) {
13495          self.bkClient.cellulare = phone;
13496          /* read back the number for confirmation, then go straight to confirm */
13497          var phoneSpaced = phone.split("").join(" ");
13498          self._bkVoiceField = null;
13499          self.bkFormErr = null;
13500          self.bkStep = "confirm";
13501          self._bkVoiceAnnounced = null;
13502          self._queueTTSChunk(phoneSpaced + ". Perfetto, ho tutti i dati.");
13503          self._bkRender();
13504        } else if (phone.length > 0 && phone.length < 9) {
13505          retryField("Ho capito solo " + phone.length + " cifre: " + phone.split("").join(" ") + ". Il numero completo deve avere almeno 9 cifre. Ripeti per favore.");
13506        } else {
13507          retryField("Non ho capito il numero. Dillo cifra per cifra, ad esempio: tre tre otto, uno due tre, quattro cinque sei sette.");
13508        }
13509        break;
13510      }
13511    }
13512  };
13513
13514  /* ── send message ─────────────────────────────────────────── */
13515  CafChat.prototype._send = async function (text) {
13516    var trimmed = (text || "").trim();
13517    if (!trimmed || this.isLoading) return;
13518    /* block chat while booking active (but allow input during askLocation) */
13519    if (this.bkStep !== "idle" && this.bkStep !== "done" && this.bkStep !== "askLocation") {
13520      /* In voice mode, route spoken text to voice booking handler */
13521      if (this._voiceMode) {
13522        var vMsg = {id:quickId(),role:"user",content:trimmed,timestamp:Date.now()};
13523        this.messages.push(vMsg);
13524        this._renderVoiceTranscript();
13525        this._bkHandleVoiceInput(trimmed);
13526      }
13527      return;
13528    }
13529    if (this._needsEmail()) return;
13530    /* gate consenso privacy: nessun messaggio parte senza approvazione */
13531    if (this._needsConsent()) return;
13532
13533    this.inputEl.value = "";
13534    this._hideError();
13535
13536    /* ── booking: user answered location question ── */
13537    if (this.bkStep === "askLocation") {
13538      var self3 = this;
13539      self3.isLoading = true;
13540      self3._updateSendBtn();
13541      var locMsg = {id:quickId(),role:"user",content:trimmed,timestamp:Date.now()};
13542      self3.messages.push(locMsg);
13543      self3._renderMessages();
13544      if (self3._voiceMode) self3._queueTTSChunk("Un momento, cerco la provincia.");
13545
13546      /* fetch province: TUTTE le regioni se national, ALTRIMENTI solo la regione
13547       * ZONA preselezionata (vedi _bkRevalidateAllProvince). Su deploy ZONA il
13548       * match/picker resta quindi coerente con la regione effettivamente servita. */
13549      var allDataAL = {regioni:[],items:[]};
13550      try { allDataAL = await self3._bkFetchAllProvince(); }
13551      catch(e){ self3._bkLastErr = e; }
13552      var allProvinceAL = allDataAL.items.map(function(x){return x.provincia;});
13553      self3.isLoading = false;
13554
13555      if (!allProvinceAL.length) {
13556        var errM = {id:quickId(),role:"assistant",content:
13557          "Mi dispiace, il servizio di prenotazione non \u00e8 disponibile al momento. Riprova pi\u00f9 tardi.",
13558          timestamp:Date.now()};
13559        self3.messages.push(errM);
13560        self3.bkStep = "idle"; self3.bkHint = "";
13561        self3._renderMessages(); self3._updateSendBtn();
13562        if (self3._voiceMode) self3._queueTTSChunk(errM.content);
13563        /* Popup diagnostico con codice + motivo reale del Planner */
13564        self3._showPlannerDiagPopup();
13565        return;
13566      }
13567
13568      var matchedProv = matchProvince(trimmed, allProvinceAL);
13569      /* lista compatta delle regioni come hint testuale */
13570      var regNamesAL = allDataAL.regioni.map(function(r){return r.descrizione;}).join(", ");
13571
13572      if (matchedProv && self3._bkApplyMatchedProvincia(allDataAL, matchedProv)) {
13573        /* matched — proceed */
13574        var okM = {id:quickId(),role:"assistant",content:
13575          "Perfetto, provincia di **" + matchedProv.descrizione + "** (" + self3.bkSelRegione.descrizione + ")! "
13576          + (self3.bkHint ? "Cerco disponibilit\u00e0 per \"" + self3.bkHint + "\"..." : "Cerco i servizi disponibili..."),
13577          timestamp:Date.now()};
13578        self3.messages.push(okM);
13579        var bkM = {id:quickId(),role:"assistant",content:"@@BOOKING@@",timestamp:Date.now()};
13580        self3.messages.push(bkM);
13581        self3._renderMessages();
13582        if (self3._voiceMode) self3._queueTTSChunk("Perfetto, provincia di " + matchedProv.descrizione + "! Cerco i servizi disponibili.");
13583        self3.bkStep = "loading"; self3._bkBusy = false;
13584        self3._bkRender(); self3._updateSendBtn();
13585        self3._bkPickProv(matchedProv);
13586      } else {
13587        /* Not matched: show regione picker (UI step) so the user can drill down.
13588         * Singolare/plurale in base a quante regioni sono effettivamente servite
13589         * (su deploy ZONA \u00e8 una sola \u2192 evita "nelle regioni: Veneto"). */
13590        var oneRegAL = allDataAL.regioni.length === 1;
13591        var noM = {id:quickId(),role:"assistant",content:
13592          "Mi dispiace, non ho trovato **" + escapeHtml(trimmed) + "** tra le province disponibili.\n\n"
13593          + (oneRegAL
13594              ? "Il servizio \u00e8 attivo in **" + regNamesAL + "**.\n\nSeleziona la regione qui sotto."
13595              : "Il servizio \u00e8 attivo nelle regioni: **" + regNamesAL + "**.\n\nSeleziona la regione qui sotto."),
13596          timestamp:Date.now()};
13597        self3.messages.push(noM);
13598        self3.bkRegioni = allDataAL.regioni;
13599        var bkM2 = {id:quickId(),role:"assistant",content:"@@BOOKING@@",timestamp:Date.now()};
13600        self3.messages.push(bkM2);
13601        self3.bkStep = "selectRegione";
13602        self3._renderMessages(); self3._bkRender(); self3._updateSendBtn();
13603        if (self3._voiceMode) {
13604          var noMClean = noM.content.replace(/\*\*/g, "").replace(/\n+/g, " ");
13605          self3._queueTTSChunk(noMClean);
13606        }
13607      }
13608      return;
13609    }
13610
13611    /* RENDER OTTIMISTICO: mostra SUBITO il messaggio utente + i puntini "sta
13612     * scrivendo" PRIMA delle analisi intent (Claude booking/pwd + check-enabled),
13613     * che giravano in SERIE lasciando la chat ferma fino a ~9-13s sul 1° messaggio.
13614     * um + thinkingMsg sono riusati/rimpiazzati dai rami sottostanti. */
13615    var um = { id: quickId(), role: "user", content: trimmed, timestamp: Date.now() };
13616    var thinkingMsg = { id: quickId(), role: "assistant", content: "", timestamp: Date.now() };
13617    this.messages.push(um, thinkingMsg);
13618    this.isLoading = true;
13619    if (this.messagesEl) this.messagesEl.setAttribute("aria-busy", "true");
13620    if (this.srAnnouncer) this.srAnnouncer.textContent = bkT("assistant_typing");
13621    if (this._voiceMode) { this._renderVoiceTranscript(); this._updateVoiceUI(); }
13622    else { this._renderMessages(); }
13623    this._updateSendBtn();
13624
13625    /* ── booking intent detection — STRICT trigger ──
13626     * Il wizard di prenotazione viene avviato SOLO se l'utente scrive
13627     * letteralmente la frase "voglio prenotare" (eventualmente seguita
13628     * da uno dei servizi fiscali gestiti dal CAF: 730, ISEE/DSU, IMU,
13629     * Modello RED, INVCIV, ACCAS/PS, Successioni, Contratti di
13630     * locazione/cedolare secca, Delega Unica/deleghe fiscali, Bonus e
13631     * detrazioni fiscali, Visure catastali, Modello REDDITI/Unico,
13632     * Colf e badanti, qualsiasi altra pratica fiscale/tributaria/
13633     * dichiarativa, apertura Partita IVA con SAPI).
13634     * Per qualsiasi altro intento o servizio risponde il chatbot. */
13635    var VOGLIO_PRENOTARE_RE = /\bvoglio\s+prenotare\b/i;
13636    /* Servizi di competenza INCA / Patronato (NON CAF): se l'utente li
13637     * menziona insieme a "voglio prenotare", il wizard NON deve aprirsi —
13638     * risponde il chatbot. Lista: pensioni, NASpI/disoccupazione, assegni
13639     * familiari ANF, permessi di soggiorno, infortuni, malattia
13640     * professionale, maternità/congedi, riscatto laurea, ricongiunzioni
13641     * contributive, Assegno Unico Universale (INPS), vertenze sindacali,
13642     * contratti di lavoro/buste paga, licenziamenti/mobbing/dimissioni,
13643     * consulenza sindacale, domanda di riconoscimento invalidità civile. */
13644    var NON_CAF_SERVICE_RE = /\b(pension[ei]\b|naspi\b|disoccupazion|assegn[io]\s+familiar|anf\b|assegno\s+unico|permess[oi]\s+di\s+soggiorno|infortun[io]|malatt[ia][ae]?\s+professional|maternit|congedo|congedi|riscatto\s+laurea|ricongiunzion|vertenz|consulenza\s+sindacal|contratt[oi]\s+di\s+lavoro|bust[ae]\s+paga|licenziament|mobbing|dimission|(domanda|riconoscimento)\s+(?:di\s+)?invalidit)/i;
13645    if (this.bkStep === "idle" && VOGLIO_PRENOTARE_RE.test(trimmed) && !NON_CAF_SERVICE_RE.test(trimmed)) {
13646      var self2 = this;
13647      /* Intent (Claude) e check-enabled sono indipendenti → in PARALLELO
13648       * (prima erano due await in serie, ~4s + ~5s). */
13649      var _bkPar = await Promise.all([
13650        analyzeBookingIntent(self2.opts.baseUrl, trimmed),
13651        self2._bkCheckEnabled()
13652      ]);
13653      var intent = _bkPar[0];
13654      var en = _bkPar[1];
13655      /* client-side regex extractor — always runs, used to fill in missing hints */
13656      var clientIntent = clientExtractIntent(trimmed);
13657      var svcHint = (intent && intent.service) || clientIntent.service || "";
13658
13659      if (en) {
13660        /* Pre-fill bkClient con dati anagrafici estratti da Claude (solo se vuoti). */
13661        if (intent) {
13662          if (intent.nome && !self2.bkClient.nome) self2.bkClient.nome = intent.nome;
13663          if (intent.cognome && !self2.bkClient.cognome) self2.bkClient.cognome = intent.cognome;
13664          if (intent.email && !self2.bkClient.email) self2.bkClient.email = intent.email;
13665          if (intent.cellulare && !self2.bkClient.cellulare) self2.bkClient.cellulare = formatPhoneIT(intent.cellulare);
13666          if (intent.nome || intent.cognome || intent.email || intent.cellulare) {
13667            self2._bkLogEvent && self2._bkLogEvent("intent_personal_data_extracted", {
13668              hasNome: !!intent.nome, hasCognome: !!intent.cognome, hasEmail: !!intent.email, hasCellulare: !!intent.cellulare
13669            });
13670          }
13671        }
13672        /* um già mostrato (render ottimistico): rimpiazza il placeholder "sta
13673         * scrivendo" col bottone "Apri il wizard". */
13674        self2.messages = self2.messages.filter(function (m) { return m.id !== thinkingMsg.id; });
13675        var launchM = { id: quickId(), role: "assistant", content: "@@BOOKING_LAUNCH@@" + svcHint, timestamp: Date.now() };
13676        self2.messages.push(launchM);
13677        self2.isLoading = false;
13678        if (self2.messagesEl) self2.messagesEl.setAttribute("aria-busy", "false");
13679        /* WCAG 4.1.3 (#C4): la comparsa del bottone "Apri il wizard" va
13680         * annunciata — l'azzeramento sopprimeva qualunque annuncio. */
13681        if (self2.srAnnouncer) self2.srAnnouncer.textContent = bkT("sr_booking_launch");
13682        self2._renderMessages();
13683        self2._saveMessages();
13684        self2._updateSendBtn();
13685        if (self2._voiceMode) {
13686          self2._queueTTSChunk(svcHint
13687            ? "Perfetto! Clicca sul bottone per prenotare " + svcHint + "."
13688            : "Perfetto! Clicca sul bottone per aprire il wizard di prenotazione.");
13689        }
13690        return;
13691      }
13692    }
13693
13694    /* ── password reset intent detection (Claude + regex fallback) ── */
13695    if (this.bkStep === "idle" && this.pwdStep === "idle"
13696        && !(VOGLIO_PRENOTARE_RE.test(trimmed) && !NON_CAF_SERVICE_RE.test(trimmed))) {
13697      var selfPwd = this;
13698      /* Pre-filtro CHEAP: chiamiamo Claude (pwd-intent, ~4s) SOLO se ci sono
13699       * parole-chiave password. Prima girava per OGNI messaggio idle (anche
13700       * "voglio prenotare" \u2192 fall-through, +4s) bloccando la chat; ora i
13701       * messaggi normali saltano l'analisi e vanno dritti alla chat. */
13702      var maybePwd = PWD_RESET_KEYWORDS.test(trimmed);
13703      var pwdIntent = maybePwd ? await analyzePasswordResetIntent(selfPwd.opts.baseUrl, trimmed) : null;
13704      var isPwdIntent = maybePwd
13705        ? (pwdIntent ? !!pwdIntent.isPasswordReset : (PWD_RESET_KEYWORDS.test(trimmed) && PWD_RESET_CONTEXT.test(trimmed)))
13706        : false;
13707      if (isPwdIntent) {
13708        selfPwd.pwdEmail = (pwdIntent && pwdIntent.email) || extractEmail(trimmed) || selfPwd.userEmail || "";
13709        selfPwd.pwdStep = "form"; selfPwd.pwdErr = "";
13710        /* um gi\u00e0 mostrato (render ottimistico): rimuovi il placeholder "sta
13711         * scrivendo", aggiungi intro + form reset. */
13712        selfPwd.messages = selfPwd.messages.filter(function (m) { return m.id !== thinkingMsg.id; });
13713        var introPwd = { id: quickId(), role: "assistant", content:
13714          "Nessun problema! Reimpostare la password di **PersonalCAF** \u00e8 semplice. Inserisci la tua email qui sotto e ti invieremo subito il link per il reset.",
13715          timestamp: Date.now() };
13716        selfPwd.messages.push(introPwd);
13717        var pwM = { id: quickId(), role: "assistant", content: "@@PWDRESET@@", timestamp: Date.now() };
13718        selfPwd.messages.push(pwM);
13719        selfPwd.isLoading = false;
13720        if (selfPwd.messagesEl) selfPwd.messagesEl.setAttribute("aria-busy", "false");
13721        if (selfPwd.srAnnouncer) { var _pd = document.createElement("div"); _pd.innerHTML = renderMarkdown(introPwd.content, selfPwd.opts.color); selfPwd.srAnnouncer.textContent = (selfPwd.opts.name || bkT("sr_assistant")) + ": " + (_pd.textContent || ""); }
13722        selfPwd._renderMessages(); selfPwd._saveMessages();
13723        selfPwd._pwdRender();
13724        selfPwd._updateSendBtn();
13725        return;
13726      }
13727    }
13728
13729    var isVoiceRequest = !!this._lastMessageWasVoice;
13730    this._lastMessageWasVoice = false;
13731
13732    /* um + thinkingMsg già pushati e renderizzati (render ottimistico). Riusiamo
13733     * il placeholder come bolla assistant in streaming; um è escluso dalla
13734     * history perché viene già inviato come `message`. */
13735    var history = this.messages
13736      .filter(function (m) { return m.id !== "welcome" && m.id !== um.id && m.content && m.content.trim().length > 0 && m.content !== "@@BOOKING@@" && m.content !== "@@PWDRESET@@" && m.content.indexOf("@@BOOKING_LAUNCH@@") !== 0; })
13737      .map(function (m) { return { role: m.role, content: m.content }; });
13738
13739    var assistantMsg = thinkingMsg; /* isLoading già true; bolla già in DOM */
13740    this._updateSendBtn();
13741
13742    var apiUrl = this.opts.baseUrl + "/api/chat";
13743    var self = this;
13744    var chatController = new AbortController();
13745    this._chatAbort = chatController;
13746    /* Watchdog di INATTIVITÀ sullo stream chat: niente timeout fisso (troncherebbe
13747     * risposte lunghe ma sane), ma se non arriva NULLA (header o delta) per
13748     * STREAM_IDLE_MS abortiamo. Riarmato a ogni chunk. Senza questo, uno stallo
13749     * upstream lasciava l'utente sui puntini fino al timeout SERVER (60-120s). */
13750    var STREAM_IDLE_MS = 25000;
13751    self._chatTimedOut = false;
13752    var _chatIdleTid = null;
13753    var _armChatIdle = function(){ if(_chatIdleTid)clearTimeout(_chatIdleTid); _chatIdleTid=setTimeout(function(){ self._chatTimedOut=true; try{chatController.abort();}catch(_e){} }, STREAM_IDLE_MS); };
13754    var _clearChatIdle = function(){ if(_chatIdleTid){clearTimeout(_chatIdleTid);_chatIdleTid=null;} };
13755    _armChatIdle();
13756
13757    try {
13758      var response = await fetch(apiUrl, {
13759        method: "POST",
13760        headers: { "Content-Type": "application/json" },
13761        body: JSON.stringify({ message: trimmed, stream: true, history: history, conversationId: self.conversationId, voiceMode: isVoiceRequest }),
13762        signal: chatController.signal,
13763      });
13764
13765      if (!response.ok) {
13766        var errBody = null;
13767        try { errBody = await response.json(); } catch (e) { /* ok */ }
13768        var errMsg = errBody && errBody.error
13769          ? errBody.error + " (" + response.status + ")"
13770          : "HTTP " + response.status;
13771        throw new Error(errMsg);
13772      }
13773
13774      var contentType = response.headers.get("content-type") || "";
13775      var fullReply = "";
13776      var rawBuffer = "";  /* buffer raw byte-stream, pulito poi via parseMantisSentinels */
13777      var ttsSentUpTo = 0;
13778
13779      if (response.body && !contentType.includes("application/json")) {
13780        var reader = response.body.getReader();
13781        var decoder = new TextDecoder();
13782        var frameId = null;
13783        var lastRenderTime = 0;
13784        var minInterval = 80;
13785
13786        var scheduleUpdate = function () {
13787          if (frameId !== null) return;
13788          frameId = requestAnimationFrame(function () {
13789            frameId = null;
13790            var now = performance.now();
13791            if (now - lastRenderTime < minInterval && fullReply.length > 500) {
13792              frameId = requestAnimationFrame(function () {
13793                frameId = null;
13794                lastRenderTime = performance.now();
13795                assistantMsg.content = fullReply;
13796                if (self._voiceMode) self._updateVoiceStreamingBubble(fullReply);
13797                else self._updateStreamingBubble(fullReply);
13798              });
13799              return;
13800            }
13801            lastRenderTime = now;
13802            assistantMsg.content = fullReply;
13803            if (self._voiceMode) self._updateVoiceStreamingBubble(fullReply);
13804            else self._updateStreamingBubble(fullReply);
13805          });
13806        };
13807
13808        while (true) {
13809          var result = await reader.read();
13810          if (result.done) break;
13811          _armChatIdle(); /* attività: riarma il watchdog di inattività */
13812          rawBuffer += decoder.decode(result.value, { stream: true });
13813          /* Rimuovi sentinel <<<MANTIS_*>>> e [F1] inline references PRIMA
13814           * di mostrare/inviare a TTS. Robusto contro fragmentation TCP. */
13815          fullReply = parseMantisSentinels(rawBuffer).clean;
13816          scheduleUpdate();
13817
13818          /* TTS chunking: split at sentence boundaries */
13819          if (isVoiceRequest && self._voiceMode) {
13820            var minChars = ttsSentUpTo === 0 ? 12 : 60;
13821            var unsent = fullReply.length - ttsSentUpTo;
13822            if (unsent >= minChars) {
13823              var region = fullReply.slice(ttsSentUpTo);
13824              var bestBreak = -1;
13825              for (var ci = minChars; ci < Math.min(region.length, minChars + 100); ci++) {
13826                var ch = region[ci];
13827                if ((ch === "." || ch === "!" || ch === "?") && (ci + 1 >= region.length || region[ci + 1] === " " || region[ci + 1] === "\n")) {
13828                  bestBreak = ci + 1; break;
13829                }
13830              }
13831              if (bestBreak === -1) {
13832                for (var cj = minChars; cj < Math.min(region.length, minChars + 60); cj++) {
13833                  if (region[cj] === "," || region[cj] === "\n") { bestBreak = cj + 1; break; }
13834                }
13835              }
13836              if (bestBreak === -1 && unsent >= minChars + 60) {
13837                for (var ck = minChars; ck < Math.min(region.length, minChars + 60); ck++) {
13838                  if (region[ck] === " ") { bestBreak = ck + 1; break; }
13839                }
13840              }
13841              if (bestBreak > 0) {
13842                var chunk = region.slice(0, bestBreak).trim();
13843                ttsSentUpTo += bestBreak;
13844                if (chunk) self._queueTTSChunk(chunk);
13845              }
13846            }
13847          }
13848        }
13849        if (frameId !== null) cancelAnimationFrame(frameId);
13850        /* Final pass: cleanup definitivo (rimuove eventuali sentinel residui
13851         * e [F<n>] aperti a fine buffer). */
13852        var finalParsed = parseMantisSentinels(rawBuffer);
13853        fullReply = finalParsed.clean;
13854        /* AI Act art. 50(2): traccia il modello che ha generato la risposta
13855         * (dal sentinel MANTIS_RETRIEVED) per la marcatura data-model e la
13856         * persistenza lato server. */
13857        try {
13858          if (finalParsed.retrieved && (finalParsed.retrieved.model || finalParsed.retrieved.modelId)) {
13859            assistantMsg.model = String(finalParsed.retrieved.model || finalParsed.retrieved.modelId);
13860          }
13861        } catch (_e) {}
13862      } else {
13863        var data = await response.json();
13864        if (!data || !data.reply || typeof data.reply !== "string") {
13865          throw new Error("Invalid response");
13866        }
13867        /* Anche in modalità JSON non-stream, applichiamo il parser per
13868         * sicurezza (defense-in-depth: il server già fa stripReferencesBlock). */
13869        var _npParsed = parseMantisSentinels(data.reply.trim());
13870        fullReply = _npParsed.clean;
13871        /* AI Act art. 50(2): modello dal body JSON o dal sentinel. */
13872        try {
13873          var _npModel = (data.model || data.modelId) ||
13874            (_npParsed.retrieved && (_npParsed.retrieved.model || _npParsed.retrieved.modelId));
13875          if (_npModel) assistantMsg.model = String(_npModel);
13876        } catch (_e) {}
13877      }
13878
13879      assistantMsg.content = fullReply;
13880
13881      if (isVoiceRequest && self._voiceMode && fullReply) {
13882        var remaining = fullReply.slice(ttsSentUpTo).trim();
13883        if (remaining) self._queueTTSChunk(remaining);
13884      }
13885    } catch (err) {
13886      var isAbort = err instanceof DOMException && err.name === "AbortError";
13887      if (isAbort) {
13888        if (fullReply && fullReply.trim()) {
13889          assistantMsg.content = fullReply;
13890        } else {
13891          this.messages = this.messages.filter(function (m) { return m.id !== assistantMsg.id; });
13892          /* Abort da watchdog (stallo upstream) ≠ stop volontario dell'utente:
13893           * mostriamo un errore esplicito invece di sparire in silenzio. */
13894          if (self._chatTimedOut) this._showError("Il server ha impiegato troppo tempo a rispondere. Riprova tra poco.");
13895        }
13896      } else {
13897        this.messages = this.messages.filter(function (m) { return m.id !== assistantMsg.id; });
13898        this._showError(err && err.message ? err.message : "Ops! Non riesco a rispondere adesso. Riprova tra poco.");
13899      }
13900    } finally {
13901      _clearChatIdle();
13902      this.isLoading = false;
13903      if (this.messagesEl) this.messagesEl.setAttribute("aria-busy", "false");
13904      this._chatAbort = null;
13905      if (!this.isOpen && assistantMsg.content) {
13906        this.unreadCount++;
13907        this._updateBadge();
13908      }
13909      this._saveMessages();
13910      this._syncConversation();
13911      if (this._updateBkCta) this._updateBkCta();
13912      if (this._voiceMode) {
13913        this._renderVoiceTranscript();
13914        this._updateVoiceUI();
13915        if (!this._isSpeaking && !this._isPlayingQueue && !this._isListening) {
13916          this._scheduleListening(200);
13917        }
13918      } else {
13919        this._renderMessages();
13920      }
13921      if (self.srAnnouncer && assistantMsg && assistantMsg.content) {
13922        var _annDiv = document.createElement("div");
13923        _annDiv.innerHTML = renderMarkdown(assistantMsg.content, self.opts.color);
13924        self.srAnnouncer.textContent = (self.opts.name || bkT("sr_assistant")) + ": " + (_annDiv.textContent || "");
13925      }
13926      this._updateSendBtn();
13927      if (!this._voiceMode) this.inputEl.focus();
13928    }
13929  };
13930
13931  /* ── open / close ─────────────────────────────────────────── */
13932  CafChat.prototype.open = function (autoOpened) {
13933    this.isOpen = true;
13934    this._bubbleAltDone = true; /* a11y #C58: stop definitivo rotazione bubble */
13935    this.unreadCount = 0;
13936    this._updateBadge();
13937    this.panelEl.classList.add("open");
13938    this.toggleEl.classList.add("open");
13939    this.toggleEl.setAttribute("aria-expanded", "true");
13940    this.toggleEl.setAttribute("aria-label", bkT("aria_close_chat"));
13941    this.bubbleEl.style.display = "none";
13942
13943    /* fullscreen on mobile */
13944    if (window.innerWidth < 640) {
13945      this.panelEl.classList.add("caf-fullscreen");
13946      this.toggleEl.classList.add("caf-fs-hide");
13947      document.body.style.overflow = "hidden";
13948      document.body.style.touchAction = "none";
13949    }
13950
13951    var self = this;
13952    if (self._needsConsent() && self.consentOverlay) {
13953      self.consentOverlay.style.display = "flex";
13954      if (self.emailOverlay) self.emailOverlay.style.display = "none";
13955      self.messagesEl.style.display = "none";
13956      self._scrollWrap.style.display = "none";
13957      self._inputAreaEl.style.display = "none";
13958      if (!autoOpened) setTimeout(function () {
13959        var ccb = self.consentOverlay.querySelector(".caf-consent-cb");
13960        if (ccb) ccb.focus();
13961      }, 200);
13962    } else if (self._needsEmail() && self.emailOverlay) {
13963      if (self.consentOverlay) self.consentOverlay.style.display = "none";
13964      self.emailOverlay.style.display = "flex";
13965      self.messagesEl.style.display = "none";
13966      self._scrollWrap.style.display = "none";
13967      self._inputAreaEl.style.display = "none";
13968      if (!autoOpened) setTimeout(function () {
13969        var emailInput = self.emailOverlay.querySelector(".caf-email-input");
13970        if (emailInput) emailInput.focus();
13971      }, 200);
13972    } else {
13973      if (self.consentOverlay) self.consentOverlay.style.display = "none";
13974      if (self.emailOverlay) self.emailOverlay.style.display = "none";
13975      /* il consenso puo' essere stato concesso da un'ALTRA tab/istanza
13976         (_needsConsent legge il localStorage live): se il pannello era
13977         stato costruito col gate attivo, la UI chat va ripristinata. */
13978      self.messagesEl.style.display = "";
13979      self._scrollWrap.style.display = "";
13980      self._inputAreaEl.style.display = "";
13981      self._scrollToBottom();
13982      /* a11y #15: se l'apertura e' AUTOMATICA (timer 18s) NON si sposta il
13983       * focus: l'utente resta dove stava sulla pagina host (WCAG 2.4.3). */
13984      if (!autoOpened) setTimeout(function () {
13985        /* Non rubare il focus a un overlay modale (banner GDPR del wizard,
13986         * popup) apparso nel frattempo: il focus va al modale (WCAG 2.4.3;
13987         * era la causa del banner GDPR irraggiungibile con Tab in CDP). */
13988        var _m = self._trapTop && self._trapTop();
13989        if (_m && _m !== self.panelEl) {
13990          try { var _f = self._trapFocusables(_m); if (_f.length) { _f[0].focus(); return; } } catch (_e) {}
13991        }
13992        self.inputEl.focus();
13993      }, 200);
13994    }
13995    /* check if booking is enabled, then show persistent CTA */
13996    if (self._syncAiFooter) self._syncAiFooter();
13997    self._bkCheckEnabled().then(function () { self._updateBkCta(); });
13998    self._updateBkExpandBanner();
13999  };
14000
14001  /* BUGFIX (fascia bianca "sotto Assistente IA"): il footer di disclosure
14002   * sta in fondo al pannello DOPO input/messaggi; quando il gate consenso o
14003   * email li nasconde (display:none), il footer risale come primo elemento
14004   * visibile sotto l'header — doppione del sottotitolo + vuoto bianco del
14005   * gate subito sotto. Durante i gate il footer va nascosto (la disclosure
14006   * IA completa e' gia' NEL testo del gate) e ripristinato quando la chat
14007   * appare. Chiamare dopo OGNI toggle dei gate. */
14008  CafChat.prototype._syncAiFooter = function () {
14009    if (!this._aiFooterEl) return;
14010    var gateOn = false;
14011    try {
14012      var c = this.consentOverlay, e = this.emailOverlay;
14013      gateOn = !!((c && c.style.display !== "none" && c.getClientRects().length) ||
14014                  (e && e.style.display !== "none" && e.getClientRects().length));
14015    } catch (_e) {}
14016    this._aiFooterEl.style.display = gateOn ? "none" : "";
14017  };
14018
14019  CafChat.prototype.close = function () {
14020    if (this._bkCtaEl) this._bkCtaEl.style.display = "none";
14021    if (this._bkExpandBannerEl) this._bkExpandBannerEl.classList.remove("active");
14022    this.isOpen = false;
14023    if (this._bkFsMode) this._bkExitFs(true);
14024    if (this._voiceMode) this._exitVoiceMode();
14025    else if (this._isListening) this._abortListening();
14026    if (this.isFullscreen) this._exitFullscreen();
14027    this.panelEl.classList.remove("open", "caf-fullscreen");
14028    /* pulizia difensiva di eventuali quote inline (versioni col pin
14029     * tastiera): deve sempre comandare il CSS del pannello */
14030    this.panelEl.style.transition = "";
14031    this.panelEl.style.height = "";
14032    this.panelEl.style.top = "";
14033    this.panelEl.style.borderBottom = "";
14034    this.toggleEl.classList.remove("open", "caf-fs-hide");
14035    this.toggleEl.setAttribute("aria-expanded", "false");
14036    this.toggleEl.setAttribute("aria-label", this._toggleLabelClosed || bkT("aria_open_chat"));
14037    if (this.toggleEl && !this._voiceMode) { try { this.toggleEl.focus(); } catch (_e) {} }
14038    this.bubbleEl.style.display = this.opts.showBubble ? "" : "none";
14039    document.body.style.overflow = "";
14040    document.body.style.touchAction = "";
14041    /* Riavvia l'alternanza del bubble se la prenotazione è abilitata
14042     * (opts.showBubble true e _bkEnabled già valutato come true). */
14043    if (this.opts.showBubble && this._bkEnabled === true) {
14044      this._startBubbleAlternation();
14045    }
14046  };
14047
14048  /* ── desktop/tablet fullscreen toggle ─────────────────────── */
14049  CafChat.prototype._toggleFullscreen = function () {
14050    if (this.isFullscreen) this._exitFullscreen();
14051    else this._enterFullscreen();
14052  };
14053
14054  CafChat.prototype._enterFullscreen = function () {
14055    this.isFullscreen = true;
14056    this.panelEl.classList.add("caf-desktop-fs");
14057    this.toggleEl.classList.add("caf-fs-hide-desk");
14058    this.bubbleEl.classList.add("caf-fs-hide-desk");
14059    document.body.style.overflow = "hidden";
14060    this._fsBtnEl.innerHTML =
14061      '<svg viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" style="width:16px;height:16px">' +
14062        '<polyline points="1 6 1 1 6 1"/><polyline points="19 6 19 1 14 1"/><polyline points="1 14 1 19 6 19"/><polyline points="19 14 19 19 14 19"/>' +
14063      '</svg>';
14064    this._fsBtnEl.setAttribute("aria-label", bkT("aria_exit_fullscreen"));
14065    var self = this;
14066    setTimeout(function () { self._scrollToBottom(); }, 100);
14067  };
14068
14069  CafChat.prototype._exitFullscreen = function () {
14070    this.isFullscreen = false;
14071    this.panelEl.classList.remove("caf-desktop-fs");
14072    this.toggleEl.classList.remove("caf-fs-hide-desk");
14073    this.bubbleEl.classList.remove("caf-fs-hide-desk");
14074    document.body.style.overflow = "";
14075    this._fsBtnEl.innerHTML =
14076      '<svg viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" style="width:16px;height:16px">' +
14077        '<polyline points="6 1 1 1 1 6"/><polyline points="14 1 19 1 19 6"/>
14077<polyline points="6 19 1 19 1 14"/><polyline points="14 19 19 19 19 14"/>' +
14078      '</svg>';
14079    this._fsBtnEl.setAttribute("aria-label", bkT("aria_fullscreen"));
14080    var self = this;
14081    setTimeout(function () { self._scrollToBottom(); }, 100);
14082  };
14083
14084  /* ── helpers ───────────────────────────────────────────────── */
14085  CafChat.prototype._scrollToBottom = function () {
14086    var el = this.messagesEl;
14087    if (el) el.scrollTop = el.scrollHeight;
14088    this.isAtBottom = true;
14089    this._updateScrollBtn();
14090  };
14091
14092  CafChat.prototype._updateScrollBtn = function () {
14093    if (this.scrollBtn) {
14094      this.scrollBtn.style.display = this.isAtBottom ? "none" : "flex";
14095    }
14096  };
14097
14098  CafChat.prototype._updateMenu = function () {
14099    if (this.menuEl) {
14100      this.menuEl.classList.toggle("open", this.menuOpen);
14101    }
14102    var mt = this.panelEl && this.panelEl.querySelector(".caf-menu-trigger");
14103    if (mt) mt.setAttribute("aria-expanded", this.menuOpen ? "true" : "false");
14104  };
14105
14106  CafChat.prototype._updateSendBtn = function () {
14107    var bkActive = this.bkStep !== "idle" && this.bkStep !== "done" && this.bkStep !== "askLocation";
14108    /* Rete di sicurezza: se lo step di prenotazione e' attivo ma NON esiste
14109     * piu' alcuna UI per proseguirlo (niente overlay fullscreen, niente
14110     * widget inline nel flusso chat, niente banner "Espandi" per riprendere),
14111     * lo stato e' orfano: bloccare l'input renderebbe la chat inutilizzabile
14112     * senza via d'uscita. */
14113    if (bkActive && !this._bkFsMode) {
14114      var _bkUiReachable = (this._bkEl && this._bkEl.isConnected) ||
14115        (this._bkExpandBannerEl && this._bkExpandBannerEl.classList.contains("active"));
14116      if (!_bkUiReachable) bkActive = false;
14117    }
14118    /* In voice mode, never disable mic/input during booking — voice handles all steps */
14119    var blockInput = bkActive && !this._voiceMode;
14120    this.sendBtn.disabled = this.isLoading || blockInput;
14121    this.inputEl.disabled = blockInput;
14122    /* WCAG 3.1.2 (#C48): placeholder dinamico localizzato (ph_write_message =
14123     * accessible name dell'input). */
14124    this.inputEl.placeholder = this.bkStep === "askLocation"
14125      ? bkT("ph_your_city")
14126      : bkActive && !this._voiceMode ? bkT("ph_follow_steps") : bkT("ph_write_message");
14127    if (this.micBtn) this.micBtn.disabled = this.isLoading || blockInput;
14128  };
14129
14130  CafChat.prototype._translateMessage = async function (content, langCode) {
14131    if (this.isLoading) return;
14132    var langName = LANG_NAMES[langCode] || langCode;
14133    var self = this;
14134    var assistantMsg = {
14135      id: quickId(),
14136      role: "assistant",
14137      transLang: langCode,
14138      content: "",
14139      timestamp: Date.now()
14140    };
14141    this.messages.push(assistantMsg);
14142    this.isLoading = true;
14143    if (this.messagesEl) this.messagesEl.setAttribute("aria-busy", "true");
14144    if (this.srAnnouncer) this.srAnnouncer.textContent = bkT("assistant_typing");
14145    this._renderMessages();
14146    this._updateSendBtn();
14147
14148    var apiUrl = this.opts.baseUrl + "/api/chat";
14149    /* AbortController + watchdog di inattività (come il send normale): senza,
14150     * un /api/chat appeso bloccava input/send/mic finché il server non chiudeva
14151     * (fino a ~120s). Agganciato a _chatAbort così "stop"/"nuova conversazione"
14152     * può annullare anche la traduzione. */
14153    var tCtrl = new AbortController();
14154    this._chatAbort = tCtrl;
14155    var _tIdleTid = null;
14156    var _armTIdle = function(){ if(_tIdleTid)clearTimeout(_tIdleTid); _tIdleTid=setTimeout(function(){ try{tCtrl.abort();}catch(_e){} }, 25000); };
14157    var _clearTIdle = function(){ if(_tIdleTid){clearTimeout(_tIdleTid);_tIdleTid=null;} };
14158    _armTIdle();
14159    try {
14160      var response = await fetch(apiUrl, {
14161        method: "POST",
14162        headers: { "Content-Type": "application/json" },
14163        body: JSON.stringify({
14164          message: "Traduci il seguente testo in " + langName + ". Rispondi solo con la traduzione.\n\n" + content,
14165          stream: true,
14166          conversationId: self.conversationId
14167        }),
14168        signal: tCtrl.signal,
14169      });
14170      if (!response.ok) throw new Error("HTTP " + response.status);
14171
14172      var ct = response.headers.get("content-type") || "";
14173      var fullReply = "";
14174      var rawBufferT = "";  /* buffer raw, ripulito via parseMantisSentinels */
14175
14176      if (response.body && !ct.includes("application/json")) {
14177        var reader = response.body.getReader();
14178        var decoder = new TextDecoder();
14179        var frameId = null;
14180        var scheduleUpdate = function () {
14181          if (frameId !== null) return;
14182          frameId = requestAnimationFrame(function () {
14183            frameId = null;
14184            assistantMsg.content = "Traduzione (" + langName + ") · " + bkT("translate_ai_note") + ":\n" + fullReply;
14185            self._updateStreamingBubble(assistantMsg.content);
14186          });
14187        };
14188        while (true) {
14189          var result = await reader.read();
14190          if (result.done) break;
14191          _armTIdle();
14192          rawBufferT += decoder.decode(result.value, { stream: true });
14193          fullReply = parseMantisSentinels(rawBufferT).clean;
14194          scheduleUpdate();
14195        }
14196        if (frameId !== null) cancelAnimationFrame(frameId);
14197        fullReply = parseMantisSentinels(rawBufferT).clean;
14198      } else {
14199        var data = await response.json();
14200        if (data && data.reply) fullReply = parseMantisSentinels(data.reply.trim()).clean;
14201      }
14202      assistantMsg.content = "Traduzione (" + langName + ") · " + bkT("translate_ai_note") + ":\n" + fullReply;
14203    } catch (err) {
14204      this.messages = this.messages.filter(function (m) { return m.id !== assistantMsg.id; });
14205      this._showError("Traduzione non riuscita. Riprova.");
14206    } finally {
14207      _clearTIdle();
14208      if (this._chatAbort === tCtrl) this._chatAbort = null;
14209      this.isLoading = false;
14210      if (this.messagesEl) this.messagesEl.setAttribute("aria-busy", "false");
14211      this._saveMessages();
14212      this._renderMessages();
14213      if (this.srAnnouncer && assistantMsg && assistantMsg.content) {
14214        var _tDiv = document.createElement("div");
14215        _tDiv.innerHTML = renderMarkdown(assistantMsg.content, this.opts.color);
14216        this.srAnnouncer.textContent = (this.opts.name || bkT("sr_assistant")) + ": " + (_tDiv.textContent || "");
14217      }
14218      this._updateSendBtn();
14219    }
14220  };
14221
14222  CafChat.prototype._updateRatingButtons = function () {
14223    var good = this.menuEl.querySelector(".caf-rate-good");
14224    var bad = this.menuEl.querySelector(".caf-rate-bad");
14225    if (!good || !bad) return;
14226    good.className = "caf-rate-btn caf-rate-good" + (this.rating === "good" ? " selected-good" : "");
14227    bad.className = "caf-rate-btn caf-rate-bad" + (this.rating === "bad" ? " selected-bad" : "");
14228    good.setAttribute("aria-pressed", this.rating === "good" ? "true" : "false");
14229    bad.setAttribute("aria-pressed", this.rating === "bad" ? "true" : "false");
14230  };
14231
14232  /* Invia il rating al server-side endpoint, che lo scrive su Firestore.
14233   * Riusa /api/conversations passando metadata.rating. Fire-and-forget. */
14234  CafChat.prototype._syncRating = function () {
14235    if (this.opts.privacy) return;
14236    /* Stesso gate consenso di _syncConversation: nessuna persistenza server
14237     * prima del consenso o dopo la revoca (GDPR artt. 6, 7(3)). */
14238    if (this.opts.consenso && this._needsConsent && this._needsConsent()) return;
14239    if (!this.conversationId || !this.rating) return;
14240
14241    var self = this;
14242    var msgs = this.messages.filter(function (m) {
14243      if (!m || !m.content || typeof m.content !== "string") return false;
14244      if (m.content === "@@BOOKING@@") return false;
14245      if (m.content === "@@PWDRESET@@") return false;
14246      if (m.content.indexOf("@@BOOKING_LAUNCH@@") === 0) return false;
14247      return m.role === "user" || m.role === "assistant";
14248    }).map(function (m) {
14249      return { id: m.id, role: m.role, content: m.content, timestamp: m.timestamp || Date.now() };
14250    });
14251
14252    var apiUrl = (this.opts.baseUrl || "") + "/api/conversations";
14253    fetch(apiUrl, {
14254      method: "POST",
14255      headers: { "Content-Type": "application/json" },
14256      body: JSON.stringify({
14257        conversationId: self.conversationId,
14258        messages: msgs,
14259        metadata: {
14260          /* minimizzazione: solo origin+pathname, niente email (non serve al rating). */
14261          pageUrl: (function(){ try { return (typeof location !== "undefined") ? (location.origin + (location.pathname || "")) : ""; } catch (_e) { return ""; } })(),
14262          source: "widget-inline",
14263          rating: self.rating
14264        }
14265      }),
14266      keepalive: true
14267    }).catch(function () { /* silent */ });
14268  };
14269
14270  CafChat.prototype._updateBadge = function () {
14271    if (!this.badgeEl) return;
14272    if (this.unreadCount > 0 && !this.isOpen) {
14273      this.badgeEl.textContent = this.unreadCount > 9 ? "9+" : String(this.unreadCount);
14274      this.badgeEl.style.display = "flex";
14275    } else {
14276      this.badgeEl.style.display = "none";
14277    }
14278  };
14279
14280  /* ══════════════════════════════════════════════════════════════
14281     VOICE MODE – full conversational voice (STT + TTS + VAD)
14282     ══════════════════════════════════════════════════════════════ */
14283
14284  /* ── beep ───────────────────────────────────────────────────── */
14285  CafChat.prototype._playBeep = function (freq, durationMs, volume) {
14286    try {
14287      var ctx = new AudioContext();
14288      var osc = ctx.createOscillator();
14289      var gain = ctx.createGain();
14290      osc.type = "sine";
14291      osc.frequency.value = freq;
14292      gain.gain.setValueAtTime(volume || 0.12, ctx.currentTime);
14293      gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + durationMs / 1000);
14294      osc.connect(gain).connect(ctx.destination);
14295      osc.start();
14296      osc.stop(ctx.currentTime + durationMs / 1000);
14297      osc.onended = function () { ctx.close(); };
14298    } catch (e) { /* silent */ }
14299  };
14300
14301  /* ── TTS helpers ────────────────────────────────────────────── */
14302  var TTS_DIGITS = ["zero","uno","due","tre","quattro","cinque","sei","sette","otto","nove"];
14303
14304  var TTS_PRONUNCIATIONS = {
14305    "CAF": "caf", "CISL": "cisl", "CGIL": "cgil", "UIL": "uil",
14306    "INPS": "inps", "INAIL": "inail", "ISEE": "isee", "IRPEF": "irpef",
14307    "CUD": "cud", "CU": "cu", "IMU": "imu", "TARI": "tari", "TASI": "tasi",
14308    "SPID": "spid", "CIE": "cie", "PEC": "pec", "ASL": "asl",
14309    "NASPI": "naspi", "RED": "red", "DSU": "dsu", "RAL": "ral",
14310    "IVA": "iva", "IBAN": "iban", "PIN": "pin", "OTP": "otp",
14311    "PDF": "pdf", "FAQ": "faq",
14312    "730": "sette trenta", "740": "sette quaranta", "770": "sette settanta",
14313    "F24": "effe ventiquattro", "F23": "effe ventitré",
14314  };
14315  var TTS_PRON_REGEX = new RegExp("\\b(" + Object.keys(TTS_PRONUNCIATIONS).join("|") + ")\\b", "g");
14316
14317  CafChat.prototype._cleanForTTS = function (raw) {
14318    return raw
14319      .replace(/\[([^\]]*)\]\([^)]*\)/g, "$1")
14320      .replace(/https?:\/\/[^\s)]+/gi, "")
14321      .replace(/www\.[^\s)]+/gi, "")
14322      .replace(/\*\*([^*]*)\*\*/g, "$1")
14323      .replace(/[*_~`#]/g, "")
14324      .replace(/^\s*[-\u2022]\s+/gm, "")
14325      .replace(/^\s*(\d+)\.\s+(?=[A-Z\u00C0-\u00DC])/gm, "$1, ")
14326      .replace(/[\u{1F600}-\u{1F9FF}\u{1FA00}-\u{1FA6F}\u{2600}-\u{27BF}]/gu, "")
14327      .replace(/\((?![+\d])([^)]{0,8})\)/g, "")
14328      .replace(TTS_PRON_REGEX, function (m) { return TTS_PRONUNCIATIONS[m] || m; })
14329      .replace(/\b([A-Z]{2,6})\b/g, function (m) { return m.charAt(0) + m.slice(1).toLowerCase(); })
14330      .replace(/\(?\+?\d[\d\s.()\-]{5,}\d\)?/g, function (m) {
14331        var d = m.replace(/[^\d]/g, ""); if (d.length < 7) return m;
14332        return m.match(/\d+/g).map(function (g) {
14333          return g.split("").map(function (c) { return TTS_DIGITS[parseInt(c)]; }).join(" ");
14334        }).join(" - ");
14335      })
14336      .replace(/---+/g, "")
14337      .replace(/\.{3,}/g, "...")
14338      .replace(/\s*[,;]\s*[,;]/g, ",")
14339      .replace(/\n{2,}/g, ". ")
14340      .replace(/\n/g, ", ")
14341      .replace(/\s{2,}/g, " ")
14342      .trim();
14343  };
14344
14345  /** Speak via browser speechSynthesis (fallback when /api/tts is unavailable).
14346   *  Returns a Promise that resolves when speech finishes. */
14347  CafChat.prototype._speakBrowserTTS = function (text) {
14348    var self = this;
14349    return new Promise(function (resolve) {
14350      if (!window.speechSynthesis) { resolve(); return; }
14351      /* Chrome loads voices async — wait for them if empty */
14352      function doSpeak() {
14353        /* workaround: Chrome sometimes stalls speechSynthesis — cancel any stale queue */
14354        window.speechSynthesis.cancel();
14355        var utter = new SpeechSynthesisUtterance(text);
14356        utter.lang = "it-IT";
14357        utter.rate = 1.05;
14358        utter.pitch = 1.0;
14359        /* try to pick an Italian voice */
14360        var voices = window.speechSynthesis.getVoices();
14361        var itVoice = voices.filter(function(v){ return v.lang && v.lang.
14361indexOf("it") === 0; });
14362        if (itVoice.length) {
14363          /* prefer more natural voices (non-compact, non-Google basic) */
14364          var preferred = itVoice.filter(function(v){ return !v.name.match(/Google/i) && !v.localService; });
14365          if (!preferred.length) preferred = itVoice.filter(function(v){ return !v.name.match(/compact/i); });
14366          utter.voice = preferred.length ? preferred[0] : itVoice[0];
14367        }
14368        var resolved = false;
14369        var done = function () { if (resolved) return; resolved = true; self._currentUtterance = null; resolve(); };
14370        utter.onend = done;
14371        utter.onerror = done;
14372        self._currentUtterance = utter;
14373        window.speechSynthesis.speak(utter);
14374        /* safety timeout */
14375        setTimeout(done, Math.max(text.length * 80, 8000));
14376      }
14377      var voices = window.speechSynthesis.getVoices();
14378      if (voices.length === 0 && window.speechSynthesis.onvoiceschanged !== undefined) {
14379        window.speechSynthesis.onvoiceschanged = function () {
14380          window.speechSynthesis.onvoiceschanged = null;
14381          doSpeak();
14382        };
14383        /* if voices still don't load after 500ms, speak anyway */
14384        setTimeout(function () { window.speechSynthesis.onvoiceschanged = null; doSpeak(); }, 500);
14385      } else {
14386        doSpeak();
14387      }
14388    });
14389  };
14390
14391  CafChat.prototype._queueTTSChunk = function (text) {
14392    var cleaned = this._cleanForTTS(text);
14393    if (!cleaned) return;
14394    var self = this;
14395    var controller = new AbortController();
14396    this._ttsAbortControllers.push(controller);
14397
14398    /* a11y #C73: /api/tts puo' non esistere sul deployment. Al primo
14399     * 404/errore di rete memoizziamo il fallimento (flag condiviso): i chunk
14400     * successivi saltano la fetch e vanno diretti al fallback speechSynthesis.
14401     * Quando l'endpoint esiste il comportamento resta IDENTICO (il flag non
14402     * viene mai impostato su risposta ok; l'abort utente non conta). */
14403    var promise = CafChat._ttsUnavailable
14404      ? Promise.resolve(null)
14405      : fetch(this.opts.baseUrl + "/api/tts", {
14406          method: "POST",
14407          headers: { "Content-Type": "application/json" },
14408          body: JSON.stringify({ text: cleaned }),
14409          signal: controller.signal,
14410        })
14411          .then(function (r) { if (!r.ok) { CafChat._ttsUnavailable = true; return null; } return r.blob(); })
14412          .then(function (b) { return b ? URL.createObjectURL(b) : null; })
14413          .catch(function (err) { if (!(err && err.name === "AbortError")) CafChat._ttsUnavailable = true; return null; });
14414
14415    /* wrap: if server TTS fails, fall back to browser speechSynthesis */
14416    var wrappedPromise = promise.then(function (url) {
14417      if (url) return { type: "audio", url: url };
14418      /* fallback to browser TTS */
14419      return { type: "browser", text: cleaned };
14420    });
14421
14422    this._audioQueue.push(wrappedPromise);
14423
14424    // Cancel any pending listen timer — mic must never open during TTS
14425    this._scheduleListening(Infinity);
14426    if (!this._isPlayingQueue) {
14427      this._isPlayingQueue = true;
14428      this._isSpeaking = true;
14429      if (this._isListening) this._stopListening();
14430      this._updateVoiceUI();
14431      this._drainAudioQueue();
14432    }
14433  };
14434
14435  CafChat.prototype._drainAudioQueue = async function () {
14436    var epoch = this._ttsEpoch;
14437    while (this._audioQueue.length > 0 && this._ttsEpoch === epoch) {
14438      var item = await this._audioQueue[0];
14439      this._audioQueue.shift();
14440      if (!item || this._ttsEpoch !== epoch) {
14441        if (item && item.type === "audio" && item.url) URL.revokeObjectURL(item.url);
14442        break;
14443      }
14444      var self = this;
14445
14446      if (item.type === "browser") {
14447        /* use browser speechSynthesis */
14448        await self._speakBrowserTTS(item.text);
14449      } else {
14450        /* use server-generated audio */
14451        var url = item.url;
14452        var nextProm = this._audioQueue.length > 0 ? this._audioQueue[0] : null;
14453        var preloaded = null;
14454        if (nextProm) {
14455          nextProm.then(function (nextItem) {
14456            if (nextItem && nextItem.type === "audio" && nextItem.url && self._ttsEpoch === epoch) { preloaded = new Audio(nextItem.url); preloaded.preload = "auto"; }
14457          }).catch(function () {});
14458        }
14459        await new Promise(function (resolve) {
14460          var resolved = false;
14461          var done = function () { if (resolved) return; resolved = true; URL.revokeObjectURL(url); self._currentAudio = null; clearTimeout(safetyTimer); resolve(); };
14462          var audio = new Audio(url);
14463          self._currentAudio = audio;
14464          audio.onended = done;
14465          audio.onerror = done;
14466          var safetyTimer = setTimeout(done, 30000);
14467          audio.play().catch(done);
14468        });
14469        void preloaded;
14470      }
14471    }
14472    if (this._ttsEpoch === epoch) {
14473      this._isPlayingQueue = false;
14474      this._isSpeaking = false;
14475      this._updateVoiceUI();
14476      if (this._voiceMode && !this._isListening) {
14477        this._scheduleListening(1000);
14478      }
14479    }
14480  };
14481
14482  CafChat.prototype._stopSpeaking = function () {
14483    this._ttsEpoch++;
14484    this._audioQueue = [];
14485    this._isPlayingQueue = false;
14486    for (var i = 0; i < this._ttsAbortControllers.length; i++) {
14487      this._ttsAbortControllers[i].abort();
14488    }
14489    this._ttsAbortControllers = [];
14490    if (this._currentAudio) {
14491      this._currentAudio.pause();
14492      this._currentAudio.currentTime = 0;
14493      this._currentAudio = null;
14494    }
14495    /* cancel browser speechSynthesis too */
14496    if (window.speechSynthesis) {
14497      window.speechSynthesis.cancel();
14498    }
14499    this._currentUtterance = null;
14500    this._isSpeaking = false;
14501  };
14502
14503  /* ── Centralised listen scheduler ──────────────────────────── */
14504  /* Single point of control: cancels any pending listen timer    */
14505  /* and re-schedules. TTS start always calls this with Infinity  */
14506  /* to cancel, ensuring the mic never opens during playback.     */
14507  CafChat.prototype._scheduleListening = function (delay) {
14508    var self = this;
14509    if (self._listenTimer) { clearTimeout(self._listenTimer); self._listenTimer = null; }
14510    if (delay === Infinity) return; // just cancel, don't reschedule
14511    self._listenTimer = setTimeout(function () {
14512      self._listenTimer = null;
14513      if (self._voiceMode && !self._isListening &&
14514          !self._isPlayingQueue && !self._isSpeaking && !self.isLoading) {
14515        self._startListening();
14516      }
14517    }, delay);
14518  };
14519
14520  CafChat.prototype._interruptResponse = function () {
14521    if (this._chatAbort) {
14522      this._chatAbort.abort();
14523      this._chatAbort = null;
14524    }
14525    this._stopSpeaking();
14526    this.isLoading = false;
14527    this._updateVoiceUI();
14528  };
14529
14530  /* ── VAD (Voice Activity Detection) ────────────────────────── */
14531  CafChat.prototype._stopVAD = function () {
14532    if (this._vadInterval) {
14533      clearInterval(this._vadInterval);
14534      this._vadInterval = null;
14535    }
14536    if (this._vadStream) {
14537      this._vadStream.getTracks().forEach(function (t) { t.stop(); });
14538      this._vadStream = null;
14539    }
14540  };
14541
14542  CafChat.prototype._startVAD = async function () {
14543    this._stopVAD();
14544    var self = this;
14545    try {
14546      var stream = await navigator.mediaDevices.getUserMedia({ audio: true });
14547      self._vadStream = stream;
14548      var ctx = new AudioContext();
14549      var source = ctx.createMediaStreamSource(stream);
14550      var analyser = ctx.createAnalyser();
14551      analyser.fftSize = 256;
14552      source.connect(analyser);
14553      var data = new Uint8Array(analyser.frequencyBinCount);
14554      var calibrationSamples = 0;
14555      var calibrationSum = 0;
14556      var ambientLevel = 10;
14557      var consecutiveAbove = 0;
14558
14559      self._vadInterval = setInterval(function () {
14560        if (!self._voiceMode) { self._stopVAD(); ctx.close(); return; }
14561        analyser.getByteFrequencyData(data);
14562        var sum = 0;
14563        for (var i = 0; i < data.length; i++) sum += data[i];
14564        var avg = sum / data.length;
14565        if (calibrationSamples < 8) {
14566          calibrationSum += avg;
14567          calibrationSamples++;
14568          if (calibrationSamples === 8) ambientLevel = calibrationSum / 8;
14569          return;
14570        }
14571        var threshold = Math.max(ambientLevel * 3, 18);
14572        if (avg > threshold) { consecutiveAbove++; } else { consecutiveAbove = 0; }
14573
14574        if (consecutiveAbove >= 4) {
14575          self._stopVAD();
14576          ctx.close();
14577          self._interruptResponse();
14578          if (self._voiceMode && !self._isListening) {
14579            self._startListening();
14580          }
14581        }
14582      }, 50);
14583    } catch (e) { /* mic unavailable */ }
14584  };
14585
14586  /* ── Speech Recognition (STT) – continuous mode ─────────────── */
14587  CafChat.prototype._startListening = function () {
14588    var self = this;
14589    if (this._isListening) return;
14590    if (this._isSpeaking || this._isPlayingQueue) return;
14591    var SR = window.SpeechRecognition || window.webkitSpeechRecognition;
14592    if (!SR) {
14593      this._showError("Il tuo browser non supporta il riconoscimento vocale. Prova con Google Chrome.");
14594      return;
14595    }
14596
14597    var recognition = new SR();
14598    recognition.lang = "it-IT";
14599    // continuous=false: browser auto-stops after user pauses, fires onend cleanly.
14600    // On Android Chrome, continuous=true causes each isFinal result to contain
14601    // the full cumulative phrase ("Ciao", "Ciao Sto", "Ciao Sto cercando"…),
14602    // and _finalBuffer+= would concatenate them → "CiaoCiao StoCiao Sto cercando…".
14603    recognition.continuous = false;
14604    recognition.interimResults = true;
14605    recognition.maxAlternatives = 1;
14606    this._recognition = recognition;
14607    this._isListening = true;
14608    this._finalBuffer = "";
14609    if (this._sendDebounce) { clearTimeout(this._sendDebounce); this._sendDebounce = null; }
14610    this._updateVoiceUI();
14611
14612    /* Track consecutive "not-allowed" errors to avoid infinite retry loops */
14613    if (!this._micRetryCount) this._micRetryCount = 0;
14614
14615    recognition.onresult = function (event) {
14616      self._micRetryCount = 0; /* reset on success */
14617      // Take the last (most complete) result from the event.
14618      // Use SET (not +=) to avoid accumulating cumulative phrases on Android.
14619      var last = event.results[event.results.length - 1];
14620      var text = last[0].transcript;
14621      if (last.isFinal) {
14622        self._finalBuffer = text;
14623      }
14624      var display = (self._finalBuffer || text).trim();
14625      if (self._voiceMode) {
14626        self._voiceInterimText = display;
14627        self._updateVoiceGhost();
14628      } else {
14629        self.inputEl.value = display;
14630      }
14631      // No debounce: with continuous=false the browser fires onend automatically.
14632    };
14633
14634    recognition.onerror = function (event) {
14635      if (event.error === "not-allowed") {
14636        self._isListening = false;
14637        self._recognition = null;
14638        self._micRetryCount = (self._micRetryCount || 0) + 1;
14639        self._updateVoiceUI();
14640        /* On auto-restart (no user gesture), browsers may fire "not-allowed"
14641           even though the user granted mic permission. In voice mode, silently
14642           re-schedule instead of showing an alarming error. Max 3 retries. */
14643        if (self._voiceMode && !self._isSpeaking && !self._isPlayingQueue && self._micRetryCount < 3) {
14644          self._scheduleListening(800);
14645        } else {
14646          self._micRetryCount = 0;
14647          self._showError("Permesso microfono negato. Consenti l'accesso al microfono nelle impostazioni del browser.");
14648        }
14649      } else if (event.error !== "no-speech" && event.error !== "aborted") {
14650        self._isListening = false;
14651        self._recognition = null;
14652        self._updateVoiceUI();
14653        self._showError("Errore microfono. Riprova.");
14654      }
14655    };
14656
14657    recognition.onend = function () {
14658      if (self._sendDebounce) { clearTimeout(self._sendDebounce); self._sendDebounce = null; }
14659      var pendingText = self._finalBuffer.trim();
14660      self._isListening = false;
14661      self._recognition = null;
14662      self._finalBuffer = "";
14663      self._voiceInterimText = "";
14664      self._updateVoiceGhost();
14665      self._updateVoiceUI();
14666
14667      if (pendingText) {
14668        self._lastMessageWasVoice = true;
14669        self._send(pendingText);
14670      } else if (self._voiceMode && !self._isSpeaking && !self._isPlayingQueue) {
14671        self._scheduleListening(400);
14672      }
14673    };
14674
14675    try {
14676      recognition.start();
14677    } catch (e) {
14678      this._isListening = false;
14679      this._recognition = null;
14680      this._micRetryCount = (this._micRetryCount || 0) + 1;
14681      this._updateVoiceUI();
14682      if (this._voiceMode && !this._isSpeaking && !this._isPlayingQueue && this._micRetryCount < 3) {
14683        this._scheduleListening(800);
14684      } else {
14685        this._micRetryCount = 0;
14686        this._showError("Impossibile avviare il microfono. Verifica i permessi del browser.");
14687      }
14688    }
14689  };
14690
14691  CafChat.prototype._stopListening = function () {
14692    if (this._recognition) {
14693      try { this._recognition.stop(); } catch (e) { /* ok */ }
14694    }
14695  };
14696
14697  CafChat.prototype._abortListening = function () {
14698    if (this._sendDebounce) { clearTimeout(this._sendDebounce); this._sendDebounce = null; }
14699    if (this._recognition) {
14700      try { this._recognition.abort(); } catch (e) { /* ok */ }
14701    }
14702    this._isListening = false;
14703    this._recognition = null;
14704    this._finalBuffer = "";
14705    this._voiceInterimText = "";
14706    this._updateVoiceGhost();
14707    this._updateVoiceUI();
14708  };
14709
14710  /* ── enter / exit voice mode ───────────────────────────────── */
14711  CafChat.prototype._enterVoiceMode = function () {
14712    var SR = window.SpeechRecognition || window.webkitSpeechRecognition;
14713    if (!SR) {
14714      this._showError("Il tuo browser non supporta il riconoscimento vocale. Prova con Google Chrome.");
14715      return;
14716    }
14717    var self = this;
14718    this._stopSpeaking();
14719    this._voiceMode = true;
14720    this._isSpeaking = false;
14721    this._audioQueue = [];
14722    this._ttsAbortControllers = [];
14723    this._ttsEpoch = (this._ttsEpoch || 0) + 1;
14724    this._lastMessageWasVoice = false;
14725    this._voiceInterimText = "";
14726
14727    /* show voice panel, hide normal chat */
14728    this.messagesEl.style.display = "none";
14729    if (this._scrollWrap) this._scrollWrap.style.display = "none";
14730    this.errorEl.style.display = "none";
14731    this._inputAreaEl.style.display = "none";
14732    this.voicePanelEl.classList.add("active");
14733
14734    this._renderVoiceTranscript();
14735    this._updateVoiceUI();
14736
14737    /* a11y #C52 (WCAG 2.4.3): il bottone mic dell'input area appena attivato
14738     * viene nascosto -> senza questo focus() il focus cadrebbe sul body della
14739     * pagina host. Sposta il focus sul controllo principale della UI vocale
14740     * e memorizza l'origine per ripristinarla all'uscita. */
14741    this._voiceReturnFocusEl = this.micBtn || null;
14742    try { if (this.voiceMicBtn) this.voiceMicBtn.focus(); } catch (_e) {}
14743
14744    var welcomeText = "";
14745    if (this.messages.length > 0 && this.messages[0].role === "assistant" && this.messages[0].content) {
14746      welcomeText = this.messages[0].content;
14747    }
14748
14749    /* getUserMedia is just an optional pre-check to trigger the browser
14750       permission prompt. SpeechRecognition has its OWN mic access and does
14751       NOT need getUserMedia. So if getUserMedia fails, we still proceed. */
14752    var proceed = function () {
14753      self._micRetryCount = 0;
14754      /* AI Act art. 50(1): dichiarazione di natura IA anche nel canale vocale. */
14755      if (self.opts.aiDisclosure !== false && !self._voiceNoticeShown) {
14756        self._voiceNoticeShown = true;
14757        self._queueTTSChunk(bkT("ai_voice_notice"));
14758      }
14759      if (welcomeText) {
14760        self._queueTTSChunk(welcomeText);
14761      } else {
14762        self._startListening();
14763      }
14764    };
14765    if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
14766      navigator.mediaDevices.getUserMedia({ audio: true })
14767        .then(function (stream) {
14768          stream.getTracks().forEach(function (t) { t.stop(); });
14769          proceed();
14770        })
14771        .catch(function () {
14772          /* getUserMedia failed but SpeechRecognition may still work */
14773          proceed();
14774        });
14775    } else {
14776      proceed();
14777    }
14778  };
14779
14780  CafChat.prototype._exitVoiceMode = function () {
14781    this._scheduleListening(Infinity); // cancel any pending listen timer
14782    this._voiceMode = false;
14783    this._abortListening();
14784    this._stopSpeaking();
14785    this._stopVAD();
14786    this._voiceInterimText = "";
14787
14788    /* hide voice panel, show normal chat */
14789    this.voicePanelEl.classList.remove("active");
14790    this.messagesEl.style.display = "";
14791    if (this._scrollWrap) this._scrollWrap.style.display = "";
14792    this._inputAreaEl.style.display = "";
14793
14794    this._renderMessages();
14795    this._scrollToBottom();
14796    /* a11y #C52: ripristina il focus al bottone che ha aperto la modalita'
14797     * vocale (fallback: input chat). */
14798    var _rf = this._voiceReturnFocusEl;
14799    this._voiceReturnFocusEl = null;
14800    if (_rf && _rf.isConnected && _rf.offsetParent !== null) {
14801      try { _rf.focus(); } catch (_e) { this.inputEl.focus(); }
14802    } else {
14803      this.inputEl.focus();
14804    }
14805  };
14806
14807  /* ── voice UI update ───────────────────────────────────────── */
14808  CafChat.prototype._updateVoiceUI = function () {
14809    if (!this._voiceMode || !this.voiceMicBtn) return;
14810    var c = this.opts.color;
14811    /* WCAG 4.1.2/4.1.3 (#C17/#C36): stato del canale vocale esposto alle AT.
14812     * Annunciamo solo i passaggi ad ascolto/elaborazione (una volta per
14813     * transizione) per non sovrapporci al TTS della risposta. */
14814    var _vs = this._isListening ? "listening" : this._isSpeaking ? "speaking" : this.isLoading ? "processing" : "idle";
14815    if (_vs !== this._voiceAnnState) {
14816      this._voiceAnnState = _vs;
14817      if (this.srAnnouncer && (_vs === "listening" || _vs === "processing")) {
14818        this.srAnnouncer.textContent = _vs === "listening" ? bkT("sr_listening") : bkT("sr_processing");
14819      }
14820    }
14821
14822    if (this._isListening) {
14823      this._setVoiceMicIcon("stop");
14824      this.voiceMicBtn.setAttribute("aria-label", bkT("aria_stop_listening"));
14825      this.voiceMicBtn.setAttribute("aria-pressed", "true");
14826      this.voiceMicBtn.style.background = c;
14827      this.voiceMicBtn.style.boxShadow = "0 0 40px 8px " + c + "4d";
14828      this.voiceMicBtn.style.animation = "none";
14829      this.voiceMicBtn.style.transform = "scale(1.05)";
14830      this.voiceStatusEl.textContent = bkT("voice_speak_now");
14831      this.voiceStatusEl.style.color = "var(--caf-c-ink)"; /* a11y #C54 */
14832      this.voiceHintEl.textContent = bkT("voice_listening_hint");
14833      this.voiceHintEl.style.color = "var(--caf-c-ink)";
14834      this.voiceRingEl.style.width = "120px";
14835      this.voiceRingEl.style.height = "120px";
14836      this.voiceRingEl.style.borderColor = c + "40";
14837      this.voiceRipplesEl.style.display = "";
14838      this.voiceBarsEl.style.display = "";
14839      this.voiceGlowEl.style.width = "200px";
14840      this.voiceGlowEl.style.height = "200px";
14841      this.voiceGlowEl.style.opacity = "0.18";
14842    } else if (this._isSpeaking) {
14843      this._setVoiceMicIcon("speaker");
14844      this.voiceMicBtn.setAttribute("aria-label", bkT("aria_speak"));
14845      this.voiceMicBtn.setAttribute("aria-pressed", "false");
14846      this.voiceMicBtn.style.background = "#34d399";
14847      this.voiceMicBtn.style.boxShadow = "0 0 30px 4px rgba(16,185,129,.2)";
14848      this.voiceMicBtn.style.animation = "caf-speaking-glow 2s ease-in-out infinite";
14849      this.voiceMicBtn.style.transform = "";
14850      this.voiceStatusEl.textContent = bkT("voice_responding");
14851      this.voiceStatusEl.style.color = "#047857";
14852      this.voiceHintEl.textContent = bkT("voice_tap_interrupt");
14853      this.voiceHintEl.style.color = "#047857";
14854      this.voiceRingEl.style.width = "110px";
14855      this.voiceRingEl.style.height = "110px";
14856      this.voiceRingEl.style.borderColor = "rgba(16,185,129,.2)";
14857      this.voiceRipplesEl.style.display = "none";
14858      this.voiceBarsEl.style.display = "none";
14859      this.voiceGlowEl.style.width = "160px";
14860      this.voiceGlowEl.style.height = "160px";
14861      this.voiceGlowEl.style.opacity = "0.12";
14862    } else if (this.isLoading) {
14863      this._setVoiceMicIcon("dots");
14864      this.voiceMicBtn.setAttribute("aria-label", bkT("sr_processing"));
14865      this.voiceMicBtn.setAttribute("aria-pressed", "false");
14866      this.voiceMicBtn.style.background = "#94a3b8";
14867      this.voiceMicBtn.style.boxShadow = "0 0 20px 4px rgba(100,116,139,.15)";
14868      this.voiceMicBtn.style.animation = "caf-mic-breathe 1.5s ease-in-out infinite";
14869      this.voiceMicBtn.style.transform = "";
14870      this.voiceStatusEl.textContent = bkT("voice_thinking");
14871      this.voiceStatusEl.style.color = "#5b6573";
14872      this.voiceHintEl.textContent = bkT("voice_thinking_hint");
14873      this.voiceHintEl.style.color = "#5b6573";
14874      this.voiceRingEl.style.width = "105px";
14875      this.voiceRingEl.style.height = "105px";
14876      this.voiceRingEl.style.borderColor = "rgba(100,116,139,.2)";
14877      this.voiceRipplesEl.style.display = "none";
14878      this.voiceBarsEl.style.display = "none";
14879      this.voiceGlowEl.style.width = "120px";
14880      this.voiceGlowEl.style.height = "120px";
14881      this.voiceGlowEl.style.opacity = "0.08";
14882    } else {
14883      this._setVoiceMicIcon("mic");
14884      this.voiceMicBtn.setAttribute("aria-label", bkT("aria_speak"));
14885      this.voiceMicBtn.setAttribute("aria-pressed", "false");
14886      this.voiceMicBtn.style.background = c;
14887      this.voiceMicBtn.style.boxShadow = "0 8px 30px -4px " + c + "80";
14888      this.voiceMicBtn.style.animation = "caf-mic-breathe 3s ease-in-out infinite";
14889      this.voiceMicBtn.style.transform = "";
14890      this.voiceStatusEl.textContent = bkT("voice_tap_to_talk");
14891      this.voiceStatusEl.style.color = "var(--caf-c-ink)"; /* a11y #C54 */
14892      this.voiceHintEl.textContent = bkT("voice_press_to_start");
14893      this.voiceHintEl.style.color = "#5b6573";
14894      this.voiceRingEl.style.width = "105px";
14895      this.voiceRingEl.style.height = "105px";
14896      this.voiceRingEl.style.borderColor = c + "30";
14897      this.voiceRipplesEl.style.display = "none";
14898      this.voiceBarsEl.style.display = "none";
14899      this.voiceGlowEl.style.width = "120px";
14900      this.voiceGlowEl.style.height = "120px";
14901      this.voiceGlowEl.style.opacity = "0.08";
14902    }
14903  };
14904
14905  /* ── voice transcript rendering ────────────────────────────── */
14906
14907  /* Full rebuild – only when messages array changes (new msg, streaming done) */
14908  CafChat.prototype._renderVoiceTranscript = function () {
14909    if (!this.voiceTranscriptEl) return;
14910    var o = this.opts;
14911    var frag = document.createDocumentFragment();
14912
14913    /* AI Act art. 50(1)+(5): l'annuncio vocale di natura IA ha anche una
14914     * controparte TESTUALE visibile e persistente nel transcript. */
14915    if (o.aiDisclosure !== false) {
14916      var _vnRow = document.createElement("div");
14917      _vnRow.className = "caf-vt-ai-notice";
14918      _vnRow.setAttribute("role", "note");
14919      _vnRow.style.cssText = "margin:2px 0 8px;padding:8px 12px;background:#f1f5f9;border-radius:10px;font-size:12px;line-height:1.45;color:#334155;text-align:center";
14920      _vnRow.textContent = bkT("ai_voice_notice");
14921      frag.appendChild(_vnRow);
14922    }
14923
14924    for (var i = 0; i < this.messages.length; i++) {
14925      var msg = this.messages[i];
14926      /* Skip booking placeholder messages in voice transcript */
14927      if (msg.content === "@@BOOKING@@") continue;
14928      if (typeof msg.content === "string" && msg.content.indexOf("@@BOOKING_LAUNCH@@") === 0) continue;
14929      var row = document.createElement("div");
14930      row.className = "caf-vt-row" + (msg.role === "user" ? " user" : "");
14931      if (msg.role === "assistant") {
14932        var av = document.createElement("div");
14933        av.className = "caf-vt-avatar";
14934        av.innerHTML = '<img src="' + escapeHtml(o.logo) + '" alt="">';
14935        row.appendChild(av);
14936      }
14937      var bbl = document.createElement("div");
14938      bbl.className = "caf-vt-bubble " + msg.role;
14939      /* AI Act art. 50(2): marcatura machine-readable anche sul transcript
14940       * del voice mode (stessa regola delle bolle chat). */
14941      if (msg.role === "assistant") {
14942        bbl.setAttribute("data-ai-generated", "true");
14943        if (msg.model) bbl.setAttribute("data-model", String(msg.model));
14944      }
14945      if (msg.role === "user") bbl.style.background = "var(--caf-c)";
14946      if (msg.content === "" && this.isLoading && i === this.messages.length - 1) {
14947        bbl.innerHTML = '<div class="caf-vt-dots"><span style="background:' + o.color + '66"></span><span style="background:' + o.color + '66"></span><span style="background:' + o.color + '66"></span></div>';
14948        this._voiceStreamingBubble = bbl;
14949      } else {
14950        bbl.innerHTML = renderMarkdown(msg.content, o.color);
14951        var _ecVQ = "";
14952        if (msg.role === "assistant") {
14953          for (var _vqi = i - 1; _vqi >= 0; _vqi--) {
14954            if (this.messages[_vqi].role === "user" && this.messages[_vqi].content) {
14955              _ecVQ = this.messages[_vqi].content; break;
14956            }
14957          }
14958        }
14959        this._renderEcommerceCards && this._renderEcommerceCards(bbl, _ecVQ);
14960        if (this.isLoading && i === this.messages.length - 1 && msg.role === "assistant") {
14961          this._voiceStreamingBubble = bbl;
14962        }
14963      }
14964      row.appendChild(bbl);
14965      frag.appendChild(row);
14966    }
14967
14968    this.voiceTranscriptEl.innerHTML = "";
14969    this._voiceGhostEl = null;
14970    this.voiceTranscriptEl.appendChild(frag);
14971    this._updateVoiceGhost();
14972    this.voiceTranscriptEl.scrollTop = this.voiceTranscriptEl.scrollHeight;
14973  };
14974
14975  /* Update only the streaming assistant bubble (no DOM rebuild) */
14976  CafChat.prototype._updateVoiceStreamingBubble = function (content) {
14977    // Recover reference if DOM was rebuilt (e.g. by _renderVoiceTranscript)
14978    if (!this._voiceStreamingBubble && this.voiceTranscriptEl) {
14979      var bubbles = this.voiceTranscriptEl.querySelectorAll(".caf-vt-bubble.assistant");
14980      if (bubbles.length > 0) {
14981        this._voiceStreamingBubble = bubbles[bubbles.length - 1];
14982      }
14983    }
14984    if (!this._voiceStreamingBubble) return;
14985    this._voiceStreamingBubble.innerHTML = renderMarkdown(content, this.opts.color);
14986    /* card NON renderizzate durante streaming (vedi nota in _updateStreamingBubble) */
14987    var el = this.voiceTranscriptEl;
14988    if (el.scrollTop + el.clientHeight >= el.scrollHeight - 80) {
14989      el.scrollTop = el.scrollHeight;
14990    }
14991  };
14992
14993  /* Update only the ghost bubble in-place (no DOM rebuild) */
14994  CafChat.prototype._updateVoiceGhost = function () {
14995    if (!this.voiceTranscriptEl) return;
14996    var o = this.opts;
14997
14998    if (this._isListening && this._voiceInterimText) {
14999      if (!this._voiceGhostEl) {
15000        this._voiceGhostEl = document.createElement("div");
15001        this._voiceGhostEl.className = "caf-vt-row user";
15002        var gBbl = document.createElement("div");
15003        gBbl.className = "caf-vt-ghost";
15004        /* a11y #C54: sfondo a colore pieno + testo onC (niente alph
15004a) */
15005        gBbl.style.background = "var(--caf-c)";
15006        gBbl.style.color = "var(--caf-on-c)";
15007        this._voiceGhostEl.appendChild(gBbl);
15008        this.voiceTranscriptEl.appendChild(this._voiceGhostEl);
15009      }
15010      this._voiceGhostEl.firstChild.textContent = this._voiceInterimText;
15011      this.voiceTranscriptEl.scrollTop = this.voiceTranscriptEl.scrollHeight;
15012    } else if (this._voiceGhostEl) {
15013      if (this._voiceGhostEl.parentNode) {
15014        this._voiceGhostEl.parentNode.removeChild(this._voiceGhostEl);
15015      }
15016      this._voiceGhostEl = null;
15017    }
15018  };
15019
15020  /* a11y #C56 (WCAG 2.2.1): niente auto-hide — l'errore resta finche'
15021   * l'utente non lo chiude (X) o non invia un altro messaggio (_sendMessage
15022   * chiama _hideError). Nota: in caso di errore di invio viene rimossa solo
15023   * la bolla ASSISTENTE parziale; il messaggio utente resta in cronologia,
15024   * quindi il testo e' sempre recuperabile (nessun intervento necessario). */
15025  CafChat.prototype._showError = function (msg) {
15026    var self = this;
15027    if (this._errorTimer) { clearTimeout(this._errorTimer); this._errorTimer = null; }
15028    this.errorEl.innerHTML = '<span aria-hidden="true">⚠️ </span><span class="caf-sr-only">' + escapeHtml(bkT("label_error")) + ': </span>' + escapeHtml(String(msg)) +
15029      '<button type="button" class="caf-error-x" aria-label="' + escapeHtml(bkT("aria_close")) + '" style="background:transparent;border:none;color:inherit;font-size:15px;line-height:1;padding:2px 6px;margin-left:8px;cursor:pointer;vertical-align:middle;min-width:24px;min-height:24px">\u2715</button>';
15030    var xb = this.errorEl.querySelector(".caf-error-x");
15031    if (xb) xb.addEventListener("click", function () {
15032      self._hideError();
15033      try { if (self.inputEl) self.inputEl.focus(); } catch (_e) {}
15034    });
15035    this.errorEl.style.display = "block";
15036  };
15037
15038  CafChat.prototype._hideError = function () {
15039    this.errorEl.style.display = "none";
15040  };
15041
15042  /* ── public API ───────────────────────────────────────────── */
15043  var instance = null;
15044
15045  /* Garantisce che il widget sia inizializzato. Ritorna l'istanza, oppure
15046   * null se la pagina è bloccata o manca data-base-url (no-op pulito). */
15047  function ensureInstance() {
15048    if (!instance) {
15049      try { window.CafChatInline.init(); } catch (_e) {}
15050    }
15051    return instance;
15052  }
15053
15054  window.CafChatInline = {
15055    init: function (userOpts) {
15056      if (instance) return instance;
15057      /* Blocco esplicito: su queste pagine il widget non parte mai. */
15058      if (isWidgetBlockedPage()) return null;
15059      var opts = resolveOpts(userOpts);
15060      if (!opts.baseUrl) {
15061        console.error("CafChatInline: data-base-url is required.");
15062        return null;
15063      }
15064      try {
15065        instance = new CafChat(opts);
15066        /* ── record widget load stat (fire-and-forget) ── */
15067        if (!opts.privacy) {
15068          fetch(opts.baseUrl + "/api/stats", { method: "POST" }).catch(function () {});
15069        }
15070      } catch(e) {
15071        console.error("CafChatInline init error:", e);
15072      }
15073      return instance;
15074    },
15075    open: function () {
15076      var inst = ensureInstance();
15077      if (inst) inst.open();
15078      else console.warn("CafChatInline: widget non disponibile su questa pagina.");
15079    },
15080    openBooking: function () {
15081      var inst = ensureInstance();
15082      if (inst) inst.openBooking();
15083      else console.warn("CafChatInline: widget non disponibile su questa pagina.");
15084    },
15085    close: function () {
15086      if (instance) instance.close();
15087    },
15088  };
15089
15090  /* auto-init when data-base-url is provided on the script tag */
15091  var detectedBaseUrl = getAttr("base-url");
15092  if (detectedBaseUrl) {
15093    if (document.readyState === "loading") {
15094      document.addEventListener("DOMContentLoaded", function () {
15095        window.CafChatInline.init();
15096      });
15097    } else {
15098      window.CafChatInline.init();
15099    }
15100  }
15101
15102  /* ── Host-page triggers: bottoni del sito con data-caf-action ──────────
15103   * Event delegation su document (capture). Funziona anche per bottoni
15104   * iniettati dopo l'init (CMS / contenuti dinamici). Valori:
15105   *   data-caf-action="open"  → apre la chat (benvenuto)
15106   *   data-caf-action="book"  → apre il wizard di prenotazione
15107   * Il lazy-init dell'API gestisce il caso "click prima dell'init". */
15108  function bindHostTriggers() {
15109    if (window.__CAF_TRIGGERS_BOUND__) return;
15110    window.__CAF_TRIGGERS_BOUND__ = true;
15111    document.addEventListener("click", function (e) {
15112      var t = e.target;
15113      if (!t || !t.closest) return;
15114      var el = t.closest("[data-caf-action]");
15115      if (!el) return;
15116      var action = (el.getAttribute("data-caf-action") || "").trim().toLowerCase();
15117      if (action !== "open" && action !== "book") return;
15118      /* Evita la navigazione se il click è su (o dentro) un <a>, anche quando
15119       * l'elemento con data-caf-action è un contenitore non-anchor
15120       * (es. <div data-caf-action="book"><a href=...>). */
15121      if (el.tagName === "A" || t.closest("a")) e.preventDefault();
15122      try {
15123        if (action === "book") window.CafChatInline.openBooking();
15124        else window.CafChatInline.open();
15125      } catch (_e) {}
15126    }, true);
15127  }
15128  /* Non agganciare i trigger sulle pagine dove il widget è disabilitato
15129   * (isWidgetBlockedPage): lì init() torna comunque null. */
15130  if (!isWidgetBlockedPage()) bindHostTriggers();
15131})();

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.