PageSourceSearch

https://webpresse.ca/js/components/nmc-reels-player.js?v=20260921d

js webpresse.ca collected 2026-10-02 21:11:39 UTC 84,984 bytes, 1,754 lines download raw bytes

1/**
2 * nmc-reels-player.js — NMC Reels Player (Web Component Shadow DOM)
3 * VAP 2.8 — Mobile fullscreen scroll-snap reels · autoplay · share · video responses
4 *
5 * Usage:
6 *   <nmc-reels-player id="nmcReelsPlayer"></nmc-reels-player>
7 *
8 *   // Depuis renderPage (après initReelsMode exposé via window) :
9 *   if (filteredMedia.length > 0) initReelsMode(filteredMedia);
10 *
11 * Événements émis (bubbles + composed) :
12 *   nmc-reels-enter → { mediaCount } — mode Reels activé
13 *   nmc-reels-exit  → {}            — mode Reels quitté
14 *   nmc-track       → { type: 'reel_view'|'reel_share', data: { mediaId } }
15 *
16 * API publique (élément) :
17 *   player.initReelsMode(mediaList)   — init + autoenter mobile
18 *   player.enterReelsMode()           — passe en plein écran
19 *   player.exitReelsMode()            — quitte le plein écran
20 *   player.renderReels()              — re-render les items
21 *   player.reelsWrapper               — getter → #reelsWrapper (shadow)
22 *   player.reelsContainer             — getter → #reelsContainer (shadow)
23 *
24 * Globals exposés (backward compat onclick handlers + scripts externes) :
25 *   window.initReelsMode, enterReelsMode, exitReelsMode, renderReels
26 *   window.toggleReelVideo, autoplayVisibleReelVideo, updateGalleryCounter
27 *   window.onReelsScroll, copyReelLink, navigateToTag, filterByPlatform
28 *   window.scrollToMedia, showResponsesFeed, exitResponsesFeed, goToParentVideo
29 *   window.openSubmitModal, closeSubmitModal, handleVideoSelect, removeSelectedVideo
30 *   window.openResponseTerms, closeResponseTerms, acceptResponseTerms
31 *   window.submitResponse, shareReel
32 *
33 * Dépendances :
34 *   window.escapeHtml, getPlatformEmoji, isMobile  (nmc-page-utils.js)
35 *   window.trackAd, currentAds, playWithPreroll, stopAllPrerolls,
36 *          refreshPrerollListeners                  (nmc-ads-engine.js)
37 *   window.enqueueEnrichment                        (nmc-ai-enrichment.js)
38 *   window.username, API_URL                        (page.html inline script)
39 *
40 * CSS global conservé dans page.css (affecte le light DOM) :
41 *   .reels-mode .container, .reels-mode .site-header, etc.
42 */
43
44(function () {
45  'use strict';
46
47  // ============================================================
48  // CSS Shadow DOM — tout le CSS reels/responses/modals
49  // ============================================================
50  const REELS_CSS = /* css */`
51    :host {
52      display: block;
53      pointer-events: none;
54    }
55
56    /* ── Reels container (caché par défaut, affiché en reels-mode) ── */
57    .reels-container {
58      display: none;
59      pointer-events: auto;
60    }
61
62    :host-context(.reels-mode) .reels-container {
63      display: block;
64      position: fixed;
65      top: 0;
66      left: 0;
67      right: 0;
68      bottom: 0;
69      overflow: hidden;
70      background: #000;
71      z-index: 9999;
72    }
73
74    /* ── Responses header ─────────────────────────────────────── */
75    .responses-header {
76      display: none;
77      position: fixed;
78      top: 0;
79      left: 0;
80      right: 0;
81      z-index: 110;
82      padding: 15px 20px;
83      padding-top: max(15px, env(safe-area-inset-top));
84      background: rgba(0,0,0,0.8);
85      backdrop-filter: blur(10px);
86      align-items: center;
87      justify-content: space-between;
88      pointer-events: auto;
89    }
90    .responses-header.active {
91      display: flex;
92    }
93    .responses-back-btn {
94      display: flex;
95      align-items: center;
96      gap: 8px;
97      color: white;
98      background: rgba(255,255,255,0.15);
99      border: none;
100      border-radius: 20px;
101      padding: 8px 16px;
102      font-size: 0.9rem;
103      font-weight: 600;
104      cursor: pointer;
105      font-family: inherit;
106    }
107
108    /* ── Reels wrapper (scroll-snap vertical) ─────────────────── */
109    .reels-wrapper {
110      height: 100%;
111      overflow-y: scroll;
112      scroll-snap-type: y mandatory;
113      -webkit-overflow-scrolling: touch;
114      scrollbar-width: none;
115      -ms-overflow-style: none;
116    }
117    .reels-wrapper::-webkit-scrollbar {
118      display: none;
119    }
120
121    /* ── Reel item ─────────────────────────────────────────────── */
122    .reel-item {
123      height: 100vh;
124      height: 100dvh;
125      width: 100%;
126      scroll-snap-align: start;
127      scroll-snap-stop: always;
128      position: relative;
129      display: flex;
130      align-items: center;
131      justify-content: center;
132      background: #000;
133    }
134    .reel-item .reel-media {
135      position: absolute;
136      top: 0; left: 0; right: 0; bottom: 0;
137      display: flex;
138      align-items: center;
139      justify-content: center;
140      z-index: 1;
141      cursor: pointer;
142    }
143    .reel-item .reel-media.reel-upload {
144      display: flex;
145      align-items: center;
146      justify-content: center;
147      z-index: 5;
148    }
149    .reel-item .reel-media.reel-upload video {
150      width: 100%; height: 100%;
151      max-width: 100vw;
152      max-height: 100vh; max-height: 100dvh;
153      object-fit: contain;
154      position: relative; z-index: 5;
155    }
156    .reel-play-indicator {
157      position: absolute;
158      top: 50%; left: 50%;
159      transform: translate(-50%, -50%);
160      width: 80px; height: 80px;
161      background: rgba(0,0,0,0.6);
162      border-radius: 50%;
163      display: flex; align-items: center; justify-content: center;
164      font-size: 2rem; color: white;
165      opacity: 0; transition: opacity 0.3s;
166      pointer-events: none; z-index: 20;
167    }
168    .reel-play-indicator.visible { opacity: 1; }
169    .reel-item video { pointer-events: none; }
170    .reel-item iframe {
171      width: 100%; height: 100%;
172      max-height: 100vh; max-height: 100dvh;
173      border: none;
174    }
175    .reel-item.vertical iframe { width: 100%; height: 100%; object-fit: contain; }
176    .reel-item.horizontal iframe {
177      width: 100%; height: auto;
178      aspect-ratio: 16/9; max-height: 50vh;
179    }
180
181    /* ── Reel info overlay (bas) ───────────────────────────────── */
182    .reel-info {
183      position: absolute;
184      bottom: 0; left: 0; right: 0;
185      padding: 80px 15px 30px;
186      background: linear-gradient(to top, rgba(0,0,0,0.9) 0%, rgba(0,0,0,0.6) 50%, transparent 100%);
187      color: white;
188      z-index: 10;
189      pointer-events: none;
190    }
191    .reel-info * { pointer-events: auto; }
192    .reel-title {
193      font-size: 1rem; font-weight: 700; margin-bottom: 8px; line-height: 1.3;
194      display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
195    }
196    .reel-description {
197      font-size: 0.85rem; color: rgba(255,255,255,0.8); margin-bottom: 10px;
198      display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
199    }
200    .reel-tags { display: flex; flex-wrap: wrap; gap: 6px; }
201    .reel-tag {
202      padding: 4px 10px;
203      background: rgba(255,255,255,0.2);
204      border-radius: 12px; font-size: 0.75rem; color: white; text-decoration: none;
205    }
206    .reel-platform {
207      display: inline-block; padding: 6px 14px;
208      background: rgba(255,255,255,0.2); border-radius: 20px;
209      color: white; font-size: 0.8rem; font-weight: 600; margin-bottom: 10px;
210      text-decoration: none; cursor: pointer;
211    }
212    .reel-platform:hover { background: rgba(255,255,255,0.3); }
213    .reel-platform-badge {
214      display: inline-block;
215      background: rgba(255,255,255,0.15); padding: 6px 14px; border-radius: 20px;
216      font-size: 0.8rem; color: white; text-decoration: none; margin-bottom: 10px;
217      backdrop-filter: blur(5px); cursor: pointer;
218    }
219    .reel-platform-badge:hover { background: rgba(255,255,255,0.25); }
220
221    /* ── Reels header (logo + créateur + bouton fermer) ─────────── */
222    .reels-header {
223      position: fixed;
224      top: 0; left: 0; right: 0;
225      padding: 15px;
226      display: flex; justify-content: space-between; align-items: center;
227      z-index: 100;
228      background: linear-gradient(to bottom, rgba(0,0,0,0.6) 0%, transparent 100%);
229      pointer-events: none;
230    }
231    .reels-header > * { pointer-events: auto; }
232    .reels-back {
233      width: 40px; height: 40px;
234      background: rgba(255,255,255,0.2);
235      border: none; border-radius: 50%;
236      color: white; font-size: 1.3rem; cursor: pointer;
237      backdrop-filter: blur(10px);
238      display: flex; align-items: center; justify-content: center;
239    }
240    .reels-logo {
241      width: 40px; height: 40px;
242      display: flex; align-items: center; justify-content: center;
243    }
244    .reels-logo img { width: 35px; height: 35px; border-radius: 8px; object-fit: cover; }
245    .reels-creator {
246      display: flex; align-items: center; gap: 10px;
247      color: white; text-decoration: none;
248    }
249    .reels-creator img { width: 35px; height: 35px; border-radius: 50%; object-fit: cover; }
250    .reels-creator span { font-weight: 600; font-size: 0.9rem; }
251
252    /* ── Bouton share ────────────────────────────────────────────── */
253    .reels-share-btn {
254      position: fixed;
255      bottom: 30px; right: 20px;
256      width: 50px; height: 50px;
257      background: rgba(255,255,255,0.2);
258      border-radius: 50%;
259      display: flex; align-items: center; justify-content: center;
260      cursor: pointer; z-index: 100;
261      backdrop-filter: blur(10px);
262      border: 2px solid rgba(255,255,255,0.3);
263      transition: all 0.3s;
264      pointer-events: auto;
265    }
266    .reels-share-btn:hover { background: rgba(255,255,255,0.3); transform: scale(1.1); }
267    .reels-share-btn.copied { background: #10B981; border-color: #10B981; }
268    .reels-share-btn img { width: 30px; height: 30px; border-radius: 50%; object-fit: cover; }
269
270    /* ── Boutons réponses fixes ──────────────────────────────────── */
271    .fixed-response-actions {
272      position: fixed;
273      bottom: 140px; right: 15px;
274      z-index: 150;
275      display: flex; flex-direction: column; gap: 6px;
276      pointer-events: auto;
277    }
278    .fixed-action-btn {
279      touch-action: manipulation;
280      -webkit-tap-highlight-color: transparent;
281      user-select: none; -webkit-user-select: none;
282      display: flex; flex-direction: row; align-items: center; gap: 4px;
283      background: rgba(255,255,255,0.15);
284      backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px);
285      border: none; border-radius: 15px; padding: 6px 12px;
286      cursor: pointer; color: white; font-size: 0.75rem;
287    }
288    .fixed-action-btn:active {
289      transform: scale(0.95);
290      background: rgba(139,92,246,0.8);
291    }
292    .fixed-action-btn .action-icon { font-size: 0.9rem; }
293    .fixed-action-btn .action-label { font-size: 0.7rem; font-weight: 600; }
294
295    /* ── Compteur reel ───────────────────────────────────────────── */
296    .reel-counter {
297      position: fixed;
298      top: 70px; left: 50%;
299      transform: translateX(-50%);
300      background: rgba(0,0,0,0.5);
301      padding: 5px 12px; border-radius: 15px;
302      font-size: 0.8rem; color: white;
303      z-index: 100; backdrop-filter: blur(10px);
304      pointer-events: none;
305    }
306
307    /* ── Pub in-feed ─────────────────────────────────────────────── */
308    .reel-item.reel-ad {
309      background: #000;
310      display: flex; flex-direction: column; align-items: center; justify-content: center;
311      position: relative;
312    }
313    .reel-item.reel-ad .reel-info {
314      position: absolute; bottom: 20px; left: 0; right: 0;
315      text-align: center; padding: 0; background: none;
316    }
317    .reel-ad-content {
318      position: absolute; top: 0; left: 0; right: 0; bottom: 0;
319      display: flex; align-items: center; justify-content: center;
320      width: 100%; height: 100%; padding: 20px;
321    }
322    .reel-ad-content a { display: block; width: 100%; max-width: 100%; }
323    .reel-ad-content img, .reel-ad-content video {
324      width: 100%; height: 100%; object-fit: cover; border-radius: 0;
325    }
326
327    /* ── Overlay pub dans reel ───────────────────────────────────── */
328    .reel-overlay-ad {
329      position: absolute; bottom: 200px; right: 10px; z-index: 50; display: block;
330    }
331    .reel-overlay-ad.top-left    { top: 10px; left: 10px; right: auto; bottom: auto; }
332    .reel-overlay-ad.top-right   { top: 10px; right: 10px; left: auto; bottom: auto; }
333    .reel-overlay-ad.bottom-left { bottom: 200px; left: 10px; top: auto; right: auto; }
334    .reel-overlay-ad.bottom-right{ bottom: 200px; right: 10px; top: auto; left: auto; }
335    .reel-overlay-ad img {
336      max-width: 100px; border-radius: 8px; box-shadow: 0 4px 15px rgba(0,0,0,0.4);
337    }
338    .reel-overlay-close {
339      position: absolute; top: -8px; right: -8px;
340      width: 22px; height: 22px;
341      background: rgba(0,0,0,0.7); border: none; border-radius: 50%;
342      color: white; font-size: 0.7rem; cursor: pointer;
343      display: flex; align-items: center; justify-content: center;
344    }
345
346    /* ── Placeholder reel ────────────────────────────────────────── */
347    .reel-placeholder {
348      width: 100%; height: 100%;
349      display: flex; flex-direction: column; align-items: center; justify-content: center;
350      background: linear-gradient(135deg, var(--bg-elevated, #252542), var(--bg-card, #1A1A2E));
351      cursor: pointer;
352    }
353    .reel-placeholder .play-icon {
354      width: 80px; height: 80px;
355      background: rgba(255,255,255,0.9); border-radius: 50%;
356      display: flex; align-items: center; justify-content: center;
357      margin-bottom: 15px;
358    }
359    .reel-placeholder .play-icon svg { width: 35px; height: 35px; fill: var(--primary, #8B5CF6); margin-left: 5px; }
360
361    /* ── Side actions (désactivé, boutons fixes utilisés) ─────────── */
362    .reel-actions {
363      position: absolute; right: 10px; bottom: 180px;
364      z-index: 100; display: flex; flex-direction: column; gap: 18px; align-items: center;
365      pointer-events: auto;
366    }
367    .reel-action-btn {
368      display: flex; flex-direction: column; align-items: center; gap: 4px;
369      background: none; border: none; cursor: pointer; color: white;
370      pointer-events: auto; text-decoration: none; touch-action: manipulation;
371      padding: 10px; margin: -10px; position: relative; z-index: 101;
372    }
373    .reel-action-btn .action-icon {
374      width: 46px; height: 46px;
375      background: rgba(255,255,255,0.15); backdrop-filter: blur(10px);
376      border-radius: 50%; display: flex; align-items: center; justify-content: center;
377      font-size: 1.3rem; transition: all 0.3s;
378    }
379    .reel-action-btn:hover .action-icon, .reel-action-btn:active .action-icon {
380      background: rgba(255,255,255,0.25); transform: scale(1.1);
381    }
382    .reel-action-btn .action-count { font-size: 0.7rem; font-weight: 600; }
383    @keyframes pulse-responses {
384      0%, 100% { box-shadow: 0 0 0 0 rgba(139,92,246,0.5); }
385      50%       { box-shadow: 0 0 0 8px rgba(139,92,246,0); }
386    }
387
388    /* ── Response badge sur vidéo ────────────────────────────────── */
389    .response-badge {
390      display: flex; align-items: center; gap: 6px;
391      background: rgba(139,92,246,0.3); padding: 6px 12px; border-radius: 15px;
392      font-size: 0.8rem; margin-bottom: 10px; width: fit-content; cursor: pointer;
393    }
394    .response-badge:hover { background: rgba(139,92,246,0.5); }
395
396    /* ── Articles en mode Reels ──────────────────────────────────── */
397    .reel-article {
398      display: flex; flex-direction: column; height: 100%;
399      background: linear-gradient(180deg, #1A1A1A 0%, #252525 100%);
400      position: relative; overflow: hidden;
401    }
402    .reel-article-scroll { flex: 1; padding: 60px 20px 20px; overflow-y: auto; }
403    .reel-article-footer {
404      position: absolute; bottom: 0; left: 0; right: 0;
405      padding: 60px 20px 25px;
406      background: linear-gradient(to top, rgba(26,26,26,0.95) 0%, rgba(26,26,26,0.7) 50%, transparent 100%);
407      z-index: 5;
408    }
409    .reel-article-thumbnail { width: 100%; max-height: 300px; object-fit: cover; border-radius: 12px; margin-bottom: 20px; }
410    .reel-article-title { font-size: 1.8rem; font-weight: 700; color: var(--primary-light, #A78BFA); margin-bottom: 20px; line-height: 1.2; word-wrap: break-word; }
411    .reel-article-content { color: white; font-size: 1.1rem; line-height: 1.8; word-wrap: break-word; overflow-wrap: break-word; padding-bottom: 100px; }
412    .reel-article-content h2 { font-size: 1.4rem; color: var(--primary-light, #A78BFA); margin: 20px 0 10px; }
413    .reel-article-content h3 { font-size: 1.2rem; color: #ccc; margin: 15px 0 8px; }
414    .reel-article-content p { margin-bottom: 15px; word-wrap: break-word; }
415    .reel-article-content ul, .reel-article-content ol { margin: 15px 0; padding-left: 25px; }
416    .reel-article-content li { margin-bottom: 8px; }
417    .reel-article-content a { color: var(--primary-light, #A78BFA); text-decoration: underline; }
418
419    /* ── Photos en mode Reels ────────────────────────────────────── */
420    .reel-photo {
421      display: flex; flex-direction: column; height: 100%; max-height: 100vh;
422      background: #000; position: relative; overflow: hidden; align-items: center; justify-content: center;
423    }
424    .reel-photo-img { max-width: 100%; max-height: 70vh; object-fit: contain; padding: 10px; box-sizing: border-box; }
425    .reel-photo-info {
426      position: absolute; bottom: 0; left: 0; right: 0;
427      padding: 60px 15px 25px;
428      background: linear-gradient(to top, rgba(0,0,0,0.9) 0%, rgba(0,0,0,0.6) 50%, transparent 100%);
429      color: white; z-index: 5;
430    }
431
432    /* ── Galeries en mode Reels ──────────────────────────────────── */
433    .reel-gallery {
434      display: flex; flex-direction: column; height: 100%; max-height: 100vh;
435      background: #000; position: relative; overflow: hidden;
436    }
437    .reel-gallery-header {
438      position: absolute; top: 0; left: 0; right: 0;
439      display: flex; align-items: center; gap: 10px; padding: 60px 15px 15px;
440      background: linear-gradient(to bottom, rgba(0,0,0,0.8) 0%, transparent 100%); z-index: 5;
441    }
442    .reel-gallery-counter {
443      margin-left: auto; background: rgba(255,255,255,0.2);
444      padding: 4px 10px; border-radius: 12px; font-size: 0.8rem; color: white;
445    }
446    .reel-gallery-slides {
447      flex: 1; display: flex; overflow-x: auto;
448      scroll-snap-type: x mandatory; scrollbar-width: none; -webkit-overflow-scrolling: touch;
449    }
450    .reel-gallery-slides::-webkit-scrollbar { display: none; }
451    .reel-gallery-slide {
452      flex: 0 0 100%; scroll-snap-align: start;
453      display: flex; align-items: center; justify-content: center;
454      padding: 60px 10px 100px; box-sizing: border-box;
455    }
456    .reel-gallery-slide img { max-width: 100%; max-height: calc(100vh - 180px); object-fit: contain; }
457    .reel-gallery-info {
458      position: absolute; bottom: 0; left: 0; right: 0;
459      padding: 80px 15px 30px;
460      background: linear-gradient(to top, rgba(0,0,0,0.9) 0%, rgba(0,0,0,0.6) 50%, transparent 100%);
461      color: white; z-index: 5;
462    }
463
464    /* ── Submit Response Modal ───────────────────────────────────── */
465    .submit-modal-overlay {
466      position: fixed; top: 0; left: 0; right: 0; bottom: 0;
467      background: rgba(0,0,0,0.95);
468      z-index: 10010;
469      display: none; align-items: flex-end; justify-content: center;
470      pointer-events: auto;
471    }
472    .submit-modal-overlay.active { display: flex; }
473    .submit-modal {
474      width: 100%; max-width: 500px; max-height: 90vh;
475      background: var(--bg-card, #1A1A2E);
476      border-radius: 24px 24px 0 0; overflow: hidden;
477      transform: translateY(100%); transition: transform 0.3s ease;
478    }
479    .submit-modal-overlay.active .submit-modal { transform: translateY(0); }
480    .submit-modal-header {
481      padding: 20px; border-bottom: 1px solid var(--border, rgba(139,92,246,0.2));
482      display: flex; justify-content: space-between; align-items: center;
483    }
484    .submit-modal-header h3 { font-size: 1.1rem; margin: 0; color: white; }
485    .submit-modal-close {
486      background: none; border: none; color: var(--text-dim, #A1A1AA); font-size: 1.5rem; cursor: pointer; padding: 5px;
487    }
488    .submit-modal-body { padding: 20px; overflow-y: auto; max-height: calc(90vh - 80px); }
489    .submit-parent-info {
490      background: rgba(139,92,246,0.15); border: 1px solid rgba(139,92,246,0.3);
491      border-radius: 12px; padding: 12px; margin-bottom: 20px;
492      display: flex; gap: 12px; align-items: center;
493    }
494    .submit-parent-info .icon { font-size: 1.5rem; }
495    .submit-parent-info .text { flex: 1; }
496    .submit-parent-info .label { font-size: 0.75rem; color: var(--text-dim, #A1A1AA); }
497    .submit-parent-info .title { font-weight: 600; font-size: 0.9rem; color: white; }
498    .submit-form-group { margin-bottom: 18px; }
499    .submit-form-group label { display: block; font-weight: 600; margin-bottom: 8px; font-size: 0.9rem; color: white; }
500    .submit-form-group input {
501      width: 100%; padding: 14px;
502      background: var(--bg-elevated, #252542);
503      border: 2px solid var(--border, rgba(139,92,246,0.2));
504      border-radius: 12px; color: white; font-family: inherit; font-size: 1rem;
505      box-sizing: border-box;
506    }
507    .submit-form-group input:focus { outline: none; border-color: var(--primary, #8B5CF6); }
508    .submit-upload-zone {
509      border: 2px dashed var(--border, rgba(139,92,246,0.2));
510      border-radius: 16px; padding: 30px 20px; text-align: center; cursor: pointer; transition: all 0.3s;
511    }
512    .submit-upload-zone:hover { border-color: var(--primary, #8B5CF6); background: rgba(139,92,246,0.1); }
513    .submit-upload-zone.has-file { border-color: #10B981; background: rgba(16,185,129,0.1); }
514    .submit-upload-zone .upload-icon { font-size: 2.5rem; margin-bottom: 10px; }
515    .submit-upload-zone .upload-text { font-weight: 600; color: white; }
516    .submit-upload-zone .upload-hint { font-size: 0.8rem; color: var(--text-dim, #A1A1AA); margin-top: 5px; }
517    .submit-video-preview { display: none; margin-top: 15px; border-radius: 12px; overflow: hidden; background: #000; }
518    .submit-video-preview.active { display: block; }
519    .submit-video-preview video { width: 100%; max-height: 200px; object-fit: contain; }
520    .submit-video-preview .preview-info {
521      padding: 10px; background: var(--bg-elevated, #252542);
522      display: flex; justify-content: space-between; align-items: center;
523    }
524    .submit-video-preview .preview-info span { color: white; font-size: 0.85rem; }
525    .submit-video-preview .remove-btn {
526      background: #EC4899; color: white; border: none;
527      padding: 6px 12px; border-radius: 8px; font-size: 0.8rem; cursor: pointer;
528    }
529    .submit-btn {
530      width: 100%; padding: 16px;
531      background: linear-gradient(135deg, var(--primary, #8B5CF6), var(--secondary, #EC4899));
532      color: white; border: none; border-radius: 14px;
533      font-size: 1rem; font-weight: 700; cursor: pointer; font-family: inherit; margin-top: 10px;
534    }
535    .submit-btn:disabled { opacity: 0.5;
535 cursor: not-allowed; }
536    .submit-progress-bar {
537      display: none; height: 6px;
538      background: var(--bg-elevated, #252542);
539      border-radius: 3px; margin-top: 15px; overflow: hidden;
540    }
541    .submit-progress-bar.active { display: block; }
542    .submit-progress-fill {
543      height: 100%;
544      background: linear-gradient(90deg, var(--primary, #8B5CF6), var(--secondary, #EC4899));
545      width: 0%; transition: width 0.3s;
546    }
547
548    /* ── Terms Modal ─────────────────────────────────────────────── */
549    .resp-terms-overlay {
550      display: none; position: fixed; inset: 0; z-index: 10020;
551      background: rgba(0,0,0,0.85); backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px);
552      align-items: center; justify-content: center; padding: 16px;
553      pointer-events: auto;
554    }
555    .resp-terms-overlay.active { display: flex; }
556    .resp-terms-modal {
557      width: 100%; max-width: 540px; max-height: 85vh; max-height: 85dvh;
558      background: var(--bg-card, #1A1A2E); border-radius: 24px;
559      display: flex; flex-direction: column; overflow: hidden;
560      box-shadow: 0 25px 60px rgba(0,0,0,0.6);
561    }
562    .resp-terms-modal-header {
563      padding: 20px 24px; border-bottom: 1px solid rgba(255,255,255,0.08);
564      display: flex; align-items: center; justify-content: space-between; flex-shrink: 0;
565    }
566    .resp-terms-modal-header h3 { font-size: 1.15rem; font-weight: 600; color: #fff; margin: 0; }
567    .resp-terms-body {
568      flex: 1; overflow-y: auto; padding: 24px; -webkit-overflow-scrolling: touch;
569      overscroll-behavior: contain; font-size: 0.85rem; line-height: 1.6; min-height: 0; color: rgba(255,255,255,0.7);
570    }
571    .resp-terms-body h4 { margin: 18px 0 6px; font-size: 0.95rem; color: #fff; }
572    .resp-terms-body p { color: rgba(255,255,255,0.7); margin: 0 0 10px; }
573    .resp-terms-body ul { color: rgba(255,255,255,0.7); padding-left: 20px; margin: 0 0 10px; }
574    .resp-terms-body li { margin-bottom: 4px; }
575    .resp-terms-footer { padding: 12px 24px; flex-shrink: 0; }
576
577    /* ── Inline style resets ─────────────────────────────────────── */
578    * { box-sizing: border-box; }
579    a { text-decoration: none; }
580    button { font-family: inherit; }
581  `;
582
583  // ============================================================
584  // HTML Shadow DOM
585  // ============================================================
586  const REELS_HTML = /* html */`
587    <!-- Reels Mode Container (mobile) -->
588    <div class="reels-container" id="reelsContainer">
589      <div class="reels-header">
590        <a href="/" class="reels-logo" title="partage.ai">
591          <img src="/share.png" alt="partage.ai">
592        </a>
593        <div class="reels-creator" id="reelsCreator">
594          <img src="/share.png" alt="" id="reelsCreatorAvatar">
595          <span id="reelsCreatorName">Créateur</span>
596        </div>
597        <button class="reels-back" id="reelsBackBtn" title="Fermer">✕</button>
598      </div>
599      <div class="reel-counter" id="reelCounter">1 / 1</div>
600      <div class="reels-wrapper" id="reelsWrapper"></div>
601      <!-- Bouton share en bas à droite -->
602      <div class="reels-share-btn" id="reelsShareBtn">
603        <img src="/share.png" alt="Partager" id="reelsShareImg">
604      </div>
605      <!-- Boutons réponses fixes -->
606      <div class="fixed-response-actions" id="fixedResponseActions" style="display:none;">
607        <button type="button" class="fixed-action-btn" id="fixedResponsesBtn">
608          <span class="action-icon">💬</span>
609          <span class="action-label">Réponses</span>
610        </button>
611        <button type="button" class="fixed-action-btn" id="fixedSubmitBtn">
612          <span class="action-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" width="24" height="24"><rect x="2" y="4" width="15" height="14" rx="2"/><path d="M17 9l5-3v12l-5-3"/></svg></span>
613          <span class="action-label">Répondre</span>
614        </button>
615      </div>
616    </div>
617
618    <!-- Responses Feed Header -->
619    <div class="responses-header" id="responsesHeader">
620      <button class="responses-back-btn" id="responsesBackBtn">← Retour</button>
621      <div style="flex:1;"></div>
622    </div>
623
624    <!-- Submit Response Modal -->
625    <div class="submit-modal-overlay" id="submitModalOverlay">
626      <div class="submit-modal">
627        <div class="submit-modal-header">
628          <h3>📤 Répondre en vidéo</h3>
629          <button class="submit-modal-close" id="submitModalClose">×</button>
630        </div>
631        <div class="submit-modal-body">
632          <div class="submit-parent-info">
633            <div class="icon">🎬</div>
634            <div class="text">
635              <div class="label">Tu réponds à :</div>
636              <div class="title" id="submitParentTitle">-</div>
637            </div>
638          </div>
639          <form id="submitResponseForm">
640            <div class="submit-form-group">
641              <label>📹 Ta vidéo</label>
642              <div class="submit-upload-zone" id="submitUploadZone">
643                <div class="upload-icon">🎬</div>
644                <div class="upload-text">Choisir ou filmer</div>
645                <div class="upload-buttons" style="display:flex;gap:10px;margin-top:10px;">
646                  <button type="button" id="chooseVideoBtn" style="flex:1;padding:12px;border-radius:8px;border:none;background:linear-gradient(135deg,var(--primary,#8B5CF6),var(--secondary,#EC4899));color:white;font-weight:600;cursor:pointer;">📁 Choisir</button>
647                  <button type="button" id="cameraVideoBtn" style="flex:1;padding:12px;border-radius:8px;border:none;background:linear-gradient(135deg,#10B981,#059669);color:white;font-weight:600;cursor:pointer;">📷 Filmer</button>
648                </div>
649                <div class="upload-hint" style="margin-top:10px;">MP4, MOV • Max 100MB</div>
650              </div>
651              <input type="file" id="submitVideoInput" accept="video/*" style="display:none">
652              <input type="file" id="submitCameraInput" accept="video/*" capture="environment" style="display:none">
653              <div class="submit-video-preview" id="submitVideoPreview">
654                <video id="submitPreviewVideo" controls playsinline></video>
655                <div class="preview-info">
656                  <span id="submitFileName">video.mp4</span>
657                  <button type="button" class="remove-btn" id="removeVideoBtn">✕</button>
658                </div>
659              </div>
660            </div>
661            <div class="submit-form-group">
662              <label>👤 Ton pseudo</label>
663              <input type="text" id="submitUserName" placeholder="@ton_nom" required>
664            </div>
665            <div class="submit-form-group">
666              <label>✨ Titre (optionnel)</label>
667              <input type="text" id="submitVideoTitle" placeholder="Ma réponse...">
668            </div>
669            <div class="submit-progress-bar" id="submitProgressBar">
670              <div class="submit-progress-fill" id="submitProgressFill"></div>
671            </div>
672            <button type="submit" class="submit-btn" id="submitBtn">📤 Envoyer ma réponse</button>
673            <p style="text-align:center;font-size:0.75rem;color:var(--text-dim,#A1A1AA);margin-top:10px;">
674              En envoyant, tu acceptes nos <a href="#" id="openTermsLink" style="color:var(--primary,#8B5CF6);">conditions d'utilisation</a>
675            </p>
676          </form>
677        </div>
678      </div>
679    </div>
680
681    <!-- Terms Modal -->
682    <div class="resp-terms-overlay" id="responseTermsOverlay">
683      <div class="resp-terms-modal">
684        <div class="resp-terms-modal-header">
685          <h3>📜 Conditions d'utilisation</h3>
686          <button id="closeTermsBtn" style="background:rgba(255,255,255,0.08);border:none;color:rgba(255,255,255,0.6);width:36px;height:36px;border-radius:10px;font-size:1.2rem;cursor:pointer;display:flex;align-items:center;justify-content:center;">×</button>
687        </div>
688        <div class="resp-terms-body" id="responseTermsBody">
689          <h3 style="font-size:1.3rem;color:#fff;margin:0 0 6px;">Conditions d'utilisation</h3>
690          <p style="font-size:0.8rem;color:rgba(255,255,255,0.4);margin-bottom:20px;">Dernière mise à jour : 9 février 2026</p>
691          <h4>1. Acceptation des conditions</h4>
692          <p>En accédant à la plateforme partage.ai (faitsdivers.ca) (« la Plateforme ») ou en y soumettant du contenu, vous acceptez d'être lié par les présentes conditions d'utilisation (« Conditions »). Si vous n'acceptez pas ces Conditions, vous ne devez pas utiliser la Plateforme.</p>
693          <h4>2. Description du service</h4>
694          <p>La Plateforme permet aux utilisateurs de consulter, partager et soumettre du contenu généré par les utilisateurs (« Contenu »), incluant notamment des textes, images, vidéos, fichiers audio et tout autre média numérique.</p>
695          <p>La Plateforme agit comme intermédiaire technique permettant la diffusion de Contenu soumis par les utilisateurs.</p>
696          <h4>3. Admissibilité</h4>
697          <p>Vous déclarez avoir l'âge légal requis dans votre juridiction pour utiliser la Plateforme. Si vous utilisez la Plateforme au nom d'une organisation, vous déclarez avoir l'autorité nécessaire pour lier cette organisation aux présentes Conditions.</p>
698          <h4>4. Soumission de contenu</h4>
699          <p>En soumettant du Contenu sur la Plateforme :</p>
700          <ul>
701            <li>Vous déclarez détenir tous les droits nécessaires pour publier ce Contenu.</li>
702            <li>Vous garantissez que le Contenu respecte les lois applicables, notamment en matière de droit d'auteur, de vie privée et de droit à l'image.</li>
703            <li>Vous êtes responsable de vous assurer que toute personne identifiable apparaissant dans votre Contenu est représentée conformément aux lois applicables.</li>
704          </ul>
705          <h4>5. Licence accordée à la Plateforme</h4>
706          <p>En soumettant du Contenu, vous accordez à partage.ai une licence mondiale, non exclusive, gratuite permettant d'utiliser, reproduire, adapter, distribuer et afficher le Contenu dans le cadre de la Plateforme.</p>
707          <h4>6. Utilisation de technologies automatisées</h4>
708          <p>Le Contenu soumis peut être traité à l'aide de systèmes automatisés, incluant des outils d'analyse et de modération assistés par intelligence artificielle.</p>
709          <h4>7. Contenu interdit</h4>
710          <p>Il est interdit de soumettre du Contenu illégal, diffamatoire, haineux ou portant atteinte à la vie privée d'autrui.</p>
711          <h4>8. Responsabilité</h4>
712          <p>Vous êtes seul responsable du Contenu que vous soumettez. La Plateforme est fournie « telle quelle », sans garantie d'aucune sorte.</p>
713          <h4>9. Droit applicable</h4>
714          <p>Les présentes Conditions sont régies par les lois de la province de Québec et les lois fédérales du Canada applicables.</p>
715        </div>
716        <div id="responseTermsHint" style="padding:8px 24px;text-align:center;font-size:0.75rem;color:rgba(255,255,255,0.4);flex-shrink:0;">↓ Défile jusqu'en bas pour accepter ↓</div>
717        <div class="resp-terms-footer">
718          <button id="responseTermsAcceptBtn" disabled style="width:100%;padding:14px;border-radius:12px;border:none;background:var(--primary,#8b5cf6);color:white;font-weight:700;font-size:1rem;cursor:pointer;opacity:0.5;">
719            🔒 Défile pour débloquer
720          </button>
721        </div>
722      </div>
723    </div>
724  `;
725
726  // ============================================================
727  // Web Component
728  // ============================================================
729  class NmcReelsPlayer extends HTMLElement {
730    constructor() {
731      super();
732      this._shadow = this.attachShadow({ mode: 'open' });
733
734      // ── State ─────────────────────────────────────────────────
735      this._reelsMedia        = [];
736      this._currentReelIndex  = 0;
737      this._isReelsMode       = false;
738      this._isResponsesFeed   = false;
739      this._responsesFeedFromGrid    = false;
740      this._currentParentVideoId     = null;
741      this._currentParentVideoTitle  = '';
742      this._responsesCache    = {};
743      this._selectedVideoFile = null;
744      this._allMediaWithResponses    = [];
745      this._responseTermsFromSubmit  = false;
746
747      // ── Bound handlers (for removeEventListener) ───────────────
748      this._onPopState    = this._handlePopState.bind(this);
749      this._onKeyDown     = this._handleKeyDown.bind(this);
750      this._onReelsScroll = this.onReelsScroll.bind(this);
751    }
752
753    connectedCallback() {
754      this._shadow.innerHTML = `<style>${REELS_CSS}</style>${REELS_HTML}`;
755      this._bindEvents();
756      this._exposeGlobals();
757      window.addEventListener('popstate', this._onPopState);
758      document.addEventListener('keydown', this._onKeyDown);
759    }
760
761    disconnectedCallback() {
762      window.removeEventListener('popstate', this._onPopState);
763      document.removeEventListener('keydown', this._onKeyDown);
764      this.reelsWrapper?.removeEventListener('scroll', this._onReelsScroll);
765    }
766
767    // ── DOM shortcuts ──────────────────────────────────────────
768    get reelsWrapper()    { return this._shadow.getElementById('reelsWrapper'); }
769    get reelsContainer()  { return this._shadow.getElementById('reelsContainer'); }
770    _$ (id) { return this._shadow.getElementById(id); }
771
772    // ── Bind events ────────────────────────────────────────────
773    _bindEvents() {
774      const s = this._shadow;
775
776      // Header
777      s.getElementById('reelsBackBtn').addEventListener('click', () => this.exitReelsMode());
778      s.getElementById('reelsShareBtn').addEventListener('click', () => this.copyReelLink());
779      s.getElementById('responsesBackBtn').addEventListener('click', () => this.exitResponsesFeed());
780
781      // Fixed response buttons
782      s.getElementById('fixedResponsesBtn').addEventListener('click', () => this._handleFixedResponsesClick());
783      s.getElementById('fixedSubmitBtn').addEventListener('click', () => this._handleFixedSubmitClick());
784
785      // Submit modal
786      s.getElementById('submitModalOverlay').addEventListener('click', (e) => {
787        if (e.target === s.getElementById('submitModalOverlay')) this.closeSubmitModal();
788      });
789      s.getElementById('submitModalClose').addEventListener('click', () => this.closeSubmitModal());
790      s.getElementById('submitResponseForm').addEventListener('submit', (e) => this._submitResponse(e));
791      s.getElementById('chooseVideoBtn').addEventListener('click', () => s.getElementById('submitVideoInput').click());
792      s.getElementById('cameraVideoBtn').addEventListener('click', () => s.getElementById('submitCameraInput').click());
793      s.getElementById('submitVideoInput').addEventListener('change', () => this.handleVideoSelect(s.getElementById('submitVideoInput')));
794      s.getElementById('submitCameraInput').addEventListener('change', () => this.handleVideoSelect(s.getElementById('submitCameraInput')));
795      s.getElementById('removeVideoBtn').addEventListener('click', () => this.removeSelectedVideo());
796      s.getElementById('openTermsLink').addEventListener('click', (e) => {
797        e.preventDefault();
798        this.openResponseTerms(true);
799      });
800
801      // Terms modal
802      s.getElementById('responseTermsOverlay').addEventListener('click', (e) => {
803        if (e.target === s.getElementById('responseTermsOverlay')) this.closeResponseTerms();
804      });
805      s.getElementById('closeTermsBtn').addEventListener('click', () => this.closeResponseTerms());
806      s.getElementById('responseTermsAcceptBtn').addEventListener('click', () => {
807        if (this._responseTermsFromSubmit) this._acceptResponseTerms();
808        else this.closeResponseTerms();
809      });
810      s.getElementById('responseTermsBody').addEventListener('scroll', () => {
811        this._onTermsScroll();
812      }, { passive: true });
813    }
814
815    _handlePopState() {
816      if (this._isResponsesFeed) this.exitResponsesFeed();
817      else if (this._isReelsMode) this.exitReelsMode();
818    }
819
820    _handleKeyDown(e) {
821      if (e.key === 'Escape') {
822        if (this._$('submitModalOverlay').classList.contains('active')) this.closeSubmitModal();
823        else if (this._isResponsesFeed) this.exitResponsesFeed();
824      }
825    }
826
827    // ── PUBLIC: initReelsMode ──────────────────────────────────
828    initReelsMode(mediaList) {
829      if (!window.isMobile()) return;
830      this._allMediaWithResponses = mediaList;
831      this._reelsMedia = mediaList.filter(m => !m.parent_id);
832
833      // Only auto-enter Reels if enough visual media (video, photo, gallery).
834      // A single YouTube among 55 articles should NOT trigger reels mode.
835      const visualPlatforms = ['upload', 'ugc', 'youtube', 'tiktok', 'twitch', 'facebook', 'vimeo', 'photo', 'gallery', 'suno'];
836      const visualMedia = this._reelsMedia.filter(m => visualPlatforms.includes(m.platform));
837      const visualRatio = this._reelsMedia.length > 0 ? visualMedia.length / this._reelsMedia.length : 0;
838      if (visualRatio < 0.3) return; // Need at least 30% visual content for reels mode
839
840      this.enterReelsMode();
841    }
842
843    // ── PUBLIC: enterReelsMode ─────────────────────────────────
844    enterReelsMode() {
845      if (!this._reelsMedia.length) return;
846      this._isReelsMode = true;
847      document.documentElement.classList.add('reels-mode');
848
849      // Forcer le container visible (fiabilité — :host-context pas toujours fiable)
850      const container = this._$('reelsContainer');
851      if (container) {
852        container.style.display = 'block';
853        container.style.position = 'fixed';
854        container.style.inset = '0';
855        container.style.overflow = 'hidden';
856        container.style.background = '#000';
857        container.style.zIndex = '9999';
858      }
859
860      // Ajuster si header pub présent
861      const adsEngine = document.getElementById('nmcAdsEngine');
862      const adHeader  = adsEngine?.headerEl;
863      if (adHeader && adHeader.classList.contains('visible') && container) {
864        container.style.top = (adHeader.offsetHeight || 80) + 'px';
865      }
866
867      // Mettre à jour le header créateur
868      const pageObj = window.currentPageObj;
869      const avatarEl = this._$('reelsCreatorAvatar');
870      const nameEl   = this._$('reelsCreatorName');
871      const shareImg = this._$('reelsShareImg');
872      if (pageObj?.avatar) {
873        if (avatarEl) avatarEl.src = pageObj.avatar;
874        if (shareImg) shareImg.src = pageObj.avatar;
875      }
876      if (nameEl) nameEl.textContent = pageObj?.creator_name || pageObj?.username || 'Créateur';
877
878      this.renderReels();
879
880      const wrapper = this.reelsWrapper;
881      wrapper.removeEventListener('scroll', this._onReelsScroll);
882      wrapper.addEventListener('scroll', this._onReelsScroll, { passive: true });
883
884      document.body.style.overflow = 'hidden';
885
886      // Autoplay la première vidéo
887      setTimeout(() => {
888        const firstVideo = this._shadow.querySelector('.reel-item video');
889        if (firstVideo) {
890          firstVideo.muted = false;
891          if (window.playWithPreroll) window.playWithPreroll(firstVideo);
892        }
893      }, 100);
894
895      // Scroller vers média spécifique si ?m=ID
896      const mediaIdParam = window.mediaIdParam;
897      if (mediaIdParam) {
898        setTimeout(async () => {
899          if (mediaIdParam.startsWith('resp_')) {
900            await this._loadResponseFeedForMedia(mediaIdParam);
901          } else {
902            this.scrollToMedia(mediaIdParam);
903          }
904        }, 100);
905      }
906
907      this.dispatchEvent(new CustomEvent('nmc-reels-enter', {
908        bubbles: true, composed: true,
909        detail: { mediaCount: this._reelsMedia.length }
910      }));
911    }
912
913    // ── PUBLIC: exitReelsMode ──────────────────────────────────
914    exitReelsMode() {
915      this._isReelsMode = false;
916      document.documentElement.classList.remove('reels-mode');
917      document.body.style.overflow = '';
918
919      const container = this._$('reelsContainer');
920      if (container) {
921        container.style.display = '';
922        container.style.position = '';
923        container.style.inset = '';
924        container.style.overflow = '';
925        container.style.background = '';
926        container.style.zIndex = '';
927        container.style.top = '';
928      }
929
930      // Pause iframes
931      this._shadow.querySelectorAll('.reel-item iframe').forEach(iframe => {
932        iframe.src = 'about:blank';
933      });
934      // Pause vidéos
935      this._shadow.querySelectorAll('.reel-item video').forEach(v => v.pause());
936      // Pause audios Suno
937      this._shadow.querySelectorAll('.reel-item audio').forEach(a => a.pause());
938
939      this.dispatchEvent(new CustomEvent('nmc-reels-exit', { bubbles: true, composed: true }));
940    }
941
942    // ── PUBLIC: renderReels ────────────────────────────────────
943    renderReels() {
944      const wrapper = this.reelsWrapper;
945      const currentAds = window.currentAds || {};
946      const infeedAd = currentAds.infeed?.[0];
947      const infeedPosition = infeedAd ? (parseInt(infeedAd.position) || 3) : -1;
948
949      let reelsItems = [];
950      this._reelsMedia.forEach((media, index) => {
951        if (infeedAd && index === infeedPosition) {
952          reelsItems.push({ type: 'ad', ad: infeedAd });
953        }
954        reelsItems.push({ type: 'media', media, originalIndex: index });
955      });
956
957      const overlayAd = currentAds['video-overlay']?.[0];
958      const _esc = window.escapeHtml || (t => t);
959      const _emoji = window.getPlatformEmoji || (() => '📺');
960      const un = window.username || '';
961
962      wrapper.innerHTML = reelsItems.map((item, index) => {
963        if (item.type === 'ad') {
964          if (window.trackAd) window.trackAd(item.ad.id, 'impression');
965          return `
966            <div class="reel-item reel-ad" data-index="${index}" data-is-ad="true">
967              <div class="reel-media reel-ad-content">
968                <a href="${_esc(item.ad.link)}" target="_blank" onclick="window.trackAd?.('${item.ad.id}','click')">
969                  <img src="${_esc(item.ad.content)}" alt="Publicité">
970                </a>
971              </div>
972              <div class="reel-info">
973                <span style="background:rgba(255,255,255,0.2);padding:4px 12px;border-radius:12px;font-size:0.75rem;">Sponsorisé</span>
974              </div>
975            </div>`;
976        }
977
978        const media = item.media;
979        const isVertical = media.vertical == 1;
980        const tagsHtml = (media.tags || []).map(tag =>
981          `<a href="/${un}/${_esc(tag.slug)}" class="reel-tag" onclick="window.navigateToTag?.('${_esc(tag.slug)}')">${tag.emoji || ''} ${_esc(tag.name)}</a>`
982        ).join('');
983
984        const isProprietaryContent = media.platform === 'upload' || media.platform === 'ugc';
985        let overlayHtml = '';
986        if (overlayAd && isProprietaryContent) {
987          const pos = overlayAd.position || 'top-right';
988          overlayHtml = `
989            <div class="reel-overlay-ad ${pos}">
990              <a href="${_esc(overlayAd.link)}" target="_blank" onclick="window.trackAd?.('${overlayAd.id}','click')">
991                <img src="${_esc(overlayAd.content)}" alt="Pub">
992              </a>
993              <button class="reel-overlay-close" onclick="event.preventDefault();event.stopPropagation();this.parentElement.style.display='none';">✕</button>
994            </div>`;
995          if (item.originalIndex === this._reelsMedia.findIndex(m => m.platform === 'upload' || m.platform === 'ugc')) {
996            if (window.trackAd) window.trackAd(overlayAd.id, 'impression');
997          }
998        }
999
1000        // Upload / UGC
1001        if (media.platform === 'upload' || media.platform === 'ugc') {
1002          let videoUrl = media.url || '';
1003          if (videoUrl.startsWith('/')) videoUrl = window.location.origin + videoUrl;
1004          let thumbUrl = media.thumbnail || '';
1005          if (thumbUrl.startsWith('/')) thumbUrl = window.location.origin + thumbUrl;
1006          return `
1007            <div class="reel-item ${isVertical ? 'vertical' : 'horizontal'}" data-index="${index}" data-original-index="${item.originalIndex}" data-media-id="${media.id}" data-platform="${media.platform}" data-allows-responses="${media.allows_responses || 0}">
1008              <div class="reel-media reel-${media.platform}" onclick="window.toggleReelVideo?.(this)">
1009                <video src="${videoUrl}" poster="${thumbUrl}" playsinline webkit-playsinline muted loop preload="auto" style="width:100%;height:100%;max-height:100vh;object-fit:contain;background:#000;pointer-events:none;"></video>
1010                <div class="reel-play-indicator">▶</div>
1011              </div>
1012              ${overlayHtml}
1013              <div class="reel-info">
1014                ${media.parent_id ? `<div class="response-badge" onclick="window.goToParentVideo?.('${media.parent_id}')">← Retour</div>` : ''}
1015                <a href="/${un}?platform=${media.platform}" class="reel-platform" onclick="event.stopPropagation();window.filterByPlatform?.('${media.platform}')">${_emoji(media.platform)} ${media.platform === 'ugc' ? 'UGC' : media.platform}</a>
1016                <h3 class="reel-title">${_esc(media.title)}</h3>
1017                ${media.description ? `<p class="reel-description">${_esc(media.description)}</p>` : ''}
1018                ${tagsHtml ? `<div class="reel-tags">${tagsHtml}</div>` : ''}
1019              </div>
1020            </div>`;
1021        }
1022
1023        // Suno — lecteur embarquable officiel (le CDN mp3 exige des URLs signées depuis 2026-09, voir nmc-media-card.js)
1024        if (media.platform === 'suno') {
1025          const uuidM = [media.embed_url, media.url, media.thumbnail].map(v => typeof v === 'string' && v.match(/^https:\/\/(?:cdn[12]\.suno\.ai\/(?:image_(?:large_)?)?|suno\.com\/(?:embed|song)\/)([a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12})/i)).find(Boolean);
1026          const embedSrc = uuidM ? `https://suno.com/embed/${uuidM[1].toLowerCase()}` : '';
1027          return `
1028            <div class="reel-item vertical" data-index="${index}" data-original-index="${item.originalIndex}" data-media-id="${media.id}" data-platform="suno" data-allows-responses="${media.allows_responses || 0}">
1029              <div class="reel-media reel-suno" style="background:linear-gradient(135deg,#1a1a2e 0%,#16213e 50%,#0f3460 100%);display:flex;flex-direction:column;align-items:center;justify-content:center;padding:30px 20px;">
1030                ${uuidM ? `<img src="https://cdn2.suno.ai/image_${uuidM[1].toLowerCase()}.jpeg" alt="Cover" loading="lazy" style="width:160px;height:160px;border-radius:16px;object-fit:cover;box-shadow:0 20px 60px rgba(0,0,0,0.5);margin-bottom:20px;" onerror="this.style.display='none'">` : ''}
1031                <h2 style="color:white;font-size:1.5rem;text-align:center;margin-bottom:15px;max-width:80%;">${_esc(media.title)}</h2>
1032                <div style="display:inline-flex;align-items:center;gap:8px;background:rgba(255,255,255,0.1);padding:8px 16px;border-radius:20px;margin-bottom:20px;">
1033                  <span>🎤</span><span style="color:#a78bfa;font-weight:600;">Créé avec Suno AI</span>
1034                </div>
1035                ${embedSrc
1036                  ? `<iframe src="${embedSrc}" data-platform="suno" data-base-url="${embedSrc}" title="${_esc(media.title)}" loading="lazy" allow="autoplay; clipboard-write; encrypted-media" referrerpolicy="strict-origin-when-cross-origin" style="width:100%;max-width:480px;height:240px;border:0;border-radius:12px;background:#0f0f1a;" data-suno-embed="true"></iframe>`
1037                  : ''}
1038                ${/^https:\/\/suno\.com\//i.test(media.url || '') ? `<a href="${_esc(media.url)}" target="_blank" rel="noopener" style="margin-top:25px;color:#a78bfa;text-decoration:none;font-weight:600;display:flex;align-items:center;gap:8px;">🔗 Écouter sur Suno</a>` : ''}
1039              </div>
1040              ${overlayHtml}
1041              <div class="reel-info">
1042                ${media.parent_id ? `<div class="response-badge" onclick="window.goToParentVideo?.('${media.parent_id}')">← Retour</div>` : ''}
1043                <a href="/${un}?platform=suno" class="reel-platform" onclick="window.filterByPlatform?.('suno')">🎤 suno</a>
1044                ${media.description ? `<p class="reel-description">${_esc(media.description)}</p>` : ''}
1045                ${tagsHtml ? `<div class="reel-tags">${tagsHtml}</div>` : ''}
1046              </div>
1047            </div>`;
1048        }
1049
1050        // Article
1051        if (media.platform === 'article') {
1052          let thumbUrl = media.thumbnail || '';
1053          if (thumbUrl.startsWith('/')) thumbUrl = window.location.origin + thumbUrl;
1054          return `
1055            <div class="reel-item" data-index="${index}" data-original-index="${item.originalIndex}" data-media-id="${media.id}" data-platform="article" data-has-audio="0">
1056              <div class="reel-article">
1057                <div class="reel-article-scroll">
1058                  ${thumbUrl ? `<img src="${thumbUrl}" class="reel-article-thumbnail" alt="">` : ''}
1059                  <h2 class="reel-article-title">${_esc(media.title)}</h2>
1060                  <div class="reel-article-content">${_esc(media.content || media.description || '')}</div>
1061                </div>
1062                <div class="reel-article-footer">
1063                  <a href="/${un}?platform=article" class="reel-platform-badge" onclick="event.stopPropagation();window.filterByPlatform?.('article')">📰 Article</a>
1064                  ${tagsHtml ? `<div class="reel-tags">${tagsHtml}</div>` : ''}
1065                </div>
1066              </div>
1067            </div>`;
1068        }
1069
1070        // Photo
1071        if (media.platform === 'photo') {
1072          let photoUrl = media.url || media.thumbnail || '';
1073          if (photoUrl.startsWith('/')) photoUrl = window.location.origin + photoUrl;
1074          return `
1075            <div class="reel-item" data-index="${index}" data-original-index="${item.originalIndex}" data-media-id="${media.id}" data-platform="photo" data-has-audio="0">
1076              <div class="reel-photo">
1077                <img src="${photoUrl}" class="reel-photo-img" alt="${_esc(media.title)}">
1078                <div class="reel-photo-info">
1079                  <a href="/${un}?platform=photo" class="reel-platform-badge" onclick="event.stopPropagation();window.filterByPlatform?.('photo')">📷 Photo</a>
1080                  <h3 class="reel-title">${_esc(media.title)}</h3>
1081                  ${media.description ? `<p class="reel-description">${_esc(media.description)}</p>` : ''}
1082                  ${tagsHtml ? `<div class="reel-tags">${tagsHtml}</div>` : ''}
1083                </div>
1084              </div>
1085            </div>`;
1086        }
1087
1088        // Gallery
1089        if (media.platform === 'gallery') {
1090          let images = [];
1091          try { images = JSON.parse(media.images || '[]'); } catch(e) {}
1092          if (!images.length && media.thumbnail) images = [media.thumbnail];
1093          const galleryHtml = images.map((img, i) => {
1094            let url = img;
1095            if (url.startsWith('/')) url = window.location.origin + url;
1096            return `<div class="reel-gallery-slide"><img src="${url}" alt="Image ${i+1}"></div>`;
1097          }).join('');
1098          return `
1099            <div class="reel-item" data-index="${index}" data-original-index="${item.originalIndex}" data-media-id="${media.id}" data-platform="gallery" data-has-audio="0">
1100              <div class="reel-gallery">
1101                <div class="reel-gallery-header">
1102                  <span class="reel-gallery-counter">1/${images.length}</span>
1103                </div>
1104                <div class="reel-gallery-slides" onscroll="window.updateGalleryCounter?.(this)">${galleryHtml}</div>
1105                <div class="reel-gallery-info">
1106                  <a href="/${un}?platform=gallery" class="reel-platform-badge" onclick="event.stopPropagation();window.filterByPlatform?.('gallery')">🖼️ Galerie</a>
1107                  <h3 class="reel-title">${_esc(media.title)}</h3>
1108                  ${media.description ? `<p class="reel-description">${_esc(media.description)}</p>` : ''}
1109                  ${tagsHtml ? `<div class="reel-tags">${tagsHtml}</div>` : ''}
1110                </div>
1111              </div>
1112            </div>`;
1113        }
1114
1115        // Iframe (YouTube, TikTok, Spotify, etc.)
1116        return `
1117          <div class="reel-item ${isVertical ? 'vertical' : 'horizontal'}" data-index="${index}" data-original-index="${item.originalIndex}" data-media-id="${media.id}" data-platform="${media.platform}" data-allows-responses="${media.allows_responses || 0}">
1118            <div class="reel-media">
1119              <iframe
1120                src="${this._getReelSrc(media.embed_url, media.platform, index === 0)}"
1121                allowfullscreen
1122                allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; fullscreen"
1123                playsinline
1124                data-platform="${media.platform}"
1125                data-base-url="${media.embed_url}"
1126                data-index="${index}"
1127              ></iframe>
1128            </div>
1129            ${overlayHtml}
1130            <div class="reel-info">
1131              ${media.parent_id ? `<div class="response-badge" onclick="window.goToParentVideo?.('${media.parent_id}')">← Retour</div>` : ''}
1132              <a href="/${un}?platform=${media.platform}" class="reel-platform" onclick="window.filterByPlatform?.('${media.platform}')">${_emoji(media.platform)} ${media.platform}</a>
1133              <h3 class="reel-title">${_esc(media.title)}</h3>
1134              ${media.description ? `<p class="reel-description">${_esc(media.description)}</p>` : ''}
1135              ${tagsHtml ? `<div class="reel-tags">${tagsHtml}</div>` : ''}
1136            </div>
1137          </div>`;
1138      }).join('');
1139
1140      this._updateReelCounter();
1141      if (window.refreshPrerollListeners) setTimeout(() => window.refreshPrerollListeners(), 100);
1142
1143      // Trigger enrichment for first reel
1144      const firstReel = this._shadow.querySelector('.reel-item[data-media-id]');
1145      if (firstReel && window.enqueueEnrichment) window.enqueueEnrichment(firstReel.dataset.mediaId);
1146
1147      // Dispatch event for nmc-track
1148      this.dispatchEvent(new CustomEvent('nmc-track', {
1149        bubbles: true, composed: true,
1150        detail: { type: 'reel_view', data: { count: this._reelsMedia.length } }
1151      }));
1152    }
1153
1154    // ── onReelsScroll ──────────────────────────────────────────
1155    onReelsScroll() {
1156      const wrapper = this.reelsWrapper;
1157      const items   = wrapper.querySelectorAll('.reel-item');
1158      const scrollTop   = wrapper.scrollTop;
1159      const itemHeight  = window.innerHeight;
1160      const newIndex    = Math.round(scrollTop / itemHeight);
1161
1162      if (newIndex !== this._currentReelIndex && newIndex >= 0 && newIndex < items.length) {
1163        if (window.stopAllPrerolls) window.stopAllPrerolls();
1164
1165        const prevItem = items[this._currentReelIndex];
1166        if (prevItem) {
1167          const prevIframe = prevItem.querySelector('iframe');
1168          if (prevIframe) this._stopVideo(prevIframe);
1169          const prevVideo = prevItem.querySelector('video');
1170          if (prevVideo) { prevVideo.pause(); prevVideo.currentTime = 0; }
1171          const prevAudio = prevItem.querySelector('audio');
1172          if (prevAudio) { prevAudio.pause(); prevAudio.currentTime = 0; }
1173        }
1174
1175        const currentItem = items[newIndex];
1176        if (currentItem && !currentItem.classList.contains('reel-ad')) {
1177          const iframe = currentItem.querySelector('iframe');
1178          if (iframe) this._restoreVideo(iframe);
1179          const video = currentItem.querySelector('video');
1180          if (video && window.playWithPreroll) window.playWithPreroll(video);
1181        }
1182
1183        this._currentReelIndex = newIndex;
1184        this._updateReelCounter();
1185
1186        const reelMediaId = currentItem?.dataset?.mediaId;
1187        if (reelMediaId && window.enqueueEnrichment) window.enqueueEnrichment(reelMediaId);
1188      }
1189    }
1190
1191    // ── scrollToMedia ──────────────────────────────────────────
1192    scrollToMedia(mediaId) {
1193      const wrapper = this.reelsWrapper;
1194      const target  = wrapper.querySelector(`.reel-item[data-media-id="${mediaId}"]`);
1195      if (target) {
1196        const items = wrapper.querySelectorAll('.reel-item');
1197        this._currentReelIndex = Array.from(items).indexOf(target);
1198        target.scrollIntoView({ behavior: 'instant' });
1199        this._updateReelCounter();
1200      }
1201    }
1202
1203    // ── copyReelLink ───────────────────────────────────────────
1204    copyReelLink() {
1205      const wrapper = this.reelsWrapper;
1206      const items   = wrapper.querySelectorAll('.reel-item');
1207      const current = items[this._currentReelIndex];
1208      if (!current || current.classList.contains('reel-ad')) return;
1209
1210      const mediaId = current.dataset.mediaId;
1211      const media   = this._reelsMedia.find(m => m.id == mediaId);
1212      if (!media) return;
1213
1214      const url = window.location.origin + '/' + (window.username || '') + '?m=' + media.id;
1215      if (navigator.share) {
1216        navigator.share({
1217          title: media.title,
1218          text: this._isResponsesFeed ? 'Regarde cette réponse vidéo sur partage.ai' : `Regarde "${media.title}" sur partage.ai`,
1219          url
1220        }).catch(() => {});
1221      } else {
1222        navigator.clipboard.writeText(url).then(() => {
1223          const btn = this._$('reelsShareBtn');
1224          if (btn) {
1225            btn.classList.add('copied');
1226            setTimeout(() => btn.classList.remove('copied'), 2000);
1227          }
1228        }).catch(() => {});
1229      }
1230
1231      this.dispatchEvent(new CustomEvent('nmc-track', {
1232        bubbles: true, composed: true,
1233        detail: { type: 'reel_share', data: { mediaId: media.id } }
1234      }));
1235    }
1236
1237    // ── shareReel (par index) ──────────────────────────────────
1238    shareReel(index) {
1239      const media = this._reelsMedia[index];
1240      if (!media) return;
1241      const url = window.location.origin + '/' + (window.username || '') + '?m=' + media.id;
1242      if (navigator.share) {
1243        navigator.share({ title: media.title, url }).catch(() => {});
1244      } else {
1245        navigator.clipboard.writeText(url).catch(() => {});
1246      }
1247    }
1248
1249    // ── navigateToTag ──────────────────────────────────────────
1250    navigateToTag(tagSlug) {
1251      this.exitReelsMode();
1252      window.location.href = '/' + (window.username || '') + '/' + tagSlug;
1253    }
1254
1255    // ── filterByPlatform ───────────────────────────────────────
1256    filterByPlatform(platform) {
1257      this.exitReelsMode();
1258      window.location.href = '/' + (window.username || '') + '?platform=' + platform;
1259    }
1260
1261    // ── autoplayVisibleReelVideo ───────────────────────────────
1262    autoplayVisibleReelVideo() {
1263      const wrapper = this.reelsWrapper;
1264      if (!wrapper) return;
1265      const items   = wrapper.querySelectorAll('.reel-item');
1266      const scrollTop   = wrapper.scrollTop;
1267      const currentIndex = Math.round(scrollTop / window.innerHeight);
1268      items.forEach((item, i) => {
1269        const video = item.querySelector('.reel-upload video');
1270        if (!video) return;
1271        if (i === currentIndex) video.play().catch(() => {});
1272        else { video.pause(); video.currentTime = 0; }
1273      });
1274    }
1275
1276    // ── toggleReelVideo ────────────────────────────────────────
1277    toggleReelVideo(container) {
1278      const video = container.querySelector('video');
1279      if (!video) return;
1280      if (video.paused) {
1281        if (window.playWithPreroll) window.playWithPreroll(video);
1282        else video.play();
1283      } else {
1284        video.pause();
1285      }
1286    }
1287
1288    // ── updateGalleryCounter ───────────────────────────────────
1289    updateGalleryCounter(slidesEl) {
1290      const slideWidth = slidesEl.offsetWidth;
1291      const currentSlide = Math.round(slidesEl.scrollLeft / slideWidth) + 1;
1292      const totalSlides  = slidesEl.querySelectorAll('.reel-gallery-slide').length;
1293      const counter = slidesEl.closest('.reel-gallery')?.querySelector('.reel-gallery-counter');
1294      if (counter) counter.textContent = `${currentSlide}/${totalSlides}`;
1295    }
1296
1297    // ── VIDEO RESPONSES ────────────────────────────────────────
1298    async showResponsesFeed(parentId, parentTitle) {
1299      this._currentParentVideoId    = parentId;
1300      this._currentParentVideoTitle = parentTitle;
1301      this._isResponsesFeed         = true;
1302      this._responsesFeedFromGrid   = !this._isReelsMode;
1303
1304      this._$('responsesHeader').classList.add('active');
1305      this._$('fixedResponseActions').style.display = 'none';
1306
1307      try {
1308        let responses = this._responsesCache[parentId];
1309        if (!responses) {
1310          const res  = await fetch(`${window.API_URL || '/api/v2'}/get-responses?parent_id=${encodeURIComponent(parentId)}`);
1311          const data = await res.json();
1312          responses  = data.responses || [];
1313          this._responsesCache[parentId] = responses;
1314        }
1315
1316        if (!responses.length) {
1317          alert('Aucune réponse encore. Sois le premier à répondre !');
1318          this.openSubmitModal(parentId, parentTitle);
1319          this._isResponsesFeed = false;
1320          this._$('responsesHeader').classList.remove('active');
1321          this._updateFixedResponseButtons();
1322          return;
1323        }
1324
1325        this._reelsMedia = responses;
1326        this._currentReelIndex = 0;
1327        this.enterReelsMode();
1328        this.renderReels();
1329
1330        this.reelsWrapper.scrollTo(0, 0);
1331        setTimeout(() => {
1332          const firstVideo = this._shadow.querySelector('.reel-item video');
1333          if (firstVideo) {
1334            firstVideo.muted = false;
1335            if (window.playWithPreroll) window.playWithPreroll(firstVideo);
1336          }
1337        }, 100);
1338
1339        history.pushState({ responsesFeed: true, parentId }, '');
1340      } catch (err) {
1341        console.error('Erreur chargement réponses:', err);
1342        alert('Erreur lors du chargement des réponses');
1343        this._isResponsesFeed = false;
1344        this._$('responsesHeader').classList.remove('active');
1345      }
1346    }
1347
1348    exitResponsesFeed() {
1349      this._isResponsesFeed    = false;
1350      this._currentParentVideoId = null;
1351      this._$('responsesHeader').classList.remove('active');
1352
1353      this._shadow.querySelectorAll('.reel-item video').forEach(v => v.pause());
1354
1355      if (this._responsesFeedFromGrid) {
1356        this._responsesFeedFromGrid = false;
1357        this.exitReelsMode();
1358      } else {
1359        this._reelsMedia = this._allMediaWithResponses.filter(m => !m.parent_id);
1360        this.renderReels();
1361      }
1362    }
1363
1364    goToParentVideo(parentId) {
1365      this.exitResponsesFeed();
1366      setTimeout(() => {
1367        const parent = this.reelsWrapper.querySelector(`[data-media-id="${parentId}"]`);
1368        if (parent) parent.scrollIntoView({ behavior: 'smooth' });
1369      }, 100);
1370    }
1371
1372    // ── SUBMIT MODAL ───────────────────────────────────────────
1373    openSubmitModal(parentId, parentTitle) {
1374      this._currentParentVideoId    = parentId;
1375      this._currentParentVideoTitle = parentTitle;
1376      this._$('submitParentTitle').textContent = parentTitle || 'Vidéo';
1377      this._$('submitModalOverlay').classList.add('active');
1378      this._resetSubmitForm();
1379    }
1380
1381    closeSubmitModal() {
1382      this._$('submitModalOverlay').classList.remove('active');
1383      this._resetSubmitForm();
1384    }
1385
1386    _resetSubmitForm() {
1387      this._selectedVideoFile = null;
1388      this._$('submitVideoInput').value = '';
1389      this._$('submitCameraInput').value = '';
1390      this._$('submitUserName').value = '';
1391      this._$('submitVideoTitle').value = '';
1392      this._$('submitUploadZone').classList.remove('has-file');
1393      this._$('submitVideoPreview').classList.remove('active');
1394      this._$('submitProgressBar').classList.remove('active');
1395      this._$('submitBtn').disabled = false;
1396
1397      const uploadText    = this._$('submitUploadZone').querySelector('.upload-text');
1398      const uploadIcon    = this._$('submitUploadZone').querySelector('.upload-icon');
1399      const uploadButtons = this._$('submitUploadZone').querySelector('.upload-buttons');
1400      const uploadHint    = this._$('submitUploadZone').querySelector('.upload-hint');
1401      if (uploadText)    uploadText.textContent = 'Choisir ou filmer';
1402      if (uploadIcon)    uploadIcon.textContent = '🎬';
1403      if (uploadButtons) uploadButtons.style.display = 'flex';
1404      if (uploadHint)    uploadHint.style.display = 'block';
1405
1406      const previewVideo = this._$('submitPreviewVideo');
1407      if (previewVideo?.src) { URL.revokeObjectURL(previewVideo.src); previewVideo.src = ''; }
1408      this._$('submitVideoPreview').style.display = 'none';
1409    }
1410
1411    handleVideoSelect(input) {
1412      const file = input.files[0];
1413      if (!file) return;
1414      if (!file.type.startsWith('video/')) { alert('Sélectionne une vidéo !'); return; }
1415      if (file.size > 100 * 1024 * 1024) { alert('Fichier trop volumineux (max 100MB)'); return; }
1416
1417      this._selectedVideoFile = file;
1418      const zone = this._$('submitUploadZone');
1419      zone.classList.add('has-file');
1420      const ut = zone.querySelector('.upload-text');
1421      const ui = zone.querySelector('.upload-icon');
1422      const ub = zone.querySelector('.upload-buttons');
1423      const uh = zone.querySelector('.upload-hint');
1424      if (ut) ut.textContent = '✅ Vidéo sélectionnée';
1425      if (ui) ui.textContent = '✅';
1426      if (ub) ub.style.display = 'none';
1427      if (uh) uh.style.display = 'none';
1428
1429      const previewVideo = this._$('submitPreviewVideo');
1430      const fileName     = this._$('submitFileName');
1431      const preview      = this._$('submitVideoPreview');
1432      if (previewVideo) {
1433        const videoURL = URL.createObjectURL(file);
1434        previewVideo.src = videoURL;
1435        previewVideo.setAttribute('playsinline', '');
1436        previewVideo.muted = true;
1437        previewVideo.preload = 'metadata';
1438        previewVideo.onloadeddata = () => {
1439          try {
1440            const canvas = document.createElement('canvas');
1441            canvas.width  = previewVideo.videoWidth;
1442            canvas.height = previewVideo.videoHeight;
1443            canvas.getContext('2d').drawImage(previewVideo, 0, 0);
1444            previewVideo.poster = canvas.toDataURL('image/jpeg');
1445          } catch(e) {}
1446        };
1447        previewVideo.load();
1448        previewVideo.play().catch(() => {});
1449      }
1450      if (fileName) fileName.textContent = file.name;
1451      if (preview)  { preview.style.display = 'block'; preview.classList.add('active'); }
1452    }
1453
1454    removeSelectedVideo() {
1455      this._selectedVideoFile = null;
1456      this._$('submitVideoInput').value = '';
1457      this._$('submitCameraInput').value = '';
1458      const zone = this._$('submitUploadZone');
1459      zone.classList.remove('has-file');
1460      const ut = zone.querySelector('.upload-text');
1461      const ui = zone.querySelector('.upload-icon');
1462      const ub = zone.querySelector('.upload-buttons');
1463      const uh = zone.querySelector('.upload-hint');
1464      if (ut) ut.textContent = 'Choisir ou filmer';
1465      if (ui) ui.textContent = '🎬';
1466      if (ub) ub.style.display = 'flex';
1467      if (uh) uh.style.display = 'block';
1468
1469      const pv = this._$('submitPreviewVideo');
1470      const pr = this._$('submitVideoPreview');
1471      if (pv?.src) { URL.revokeObjectURL(pv.src); pv.src = ''; }
1472      if (pr)      pr.classList.remove('active');
1473    }
1474
1475    // ── TERMS MODAL ────────────────────────────────────────────
1476    openResponseTerms(readOnly) {
1477      this._responseTermsFromSubmit = !readOnly;
1478      const body   = this._$('responseTermsBody');
1479      const btn    = this._$('responseTermsAcceptBtn');
1480      const hint   = this._$('responseTermsHint');
1481      const overlay = this._$('responseTermsOverlay');
1482      body.scrollTop = 0;
1483      overlay.classList.add('active');
1484      if (readOnly) {
1485        btn.disabled = false; btn.style.opacity = '1';
1486        btn.textContent = '📖 Fermer';
1487        hint.style.display = 'none';
1488      } else {
1489        btn.disabled = true; btn.style.opacity = '0.5';
1490        btn.textContent = '🔒 Défile pour débloquer';
1491        hint.style.display = '';
1492        requestAnimationFrame(() => {
1493          if (body.scrollHeight <= body.clientHeight + 40) {
1494            btn.disabled = false; btn.style.opacity = '1';
1495            btn.textContent = "✅ J'accepte et j'envoie";
1496            hint.style.display = 'none';
1497          }
1498        });
1499      }
1500    }
1501
1502    closeResponseTerms() {
1503      this._$('responseTermsOverlay').classList.remove('active');
1504    }
1505
1506    _acceptResponseTerms() {
1507      this.closeResponseTerms();
1508      this._doSubmitResponse();
1509    }
1510
1511    _onTermsScroll() {
1512      if (!this._responseTermsFromSubmit) return;
1513      const body = this._$('responseTermsBody');
1514      const atBottom = (body.scrollTop + body.clientHeight) >= (body.scrollHeight - 40);
1515      if (atBottom) {
1516        const btn  = this._$('responseTermsAcceptBtn');
1517        const hint = this._$('responseTermsHint');
1518        btn.disabled = false; btn.style.opacity = '1';
1519        btn.textContent = "✅ J'accepte et j'envoie";
1520        if (hint) hint.style.display = 'none';
1521      }
1522    }
1523
1524    // ── SUBMIT FORM ────────────────────────────────────────────
1525    async _submitResponse(e) {
1526      e.preventDefault();
1527      if (!this._selectedVideoFile) { alert('Sélectionne une vidéo !'); return; }
1528      const name = this._$('submitUserName').value.trim();
1529      if (!name) { alert('Entre ton pseudo !'); return; }
1530      this.openResponseTerms(false);
1531    }
1532
1533    async _doSubmitResponse() {
1534      const name = this._$('submitUserName').value.trim();
1535      const submitBtn    = this._$('submitBtn');
1536      const progressBar  = this._$('submitProgressBar');
1537      const progressFill = this._$('submitProgressFill');
1538
1539      submitBtn.disabled = true;
1540      progressBar.classList.add('active');
1541
1542      const formData = new FormData();
1543      formData.append('page_slug',  window.username || '');
1544      formData.append('parent_id',  this._currentParentVideoId);
1545      formData.append('video',      this._selectedVideoFile);
1546      formData.append('name',       name.replace('@', ''));
1547      formData.append('title',      this._$('submitVideoTitle').value.trim());
1548
1549      const xhr = new XMLHttpRequest();
1550      xhr.upload.onprogress = (e) => {
1551        if (e.lengthComputable) progressFill.style.width = Math.round((e.loaded / e.total) * 100) + '%';
1552      };
1553      xhr.onload = () => {
1554        if (xhr.status >= 200 && xhr.status < 300) {
1555          try {
1556            const res = JSON.parse(xhr.responseText);
1557            if (res.success) {
1558              alert('✅ Réponse envoyée ! Elle sera visible après modération.');
1559              this.closeSubmitModal();
1560              delete this._responsesCache[this._currentParentVideoId];
1561            } else {
1562              alert(res.error || "Erreur lors de l'envoi");
1563            }
1564          } catch { alert('Erreur serveur'); }
1565        } else {
1566          alert('Erreur ' + xhr.status);
1567        }
1568        submitBtn.disabled = false;
1569        progressBar.classList.remove('active');
1570      };
1571      xhr.onerror = () => {
1572        alert('Erreur de connexion');
1573        submitBtn.disabled = false;
1574        progressBar.classList.remove('active');
1575      };
1576      xhr.open('POST', (window.API_URL || '/api/v2') + '/submit-response');
1577      xhr.send(formData);
1578    }
1579
1580    // ── PRIVATE: helpers ───────────────────────────────────────
1581    _getReelSrc(embedUrl) { return embedUrl; }
1582
1583    _stopVideo(iframe) {
1584      if (!iframe) return;
1585      const platform = iframe.dataset.platform;
1586      const baseUrl  = iframe.dataset.baseUrl;
1587      if (platform === 'tiktok') return;
1588      if (platform === 'spotify' && baseUrl) { iframe.src = baseUrl; return; }
1589      iframe.dataset.wasStopped = 'true';
1590      iframe.src = 'about:blank';
1591    }
1592
1593    _restoreVideo(iframe) {
1594      if (!iframe) return;
1595      if (iframe.dataset.wasStopped === 'true' && iframe.dataset.baseUrl) {
1596        iframe.src = iframe.dataset.baseUrl;
1597        iframe.dataset.wasStopped = 'false';
1598      }
1599    }
1600
1601    _updateReelCounter() {
1602      const counter = this._$('reelCounter');
1603      if (counter) {
1604        const wrapper = this.reelsWrapper;
1605        const items   = wrapper.querySelectorAll('.reel-item');
1606        const current = items[this._currentReelIndex];
1607        if (current?.classList.contains('reel-ad')) {
1608          counter.textContent = 'Sponsorisé';
1609        } else if (current?.dataset?.originalIndex !== undefined) {
1610          const oi = parseInt(current.dataset.originalIndex);
1611          counter.textContent = `${oi + 1} / ${this._reelsMedia.length}`;
1612        } else {
1613          counter.textContent = `${this._currentReelIndex + 1} / ${this._reelsMedia.length}`;
1614        }
1615      }
1616      this._updateFixedResponseButtons();
1617    }
1618
1619    _updateFixedResponseButtons() {
1620      const fixedActions = this._$('fixedResponseActions');
1621      const shareBtnEl   = this._$('reelsShareBtn');
1622      const logoEl       = this._shadow.querySelector('.reels-logo');
1623      if (!fixedActions) return;
1624
1625      const wrapper = this.reelsWrapper;
1626      const items   = wrapper.querySelectorAll('.reel-item');
1627      const current = items[this._currentReelIndex];
1628
1629      if (current?.classList.contains('reel-ad') || current?.dataset?.isAd === 'true') {
1630        fixedActions.style.display = 'none';
1631        if (logoEl) logoEl.style.display = 'none';
1632        return;
1633      }
1634
1635      const oi = current?.dataset?.originalIndex;
1636      const currentMedia = oi !== undefined ? this._reelsMedia[parseInt(oi)] : null;
1637      if (!currentMedia) { fixedActions.style.display = 'none'; return; }
1638
1639      if (logoEl) logoEl.style.display = '';
1640      if (this._isResponsesFeed) {
1641        if (logoEl) logoEl.style.display = 'none';
1642        if (shareBtnEl) shareBtnEl.style.display = '';
1643      } else {
1644        if (shareBtnEl) shareBtnEl.style.display = '';
1645        if (logoEl) logoEl.style.display = '';
1646      }
1647
1648      if (currentMedia.allows_responses == 1 || currentMedia.allows_responses === true) {
1649        fixedActions.style.display = 'flex';
1650        const responsesBtn = this._$('fixedResponsesBtn');
1651        if (responsesBtn) {
1652          const count = parseInt(currentMedia.responses_count) || 0;
1653          responsesBtn.querySelector('.action-icon').textContent = count > 0 ? '💬 ' + count : '💬';
1654          responsesBtn.style.display = '';
1655        }
1656      } else {
1657        fixedActions.style.display = 'none';
1658      }
1659    }
1660
1661    _handleFixedResponsesClick() {
1662      const wrapper = this.reelsWrapper;
1663      const items   = wrapper.querySelectorAll('.reel-item');
1664      const current = items[this._currentReelIndex];
1665      const oi      = current?.dataset?.originalIndex;
1666      const media   = oi !== undefined ? this._reelsMedia[parseInt(oi)] : null;
1667      if (media) this.showResponsesFeed(media.id, media.title);
1668    }
1669
1670    _handleFixedSubmitClick() {
1671      const wrapper = this.reelsWrapper;
1672      const items   = wrapper.querySelectorAll('.reel-item');
1673      const current = items[this._currentReelIndex];
1674      const oi      = current?.dataset?.originalIndex;
1675      const media   = oi !== undefined ? this._reelsMedia[parseInt(oi)] : null;
1676      if (media) this.openSubmitModal(media.id, media.title);
1677    }
1678
1679    async _loadResponseFeedForMedia(responseId) {
1680      try {
1681        const res  = await fetch(`${window.API_URL || '/api/v2'}/get-response-info?id=${encodeURIComponent(responseId)}`);
1682        const data = await res.json();
1683        if (data.success && data.response) {
1684          if (!data.response.parent_id) { this.scrollToMedia(responseId); return; }
1685          const rootId    = data.root_info?.id || data.parent_info?.id || data.response.parent_id;
1686          const rootTitle = data.root_info?.title || data.parent_info?.title || data.response.title || 'Vidéo';
1687          if (rootId) {
1688            await this.showResponsesFeed(rootId, rootTitle);
1689            setTimeout(() => this.scrollToMedia(responseId), 300);
1690          } else {
1691            this.scrollToMedia(responseId);
1692          }
1693        } else {
1694          this.scrollToMedia(responseId);
1695        }
1696      } catch (e) {
1697        console.error('Error loading response feed:', e);
1698        this.scrollToMedia(responseId);
1699      }
1700    }
1701
1702    // ── Expose globals (backward compat) ───────────────────────
1703    _exposeGlobals() {
1704      const self = this;
1705      window.initReelsMode           = (m)        => self.initReelsMode(m);
1706      window.enterReelsMode          = ()          => self.enterReelsMode();
1707      window.exitReelsMode           = ()          => self.exitReelsMode();
1708      window.renderReels             = ()          => self.renderReels();
1709      window.onReelsScroll           = ()          => self.onReelsScroll();
1710      window.toggleReelVideo         = (c)         => self.toggleReelVideo(c);
1711      window.autoplayVisibleReelVideo = ()         => self.autoplayVisibleReelVideo();
1712      window.updateGalleryCounter    = (el)        => self.updateGalleryCounter(el);
1713      window.scrollToMedia           = (id)        => self.scrollToMedia(id);
1714      window.copyReelLink            = ()          => self.copyReelLink();
1715      window.shareReel               = (i)         => self.shareReel(i);
1716      window.navigateToTag           = (slug)      => self.navigateToTag(slug);
1717      window.filterByPlatform        = (p)         => self.filterByPlatform(p);
1718      window.showResponsesFeed       = (id, t)     => self.showResponsesFeed(id, t);
1719      window.exitResponsesFeed       = ()          => self.exitResponsesFeed();
1720      window.goToParentVideo         = (id)        => self.goToParentVideo(id);
1721      window.openSubmitModal         = (id, t)     => self.openSubmitModal(id, t);
1722      window.closeSubmitModal        = ()          => self.closeSubmitModal();
1723      window.handleVideoSelect       = (input)     => self.handleVideoSelect(input);
1724      window.removeSelectedVideo     = ()          => self.removeSelectedVideo();
1725      window.openResponseTerms       = (ro)        => self.openResponseTerms(ro);
1726      window.closeResponseTerms      = ()          => self.closeResponseTerms();
1727      window.acceptResponseTerms     = ()          => self._acceptResponseTerms();
1728      window.submitResponse          = (e)         => self._submitResponse(e);
1729      // isReelsMode getter
1730      Object.defineProperty(window, 'isReelsMode', {
1731        get: () => self._isReelsMode,
1732        configurable: true
1733      });
1734      Object.defineProperty(window, 'isResponsesFeed', {
1735        get: () => self._isResponsesFeed,
1736        configurable: true
1737      });
1738      // reelsMedia + currentReelIndex — requis par nmc-media-grid pour ouvrir un reel au tap
1739      Object.defineProperty(window, 'reelsMedia', {
1740        get: () => self._reelsMedia,
1741        configurable: true
1742      });
1743      Object.defineProperty(window, 'currentReelIndex', {
1744        get: () => self._currentReelIndex,
1745        set: (v) => { self._currentReelIndex = v; },
1746        configurable: true
1747      });
1748    }
1749  }
1750
1751  if (!customElements.get('nmc-reels-player')) {
1752    customElements.define('nmc-reels-player', NmcReelsPlayer);
1753  }
1754})();

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.