PageSourceSearch

https://cryptoqctools.com/wp-content/plugins/cq-vip-funnel/assets/js/cq-funnel-client.js?ver=3.3.2

js cryptoqctools.com collected 2026-10-05 23:19:21 UTC 130,074 bytes, 3,233 lines download raw bytes

1/**
2 * Crypto Québec — Funnel Web Desktop & iPhone 16 Pro State Machine
3 * Version 2.0: Email as Cornerstone, 2FA OTP via AgentMail,
4 * Multi-jurisdiction Legal Consents (LCAP, CAN-SPAM, RGPD),
5 * Profile Linking with CryptoBot DB.
6 */
7
8// API Base Configuration from WordPress Plugin
9const API_BASE = (window.CQ_FUNNEL_CONFIG && window.CQ_FUNNEL_CONFIG.apiUrl) ? window.CQ_FUNNEL_CONFIG.apiUrl.replace(/\/+$/, "") : "";
10function getApiUrl(endpoint) {
11  return API_BASE + (endpoint.startsWith("/") ? "" : "/") + endpoint;
12}
13
14// URL Search Parameters & Attribution Extraction
15const urlParams = new URLSearchParams(window.location.search);
16const urlSessionId = urlParams.get('session_id');
17
18// State Object
19const FunnelState = {
20  sessionId: urlSessionId || localStorage.getItem('cq_web_session_id') || `web_${Math.random().toString(36).substring(2, 11)}`,
21  language: localStorage.getItem('cq_web_lang') || 'fr',
22  funnelData: null,
23  email: localStorage.getItem('cq_web_email') || null,
24  emailVerified: false,
25  sessionToken: localStorage.getItem('cq_session_token') || null,
26  telegramId: null,
27  telegramUsername: null,
28  existingProfile: null,
29  profile: null,
30  weexUid: localStorage.getItem('cq_weex_uid') || null,
31  bitunixUid: localStorage.getItem('cq_bitunix_uid') || null,
32  tradingviewUsername: localStorage.getItem('cq_tv_username') || null,
33  // Advertising Attribution Tracking
34  utmSource: urlParams.get('utm_source') || localStorage.getItem('cq_utm_source') || null,
35  utmCampaign: urlParams.get('utm_campaign') || localStorage.getItem('cq_utm_campaign') || null,
36  utmMedium: urlParams.get('utm_medium') || localStorage.getItem('cq_utm_medium') || null,
37  utmTerm: urlParams.get('utm_term') || localStorage.getItem('cq_utm_term') || null,
38  utmContent: urlParams.get('utm_content') || localStorage.getItem('cq_utm_content') || null,
39  ref: urlParams.get('ref') || localStorage.getItem('cq_ref') || null,
40  fbclid: urlParams.get('fbclid') || localStorage.getItem('cq_fbclid') || null,
41  gclid: urlParams.get('gclid') || localStorage.getItem('cq_gclid') || null,
42  ttclid: urlParams.get('ttclid') || localStorage.getItem('cq_ttclid') || null
43};
44
45// Persist ad attribution parameters
46if (FunnelState.utmSource) localStorage.setItem('cq_utm_source', FunnelState.utmSource);
47if (FunnelState.utmCampaign) localStorage.setItem('cq_utm_campaign', FunnelState.utmCampaign);
48if (FunnelState.utmMedium) localStorage.setItem('cq_utm_medium', FunnelState.utmMedium);
49if (FunnelState.utmTerm) localStorage.setItem('cq_utm_term', FunnelState.utmTerm);
50if (FunnelState.utmContent) localStorage.setItem('cq_utm_content', FunnelState.utmContent);
51if (FunnelState.ref) localStorage.setItem('cq_ref', FunnelState.ref);
52if (FunnelState.fbclid) localStorage.setItem('cq_fbclid', FunnelState.fbclid);
53if (FunnelState.gclid) localStorage.setItem('cq_gclid', FunnelState.gclid);
54if (FunnelState.ttclid) localStorage.setItem('cq_ttclid', FunnelState.ttclid);
55localStorage.setItem('cq_web_session_id', FunnelState.sessionId);
56
57// Ad Event Pixel Dispatch Helper (Meta, TikTok, Google)
58function trackAdEvent(eventName, params = {}) {
59  try {
60    if (window.fbq) window.fbq('track', eventName, params);
61    if (window.ttq) window.ttq.track(eventName, params);
62    if (window.gtag) window.gtag('event', eventName, params);
63    console.log(`[AdPixel] Dispatched event: ${eventName}`, params);
64  } catch(e) {}
65}
66
67// ─── Télémétrie First-Party & GA4 / GTM ──────────────────────────────────────────
68function trackFunnelEvent(eventName, eventDetails = {}) {
69  try {
70    const isMobile = window.innerWidth <= 600;
71    const deviceType = isMobile ? 'mobile' : 'desktop';
72    const payload = {
73      event_name: eventName,
74      details: eventDetails,
75      device: deviceType,
76      page: window.location.pathname || '/'
77    };
78
79    // 1. Google Tag Manager / GA4 dataLayer
80    window.dataLayer = window.dataLayer || [];
81    window.dataLayer.push({
82      event: 'cq_funnel_' + eventName,
83      cq_event: eventName,
84      cq_device: deviceType,
85      cq_page: payload.page,
86      ...eventDetails
87    });
88
89    // 2. Pixel / Gtag dispatch
90    if (window.gtag) {
91      window.gtag('event', 'cq_funnel_' + eventName, {
92        event_category: 'CQ Funnel',
93        event_label: eventName,
94        device: deviceType,
95        ...eventDetails
96      });
97    }
98
99    // 3. First-Party Native REST API (WordPress DB / Admin)
100    const baseApi = (window.CQ_FUNNEL_CONFIG && window.CQ_FUNNEL_CONFIG.apiUrl) ? window.CQ_FUNNEL_CONFIG.apiUrl : '/wp-json/cq-funnel/v1';
101    const trackUrl = baseApi.replace(/\/$/, '') + '/track-event';
102    const jsonStr = JSON.stringify(payload);
103
104    if (navigator.sendBeacon) {
105      const blob = new Blob([jsonStr], { type: 'application/json' });
106      navigator.sendBeacon(trackUrl, blob);
107    } else {
108      fetch(trackUrl, {
109        method: 'POST',
110        headers: { 'Content-Type': 'application/json' },
111        body: jsonStr,
112        keepalive: true
113      }).catch(() => {});
114    }
115  } catch (err) {
116    // Fail-safe
117  }
118}
119
120// Synthesized Audio Feedback
121const AudioFX = {
122  ctx: null,
123  init() {
124    if (window.CQ_FUNNEL_CONFIG && window.CQ_FUNNEL_CONFIG.enableSound === false) return;
125    if (!this.ctx && (window.AudioContext || window.webkitAudioContext)) {
126      this.ctx = new (window.AudioContext || window.webkitAudioContext)();
127    }
128  },
129  playPop() {
130    try {
131      this.init();
132      if (!this.ctx) return;
133      if (this.ctx.state === 'suspended') this.ctx.resume();
134      const osc = this.ctx.createOscillator();
135      const gain = this.ctx.createGain();
136      osc.type = 'sine';
137      osc.frequency.setValueAtTime(480, this.ctx.currentTime);
138      osc.frequency.exponentialRampToValueAtTime(840, this.ctx.currentTime + 0.08);
139      gain.gain.setValueAtTime(0.12, this.ctx.currentTime);
140      gain.gain.exponentialRampToValueAtTime(0.01, this.ctx.currentTime + 0.08);
141      osc.connect(gain);
142      gain.connect(this.ctx.destination);
143      osc.start();
144      osc.stop(this.ctx.currentTime + 0.08);
145    } catch(e) {}
146  },
147  playSuccess() {
148    try {
149      this.init();
150      if (!this.ctx) return;
151      if (this.ctx.state === 'suspended') this.ctx.resume();
152      const now = this.ctx.currentTime;
153      [523.25, 659.25, 783.99, 1046.50].forEach((freq, i) => {
154        const osc = this.ctx.createOscillator();
155        const gain = this.ctx.createGain();
156        osc.type = 'triangle';
157        osc.frequency.setValueAtTime(freq, now + i * 0.06);
158        gain.gain.setValueAtTime(0.08, now + i * 0.06);
159        gain.gain.exponentialRampToValueAtTime(0.01, now + i * 0.06 + 0.15);
160        osc.connect(gain);
161        gain.connect(this.ctx.destination);
162        osc.start(now + i * 0.06);
163        osc.stop(now + i * 0.06 + 0.15);
164      });
165    } catch(e) {}
166  }
167};
168
169// UI Elements
170const UI = {
171  drawer: document.getElementById('iphone-drawer'),
172  backdrop: document.getElementById('drawer-backdrop'),
173  triggerTab: document.getElementById('slide-trigger-tab'),
174  closeBtn: document.getElementById('close-drawer-btn'),
175  navOpenBtn: document.getElementById('nav-open-phone-btn'),
176  heroOpenBtn: document.getElementById('hero-open-phone-btn'),
177  chatStream: document.getElementById('chat-stream'),
178  inputDock: document.getElementById('input-dock'),
179  clock: document.getElementById('ios-clock'),
180  langFlag: document.getElementById('current-lang-flag'),
181  langCode: document.getElementById('current-lang-code'),
182  langMenuBtn: document.getElementById('lang-menu-btn'),
183  restartBtn: document.getElementById('restart-funnel-btn'),
184  drawerRestartBtn: document.getElementById('drawer-restart-btn'),
185  dockRestartBtn: document.getElementById('dock-restart-btn'),
186  botStatusText: document.getElementById('bot-status-text'),
187  // Legal Modal Elements
188  legalModal: document.getElementById('legal-modal'),
189  closeLegalBtn: document.getElementById('close-legal-modal-btn'),
190  acceptLegalBtn: document.getElementById('accept-legal-modal-btn'),
191  // UID Visual Help Modal Elements
192  uidHelpModal: document.getElementById('uid-help-modal'),
193  closeUidHelpBtn: document.getElementById('close-uid-help-btn'),
194  doneUidHelpBtn: document.getElementById('done-uid-help-btn'),
195  tabBtnWeex: document.getElementById('tab-btn-weex'),
196  tabBtnBitunix: document.getElementById('tab-btn-bitunix'),
197  // Font Size Controls
198  fontIncreaseBtn: document.getElementById('font-increase-btn'),
199  fontDecreaseBtn: document.getElementById('font-decrease-btn'),
200  // Inbox Elements
201  drawerInboxBtn: document.getElementById('drawer-inbox-btn'),
202  drawerInboxBadge: document.getElementById('drawer-inbox-badge'),
203  chatInboxBtn: document.getElementById('chat-inbox-btn'),
204  chatInboxBadge: document.getElementById('chat-inbox-badge'),
205  inboxModal: document.getElementById('inbox-modal'),
206  closeInboxBtn: document.getElementById('close-inbox-modal-btn'),
207  doneInboxBtn: document.getElementById('done-inbox-modal-btn'),
208  markAllReadBtn: document.getElementById('mark-all-read-btn'),
209  inboxList: document.getElementById('inbox-messages-list'),
210  inboxLoading: document.getElementById('inbox-loading'),
211  inboxEmpty: document.getElementById('inbox-empty'),
212  // VIP Feed & Podcasts Elements
213  drawerFeedBtn: document.getElementById('drawer-feed-btn'),
214  chatFeedBtn: document.getElementById('chat-feed-btn'),
215  feedModal: document.getElementById('vip-feed-modal'),
216  closeFeedBtn: document.getElementById('close-vip-feed-modal-btn'),
217  doneFeedBtn: document.getElementById('done-vip-feed-modal-btn'),
218  refreshFeedBtn: document.getElementById('refresh-vip-feed-btn'),
219  toggleExpelledTestBtn: document.getElementById('toggle-expelled-test-btn'),
220  feedSearch: document.getElementById('vip-feed-search'),
221  feedList: document.getElementById('vip-feed-list'),
222  feedLoading: document.getElementById('vip-feed-loading'),
223  feedExpelled: document.getElementById('vip-feed-expelled'),
224  feedEmpty: document.getElementById('vip-feed-empty'),
225};
226
227// Digital Clock
228function updateClock() {
229  const now = new Date();
230  const hrs = String(now.getHours()).padStart(2, '0');
231  const mins = String(now.getMinutes()).padStart(2, '0');
232  if (UI.clock) UI.clock.textContent = `${hrs}:${mins}`;
233}
234setInterval(updateClock, 1000);
235updateClock();
236
237// Drawer Controls
238function openDrawer(source = 'unknown') {
239  UI.drawer.classList.add('open');
240  UI.backdrop.classList.add('active');
241  AudioFX.playPop();
242  if (source !== 'side_tab') {
243    trackFunnelEvent('drawer_open', { trigger: source });
244  }
245  if (UI.chatStream.children.length === 0) {
246    startFunnel();
247  }
248}
249
250function closeDrawer() {
251  UI.drawer.classList.remove('open');
252  UI.backdrop.classList.remove('active');
253  trackFunnelEvent('drawer_close');
254}
255
256if (UI.triggerTab) {
257  UI.triggerTab.addEventListener('click', () => {
258    trackFunnelEvent('tab_click', {
259      trigger: 'side_tab',
260      title: 'ACCÈS VIP CQ',
261      badge: 'GRATUIT'
262    });
263    openDrawer('side_tab');
264  });
265}
266if (UI.navOpenBtn) UI.navOpenBtn.addEventListener('click', () => openDrawer('nav_btn'));
267if (UI.heroOpenBtn) UI.heroOpenBtn.addEventListener('click', () => openDrawer('hero_btn'));
268if (UI.closeBtn) UI.closeBtn.addEventListener('click', closeDrawer);
269if (UI.backdrop) UI.backdrop.addEventListener('click', closeDrawer);
270
271// Custom site-wide trigger support (buttons with class .cq-open-funnel-btn or href='#open-cq-funnel')
272document.addEventListener('click', (e) => {
273  const trigger = e.target.closest('.cq-open-funnel-btn, [href="#open-cq-funnel"], [href="#cq-funnel"]');
274  if (trigger) {
275    e.preventDefault();
276    trackFunnelEvent('cta_button_click', { href: trigger.getAttribute('href') || 'button' });
277    openDrawer('page_cta');
278  }
279});
280
281// If URL has session_id, automatically open drawer on launch
282if (urlSessionId) {
283  setTimeout(() => {
284    openDrawer('auto_session_url');
285  }, 350);
286}
287
288// Legal Modal Controls
289function openLegalModal() {
290  if (UI.legalModal) UI.legalModal.classList.add('active');
291}
292function closeLegalModal() {
293  if (UI.legalModal) UI.legalModal.classList.remove('active');
294}
295if (UI.closeLegalBtn) UI.closeLegalBtn.addEventListener('click', closeLegalModal);
296if (UI.acceptLegalBtn) UI.acceptLegalBtn.addEventListener('click', closeLegalModal);
297
298// UID Visual Help Modal Controls
299function openUidHelpModal(initialTab = 'weex') {
300  const modal = UI.uidHelpModal || document.getElementById('uid-help-modal');
301  if (modal) {
302    modal.classList.add('active');
303    modal.style.setProperty('display', 'flex', 'important');
304    switchUidHelpTab(initialTab);
305    if (window.AudioFX && typeof window.AudioFX.playPop === 'function') {
306      window.AudioFX.playPop();
307    }
308  }
309}
310
311function closeUidHelpModal() {
312  const modal = UI.uidHelpModal || document.getElementById('uid-help-modal');
313  if (modal) {
314    modal.classList.remove('active');
315    modal.style.setProperty('display', 'none', 'important');
316  }
317}
318
319function switchUidHelpTab(tab) {
320  const btnWeex = document.getElementById('tab-btn-weex');
321  const btnBitunix = document.getElementById('tab-btn-bitunix');
322  const paneWeex = document.getElementById('tab-content-weex');
323  const paneBitunix = document.getElementById('tab-content-bitunix');
324
325  if (tab === 'weex') {
326    if (btnWeex) btnWeex.classList.add('active');
327    if (btnBitunix) btnBitunix.classList.remove('active');
328    if (paneWeex) paneWeex.style.display = 'block';
329    if (paneBitunix) paneBitunix.style.display = 'none';
330  } else {
331    if (btnWeex) btnWeex.classList.remove('active');
332    if (btnBitunix) btnBitunix.classList.add('active');
333    if (paneWeex) paneWeex.style.display = 'none';
334    if (paneBitunix) paneBitunix.style.display = 'block';
335  }
336}
337
338if (UI.closeUidHelpBtn) UI.closeUidHelpBtn.addEventListener('click', closeUidHelpModal);
339if (UI.doneUidHelpBtn) UI.doneUidHelpBtn.addEventListener('click', closeUidHelpModal);
340if (UI.tabBtnWeex) UI.tabBtnWeex.addEventListener('click', () => switchUidHelpTab('weex'));
341if (UI.tabBtnBitunix) UI.tabBtnBitunix.addEventListener('click', () => switchUidHelpTab('bitunix'));
342
343// Global click event delegation for UID help button & modal controls
344document.addEventListener('click', (e) => {
345  const helpTrigger = e.target.closest('#btn-show-uid-help, .btn-uid-help');
346  if (helpTrigger) {
347    e.preventDefault();
348    e.stopPropagation();
349    const currentExchange = (typeof FunnelState !== 'undefined' && FunnelState.exchange) ? FunnelState.exchange : 'weex';
350    openUidHelpModal(currentExchange);
351    return;
352  }
353
354  const closeTrigger = e.target.closest('#close-uid-help-btn, #done-uid-help-btn');
355  if (closeTrigger) {
356    e.preventDefault();
357    closeUidHelpModal();
358    return;
359  }
360
361  if (e.target && e.target.id === 'uid-help-modal') {
362    closeUidHelpModal();
363    return;
364  }
365
366  const tabBtn = e.target.closest('.uid-tab-btn');
367  if (tabBtn && tabBtn.dataset.tab) {
368    e.preventDefault();
369    switchUidHelpTab(tabBtn.dataset.tab);
370    return;
371  }
372});
373
374// ─── Font Size Adjustment (A+ / A−) ───────────────────────────────────────────
375const FONT_SCALES = [0.82, 0.92, 1.0, 1.13, 1.28];
376const FONT_SCALE_DEFAULT_IDX = 2; // 1.0 = default centre
377const FONT_SCALE_KEY = 'cq_chat_font_scale_idx';
378
379const FontScale = {
380  idx: parseInt(localStorage.getItem(FONT_SCALE_KEY) ?? FONT_SCALE_DEFAULT_IDX, 10),
381
382  apply() {
383    const scale = FONT_SCALES[this.idx];
384    if (UI.chatStream) {
385      UI.chatStream.style.setProperty('--chat-font-scale', scale);
386    }
387    // Clamp button disabled states
388    if (UI.fontDecreaseBtn) UI.fontDecreaseBtn.disabled = this.idx <= 0;
389    if (UI.fontIncreaseBtn) UI.fontIncreaseBtn.disabled = this.idx >= FONT_SCALES.length - 1;
390    // Persist
391    localStorage.setItem(FONT_SCALE_KEY, this.idx);
392  },
393
394  increase() {
395    if (this.idx < FONT_SCALES.length - 1) { this.idx++; this.apply(); this.flash('+'); }
396  },
397
398  decrease() {
399    if (this.idx > 0) { this.idx--; this.apply(); this.flash('−'); }
400  },
401
402  flash(dir) {
403    const btn = dir === '+' ? UI.fontIncreaseBtn : UI.fontDecreaseBtn;
404    if (!btn) return;
405    btn.style.transition = 'transform 0.12s ease, background 0.12s ease';
406    btn.style.transform = 'scale(1.35)';
407    btn.style.background = dir === '+' ? 'rgba(16,185,129,0.28)' : 'rgba(255,255,255,0.18)';
408    setTimeout(() => {
409      btn.style.transform = '';
410      btn.style.background = '';
411    }, 180);
412  }
413};
414
415// Init on load
416FontScale.apply();
417
418if (UI.fontIncreaseBtn) UI.fontIncreaseBtn.addEventListener('click', () => FontScale.increase());
419if (UI.fontDecreaseBtn) UI.fontDecreaseBtn.addEventListener('click', () =>
419 FontScale.decrease());
420// ─────────────────────────────────────────────────────────────────────────────
421
422window.addEventListener('keydown', (e) => {
423  if (e.key === 'Escape') {
424    if (UI.uidHelpModal && UI.uidHelpModal.style.display === 'flex') {
425      closeUidHelpModal();
426    } else if (UI.legalModal && UI.legalModal.classList.contains('active')) {
427      closeLegalModal();
428    } else if (UI.drawer.classList.contains('open')) {
429      closeDrawer();
430    }
431  }
432});
433
434// Chat Helpers
435function scrollChatToBottom() {
436  setTimeout(() => {
437    UI.chatStream.scrollTop = UI.chatStream.scrollHeight;
438  }, 50);
439}
440
441function showTypingIndicator() {
442  const typing = document.createElement('div');
443  typing.className = 'typing-indicator-bubble';
444  typing.id = 'active-typing';
445  typing.innerHTML = `
446    <span class="typing-dot"></span>
447    <span class="typing-dot"></span>
448    <span class="typing-dot"></span>
449  `;
450  UI.chatStream.appendChild(typing);
451  scrollChatToBottom();
452}
453
454function hideTypingIndicator() {
455  const typing = document.getElementById('active-typing');
456  if (typing) typing.remove();
457}
458
459function appendBotMessage(htmlContent, delay = 500) {
460  return new Promise((resolve) => {
461    showTypingIndicator();
462    setTimeout(() => {
463      hideTypingIndicator();
464      const bubble = document.createElement('div');
465      bubble.className = 'message-bubble bot-msg';
466      bubble.innerHTML = htmlContent.replace(/\n/g, '<br>');
467      UI.chatStream.appendChild(bubble);
468      scrollChatToBottom();
469      AudioFX.playPop();
470      resolve(bubble);
471    }, delay);
472  });
473}
474
475function appendUserMessage(text) {
476  const bubble = document.createElement('div');
477  bubble.className = 'message-bubble user-msg';
478  bubble.textContent = text;
479  UI.chatStream.appendChild(bubble);
480  scrollChatToBottom();
481  AudioFX.playPop();
482}
483
484// API Communication
485async function syncSessionWithBackend() {
486  try {
487    const res = await fetch(getApiUrl('/api/session'), {
488      method: 'POST',
489      headers: { 'Content-Type': 'application/json' },
490      body: JSON.stringify({
491        session_id: FunnelState.sessionId,
492        language: FunnelState.language,
493        utm_source: FunnelState.utmSource || 'desktop_landing',
494        utm_campaign: FunnelState.utmCampaign,
495        utm_medium: FunnelState.utmMedium,
496        utm_term: FunnelState.utmTerm,
497        utm_content: FunnelState.utmContent,
498        ref: FunnelState.ref,
499        fbclid: FunnelState.fbclid,
500        gclid: FunnelState.gclid,
501        ttclid: FunnelState.ttclid
502      })
503    });
504    return await res.json();
505  } catch (err) {
506    console.warn('Backend sync session fallback:', err);
507  }
508}
509
510async function fetchFunnelContent(lang = 'fr') {
511  try {
512    const res = await fetch(getApiUrl(`/api/funnel-data?lang=${lang}`));
513    return await res.json();
514  } catch(e) {
515    console.error('Failed to load funnel content:', e);
516    return null;
517  }
518}
519
520function updateLanguageUI() {
521  const flags = { fr: '🇫🇷', en: '🇬🇧', es: '🇪🇸' };
522  UI.langFlag.textContent = flags[FunnelState.language] || '🇫🇷';
523  UI.langCode.textContent = FunnelState.language.toUpperCase();
524}
525
526UI.langMenuBtn.addEventListener('click', () => {
527  const nextLang = FunnelState.language === 'fr' ? 'en' : FunnelState.language === 'en' ? 'es' : 'fr';
528  setLanguage(nextLang);
529});
530
531function restartBotToBeginning() {
532  AudioFX.playPop();
533
534  // Clear any existing typing indicators or prompts
535  hideTypingIndicator();
536
537  // Reset conversation stream
538  UI.chatStream.innerHTML = '';
539
540  // Reset dynamic dock status
541  UI.inputDock.innerHTML = `
542    <div class="dock-placeholder" id="dock-status">
543      <span>Renseignez votre courriel ci-dessus pour débloquer le funnel</span>
544    </div>
545  `;
546
547  // Reset in-session progress attributes
548  FunnelState.weexUid = null;
549  FunnelState.bitunixUid = null;
550  FunnelState.tradingviewUsername = null;
551  FunnelState.profile = null;
552  FunnelState.existingProfile = null;
553  FunnelState.emailVerified = false;
554
555  // Flash Dynamic Island status
556  const islandText = document.querySelector('.island-text');
557  if (islandText) {
558    const originalText = islandText.textContent;
559    islandText.textContent = '↺ Reset...';
560    islandText.style.color = '#10b981';
561    setTimeout(() => {
562      islandText.textContent = originalText;
563      islandText.style.color = '';
564    }, 1800);
565  }
566
567  // Smoothly scroll chat container to top
568  UI.chatStream.scrollTop = 0;
569
570  // Start funnel afresh from Step 1
571  startFunnel();
572}
573
574if (UI.restartBtn) UI.restartBtn.addEventListener('click', restartBotToBeginning);
575if (UI.drawerRestartBtn) UI.drawerRestartBtn.addEventListener('click', restartBotToBeginning);
576if (UI.dockRestartBtn) UI.dockRestartBtn.addEventListener('click', restartBotToBeginning);
577
578async function setLanguage(lang) {
579  FunnelState.language = lang;
580  localStorage.setItem('cq_web_lang', lang);
581  updateLanguageUI();
582  FunnelState.funnelData = await fetchFunnelContent(lang);
583  UI.chatStream.innerHTML = '';
584  startFunnel();
585}
586
587
588// ==========================================================================
589// FUNNEL FLOW : EMAIL AS CORNERSTONE & 2FA OTP VIA AGENTMAIL
590// ==========================================================================
591
592async function checkAndResumeSession() {
593  const urlParams = new URLSearchParams(window.location.search);
594  const paramSessionId = urlParams.get('session_id');
595  if (!paramSessionId) return false;
596
597  try {
598    const res = await fetch(getApiUrl(`/api/session-info?session_id=${paramSessionId}`));
599    const data = await res.json();
600    if (data.found && data.email && data.email_verified) {
601      FunnelState.sessionId = data.session_id;
602      FunnelState.email = data.email;
603      FunnelState.emailVerified = true;
604      FunnelState.language = data.language || 'fr';
605      FunnelState.weexUid = data.weex_uid;
606      FunnelState.bitunixUid = data.bitunix_uid;
607      FunnelState.tradingviewUsername = data.tradingview_username;
608      FunnelState.profile = data.qualification_profile;
609
610      localStorage.setItem('cq_web_session_id', data.session_id);
611      localStorage.setItem('cq_web_email', data.email);
612      localStorage.setItem('cq_web_lang', FunnelState.language);
613
614      updateLanguageUI();
615      FunnelState.funnelData = await fetchFunnelContent(FunnelState.language);
616      checkAndEnableVipBottomMenu();
617
618      await appendBotMessage(`🎉 **Bon retour parmi nous !**\n\nVotre session pour <code>${data.email}</code> a été restaurée avec succès.\n\nVos accès réservés sont prêts à être activés :`);
619
620      if (!FunnelState.weexUid && !FunnelState.bitunixUid) {
621        step4_PartnerOffer();
622      } else if (!FunnelState.tradingviewUsername) {
623        step6_TradingViewUsername();
624      } else {
625        step7_TelegramVIPGateway();
626      }
627      return true;
628    }
629  } catch(e) {
630    console.error('Session resume error:', e);
631  }
632  return false;
633}
634
635async function startFunnel() {
636  await syncSessionWithBackend();
637  updateLanguageUI();
638  FunnelState.funnelData = await fetchFunnelContent(FunnelState.language);
639
640  // Check if session can be resumed from magic rescue email link
641  const resumed = await checkAndResumeSession();
642  if (resumed) return;
643
644  // Étape 1 : Demande de courriel (Pierre Angulaire) + Cases de consentement
645  await appendBotMessage(`👋 **Bienvenue chez Crypto Québec !**\n\nPour sécuriser votre profil, débloquer les indicateurs TradingView et accéder à nos formations offertes, veuillez commencer par valider votre adresse courriel :`);
646
647  displayEmailCaptureCard();
648}
649
650function displayEmailCaptureCard() {
651  const card = document.createElement('div');
652  card.className = 'email-consent-card';
653  card.id = 'email-capture-card';
654  card.innerHTML = `
655    <div class="consent-title">
656      <span>🔐</span>
657      <span>Validation Sécurisée par Courriel</span>
658    </div>
659    <div class="prompt-input-row" style="margin-bottom: 10px;">
660      <input type="email" id="initial-email-input" class="prompt-input" placeholder="[email protected]" value="${FunnelState.email || ''}" autofocus>
661    </div>
662
663    <div class="consent-checkbox-list">
664      <label class="consent-item">
665        <input type="checkbox" id="chk-casl" class="consent-check-input">
666        <span class="consent-text">
667          🇨🇦 <strong>Canada (LCAP) :</strong> J'accepte de recevoir la newsletter et communications de Crypto Québec. Désabonnement en 1 clic à tout moment.
668        </span>
669      </label>
670
671      <label class="consent-item">
672        <input type="checkbox" id="chk-canspam" class="consent-check-input">
673        <span class="consent-text">
674          🇺🇸 <strong>USA (CAN-SPAM) :</strong> I agree to receive informational & trading updates from Crypto Québec.
675        </span>
676      </label>
677
678      <label class="consent-item">
679        <input type="checkbox" id="chk-gdpr" class="consent-check-input">
680        <span class="consent-text">
681          🇪🇺 <strong>Europe (RGPD) :</strong> J'accepte le traitement de mes données personnelles conformément à la Politique de Confidentialité.
682        </span>
683      </label>
684    </div>
685
686    <div style="text-align: center; margin-bottom: 12px;">
687      <span class="legal-popup-link" id="link-open-terms">📜 Lire les Conditions Générales et Politique de Confidentialité</span>
688    </div>
689
690    <button id="btn-send-otp" class="prompt-submit-btn" style="width: 100%; padding: 10px; font-size: 12px;">
691      Recevoir mon code de validation ➔
692    </button>
693  `;
694
695  UI.chatStream.appendChild(card);
696  scrollChatToBottom();
697
698  // Wire Terms link
699  document.getElementById('link-open-terms').addEventListener('click', openLegalModal);
700
701  // Wire Send OTP Button
702  document.getElementById('btn-send-otp').addEventListener('click', handleSendOtp);
703  document.getElementById('initial-email-input').addEventListener('keydown', (e) => {
704    if (e.key === 'Enter') handleSendOtp();
705  });
706}
707
708// --------------------------------------------------------------------------
709// TELEGRAM AUTH FLOW
710// --------------------------------------------------------------------------
711
712function displayTelegramAuthCard() {
713  const oldCard = document.getElementById('email-capture-card');
714  if (oldCard) oldCard.remove();
715
716  const card = document.createElement('div');
717  card.className = 'email-consent-card tg-auth-card';
718  card.id = 'telegram-auth-card';
719  card.innerHTML = `
720    <div class="consent-title tg-title">
721      <span class="tg-svg-icon-title">
722        <svg viewBox="0 0 24 24" width="20" height="20"><path fill="#0088cc" d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm4.64 6.8c-.15 1.58-.8 5.42-1.13 7.19-.14.75-.42 1-.68 1.03-.58.05-1.02-.38-1.58-.75-.88-.58-1.38-.94-2.23-1.5-.99-.65-.35-1.01.22-1.59.15-.15 2.71-2.48 2.76-2.69a.2.2 0 00-.05-.18c-.06-.05-.14-.03-.21-.02-.09.02-1.49.95-4.22 2.79-.4.27-.76.41-1.08.4-.36-.01-1.04-.2-1.55-.37-.63-.2-1.12-.31-1.08-.66.02-.18.27-.36.75-.55 2.92-1.27 4.86-2.11 5.83-2.51 2.78-1.16 3.35-1.36 3.73-1.36.08 0 .27.02.39.12.1.08.13.19.14.27-.01.06.01.24 0 .38z"/></svg>
723      </span>
724      <span>Connexion Officielle Telegram</span>
725    </div>
726    <p class="tg-auth-desc">
727      Identifiez-vous instantanément avec votre compte Telegram pour charger automatiquement vos formations, UIDs et accès VIP :
728    </p>
729
730    <!-- Option A: Code instantané envoyé par le Bot (Fonctionne 100% sur mobile & Tailscale) -->
731    <div class="tg-auth-mode-box">
732      <label class="prompt-label">📱 Votre @pseudo ou ID Telegram</label>
733      <div class="prompt-input-row" style="margin-bottom: 8px;">
734        <input type="text" id="tg-identifier-input" class="prompt-input" placeholder="@The_c1ark_Ken7 ou 7760324627" autofocus>
735      </div>
736      <button id="btn-send-tg-code" class="prompt-submit-btn tg-btn-submit" style="width: 100%; padding: 10px; font-size: 12px;">
737        Recevoir mon code sur Telegram ➔
738      </button>
739    </div>
740
741    <!-- Option B: Widget Telegram officiel (si supporté par le domaine) -->
742    <div class="auth-divider-mini">
743      <span>OU VIA LE WIDGET TELEGRAM</span>
744    </div>
745    <div id="tg-widget-container" class="tg-widget-box"></div>
746
747    <div style="text-align: center; margin-top: 14px;">
748      <span class="legal-popup-link" id="link-back-email">✉️ Revenir à la connexion par courriel</span>
749    </div>
750  `;
751
752  UI.chatStream.appendChild(card);
753  scrollChatToBottom();
754
755  // Inject Telegram Widget script
756  try {
757    const script = document.createElement('script');
758    script.async = true;
759    script.src = "https://telegram.org/js/telegram-widget.js?22";
760    script.setAttribute('data-telegram-login', 'CryptoQc_Welcome_bot');
761    script.setAttribute('data-size', 'large');
762    script.setAttribute('data-radius', '10');
763    script.setAttribute('data-onauth', 'onTelegramAuth(user)');
764    script.setAttribute('data-request-access', 'write');
765    const container = document.getElementById('tg-widget-container');
766    if (container) container.appendChild(script);
767  } catch (e) {
768    console.warn('Could not inject Telegram widget:', e);
769  }
770
771  // Wire back link
772  document.getElementById('link-back-email').addEventListener('click', () => {
773    card.remove();
774    displayEmailCaptureCard();
775  });
776
777  // Wire Send Telegram code
778  const inputEl = document.getElementById('tg-identifier-input');
779  const btnEl = document.getElementById('btn-send-tg-code');
780
781  btnEl.addEventListener('click', handleSendTelegramCode);
782  inputEl.addEventListener('keydown', (e) => {
783    if (e.key === 'Enter') handleSendTelegramCode();
784  });
785}
786
787async function handleSendTelegramCode() {
788  const inputEl = document.getElementById('tg-identifier-input');
789  const val = (inputEl.value || '').trim();
790  if (!val) {
791    alert('Veuillez entrer votre pseudo Telegram (@nom) ou votre ID Telegram.');
792    return;
793  }
794
795  const card = document.getElementById('telegram-auth-card');
796  const loadingPill = document.createElement('div');
797  loadingPill.className = 'validation-pill valid-loading';
798  loadingPill.innerHTML = `<span>⏳</span><span>
798Recherche du compte Telegram et envoi du code...</span>`;
799  UI.chatStream.appendChild(loadingPill);
800  scrollChatToBottom();
801
802  try {
803    const res = await fetch(getApiUrl('/api/auth/telegram-send-code'), {
804      method: 'POST',
805      headers: { 'Content-Type': 'application/json' },
806      body: JSON.stringify({
807        session_id: FunnelState.sessionId,
808        identifier: val
809      })
810    });
811    const data = await res.json();
812    loadingPill.remove();
813
814    if (!data.success) {
815      if (data.not_found) {
816        card.remove();
817        await appendBotMessage(`⚠️ **Compte Telegram non détecté dans notre bot.**\n\nPour pouvoir vous connecter, vous devez avoir démarré au moins une fois notre assistant officiel **@CryptoQc_Welcome_bot**.`);
818        const startBox = document.createElement('div');
819        startBox.className = 'chat-btn-group';
820        startBox.innerHTML = `
821          <a href="${data.bot_url || 'https://cryptoqctools.com/go/web-button'}" target="_blank" class="chat-btn chat-btn-primary" style="text-decoration: none;">
822            <span>🚀 Démarrer @CryptoQc_Welcome_bot</span>
823          </a>
824          <button id="retry-tg-btn" class="chat-btn chat-btn-secondary">
825            <span>↺ Réessayer</span>
826          </button>
827        `;
828        UI.chatStream.appendChild(startBox);
829        scrollChatToBottom();
830        document.getElementById('retry-tg-btn').addEventListener('click', () => {
831          startBox.remove();
832          displayTelegramAuthCard();
833        });
834      } else {
835        alert(data.message || "Erreur lors de l'envoi du code.");
836      }
837      return;
838    }
839
840    // Success: code sent to their Telegram chat!
841    AudioFX.playPop();
842    card.remove();
843    displayTelegramOtpInputCard(data.telegram_id, data.first_name, data.username);
844
845  } catch(err) {
846    loadingPill.remove();
847    console.error(err);
848    alert('Erreur réseau lors de la communication.');
849  }
850}
851
852function displayTelegramOtpInputCard(telegramId, firstName, username) {
853  const card = document.createElement('div');
854  card.className = 'email-consent-card tg-auth-card';
855  card.id = 'tg-otp-card';
856  card.innerHTML = `
857    <div class="consent-title tg-title">
858      <span class="tg-logo-glyph">✈️</span>
859      <span>Code envoyé sur Telegram</span>
860    </div>
861    <p class="tg-auth-desc">
862      Un code de sécurité à 4 chiffres vient d'être envoyé par <strong>@CryptoQc_Welcome_bot</strong> à <strong>${firstName}</strong> (@${username || telegramId}) :
863    </p>
864
865    <div class="otp-inputs-row" style="justify-content: center; gap: 10px; margin-bottom: 12px;">
866      <input type="text" class="otp-box tg-code-box" maxlength="1" id="tg-digit-1" autofocus>
867      <input type="text" class="otp-box tg-code-box" maxlength="1" id="tg-digit-2">
868      <input type="text" class="otp-box tg-code-box" maxlength="1" id="tg-digit-3">
869      <input type="text" class="otp-box tg-code-box" maxlength="1" id="tg-digit-4">
870    </div>
871
872    <button id="btn-verify-tg-code" class="prompt-submit-btn tg-action-btn" style="width: 100%; padding: 10px; font-size: 12px;">
873      Valider et se connecter ➔
874    </button>
875
876    <div style="text-align: center; margin-top: 10px;">
877      <span class="legal-popup-link" id="btn-resend-tg-code">↺ Renvoyer un nouveau code</span>
878    </div>
879  `;
880
881  UI.chatStream.appendChild(card);
882  scrollChatToBottom();
883
884  const boxes = [
885    document.getElementById('tg-digit-1'),
886    document.getElementById('tg-digit-2'),
887    document.getElementById('tg-digit-3'),
888    document.getElementById('tg-digit-4')
889  ];
890
891  boxes.forEach((b, idx) => {
892    b.addEventListener('input', (e) => {
893      if (e.target.value.length === 1 && idx < 3) {
894        boxes[idx + 1].focus();
895      }
896      const full = boxes.map(bx => bx.value).join('');
897      if (full.length === 4) handleVerifyTelegramCode();
898    });
899    b.addEventListener('keydown', (e) => {
900      if (e.key === 'Backspace' && !e.target.value && idx > 0) {
901        boxes[idx - 1].focus();
902      }
903      if (e.key === 'Enter') handleVerifyTelegramCode();
904    });
905  });
906
907  document.getElementById('btn-verify-tg-code').addEventListener('click', handleVerifyTelegramCode);
908  document.getElementById('btn-resend-tg-code').addEventListener('click', () => {
909    card.remove();
910    displayTelegramAuthCard();
911  });
912
913  async function handleVerifyTelegramCode() {
914    const code = boxes.map(b => b.value).join('').trim();
915    if (code.length < 4) {
916      alert('Veuillez entrer les 4 chiffres du code.');
917      return;
918    }
919
920    card.remove();
921    const loadingPill = document.createElement('div');
922    loadingPill.className = 'validation-pill valid-loading';
923    loadingPill.innerHTML = `<span>⏳</span><span>
923Vérification du compte Telegram...</span>`;
924    UI.chatStream.appendChild(loadingPill);
925    scrollChatToBottom();
926
927    try {
928      const res = await fetch(getApiUrl('/api/auth/telegram-verify-code'), {
929        method: 'POST',
930        headers: { 'Content-Type': 'application/json' },
931        body: JSON.stringify({
932          session_id: FunnelState.sessionId,
933          telegram_id: telegramId,
934          code: code
935        })
936      });
937      const data = await res.json();
938      loadingPill.remove();
939
940      if (!data.verified) {
941        alert(data.message || 'Code Telegram erroné.');
942        displayTelegramOtpInputCard(telegramId, firstName, username);
943        return;
944      }
945
946      onTelegramLoginSuccess(data);
947
948    } catch(err) {
949      loadingPill.remove();
950      console.error(err);
951      alert('Erreur serveur lors de la validation.');
952      displayTelegramOtpInputCard(telegramId, firstName, username);
953    }
954  }
955}
956
957// Global Official Telegram Widget Callback
958window.onTelegramAuth = async function(user) {
959  console.log('Telegram Widget Auth received:', user);
960  const card = document.getElementById('telegram-auth-card');
961  if (card) card.remove();
962
963  const loadingPill = document.createElement('div');
964  loadingPill.className = 'validation-pill valid-loading';
965  loadingPill.innerHTML = `<span>⏳</span><span>Authentification Telegram en cours...</span>`;
966  UI.chatStream.appendChild(loadingPill);
967  scrollChatToBottom();
968
969  try {
970    const res = await fetch(getApiUrl('/api/auth/telegram-widget'), {
971      method: 'POST',
972      headers: { 'Content-Type': 'application/json' },
973      body: JSON.stringify({
974        session_id: FunnelState.sessionId,
975        ...user
976      })
977    });
978    const data = await res.json();
979    loadingPill.remove();
980
981    if (!data.verified) {
982      alert(data.message || 'Erreur lors de la validation Telegram.');
983      displayTelegramAuthCard();
984      return;
985    }
986
987    onTelegramLoginSuccess(data);
988
989  } catch (err) {
990    loadingPill.remove();
991    console.error(err);
992    alert('Erreur de connexion au serveur.');
993  }
994};
995
996async function onTelegramLoginSuccess(data) {
997  AudioFX.playSuccess();
998  FunnelState.telegramId = data.telegram_id;
999  FunnelState.telegramUsername = data.username;
1000
1001  // Flash Island status
1002  const islandText = document.querySelector('.island-text');
1003  if (islandText) {
1004    islandText.textContent = `✈️ ${data.first_name || 'Telegram'}`;
1005    islandText.style.color = '#38bdf8';
1006  }
1007
1008  const successPill = document.createElement('div');
1009  successPill.className = 'validation-pill valid-success';
1010  successPill.innerHTML = `
1011    <span>✅</span>
1012    <div>
1013      <strong>Authentifié avec succès via Telegram !</strong><br>
1014      <small>Membre : ${data.first_name || ''} (@${data.username || data.telegram_id})</small>
1015    </div>
1016  `;
1017  UI.chatStream.appendChild(successPill);
1018
1019  // If user already has profile details (UIDs, email, etc.)
1020  if (data.profile) {
1021    FunnelState.existingProfile = data.profile;
1022    if (data.profile.email) {
1023      FunnelState.email = data.profile.email;
1024      FunnelState.emailVerified = true;
1025      localStorage.setItem('cq_web_email', data.profile.email);
1026    }
1027    if (data.profile.weex_uid) {
1028      FunnelState.weexUid = String(data.profile.weex_uid).trim();
1029      localStorage.setItem('cq_weex_uid', FunnelState.weexUid);
1030    }
1031    if (data.profile.bitunix_uid) {
1032      FunnelState.bitunixUid = String(data.profile.bitunix_uid).trim();
1033      localStorage.setItem('cq_bitunix_uid', FunnelState.bitunixUid);
1034    }
1035    if (data.profile.tradingview_username) {
1036      FunnelState.tradingviewUsername = data.profile.tradingview_username;
1037      localStorage.setItem('cq_tv_username', FunnelState.tradingviewUsername);
1038    }
1039    checkAndEnableVipBottomMenu();
1040    displayRecognizedProfileCard(data.profile);
1041  } else if (data.has_email && data.email) {
1042    FunnelState.email = data.email;
1043    FunnelState.emailVerified = true;
1044    localStorage.setItem('cq_web_email', data.email);
1045    displayEmailCaptureCard();
1046  } else {
1047    // New Telegram user or email missing
1048    await appendBotMessage(`👋 **Bienvenue ${data.first_name || ''} !**\n\nVotre compte Telegram (@${data.username || data.telegram_id}) est vérifié.\n\nPour débloquer vos formations gratuites sur **Teachable** et vos indicateurs TradingView, veuillez renseigner votre courriel :`);
1049    displayEmailCaptureCard();
1050  }
1051}
1052
1053async function handleSendOtp() {
1054  const emailInput = document.getElementById('initial-email-input');
1055  const cleanEmail = (emailInput ? emailInput.value : (FunnelState.email || '')).trim().toLowerCase();
1056
1057  if (!cleanEmail || !cleanEmail.includes('@') || !cleanEmail.includes('.')) {
1058    alert('Veuillez entrer une adresse courriel valide.');
1059    return;
1060  }
1061
1062  const chkCasl = document.getElementById('chk-casl') ? document.getElementById('chk-casl').checked : false;
1063  const chkCanSpam = document.getElementById('chk-canspam') ? document.getElementById('chk-canspam').checked : false;
1064  const chkGdpr = document.getElementById('chk-gdpr') ? document.getElementById('chk-gdpr').checked : false;
1065
1066  // Si aucune case n'est cochée -> Deuxième chance explicative
1067  if (!chkCasl && !chkCanSpam && !chkGdpr) {
1068    displayConsentSecondChanceCard(cleanEmail);
1069    return;
1070  }
1071
1072  proceedSendOtp(cleanEmail, chkCasl, chkCanSpam, chkGdpr);
1073}
1074
1075function displayConsentSecondChanceCard(cleanEmail) {
1076  const oldCard = document.getElementById('email-capture-card');
1077  if (oldCard) oldCard.remove();
1078
1079  appendUserMessage(`Courriel : ${cleanEmail}`);
1080
1081  const card = document.createElement('div');
1082  card.className = 'email-consent-card second-chance-card';
1083  card.id = 'consent-second-chance-card';
1084  card.innerHTML = `
1085    <div class="consent-title" style="color: #f59e0b;">
1086      <span>📢</span>
1087      <span>Restez connecté avec Crypto Québec</span>
1088    </div>
1089    <p class="second-chance-lead">
1090      Vous n'avez coché aucune case de communication. Saviez-vous que <strong>c'est exclusivement de cette manière</strong> que Crypto Québec peut communiquer avec vous et vous transmettre :
1091    </p>
1092
1093    <div class="second-chance-benefits">
1094      <div class="benefit-item">
1095        <span class="benefit-icon">📈</span>
1096        <div class="benefit-text">
1097          <strong>Analyses exclusives de marché</strong>
1098          <small>Points macro, niveaux clés Bitcoin, Ethereum & Altcoins</small>
1099        </div>
1100      </div>
1101      <div class="benefit-item">
1102        <span class="benefit-icon">⚡</span>
1103        <div class="benefit-text">
1104          <strong>Signaux de trading & alertes en direct</strong>
1105          <small>Configurations détectées par nos algorithmes propriétaires</small>
1106        </div>
1107      </div>
1108      <div class="benefit-item">
1109        <span class="benefit-icon">🚀</span>
1110        <div class="benefit-text">
1111          <strong>Mises à jour crypto & nouveaux indicateurs</strong>
1112          <small>La meilleure façon de rester en contact avec l'équipe et de ne rien manquer</small>
1113        </div>
1114      </div>
1115    </div>
1116
1117    <div class="second-chance-actions">
1118      <button id="btn-second-chance-accept" class="second-chance-optin-btn">
1119        ✅ Oui, je souhaite recevoir les analyses & signaux ➔
1120      </button>
1121      <button id="btn-second-chance-skip" class="second-chance-skip-btn">
1122        Continuer sans recevoir les emails d'analyses ➔
1123      </button>
1124    </div>
1125
1126    <div class="second-chance-tg-notice">
1127      <div class="tg-notice-header">
1128        <span>✈️</span>
1129        <span>Communauté Officielle Telegram</span>
1130      </div>
1131      <p class="tg-notice-text">
1132        Quel que soit votre choix, <strong>Telegram</strong> vous offre également la possibilité de suivre toute la communauté Crypto Québec en temps réel et d'échanger avec plus de 5,000 passionnés.
1133      </p>
1134      <div class="tg-dual-buttons-row">
1135        <a href="https://cryptoqctools.com/go/web-button" target="_blank" class="tg-dual-btn tg-dual-telegram">
1136          <span class="tg-btn-icon">✈️</span>
1137          <span>Rejoindre sur Telegram</span>
1138          <span class="tg-btn-arrow">↗</span>
1139        </a>
1140        <button id="btn-sc-continue-here" class="tg-dual-btn tg-dual-continue">
1141          <span class="tg-btn-icon">💬</span>
1142          <span>Continuer sur ce bot</span>
1143          <span class="tg-btn-arrow">➔</span>
1144        </button>
1145      </div>
1146    </div>
1147  `;
1148
1149  UI.chatStream.appendChild(card);
1150  scrollChatToBottom();
1151  AudioFX.playPop();
1152
1153  document.getElementById('btn-second-chance-accept').addEventListener('click', () => {
1154    card.remove();
1155    proceedSendOtp(cleanEmail, true, true, true);
1156  });
1157
1158  document.getElementById('btn-second-chance-skip').addEventListener('click', () => {
1159    card.remove();
1160    proceedSendOtp(cleanEmail, false, false, false);
1161  });
1162
1163  document.getElementById('btn-sc-continue-here').addEventListener('click', () => {
1164    card.remove();
1165    proceedSendOtp(cleanEmail, false, false, false);
1166  });
1167}
1168
1169async function proceedSendOtp(cleanEmail, chkCasl, chkCanSpam, chkGdpr) {
1170  FunnelState.email = cleanEmail;
1171  localStorage.setItem('cq_web_email', cleanEmail);
1172  trackFunnelEvent('email_submit', {
1173    domain: cleanEmail.split('@')[1] || '',
1174    casl: Boolean(chkCasl),
1175    canspam: Boolean(chkCanSpam),
1176    gdpr: Boolean(chkGdpr)
1177  });
1178
1179  const oldCard = document.getElementById('email-capture-card');
1180  if (oldCard) oldCard.remove();
1181  const oldSecondChance = document.getElementById('consent-second-chance-card');
1182  if (oldSecondChance) oldSecondChance.remove();
1183
1184  // Loading pill
1185  const loadingPill = document.createElement('div');
1186  loadingPill.className = 'validation-pill valid-loading';
1187  loadingPill.innerHTML = `<span>⏳</span><span>Envoi de votre code de validation...</span>`;
1188  UI.chatStream.appendChild(loadingPill);
1189  scrollChatToBottom();
1190
1191  try {
1192    const res = await fetch(getApiUrl('/api/auth/send-otp'), {
1193      method: 'POST',
1194      headers: { 'Content-Type': 'application/json' },
1195      body: JSON.stringify({
1196        session_id: FunnelState.sessionId,
1197        email: cleanEmail,
1198        consent_casl: Boolean(chkCasl),
1199        consent_canspam: Boolean(chkCanSpam),
1200        consent_gdpr: Boolean(chkGdpr)
1201      })
1202    });
1203    const data = await res.json();
1204    loadingPill.remove();
1205
1206    await appendBotMessage(`🔐 **Code de validation envoyé !**\n\nNous venons de transmettre un code de sécurité à 6 chiffres à **${cleanEmail}**.\n\nVeuillez consulter votre boîte de réception et entrer le code ci-dessous :`);
1207
1208    displayOtpInputCard();
1209
1210    // Information Telegram systématique (quel que soit le choix)
1211    const tgReminder = document.createElement('div');
1212    tgReminder.className = 'second-chance-tg-notice';
1213    tgReminder.style.margin = '10px 0 6px';
1214    tgReminder.innerHTML = `
1215      <div class="tg-notice-header">
1216        <span>✈️</span>
1217        <span>Suivre la communauté en temps réel</span>
1218      </div>
1219      <p class="tg-notice-text">
1220        Telegram vous offre également la possibilité de suivre toute la communauté Crypto Québec en temps réel pour des alertes instantanées.
1221      </p>
1222      <div class="tg-dual-buttons-row">
1223        <a href="https://cryptoqctools.com/go/web-button" target="_blank" class="tg-dual-btn tg-dual-telegram">
1224          <span class="tg-btn-icon">✈️</span>
1225          <span>Rejoindre sur Telegram</span>
1226          <span class="tg-btn-arrow">↗</span>
1227        </a>
1228        <button id="btn-reminder-continue" class="tg-dual-btn tg-dual-continue">
1229          <span class="tg-btn-icon">💬</span>
1230          <span>Continuer sur ce bot</span>
1231          <span class="tg-btn-arrow">➔</span>
1232        </button>
1233      </div>
1234    `;
1235    UI.chatStream.appendChild(tgReminder);
1236    scrollChatToBottom();
1237
1238    const continueBtn = document.getElementById('btn-reminder-continue');
1239    if (continueBtn) {
1240      continueBtn.addEventListener('click', () => {
1241        const codeInput = document.getElementById('otp-code-input');
1242        if (codeInput) {
1243          codeInput.focus();
1244          codeInput.scrollIntoView({ behavior: 'smooth', block: 'center' });
1245        }
1246      });
1247    }
1248
1249  } catch (err) {
1250    loadingPill.remove();
1251    console.error('Error sending OTP:', err);
1252    alert('Erreur lors de l\'envoi du code. Veuillez réessayer.');
1253    displayEmailCaptureCard();
1254  }
1255}
1256
1257function displayOtpInputCard() {
1258  const card = document.createElement('div');
1259  card.className = 'otp-input-card';
1260  card.id = 'otp-input-card';
1261  card.innerHTML = `
1262    <div class="otp-title">Code de validation à 6 chiffres</div>
1263    <div class="otp-sub">Entrez le code reçu à <strong>${FunnelState.email}</strong></div>
1264
1265    <div style="display: flex; justify-content: center; gap: 8px; margin-bottom: 12px;">
1266      <input type="text" id="otp-code-input" class="prompt-input" maxlength="6" placeholder="123456" style="text-align: center; font-size: 20px; letter-spacing: 6px; font-weight: 800; width: 180px; font-family: monospace;" autofocus>
1267    </div>
1268
1269    <button id="btn-verify-otp" class="prompt-submit-btn" style="width: 100%; padding: 10px;">
1270      Valider le code ➔
1271    </button>
1272
1273    <div class="otp-resend-row">
1274      Vous n'avez pas reçu le code ? 
1275      <button id="btn-resend-otp" class="otp-resend-btn">Renvoyer</button>
1276    </div>
1277  `;
1278
1279  UI.chatStream.appendChild(card);
1280  scrollChatToBottom();
1281
1282  const codeInput = document.getElementById('otp-code-input');
1283  const verifyBtn = document.getElementById('btn-verify-otp');
1284  const resendBtn = document.getElementById('btn-resend-otp');
1285
1286  async function handleVerify() {
1287    const code = (codeInput.value || '').trim();
1288    if (code.length !== 6 || isNaN(code)) {
1289      alert('Veuillez entrer les 6 chiffres du code.');
1290      return;
1291    }
1292
1293    appendUserMessage(`Code : ${code}`);
1294    card.remove();
1295
1296    const loadingPill = document.createElement('div');
1297    loadingPill.className = 'validation-pill valid-loading';
1298    loadingPill.innerHTML = `<span>⏳</span><span>Vérification du code et recherche du profil...</span>`;
1299    UI.chatStream.appendChild(loadingPill);
1300    scrollChatToBottom();
1301
1302    try {
1303      const res = await fetch(getApiUrl('/api/auth/verify-otp'), {
1304        method: 'POST',
1305        headers: { 'Content-Type': 'application/json' },
1306        body: JSON.stringify({
1307          session_id: FunnelState.sessionId,
1308          email: FunnelState.email,
1309          otp_code: code
1310        })
1311      });
1312      const data = await res.json();
1313      loadingPill.remove();
1314
1315      if (!data.verified) {
1316        alert(data.message || 'Code erroné ou expiré.');
1317        displayOtpInputCard();
1318        return;
1319      }
1320
1321      // OTP Validated!
1322      FunnelState.emailVerified = true;
1323      if (data.session_token) {
1324        FunnelState.sessionToken = data.session_token;
1325        localStorage.setItem('cq_session_token', data.session_token);
1326      }
1327      AudioFX.playSuccess();
1328      trackAdEvent('Lead', { email: FunnelState.email });
1329      trackFunnelEvent('otp_verified');
1330
1331      const successPill = document.createElement('div');
1332      successPill.className = 'validation-pill valid-success';
1333      successPill.innerHTML = `
1334        <span>✅</span>
1335        <div>
1336          <strong>Courriel validé avec succès !</strong><br>
1337          <small>Consentements archivés en conformité LCAP, CAN-SPAM et RGPD.</small>
1338        </div>
1339      `;
1340      UI.chatStream.appendChild(successPill);
1341
1342      // Check if user is recognized from existing DB
1343      if (data.profile) {
1344        FunnelState.existingProfile = data.profile;
1345        if (data.profile.weex_uid) {
1346          FunnelState.weexUid = String(data.profile.weex_uid).trim();
1347          localStorage.setItem('cq_weex_uid', FunnelState.weexUid);
1348        }
1349        if (data.profile.bitunix_uid) {
1350          FunnelState.bitunixUid = String(data.profile.bitunix_uid).trim();
1351          localStorage.setItem('cq_bitunix_uid', FunnelState.bitunixUid);
1352        }
1353        if (data.profile.tradingview_username) {
1354          FunnelState.tradingviewUsername = data.profile.tradingview_username;
1355          localStorage.setItem('cq_tv_username', FunnelState.tradingviewUsername);
1356        }
1357        checkAndEnableVipBottomMenu();
1358      }
1359
1360      if (data.is_existing && data.profile) {
1361        displayRecognizedProfileCard(data.profile);
1362      } else {
1363        await appendBotMessage(`🎉 **Votre profil est maintenant créé et sécurisé !**\n\nPoursuivons pour débloquer vos accès TradingView et vos formations offertes.`);
1364        step_LanguageSelector();
1365      }
1366    } catch(err) {
1367      loadingPill.remove();
1368      console.error('Error verifying OTP:', err);
1369      alert('Erreur serveur lors de la vérification.');
1370      displayOtpInputCard();
1371    }
1372  }
1373
1374  verifyBtn.addEventListener('click', handleVerify);
1375  codeInput.addEventListener('keydown', (e) => {
1376    if (e.key === 'Enter') handleVerify();
1377  });
1378
1379  resendBtn.addEventListener('click', () => {
1380    card.remove();
1381    if (FunnelState.email) {
1382      proceedSendOtp(FunnelState.email, true, true, true);
1383    } else {
1384      displayEmailCaptureCard();
1385    }
1386  });
1387}
1388
1389// ==========================================================================
1390// VIP BOTTOM DOCK MENU (Demande d'Indicateurs & Accès Formations)
1391// ==========================================================================
1392
1393function hasValidatedUid() {
1394  if (FunnelState.weexUid && String(FunnelState.weexUid).trim()) return true;
1395  if (FunnelState.bitunixUid && String(FunnelState.bitunixUid).trim()) return true;
1396  if (FunnelState.existingProfile) {
1397    if (FunnelState.existingProfile.weex_uid && String(FunnelState.existingProfile.weex_uid).trim()) return true;
1398    if (FunnelState.existingProfile.bitunix_uid && String(FunnelState.existingProfile.bitunix_uid).trim()) return true;
1399  }
1400  const lsW = localStorage.getItem('cq_weex_uid');
1401  if (lsW && lsW.trim()) {
1402    FunnelState.weexUid = lsW.trim();
1403    return true;
1404  }
1405  const lsB = localStorage.getItem('cq_bitunix_uid');
1406  if (lsB && lsB.trim()) {
1407    FunnelState.bitunixUid = lsB.trim();
1408    return true;
1409  }
1410  return false;
1411}
1412
1413function getExistingUidSummary() {
1414  const weex = FunnelState.weexUid || (FunnelState.existingProfile && FunnelState.existingProfile.weex_uid);
1415  const bitunix = FunnelState.bitunixUid || (FunnelState.existingProfile && FunnelState.existingProfile.bitunix_uid);
1416  const parts = [];
1417  if (weex) parts.push(`WEEX (UID: <code>${weex}</code>)`);
1418  if (bitunix) parts.push(`Bitunix (UID: <code>${bitunix}</code>)`);
1419  return parts.length > 0 ? parts.join(' et ') : 'Compte Partenaire Validé';
1420}
1421
1422function isValidatedMember() {
1423  return Boolean(FunnelState.emailVerified || FunnelState.telegramId);
1424}
1425
1426function checkAndEnableVipBottomMenu() {
1427  if (isValidatedMember() && hasValidatedUid()) {
1428    renderVipBottomMenu();
1429    if (UI.drawerFeedBtn) UI.drawerFeedBtn.style.display = 'inline-flex';
1430    if (UI.chatFeedBtn) UI.chatFeedBtn.style.display = 'inline-flex';
1431  }
1432}
1433
1434function renderVipBottomMenu() {
1435  if (!UI.inputDock) return;
1436
1437  const currentUid = FunnelState.weexUid || FunnelState.bitunixUid || 
1438    (FunnelState.existingProfile && (FunnelState.existingProfile.weex_uid || FunnelState.existingProfile.bitunix_uid)) || 'Validé';
1439
1440  UI.inputDock.innerHTML = `
1441    <div class="vip-dock-menu" id="vip-dock-menu">
1442      <div class="vip-dock-header">
1443        <span class="vip-pill-badge">✨ ACCÈS VIP ACTIF</span>
1444        <span class="vip-uid-info">UID : <code>${currentUid}</code> 🟢</span>
1445      </div>
1446      <div class="vip-dock-buttons">
1447        <button id="vip-btn-indicators" class="vip-dock-btn vip-indicators-btn" title="Faire une demande d'indicateurs TradingView">
1448          <span class="vip-btn-icon">📊</span>
1449          <div class="vip-btn-text">
1450            <strong>Indicateurs TV</strong>
1451            <small>Demande licences</small>
1452          </div>
1453          <span class="vip-btn-arrow">➔</span>
1454        </button>
1455
1456        <button id="vip-btn-courses" class="vip-dock-btn vip-courses-btn" title="Accéder directement aux formations offertes">
1457          <span class="vip-btn-icon">🎓</span>
1458          <div class="vip-btn-text">
1459            <strong>Formations</strong>
1460            <small>Teachable 0$</small>
1461          </div>
1462          <span class="vip-btn-arrow">➔</span>
1463        </button>
1464
1465        <button id="vip-btn-feed" class="vip-dock-btn vip-feed-btn" title="Consulter les alertes Telegram VIP et les podcasts">
1466          <span class="vip-btn-icon">📡</span>
1467          <div class="vip-btn-text">
1468            <strong>Flux VIP</strong>
1469            <small>Telegram & Pods</small>
1470          </div>
1471          <span class="vip-btn-arrow">➔</span>
1472        </button>
1473      </div>
1474    </div>
1475  `;
1476
1477  // Afficher les raccourcis dans les barres d'outils
1478  if (UI.drawerFeedBtn) UI.drawerFeedBtn.style.display = 'inline-flex';
1479  if (UI.chatFeedBtn) UI.chatFeedBtn.style.display = 'inline-flex';
1480
1481  // Wire bottom VIP buttons
1482  const btnInd = document.getElementById('vip-btn-indicators');
1483  const btnCourses = document.getElementById('vip-btn-courses');
1484  const btnFeed = document.getElementById('vip-btn-feed');
1485
1486  if (btnInd) btnInd.addEventListener('click', triggerVipIndicatorRequest);
1487  if (btnCourses) btnCourses.addEventListener('click', triggerVipCoursesAccess);
1488  if (btnFeed) btnFeed.addEventListener('click', openVipFeedModal);
1489}
1490
1491async function triggerVipIndicatorRequest() {
1492  AudioFX.playPop();
1493  appendUserMessage("Je souhaite faire ma demande d'indicateurs TradingView 📊");
1494
1495  const existingTv = FunnelState.tradingviewUsername || 
1496    (FunnelState.existingProfile && FunnelState.existingProfile.tradingview_username);
1497
1498  if (existingTv) {
1499    await appendBotMessage(`📊 **Attribution des Indicateurs TradingView VIP**\n\nVotre pseudo TradingView actuellement enregistré est **@${existingTv}**.\n\nNos algorithmes exclusifs (*Impulsion Scalping, Scanner MTF, Trend Index*) sont attribués directement sur ce compte.`);
1500    
1501    const card = document.createElement('div');
1502    card.className = 'chat-btn-group';
1503    card.innerHTML = `
1504      <button id="btn-confirm-tv" class="chat-btn chat-btn-primary">
1505        <span>✅ Confirmer l'accès pour @${existingTv}</span>
1506      </button>
1507      <button id="btn-change-tv" class="chat-btn chat-btn-secondary">
1508        <span>✏️ Changer mon pseudo TradingView</span>
1509      </button>
1510    `;
1511    UI.chatStream.appendChild(card);
1512    scrollChatToBottom();
1513
1514    document.getElementById('btn-confirm-tv').addEventListener('click', async () => {
1515      card.remove();
1516      const loadingPill = document.createElement('div');
1517      loadingPill.className = 'validation-pill valid-loading';
1518      loadingPill.innerHTML = `<span>⏳</span><span>Transmission de votre demande d'accès à nos administrateurs...</span>`;
1519      UI.chatStream.appendChild(loadingPill);
1520      scrollChatToBottom();
1521
1522      try {
1523        await fetch(getApiUrl('/api/submit-tradingview'), {
1524          method: 'POST',
1525          headers: { 'Content-Type': 'application/json' },
1526          body: JSON.stringify({
1527            session_id: FunnelState.sessionId,
1528            tradingview_username: existingTv
1529          })
1530        });
1531      } catch(e) {
1532        console.error(e);
1533      }
1534      loadingPill.remove();
1535
1536      AudioFX.playSuccess();
1537      const successPill = document.createElement('div');
1538      successPill.className = 'validation-pill valid-success';
1539      successPill.innerHTML = `
1540        <span>📨</span>
1541        <div>
1542          <strong>Demande transmise avec succès !</strong><br>
1543          <small>Votre demande pour <code>@${existingTv}</code> est en cours de validation par nos administrateurs.</small>
1544        </div>
1545      `;
1546      UI.chatStream.appendChild(successPill);
1547      scrollChatToBottom();
1548
1549      step7_TelegramVIPGateway();
1550    });
1551    document.getElementById('btn-change-tv').addEventListener('click', () => {
1552      card.remove();
1553      step6_TradingViewUsername();
1554    });
1555  } else {
1556    step6_TradingViewUsername();
1557  }
1558}
1559
1560async function triggerVipCoursesAccess() {
1561  AudioFX.playPop();
1562  appendUserMessage("Je souhaite accéder à mes formations offertes 🎓");
1563
1564  const courses = (FunnelState.existingProfile && FunnelState.existingProfile.teachable_courses) || [];
1565  const email = FunnelState.email || 
1566    (FunnelState.existingProfile && FunnelState.existingProfile.email) || 'votre courriel vérifié';
1567
1568  let coursesListHtml = '';
1569  if (courses.length > 0) {
1570    coursesListHtml = courses.map(c => `
1571      <div class="teachable-course-item">
1572        <span class="chk">✓</span>
1573        <span>${c}</span>
1574      </div>
1575    `).join('');
1576  } else {
1577    coursesListHtml = `
1578      <div class="teachable-course-item">
1579        <span class="chk">✓</span>
1580        <span>Formation Complète Trading Crypto Québec (Offerte 0$)</span>
1581      </div>
1582      <div class="teachable-course-item">
1583        <span class="chk">✓</span>
1584        <span>Masterclass Scalping & Indicateurs Algorithmiques</span>
1585      </div>
1586    `;
1587  }
1588
1589  const card = document.createElement('div');
1590  card.className = 'teachable-hub-card';
1591  card.innerHTML = `
1592    <div class="recognized-hdr">
1593      <span class="recognized-badge">ESPACE MEMBRE ACTIF</span>
1594      <span class="recognized-title">Vos Formations Offertes (0$)</span>
1595    </div>
1596    <p style="font-size: 11px; color: #94a3b8; margin: 6px 0 10px;">
1597      Synchronisé avec le compte : <code>${email}</code>
1598    </p>
1599    <div class="teachable-course-list">
1600      ${coursesListHtml}
1601    </div>
1602    <a href="https://formationcryptoquebec.com/sign_in" target="_blank" class="prompt-submit-btn" style="display: block; text-align: center; text-decoration: none; padding: 10px; font-size: 12px;">
1603      🚀 Ouvrir mon Espace Académie Teachable ➔
1604    </a>
1605  `;
1606  UI.chatStream.appendChild(card);
1607  scrollChatToBottom();
1608}
1609
1610function displayRecognizedProfileCard(profile) {
1611  if (profile.weex_uid) {
1612    FunnelState.weexUid = String(profile.weex_uid).trim();
1613    localStorage.setItem('cq_weex_uid', FunnelState.weexUid);
1614  }
1615  if (profile.bitunix_uid) {
1616    FunnelState.bitunixUid = String(profile.bitunix_uid).trim();
1617    localStorage.setItem('cq_bitunix_uid', FunnelState.bitunixUid);
1618  }
1619  if (profile.tradingview_username) {
1620    FunnelState.tradingviewUsername = profile.tradingview_username;
1621    localStorage.setItem('cq_tv_username', FunnelState.tradingviewUsername);
1622  }
1623
1624  checkAndEnableVipBottomMenu();
1625
1626  const card = document.createElement('div');
1627  card.className = 'recognized-profile-card';
1628  card.innerHTML = `
1629    <div class="recognized-hdr">
1630      <span class="recognized-badge">COMPTE RETROUVÉ</span>
1631      <span class="recognized-title">Ravi de vous revoir ${profile.first_name || profile.username || 'Cher Membre'} !</span>
1632    </div>
1633    <div style="font-size: 11px; color: var(--text-secondary); line-height: 1.4;">
1634      Votre profil Crypto Québec a été identifié dans notre base de données :
1635    </div>
1636    <div class="profile-pill-grid">
1637      <div class="profile-pill-item">
1638        Telegram : <strong>${profile.telegram_id ? `@${profile.username || profile.telegram_id}` : 'Non lié'}</strong>
1639      </div>
1640      <div class="profile-pill-item">
1641        WEEX : <strong>${profile.weex_uid ? `${profile.weex_uid} (Validé 🟢)` : 'À renseigner'}</strong>
1642      </div>
1643      <div class="profile-pill-item">
1644        Bitunix : <strong>${profile.bitunix_uid ? `${profile.bitunix_uid} (Validé 🟢)` : 'À renseigner'}</strong>
1645      </div>
1646      <div class="profile-pill-item">
1647        Formations : <strong>${profile.teachable_courses ? profile.teachable_courses.length : (profile.teachable_verified ? 1 : 0)} active(s)</strong>
1648      </div>
1649    </div>
1650  `;
1651
1652  UI.chatStream.appendChild(card);
1653  scrollChatToBottom();
1654
1655  setTimeout(() => {
1656    step_LanguageSelector();
1657  }, 1000);
1658}
1659
1660
1661// ==========================================================================
1662// STEP: LANGUAGE SELECTOR & PROGRESSION (From Funnel 9)
1663// ==========================================================================
1664
1665async function step_LanguageSelector() {
1666  const data = FunnelState.funnelData;
1667  await appendBotMessage(data.welcome.select_language);
1668
1669  const langContainer = document.createElement('div');
1670  langContainer.className = 'chat-btn-group';
1671  langContainer.innerHTML = `
1672    <button class="chat-btn" data-lang="fr">
1673      <span>🇫🇷 Français</span>
1674      <span>➔</span>
1675    </button>
1676    <button class="chat-btn" data-lang="en">
1677      <span>🇬🇧 English</span>
1678      <span>➔</span>
1679    </button>
1680    <button class="chat-btn" data-lang="es">
1681      <span>🇪🇸 Español</span>
1682      <span>➔</span>
1683    </button>
1684  `;
1685  UI.chatStream.appendChild(langContainer);
1686  scrollChatToBottom();
1687
1688  langContainer.querySelectorAll('button').forEach(btn => {
1689    btn.addEventListener('click', async (e) => {
1690      const selectedLang = e.currentTarget.getAttribute('data-lang');
1691      appendUserMessage(e.currentTarget.textContent.trim());
1692      langContainer.remove();
1693      if (selectedLang !== FunnelState.language) {
1694        FunnelState.language = selectedLang;
1695        localStorage.setItem('cq_web_lang', selectedLang);
1696        updateLanguageUI();
1697        FunnelState.funnelData = await fetchFunnelContent(selectedLang);
1698      }
1699      step2_IntroValue();
1700    });
1701  });
1702}
1703
1704async function step2_IntroValue() {
1705  const data = FunnelState.funnelData;
1706  await appendBotMessage(data.intro.text);
1707  step3_Qualification();
1708}
1709
1710async function step3_Qualification() {
1711  const data = FunnelState.funnelData;
1712  await appendBotMessage(data.qualification.question);
1713
1714  const qualGroup = document.createElement('div');
1715  qualGroup.className = 'chat-btn-group';
1716  
1717  data.qualification.options.forEach(opt => {
1718    const btn = document.createElement('button');
1719    btn.className = 'chat-btn';
1720    btn.innerHTML = `
1721      <div>
1722        <div style="font-weight: 700; color: #fff;">${opt.title}</div>
1723        <div style="font-size: 11px; color: #94a3b8; font-weight: normal; margin-top: 2px;">${opt.desc}</div>
1724      </div>
1725      <span style="color: var(--cq-emerald); font-weight: bold;">➔</span>
1726    `;
1727    btn.addEventListener('click', async () => {
1728      trackFunnelEvent('profile_select', { profile_id: opt.id, profile_title: opt.title });
1729      appendUserMessage(opt.title);
1730      qualGroup.remove();
1731      FunnelState.profile = opt.id;
1732      
1733      fetch(getApiUrl('/api/qualify'), {
1734        method: 'POST',
1735        headers: { 'Content-Type': 'application/json' },
1736        body: JSON.stringify({
1737          session_id: FunnelState.sessionId,
1738          profile_type: opt.id
1739        })
1740      });
1741
1742      const pathInfo = data.paths[opt.id] || data.paths['debutant'];
1743      await appendBotMessage(`✨ **${pathInfo.title}**\n\n${pathInfo.text}`);
1744
1745      // VÉRIFICATION D'UID EXISTANT : Ne pas redemander si déjà présent
1746      if (hasValidatedUid()) {
1747        const summary = getExistingUidSummary();
1748        await appendBotMessage(`✅ **Compte Partenaire Déjà Enregistré !**\n\nVotre compte **${summary}** est déjà associé et validé dans notre base de données.\n\nIl n'est pas nécessaire de le redemander. Vos accès VIP sont directement débloqués ! 🚀`);
1749        checkAndEnableVipBottomMenu();
1750        step5_TeachableActivation();
1751      } else {
1752        step4_PartnerOffer();
1753      }
1754    });
1755    qualGroup.appendChild(btn);
1756  });
1757
1758  UI.chatStream.appendChild(qualGroup);
1759  scrollChatToBottom();
1760}
1761
1762async function step4_PartnerOffer() {
1763  const data = FunnelState.funnelData;
1764  if (hasValidatedUid()) {
1765    const summary = getExistingUidSummary();
1766    await appendBotMessage(`✅ **Compte Partenaire Actif :** Vos accès **${summary}** sont déjà enregistrés sur votre profil. Inutile de les renseigner à nouveau.`);
1767    checkAndEnableVipBottomMenu();
1768    step5_TeachableActivation();
1769    return;
1770  }
1771  await appendBotMessage(data.partners.text);
1772
1773  const partnersContainer = document.createElement('div');
1774  partnersContainer.className = 'system-card';
1775  partnersContainer.innerHTML = `
1776    <!-- WEEX Card -->
1777    <div class="exchange-select-card">
1778      <div class="ex-card-hdr">
1779        <span class="ex-name">${data.partners.weex.name}</span>
1780        <span class="ex-code-pill" title="Cliquez pour copier">CODE : ${data.partners.weex.code} 📋</span>
1781      </div>
1782      <div style="font-size: 10px; color: var(--cq-emerald); font-weight: 700; margin-bottom: 4px;">
1783        ${data.partners.weex.badge}
1784      </div>
1785      <div class="ex-desc">${data.partners.weex.description}</div>
1786      <div class="ex-card-actions">
1787        <a href="${data.partners.weex.link}" target="_blank" class="ex-link-btn" data-partner="weex">Créer compte WEEX ↗</a>
1788        <button class="ex-validate-btn" id="btn-input-weex">Valider mon UID ⚡</button>
1789      </div>
1790    </div>
1791
1792    <!-- Bitunix Card -->
1793    <div class="exchange-select-card">
1794      <div class="ex-card-hdr">
1795        <span class="ex-name">${data.partners.bitunix.name}</span>
1796        <span class="ex-code-pill" title="Cliquez pour copier">CODE : ${data.partners.bitunix.code} 📋</span>
1797      </div>
1798      <div style="font-size: 10px; color: var(--cq-purple); font-weight: 700; margin-bottom: 4px;">
1799        ${data.partners.bitunix.badge}
1800      </div>
1801      <div class="ex-desc">${data.partners.bitunix.description}</div>
1802      <div class="ex-card-actions">
1803        <a href="${data.partners.bitunix.link}" target="_blank" class="ex-link-btn" data-partner="bitunix">Créer compte Bitunix ↗</a>
1804        <button class="ex-validate-btn" style="background: var(--cq-purple); color: #fff;" id="btn-input-bitunix">Valider mon UID ⚡</button>
1805      </div>
1806    </div>
1807  `;
1808
1809  UI.chatStream.appendChild(partnersContainer);
1810  scrollChatToBottom();
1811
1812  partnersContainer.querySelectorAll('.ex-link-btn').forEach(link => {
1813    link.addEventListener('click', () => {
1814      const partner = link.getAttribute('data-partner') || (link.href.includes('weex') ? 'weex' : 'bitunix');
1815      trackFunnelEvent('partner_click', { partner: partner, url: link.href });
1816    });
1817  });
1818
1819  partnersContainer.querySelectorAll('.ex-code-pill').forEach(pill => {
1820    pill.addEventListener('click', (e) => {
1821      const text = e.target.textContent.includes('QUEBEC') ? 'QUEBEC' : 'gwzp';
1822      navigator.clipboard.writeText(text);
1823      const original = e.target.textContent;
1824      e.target.textContent = 'COPIÉ ! ✅';
1825      setTimeout(() => e.target.textContent = original, 1500);
1826    });
1827  });
1828
1829  document.getElementById('btn-input-weex').addEventListener('click', () => {
1830    trackFunnelEvent('uid_prompt_open', { partner: 'weex' });
1831    partnersContainer.remove();
1832    promptUidInput('weex');
1833  });
1834
1835  document.getElementById('btn-input-bitunix').addEventListener('click', () => {
1836    trackFunnelEvent('uid_prompt_open', { partner: 'bitunix' });
1837    partnersContainer.remove();
1838    promptUidInput('bitunix');
1839  });
1840}
1841
1842function renderAffiliationRecoveryCard(exchange, uid, dataRes) {
1843  const exName = exchange === 'weex' ? 'WEEX' : 'Bitunix';
1844  const code = exchange === 'weex' ? 'QUEBEC' : 'gwzp';
1845  const supportUrl = exchange === 'weex' ? 'https://support.weex.com' : 'https://bitunix.zendesk.com/hc/fr';
1846  const prewrittenMsg = `Bonjour, je souhaite lier mon compte au partenaire officiel Crypto Québec (Code: ${code}). Pouvez-vous rattacher mon UID (${uid}) s'il vous plaît ?`;
1847
1848  const card = document.createElement('div');
1849  card.className = 'affiliation-recovery-card';
1850  card.innerHTML = `
1851    <div class="recovery-header">
1852      <span class="recovery-icon">⚠️</span>
1853      <div class="recovery-title">Compte ${exName} non rattaché au code officiel (${code})</div>
1854    </div>
1855    <div class="recovery-desc">
1856      Votre UID <code>${uid}</code> est bien enregistré. Cependant, pour activer automatiquement vos indicateurs TradingView et formations offertes, votre compte doit être lié à <strong>Crypto Québec</strong>.
1857    </div>
1858
1859    <div class="recovery-options-grid">
1860      <!-- Option 1 : Demande de transfert 1-clic -->
1861      <div class="recovery-option-box">
1862        <div class="recovery-option-title">
1863          <span>⚡ Option 1 : Lier mon compte existant via le Support</span>
1864        </div>
1865        <div class="recovery-option-text">
1866          Copiez ce message et collez-le au Live Chat du support ${exName} :
1867        </div>
1868        <div class="recovery-copy-row">
1869          <div class="recovery-msg-box" id="msg-to-copy">${prewrittenMsg}</div>
1870          <button type="button" class="recovery-copy-btn" id="btn-copy-support-msg">📋 Copier</button>
1871        </div>
1872      </div>
1873
1874      <!-- Option 2 : Créer un sous-compte -->
1875      <div class="recovery-option-box">
1876        <div class="recovery-option-title">
1877          <span>👥 Option 2 : Créer un Sous-Compte Rattaché (30 sec)</span>
1878        </div>
1879        <div class="recovery-option-text">
1880          Dans votre appli ${exName}, créez un sous-compte avec le code <code>${code}</code> sans avoir à refaire de KYC complet.
1881        </div>
1882      </div>
1883    </div>
1884
1885    <div class="recovery-actions-group">
1886      <a href="${supportUrl}" target="_blank" class="recovery-support-btn">
1887        <span>💬 Ouvrir le Support ${exName} ↗</span>
1888      </a>
1889      <div class="recovery-alt-buttons">
1890        <button type="button" class="recovery-retry-btn" id="btn-recovery-retry">
1891          🔄 Saisir un autre UID
1892        </button>
1893        <button type="button" class="recovery-continue-btn" id="btn-recovery-continue">
1894          Continuer en attente ➔
1895        </button>
1896      </div>
1897    </div>
1898  `;
1899
1900  UI.chatStream.appendChild(card);
1901  scrollChatToBottom();
1902
1903  // Wire copy button
1904  const copyBtn = card.querySelector('#btn-copy-support-msg');
1905  if (copyBtn) {
1906    copyBtn.addEventListener('click', () => {
1907      navigator.clipboard.writeText(prewrittenMsg);
1908      copyBtn.textContent = 'Copié ! ✅';
1909      setTimeout(() => copyBtn.textContent = '📋 Copier', 2000);
1910    });
1911  }
1912
1913  // Wire retry button
1914  card.querySelector('#btn-recovery-retry').addEventListener('click', () => {
1915    card.remove();
1916    promptUidInput(exchange);
1917  });
1918
1919  // Wire continue button
1920  card.querySelector('#btn-recovery-continue').addEventListener('click', () => {
1921    card.remove();
1922    step5_TeachableActivation();
1923  });
1924}
1925
1926function promptUidInput(exchange) {
1927  const data = FunnelState.funnelData;
1928  const exName = exchange === 'weex' ? 'WEEX' : 'Bitunix';
1929  const code = exchange === 'weex' ? 'QUEBEC' : 'gwzp';
1930
1931  const box = document.createElement('div');
1932  box.className = 'input-prompt-box';
1933  box.innerHTML = `
1934    <label class="prompt-label">Saisie d'UID Partenaire ${exName} (Code ${code})</label>
1935    <div class="prompt-input-row">
1936      <input type="text" id="uid-input" class="prompt-input" placeholder="Ex: 88472910" autofocus>
1937      <button id="submit-uid-btn" class="prompt-submit-btn">Vérifier ➔</button>
1938    </div>
1939    <div style="margin-top: 4px; text-align: left;">
1940      <button type="button" class="btn-uid-help" id="btn-show-uid-help">🔍 Où trouver mon UID ? (Aide en 3 sec)</button>
1941    </div>
1942  `;
1943  UI.chatStream.appendChild(box);
1944  scrollChatToBottom();
1945
1946  const helpBtn = box.querySelector('#btn-show-uid-help');
1947  if (helpBtn) {
1948    helpBtn.addEventListener('click', () => {
1949      openUidHelpModal(exchange);
1950    });
1951  }
1952
1953  const input = document.getElementById('uid-input');
1954  const btn = document.getElementById('submit-uid-btn');
1955
1956  async function handleSubmit() {
1957    const rawVal = input.value.trim();
1958    if (!rawVal) return;
1959    appendUserMessage(`Mon UID ${exName} : ${rawVal}`);
1960    box.remove();
1961
1962    const loadingPill = document.createElement('div');
1963    loadingPill.className = 'validation-pill valid-loading';
1964    loadingPill.innerHTML = `<span>⏳</span><span>Interrogation en temps réel de l'API Partner ${exName}...</span>`;
1965    UI.chatStream.appendChild(loadingPill);
1966    scrollChatToBottom();
1967
1968    try {
1969      const res = await fetch(getApiUrl('/api/validate-uid'), {
1970        method: 'POST',
1971        headers: { 'Content-Type': 'application/json' },
1972        body: JSON.stringify({
1973          session_id: FunnelState.sessionId,
1974          exchange: exchange,
1975          uid: rawVal,
1976          email: FunnelState.email
1977        })
1978      });
1979      const dataRes = await res.json();
1980      loadingPill.remove();
1981
1982      if (exchange === 'weex') {
1983        FunnelState.weexUid = rawVal;
1984        localStorage.setItem('cq_weex_uid', rawVal);
1985      } else if (exchange === 'bitunix') {
1986        FunnelState.bitunixUid = rawVal;
1987        localStorage.setItem('cq_bitunix_uid', rawVal);
1988      }
1989      checkAndEnableVipBottomMenu();
1990
1991      if (dataRes.validation_status === 'valide' || dataRes.validation_status === 'recu') {
1992        AudioFX.playSuccess();
1993        const activeEx = dataRes.exchange || exchange;
1994        trackAdEvent('CompleteRegistration', { exchange: activeEx, uid: rawVal });
1995        const partnerCode = dataRes.partner_code || (activeEx === 'weex' ? 'QUEBEC' : 'gwzp');
1996
1997        if (dataRes.validation_status === 'valide') {
1998          FunnelState.vipApproved = true;
1999          localStorage.setItem('cq_vip_approved', '1');
2000          checkAndEnableVipBottomMenu();
2001
2002          if (dataRes.cross_checked) {
2003            const crossExName = activeEx === 'weex' ? 'WEEX' : 'Bitunix';
2004            await appendBotMessage(`🔄 **Plateforme détectée automatiquement : ${crossExName} !**\n\nVotre UID a été validé avec succès sous notre code partenaire officiel \`${partnerCode}\`.`);
2005          }
2006          // Format bot identique : Accès VIP Débloqué & Auto-Approuvé
2007          await appendBotMessage(`🎉 **ACCÈS VIP DÉBLOQUÉ & AUTO-APPROUVÉ !**\n\nFélicitations, votre affiliation partenaire a été confirmée en temps réel. Votre accès VIP est immédiatement activé ! Voici vos liens officiels aux Canaux VIP Crypto Québec :`);
2008          const vipCard = document.createElement('div');
2009          vipCard.className = 'chat-btn-group';
2010          vipCard.style.display = 'flex';
2011          vipCard.style.flexDirection = 'column';
2012          vipCard.style.gap = '8px';
2013          vipCard.innerHTML = `
2014            <a href="https://t.me/+XwuXH4tVr-I3MTBh" target="_blank" class="chat-btn chat-btn-primary" style="text-decoration: none;">
2015              <span>👑 🇫🇷 Rejoindre le Canal VIP (Français)</span>
2016            </a>
2017            <a href="https://t.me/+RVyXd4wpCIhhNzYx" target="_blank" class="chat-btn chat-btn-secondary" style="text-decoration: none;">
2018              <span>👑 🇬🇧 Join VIP Channel (English)</span>
2019            </a>
2020          `;
2021          UI.chatStream.appendChild(vipCard);
2022          scrollChatToBottom();
2023        } else {
2024          // Format bot standard : Statut reçu / En cours de validation
2025          await appendBotMessage(dataRes.message || `✅ **UID ${activeEx.toUpperCase()} (${rawVal}) Enregistré !**\n\nStatut : \`recu\` (En cours de validation par nos administrateurs).\nVous recevrez une notification dès que votre accès VIP sera débloqué !`);
2026          const noticePill = document.createElement('div');
2027          noticePill.className = 'validation-pill valid-loading';
2028          noticePill.innerHTML = `
2029            <span>⏳</span>
2030            <div>
2031              <strong>Demande transmise à nos administrateurs</strong><br>
2032              <small>UID ${rawVal} (${activeEx.toUpperCase()}) • Code partenaire : <code>${partnerCode}</code></small>
2033            </div>
2034          `;
2035          UI.chatStream.appendChild(noticePill);
2036          scrollChatToBottom();
2037        }
2038
2039        setTimeout(() => {
2040          step5_TeachableActivation();
2041        }, 1200);
2042      } else {
2043        // Affiliation Recovery Card
2044        renderAffiliationRecoveryCard(exchange, rawVal, dataRes);
2045      }
2046    } catch(err) {
2047      loadingPill.remove();
2048      console.error(err);
2049      step5_TeachableActivation();
2050    }
2051  }
2052
2053  btn.addEventListener('click', handleSubmit);
2054  input.addEventListener('keydown', (e) => {
2055    if (e.key === 'Enter') handleSubmit();
2056  });
2057}
2058
2059// ─── Step 5: Choix et Activation des Formations Teachable ───────────────────────
2060async function step5_TeachableActivation() {
2061  await appendBotMessage(`🎓 **Choisissez vos Formations Offertes (0$ VIP)**\n\nEn tant que membre de l'équipe Crypto Québec, vous avez accès gratuitement à nos formations spécialisées (valeur de plus de 2,000$).\n\nActivez les cours de votre choix ci-dessous :`);
2062
2063  const coursesContainer = document.createElement('div');
2064  coursesContainer.className = 'teachable-catalog-container';
2065  coursesContainer.id = 'teachable-catalog-container';
2066  coursesContainer.innerHTML = `
2067    <div class="catalog-loading-spinner" style="text-align: center; padding: 20px; color: #94a3b8; font-size: 12px;">
2068      <span>⏳ Chargement de l'Académie Crypto Québec...</span>
2069    </div>
2070  `;
2071  UI.chatStream.appendChild(coursesContainer);
2072  scrollChatToBottom();
2073
2074  let courses = [];
2075  try {
2076    const res = await fetch(getApiUrl('/api/teachable/catalog'));
2077    const data = await res.json();
2078    if (data.success && Array.isArray(data.courses)) {
2079      courses = data.courses;
2080    }
2081  } catch(e) {
2082    console.warn('Fallback teachable catalog:', e);
2083  }
2084
2085  // Fallback direct si endpoint temporairement inaccessible
2086  if (courses.length === 0) {
2087    courses = [
2088      { id: "2810752", title: "L'Indicateur Crypto Québec Impulsion Scalping", short_title: "Impulsion Scalping", icon: "🎯", value: "GRATUIT (valeur de 397 $)", desc: "Algorithme propriétaire pour scalping rapide avec signaux d'impulsion clairs.", url: "https://crypto-quebec.teachable.com/courses/2810752" },
2089      { id: "2733704", title: "L'Art du Trading sur Futures : De Débutant à Expert", short_title: "Futures Trading Pro", icon: "📈", value: "GRATUIT (valeur de 497 $)", desc: "Maîtrise complète du levier, gestion du risque et stratégies Futures.", url: "https://crypto-quebec.teachable.com/courses/2733704" },
2090      { id: "2704829", title: "Stratégie de Confluences Statistiques", short_title: "Confluences Statistiques", icon: "⚡", value: "GRATUIT (valeur de 347 $)", desc: "Méthode rigoureuse d'association de signaux techniques et de calcul de ratio R:R.", url: "https://crypto-quebec.teachable.com/courses/2704829" },
2091      { id: "2725787", title: "AI AGENTS Académie", short_title: "AI AGENTS Académie", icon: "🤖", value: "GRATUIT (valeur de 697 $)", desc: "Déploiement et automatisation d'agents d'IA appliqués au trading.", url: "https://crypto-quebec.teachable.com/courses/2725787" },
2092      { id: "2592696", title: "Meme Coin Domination Académie", short_title: "Meme Coin Académie", icon: "🚀", value: "GRATUIT (valeur de 497 $)", desc: "Stratég
2092ies d'identification et de sniping des tokens viraux.", url: "https://crypto-quebec.teachable.com/courses/2592696" },
2093      { id: "2547792", title: "Modélisation des Inflexions & Cycles Majeurs", short_title: "Inflexions & Cycles", icon: "📊", value: "GRATUIT (valeur de 297 $)", desc: "Lecture avancée des cycles de marché et identification des retournements majeurs.", url: "https://crypto-quebec.teachable.com/courses/2547792" },
2094      { id: "2781029", title: "Pack Indicateurs Crypto Québec", short_title: "Pack Indicateurs CQ", icon: "📦", value: "GRATUIT (valeur de 197 $)", desc: "Prise en main guidée des indicateurs et templates officiels.", url: "https://crypto-quebec.teachable.com/courses/2781029" },
2095      { id: "2514623", title: "Cours de Trading Crypto - Pour Débutant", short_title: "Trading Débutant", icon: "🌱", value: "GRATUIT (valeur de 147 $)", desc: "Bases solides : vocabulaire, ordres limites/market, gestion du capital.", url: "https://crypto-quebec.teachable.com/courses/2514623" },
2096      { id: "2791297", title: "Formation Trading Gratuite", short_title: "Formation Fondations", icon: "🎁", value: "GRATUIT", desc: "Fondations et principes clés de l'écosystème Crypto Québec.", url: "https://crypto-quebec.teachable.com/courses/2791297" }
2097    ];
2098  }
2099
2100  // Render cards
2101  coursesContainer.innerHTML = `
2102    <div class="courses-grid" style="display: flex; flex-direction: column; gap: 8px; margin: 10px 0;">
2103      ${courses.map(c => `
2104        <div class="course-picker-card" id="course-card-${c.id}" style="background: rgba(255,255,255,0.03); border: 1px solid rgba(255,255,255,0.08); border-radius: 10px; padding: 10px; display: flex; flex-direction: column; gap: 6px;">
2105          <div style="display: flex; align-items: flex-start; justify-content: space-between; gap: 8px;">
2106            <div style="display: flex; align-items: center; gap: 8px;">
2107              <span style="font-size: 20px;">${c.icon}</span>
2108              <div>
2109                <div style="font-size: 12px; font-weight: 700; color: #fff;">${c.short_title || c.title}</div>
2110                <div style="font-size: 9px; font-weight: 700; color: #10b981; letter-spacing: 0.5px;">${c.value}</div>
2111              </div>
2112            </div>
2113            <button class="btn-unlock-course" data-id="${c.id}" data-url="${c.url}" style="background: rgba(16,185,129,0.15); border: 1px solid #10b981; color: #10b981; padding: 5px 9px; border-radius: 6px; font-size: 10px; font-weight: 700; cursor: pointer; white-space: nowrap;">
2114              🔓 Débloquer (0$)
2115            </button>
2116          </div>
2117          <div style="font-size: 10px; color: #94a3b8; line-height: 1.35;">${c.desc}</div>
2118        </div>
2119      `).join('')}
2120    </div>
2121    <div style="margin-top: 12px;">
2122      <button id="btn-done-courses" class="prompt-submit-btn" style="width: 100%; padding: 11px; font-size: 12px; font-weight: 700;">
2123        Passer au choix de mes indicateurs TradingView ➔
2124      </button>
2125    </div>
2126  `;
2127  scrollChatToBottom();
2128
2129  // Wire unlock buttons
2130  coursesContainer.querySelectorAll('.btn-unlock-course').forEach(btn => {
2131    btn.addEventListener('click', async (e) => {
2132      const b = e.currentTarget;
2133      const cId = b.getAttribute('data-id');
2134      const cUrl = b.getAttribute('data-url');
2135      b.disabled = true;
2136      b.textContent = '⏳ Activation...';
2137
2138      try {
2139        await fetch(getApiUrl('/api/validate-teachable'), {
2140          method: 'POST',
2141          headers: { 'Content-Type': 'application/json' },
2142          body: JSON.stringify({
2143            session_id: FunnelState.sessionId,
2144            email: FunnelState.email,
2145            course_id: cId
2146          })
2147        });
2148      } catch(err) {}
2149
2150      AudioFX.playSuccess();
2151      b.style.background = '#10b981';
2152      b.style.color = '#000';
2153      b.style.borderColor = '#10b981';
2154      b.innerHTML = `✅ Débloqué ➔ Ouvrir`;
2155      b.onclick = () => window.open(cUrl, '_blank');
2156    });
2157  });
2158
2159  // Wire continue button
2160  document.getElementById('btn-done-courses').addEventListener('click', () => {
2161    coursesContainer.style.opacity = '0.7';
2162    step6_TradingViewUsername();
2163  });
2164}
2165
2166// ─── Step 6: Nom TradingView & Sélecteur Interactif d'Indicateurs (Max 5) ────────
2167async function step6_TradingViewUsername() {
2168  const existingTv = FunnelState.tradingviewUsername || 
2169    (FunnelState.existingProfile && FunnelState.existingProfile.tradingview_username);
2170
2171  if (existingTv) {
2172    await appendBotMessage(`📊 **Compte TradingView Actuel :**\n\nVotre pseudo enregistré est **@${existingTv}**.\n\nVoulez-vous choisir vos 5 indicateurs pour ce compte ?`);
2173    const confirmCard = document.createElement('div');
2174    confirmCard.className = 'chat-btn-group';
2175    confirmCard.innerHTML = `
2176      <button id="btn-keep-tv" class="chat-btn chat-btn-primary">
2177        <span>✅ Choisir mes indicateurs pour @${existingTv}</span>
2178      </button>
2179      <button id="btn-change-tv" class="chat-btn chat-btn-secondary">
2180        <span>✏️ Changer de pseudo TradingView</span>
2181      </button>
2182    `;
2183    UI.chatStream.appendChild(confirmCard);
2184    scrollChatToBottom();
2185
2186    document.getElementById('btn-keep-tv').addEventListener('click', () => {
2187      confirmCard.remove();
2188      renderInteractiveIndicatorPicker(existingTv);
2189    });
2190    document.getElementById('btn-change-tv').addEventListener('click', () => {
2191      confirmCard.remove();
2192      renderTradingViewUsernamePrompt();
2193    });
2194    return;
2195  }
2196
2197  renderTradingViewUsernamePrompt();
2198}
2199
2200function renderTradingViewUsernamePrompt() {
2201  const data = FunnelState.funnelData;
2202  appendBotMessage(`✍️ **Saisie de votre Username TradingView**\n\nEntrez votre nom d'utilisateur TradingView ci-dessous pour nous permettre de vous accorder l'accès direct aux indicateurs algorithmiques :`);
2203
2204  const tvBox = document.createElement('div');
2205  tvBox.className = 'input-prompt-box';
2206  tvBox.innerHTML = `
2207    <label class="prompt-label">📊 Pseudo TradingView</label>
2208    <div class="prompt-input-row">
2209      <input type="text" id="tv-input" class="prompt-input" placeholder="Ex: CryptoTrader_QC" autofocus>
2210      <button id="submit-tv-btn" class="prompt-submit-btn">Continuer ➔</button>
2211    </div>
2212  `;
2213  UI.chatStream.appendChild(tvBox);
2214  scrollChatToBottom();
2215
2216  const tvInput = document.getElementById('tv-input');
2217  const tvBtn = document.getElementById('submit-tv-btn');
2218
2219  function handleTvProceed() {
2220    const tvVal = tvInput.value.trim().replace('@', '');
2221    if (!tvVal) return;
2222    appendUserMessage(`Mon TradingView : @${tvVal}`);
2223    FunnelState.tradingviewUsername = tvVal;
2224    localStorage.setItem('cq_tv_username', tvVal);
2225    tvBox.remove();
2226    renderInteractiveIndicatorPicker(tvVal);
2227  }
2228
2229  tvBtn.addEventListener('click', handleTvProceed);
2230  tvInput.addEventListener('keydown', (e) => {
2231    if (e.key === 'Enter') handleTvProceed();
2232  });
2233}
2234
2235// ─── Sélecteur Interactif d'Indicateurs (Max 5, recherche, validation) ─────
2236async function renderInteractiveIndicatorPicker(tvUsername) {
2237  AudioFX.playPop();
2238
2239  await appendBotMessage(`📊 **Sélection de vos Indicateurs TradingView**\n\nVeuillez choisir **jusqu'à 5 indicateurs** pour votre compte TradingView (\`@${tvUsername}\`) :`);
2240
2241  const pickerContainer = document.createElement('div');
2242  pickerContainer.className = 'indicators-picker-widget';
2243  pickerContainer.id = 'indicators-picker-widget';
2244  pickerContainer.innerHTML = `
2245    <div style="background: rgba(18,20,28,0.95); border: 1px solid rgba(255,255,255,0.1); border-radius: 14px; padding: 14px; box-shadow: 0 10px 30px rgba(0,0,0,0.5);">
2246      
2247      <!-- Top header with live counter -->
2248      <div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 10px;">
2249        <span style="font-size: 11px; font-weight: 700; color: #10b981; letter-spacing: 0.5px;" id="ind-counter-label">
2250          📌 SÉLECTION ACTUELLE (0/5)
2251        </span>
2252        <button id="btn-ind-reset" style="background: none; border: none; color: #64748b; font-size: 11px; cursor: pointer; text-decoration: underline;">
2253          🔄 Réinitialiser
2254        </button>
2255      </div>
2256
2257      <!-- Live Search Filter -->
2258      <div style="margin-bottom: 10px;">
2259        <input type="text" id="ind-search-input" placeholder="🔎 Filtrer (Scalping, Vector, MTF, Scanner...)" style="width: 100%; background: rgba(0,0,0,0.4); border: 1px solid rgba(255,255,255,0.1); border-radius: 8px; color: #fff; padding: 8px 12px; font-size: 11px; box-sizing: border-box;">
2260      </div>
2261
2262      <!-- Limit warning toast -->
2263      <div id="ind-limit-toast" style="display: none; background: rgba(239,68,68,0.15); border: 1px solid #ef4444; color: #fca5a5; font-size: 10px; padding: 6px 10px; border-radius: 6px; margin-bottom: 8px;">
2264        ⚠️ Limite atteinte ! Vous pouvez sélectionner un maximum de 5 indicateurs.
2265      </div>
2266
2267      <!-- Indicators scroll list -->
2268      <div id="ind-list-scroll" style="max-height: 240px; overflow-y: auto; display: flex; flex-direction: column; gap: 6px; padding-right: 4px;">
2269        <div style="text-align: center; color: #94a3b8; font-size: 11px; padding: 20px;">
2270          ⏳ Chargement du catalogue d'indicateurs...
2271        </div>
2272      </div>
2273
2274      <!-- Submit button -->
2275      <div style="margin-top: 12px;">
2276        <button id="btn-validate-indicators" class="prompt-submit-btn" style="width: 100%; padding: 11px; font-size: 12px; font-weight: 700;" disabled>
2277          ⚪ Choisissez au moins 1 indicateur
2278        </button>
2279      </div>
2280    </div>
2281  `;
2282  UI.chatStream.appendChild(pickerContainer);
2283  scrollChatToBottom();
2284
2285  let catalog = [];
2286  try {
2287    const res = await fetch(getApiUrl('/api/indicators/catalog'));
2288    const data = await res.json();
2289    if (data.success && Array.isArray(data.indicators)) {
2290      catalog = data.indicators;
2291    }
2292  } catch(e) {
2293    console.warn('Fallback indicator fetch:', e);
2294  }
2295
2296  if (catalog.length === 0) {
2297    catalog = [
2298      { id: "ind_machine_learning_rsi_premium", title: "CQ MACHINE LEARNING RSI PREMIUM", desc: "Oscillateur RSI adaptatif propulsé par Machine Learning avec détection automatique des divergences régulières/cachées et tableau de bord en temps réel." },
2299      { id: "ind_trendline_vector_divergence_auto", title: "CQ Trendline + Vector + Divergence AUTO LIVE v3.1", desc: "Confluence automatisée Trendline Break + Vector Candle + Divergence sur 1H / 4H / 1D." },
2300      { id: "ind_auto_setup_watchlist_alert", title: "CQ AUTO SETUP WATCHLIST ALERT v1.0", desc: "Moteur d'alertes en temps réel scannant l'ensemble de votre watchlist TradingView pour setups confluents." },
2301      { id: "ind_scanner_crypto_auto", title: "SCANNER CRYPTO 100% AUTO", desc: "Scanner algorithmique 100% automatique détectant en temps réel les opportunités de bull run." },
2302      { id: "ind_bull_run_cycle_map", title: "CQ Bull Run Cycle Map PRO V1.1", desc: "Cartographie algorithmique des cycles de marché et bull runs." },
2303      { id: "ind_ai_trend_index_v2", title: "CQ AI Trend Index v2", desc: "Analyse directionnelle intelligente multi-couches IA et momentum." },
2304      { id: "ind_prime_flow_oscillator", title: "CQ Prime Flow Oscillator Divergences Pro", desc: "Oscillateur haut de gamme et détection automatique de divergences." },
2305      { id: "ind_vector_flow_pro", title: "CQ Vector Flow PRO", desc: "Analyse vectorielle des flux d'ordres et pression acheteuse/vendeuse." },
2306      { id: "ind_ai_strategy_optimizer", title: "CQ AI Strategy Optimizer PRO", desc: "Moteur d'optimisation IA pour identifier la stratég
2306ie la plus performante." },
2307      { id: "ind_power_order_blocks", title: "CQ Power Order Blocks Auto Presets", desc: "Détecteur automatique de blocs d'ordres institutionnels (Order Blocks)." }
2308    ];
2309  }
2310
2311  // Pre-select first 3 recommended by default
2312  const selectedIds = new Set();
2313  if (catalog.length >= 3) {
2314    selectedIds.add(catalog[0].id);
2315    selectedIds.add(catalog[1].id);
2316    selectedIds.add(catalog[2].id);
2317  }
2318
2319  const listContainer = document.getElementById('ind-list-scroll');
2320  const counterLabel  = document.getElementById('ind-counter-label');
2321  const validateBtn   = document.getElementById('btn-validate-indicators');
2322  const limitToast    = document.getElementById('ind-limit-toast');
2323  const searchInput   = document.getElementById('ind-search-input');
2324  const resetBtn      = document.getElementById('btn-ind-reset');
2325
2326  function updateUIState() {
2327    const count = selectedIds.size;
2328    counterLabel.textContent = `📌 SÉLECTION ACTUELLE (${count}/5)`;
2329
2330    if (count === 0) {
2331      validateBtn.disabled = true;
2332      validateBtn.textContent = '⚪ Choisissez au moins 1 indicateur';
2333      validateBtn.style.opacity = '0.5';
2334    } else {
2335      validateBtn.disabled = false;
2336      validateBtn.textContent = `✅ Valider ma sélection (${count}/5)`;
2337      validateBtn.style.opacity = '1';
2338    }
2339  }
2340
2341  function renderList(items) {
2342    listContainer.innerHTML = items.map(ind => {
2343      const isChecked = selectedIds.has(ind.id);
2344      return `
2345        <label class="ind-checkbox-item" data-id="${ind.id}" style="display: flex; align-items: flex-start; gap: 8px; padding: 7px 9px; border-radius: 8px; background: ${isChecked ? 'rgba(16,185,129,0.1)' : 'rgba(255,255,255,0.02)'}; border: 1px solid ${isChecked ? '#10b981' : 'rgba(255,255,255,0.06)'}; cursor: pointer; transition: all 0.15s ease;">
2346          <input type="checkbox" value="${ind.id}" ${isChecked ? 'checked' : ''} style="accent-color: #10b981; margin-top: 2px;">
2347          <div style="flex: 1; min-width: 0;">
2348            <div style="font-size: 11px; font-weight: 700; color: ${isChecked ? '#fff' : '#cbd5e1'}; line-height: 1.3;">
2349              ${ind.title}
2350            </div>
2351            <div style="font-size: 9px; color: #64748b; margin-top: 2px; line-height: 1.3;">
2352              ${ind.desc || ''}
2353            </div>
2354          </div>
2355        </label>
2356      `;
2357    }).join('');
2358
2359    // Wire individual checkboxes
2360    listContainer.querySelectorAll('input[type="checkbox"]').forEach(chk => {
2361      chk.addEventListener('change', (e) => {
2362        const id = e.target.value;
2363        if (e.target.checked) {
2364          if (selectedIds.size >= 5) {
2365            e.target.checked = false;
2366            limitToast.style.display = 'block';
2367            setTimeout(() => limitToast.style.display = 'none', 3000);
2368            return;
2369          }
2370          selectedIds.add(id);
2371          AudioFX.playPop();
2372        } else {
2373          selectedIds.delete(id);
2374          AudioFX.playPop();
2375        }
2376        updateUIState();
2377        renderList(filteredCatalog());
2378      });
2379    });
2380  }
2381
2382  function filteredCatalog() {
2383    const q = (searchInput.value || '').trim().toLowerCase();
2384    if (!q) return catalog;
2385    return catalog.filter(c => 
2386      c.title.toLowerCase().includes(q) || (c.desc && c.desc.toLowerCase().includes(q))
2387    );
2388  }
2389
2390  searchInput.addEventListener('input', () => {
2391    renderList(filteredCatalog());
2392  });
2393
2394  resetBtn.addEventListener('click', () => {
2395    selectedIds.clear();
2396    updateUIState();
2397    renderList(filteredCatalog());
2398    AudioFX.playPop();
2399  });
2400
2401  // Initial render
2402  updateUIState();
2403  renderList(catalog);
2404
2405  // Wire submission
2406  validateBtn.addEventListener('click', async () => {
2407    if (selectedIds.size === 0) return;
2408    const idsArray = Array.from(selectedIds);
2409    pickerContainer.remove();
2410
2411    const loadingPill = document.createElement('div');
2412    loadingPill.className = 'validation-pill valid-loading';
2413    loadingPill.innerHTML = `<span>⏳</span><span>Transmission de vos ${idsArray.length} indicateurs aux administrateurs...</span>`;
2414    UI.chatStream.appendChild(loadingPill);
2415    scrollChatToBottom();
2416
2417    try {
2418      const res = await fetch(getApiUrl('/api/submit-indicators'), {
2419        method: 'POST',
2420        headers: { 'Content-Type': 'application/json' },
2421        body: JSON.stringify({
2422          session_id: FunnelState.sessionId,
2423          tradingview_username: tvUsername,
2424          indicator_ids: idsArray,
2425          email: FunnelState.email,
2426          telegram_id: FunnelState.telegramId,
2427          lang: FunnelState.language || 'fr',
2428          weex_uid: FunnelState.weexUid || localStorage.getItem('cq_weex_uid') || '',
2429          bitunix_uid: FunnelState.bitunixUid || localStorage.getItem('cq_bitunix_uid') || '',
2430          vip_approved: FunnelState.vipApproved || (localStorage.getItem('cq_vip_approved') === '1')
2431        })
2432      });
2433      const dataRes = await res.json();
2434      loadingPill.remove();
2435
2436      // Nettoyer tout ancien waiting pill
2437      const oldWaitingPill = document.getElementById('tv-waiting-pill');
2438      if (oldWaitingPill) oldWaitingPill.remove();
2439
2440      const isApproved = (dataRes && (dataRes.status === 'approved' || dataRes.auto_approved)) ||
2441                         FunnelState.vipApproved || (localStorage.getItem('cq_vip_approved') === '1');
2442
2443      if (isApproved) {
2444        AudioFX.playSuccess();
2445        if (typeof confetti === 'function') {
2446          confetti({ particleCount: 90, spread: 80, origin: { y: 0.6 } });
2447        }
2448
2449        // 1. Message direct d'approbation bot avec liens d'accès TradingView
2450        await appendBotMessage(dataRes.message || `🎉 **FÉLICITATIONS ! VOTRE ACCÈS TRADINGVIEW EST ACTIVÉ** 🚀\n\n👤 **Compte :** \`@${tvUsername}\`\nVos ${idsArray.length} indicateurs ont été validés avec succès !`);
2451
2452        // 2. Badge vert de confirmation en direct
2453        const approvedBadge = document.createElement('div');
2454        approvedBadge.className = 'validation-pill valid-success';
2455        approvedBadge.innerHTML = `
2456          <span>✅</span>
2457          <div>
2458            <strong>Accès TradingView Validé & Actif !</strong><br>
2459            <small>Compte : <code>@${tvUsername}</code> • Validé par Crypto Québec</small>
2460          </div>
2461        `;
2462        UI.chatStream.appendChild(approvedBadge);
2463        scrollChatToBottom();
2464
2465        // 3. Flash Dynamic Island
2466        const islandText = document.querySelector('.island-text');
2467        if (islandText) {
2468          islandText.textContent = `🟢 TV VALIDÉ (@${tvUsername})`;
2469          islandText.style.color = '#10b981';
2470        }
2471
2472        // 4. Mettre à jour état VIP
2473        FunnelState.tradingviewApproved = true;
2474        localStorage.setItem('cq_tv_approved', '1');
2475        renderVipBottomMenu();
2476
2477      } else {
2478        AudioFX.playPop();
2479        // Affichage de la boîte de confirmation initiale
2480        await appendBotMessage(dataRes.message || `✅ **Demande d'Indicateurs Enregistrée !**\n\n📈 **Compte TradingView :** \`@${tvUsername}\`\n🎯 **Indicateurs sélectionnés (${idsArray.length}/5)**\n\n⏳ **Statut :** En attente de validation par nos administrateurs.`);
2481
2482        const waitingPill = document.createElement('div');
2483        waitingPill.className = 'validation-pill valid-loading';
2484        waitingPill.id = 'tv-waiting-pill';
2485        waitingPill.innerHTML = `
2486          <span>⏳</span>
2487          <div>
2488            <strong>En attente d'approbation administrateur...</strong><br>
2489            <small>Dès que l'administrateur valide votre demande, le message d'approbation et vos accès s'afficheront directement ici.</small>
2490          </div>
2491        `;
2492        UI.chatStream.appendChild(waitingPill);
2493        scrollChatToBottom();
2494
2495        // Démarrer la surveillance en temps réel de l'approbation admin
2496        pollTradingViewApproval(tvUsername);
2497      }
2498
2499    } catch(err) {
2500      loadingPill.remove();
2501      console.error(err);
2502    }
2503
2504    setTimeout(() => {
2505      step7_TelegramVIPGateway();
2506    }, 2000);
2507  });
2508}
2509
2510function pollTradingViewApproval(tvUsername) {
2511  if (FunnelState._tvPollTimer) {
2512    clearTimeout(FunnelState._tvPollTimer);
2513    FunnelState._tvPollTimer = null;
2514  }
2515
2516  let attempts = 0;
2517  const maxAttempts = 120; // ~15 minutes
2518
2519  function getNextInterval(attempt) {
2520    if (attempt < 10) return 3000;   // 3s (premières 30s)
2521    if (attempt < 25) return 6000;   // 6s (jusqu'à 2 min)
2522    return 12000;                    // 12s ensuite
2523  }
2524
2525  async function checkStatus() {
2526    // Si l'onglet est masqué, suspendre les requêtes et attendre le réveil
2527    if (document.visibilityState === 'hidden') {
2528      FunnelState._tvPollTimer = setTimeout(checkStatus, 6000);
2529      return;
2530    }
2531
2532    attempts++;
2533    if (attempts > maxAttempts) {
2534      FunnelState._tvPollTimer = null;
2535      return;
2536    }
2537
2538    try {
2539      const sToken = FunnelState.sessionToken || localStorage.getItem('cq_session_token') || '';
2540      const url = getApiUrl(`/api/tradingview-status?username=${encodeURIComponent(tvUsername)}&session_id=${encodeURIComponent(FunnelState.sessionId || '')}&email=${encodeURIComponent(FunnelState.email || '')}&session_token=${encodeURIComponent(sToken)}`);
2541      const res = await fetch(url, {
2542        headers: sToken ? { 'X-CQ-Session-Token': sToken } : {}
2543      });
2544      const data = await res.json();
2545
2546      if (data && data.status === 'approved') {
2547        FunnelState._tvPollTimer = null;
2548
2549        const waitingPill = document.getElementById('tv-waiting-pill');
2550        if (waitingPill) waitingPill.remove();
2551
2552        AudioFX.playSuccess();
2553        if (typeof confetti === 'function') {
2554          confetti({ particleCount: 90, spread: 80, origin: { y: 0.6 } });
2555        }
2556
2557        // 🎯 1. LE MESSAGE D'APPROBATION REVIENT DIRECTEMENT DANS L'INTERFACE
2558        await appendBotMessage(data.message || `🎉 **FÉLICITATIONS ! VOTRE ACCÈS TRADINGVIEW EST ACTIVÉ** 🚀\n\n👤 **Compte :** \`@${tvUsername}\`\nVos indicateurs ont été validés avec succès par l'équipe Crypto Québec !`);
2559
2560        // 2. Badge de confirmation en direct
2561        const approvedBadge = document.createElement('div');
2562        approvedBadge.className = 'validation-pill valid-success';
2563        approvedBadge.innerHTML = `
2564          <span>✅</span>
2565          <div>
2566            <strong>Accès TradingView Validé & Actif !</strong><br>
2567            <small>Compte : <code>@${tvUsername}</code> • Validé par Crypto Québec</small>
2568          </div>
2569        `;
2570        UI.chatStream.appendChild(approvedBadge);
2571        scrollChatToBottom();
2572
2573        // 3. Flash Dynamic Island
2574        const islandText = document.querySelector('.island-text');
2575        if (islandText) {
2576          islandText.textContent = `🟢 TV VALIDÉ (@${tvUsername})`;
2577          islandText.style.color = '#10b981';
2578        }
2579
2580        // 4. Mettre à jour le dock VIP
2581        FunnelState.tradingviewApproved = true;
2582        renderVipBottomMenu();
2583        return;
2584
2585      } else if (data && data.status === 'refused') {
2586        FunnelState._tvPollTimer = null;
2587
2588        const waitingPill = document.getElementById('tv-waiting-pill');
2589        if (waitingPill) waitingPill.remove();
2590
2591        AudioFX.playError?.();
2592        await appendBotMessage(data.message || `❌ **Votre demande d'accès TradingView a été refusée par l'administrateur**.\n\nVeuillez vérifier votre nom d'utilisateur TradingView ou contacter le support.`);
2593        return;
2594      }
2595    } catch (e) {
2596      console.debug('TV poll error:', e);
2597    }
2598
2599    const nextWait = getNextInterval(attempts);
2600    FunnelState._tvPollTimer = setTimeout(checkStatus, nextWait);
2601  }
2602
2603  FunnelState._tvPollTimer = setTimeout(checkStatus, 3000);
2604}
2605
2606// ─── Step 7: Passerelle VIP Telegram ───────────────────────────────────────────
2607async function step7_TelegramVIPGateway() {
2608  const data = FunnelState.funnelData;
2609  const tgUrl = 'https://cryptoqctools.com/go/web-button';
2610
2611  await appendBotMessage(`🎉 **Félicitations et bienvenue dans l'équipe Crypto Québec !**\n\nVous rejoignez une communauté de plus de 5,000 passionnés de trading.\n\n${(data && data.telegram_bridge && data.telegram_bridge.text) || "Rejoignez dès maintenant notre communauté Telegram pour suivre les alertes et points de marché :"}`);
2612
2613  const tgBox = document.createElement('div');
2614  tgBox.className = 'chat-btn-group';
2615  tgBox.innerHTML = `
2616    <a href="${tgUrl}" target="_blank" class="chat-btn chat-btn-primary" style="text-decoration: none;">
2617      <span>${(data && data.telegram_bridge && data.telegram_bridge.button) || "🚀 Rejoindre @CryptoQc_Welcome_bot"}</span>
2618    </a>
2619  `;
2620  UI.chatStream.appendChild(tgBox);
2621  scrollChatToBottom();
2622}
2623
2624
2625// ==========================================================================
2626// INBOX & NOTIFICATION SYSTEM (v3.0.0)
2627// ==========================================================================
2628
2629let inboxPollingInterval = null;
2630let lastKnownUnreadCount = 0;
2631
2632async function checkAndShowInboxBadge(email) {
2633  const targetEmail = (email || FunnelState.email || localStorage.getItem('cq_web_email') || '').trim().toLowerCase();
2634  if (!targetEmail || !targetEmail.includes('@')) return;
2635
2636  try {
2637    const sToken = FunnelState.sessionToken || localStorage.getItem('cq_session_token') || '';
2638    const res = await fetch(getApiUrl(`/api/user-inbox?email=${encodeURIComponent(targetEmail)}&session_id=${FunnelState.sessionId || ''}&session_token=${encodeURIComponent(sToken)}`), {
2639      headers: sToken ? { 'X-CQ-Session-Token': sToken } : {}
2640    });
2641    if (res.status === 401) {
2642      // Non autorisé (en attente de validation OTP)
2643      return;
2644    }
2645    const data = await res.json();
2646    if (!data.success) return;
2647
2648    const unread = data.unread || 0;
2649    const messages = data.messages || [];
2650
2651    // Afficher les boutons enveloppe
2652    if (UI.drawerInboxBtn) UI.drawerInboxBtn.style.display = 'inline-flex';
2653    if (UI.chatInboxBtn) UI.chatInboxBtn.style.display = 'inline-flex';
2654
2655    // Mettre à jour les badges
2656    const badgeText = unread > 9 ? '9+' : String(unread);
2657    const showBadge = unread > 0;
2658
2659    if (UI.drawerInboxBadge) {
2660      UI.drawerInboxBadge.textContent = badgeText;
2661      UI.drawerInboxBadge.style.display = showBadge ? 'block' : 'none';
2662    }
2663    if (UI.chatInboxBadge) {
2664      UI.chatInboxBadge.textContent = badgeText;
2665      UI.chatInboxBadge.style.display = showBadge ? 'block' : 'none';
2666    }
2667
2668    // Si nouveau message d'approbation reçu pendant la session
2669    if (unread > lastKnownUnreadCount && lastKnownUnreadCount > 0) {
2670      AudioFX.playSuccess();
2671      const latest = messages[0];
2672      if (latest) {
2673        appendBotMessage(`📬 **Nouveau message reçu !**\n\n**${latest.subject || 'Notification officielle'}**\nConsultez votre boîte de réception en cliquant sur l'enveloppe ✉️.`);
2674      }
2675    }
2676    lastKnownUnreadCount = unread;
2677  } catch (err) {
2678    console.debug('CQ Inbox polling notice:', err);
2679  }
2680}
2681
2682function startInboxPolling() {
2683  if (inboxPollingInterval) clearInterval(inboxPollingInterval);
2684  checkAndShowInboxBadge();
2685  inboxPollingInterval = setInterval(() => {
2686    // Ne poller que si l'onglet est actif
2687    if (document.visibilityState === 'visible') {
2688      if (FunnelState.email || localStorage.getItem('cq_web_email')) {
2689        checkAndShowInboxBadge();
2690      }
2691    }
2692  }, 25000);
2693}
2694
2695function openInboxModal() {
2696  if (!UI.inboxModal) return;
2697  UI.inboxModal.classList.add('active');
2698  loadAndRenderInboxMessages();
2699}
2700
2701function closeInboxModal() {
2702  if (!UI.inboxModal) return;
2703  UI.inboxModal.classList.remove('active');
2704}
2705
2706async function loadAndRenderInboxMessages() {
2707  const targetEmail = (FunnelState.email || localStorage.getItem('cq_web_email') || '').trim().toLowerCase();
2708  if (!targetEmail) return;
2709
2710  if (UI.inboxLoading) UI.inboxLoading.style.display = 'block';
2711  if (UI.inboxEmpty) UI.inboxEmpty.style.display = 'none';
2712  if (UI.inboxList) UI.inboxList.style.display = 'none';
2713
2714  try {
2715    const sToken = FunnelState.sessionToken || localStorage.getItem('cq_session_token') || '';
2716    const res = await fetch(getApiUrl(`/api/user-inbox?email=${encodeURIComponent(targetEmail)}&session_id=${FunnelState.sessionId || ''}&session_token=${encodeURIComponent(sToken)}`), {
2717      headers: sToken ? { 'X-CQ-Session-Token': sToken } : {}
2718    });
2719    const data = await res.json();
2720
2721    if (UI.inboxLoading) UI.inboxLoading.style.display = 'none';
2722
2723    const messages = data.messages || [];
2724    if (!messages.length) {
2725      if (UI.inboxEmpty) UI.inboxEmpty.style.display = 'block';
2726      if (UI.markAllReadBtn) UI.markAllReadBtn.style.display = 'none';
2727      return;
2728    }
2729
2730    if (UI.markAllReadBtn) UI.markAllReadBtn.style.display = (data.unread > 0) ? 'inline-block' : 'none';
2731
2732    if (UI.inboxList) {
2733      UI.inboxList.innerHTML = '';
2734      messages.forEach(msg => {
2735        const card = document.createElement('div');
2736        card.className = `inbox-msg-card ${msg.read ? 'read' : 'unread'}`;
2737
2738        let badgeClass = 'inbox-badge-general';
2739        let badgeLabel = '💬 Message Officiel';
2740        const t = (msg.type || '').toLowerCase();
2741        if (t.includes('tradingview')) {
2742          badgeClass = 'inbox-badge-tradingview';
2743          badgeLabel = '📈 TradingView';
2744        } else if (t.includes('uid')) {
2745          badgeClass = 'inbox-badge-uid';
2746          badgeLabel = '🟢 UID Partenaire';
2747        } else if (t.includes('teachable')) {
2748          badgeClass = 'inbox-badge-teachable';
2749          badgeLabel = '🎓 Teachable';
2750        }
2751
2752        const dateStr = msg.created_at ? msg.created_at.slice(0, 16).replace('T', ' ') : '';
2753        const bodyFormatted = formatBotMarkdown(msg.body || '');
2754
2755        card.innerHTML = `
2756          <div class="inbox-msg-header">
2757            <span class="inbox-msg-badge ${badgeClass}">${badgeLabel}</span>
2758            <span class="inbox-msg-time">${dateStr}</span>
2759          </div>
2760          <div class="inbox-msg-subject">${msg.subject || ''}</div>
2761          <div class="inbox-msg-body">${bodyFormatted}</div>
2762        `;
2763        UI.inboxList.appendChild(card);
2764      });
2765      UI.inboxList.style.display = 'flex';
2766    }
2767
2768    // Si messages non lus, marquer tout comme lu après affichage
2769    if (data.unread > 0) {
2770      markInboxAsRead(targetEmail);
2771    }
2772  } catch (err) {
2773    if (UI.inboxLoading) UI.inboxLoading.innerHTML = '<span style="color:#ef4444;">Erreur de chargement.</span>';
2774  }
2775}
2776
2777async function markInboxAsRead(email, messageId = '') {
2778  try {
2779    const sToken = FunnelState.sessionToken || localStorage.getItem('cq_session_token') || '';
2780    await fetch(getApiUrl('/api/mark-inbox-read'), {
2781      method: 'POST',
2782      headers: {
2783        'Content-Type': 'application/json',
2784        'X-CQ-Session-Token': sToken
2785      },
2786      body: JSON.stringify({ email: email, message_id: messageId, session_token: sToken })
2787    });
2788    // Réinitialiser les badges UI
2789    if (UI.drawerInboxBadge) UI.drawerInboxBadge.style.display = 'none';
2790    if (UI.chatInboxBadge) UI.chatInboxBadge.style.display = 'none';
2791    lastKnownUnreadCount = 0;
2792  } catch(e) {}
2793}
2794
2795function initInboxEventListeners() {
2796  if (UI.drawerInboxBtn) UI.drawerInboxBtn.addEventListener('click', openInboxModal);
2797  if (UI.chatInboxBtn) UI.chatInboxBtn.addEventListener('click', openInboxModal);
2798  if (UI.closeInboxBtn) UI.closeInboxBtn.addEventListener('click', closeInboxModal);
2799  if (UI.doneInboxBtn) UI.doneInboxBtn.addEventListener('click', closeInboxModal);
2800  if (UI.markAllReadBtn) {
2801    UI.markAllReadBtn.addEventListener('click', () => {
2802      const email = FunnelState.email || localStorage.getItem('cq_web_email');
2803      if (email) markInboxAsRead(email);
2804      loadAndRenderInboxMessages();
2805    });
2806  }
2807  if (UI.inboxModal) {
2808    UI.inboxModal.addEventListener('click', (e) => {
2809      if (e.target === UI.inboxModal) closeInboxModal();
2810    });
2811  }
2812
2813  // Écouteur de changement de visibilité d'onglet pour rafraîchir immédiatement à la reprise
2814  document.addEventListener('visibilitychange', () => {
2815    if (document.visibilityState === 'visible') {
2816      if (FunnelState.email || localStorage.getItem('cq_web_email')) {
2817        checkAndShowInboxBadge();
2818      }
2819    }
2820  });
2821}
2822
2823// ==========================================================================
2824// VIP FEED & PODCAST SYSTEM (Inspiré de CQ-mobile-app FeedScreen)
2825// Contrôle d'expulsion selon les critères de CryptoAds (J+15)
2826// ==========================================================================
2827
2828let currentFeedFilter = 'all';
2829let currentFeedItems = [];
2830let isSimulatedExpelled = false;
2831
2832function openVipFeedModal() {
2833  if (!UI.feedModal) return;
2834  UI.feedModal.classList.add('active');
2835  if (window.AudioFX && typeof window.AudioFX.playPop === 'function') {
2836    window.AudioFX.playPop();
2837  }
2838  loadAndRenderVipFeed(isSimulatedExpelled);
2839}
2840
2841function closeVipFeedModal() {
2842  if (!UI.feedModal) return;
2843  UI.feedModal.classList.remove('active');
2844  if (UI.feedList) {
2845    const iframes = UI.feedList.querySelectorAll('iframe');
2846    iframes.forEach(f => {
2847      const src = f.src;
2848      f.src = src;
2849    });
2850  }
2851}
2852
2853async function loadAndRenderVipFeed(simulateExpelled = false) {
2854  if (UI.feedLoading) UI.feedLoading.style.display = 'block';
2855  if (UI.feedExpelled) UI.feedExpelled.style.display = 'none';
2856  if (UI.feedEmpty) UI.feedEmpty.style.display = 'none';
2857  if (UI.feedList) UI.feedList.style.display = 'none';
2858
2859  const email = (FunnelState.email || localStorage.getItem('cq_web_email') || '').trim();
2860  const tgId  = (FunnelState.telegramId || localStorage.getItem('cq_telegram_id') || '').trim();
2861  const uid   = FunnelState.weexUid || FunnelState.bitunixUid || (FunnelState.existingProfile && (FunnelState.existingProfile.weex_uid || FunnelState.existingProfile.bitunix_uid)) || '';
2862  const exch  = FunnelState.selectedExchange || (FunnelState.weexUid ? 'weex' : (FunnelState.bitunixUid ? 'bitunix' : 'weex'));
2863
2864  try {
2865    const params = new URLSearchParams();
2866    if (email) params.append('email', email);
2867    if (tgId) params.append('telegram_id', tgId);
2868    if (uid) params.append('uid', uid);
2869    if (exch) params.append('exchange', exch);
2870    if (simulateExpelled) params.append('simulate_expelled', '1');
2871
2872    const res = await fetch(getApiUrl(`/api/vip/feed?${params.toString()}`));
2873    const data = await res.json();
2874
2875    if (UI.feedLoading) UI.feedLoading.style.display = 'none';
2876
2877    // 1. CAS EXPULSION (Règles CryptoAds & Inactivité J+15)
2878    if (data.is_expelled) {
2879      renderExpulsionCard(data);
2880      return;
2881    }
2882
2883    // 2. CAS ACCÈS REFUSÉ (NON APPROUVÉ)
2884    if (!data.success && !data.is_approved) {
2885      if (UI.feedEmpty) {
2886        UI.feedEmpty.innerHTML = `
2887          <div style="font-size: 38px; margin-bottom: 12px;">🔒</div>
2888          <h4 style="margin: 0 0 8px; color: #f8fafc; font-size: 16px;">Accès Réservé aux Membres VIP</h4>
2889          <p style="margin: 0 0 16px; color: #94a3b8; font-size: 13px;">${data.message || 'Validez votre compte partenaire WEEX ou Bitunix pour déverrouiller ce flux.'}</p>
2890          <button class="primary-legal-btn" onclick="closeVipFeedModal();">Retourner au Funnel</button>
2891        `;
2892        UI.feedEmpty.style.display = 'block';
2893      }
2894      return;
2895    }
2896
2897    // 3. CAS ACTIF : Affichage des publications & podcasts
2898    currentFeedItems = data.items || [];
2899    renderFilteredFeedItems();
2900
2901  } catch(err) {
2902    console.error('Erreur chargement VIP Feed:', err);
2903    if (UI.feedLoading) UI.feedLoading.style.display = 'none';
2904    if (UI.feedEmpty) {
2905      UI.feedEmpty.innerHTML = `
2906        <div style="font-size: 38px; margin-bottom: 12px;">⚠️</div>
2907        <h4 style="margin: 0 0 8px; color: #ef4444; font-size: 16px;">Erreur de connexion</h4>
2908        <p style="margin: 0; color: #94a3b8; font-size: 13px;">Impossible de récupérer le flux pour le moment. Veuillez réessayer.</p>
2909      `;
2910      UI.feedEmpty.style.display = 'block';
2911    }
2912  }
2913}
2914
2915function renderExpulsionCard(data) {
2916  if (!UI.feedExpelled) return;
2917  UI.feedExpelled.style.display = 'block';
2918  if (UI.feedList) UI.feedList.style.display = 'none';
2919  if (UI.feedEmpty) UI.feedEmpty.style.display = 'none';
2920
2921  const exchange = data.exchange || 'WEEX';
2922  const uid = data.uid || 'Non renseigné';
2923  const partnerUrl = data.partner_url || (exchange === 'Bitunix' ? 'https://www.bitunix.com/register?vipCode=gwzp' : 'https://trade.weex.com/register?vipCode=QUEBEC');
2924  const supportUrl = data.support_url || 'https://t.me/crypto_quebec_channel';
2925
2926  UI.feedExpelled.innerHTML = `
2927    <div class="vip-expulsion-card">
2928      <div class="expulsion-banner">
2929        <span class="expulsion-icon">⛔</span>
2930        <div class="expulsion-badge">COMPTE SUSPENDU • SYSTÈME CRYPTOADS</div>
2931      </div>
2932
2933      <h3 class="expulsion-title">Accès VIP Révoqué (Règle Inactivité J+15)</h3>
2934      
2935      <p class="expulsion-desc">
2936        ${data.message || `Votre période de grâce de 15 jours sans transaction enregistrée sur votre compte partenaire ${exchange} est expirée. Conformément aux critères de Crypto Québec et du système CryptoAds, vos licences d'indicateurs TradingView et votre accès au flux VIP ont été révoqués.`}
2937      </p>
2938
2939      <div class="expulsion-account-box">
2940        <div class="account-item">
2941          <span class="acc-lbl">Plateforme :</span>
2942          <span class="acc-val"><strong>${exchange}</strong></span>
2943        </div>
2944        <div class="account-item">
2945          <span class="acc-lbl">UID Associé :</span>
2946          <span class="acc-val"><code>${uid}</code></span>
2947        </div>
2948        <div class="account-item">
2949          <span class="acc-lbl">Statut CRM :</span>
2950          <span class="acc-val" style="color:#ef4444; font-weight:700;">INACTIF (J+15)</span>
2951        </div>
2952      </div>
2953
2954      <div class="expulsion-steps">
2955        <h4>⚡ Procédure de Réactivation Immédiate :</h4>
2956        <ol>
2957          <li>Ouvrez votre compte <strong>${exchange}</strong> (UID: <code>${uid}</code>).</li>
2958          <li>Exécutez au minimum un trade (Spot ou Futures) pour réactiver vos droits.</li>
2959          <li>Transmettez la preuve d'exécution à l'équipe Telegram pour lever la suspension de vos indicateurs.</li>
2960        </ol>
2961      </div>
2962
2963      <div class="expulsion-actions">
2964        <a href="${partnerUrl}" target="_blank" rel="noopener noreferrer" class="chat-btn chat-btn-primary expulsion-btn-broker">
2965          <span>🚀 Ouvrir mon compte ${exchange}</span>
2966        </a>
2967        <a href="${supportUrl}" target="_blank" rel="noopener noreferrer" class="chat-btn chat-btn-secondary expulsion-btn-support">
2968          <span>💬 Contacter le Support Telegram</span>
2969        </a>
2970      </div>
2971
2972      <div class="expulsion-test-hint">
2973        <small>💡 Note : Vous pouvez basculer l'état pour tester via le bouton « ⚖️ Mode Test Expulsion » ci-dessous.</small>
2974      </div>
2975    </div>
2976  `;
2977}
2978
2979function renderFilteredFeedItems() {
2980  if (!UI.feedList) return;
2981
2982  const query = (UI.feedSearch ? UI.feedSearch.value : '').toLowerCase().trim();
2983  const filter = currentFeedFilter;
2984
2985  const filtered = currentFeedItems.filter(item => {
2986    const matchesCat = (filter === 'all')
2987      ? true
2988      : (filter === 'news' ? (item.type === 'news' || item.type === 'podcast') : item.type === filter);
2989
2990    if (!matchesCat) return false;
2991
2992    if (!query) return true;
2993    const inTitle = (item.title || '').toLowerCase().includes(query);
2994    const inContent = (item.content || '').toLowerCase().includes(query);
2995    const inTags = item.tags && item.tags.some(t => t.toLowerCase().includes(query));
2996    return inTitle || inContent || inTags;
2997  });
2998
2999  if (filtered.length === 0) {
3000    if (UI.feedEmpty) UI.feedEmpty.style.display = 'block';
3001    UI.feedList.style.display = 'none';
3002    return;
3003  }
3004
3005  if (UI.feedEmpty) UI.feedEmpty.style.display = 'none';
3006  UI.feedList.innerHTML = '';
3007  UI.feedList.style.display = 'flex';
3008
3009  filtered.forEach(item => {
3010    const card = document.createElement('div');
3011    card.className = `vip-feed-card feed-type-${item.type || 'post'}`;
3012
3013    if (item.type === 'podcast') {
3014      card.innerHTML = buildPodcastCardHtml(item);
3015    } else if (item.type === 'signal') {
3016      card.innerHTML = buildSignalCardHtml(item);
3017    } else {
3018      card.innerHTML = buildAnalysisCardHtml(item);
3019    }
3020
3021    UI.feedList.appendChild(card);
3022  });
3023
3024  wireFeedCardInteractions();
3025}
3026
3027function buildPodcastCardHtml(item) {
3028  const epNumber = item.episodeNumber ? `Épisode #${item.episodeNumber}` : 'Épisode';
3029  const duration = item.duration || '60 min';
3030  const ytUrl = item.youtubeUrl || (item.youtubeId ? `https://www.youtube.com/watch?v=${item.youtubeId}` : 'https://youtube.com/@cryptoquebec');
3031  const embedId = item.youtubeId || '';
3032
3033  return `
3034    <div class="feed-card-header">
3035      <span class="feed-badge feed-badge-podcast">🎧 PODCAST OFFICIEL</span>
3036      <span class="feed-badge-sub">${epNumber} • ${duration}</span>
3037    </div>
3038    <h4 class="feed-podcast-title">${item.title}</h4>
3039    <p class="feed-podcast-desc">${item.content || item.description || ''}</p>
3040    
3041    ${embedId ? `
3042      <div class="podcast-player-slot" id="player-slot-${item.id}">
3043        <button class="podcast-play-btn" data-yt-id="${embedId}" data-target-slot="player-slot-${item.id}">
3044          <span class="play-icon">▶️</span>
3045          <span>Écouter / Regarder l'émission</span>
3046        </button>
3047      </div>
3048    ` : ''}
3049
3050    <div class="feed-card-footer">
3051      <span class="feed-timestamp">📅 ${item.publishedAt ? item.publishedAt.slice(0, 10) : 'Récemment'}</span>
3052      <a href="${ytUrl}" target="_blank" rel="noopener noreferrer" class="feed-ext-link">
3053        <span>Ouvrir sur YouTube ↗</span>
3054      </a>
3055    </div>
3056  `;
3057}
3058
3059function buildSignalCardHtml(item) {
3060  const title = item.title || '🎯 SIGNAL VIP';
3061  const entry = item.entryPrice || 'Marché';
3062  const sl    = item.stopLoss || 'Invalidation';
3063  const tp    = item.takeProfit || 'Target 1';
3064  const rr    = item.riskReward || '1:2.5';
3065  const tgUrl = item.channelUrl || 'https://t.me/crypto_quebec_channel';
3066  const formattedContent = typeof formatBotMarkdown === 'function' ? formatBotMarkdown(item.content || '') : item.content;
3067
3068  return `
3069    <div class="feed-card-header">
3070      <span class="feed-badge feed-badge-signal">🎯 SIGNAL VIP</span>
3071      <span class="feed-badge-dir">LONG / ACHAT</span>
3072    </div>
3073    <h4 class="feed-signal-title">${title}</h4>
3074
3075    <div class="signal-metrics-grid">
3076      <div class="signal-metric-box">
3077        <span class="metric-label">ENTRÉE</span>
3078        <span class="metric-val">${entry}</span>
3079      </div>
3080      <div class="signal-metric-box stop-loss">
3081        <span class="metric-label">STOP LOSS</span>
3082        <span class="metric-val">${sl}</span>
3083      </div>
3084      <div class="signal-metric-box take-profit">
3085        <span class="metric-label">OBJECTIF</span>
3086        <span class="metric-val">${tp}</span>
3087      </div>
3088      <div class="signal-metric-box">
3089        <span class="metric-label">RATIO R:R</span>
3090        <span class="metric-val">${rr}</span>
3091      </div>
3092    </div>
3093
3094    <div class="feed-signal-content">${formattedContent}</div>
3095
3096    <div class="feed-card-actions">
3097      <button class="feed-copy-setup-btn" data-setup="${encodeURIComponent(`CQ SIGNAL VIP:\n${title}\nEntrée: ${entry} | SL: ${sl} | TP: ${tp} | R:R: ${rr}`)}">
3098        <span class="copy-icon">📋</span>
3099        <span class="copy-text">Copier le setup</span>
3100      </button>
3101      <a href="${tgUrl}" target="_blank" rel="noopener noreferrer" class="feed-tg-btn">
3102        <span class="tg-icon">📲</span>
3103        <span>Rejoindre sur Telegram</span>
3104      </a>
3105    </div>
3106  `;
3107}
3108
3109function buildAnalysisCardHtml(item) {
3110  const title = item.title || '📊 ANALYSE TECHNIQUE';
3111  const author = item.author || 'Simon (Crypto Québec)';
3112  const formattedContent = typeof formatBotMarkdown === 'function' ? formatBotMarkdown(item.content || '') : item.content;
3113  const tgUrl = item.channelUrl || 'https://t.me/crypto_quebec_channel';
3114
3115  let tagsHtml = '';
3116  if (item.tags && item.tags.length > 0) {
3117    tagsHtml = `<div class="feed-tags">${item.tags.map(t => `<span class="feed-tag">#${t}</span>`).join('')}</div>`;
3118  }
3119
3120  return `
3121    <div class="feed-card-header">
3122      <span class="feed-badge feed-badge-analysis">📊 ANALYSE MARCHÉ</span>
3123      <span class="feed-badge-sub">Par ${author}</span>
3124    </div>
3125    <h4 class="feed-analysis-title">${title}</h4>
3126    <div class="feed-analysis-content">${formattedContent}</div>
3127    ${tagsHtml}
3128    <div class="feed-card-footer">
3129      <span class="feed-timestamp">
3129📡 Source : Telegram VIP</span>
3130      <a href="${tgUrl}" target="_blank" rel="noopener noreferrer" class="feed-ext-link">
3131        <span>Voir sur Telegram ↗</span>
3132      </a>
3133    </div>
3134  `;
3135}
3136
3137function wireFeedCardInteractions() {
3138  if (!UI.feedList) return;
3139
3140  const playBtns = UI.feedList.querySelectorAll('.podcast-play-btn');
3141  playBtns.forEach(btn => {
3142    btn.addEventListener('click', () => {
3143      const ytId = btn.getAttribute('data-yt-id');
3144      const targetSlotId = btn.getAttribute('data-target-slot');
3145      const slot = document.getElementById(targetSlotId);
3146      if (slot && ytId) {
3147        slot.innerHTML = `
3148          <div class="feed-video-wrapper">
3149            <iframe 
3150              src="https://www.youtube-nocookie.com/embed/${ytId}?autoplay=1&rel=0&modestbranding=1" 
3151              title="Crypto Québec Podcast"
3152              frameborder="0" 
3153              allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" 
3154              allowfullscreen>
3155            </iframe>
3156          </div>
3157        `;
3158      }
3159    });
3160  });
3161
3162  const copyBtns = UI.feedList.querySelectorAll('.feed-copy-setup-btn');
3163  copyBtns.forEach(btn => {
3164    btn.addEventListener('click', () => {
3165      const textToCopy = decodeURIComponent(btn.getAttribute('data-setup') || '');
3166      if (navigator.clipboard && navigator.clipboard.writeText) {
3167        navigator.clipboard.writeText(textToCopy).then(() => {
3168          const original = btn.innerHTML;
3169          btn.innerHTML = `<span>✓</span><span>Copié !</span>`;
3170          btn.classList.add('copied');
3171          setTimeout(() => {
3172            btn.innerHTML = original;
3173            btn.classList.remove('copied');
3174          }, 2000);
3175        });
3176      }
3177    });
3178  });
3179}
3180
3181function initVipFeedEventListeners() {
3182  if (UI.drawerFeedBtn) UI.drawerFeedBtn.addEventListener('click', openVipFeedModal);
3183  if (UI.chatFeedBtn) UI.chatFeedBtn.addEventListener('click', openVipFeedModal);
3184  if (UI.closeFeedBtn) UI.closeFeedBtn.addEventListener('click', closeVipFeedModal);
3185  if (UI.doneFeedBtn) UI.doneFeedBtn.addEventListener('click', closeVipFeedModal);
3186
3187  if (UI.refreshFeedBtn) {
3188    UI.refreshFeedBtn.addEventListener('click', () => {
3189      loadAndRenderVipFeed(isSimulatedExpelled);
3190    });
3191  }
3192
3193  if (UI.toggleExpelledTestBtn) {
3194    UI.toggleExpelledTestBtn.addEventListener('click', () => {
3195      isSimulatedExpelled = !isSimulatedExpelled;
3196      if (isSimulatedExpelled) {
3197        UI.toggleExpelledTestBtn.innerHTML = `✅ Réactiver (Compte Actif)`;
3198        UI.toggleExpelledTestBtn.classList.add('active-test');
3199      } else {
3200        UI.toggleExpelledTestBtn.innerHTML = `⚖️ Mode Test Expulsion`;
3201        UI.toggleExpelledTestBtn.classList.remove('active-test');
3202      }
3203      loadAndRenderVipFeed(isSimulatedExpelled);
3204    });
3205  }
3206
3207  if (UI.feedSearch) {
3208    UI.feedSearch.addEventListener('input', () => {
3209      renderFilteredFeedItems();
3210    });
3211  }
3212
3213  const filterPills = document.querySelectorAll('.vip-filter-pill');
3214  filterPills.forEach(pill => {
3215    pill.addEventListener('click', () => {
3216      filterPills.forEach(p => p.classList.remove('active'));
3217      pill.classList.add('active');
3218      currentFeedFilter = pill.getAttribute('data-filter') || 'all';
3219      renderFilteredFeedItems();
3220    });
3221  });
3222
3223  if (UI.feedModal) {
3224    UI.feedModal.addEventListener('click', (e) => {
3225      if (e.target === UI.feedModal) closeVipFeedModal();
3226    });
3227  }
3228}
3229
3230// Initialisations globales des écouteurs
3231initInboxEventListeners();
3232initVipFeedEventListeners();
3233

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.