PageSourceSearch

https://www.kosher.com/wp-content/plugins/kosher-stories/assets/stories.js?ver=1790277022

js kosher.com collected 2026-10-02 07:42:09 UTC 69,084 bytes, 2,246 lines download raw bytes

1document.addEventListener('DOMContentLoaded', function () {
2
3    const console = createDebugConsole(window.console);
4
5
6    const viewer = document.getElementById('kosher-stories-viewer');
7    const content = document.querySelector('.kosher-stories-content');
8    const storyBody = document.querySelector('.kosher-stories-body');
9    const close = document.querySelector('.kosher-close');
10
11    let currentCategoryIndex = 0;
12    const categories = Array.from(document.querySelectorAll('.kosher-story-thumb'));
13
14    let currentTermId = null;
15    let storyMuted = false;
16    const analyticsSessionId = getAnalyticsSessionId();
17    let analyticsQueue = [];
18    let analyticsFlushTimer = null;
19
20    if (!viewer || !content || !storyBody) return;
21
22    function createDebugConsole(nativeConsole) {
23        let enabled = false;
24
25        try {
26            enabled = localStorage.getItem('kayco_stories_debug') === '1';
27        } catch (error) {
28            enabled = false;
29        }
30
31        return {
32            log: function () {
33                if (enabled && nativeConsole && nativeConsole.log) {
34                    nativeConsole.log.apply(nativeConsole, arguments);
35                }
36            },
37            warn: function () {
38                if (enabled && nativeConsole && nativeConsole.warn) {
39                    nativeConsole.warn.apply(nativeConsole, arguments);
40                }
41            },
42            error: function () {
43                if (enabled && nativeConsole && nativeConsole.error) {
44                    nativeConsole.error.apply(nativeConsole, arguments);
45                }
46            }
47        };
48    }
49
50    function getAnalyticsSessionId() {
51        const key = 'kayco_story_session_id';
52        let sessionId = sessionStorage.getItem(key);
53
54        if (!sessionId) {
55            sessionId = 'story_' + Date.now().toString(36) + '_' + Math.random().toString(36).slice(2);
56            sessionStorage.setItem(key, sessionId);
57        }
58
59        return sessionId;
60    }
61
62    function queueStoryEvent(event) {
63        analyticsQueue.push({
64            sessionId: analyticsSessionId,
65            timestamp: Date.now(),
66            ...event
67        });
68
69        if (analyticsQueue.length >= 12) {
70            flushStoryEvents();
71            return;
72        }
73
74        clearTimeout(analyticsFlushTimer);
75        analyticsFlushTimer = setTimeout(flushStoryEvents, 1200);
76    }
77
78    function flushStoryEvents(useBeacon = false) {
79        if (!analyticsQueue.length) {
80            return;
81        }
82
83        const events = analyticsQueue.splice(0, analyticsQueue.length);
84        const body = new URLSearchParams({
85            action: 'kayco_track_story_events',
86            events: JSON.stringify(events)
87        });
88
89        if (useBeacon && navigator.sendBeacon) {
90            navigator.sendBeacon(kosherStories.ajax_url, body);
91            return;
92        }
93
94        fetch(kosherStories.ajax_url, {
95            method: 'POST',
96            headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
97            body
98        }).catch(() => {
99            analyticsQueue = events.concat(analyticsQueue).slice(0, 40);
100        });
101    }
102
103    function queueDropoffFromState() {
104        const storyState = storyBody._kaycoStoryState;
105
106        if (!storyState || storyState.completed) {
107            return;
108        }
109
110        const postId = storyState.postIds[storyState.current];
111        if (!postId) {
112            return;
113        }
114
115        queueStoryEvent({
116            eventType: 'dropoff',
117            postId,
118            termId: storyState.termId,
119            slideIndex: storyState.current,
120            totalSlides: storyState.totalSlides,
121            duration: storyState.activeDuration || 0
122        });
123
124        storyState.completed = true;
125        flushStoryEvents(true);
126    }
127
128    function getDefaultThumbSrc() {
129        return kosherStories.default_thumb_url || '';
130    }
131
132    function getStoryThumbSrc(story) {
133        if (story && story.img) {
134            return story.img;
135        }
136
137        return getDefaultThumbSrc();
138    }
139
140    function resetAllVideos() {
141    document.querySelectorAll('#kosher-stories video').forEach(video => {
142        video.pause();
143        video.currentTime = 0;
144    });
145}
146
147function stopAllStoryMedia() {
148
149    // 🔴 STOP ALL VIDEOS (both normal + bg)
150    document.querySelectorAll('#kosher-stories-viewer video').forEach(video => {
151        video.onloadedmetadata = null;
152        video.onloadeddata = null;
153        video.pause();
154        video.currentTime = 0;
155
156        video.querySelectorAll('source').forEach(source => {
157            source.removeAttribute('src');
158        });
159
160        video.removeAttribute('src');
161        video.load();
162    });
163
164    // ⏱️ CLEAR ALL TIMERS (VERY IMPORTANT)
165    document.querySelectorAll('.kosher-stories-content, .kosher-stories-body').forEach(wrapper => {
166        if (wrapper._timer) {
167            clearTimeout(wrapper._timer);
168            wrapper._timer = null;
169        }
170    });
171
172}
173
174function playActiveVideo(activeSlide) {
175
176    const video = activeSlide.querySelector('video');
177
178    if (!video) return;
179
180    video.currentTime = 0;
181
182    const playPromise = video.play();
183
184    if (playPromise !== undefined) {
185        playPromise.catch(err => {
186            console.warn('Autoplay blocked:', err);
187        });
188    }
189}
190
191    // =========================
192    // 📊 TRACK VIEW
193    // =========================
194    function trackView(postId, termId, duration) {
195        fetch(kosherStories.ajax_url, {
196            method: 'POST',
197            headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
198            body: new URLSearchParams({
199                action: 'kosher_track_story_view',
200                post_id: postId,
201                term_id: termId,
202                duration: duration
203            })
204        });
205    }
206
207    function getPollConfig() {
208        const currentConfig = window.KosherPollsPluginConfig || window.kosherPolls || window.kaycoPolls || {};
209
210        return {
211            ajaxUrl: currentConfig.ajaxUrl || kosherStories.ajax_url,
212            nonce: currentConfig.nonce || kosherStories.poll_nonce || '',
213            messages: currentConfig.messages || {},
214            action: window.KosherPollsPluginConfig ? 'kosher_submit_poll_vote' : 'kayco_submit_poll_vote'
215        };
216    }
217
218    function formatPollNumber(value) {
219        return new Intl.NumberFormat().format(value || 0);
220    }
221
222    function createPollVoterMarkup(voter) {
223        return `
224            <div class="kayco-poll-voter">
225                ${voter.avatar ? `<img src="${voter.avatar}" alt="${voter.name}" loading="lazy" />` : ''}
226                <div class="kayco-poll-voter__copy">
227                    <strong>${voter.name}</strong>
228                    ${voter.voted_at ? `<small>${voter.voted_at}</small>` : ''}
229                </div>
230            </div>
231        `;
232    }
233
234    function createPollAvatarMarkup(voter) {
235        return voter.avatar ? `<img src="${voter.avatar}" alt="${voter.name}" loading="lazy" />` : '';
236    }
237
238    function initStoryPollCards(scope) {
239        if (!scope) {
240            return;
241        }
242
243        scope.querySelectorAll('.story-poll .kayco-poll-card').forEach((card) => {
244            if (card.dataset.storyPollBound === 'true') {
245                card.classList.add('is-visible');
246                return;
247            }
248
249            card.dataset.storyPollBound = 'true';
250            card.classList.add('is-visible');
251            attachStoryPollHandlers(card);
252            syncStoryPollSelection(card);
253        });
254    }
255
256    function storyPollCanVote(card) {
257        if (card.dataset.canVote) {
258            return card.dataset.canVote === 'true';
259        }
260
261        return !!card.querySelector('.js-kayco-poll-submit') || !!card.querySelector('.js-kayco-poll-form input[type="radio"]:not([disabled])');
262    }
263
264    function attachStoryPollHandlers(card) {
265        const form = card.querySelector('.js-kayco-poll-form');
266        const toggle = card.querySelector('.js-kayco-poll-view-votes');
267        let isSavingChange = false;
268
269        if (toggle) {
270            toggle.addEventListener('click', function (event) {
271                event.preventDefault();
272                event.stopPropagation();
273
274                const isOpen = toggle.classList.contains('is-open');
275
276                card.querySelectorAll('.kayco-poll-option__voters').forEach((voters) => {
277                    voters.toggleAttribute('hidden', isOpen);
278                });
279
280                toggle.classList.toggle('is-open', !isOpen);
281
282                const label = toggle.querySelector('span');
283                if (label) {
284                    label.textContent = isOpen ? 'View votes' : 'Hide votes';
285                }
286            });
287        }
288
289        card.querySelectorAll('input[type="radio"]').forEach((input) => {
290            input.addEventListener('change', async function () {
291                syncStoryPollSelection(card);
292
293                if (card.dataset.hasVoted !== 'true' || isSavingChange || !form || !storyPollCanVote(card)) {
294                    return;
295                }
296
297                const checked = form.querySelector('input[type="radio"]:checked');
298                const message = card.querySelector('.js-kayco-poll-message');
299                const previousVote = card.dataset.userVote || '';
300
301                if (!checked || checked.value === previousVote) {
302                    return;
303                }
304
305                isSavingChange = true;
306
307                if (message) {
308                    message.textContent = getPollConfig().messages.loading || 'Saving your vote...';
309                }
310
311                card.querySelectorAll('input[type="radio"]').forEach((field) => {
312                    field.disabled = true;
313                });
314
315                try {
316                    const result = await submitStoryPollVote(card, checked.value);
317
318                    if (result.success && result.data && result.data.payload) {
319                        applyStoryPollPayload(card, result.data.payload, result.data.message || getPollConfig().messages.updated);
320                        return;
321                    }
322
323                    if (result.data && result.data.payload) {
324                        applyStoryPollPayload(card, result.data.payload, result.data.message || getPollConfig().messages.error);
325                    } else if (message) {
326                        message.textContent = (result.data && result.data.message) || getPollConfig().messages.error || 'Something went wrong while saving your vote.';
327                    }
328                } catch (error) {
329                    if (message) {
330                        message.textContent = getPollConfig().messages.error || 'Something went wrong while saving your vote.';
331                    }
332                } finally {
333                    isSavingChange = false;
334                }
335            });
336        });
337
338        if (!form) {
339            return;
340        }
341
342        form.addEventListener('submit', async function (event) {
343            event.preventDefault();
344            event.stopPropagation();
345
346            const checked = form.querySelector('input[type="radio"]:checked');
347            const submit = form.querySelector('.js-kayco-poll-submit');
348            const message = card.querySelector('.js-kayco-poll-message');
349
350            if (!checked || !submit || submit.disabled) {
351                return;
352            }
353
354            submit.disabled = true;
355            submit.classList.add('is-loading');
356
357            if (message) {
358                message.textContent = getPollConfig().messages.loading || 'Saving your vote...';
359            }
360
361            try {
362                const result = await submitStoryPollVote(card, checked.value);
363
364                if (result.success && result.data && result.data.payload) {
365                    applyStoryPollPayload(card, result.data.payload, result.data.message);
366                    return;
367                }
368
369                if (result.data && result.data.payload) {
370                    applyStoryPollPayload(card, result.data.payload, result.data.message || getPollConfig().messages.error);
371                } else if (message) {
372                    message.textContent = (result.data && result.data.message) || getPollConfig().messages.error || 'Something went wrong while saving your vote.';
373                }
374            } catch (error) {
375                if (message) {
376                    message.textContent = getPollConfig().messages.error || 'Something went wrong while saving your vote.';
377                }
378            } finally {
379                submit.classList.remove('is-loading');
380                syncStoryPollSelection(card);
381            }
382        });
383    }
384
385    function syncStoryPollSelection(card) {
386        const checked = card.querySelector('input[type="radio"]:checked');
387        const submit = card.querySelector('.js-kayco-poll-submit');
388
389        card.querySelectorAll('.kayco-poll-option').forEach((option) => {
390            const isSelected = !!checked && option.dataset.optionId === checked.value;
391            option.classList.toggle('is-selected', isSelected);
392            option.dataset.isUserVote = isSelected ? 'true' : 'false';
393
394            const status = option.querySelector('.js-kayco-poll-user-status');
395            if (status) {
396                status.hidden = !isSelected || card.dataset.hasVoted !== 'true';
397            }
398        });
399
400        if (submit) {
401            const label = submit.querySelector('span');
402
403            if (label) {
404                label.textContent = 'Submit Vote';
405            }
406
407            submit.hidden = card.dataset.hasVoted === 'true';
408            submit.disabled = card.dataset.hasVoted === 'true' || !checked || !storyPollCanVote(card);
409        }
410    }
411
412    function applyStoryPollPayload(card, payload, message) {
413        card.dataset.userVote = payload.userVote || '';
414        card.dataset.hasVoted = payload.hasVoted ? 'true' : 'false';
415        card.dataset.canVote = payload.canVote ? 'true' : 'false';
416        card.classList.add('has-voted');
417
418        const total = card.querySelector('.js-kayco-poll-total');
419        if (total) {
420            total.textContent = formatPollNumber(payload.totalVotes || 0);
421        }
422
423        const messageNode = card.querySelector('.js-kayco-poll-message');
424        if (messageNode) {
425            messageNode.textContent = message || getPollConfig().messages.updated || getPollConfig().messages.success || 'Your vote is saved.';
426        }
427
428        const submit = card.querySelector('.js-kayco-poll-submit');
429        if (submit) {
430            submit.classList.remove('is-loading');
431            submit.hidden = !payload.canVote || payload.hasVoted;
432        }
433
434        const viewVotes = card.querySelector('.js-kayco-poll-view-votes');
435        if (viewVotes) {
436            viewVotes.hidden = false;
437            viewVotes.classList.remove('is-open');
438            const label = viewVotes.querySelector('span');
439            if (label) {
440                label.textContent = 'View votes';
441            }
442        }
443
444        (payload.options || []).forEach((optionData) => {
445            const option = card.querySelector(`.kayco-poll-option[data-option-id="${optionData.id}"]`);
446
447            if (!option) {
448                return;
449            }
450
451            const input = option.querySelector('input[type="radio"]');
452            if (input) {
453                input.checked = !!optionData.isUserVote;
454                input.disabled = !payload.canVote;
455            }
456
457            option.classList.toggle('is-selected', !!optionData.isUserVote);
458            option.dataset.isUserVote = optionData.isUserVote ? 'true' : 'false';
459
460            const status = option.querySelector('.js-kayco-poll-user-status');
461            if (status) {
462                status.hidden = !optionData.isUserVote;
463            }
464
465            const percentNode = option.querySelector('.js-kayco-poll-percent');
466            if (percentNode) {
467                percentNode.textContent = `${optionData.percent}%`;
468            }
469
470            const countLabel = option.querySelector('.js-kayco-poll-count-label');
471            if (countLabel) {
472                countLabel.textContent = optionData.countLabel || `${formatPollNumber(optionData.count || 0)} votes`;
473            }
474
475            const bar = option.querySelector('.js-kayco-poll-bar');
476            if (bar) {
477                bar.style.width = `${optionData.percent}%`;
478            }
479
480            const avatars = option.querySelector('.kayco-poll-option__avatars');
481            if (avatars) {
482                avatars.innerHTML = (optionData.avatarStack || []).map(createPollAvatarMarkup).join('');
483            }
484
485            const voters = option.querySelector('.kayco-poll-option__voters');
486            if (voters) {
487                voters.innerHTML = (optionData.voters || []).map(createPollVoterMarkup).join('');
488                voters.setAttribute('hidden', '');
489            }
490        });
491
492        syncStoryPollSelection(card);
493    }
494
495    function submitStoryPollVote(card, optionValue) {
496        const config = getPollConfig();
497        const body = new URLSearchParams({
498            action: config.action,
499            nonce: config.nonce,
500            poll_id: card.dataset.pollId || '',
501            poll_option: optionValue
502        });
503
504        return fetch(config.ajaxUrl, {
505            method: 'POST',
506            credentials: 'same-origin',
507            headers: {
508                'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8'
509            },
510            body: body.toString()
511        }).then((response) => response.json());
512    }
513
514    window.addEventListener('beforeunload', function () {
515        queueDropoffFromState();
516        flushStoryEvents(true);
517    });
518
519function updateThumbFromServer(termId) {
520
521    fetch(kosherStories.ajax_url, {
522        method: 'POST',
523        headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
524        body: new URLSearchParams({
525            action: 'kosher_get_story_thumb',
526            term_id: termId,
527            t: Date.now()
528        })
529    })
530    .then(res => res.json())
531    .then(data => {
532
533        if (!data.success) return;
534
535        const stories = Array.isArray(data.data) ? data.data : [];
536
537        if (!stories.length) {
538            return;
539        }
540
541        const seenData = getSeenData();
542        const seenPosts = (seenData[termId]?.posts || []).map(String);
543        const seenSet = new Set(seenPosts);
544
545        let target = null;
546
547        // 🔥 FIRST unseen
548        for (let story of stories) {
549            if (!seenSet.has(String(story.id))) {
550                target = story;
551                break;
552            }
553        }
554
555        // fallback → first story
556        if (!target) target = stories[0];
557
558        const thumb = document.querySelector(`.kosher-story-thumb[data-term="${termId}"]`);
559        if (!thumb) return;
560
561        const inner = thumb.querySelector('.thumb-inner');
562        if (!inner) return;
563
564        const thumbSrc = getStoryThumbSrc(target);
565        if (!thumbSrc) return;
566
567        const img = new Image();
568        img.src = thumbSrc;
569
570        img.onload = () => {
571            inner.innerHTML = '';
572            inner.appendChild(img);
573        };
574
575    });
576}
577
578    function preloadAllThumbs() {
579
580    categories.forEach(el => {
581
582        const termId = el.getAttribute('data-term');
583
584        fetch(kosherStories.ajax_url, {
585            method: 'POST',
586            headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
587            body: new URLSearchParams({
588                action: 'kosher_get_story_thumb',
589                term_id: termId
590            })
591        })
592        .then(res => res.json())
593        .then(data => {
594
595            if (!data.success) return;
596
597            const stories = Array.isArray(data.data) ? data.data : [];
598            const target = stories.find(story => story && story.img) || stories[0];
599            const thumbSrc = getStoryThumbSrc(target);
600
601            if (!target || !thumbSrc) return;
602
603            const inner = el.querySelector('.thumb-inner');
604            if (!inner) return;
605
606            inner.innerHTML = `<img src="${thumbSrc}" alt="">`;
607        });
608
609    });
610
611}
612
613    // =========================
614    // 👁️ STORAGE
615    // =========================
616    function getSeenData() {
617        return JSON.parse(localStorage.getItem('kosher_story_seen') || '{}');
618    }
619
620    function saveSeenData(data) {
621        localStorage.setItem('kosher_story_seen', JSON.stringify(data));
622    }
623
624    function markStorySeen(termId, postId) {
625        const data = getSeenData();
626
627        if (!data[termId]) {
628            data[termId] = { posts: [] };
629        }
630
631        if (!data[termId].posts.includes(String(postId))) {
632            data[termId].posts.push(String(postId));
633        }
634
635        saveSeenData(data);
636    }
637
638    function markCategorySeen(termId, postIds) {
639        const data = getSeenData();
640        data[termId] = { posts: postIds.map(id => String(id)) };
641        saveSeenData(data);
642        updateThumbUI(termId, true);
643    }
644
645    function updateThumbUI(termId, seen) {
646        const el = document.querySelector(`.kosher-story-thumb[data-term="${termId}"]`);
647        if (!el) return;
648        seen ? el.classList.add('seen') : el.classList.remove('seen');
649    }
650
651    function checkSeenState(termId, postIds) {
652
653        const data = getSeenData();
654
655        if (!data[termId]) {
656            updateThumbUI(termId, false);
657            return;
658        }
659
660        const seenPosts = (data[termId].posts || []).map(String);
661        const incoming = postIds.map(id => String(id));
662
663        const seenSet = new Set(seenPosts);
664
665        const hasUnseen = incoming.some(id => !seenSet.has(id));
666
667        if (hasUnseen) {
668            updateThumbUI(termId, false);
669        } else {
670            updateThumbUI(termId, true);
671        }
672    }
673
674    function initAllThumbsSeenState() {
675        const data = getSeenData();
676
677        categories.forEach(el => {
678            const termId = el.getAttribute('data-term');
679            if (!data[termId]) return;
680            updateThumbUI(termId, true);
681        });
682    }
683
684    initAllThumbsSeenState();
685    preloadAllThumbs();
686    refreshStoriesState();
687
688    // =========================
689    // 🔥 START INDEX LOGIC
690    // =========================
691    function getStartIndex(postIds, termId) {
692
693        const data = getSeenData();
694
695        if (!data[termId]) return 0;
696
697        const seenPosts = (data[termId].posts || []).map(String);
698        const seenSet = new Set(seenPosts);
699
700        // 🔥 FIRST unseen
701        for (let i = 0; i < postIds.length; i++) {
702            if (!seenSet.has(String(postIds[i]))) {
703                return i; // 👈 THIS WILL BE 10
704            }
705        }
706
707        return 0;
708    }
709
710    // =========================
711    // 🔥 LIVE REFRESH
712    // =========================
713function refreshStoriesState() {
714
715    // 🚫 DO NOT OVERRIDE while viewer is open
716    if (!viewer.classList.contains('hidden')) {
717        return;
718    }
719
720    categories.forEach(el => {
721
722        const termId = el.getAttribute('data-term');
723
724        fetch(kosherStories.ajax_url, {
725            method: 'POST',
726            headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
727            body: new URLSearchParams({
728                action: 'kosher_get_story_ids',
729                term_id: termId
730            })
731        })
732        .then(res => res.json())
733        .then(data => {
734
735            if (!data.success) return;
736
737            const postIds = data.data.post_ids;
738
739            const dataSeen = getSeenData();
740            const seenPosts = (dataSeen[termId]?.posts || []).map(String);
741
742            const seenSet = new Set(seenPosts);
743
744            const hasUnseen = postIds.some(id => !seenSet.has(String(id)));
745
746            updateThumbUI(termId, !hasUnseen);
747
748            // ✅ ONLY update server thumb when viewer CLOSED
749            updateThumbFromServer(termId);
750
751        });
752
753    });
754}
755
756    setInterval(refreshStoriesState, 4000);
757
758    // =========================
759    // 🖼️ THUMB UPDATE (NEW)
760    // =========================
761function updateThumbFromSlides(termId) {
762
763
764    // ⏳ Ensure DOM is ready (important after AJAX render)
765    requestAnimationFrame(() => {
766
767        const wrapper = document.querySelector('.kosher-stories-body');
768
769        if (!wrapper) {
770            console.error('❌ NO .kosher-stories-content FOUND');
771            return;
772        }
773
774        const firstSlide = wrapper.querySelector('.slide');
775
776        if (!firstSlide) {
777            console.error('❌ NO .slide FOUND');
778            return;
779        }
780
781
782        let imgSrc = null;
783
784        // =========================
785        // 🖼️ 1. DIRECT IMAGE
786        // =========================
787        const img = firstSlide.querySelector('img');
788
789        if (img && img.src) {
790            imgSrc = img.src;
791        }
792
793        // =========================
794        // 🎬 2. VIDEO POSTER
795        // =========================
796        if (!imgSrc) {
797
798            const video = firstSlide.querySelector('video');
799
800            if (video) {
801
802                const poster = video.getAttribute('poster');
803
804                if (poster) {
805                    imgSrc = poster;
806                } else {
807                    console.warn('⚠️ Video has NO poster');
808                }
809            }
810        }
811
812        // =========================
813        // 🖼️ 3. BACKGROUND IMAGE (YOUR CASE)
814        // =========================
815        if (!imgSrc) {
816
817            const bgImg = firstSlide.querySelector('.story-background--image img');
818
819            if (bgImg && bgImg.src) {
820                imgSrc = bgImg.src;
821            }
822        }
823
824        // =========================
825        // 🎬 4. VIDEO → TRY FRAME (OPTIONAL SAFE FALLBACK)
826        // =========================
827        if (!imgSrc) {
828
829            const video = firstSlide.querySelector('video');
830
831            if (video && video.src) {
832
833                console.warn('⚠️ No poster → fallback to video frame');
834
835                try {
836                    const canvas = document.createElement('canvas');
837                    const ctx = canvas.getContext('2d');
838
839                    canvas.width = 300;
840                    canvas.height = 200;
841
842                    video.currentTime = 0.1;
843
844                    video.addEventListener('loadeddata', function captureFrame() {
845
846                        ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
847
848                        imgSrc = canvas.toDataURL('image/jpeg');
849
850                        applyThumb(imgSrc);
851
852                        video.removeEventListener('loadeddata', captureFrame);
853
854                    });
855
856                    return; // stop normal flow (async)
857                } catch (e) {
858                    console.error('❌ Failed to generate video frame', e);
859                }
860            }
861        }
862
863        // =========================
864        // ❌ FINAL FALLBACK
865        // =========================
866        if (!imgSrc) {
867            console.warn('❌ No thumbnail source found → using default icon');
868            imgSrc = getDefaultThumbSrc();
869        }
870
871        if (imgSrc) {
872            applyThumb(imgSrc);
873        }
874
875        // =========================
876        // 🎯 APPLY THUMB
877        // =========================
878        function applyThumb(src) {
879
880
881            const wrappers = document.querySelectorAll('.kosher-story-thumb');
882
883
884            let found = false;
885
886            wrappers.forEach(wrapper => {
887
888                const dataTerm = wrapper.getAttribute('data-term');
889
890
891                if (String(dataTerm) === String(termId)) {
892
893                    const inner = wrapper.querySelector('.thumb-inner');
894
895                    if (!inner) {
896                        console.error('❌ .thumb-inner missing inside:', wrapper);
897                        return;
898                    }
899
900                    inner.innerHTML = `<img src="${src}" alt="">`;
901
902
903                    found = true;
904                }
905            });
906
907            if (!found) {
908                console.error('❌ NO MATCHING THUMB FOUND FOR termId:', termId);
909            }
910        }
911
912    });
913}
914
915    function parsePostIds(value) {
916        try {
917            const ids = JSON.parse(value || '[]');
918            return Array.isArray(ids) ? ids : [];
919        } catch (error) {
920            return [];
921        }
922    }
923
924    function stopStoryWrapper(wrapper, resetSlides = false) {
925        if (!wrapper) {
926            return;
927        }
928
929        if (wrapper._timer) {
930            clearTimeout(wrapper._timer);
931            wrapper._timer = null;
932        }
933
934        // Invalidate delayed play retries created by the story engine. Without
935        // this, a retry can restart a video after its carousel card lost focus.
936        wrapper._kaycoPlaybackGeneration = (wrapper._kaycoPlaybackGeneration || 0) + 1;
937
938        wrapper.querySelectorAll('video').forEach(video => {
939            video.onloadedmetadata = null;
940            video.onloadeddata = null;
941
942            video.pause();
943            video.currentTime = 0;
944        });
945
946        if (!resetSlides) {
947            return;
948        }
949
950        wrapper.querySelectorAll('.slide').forEach((slide, index) => {
951            slide.style.display = index === 0 ? 'block' : 'none';
952        });
953
954        wrapper.querySelectorAll('.bar').forEach((bar) => {
955            const span = bar.querySelector('span');
956            bar.classList.remove('seen');
957
958            if (span) {
959                span.style.animation = 'none';
960                span.style.transform = 'scaleX(0)';
961            }
962        });
963    }
964
965    function loadStoriesCarousel(startIndex = 0) {
966        const startThumb = categories[startIndex];
967        const startTermId = startThumb ? startThumb.getAttribute('data-term') : '';
968
969        fetch(kosherStories.ajax_url, {
970            method: 'POST',
971            headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
972            body: new URLSearchParams({
973                action: 'kosher_get_all_stories_carousel',
974                start_term_id: startTermId
975            })
976        })
977        .then(res => res.json())
978        .then(data => {
979            if (!data.success) {
980                loadCategory(startIndex);
981                return;
982            }
983
984            storyBody.innerHTML = data.data.html;
985            viewer.classList.add('is-reels-carousel');
986            viewer.classList.remove('hidden');
987            initReelsCarousel(storyBody, data.data.start_index || 0);
988        })
989        .catch(() => loadCategory(startIndex));
990    }
991
992    function initReelsCarousel(scope, initialIndex = 0, options = {}) {
993        const track = scope.querySelector('.kosher-reels__track');
994        const shell = scope.querySelector('.kosher-reels') || scope;
995        const originalCards = Array.from(scope.querySelectorAll('.kosher-reel-card:not([data-reel-clone])'));
996        const prev = scope.querySelector('.kosher-reels__nav--prev');
997        const next = scope.querySelector('.kosher-reels__nav--next');
998
999        if (!track || !originalCards.length) {
1000            return;
1001        }
1002
1003        if (originalCards.length > 1 && track.dataset.reelsLoopReady !== 'true') {
1004            originalCards.forEach((card) => {
1005                const clone = card.cloneNode(true);
1006                clone.dataset.reelClone = 'after';
1007                track.appendChild(clone);
1008            });
1009
1010            originalCards.slice().reverse().forEach((card) => {
1011                const clone = card.cloneNode(true);
1012                clone.dataset.reelClone = 'before';
1013                track.insertBefore(clone, track.firstChild);
1014            });
1015
1016            track.dataset.reelsLoopReady = 'true';
1017        }
1018
1019        const cards = Array.from(scope.querySelectorAll('.kosher-reel-card'));
1020        const originalCount = originalCards.length;
1021        const middleOffset = originalCount > 1 ? originalCount : 0;
1022
1023        let activeIndex = Math.max(0, Math.min(originalCount - 1, initialIndex));
1024        let activeVisualIndex = middleOffset + activeIndex;
1025        let activeStarted = false;
1026        let wheelLocked = false;
1027        let touchStartX = null;
1028        let touchStartY = null;
1029        let touchLastX = null;
1030        let touchDragging = false;
1031        let touchBaseTranslateX = 0;
1032        let trackTranslateX = 0;
1033
1034        function syncButtons() {
1035            if (prev) {
1036                prev.disabled = originalCount <= 1;
1037            }
1038
1039            if (next) {
1040                next.disabled = originalCount <= 1;
1041            }
1042        }
1043
1044        function normalizeIndex(index) {
1045            if (!originalCount) {
1046                return 0;
1047            }
1048
1049            return (index % originalCount + originalCount) % originalCount;
1050        }
1051
1052        function getCenteredTrackTranslate() {
1053            const activeCard = cards[activeVisualIndex];
1054            const viewport = scope.querySelector('.kosher-reels__viewport') || scope;
1055            if (!activeCard || !viewport) {
1056                return trackTranslateX;
1057            }
1058
1059            const viewportWidth = viewport.clientWidth || viewport.getBoundingClientRect().width;
1060            const cardWidth = activeCard.offsetWidth || activeCard.getBoundingClientRect().width;
1061            const cardLeft = activeCard.offsetLeft;
1062
1063            return (viewportWidth / 2) - (cardLeft + (cardWidth / 2));
1064        }
1065
1066        function centerActiveCard(animate = true) {
1067            trackTranslateX = getCenteredTrackTranslate();
1068
1069            track.style.transition = animate ? '' : 'none';
1070            track.style.transform = `translate3d(${trackTranslateX}px, 0, 0)`;
1071
1072            if (!animate) {
1073                void track.offsetWidth;
1074                track.style.transition = '';
1075            }
1076        }
1077
1078        function activateCard(index, initialSlideIndex = null, shouldStart = false, allowMutedFallback = false) {
1079            activeIndex = normalizeIndex(index);
1080            activeVisualIndex = middleOffset + activeIndex;
1081            activeStarted = shouldStart;
1082            shell.classList.toggle('is-playing', activeStarted);
1083
1084            cards.forEach((card, cardIndex) => {
1085                const dailyStories = card.querySelector('.daily-stories');
1086                const isActive = cardIndex === activeVisualIndex;
1087
1088                card.classList.toggle('is-active', isActive);
1089                card.setAttribute('aria-hidden', isActive ? 'false' : 'true');
1090                stopStoryWrapper(dailyStories, true);
1091            });
1092
1093            centerActiveCard();
1094            syncButtons();
1095
1096            if (!shouldStart) {
1097                return;
1098            }
1099
1100            startActiveCard(initialSlideIndex, allowMutedFallback);
1101        }
1102
1103        function startActiveCard(initialSlideIndex = null, allowMutedFallback = false) {
1104            const activeCard = cards[activeVisualIndex];
1105            const dailyStories = activeCard.querySelector('.daily-stories');
1106            const termId = activeCard.getAttribute('data-term');
1107            const postIds = parsePostIds(activeCard.getAttribute('data-post-ids'));
1108
1109
1110            currentCategoryIndex = categories.findIndex(category => category.getAttribute('data-term') === termId);
1111            currentTermId = termId;
1112
1113            if (postIds.length) {
1114                checkSeenState(termId, postIds);
1115            }
1116
1117            initStoryEngine(dailyStories, postIds, termId, initialSlideIndex, {
1118                carousel: true,
1119                allowMutedFallback,
1120                onComplete: function () {
1121                    markCategorySeen(termId, postIds);
1122                    // moveCarousel(1, null, true);
1123                },
1124                onPreviousCategory: function () {
1125                    moveCarousel(-1, 'last', true);
1126                }
1127            });
1128
1129            activeStarted = true;
1130            shell.classList.add('is-playing');
1131        }
1132
1133        function moveCarousel(direction, initialSlideIndex = null, allowMutedFallback = false) {
1134
1135
1136            if (originalCount <= 1) {
1137                startActiveCard(null, allowMutedFallback);
1138                return;
1139            }
1140
1141            activeVisualIndex += direction;
1142            activeIndex = normalizeIndex(activeVisualIndex - middleOffset);
1143            activeStarted = true;
1144            shell.classList.add('is-playing');
1145
1146            cards.forEach((card, cardIndex) => {
1147                const dailyStories = card.querySelector('.daily-stories');
1148                const isActive = cardIndex === activeVisualIndex;
1149
1150                card.classList.toggle('is-active', isActive);
1151                card.setAttribute('aria-hidden', isActive ? 'false' : 'true');
1152                stopStoryWrapper(dailyStories, true);
1153            });
1154
1155            centerActiveCard();
1156            syncButtons();
1157
1158            const needsSnap = activeVisualIndex < middleOffset || activeVisualIndex >= middleOffset + originalCount;
1159
1160            if (!needsSnap) {
1161                startActiveCard(initialSlideIndex, allowMutedFallback);
1162                return;
1163            }
1164
1165            startActiveCard(initialSlideIndex, allowMutedFallback);
1166
1167            setTimeout(() => {
1168                activeVisualIndex = middleOffset + activeIndex;
1169
1170                cards.forEach((card, cardIndex) => {
1171                    const dailyStories = card.querySelector('.daily-stories');
1172                    const isActive = cardIndex === activeVisualIndex;
1173
1174                    card.classList.toggle('is-active', isActive);
1175                    card.setAttribute('aria-hidden', isActive ? 'false' : 'true');
1176                    stopStoryWrapper(dailyStories, true);
1177                });
1178
1179                centerActiveCard(false);
1180                startActiveCard(initialSlideIndex, allowMutedFallback);
1181            }, 390);
1182
1183        }
1184
1185        cards.forEach((card, index) => {
1186            card.addEventListener('click', function (event) {
1187                if (
1188                    event.target.closest('.prev-slide') ||
1189                    event.target.closest('.next-slide') ||
1190                    event.target.closest('.story-poll') ||
1191                    event.target.closest('.overlay a')
1192                ) {
1193                    return;
1194                }
1195
1196                if (event._kaycoStoryHandled) {
1197                    return;
1198                }
1199
1200                if (index === activeVisualIndex) {
1201                    if (!activeStarted) {
1202                        startActiveCard();
1203                        return;
1204                    }
1205
1206                    const dailyStories = card.querySelector('.daily-stories');
1207                    if (
1208                        dailyStories &&
1209                        dailyStories._kaycoStoryControls &&
1210                        dailyStories._kaycoStoryControls.hasActiveVideo()
1211                    ) {
1212                        dailyStories._kaycoStoryControls.toggleVideoPlayback();
1213                    }
1214                    return;
1215                }
1216
1217                activateCard(normalizeIndex(index - middleOffset), null, true);
1218            });
1219        });
1220
1221        if (prev) {
1222            prev.addEventListener('click', function () {
1223
1224                moveCarousel(-1, 'last');
1225            });
1226        }
1227
1228        if (next) {
1229            next.addEventListener('click', function () {
1230
1231                moveCarousel(1);
1232            });
1233        }
1234
1235        scope.addEventListener('wheel', function (event) {
1236            const horizontalIntent = Math.abs(event.deltaX) > Math.abs(event.deltaY) && Math.abs(event.deltaX) >= 24;
1237
1238            if (wheelLocked || !horizontalIntent) {
1239                return;
1240            }
1241
1242            event.preventDefault();
1243            wheelLocked = true;
1244            moveCarousel(event.deltaX > 0 ? 1 : -1, null, true);
1245            setTimeout(() => {
1246                wheelLocked = false;
1247            }, 420);
1248        }, { passive: false });
1249
1250        scope.addEventListener('touchstart', function (event) {
1251            if (event.touches.length !== 1) {
1252                touchStartX = null;
1253                touchStartY = null;
1254                touchLastX = null;
1255                touchDragging = false;
1256                return;
1257            }
1258
1259            touchStartX = event.touches[0].clientX;
1260            touchStartY = event.touches[0].clientY;
1261            touchLastX = touchStartX;
1262            touchBaseTranslateX = trackTranslateX;
1263            touchDragging = false;
1264        }, { passive: true });
1265
1266        scope.addEventListener('touchmove', function (event) {
1267            if (touchStartX === null || touchStartY === null || event.touches.length !== 1) {
1268                return;
1269            }
1270
1271            const touch = event.touches[0];
1272            const deltaX = touch.clientX - touchStartX;
1273            const deltaY = touch.clientY - touchStartY;
1274
1275            if (!touchDragging && Math.abs(deltaX) <= 8) {
1276                return;
1277            }
1278
1279            if (!touchDragging && Math.abs(deltaX) <= Math.abs(deltaY)) {
1280                return;
1281            }
1282
1283            event.preventDefault();
1284            touchDragging = true;
1285            touchLastX = touch.clientX;
1286            track.style.transition = 'none';
1287            trackTranslateX = touchBaseTranslateX + deltaX;
1288            track.style.transform = `translate3d(${trackTranslateX}px, 0, 0)`;
1289        }, { passive: false });
1290
1291        scope.addEventListener('touchend', function (event) {
1292            if (touchStartX === null || touchStartY === null) {
1293                return;
1294            }
1295
1296            const touch = event.changedTouches[0];
1297            const deltaX = (touchLastX !== null ? touchLastX : touch.clientX) - touchStartX;
1298            const deltaY = touch.clientY - touchStartY;
1299
1300            touchStartX = null;
1301            touchStartY = null;
1302            touchLastX = null;
1303
1304            if (!touchDragging || Math.abs(deltaX) < 36 || Math.abs(deltaX) <= Math.abs(deltaY)) {
1305                touchDragging = false;
1306                centerActiveCard(true);
1307                return;
1308            }
1309
1310            touchDragging = false;
1311            moveCarousel(deltaX < 0 ? 1 : -1, null, true);
1312        }, { passive: true });
1313
1314        if (scope._reelsResizeHandler) {
1315            window.removeEventListener('resize', scope._reelsResizeHandler);
1316        }
1317
1318        scope._reelsResizeHandler = centerActiveCard;
1319        window.addEventListener('resize', scope._reelsResizeHandler, { passive: true });
1320        activateCard(activeIndex, null, options.autoplay === true);
1321
1322        requestAnimationFrame(() => {
1323            centerActiveCard(false);
1324        });
1325    }
1326
1327    // =========================
1328    // LOAD CATEGORY
1329    // =========================
1330    function loadCategory(index, options = {}) {
1331
1332        if (!categories[index]) {
1333            viewer.classList.add('hidden');
1334            storyBody.innerHTML = '';
1335            return;
1336        }
1337
1338        currentCategoryIndex = index;
1339        const termId = categories[index].getAttribute('data-term');
1340        currentTermId = termId;
1341        viewer.classList.remove('is-reels-carousel');
1342
1343        fetch(kosherStories.ajax_url, {
1344            method: 'POST',
1345            headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
1346            body: new URLSearchParams({
1347                action: 'kosher_get_category_stories',
1348                term_id: termId
1349            })
1350        })
1351        .then(res => res.json())
1352.then(data => {
1353
1354    if (!data.success) {
1355        loadCategory(index + 1);
1356        return;
1357    }
1358
1359    storyBody.innerHTML = data.data.html;
1360    viewer.classList.remove('hidden');
1361
1362    // 🔥 FIXED TIMING
1363setTimeout(() => {
1364    updateThumbFromSlides(termId);
1365}, 50);
1366
1367    initStoryEngine(storyBody, data.data.post_ids, termId, options.initialSlideIndex);
1368
1369    if (data.data.post_ids) {
1370        checkSeenState(termId, data.data.post_ids);
1371    }
1372})
1373        .catch(() => loadCategory(index + 1));
1374    }
1375
1376    categories.forEach((el, i) => {
1377        el.addEventListener('click', () => loadStoriesCarousel(i));
1378    });
1379
1380    document.querySelectorAll('[data-kosher-stories-inline]').forEach((inlineCarousel) => {
1381        initReelsCarousel(inlineCarousel, 0, { autoplay: false });
1382    });
1383
1384    function closeViewer() {
1385        queueDropoffFromState();
1386        stopAllStoryMedia();
1387        if (storyBody._reelsResizeHandler) {
1388            window.removeEventListener('resize', storyBody._reelsResizeHandler);
1389            storyBody._reelsResizeHandler = null;
1390        }
1391        viewer.classList.add('hidden');
1392        viewer.classList.remove('is-reels-carousel');
1393        storyBody.innerHTML = '';
1394        refreshStoriesState();
1395    }
1396
1397    if (close) {
1398        close.addEventListener('click', closeViewer);
1399    }
1400
1401    document.addEventListener('keydown', e => {
1402        if (e.key === 'Escape' && !viewer.classList.contains('hidden')) {
1403            closeViewer();
1404        }
1405    });
1406
1407    // =========================
1408    // 🎬 STORY ENGINE
1409    // =========================
1410    window.initStoryEngine = function(wrapper, postIds, termId, initialSlideIndex = null, options = {}) {
1411
1412        if (wrapper._timer) {
1413            clearTimeout(wrapper._timer);
1414            wrapper._timer = null;
1415        }
1416
1417        const slides = wrapper.querySelectorAll('.slide');
1418        const bars = wrapper.querySelectorAll('.bar');
1419        const progressBars = wrapper.querySelector('.progress-bars');
1420        const prevArrow = wrapper.querySelector('.prev-slide');
1421        const nextArrow = wrapper.querySelector('.next-slide');
1422        const muteButton = wrapper.querySelector('.kosher-story-sound');
1423
1424        if (!slides.length) return;
1425
1426        let current = resolveInitialSlideIndex(postIds, termId, initialSlideIndex, slides.length);
1427        let isTransitioning = false;
1428        let isPaused = false;
1429        let remainingTime = 0;
1430        let activeDuration = 0;
1431        let slideStartedAt = 0;
1432        let preventClickAfterTouch = false;
1433        let touchStartX = null;
1434        let touchStartY = null;
1435        let touchStartTarget = null;
1436        const playbackGeneration = (wrapper._kaycoPlaybackGeneration || 0) + 1;
1437        wrapper._kaycoPlaybackGeneration = playbackGeneration;
1438        wrapper._kaycoStoryState = {
1439            termId,
1440            postIds,
1441            current,
1442            totalSlides: slides.length,
1443            activeDuration: 0,
1444            completed: false
1445        };
1446        storyBody._kaycoStoryState = wrapper._kaycoStoryState;
1447
1448function moveTo(index) {
1449
1450    slides.forEach((slide, i) => {
1451
1452        const video = slide.querySelector('video');
1453        const bgVideo = slide.querySelector('.story-bg-video');
1454
1455        // 🎯 HIDE/SHOW
1456        slide.style.display = i === index ? 'block' : 'none';
1457
1458        // Stop inactive slides only. The active slide is controlled by the play path.
1459        if (video && i !== index) {
1460
1461            video.pause();
1462            video.currentTime = 0;
1463        }
1464
1465        if (bgVideo && i !== index) {
1466
1467            bgVideo.pause();
1468            bgVideo.currentTime = 0;
1469        }
1470
1471        // Playback starts after the focused slide is visible.
1472    });
1473}
1474
1475        function clearStoryTimer() {
1476            if (wrapper._timer) {
1477                clearTimeout(wrapper._timer);
1478                wrapper._timer = null;
1479            }
1480        }
1481
1482        function getActiveSlide() {
1483            return slides[current] || null;
1484        }
1485
1486        function activeSlideHasPoll() {
1487            const slide = getActiveSlide();
1488            return !!(slide && (slide.dataset.hasPoll === 'true' || slide.querySelector('.story-poll')));
1489        }
1490
1491        function getActiveProgressBar() {
1492            const bar = bars[current];
1493            return bar ? bar.querySelector('span') : null;
1494        }
1495
1496        function getActiveVideos() {
1497            const slide = getActiveSlide();
1498            return slide ? Array.from(slide.querySelectorAll('video')) : [];
1499        }
1500
1501        function syncMuteButton() {
1502            if (!muteButton) {
1503                return;
1504            }
1505
1506            const activeSlide = getActiveSlide();
1507            const hasSound = !!activeSlide && activeSlide.dataset.hasSound === 'true';
1508
1509            muteButton.hidden = !hasSound;
1510            muteButton.setAttribute('aria-pressed', storyMuted ? 'true' : 'false');
1511            muteButton.setAttribute('aria-label', storyMuted ? 'Unmute story' : 'Mute story');
1512            muteButton.title = storyMuted ? 'Unmute story' : 'Mute story';
1513        }
1514
1515        function applyMuteState() {
1516            getActiveVideos().forEach(video => {
1517                video.muted = storyMuted;
1518                video.defaultMuted = storyMuted;
1519            });
1520            syncMuteButton();
1521        }
1522
1523        function keepCarouselVideoPlaying(video) {
1524            if (!options.carousel || !video) {
1525                return;
1526            }
1527
1528            const expectedPostId = postIds[current];
1529
1530            [250, 800, 1500].forEach((delay) => {
1531                setTimeout(() => {
1532                    const stillActive = wrapper._kaycoStoryState &&
1533                        wrapper._kaycoPlaybackGeneration === playbackGeneration &&
1534                        (!options.carousel || !!wrapper.closest('.kosher-reel-card.is-active')) &&
1535                        wrapper._kaycoStoryState.current === current &&
1536                        postIds[current] === expectedPostId;
1537
1538                    if (!stillActive || video.ended || !video.paused) {
1539                        return;
1540                    }
1541
1542
1543                    video.play().catch((error) => {
1544
1545                    });
1546                }, delay);
1547            });
1548        }
1549
1550        function playStoryVideo(video) {
1551            if (!video) {
1552
1553                return Promise.reject(new Error('No video element'));
1554            }
1555
1556            if (!video.paused && !video.ended) {
1557
1558                return Promise.resolve(true);
1559            }
1560
1561            if (options.allowMutedFallback) {
1562                video.muted = storyMuted;
1563                video.defaultMuted = storyMuted;
1564                video.toggleAttribute('muted', storyMuted);
1565            }
1566
1567            video.playsInline = true;
1568            video.setAttribute('playsinline', '');
1569            video.setAttribute('webkit-playsinline', '');
1570
1571
1572            const playPromise = video.play();
1573
1574            if (playPromise !== undefined) {
1575                return playPromise.then(() => {
1576
1577                    setTimeout(() => {
1578
1579                    }, 800);
1580                    keepCarouselVideoPlaying(video);
1581                    return true;
1582                }).catch((error) => {
1583
1584                    if (options.allowMutedFallback && !storyMuted) {
1585                        storyMuted = true;
1586                        applyMuteState();
1587                        video.play().then(() => {
1588
1589                        }).catch((retryError) => {
1590
1591                            clearStoryTimer();
1592                            isPaused = true;
1593                            setProgressAnimationState('paused');
1594                            throw retryError;
1595                        });
1596                    }
1597
1598                    clearStoryTimer();
1599                    isPaused = true;
1600                    setProgressAnimationState('paused');
1601                    throw error;
1602                });
1603            }
1604
1605            return Promise.resolve(true);
1606        }
1607
1608        function setProgressAnimationState(state) {
1609            const progressBar = getActiveProgressBar();
1610
1611            if (progressBar) {
1612                progressBar.style.animationPlayState = state;
1613            }
1614        }
1615
1616        function setPollSlideMode(enabled) {
1617            wrapper.classList.toggle('is-poll-slide', enabled);
1618
1619            if (progressBars) {
1620                progressBars.toggleAttribute('hidden', enabled);
1621            }
1622        }
1623
1624        function pauseActiveMedia() {
1625            getActiveVideos().forEach(video => {
1626
1627                video.pause();
1628            });
1629        }
1630
1631        function resumeActiveMedia() {
1632            getActiveVideos().forEach(video => {
1633                playStoryVideo(video);
1634            });
1635        }
1636
1637        function startStoryTimer(duration) {
1638            activeDuration = duration;
1639            remainingTime = duration;
1640            slideStartedAt = Date.now();
1641
1642            clearStoryTimer();
1643
1644            wrapper._timer = setTimeout(() => {
1645                wrapper._timer = null;
1646                playSlide(current + 1);
1647            }, duration);
1648        }
1649
1650        function pauseStoryPlayback() {
1651            if (activeSlideHasPoll()) {
1652                pauseActiveMedia();
1653                return;
1654            }
1655
1656            if (isPaused) {
1657                return;
1658            }
1659
1660            const slide = getActiveSlide();
1661            if (!slide) {
1662                return;
1663            }
1664
1665            isPaused = true;
1666
1667            if (activeDuration > 0) {
1668                const elapsed = Date.now() - slideStartedAt;
1669                remainingTime = Math.max(0, activeDuration - elapsed);
1670            }
1671
1672            clearStoryTimer();
1673            pauseActiveMedia();
1674            setProgressAnimationState('paused');
1675        }
1676
1677        function resumeStoryPlayback() {
1678            if (activeSlideHasPoll()) {
1679                resumeActiveMedia();
1680                return;
1681            }
1682
1683            if (!isPaused) {
1684                return;
1685            }
1686
1687            if (remainingTime <= 0) {
1688                isPaused = false;
1689                playSlide(current + 1);
1690                return;
1691            }
1692
1693            isPaused = false;
1694            slideStartedAt = Date.now();
1695
1696            clearStoryTimer();
1697            resumeActiveMedia();
1698            setProgressAnimationState('running');
1699
1700            wrapper._timer = setTimeout(() => {
1701                wrapper._timer = null;
1702                playSlide(current + 1);
1703            }, remainingTime);
1704        }
1705
1706        function toggleStoryPlayback() {
1707            if (isPaused) {
1708                resumeStoryPlayback();
1709            } else {
1710                pauseStoryPlayback();
1711            }
1712        }
1713
1714        function activeSlideHasVideo() {
1715            return getActiveVideos().length > 0;
1716        }
1717
1718        function toggleActiveVideoPlayback() {
1719            if (!activeSlideHasVideo() || activeSlideHasPoll()) {
1720                return false;
1721            }
1722
1723            toggleStoryPlayback();
1724            return true;
1725        }
1726
1727        wrapper._kaycoStoryControls = {
1728            hasActiveVideo: activeSlideHasVideo,
1729            toggleVideoPlayback: toggleActiveVideoPlayback
1730        };
1731
1732        if (muteButton) {
1733            syncMuteButton();
1734            muteButton.onclick = function (event) {
1735                event.preventDefault();
1736                event.stopPropagation();
1737                storyMuted = !storyMuted;
1738                applyMuteState();
1739            };
1740        }
1741
1742        function setBars(i, duration) {
1743            bars.forEach((bar, index) => {
1744                const span = bar.querySelector('span');
1745
1746                span.style.animation = 'none';
1747                span.style.transform = 'scaleX(0)';
1748
1749                bar.classList.remove('seen');
1750
1751                void span.offsetWidth;
1752
1753                if (index < i) {
1754                    span.style.transform = 'scaleX(1)';
1755                    bar.classList.add('seen');
1756                }
1757
1758                if (index === i) {
1759                    span.style.animation = `progress ${duration}ms linear forwards`;
1760                    span.style.animationPlayState = 'running';
1761                }
1762            });
1763        }
1764
1765        function resetVideos(targetIndex = current) {
1766            slides.forEach((s, index) => {
1767                const v = s.querySelector('video');
1768                if (v && index !== targetIndex) {
1769
1770                    v.pause();
1771                    v.currentTime = 0;
1772                }
1773            });
1774        }
1775
1776        function playSlide(i) {
1777
1778            if (isTransitioning) return;
1779
1780            isTransitioning = true;
1781            setTimeout(() => isTransitioning = false, 80);
1782
1783            if (i >= slides.length || i < 0) {
1784
1785                if (termId) {
1786                    markCategorySeen(termId, postIds);
1787                }
1788
1789                if (i >= slides.length && postIds[current]) {
1790                    wrapper._kaycoStoryState.completed = true;
1791                    queueStoryEvent({
1792                        eventType: 'complete',
1793                        postId: postIds[current],
1794                        termId,
1795                        slideIndex: current,
1796                        totalSlides: slides.length,
1797                        duration: activeDuration || 0
1798                    });
1799                    flushStoryEvents();
1800                }
1801
1802                if (options.carousel && typeof options.onComplete === 'function') {
1803                    options.onComplete();
1804                    return;
1805                }
1806
1807                loadCategory(currentCategoryIndex + 1);
1808                return;
1809            }
1810
1811            clearStoryTimer();
1812
1813            current = i;
1814            wrapper._kaycoStoryState.current = current;
1815            wrapper._kaycoStoryState.completed = false;
1816            isPaused = false;
1817            remainingTime = 0;
1818            activeDuration = 0;
1819            slideStartedAt = 0;
1820
1821            resetVideos(i);
1822
1823            const slide = slides[current];
1824            const video = slide.querySelector('video');
1825            const bgVideo = slide.querySelector('.story-bg-video');
1826            const hasPoll = slide.dataset.hasPoll === 'true' || !!slide.querySelector('.story-poll');
1827
1828            let duration = parseInt(slide.dataset.timeout) || 3000;
1829
1830            initStoryPollCards(slide);
1831            setPollSlideMode(hasPoll);
1832            syncMuteButton();
1833
1834
1835            // =========================
1836            // 🎬 BACKGROUND VIDEO
1837            // =========================
1838if (bgVideo) {
1839
1840    bgVideo.muted = storyMuted;
1841    bgVideo.currentTime = 0;
1842
1843    const startVideo = () => {
1844        return playStoryVideo(bgVideo);
1845    };
1846
1847    const handleDuration = () => {
1848
1849        let videoDuration = bgVideo.duration;
1850
1851
1852        if (
1853            !videoDuration ||
1854            isNaN(videoDuration) ||
1855            videoDuration === Infinity ||
1856            videoDuration === 0
1857        ) {
1858            console.warn('⚠️ BG duration invalid → fallback');
1859            videoDuration = 3;
1860        }
1861
1862        const ms = videoDuration * 1000;
1863
1864
1865        if (!hasPoll) {
1866            setBars(current, ms);
1867        }
1868
1869        if (termId && postIds[current]) {
1870            markStorySeen(termId, postIds[current]);
1871            queueStoryEvent({
1872                eventType: 'view',
1873                postId: postIds[current],
1874                termId,
1875                slideIndex: current,
1876                totalSlides: slides.length,
1877                duration: hasPoll ? 0 : ms
1878            });
1879        }
1880
1881        wrapper._kaycoStoryState.activeDuration = hasPoll ? 0 : ms;
1882
1883        if (!hasPoll) {
1884            startStoryTimer(ms);
1885        }
1886    };
1887
1888    if (bgVideo.readyState >= 1) {
1889        moveTo(current);
1890        startVideo().then(() => {
1891            handleDuration();
1892            if (isPaused) {
1893                pauseStoryPlayback();
1894            }
1895        }).catch(() => {});
1896    } else {
1897        moveTo(current);
1898        const bgPlayPromise = startVideo();
1899        bgVideo.onloadedmetadata = () => {
1900            bgPlayPromise.then(() => {
1901                handleDuration();
1902                if (isPaused) {
1903                    pauseStoryPlayback();
1904                }
1905            }).catch(() => {});
1906        };
1907    }
1908
1909    return;
1910}
1911
1912            // =========================
1913            // 🎬 NORMAL VIDEO
1914            // =========================
1915if (video) {
1916
1917
1918    video.muted = storyMuted;
1919    video.currentTime = 0;
1920
1921    const startVideo = () => {
1922        return playStoryVideo(video);
1923    };
1924
1925    const handleDuration = () => {
1926
1927        let videoDuration = video.duration;
1928
1929
1930        if (
1931            videoDuration &&
1932            !isNaN(videoDuration) &&
1933            videoDuration !== Infinity &&
1934            videoDuration > 0
1935        ) {
1936            duration = videoDuration * 1000;
1937
1938
1939        } else {
1940
1941            console.warn('⚠️ Invalid video duration → fallback to dataset');
1942
1943            duration = parseInt(slide.dataset.timeout) || 3000;
1944        }
1945
1946        if (!hasPoll) {
1947            setBars(current, duration);
1948        }
1949
1950
1951        if (termId && postIds[current]) {
1952            markStorySeen(termId, postIds[current]);
1953            queueStoryEvent({
1954                eventType: 'view',
1955                postId: postIds[current],
1956                termId,
1957                slideIndex: current,
1958                totalSlides: slides.length,
1959                duration: hasPoll ? 0 : duration
1960            });
1961        }
1962
1963        wrapper._kaycoStoryState.activeDuration = hasPoll ? 0 : duration;
1964        if (!hasPoll) {
1965            startStoryTimer(duration);
1966        }
1967    };
1968
1969    // 🔥 CRITICAL: wait for metadata (THIS is why you see 3000ms)
1970    if (video.readyState >= 1) {
1971        moveTo(current);
1972        startVideo().then(() => {
1973            handleDuration();
1974            if (isPaused) {
1975                pauseStoryPlayback();
1976            }
1977        }).catch(() => {});
1978    } else {
1979        moveTo(current);
1980        const playPromise = startVideo();
1981        video.onloadedmetadata = () => {
1982            playPromise.then(() => {
1983                handleDuration();
1984                if (isPaused) {
1985                    pauseStoryPlayback();
1986                }
1987            }).catch(() => {});
1988        };
1989    }
1990
1991    return; // ⛔ STOP here so dataset logic never runs
1992} else {
1993            }
1994
1995            // =========================
1996            // 📊 APPLY BAR
1997            // =========================
1998            moveTo(current);
1999            if (!hasPoll) {
2000                setBars(current, duration);
2001            }
2002
2003
2004            if (termId && postIds[current]) {
2005                markStorySeen(termId, postIds[current]);
2006                queueStoryEvent({
2007                    eventType: 'view',
2008                    postId: postIds[current],
2009                    termId,
2010                    slideIndex: current,
2011                    totalSlides: slides.length,
2012                    duration: hasPoll ? 0 : duration
2013                });
2014            }
2015
2016            wrapper._kaycoStoryState.activeDuration = hasPoll ? 0 : duration;
2017            if (!hasPoll) {
2018                startStoryTimer(duration);
2019            }
2020        }
2021
2022        function next() {
2023            playSlide(current + 1);
2024        }
2025
2026        function prev() {
2027            if (current === 0) {
2028                if (options.carousel && typeof options.onPreviousCategory === 'function') {
2029                    options.onPreviousCategory();
2030                    return;
2031                }
2032
2033                if (currentCategoryIndex === 0) {
2034                    return;
2035                }
2036
2037                loadCategory(currentCategoryIndex - 1, { initialSlideIndex: 'last' });
2038                return;
2039            }
2040
2041            playSlide(current - 1);
2042        }
2043
2044        function openStoryLink(link) {
2045            const href = link.getAttribute('href');
2046
2047            if (!href) {
2048                return;
2049            }
2050
2051            window.open(href, '_blank', 'noopener,noreferrer');
2052        }
2053
2054        function queueInteractionEvent(event) {
2055            const postId = postIds[current];
2056            const rect = wrapper.getBoundingClientRect();
2057
2058            if (!postId || !rect.width || !rect.height) {
2059                return;
2060            }
2061
2062            const xPercent = ((event.clientX - rect.left) / rect.width) * 100;
2063            const yPercent = ((event.clientY - rect.top) / rect.height) * 100;
2064            const element = event.target.closest('.story-el');
2065
2066            queueStoryEvent({
2067                eventType: 'heatmap',
2068                postId,
2069                termId,
2070                slideIndex: current,
2071                totalSlides: slides.length,
2072                xPercent,
2073                yPercent
2074            });
2075
2076            if (element) {
2077                queueStoryEvent({
2078                    eventType: 'click',
2079                    postId,
2080                    termId,
2081                    slideIndex: current,
2082                    totalSlides: slides.length,
2083                    elementId: element.getAttribute('data-element-id') || '',
2084                    elementType: element.getAttribute('data-element-type') || '',
2085                    xPercent,
2086                    yPercent
2087                });
2088            }
2089        }
2090
2091        wrapper.onclick = function(e) {
2092            if (preventClickAfterTouch) {
2093                preventClickAfterTouch = false;
2094                e._kaycoStoryHandled = true;
2095                return;
2096            }
2097
2098            if (
2099                e.target.closest('.bar') ||
2100                e.target.closest('.prev-slide') ||
2101                e.target.closest('.next-slide') ||
2102                e.target.closest('.kosher-story-sound')
2103            ) {
2104                return;
2105            }
2106
2107            queueInteractionEvent(e);
2108
2109            const link = e.target.closest('.overlay a');
2110
2111            if (link) {
2112                e.preventDefault();
2113                e.stopPropagation();
2114                openStoryLink(link);
2115                return;
2116            }
2117
2118            if (e.target.closest('.story-poll')) {
2119                return;
2120            }
2121
2122            if (activeSlideHasPoll()) {
2123                return;
2124            }
2125
2126            toggleStoryPlayback();
2127            e._kaycoStoryHandled = true;
2128        };
2129
2130        if (prevArrow) {
2131            prevArrow.onclick = function(e) {
2132                e.preventDefault();
2133                e.stopPropagation();
2134                prev();
2135            };
2136        }
2137
2138        if (nextArrow) {
2139            nextArrow.onclick = function(e) {
2140                e.preventDefault();
2141                e.stopPropagation();
2142                next();
2143            };
2144        }
2145
2146        wrapper.addEventListener('touchstart', function (event) {
2147            if (event.touches.length !== 1) {
2148                touchStartX = null;
2149                touchStartY = null;
2150                touchStartTarget = null;
2151                return;
2152            }
2153
2154            touchStartX = event.touches[0].clientX;
2155            touchStartY = event.touches[0].clientY;
2156            touchStartTarget = event.target;
2157        }, { passive: true });
2158
2159        wrapper.addEventListener('touchend', function (event) {
2160            if (touchStartX === null || !touchStartTarget) {
2161                return;
2162            }
2163
2164            if (
2165                touchStartTarget.closest('.overlay a') ||
2166                touchStartTarget.closest('.story-poll') ||
2167                touchStartTarget.closest('.bar') ||
2168                touchStartTarget.closest('.prev-slide') ||
2169                touchStartTarget.closest('.next-slide') ||
2170                touchStartTarget.closest('.kosher-story-sound')
2171            ) {
2172                touchStartX = null;
2173                touchStartY = null;
2174                touchStartTarget = null;
2175                return;
2176            }
2177
2178            const changedTouch = event.changedTouches[0];
2179            const originalTouchTarget = touchStartTarget;
2180
2181            if (!changedTouch) {
2182                touchStartX = null;
2183                touchStartY = null;
2184                touchStartTarget = null;
2185                return;
2186            }
2187
2188            const deltaX = changedTouch.clientX - touchStartX;
2189            const deltaY = changedTouch.clientY - touchStartY;
2190            const swipeThreshold = 40;
2191            const tapThreshold = 10;
2192
2193            touchStartX = null;
2194            touchStartY = null;
2195            touchStartTarget = null;
2196
2197            if (Math.abs(deltaX) <= tapThreshold && Math.abs(deltaY) <= tapThreshold) {
2198                if (activeSlideHasPoll()) {
2199                    return;
2200                }
2201                preventClickAfterTouch = true;
2202                queueInteractionEvent({
2203                    clientX: changedTouch.clientX,
2204                    clientY: changedTouch.clientY,
2205                    target: originalTouchTarget
2206                });
2207                toggleStoryPlayback();
2208                return;
2209            }
2210
2211            if (Math.abs(deltaX) < swipeThreshold || Math.abs(deltaX) <= Math.abs(deltaY)) {
2212                return;
2213            }
2214
2215            preventClickAfterTouch = true;
2216
2217            if (deltaX < 0) {
2218                next();
2219            } else {
2220                prev();
2221            }
2222        }, { passive: true });
2223
2224        bars.forEach((bar, i) => {
2225            bar.onclick = function(e) {
2226                e.stopPropagation();
2227                playSlide(i);
2228            };
2229        });
2230
2231        playSlide(current);
2232    };
2233
2234    function resolveInitialSlideIndex(postIds, termId, initialSlideIndex, slideCount) {
2235        if (initialSlideIndex === 'last') {
2236            return Math.max(0, slideCount - 1);
2237        }
2238
2239        if (typeof initialSlideIndex === 'number' && !Number.isNaN(initialSlideIndex)) {
2240            return Math.max(0, Math.min(slideCount - 1, initialSlideIndex));
2241        }
2242
2243        return getStartIndex(postIds, termId);
2244    }
2245
2246});

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.