PageSourceSearch

https://cryptoqctools.com/wp-content/themes/crypton1te-theme/js/trading-tools.js?ver=1.0.0

js cryptoqctools.com collected 2026-10-05 23:19:42 UTC 27,023 bytes, 584 lines download raw bytes

1/**
2 * Crypto Québec / CRYPTON1TE — Outils de Trading Interactifs
3 * 1. Calculateur de Taille de Position & Gestion du Risque
4 * 2. Checklist Interactive Pre-Trade (Filtre de Confluence)
5 * 3. Journal de Trading & Analytics (LocalStorage)
6 */
7
8(function () {
9    'use strict';
10
11    function getLang() {
12        return localStorage.getItem('selected_language') || localStorage.getItem('cq_lang') || document.documentElement.lang || 'fr';
13    }
14
15    let refreshAllTools = function() {};
16
17    // ─── 1. CALCULATEUR DE TAILLE DE POSITION ───────────────────────────────
18    function initPositionCalculator() {
19        const capitalInput = document.getElementById('cq-calc-capital');
20        const riskInput = document.getElementById('cq-calc-risk-pct');
21        const entryInput = document.getElementById('cq-calc-entry');
22        const stopInput = document.getElementById('cq-calc-stop');
23        const tpInput = document.getElementById('cq-calc-tp');
24
25        if (!capitalInput || !entryInput || !stopInput) return () => {};
26
27        function calculate() {
28            const capital = parseFloat(capitalInput.value) || 0;
29            const riskPct = parseFloat(riskInput.value) || 0;
30            const entry = parseFloat(entryInput.value) || 0;
31            const stop = parseFloat(stopInput.value) || 0;
32            const tp = parseFloat(tpInput.value) || 0;
33
34            const riskAmount = (capital * riskPct) / 100;
35            const stopDistance = Math.abs(entry - stop);
36            
37            let positionUnits = 0;
38            let totalPosValue = 0;
39            let leverageReq = 0;
40            let rewardAmount = 0;
41            let rrRatio = 0;
42
43            if (entry > 0 && stopDistance > 0) {
44                positionUnits = riskAmount / stopDistance;
45                totalPosValue = positionUnits * entry;
46                if (capital > 0) {
47                    leverageReq = totalPosValue / capital;
48                }
49            }
50
51            if (tp > 0 && entry > 0 && stopDistance > 0) {
52                const tpDistance = Math.abs(tp - entry);
53                rewardAmount = positionUnits * tpDistance;
54                rrRatio = tpDistance / stopDistance;
55            }
56
57            // Update UI
58            document.getElementById('cq-res-risk-amt').textContent = `$${riskAmount.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
59            document.getElementById('cq-res-pos-units').textContent = positionUnits > 0 ? positionUnits.toFixed(4) : '0.0000';
60            document.getElementById('cq-res-pos-val').textContent = `$${totalPosValue.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
61            document.getElementById('cq-res-leverage').textContent = `${leverageReq.toFixed(1)}x`;
62
63            const rrEl = document.getElementById('cq-res-rr');
64            const rrStatusEl = document.getElementById('cq-res-rr-status');
65
66            const lang = getLang();
67
68            const statusTextDict = {
69                fr: {
70                    excellent: 'Excellent (R:R ≥ 1:2)',
71                    acceptable: 'Acceptable (R:R ≥ 1:1.5)',
72                    highRisk: 'Risque Élevé (R:R < 1:1.5)',
73                    awaiting: 'Attente d\'Objectif (TP)'
74                },
75                en: {
76                    excellent: 'Excellent (R:R ≥ 1:2)',
77                    acceptable: 'Acceptable (R:R ≥ 1:1.5)',
78                    highRisk: 'High Risk (R:R < 1:1.5)',
79                    awaiting: 'Awaiting Target (TP)'
80                },
81                es: {
82                    excellent: 'Excelente (R:R ≥ 1:2)',
83                    acceptable: 'Aceptable (R:R ≥ 1:1.5)',
84                    highRisk: 'Riesgo Alto (R:R < 1:1.5)',
85                    awaiting: 'Esperando Objetivo (TP)'
86                }
87            };
88            const statusText = statusTextDict[lang] || statusTextDict['fr'];
89
90            if (rrRatio > 0) {
91                rrEl.textContent = `1 : ${rrRatio.toFixed(2)}`;
92                if (rrRatio >= 2.0) {
93                    rrEl.style.color = '#00e676';
94                    if (rrStatusEl) rrStatusEl.className = 'badge bg-success';
95                    if (rrStatusEl) rrStatusEl.textContent = statusText.excellent;
96                } else if (rrRatio >= 1.5) {
97                    rrEl.style.color = '#00e5ff';
98                    if (rrStatusEl) rrStatusEl.className = 'badge bg-info text-dark';
99                    if (rrStatusEl) rrStatusEl.textContent = statusText.acceptable;
100                } else {
101                    rrEl.style.color = '#ff3d00';
102                    if (rrStatusEl) rrStatusEl.className = 'badge bg-danger';
103                    if (rrStatusEl) rrStatusEl.textContent = statusText.highRisk;
104                }
105            } else {
106                rrEl.textContent = '1 : 0.00';
107                rrEl.style.color = '#7b839a';
108                if (rrStatusEl) rrStatusEl.className = 'badge bg-secondary';
109                if (rrStatusEl) rrStatusEl.textContent = statusText.awaiting;
110            }
111        }
112
113        const inputs = [capitalInput, riskInput, entryInput, stopInput, tpInput];
114        inputs.forEach(input => {
115            if (input) input.addEventListener('input', calculate);
116        });
117
118        // Quick Risk Preset Buttons
119        document.querySelectorAll('.cq-risk-preset-btn').forEach(btn => {
120            btn.addEventListener('click', function () {
121                document.querySelectorAll('.cq-risk-preset-btn').forEach(b => b.classList.remove('active'));
122                this.classList.add('active');
123                riskInput.value = this.getAttribute('data-risk');
124                calculate();
125            });
126        });
127
128        calculate();
129        return calculate;
130    }
131
132    // ─── 2. CHECKLIST PRE-TRADE ─────────────────────────────────────────────
133    function initPreTradeChecklist() {
134        const checkboxes = document.querySelectorAll('.cq-chk-item');
135        const scoreEl = document.getElementById('cq-chk-score-val');
136        const barEl = document.getElementById('cq-chk-score-bar');
137        const statusEl = document.getElementById('cq-chk-status');
138
139        if (!checkboxes.length || !scoreEl) return () => {};
140
141        function updateChecklistScore() {
142            let total = checkboxes.length;
143            let checked = 0;
144
145            checkboxes.forEach(chk => {
146                if (chk.checked) checked++;
147            });
148
149            const percent = Math.round((checked / total) * 100);
150            scoreEl.textContent = `${checked} / ${total} (${percent}%)`;
151            if (barEl) barEl.style.width = `${percent}%`;
152
153            const lang = getLang();
154
155            if (statusEl) {
156                if (percent === 100) {
157                    statusEl.className = 'alert alert-success mt-3 mb-0';
158                    if (lang === 'en') {
159                        statusEl.innerHTML = '<i class="fa-solid fa-circle-check me-2"></i><strong>HIGH CONFLUENCE (100%)</strong> — All theoretical criteria configured are satisfied.';
160                    } else if (lang === 'es') {
161                        statusEl.innerHTML = '<i class="fa-solid fa-circle-check me-2"></i><strong>ALTA CONFLUENCIA (100%)</strong> — Todos los criterios teóricos configurados se han cumplido.';
162                    } else {
163                        statusEl.innerHTML = '<i class="fa-solid fa-circle-check me-2"></i><strong>CONFLUENCE ÉLEVÉE (100%)</strong> — Tous les critères théoriques configurés sont validés.';
164                    }
165                } else if (percent >= 66) {
166                    statusEl.className = 'alert alert-warning mt-3 mb-0';
167                    if (lang === 'en') {
168                        statusEl.innerHTML = '<i class="fa-solid fa-triangle-exclamation me-2"></i><strong>MODERATE CONFLUENCE (' + percent + '%)</strong> — The majority of theoretical criteria are satisfied.';
169                    } else if (lang === 'es') {
170                        statusEl.innerHTML = '<i class="fa-solid fa-triangle-exclamation me-2"></i><strong>CONFLUENCIA MODERADA (' + percent + '%)</strong> — La mayoría de los criterios teóricos se han cumplido.';
171                    } else {
172                        statusEl.innerHTML = '<i class="fa-solid fa-triangle-exclamation me-2"></i><strong>CONFLUENCE MODÉRÉE (' + percent + '%)</strong> — La majorité des critères théoriques sont validés.';
173                    }
174                } else {
175                    statusEl.className = 'alert alert-danger mt-3 mb-0';
176                    if (lang === 'en') {
177                        statusEl.innerHTML = '<i class="fa-solid fa-circle-info me-2"></i><strong>LOW CONFLUENCE (' + percent + '%)</strong> — Low confluence level based on this evaluation model.';
178                    } else if (lang === 'es') {
179                        statusEl.innerHTML = '<i class="fa-solid fa-circle-info me-2"></i><strong>CONFLUENCIA BAJA (' + percent + '%)</strong> — Bajo nivel de confluencia según este modelo de evaluación.';
180                    } else {
181                        statusEl.innerHTML = '<i class="fa-solid fa-circle-info me-2"></i><strong>CONFLUENCE FAIBLE (' + percent + '%)</strong> — Faible niveau de confluence selon ce modèle d\'évaluation.';
182                    }
183                }
184            }
185        }
186
187        checkboxes.forEach(chk => chk.addEventListener('change', updateChecklistScore));
188        updateChecklistScore();
189        return updateChecklistScore;
190    }
191
192    // ─── 3. JOURNAL DE TRADING & ANALYTICS ──────────────────────────────────
193    const STORAGE_KEY = 'cq_trading_journal_v1';
194
195    function getJournalEntries() {
196        try {
197            const data = localStorage.getItem(STORAGE_KEY);
198            return data ? JSON.parse(data) : [];
199        } catch (e) {
200            console.error('Error reading trading journal from localStorage', e);
201            return [];
202        }
203    }
204
205    function saveJournalEntries(entries) {
206        try {
207            localStorage.setItem(STORAGE_KEY, JSON.stringify(entries));
208        } catch (e) {
209            console.error('Error saving trading journal to localStorage', e);
210        }
211    }
212
213    function renderJournal() {
214        const tbody = document.getElementById('cq-journal-tbody');
215        const entries = getJournalEntries();
216
217        if (!tbody) return;
218
219        const lang = getLang();
220        const emptyMsg = {
221            fr: 'Aucun trade enregistré pour le moment. Remplissez le formulaire ci-dessus pour suivre vos performances !',
222            en: 'No trades recorded yet. Fill out the form above to track your performance!',
223            es: '¡No hay operaciones registradas aún. Complete el formulario arriba para seguir su rendimiento!'
224        }[lang] || 'Aucun trade enregistré pour le moment. Remplissez le formulaire ci-dessus pour suivre vos performances !';
225
226        if (entries.length === 0) {
227            tbody.innerHTML = `
228                <tr>
229                    <td colspan="7" class="text-center py-4" style="color: #e2e8f0 !important; font-size: 14px; font-weight: 500;">
230                        <i class="fa-solid fa-journal-whills fa-2x mb-2 d-block" style="color: #00e5ff !important;"></i>
231                        ${emptyMsg}
232                    </td>
233                </tr>`;
234            updateJournalStats(entries);
235            return;
236        }
237
238        tbody.innerHTML = entries.map((entry, index) => {
239            const isWin = entry.pnl >= 0;
240            const pnlFormatted = `${isWin ? '+' : ''}$${parseFloat(entry.pnl).toLocaleString('en-US', { minimumFractionDigits: 2 })}`;
241            const dirBadge = entry.direction === 'LONG' 
242                ? '<span class="badge bg-success">LONG</span>' 
243                : '<span class="badge bg-danger">SHORT</span>';
244
245            return `
246                <tr>
247                    <td class="text-nowrap" style="color: #a0aec0;">${entry.date || '-'}</td>
248                    <td class="font-weight-bold text-white">${entry.pair}</td>
249                    <td>${dirBadge}</td>
250                    <td>$${parseFloat(entry.entryPrice).toLocaleString()}</td>
251                    <td>$${parseFloat(entry.exitPrice).toLocaleString()}</td>
252                    <td class="${isWin ? 'text-success font-weight-bold' : 'text-danger font-weight-bold'}">${pnlFormatted}</td>
253                    <td>
254                        <button class="btn btn-sm btn-outline-danger py-0 px-2 cq-del-trade-btn" data-index="${index}">
255                            <i class="fa-solid fa-trash-can"></i>
256                        </button>
257                    </td>
258                </tr>`;
259        }).join('');
260
261        // Attach delete listeners
262        document.querySelectorAll('.cq-del-trade-btn').forEach(btn => {
263            btn.addEventListener('click', function () {
264                const idx = parseInt(this.getAttribute('data-index'), 10);
265                deleteTrade(idx);
266            });
267        });
268
269        updateJournalStats(entries);
270    }
271
272    function updateJournalStats(entries) {
273        const totalTradesEl = document.getElementById('cq-j-total-trades');
274        const winRateEl = document.getElementById('cq-j-win-rate');
275        const totalPnlEl = document.getElementById('cq-j-total-pnl');
276
277        if (!totalTradesEl) return;
278
279        const total = entries.length;
280        if (total === 0) {
281            totalTradesEl.textContent = '0';
282            winRateEl.textContent = '0%';
283            totalPnlEl.textContent = '$0.00';
284            totalPnlEl.className = 'h4 mb-0 text-white';
285            return;
286        }
287
288        const wins = entries.filter(e => parseFloat(e.pnl) >= 0).length;
289        const winRate = Math.round((wins / total) * 100);
290        const totalPnl = entries.reduce((acc, curr) => acc + parseFloat(curr.pnl || 0), 0);
291
292        totalTradesEl.textContent = total.toString();
293        winRateEl.textContent = `${winRate}%`;
294        totalPnlEl.textContent = `${totalPnl >= 0 ? '+' : ''}$${totalPnl.toLocaleString('en-US', { minimumFractionDigits: 2 })}`;
295        totalPnlEl.className = `h4 mb-0 ${totalPnl >= 0 ? 'text-success' : 'text-danger'}`;
296    }
297
298    function deleteTrade(index) {
299        const entries = getJournalEntries();
300        entries.splice(index, 1);
301        saveJournalEntries(entries);
302        renderJournal();
303    }
304
305    function initJournal() {
306        const form = document.getElementById('cq-journal-form');
307        const clearBtn = document.getElementById('cq-j-clear-btn');
308        const exportBtn = document.getElementById('cq-j-export-btn');
309
310        if (form) {
311            form.addEventListener('submit', function (e) {
312                e.preventDefault();
313                const pair = document.getElementById('cq-j-pair').value.toUpperCase().trim();
314                const direction = document.getElementById('cq-j-dir').value;
315                const entryPrice = parseFloat(document.getElementById('cq-j-entry').value) || 0;
316                const exitPrice = parseFloat(document.getElementById('cq-j-exit').value) || 0;
317                const pnl = parseFloat(document.getElementById('cq-j-pnl').value) || 0;
318                const date = new Date().toISOString().split('T')[0];
319
320                if (!pair || !entryPrice || !exitPrice) return;
321
322                const newTrade = { pair, direction, entryPrice, exitPrice, pnl, date };
323                const entries = getJournalEntries();
324                entries.unshift(newTrade);
325                saveJournalEntries(entries);
326                renderJournal();
327                form.reset();
328            });
329        }
330
331        if (clearBtn) {
332            clearBtn.addEventListener('click', function () {
333                if (confirm('Voulez-vous vraiment réinitialiser l\'ensemble de votre journal de trading ?')) {
334                    saveJournalEntries([]);
335                    renderJournal();
336                }
337            });
338        }
339
340        if (exportBtn) {
341            exportBtn.addEventListener('click', function () {
342                const entries = getJournalEntries();
343                const jsonStr = JSON.stringify(entries, null, 2);
344                const blob = new Blob([jsonStr], { type: 'application/json' });
345                const url = URL.createObjectURL(blob);
346                const a = document.createElement('a');
347                a.href = url;
348                a.download = `CQ_Trading_Journal_${new Date().toISOString().split('T')[0]}.json`;
349                a.click();
350                URL.revokeObjectURL(url);
351            });
352        }
353
354        const importBtn = document.getElementById('cq-j-import-btn');
355        const importFile = document.getElementById('cq-j-import-file');
356
357        if (importBtn && importFile) {
358            importBtn.addEventListener('click', function () {
359                importFile.click();
360            });
361
362            importFile.addEventListener('change', function (e) {
363                const file = e.target.files[0];
364                if (!file) return;
365
366                const reader = new FileReader();
367                reader.onload = function (event) {
368                    try {
369                        const importedData = JSON.parse(event.target.result);
370                        if (Array.isArray(importedData)) {
371                            const existing = getJournalEntries();
372                            const merged = [...importedData, ...existing];
373                            saveJournalEntries(merged);
374                            renderJournal();
375                            alert(`${importedData.length} trade(s) importé(s) avec succès !`);
376                        } else {
377                            alert("Format de fichier JSON invalide.");
378                        }
379                    } catch (err) {
380                        alert("Erreur lors de la lecture du fichier JSON.");
381                    }
382                };
383                reader.readAsText(file);
384                importFile.value = '';
385            });
386        }
387
388        renderJournal();
389        return renderJournal;
390    }
391
392    // ─── INITIALISATION GLOBALE AU CHARGEMENT DU DOM ─────────────────────────
393    
394    // ─── 4. UNIVERSAL INFOBULLE / TOOLTIP ENGINE ──────────────────────────────
395    function initUniversalTooltips() {
396        let tooltipBox = document.getElementById('cq-custom-tooltip');
397        if (!tooltipBox) {
398            tooltipBox = document.createElement('div');
399            tooltipBox.id = 'cq-custom-tooltip';
400            tooltipBox.style.cssText = 'position: fixed; display: none; background: #0d1926; color: #e2e8f0; border: 1px solid #00e5ff; box-shadow: 0 8px 32px rgba(0, 229, 255, 0.35); border-radius: 8px; padding: 10px 14px; font-size: 12.5px; font-weight: 500; line-height: 1.45; max-width: 290px; z-index: 99999999; pointer-events: none; backdrop-filter: blur(8px); font-family: system-ui, -apple-system, sans-serif; opacity: 0; transition: opacity 0.15s ease-in-out;';
401            document.body.appendChild(tooltipBox);
402        }
403
404        let activeIcon = null;
405
406        function showTooltip(icon) {
407            let text = icon.getAttribute('title') || icon.getAttribute('data-bs-title') || icon.getAttribute('data-original-title') || icon.getAttribute('data-stored-title');
408            if (!text) return;
409
410            if (icon.hasAttribute('title')) {
411                icon.setAttribute('data-stored-title', icon.getAttribute('title'));
412                icon.removeAttribute('title');
413            }
414
415            tooltipBox.innerHTML = '<div style="color: #00e5ff; font-weight: 700; font-size: 11px; text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 4px; display: flex; align-items: center; gap: 5px;"><i class="fa-solid fa-circle-info"></i> INFO / CONFLUENCE</div><div>
415' + text + '</div>';
416
417            const rect = icon.getBoundingClientRect();
418            tooltipBox.style.display = 'block';
419
420            let top = rect.top - tooltipBox.offsetHeight - 8;
421            let left = rect.left + (rect.width / 2) - (tooltipBox.offsetWidth / 2);
422
423            if (top < 10) {
424                top = rect.bottom + 8;
425            }
426            if (left < 10) left = 10;
427            if (left + tooltipBox.offsetWidth > window.innerWidth - 10) {
428                left = window.innerWidth - tooltipBox.offsetWidth - 10;
429            }
430
431            tooltipBox.style.top = top + 'px';
432            tooltipBox.style.left = left + 'px';
433            tooltipBox.style.opacity = '1';
434            activeIcon = icon;
435        }
436
437        function hideTooltip() {
438            if (!tooltipBox) return;
439            tooltipBox.style.opacity = '0';
440            setTimeout(function() {
441                if (tooltipBox.style.opacity === '0') {
442                    tooltipBox.style.display = 'none';
443                }
444            }, 150);
445
446            if (activeIcon && activeIcon.hasAttribute('data-stored-title')) {
447                activeIcon.setAttribute('title', activeIcon.getAttribute('data-stored-title'));
448                activeIcon.removeAttribute('data-stored-title');
449                activeIcon = null;
450            }
451        }
452
453        // Tooltip click handler (non-interfering bubble phase)
454        document.addEventListener('click', function(e) {
455            const icon = e.target.closest('[data-bs-toggle="tooltip"], .fa-circle-info');
456            if (icon) {
457                // If icon is purely standalone (not inside button/link with another action)
458                var interactiveParent = icon.closest('button, a, .btn, .indicator-card, .chart-tf-btn');
459                if (!interactiveParent) {
460                    e.preventDefault();
461                }
462                if (activeIcon === icon && tooltipBox.style.display === 'block' && tooltipBox.style.opacity === '1') {
463                    hideTooltip();
464                } else {
465                    showTooltip(icon);
466                }
467            } else {
468                hideTooltip();
469            }
470        }, false);
471
472        document.addEventListener('mouseover', function(e) {
473            const icon = e.target.closest('[data-bs-toggle="tooltip"], .fa-circle-info');
474            if (icon) {
475                showTooltip(icon);
476            }
477        });
478
479        document.addEventListener('mouseout', function(e) {
480            const icon = e.target.closest('[data-bs-toggle="tooltip"], .fa-circle-info');
481            if (icon) {
482                hideTooltip();
483            }
484        });
485
486        window.addEventListener('scroll', hideTooltip, { passive: true });
487        document.addEventListener('keydown', function(e) {
488            if (e.key === 'Escape') hideTooltip();
489        });
490    }
491
492    document.addEventListener('DOMContentLoaded', function () {
493        initUniversalTooltips();
494        const recalc = initPositionCalculator();
495        const recheck = initPreTradeChecklist();
496        const rejournal = initJournal();
497
498        refreshAllTools = function() {
499            if (typeof recalc === 'function') recalc();
500            if (typeof recheck === 'function') recheck();
501            if (typeof rejournal === 'function') rejournal();
502        };
503
504        // Hook window.setLanguage for live i18n dynamic refreshes
505        if (window.setLanguage && !window.setLanguage._cqToolsHooked) {
506            const origSetLang = window.setLanguage;
507            window.setLanguage = function(lang) {
508                origSetLang(lang);
509                setTimeout(refreshAllTools, 50);
510            };
511            window.setLanguage._cqToolsHooked = true;
512        }
513
514        // Listen for language selector button clicks as backup
515        document.querySelectorAll('.lang-btn, [data-lang]').forEach(btn => {
516            btn.addEventListener('click', function() {
517                setTimeout(refreshAllTools, 100);
518            });
519        });
520
521        // Initialisation automatique des Tooltips Bootstrap
522        if (typeof bootstrap !== 'undefined' && bootstrap.Tooltip) {
523            var tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'));
524            tooltipTriggerList.map(function (tooltipTriggerEl) {
525                return new bootstrap.Tooltip(tooltipTriggerEl);
526            });
527        } else if (typeof $ !== 'undefined' && $.fn.tooltip) {
528            $('[data-bs-toggle="tooltip"]').tooltip();
529        }
530    });
531
532})();
533
534
535// Global Tool Modal Handlers (openToolModal / closeToolModal)
536window.openToolModal = function(modalId) {
537    if (!modalId) return;
538    try {
539        var modalEl = document.getElementById(modalId);
540        if (modalEl) {
541            modalEl.style.display = 'flex';
542            modalEl.classList.add('show', 'active');
543            document.body.style.overflow = 'hidden';
544
545            var lang = localStorage.getItem('selected_language') || document.documentElement.lang || 'fr';
546
547            if (modalId === 'modalPodcast') {
548                if (window.updatePodcastModalLanguage) {
549                    try { window.updatePodcastModalLanguage(lang); } catch(e) { console.error(e); }
550                }
551            }
552            if (modalId === 'modalGlossaire') {
553                if (window.initCQGlossary) {
554                    try { window.initCQGlossary('glossary-app-container'); } catch(e) { console.error(e); }
555                }
556            }
557        }
558    } catch(err) {
559        console.error('Error in openToolModal:', err);
560    }
561};
562
563window.closeToolModal = function(modalId) {
564    try {
565        var modalEl = (typeof modalId === 'string') ? document.getElementById(modalId) : modalId;
566        if (modalEl) {
567            modalEl.style.display = 'none';
568            modalEl.classList.remove('show', 'active');
569            document.body.style.overflow = '';
570            
571            // Stop any playing iframe video inside the modal
572            var iframe = modalEl.querySelector('iframe');
573            if (iframe) {
574                iframe.src = 'about:blank';
575            }
576            var cover = modalEl.querySelector('#podcast-cover-poster');
577            if (cover) {
578                cover.style.display = 'flex';
579            }
580        }
581    } catch(err) {
582        console.error('Error in closeToolModal:', err);
583    }
584};

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.