<?php
// components/Assistant/views/index.php
if (!defined('BEEZUI_ROOT')) {
    // This file resides in <root>/components/Assistant/views
    define('BEEZUI_ROOT', dirname(dirname(dirname(__DIR__))));
}
require_once BEEZUI_ROOT . '/components/index.php';
require_once BEEZUI_ROOT . '/pages/layout.php';

use Components\Assistant\{Flow, State, FlowUtils};

// Resolve flow id and YAML path based on request (supports flow=blueprint)
[$flowId, $yaml] = FlowUtils::resolveFlowAndYaml();
$stateKey = FlowUtils::stateKeyFor($flowId);
$flow = new Flow($yaml);
$state = State::get($stateKey);
if (!$state['current']) {
    $state['current'] = $flow->firstStepId();
    $state['stack'] = [];
    State::put($state, $stateKey);
}

ob_start();
?>
    <style>[x-cloak] {
            display: none !important;
        }</style>
    <div x-data="scrollManager()"
         class="relative h-full min-h-[50vh] w-full overflow-hidden text-gray-900 dark:bg-gray-900 dark:text-gray-100 flex flex-col"
         id="assistant-shell"
         x-cloak
    >
        <!-- Scroll to bottom button -->
        <button x-show="!isAtBottom"
                @click="scrollBottom"
                type="button"
                aria-label="Scroll to bottom"
                class="absolute bottom-24 right-6 z-20 rounded-full bg-zinc-600/70 text-white shadow-lg hover:bg-brand-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 p-2"
                style="display: none;"
                x-transition
                x-cloak
        >
            <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="w-6 h-6">
                <path fill-rule="evenodd"
                      d="M12 3.75a.75.75 0 01.75.75v11.69l3.72-3.72a.75.75 0 111.06 1.06l-5 5a.75.75 0 01-1.06 0l-5-5a.75.75 0 111.06-1.06l3.72 3.72V4.5a.75.75 0 01.75-.75z"
                      clip-rule="evenodd"/>
            </svg>
        </button>
        <!-- Header -->
        <header class="shrink-0 h-14 border-b border-gray-200 bg-white/80 backdrop-blur dark:border-white/10 dark:bg-gray-900/80">
            <div class="mx-auto flex h-full max-w-3xl items-center justify-between px-4">
                <h1 class="text-lg ">MailBeez Assistant</h1>
                <div id="assistant-header-title" class="text-xs text-gray-500 dark:text-gray-400"></div>
            </div>
        </header>

        <!-- Scrollable Chat Area -->
        <main class="flex-1 min-h-0 overflow-y-auto px-4 py-4 scroll-smooth" id="chatMessages">

            <!-- Chat content loads here -->
            <div id="assistant-step"
                 hx-get="<?= \Components\Router::url('assistant_step', ['id' => $state['current'], 'partial' => 1, 'flow' => $flowId, 'module' => \Components\Assistant\FlowUtils::req('module')]); ?>"
                 hx-trigger="load"
                 hx-target="#assistant-step"
                 hx-swap="innerHTML">
                <!-- Initial placeholder -->
            </div>

        </main>

        <!-- Composer -->
        <footer id="assistant-footer" hidden
                class="shrink-0 border-t border-gray-200 bg-white/80 backdrop-blur dark:border-white/10 dark:bg-gray-900/80">
            <div class="mx-auto max-w-3xl px-4 py-3">
                <!-- The composer content is dynamically swapped (OOB) -->
                <div id="assistant-composer">
                    <!-- Placeholder composer; step responses will swap this out-of-band -->

                </div>

            </div>
        </footer>
    </div>

    <!-- Tailwind Plus Elements for rich select is loaded globally in frame.php to avoid double registration of custom elements (el-*) -->
    <?php echo \Components\Common\ClientValidation::script(); ?>
    <script>
        // Alpine component for staggered chat-like reveal and auto-scroll
        (function () {
            const register = () => {
                Alpine.data('chatStep', () => ({
                    init() {
                        try {
                            const root = this.$el;
                            const nodes = Array.from(root.querySelectorAll('[data-reveal]'));
                            if (!nodes.length) return;
                            // Ensure initial hidden state (in case HTMX swaps left old classes)
                            nodes.forEach(el => {
                                el.classList.add('opacity-0', 'translate-y-2');
                                el.classList.remove('opacity-100', 'translate-y-0');
                            });
                            let i = 0;
                            const showNext = () => {
                                if (i >= nodes.length) return;
                                const el = nodes[i++];
                                requestAnimationFrame(() => {
                                    el.classList.add('transition', 'ease-out', 'duration-300', 'opacity-100', 'translate-y-0');
                                    el.classList.remove('opacity-0', 'translate-y-2');
                                    // Avoid per-item scrollIntoView to prevent bounce; we'll do a single bottom scroll after reveal completes
                                });
                                if (i < nodes.length) {
                                    setTimeout(showNext, 120);
                                } else {
                                    /* scroll handled by response script */
                                }
                            };
                            setTimeout(showNext, 120);
                        } catch (e) { /* no-op */
                        }
                    }
                }));
            };
            if (window.Alpine) {
                register();
            }
            document.addEventListener('alpine:init', register);
        })();

        // Lightweight client-side validation to avoid loading effects on invalid input
        (function () {
            function parseMeta(form) {
                try {
                    return JSON.parse(form.getAttribute('data-item-meta') || '{}') || {};
                } catch (e) {
                    return {};
                }
            }

            function inputEl(form, id) {
                return form.querySelector('input[name="' + id + '"]')
                    || document.querySelector('input[name="' + id + '"][form="' + (form.id || '') + '"]')
                    || form.querySelector('el-select[name="' + id + '"]')
                    || document.querySelector('el-select[name="' + id + '"][form="' + (form.id || '') + '"]')
                    || document.getElementById('f-' + id);
            }

            function errorEl(form, id) {
                return document.getElementById('err-' + id) || form.querySelector('#err-' + id);
            }

            function clearVisual(form, id) {
                try {
                    const el = inputEl(form, id);
                    if (el) {
                        el.classList.remove('border-red-500', 'focus:border-red-500', 'focus:ring-red-500');
                        el.removeAttribute('aria-invalid');
                    }
                    const err = errorEl(form, id);
                    if (err) {
                        err.textContent = '';
                        err.classList.add('invisible');
                    }
                } catch (e) { /* no-op */
                }
            }

            function showError(form, id, msg) {
                try {
                    const el = inputEl(form, id);
                    if (el) {
                        el.classList.add('border-red-500', 'focus:border-red-500', 'focus:ring-red-500');
                        el.setAttribute('aria-invalid', 'true');
                    }
                    const err = errorEl(form, id);
                    if (err) {
                        err.textContent = msg;
                        err.classList.remove('invisible');
                    }
                } catch (e) { /* no-op */
                }
            }

            function valueOf(form, id){
                const el = inputEl(form, id);
                if (el && typeof el.value === 'string') return el.value;
                const checked = form.querySelector('input[name="' + id + '"]:checked')
                    || document.querySelector('input[name="' + id + '"][form="' + (form.id || '') + '"]:checked');
                if (checked) return checked.value || 'yes';
                const sel = form.querySelector('el-select[name="' + id + '"]')
                    || document.querySelector('el-select[name="' + id + '"][form="' + (form.id || '') + '"]');
                if (sel) {
                    try { return sel.value || sel.getAttribute('value') || ''; } catch (e) {}
                }
                return '';
            }

            function validate(form) {
                const meta = parseMeta(form);
                const id = meta.id;
                if (!id) return true;
                const rules = (meta.validate || {});
                const schema = {}; schema[id] = rules;
                const v = window.MBZValidator.create({
                    schema: schema,
                    getValue: function(name){
                        if (name === id) return valueOf(form, id);
                        const cache = (window.__assistantAnswers || {});
                        const val = cache[name];
                        return (val === undefined || val === null) ? '' : String(val);
                    },
                    getElement: function(name){ return inputEl(form, name); }
                });
                const ok = v.validateField(id);
                if (!ok){
                    const msg = (v.clientErrors && v.clientErrors[id] && v.clientErrors[id][0]) ? v.clientErrors[id][0] : 'Invalid value.';
                    showError(form, id, msg);
                    return false;
                }
                clearVisual(form, id);
                return true;
            }

            function attachToForm(form) {
                if (!form || form.__assistantValidated) return;
                form.__assistantValidated = true;
                const id = (parseMeta(form).id || '');
                const clear = () => clearVisual(form, id);
                const cacheAnswer = () => {
                    try {
                        const meta = parseMeta(form);
                        if (!meta.id) return;
                        window.__assistantAnswers = window.__assistantAnswers || {};
                        window.__assistantAnswers[meta.id] = valueOf(form, meta.id);
                    } catch (e) {}
                };
                // Block invalid submissions early (works with HTMX)
                form.addEventListener('htmx:configRequest', function (e) {
                    if (!validate(form)) {
                        e.preventDefault();
                        e.stopPropagation();
                    } else {
                        cacheAnswer();
                    }
                });
                // Fallback for non-HTMX or direct submits
                form.addEventListener('submit', function (e) {
                    if (!validate(form)) {
                        e.preventDefault();
                        e.stopPropagation();
                    } else {
                        cacheAnswer();
                    }
                });
                form.addEventListener('input', function(){ validate(form); }, true);
                form.addEventListener('change', function(){ validate(form); }, true);
            }

            function attachAll() {
                document.querySelectorAll('form[data-assistant-form]').forEach(attachToForm);
            }

            if (window.htmx && !window.__assistantValidationBound) {
                document.body.addEventListener('htmx:afterSwap', attachAll);
                document.body.addEventListener('htmx:oobAfterSwap', attachAll);
                window.__assistantValidationBound = true;
            }
            if (document.readyState === 'loading') {
                document.addEventListener('DOMContentLoaded', attachAll, {once: true});
            } else {
                attachAll();
            }
        })();
    </script>


    <script>
        (function () {
            function setupAssistantShellHeight() {
                try {
                    var shell = document.getElementById('assistant-shell');
                    if (!shell) return;
                    shell.style.minHeight = '70vh';
                    var parent = shell.parentElement;
                    if (!parent) return;
                    var apply = function () {
                        try {
                            var h = parent.clientHeight || parent.getBoundingClientRect().height || 0;
                            if (h > 0) {
                                shell.style.height = h + 'px';
                                shell.style.maxHeight = h + 'px';
                            } else {
                                shell.style.height = '100%';
                            }
                        } catch (e) {
                        }
                    };
                    apply();
                    var ro = null;
                    if (window.ResizeObserver) {
                        ro = new ResizeObserver(function () {
                            apply();
                        });
                        try {
                            ro.observe(parent);
                        } catch (e) {
                        }
                    } else {
                        window.addEventListener('resize', apply);
                    }
                    // store cleanup
                    shell.__assistantSizingCleanup = function () {
                        try {
                            ro && ro.disconnect && ro.disconnect();
                        } catch (e) {
                        }
                        try {
                            window.removeEventListener('resize', apply);
                        } catch (e) {
                        }
                    };
                } catch (e) {
                }
            }

            if (document.readyState === 'complete' || document.readyState === 'interactive') {
                setupAssistantShellHeight();
            } else {
                document.addEventListener('DOMContentLoaded', function once() {
                    document.removeEventListener('DOMContentLoaded', once);
                    setupAssistantShellHeight();
                });
            }
            if (window.htmx && !window.__assistantShellSizingBound) {
                document.body.addEventListener('htmx:afterSettle', function () {
                    setupAssistantShellHeight();
                });
                window.__assistantShellSizingBound = true;
            }
        })();
    </script>
    <script>
        // Alpine store for scroll-to-bottom button and scroll management
        function scrollManager() {
            return {
                isAtBottom: true,
                autoStickToBottom: true, // if true, we keep anchoring to bottom on content/size changes
                nearBottom(chat) {
                    if (!chat) return true;
                    const delta = chat.scrollHeight - chat.clientHeight - chat.scrollTop;
                    return delta <= 40; // a slightly larger epsilon helps with fractional scroll values
                },
                scrollBottom() {
                    const chat = document.getElementById('chatMessages');
                    if (!chat) return;
                    try {
                        chat.scrollTo({ top: chat.scrollHeight, behavior: 'smooth' });
                    } catch (_) {
                        chat.scrollTop = chat.scrollHeight;
                    }
                },
                updateBtn() {
                    const chat = document.getElementById('chatMessages');
                    if (!chat) {
                        this.isAtBottom = true;
                        this.autoStickToBottom = true;
                        return;
                    }
                    const atBottom = this.nearBottom(chat);
                    this.isAtBottom = atBottom;
                    this.autoStickToBottom = atBottom; // stick only if user is currently at bottom
                },
                maybeStick() {
                    if (this.autoStickToBottom) {
                        this.scrollBottom();
                        this.isAtBottom = true;
                    }
                },
                init() {
                    const chat = document.getElementById('chatMessages');
                    if (chat) {
                        // Track manual scrolling by the user
                        chat.addEventListener('scroll', () => {
                            this.updateBtn();
                        }, { passive: true });
                        this.updateBtn();
                        this.isAtBottom = true;

                        // Observe size changes of the chat area (footer/header appearing, window resize)
                        if (window.ResizeObserver) {
                            try {
                                const ro = new ResizeObserver(() => {
                                    // When size changes and we're sticking, keep bottom in view
                                    this.maybeStick();
                                    this.updateBtn();
                                });
                                ro.observe(chat);
                                this.__ro = ro;
                            } catch (_) { }
                        } else {
                            window.addEventListener('resize', () => {
                                this.maybeStick();
                                this.updateBtn();
                            });
                        }

                        // Observe content mutations inside the assistant step container
                        try {
                            const step = document.getElementById('assistant-step');
                            if (window.MutationObserver && step) {
                                const mo = new MutationObserver(() => {
                                    // If new nodes are added (reveal animations, late loads), keep anchored
                                    this.maybeStick();
                                    this.updateBtn();
                                });
                                mo.observe(step, { childList: true, subtree: true, attributes: true, characterData: false });
                                this.__mo = mo;
                            }
                        } catch (_) { }
                    }

                    // After each htmx swap for the main step, force bottom and enable sticky
                    document.body.addEventListener('htmx:afterSwap', (e) => {
                        if (!e || !e.target) return;
                        if (e.target.id === 'assistant-step') {
                            this.autoStickToBottom = true;
                            this.scrollBottom();
                            this.isAtBottom = true;
                            // Also schedule a couple of follow-up scrolls for staggered reveals
                            requestAnimationFrame(() => this.maybeStick());
                            setTimeout(() => this.maybeStick(), 150);
                            setTimeout(() => this.maybeStick(), 350);
                        }
                    });

                    // When composer/footer swaps OOB, keep bottom in view as its height affects the chat area
                    document.body.addEventListener('htmx:oobAfterSwap', (e) => {
                        try {
                            const tgt = e && e.detail && e.detail.target ? e.detail.target : null;
                            if (!tgt) return;
                            if (tgt.id === 'assistant-composer' || tgt.id === 'assistant-footer') {
                                this.autoStickToBottom = true;
                                this.maybeStick();
                            }
                        } catch (_) { }
                    });
                }
            }
        }

        // Small helper for assistant UX: thinking indicator and controls
        window.assistantUX = {
            showThinking() {
                const target = document.getElementById('assistant-chat-root') || document.getElementById('assistant-step');
                if (!target) return;
                window.assistantUX.hideThinking();
                // Insert with initial hidden state and transition classes
                const html = '<div id="assistant-thinking" class="flex items-start gap-3 opacity-0 translate-y-0 transition ease-out duration-300" data-temp="1">'
                    + '<div class="rounded-2xl rounded-tl-sm bg-brand-50 px-4 py-2 text-sm text-brand-900/50"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class=" animate-spin" role="status" aria-label="Loading"><path d="M21 12a9 9 0 1 1-6.219-8.56"></path></svg></div>'
                    + '</div>';
                target.insertAdjacentHTML('beforeend', html);

                // Ensure the thinking indicator is fully visible by scrolling to the bottom
                const doScroll = () => {
                    const chat = document.getElementById('chatMessages');
                    if (!chat) return;
                    try {
                        chat.scrollTo({ top: chat.scrollHeight, behavior: 'smooth' });
                    } catch (_) {
                        chat.scrollTop = chat.scrollHeight;
                    }
                };
                // Scroll immediately after insertion
                doScroll();

                // Trigger reveal on next frame for smooth transition and scroll again after layout
                const el = document.getElementById('assistant-thinking');
                if (el) {
                    requestAnimationFrame(function () {
                        el.classList.add('opacity-100', 'translate-y-0');
                        el.classList.remove('opacity-0', 'translate-y-2');
                        // Scroll again to account for the revealed element height
                        doScroll();
                        // And once more after a short delay to handle late layout changes
                        setTimeout(doScroll, 150);
                    });
                }
            },
            hideThinking() {
                const t = document.getElementById('assistant-thinking');
                if (t && t.parentNode) t.parentNode.removeChild(t);
            },
            toggleControls(disabled) {
                try {
                    const root = document.getElementById('assistant-shell') || document;
                    root.querySelectorAll('#assistant-form input, #assistant-form button, #assistant-form textarea, #assistant-step input, #assistant-step button').forEach(function (el) {
                        if (disabled) {
                            el.setAttribute('disabled', 'disabled');
                        } else {
                            el.removeAttribute('disabled');
                        }
                    });
                    // Hide/show the inline form (if present) and mark hidden to avoid Tailwind space-y gaps
                    const form = root.querySelector('#assistant-form');
                    if (form) {
                        if (disabled) {
                            form.style.display = 'none';
                            form.setAttribute('hidden', '');
                        } else {
                            form.style.display = '';
                            form.removeAttribute('hidden');
                        }
                    }
                    // Hide/show choice groups (radio/checkbox chip containers) and mark hidden
                    const groups = root.querySelectorAll('#assistant-step [data-group="radio"], #assistant-step [data-group="checkbox"]');
                    groups.forEach(function (el) {
                        if (disabled) {
                            el.style.display = 'none';
                            el.setAttribute('hidden', '');
                        } else {
                            el.style.display = '';
                            el.removeAttribute('hidden');
                        }
                    });
                    // Hide/show any visible buttons under step or form (eg. Continue, Send)
                    const btns = root.querySelectorAll('#assistant-step button, #assistant-form button');
                    btns.forEach(function (el) {
                        el.style.display = disabled ? 'none' : '';
                    });
                    // Hide/show inline error placeholders to avoid reserving vertical space
                    const errs = root.querySelectorAll('#assistant-step [data-error-for]');
                    errs.forEach(function (el) {
                        if (disabled) {
                            el.setAttribute('hidden', '');
                        } else {
                            el.removeAttribute('hidden');
                        }
                    });
                } catch (e) {
                }
            }
        };
        // Simplified HTMX event listeners for assistant UX
        (function () {
            if (window.__assistantHtmxUXBound) return;
            window.__assistantHtmxUXBound = true;

            // Utility to append user bubble (for text or choice, as before)
            function esc(s) {
                try {
                    var d = document.createElement('div');
                    d.textContent = (s == null ? '' : String(s));
                    return d.innerHTML;
                } catch (e) {
                    return String(s || '');
                }
            }

            function getMeta() {
                var el = document.getElementById('assistant-meta');
                var m = {stepId: '', itemIndex: 0, itemId: ''};
                if (!el) return m;
                m.stepId = el.getAttribute('data-step-id') || '';
                m.itemId = el.getAttribute('data-item-id') || '';
                var idx = parseInt(el.getAttribute('data-item-index') || '0', 10);
                m.itemIndex = isNaN(idx) ? 0 : idx;
                return m;
            }

            function appendUserBubble(text) {
                if (!text) return;
                var target = document.getElementById('assistant-chat-root') || document.getElementById('assistant-step');
                if (!target) return;
                var html = '<div class="flex justify-end" data-temp="1">'
                    + '<div class="rounded-2xl rounded-tr-sm bg-brand px-4 py-2 text-sm text-white">' + esc(text) + '</div>'
                    + '</div>';
                target.insertAdjacentHTML('beforeend', html);
            }

            document.body.addEventListener('htmx:beforeRequest', function (e) {
                var elt = (e && e.detail && e.detail.elt) ? e.detail.elt : (e && e.target ? e.target : null);
                // Only for assistant-related requests
                var isAssistant = !!(elt && (elt.closest && (elt.closest('#assistant-shell') || elt.closest('#assistant-form') || elt.id === 'assistant-form')));
                if (!isAssistant) return;
                window.__assistantPrevMeta = getMeta();
                try {
                    var form = document.getElementById('assistant-form');
                    var meta = {};
                    try {
                        meta = JSON.parse((form && form.getAttribute('data-item-meta')) || '{}') || {};
                    } catch (_) {
                        meta = {};
                    }
                    var type = meta.type || 'text';
                    var id = meta.id || '';
                    if (type === 'text' && id) {
                        var input = document.getElementById('f-' + id) || (form && form.querySelector('input[name="' + id + '"]'));
                        var val = input && typeof input.value === 'string' ? input.value.trim() : '';
                        if (val) appendUserBubble(val);
                    } else if (type === 'single_choice' && id) {
                        var lab = '';
                        var r = document.querySelector('input[name="' + id + '"]:checked');
                        if (r) {
                            var span = r.nextElementSibling;
                            lab = span && span.textContent ? span.textContent.trim() : (r.value || '');
                        }
                        if (lab) appendUserBubble(lab);
                    } else if (type === 'single_choice_rich' && id) {
                        try {
                            var sel = document.querySelector('el-select[name="' + id + '"]') || document.querySelector('el-select[name="' + id + '"][form="' + (document.getElementById('assistant-form')?.id || '') + '"]');
                            var v = sel ? (sel.value || sel.getAttribute('value') || '') : '';
                            var lab2 = '';
                            if (sel && v) {
                                var escVal = (window.CSS && CSS.escape) ? CSS.escape(v) : v.replace(/([\\^$*+?.()|[\]{}])/g, '\\$1');
                                var opt = sel.querySelector('el-option[value="' + escVal + '"]');
                                if (opt) {
                                    var p = opt.querySelector('p');
                                    lab2 = p && p.textContent ? p.textContent.trim() : v;
                                } else {
                                    lab2 = v;
                                }
                            }
                            if (lab2) appendUserBubble(lab2);
                        } catch (_err) {
                        }
                    } else if (type === 'multi_choice' && id) {
                        try {
                            var boxes = Array.from(document.querySelectorAll('input[type="checkbox"][name="' + id + '[]"]:checked'));
                            var labels = boxes.map(function (cb) {
                                var s = cb.nextElementSibling; // span with label
                                var txt = s && s.textContent ? s.textContent.trim() : '';
                                return txt || (cb.value || '');
                            }).filter(function (s) {
                                return !!s;
                            });
                            if (labels.length) {
                                appendUserBubble(labels.join(', '));
                            }
                        } catch (_e) {
                        }
                    }
                    window.assistantUX.showThinking();
                    window.assistantUX.toggleControls(true);
                } catch (err) {
                }
            });
            document.body.addEventListener('htmx:afterRequest', function (e) {
                window.assistantUX.hideThinking();
                window.assistantUX.toggleControls(false);
            });
            document.body.addEventListener('htmx:afterSwap', function (e) {
                if (!e || !e.target || e.target.id !== 'assistant-step') return;
                try {
                    var prev = window.__assistantPrevMeta || {stepId: '', itemIndex: 0};
                    var next = getMeta();
                    if (next.stepId !== prev.stepId || next.itemIndex > prev.itemIndex) {
                        document.querySelectorAll('#assistant-step [data-temp="1"]').forEach(function (n) {
                            if (n && n.parentNode) n.parentNode.removeChild(n);
                        });
                    }
                } catch (err) {
                }
            });
            document.body.addEventListener('htmx:sendError', function () {
                window.assistantUX.hideThinking();
                window.assistantUX.toggleControls(false);
            });
            document.body.addEventListener('htmx:responseError', function () {
                window.assistantUX.hideThinking();
                window.assistantUX.toggleControls(false);
            });
        })();
    </script>

    <script>
        // wordReveal effect adapted from dashboard.php
        function wordReveal() {
            return {
                chunks: [],
                visible: "",
                index: 0,
                _rootEl: null,
                start(rootEl) {
                    try {
                        this._rootEl = rootEl || null;
                        const source = rootEl && rootEl.querySelector ? rootEl.querySelector('[data-source]') : null;
                        if (!source) return;
                        this.chunks = this.extractChunks(source);
                        this.revealNext();
                    } catch (e) { /* no-op */ }
                },
                extractChunks(el) {
                    const chunks = [];
                    el.childNodes.forEach(node => {
                        if (node.nodeType === Node.TEXT_NODE) {
                            const words = (node.textContent || '').split(/\s+/).filter(Boolean);
                            words.forEach(w => chunks.push(w + ' '));
                        } else if (node.nodeType === Node.ELEMENT_NODE) {
                            chunks.push(node.outerHTML + ' ');
                        }
                    });
                    return chunks;
                },
                revealNext() {
                    if (this.index >= this.chunks.length) {
                        try {
                            const evt = new CustomEvent('wordreveal:done', { bubbles: true });
                            (this._rootEl || document).dispatchEvent(evt);
                        } catch (e) { /* no-op */ }
                        return;
                    }
                    const chunkSize = Math.floor(Math.random() * 2) + 1;
                    const part = this.chunks.slice(this.index, this.index + chunkSize).join('');
                    this.visible += part;
                    this.index += chunkSize;
                    setTimeout(() => this.revealNext(), Math.floor(Math.random() * 50) + 50);
                }
            }
        }
    </script>

<?php
$content = ob_get_clean();
echo $content;
