/**
 * Fichier  : assets/js/mobile-flow.js
 * Auteur   : KREMER Régis
 * Version  : 2.6.78
 * Date     : 2026-07-17
 * Description : Socle mobile/tablette commun — assistants, cartes, volets, détails, dialogues et viewport.
 *
 * Changelog :
 *   v2.6.78 (2026-07-17) — MOBILE FLOW FOUNDATION2 : création du socle réutilisable mobile/tablette.
 */
// ================================================================
// MageStock Mobile Flow Foundation
// Composants sans framework, déclaratifs et programmatiques.
// Aucun flux métier n'est refondu dans ce fichier.
// ================================================================
(function mobileFlowFoundation(global, document) {
    'use strict';

    if (!global || !document || global.MageStockMobileUI) return;

    const VERSION = '2.6.78';
    const FOCUSABLE_SELECTOR = [
        'a[href]:not([tabindex="-1"])',
        'area[href]:not([tabindex="-1"])',
        'button:not([disabled]):not([tabindex="-1"])',
        'input:not([disabled]):not([type="hidden"]):not([tabindex="-1"])',
        'select:not([disabled]):not([tabindex="-1"])',
        'textarea:not([disabled]):not([tabindex="-1"])',
        'iframe:not([tabindex="-1"])',
        '[contenteditable="true"]:not([tabindex="-1"])',
        '[tabindex]:not([tabindex="-1"])'
    ].join(',');

    let uidSequence = 0;

    function uid(prefix) {
        uidSequence += 1;
        return String(prefix || 'ms-ui') + '-' + Date.now().toString(36) + '-' + uidSequence.toString(36);
    }

    function resolveElement(value, root) {
        if (!value) return null;
        if (value instanceof Element || value === document || value === global) return value;
        if (typeof value === 'string') return (root || document).querySelector(value);
        return null;
    }

    function translate(key, fallback, params) {
        if (typeof global.t === 'function') {
            try { return global.t(key, params || {}, fallback || ''); } catch (_) {}
        }
        return fallback || key || '';
    }

    function escapeHtml(value) {
        return String(value == null ? '' : value)
            .replace(/&/g, '&amp;')
            .replace(/</g, '&lt;')
            .replace(/>/g, '&gt;')
            .replace(/"/g, '&quot;')
            .replace(/'/g, '&#039;');
    }

    function isVisible(element) {
        if (!element || !(element instanceof Element)) return false;
        if (element.hidden || element.getAttribute('aria-hidden') === 'true') return false;
        const style = global.getComputedStyle(element);
        return style.display !== 'none' && style.visibility !== 'hidden';
    }

    function getFocusable(container) {
        if (!container) return [];
        return Array.from(container.querySelectorAll(FOCUSABLE_SELECTOR)).filter((element) => {
            if (!isVisible(element)) return false;
            if (element.closest('[inert]')) return false;
            return true;
        });
    }

    function isFormControl(element) {
        return !!element && !!element.matches && element.matches('input, textarea, select, [contenteditable="true"]');
    }

    function setTextOrNode(container, value) {
        if (!container) return;
        container.innerHTML = '';
        if (value instanceof Node) {
            container.appendChild(value);
            return;
        }
        container.textContent = value == null ? '' : String(value);
    }

    function createButton(label, className, onClick) {
        const button = document.createElement('button');
        button.type = 'button';
        button.className = className || 'btn btn-secondary';
        button.textContent = label || '';
        if (typeof onClick === 'function') button.addEventListener('click', onClick);
        return button;
    }

    function dispatch(name, detail, target, cancelable) {
        const event = new CustomEvent(name, {
            bubbles: true,
            cancelable: !!cancelable,
            detail: detail || {}
        });
        (target || document).dispatchEvent(event);
        return event;
    }

    // ------------------------------------------------------------
    // Live region
    // ------------------------------------------------------------
    const LiveRegion = {
        polite: null,
        assertive: null,
        init() {
            this.polite = document.getElementById('msLiveRegionPolite');
            this.assertive = document.getElementById('msLiveRegionAssertive');
            if (!this.polite) {
                this.polite = document.createElement('div');
                this.polite.id = 'msLiveRegionPolite';
                this.polite.className = 'ms-sr-only';
                this.polite.setAttribute('aria-live', 'polite');
                this.polite.setAttribute('aria-atomic', 'true');
                document.body.appendChild(this.polite);
            }
            if (!this.assertive) {
                this.assertive = document.createElement('div');
                this.assertive.id = 'msLiveRegionAssertive';
                this.assertive.className = 'ms-sr-only';
                this.assertive.setAttribute('aria-live', 'assertive');
                this.assertive.setAttribute('aria-atomic', 'true');
                document.body.appendChild(this.assertive);
            }
        },
        announce(message, priority) {
            if (!message) return;
            if (!this.polite || !this.assertive) this.init();
            const region = priority === 'assertive' ? this.assertive : this.polite;
            region.textContent = '';
            global.setTimeout(() => { region.textContent = String(message); }, 24);
        }
    };

    // ------------------------------------------------------------
    // Device and viewport profile
    // ------------------------------------------------------------
    const Device = {
        profile: 'desktop',
        orientation: 'portrait',
        coarse: false,
        update() {
            const width = Math.max(1, Math.round(global.visualViewport?.width || global.innerWidth || document.documentElement.clientWidth || 1));
            const height = Math.max(1, Math.round(global.visualViewport?.height || global.innerHeight || document.documentElement.clientHeight || 1));
            const nextProfile = width < 600 ? 'phone' : (width < 1200 ? 'tablet' : 'desktop');
            const nextOrientation = width > height ? 'landscape' : 'portrait';
            const coarse = !!global.matchMedia?.('(pointer: coarse)').matches;
            const changed = nextProfile !== this.profile || nextOrientation !== this.orientation || coarse !== this.coarse;

            this.profile = nextProfile;
            this.orientation = nextOrientation;
            this.coarse = coarse;

            document.documentElement.dataset.msDevice = nextProfile;
            document.documentElement.dataset.msOrientation = nextOrientation;
            document.documentElement.dataset.msPointer = coarse ? 'coarse' : 'fine';
            document.documentElement.style.setProperty('--ms-viewport-width', width + 'px');

            if (changed) {
                dispatch('magestock:devicechange', {
                    profile: nextProfile,
                    orientation: nextOrientation,
                    coarse,
                    width,
                    height
                });
            }
        },
        isPhone() { return this.profile === 'phone'; },
        isTablet() { return this.profile === 'tablet'; },
        isDesktop() { return this.profile === 'desktop'; }
    };

    const Viewport = {
        baselineHeight: 0,
        keyboardOpen: false,
        frame: null,
        initialized: false,
        init() {
            if (this.initialized) return;
            this.initialized = true;
            const schedule = () => this.schedule();
            global.visualViewport?.addEventListener?.('resize', schedule, { passive: true });
            global.visualViewport?.addEventListener?.('scroll', schedule, { passive: true });
            global.addEventListener('resize', schedule, { passive: true });
            global.addEventListener('orientationchange', () => {
                this.baselineHeight = 0;
                global.setTimeout(schedule, 120);
            }, { passive: true });
            global.screen?.orientation?.addEventListener?.('change', () => {
                this.baselineHeight = 0;
                global.setTimeout(schedule, 120);
            });
            document.addEventListener('focusin', (event) => {
                schedule();
                if (!isFormControl(event.target) || Device.isDesktop()) return;
                global.setTimeout(() => {
                    try { event.target.scrollIntoView({ block: 'center', inline: 'nearest', behavior: 'smooth' }); } catch (_) {}
                }, 260);
            }, true);
            document.addEventListener('focusout', () => global.setTimeout(schedule, 120), true);
            global.addEventListener('pageshow', schedule, { passive: true });
            this.update();
        },
        schedule() {
            if (this.frame) global.cancelAnimationFrame(this.frame);
            this.frame = global.requestAnimationFrame(() => {
                this.frame = null;
                this.update();
            });
        },
        update() {
            Device.update();
            const vv = global.visualViewport;
            const visualHeight = Math.max(1, Math.round(vv?.height || global.innerHeight || document.documentElement.clientHeight || 1));
            const visualWidth = Math.max(1, Math.round(vv?.width || global.innerWidth || document.documentElement.clientWidth || 1));
            const offsetTop = Math.max(0, Math.round(vv?.offsetTop || 0));
            const layoutHeight = Math.max(visualHeight, Math.round(document.documentElement.clientHeight || global.innerHeight || visualHeight));
            const activeControl = isFormControl(document.activeElement);

            if (!activeControl) this.baselineHeight = Math.max(this.baselineHeight || 0, visualHeight);
            if (!this.baselineHeight) this.baselineHeight = visualHeight;

            const keyboardHeight = Math.max(0, Math.round(this.baselineHeight - visualHeight));
            const visualBottomGap = Math.max(0, Math.round(layoutHeight - visualHeight - offsetTop));
            const keyboardOpen = activeControl
                && Device.profile !== 'desktop'
                && keyboardHeight >= 110
                && visualHeight <= this.baselineHeight * 0.87;

            document.documentElement.style.setProperty('--ms-visual-height', visualHeight + 'px');
            document.documentElement.style.setProperty('--ms-visual-width', visualWidth + 'px');
            document.documentElement.style.setProperty('--ms-visual-offset-top', offsetTop + 'px');
            document.documentElement.style.setProperty('--ms-keyboard-height', (keyboardOpen ? keyboardHeight : 0) + 'px');
            document.documentElement.style.setProperty('--ms-visual-bottom-gap', (keyboardOpen ? visualBottomGap : 0) + 'px');

            if (document.body) {
                document.body.classList.toggle('ms-soft-keyboard-open', keyboardOpen);
                document.body.classList.toggle('ms-coarse-landscape', Device.coarse && Device.orientation === 'landscape' && visualHeight < 700);
                document.body.dataset.msKeyboard = keyboardOpen ? 'open' : 'closed';
            }

            if (keyboardOpen !== this.keyboardOpen) {
                this.keyboardOpen = keyboardOpen;
                dispatch('magestock:keyboardchange', {
                    open: keyboardOpen,
                    height: keyboardHeight,
                    visualHeight,
                    visualWidth,
                    bottomGap: visualBottomGap
                });
            }

            dispatch('magestock:viewportchange', {
                visualHeight,
                visualWidth,
                offsetTop,
                keyboardOpen,
                keyboardHeight,
                bottomGap: visualBottomGap,
                profile: Device.profile,
                orientation: Device.orientation
            });
            StickyActions.refresh();
        }
    };

    // ------------------------------------------------------------
    // Overlay stack, focus trap and background inerting
    // ------------------------------------------------------------
    const OverlayStack = {
        stack: [],
        keyHandler: null,
        backgroundState: new Map(),
        activate(config) {
            const overlay = resolveElement(config?.overlay);
            const dialog = resolveElement(config?.dialog, overlay || document);
            if (!overlay || !dialog) return null;

            const existing = this.stack.find((entry) => entry.overlay === overlay);
            if (existing) {
                existing.trigger = resolveElement(config?.trigger) || existing.trigger;
                existing.closeOnEscape = config?.closeOnEscape !== false;
                existing.initialFocus = config?.initialFocus || existing.initialFocus;
                this.focusInitial(existing);
                return existing;
            }

            const previous = this.top();
            if (previous) {
                previous.dialog.setAttribute('aria-hidden', 'true');
                previous.dialog.inert = true;
            }

            const entry = {
                overlay,
                dialog,
                trigger: resolveElement(config?.trigger) || document.activeElement,
                close: typeof config?.close === 'function' ? config.close : null,
                closeOnEscape: config?.closeOnEscape !== false,
                initialFocus: config?.initialFocus || null,
                restoreFocus: config?.restoreFocus !== false
            };

            this.stack.push(entry);
            overlay.setAttribute('aria-hidden', 'false');
            dialog.setAttribute('aria-hidden', 'false');
            dialog.inert = false;
            document.body?.classList.add('ms-overlay-open');
            document.documentElement.classList.add('ms-overlay-open');
            this.setBackgroundInert(true);
            this.bindKeyboard();
            this.focusInitial(entry);
            dispatch('magestock:overlayopen', { overlay, dialog, depth: this.stack.length }, overlay);
            return entry;
        },
        deactivate(overlay, options) {
            const element = resolveElement(overlay);
            const index = this.stack.map((entry) => entry.overlay).lastIndexOf(element);
            if (index < 0) return;
            const entry = this.stack[index];
            this.stack.splice(index, 1);
            entry.overlay.setAttribute('aria-hidden', 'true');
            entry.dialog.setAttribute('aria-hidden', 'true');
            entry.dialog.inert = false;

            const top = this.top();
            if (top) {
                top.dialog.removeAttribute('aria-hidden');
                top.dialog.inert = false;
                this.focusInitial(top, true);
            } else {
                document.body?.classList.remove('ms-overlay-open');
                document.documentElement.classList.remove('ms-overlay-open');
                this.setBackgroundInert(false);
                this.unbindKeyboard();
            }

            if (entry.restoreFocus && options?.restoreFocus !== false && entry.trigger?.focus && document.contains(entry.trigger)) {
                global.setTimeout(() => {
                    try { entry.trigger.focus({ preventScroll: true }); } catch (_) { try { entry.trigger.focus(); } catch (__) {} }
                }, 30);
            }
            dispatch('magestock:overlayclose', { overlay: entry.overlay, dialog: entry.dialog, depth: this.stack.length }, entry.overlay);
        },
        top() { return this.stack[this.stack.length - 1] || null; },
        setBackgroundInert(active) {
            const nodes = document.querySelectorAll('.app-shell, #slideBackdrop, #pwaInstallCard, #printOverlay, #scrollTopProgress, #scrollTopBtn, #imgZoomOverlay');
            if (active) {
                nodes.forEach((node) => {
                    if (this.stack.some((entry) => entry.overlay === node || node.contains(entry.overlay))) return;
                    if (!this.backgroundState.has(node)) {
                        this.backgroundState.set(node, {
                            inert: !!node.inert,
                            ariaHidden: node.getAttribute('aria-hidden')
                        });
                    }
                    node.inert = true;
                    node.setAttribute('aria-hidden', 'true');
                });
                return;
            }
            this.backgroundState.forEach((state, node) => {
                if (!document.contains(node)) return;
                node.inert = state.inert;
                if (state.ariaHidden == null) node.removeAttribute('aria-hidden');
                else node.setAttribute('aria-hidden', state.ariaHidden);
            });
            this.backgroundState.clear();
        },
        bindKeyboard() {
            if (this.keyHandler) return;
            this.keyHandler = (event) => {
                const entry = this.top();
                if (!entry) return;
                if (event.key === 'Escape' && entry.closeOnEscape) {
                    event.preventDefault();
                    event.stopPropagation();
                    if (entry.close) entry.close('escape');
                    return;
                }
                if (event.key !== 'Tab') return;
                const focusables = getFocusable(entry.dialog);
                if (!focusables.length) {
                    event.preventDefault();
                    entry.dialog.focus({ preventScroll: true });
                    return;
                }
                const first = focusables[0];
                const last = focusables[focusables.length - 1];
                const active = document.activeElement;
                if (event.shiftKey && (active === first || !entry.dialog.contains(active))) {
                    event.preventDefault();
                    last.focus();
                } else if (!event.shiftKey && active === last) {
                    event.preventDefault();
                    first.focus();
                }
            };
            document.addEventListener('keydown', this.keyHandler, true);
        },
        unbindKeyboard() {
            if (!this.keyHandler) return;
            document.removeEventListener('keydown', this.keyHandler, true);
            this.keyHandler = null;
        },
        focusInitial(entry, fromNested) {
            global.setTimeout(() => {
                if (this.top() !== entry) return;
                if (fromNested && entry.dialog.contains(document.activeElement)) return;
                let target = null;
                if (entry.initialFocus) target = resolveElement(entry.initialFocus, entry.dialog);
                if (!target && entry.dialog.querySelector('[autofocus]')) target = entry.dialog.querySelector('[autofocus]');
                if (!target && !entry.dialog.querySelector('[data-modal-no-autofocus="1"], [data-no-autofocus="1"]')) {
                    target = entry.dialog.querySelector('input:not([type="hidden"]):not([disabled]), select:not([disabled]), textarea:not([disabled])');
                }
                if (!target) target = getFocusable(entry.dialog)[0];
                if (!target) target = entry.dialog;
                if (!target.hasAttribute('tabindex') && !target.matches(FOCUSABLE_SELECTOR)) target.setAttribute('tabindex', '-1');
                try { target.focus({ preventScroll: true }); } catch (_) { try { target.focus(); } catch (__) {} }
            }, 210);
        }
    };

    // ------------------------------------------------------------
    // Accessible bridge for the historical global Modal component
    // ------------------------------------------------------------
    const LegacyModalBridge = {
        patched: false,
        originalShow: null,
        originalClose: null,
        closeCallbacks: [],
        patch() {
            if (this.patched || !global.Modal) return;
            const modal = global.Modal;
            const overlay = document.getElementById('modalOverlay');
            const box = document.getElementById('modalBox');
            if (!overlay || !box || typeof modal.show !== 'function' || typeof modal.close !== 'function') return;

            this.patched = true;
            this.originalShow = modal.show.bind(modal);
            this.originalClose = modal.close.bind(modal);

            box.setAttribute('role', 'dialog');
            box.setAttribute('aria-modal', 'true');
            box.setAttribute('aria-labelledby', 'modalTitle');
            box.setAttribute('aria-describedby', 'modalSub');
            box.setAttribute('tabindex', '-1');
            overlay.setAttribute('aria-hidden', overlay.classList.contains('visible') ? 'false' : 'true');

            modal.show = (...args) => {
                const trigger = document.activeElement;
                this.originalShow(...args);
                const title = document.getElementById('modalTitle');
                if (title && !title.hasAttribute('tabindex')) title.setAttribute('tabindex', '-1');
                const footer = document.getElementById('modalFooter');
                footer?.toggleAttribute('hidden', !footer.children.length);
                OverlayStack.activate({
                    overlay,
                    dialog: box,
                    trigger,
                    close: () => modal.close(),
                    closeOnEscape: !box.matches('[data-modal-lock="1"]') && !box.querySelector('[data-modal-lock="1"]')
                });
                return box;
            };

            modal.close = (...args) => {
                const wasOpen = overlay.classList.contains('visible') || OverlayStack.stack.some((entry) => entry.overlay === overlay);
                OverlayStack.deactivate(overlay);
                this.originalClose(...args);
                document.getElementById('modalFooter')?.removeAttribute('hidden');
                if (wasOpen) {
                    const callbacks = this.closeCallbacks.splice(0);
                    callbacks.forEach((callback) => { try { callback(); } catch (error) { console.error(error); } });
                }
            };

            modal.open = (...args) => modal.show(...args);
            modal.onClose = (callback) => {
                if (typeof callback === 'function') this.closeCallbacks.push(callback);
            };
            modal.setLocked = (locked) => {
                box.toggleAttribute('data-modal-lock', !!locked);
                document.getElementById('modalCloseBtn')?.toggleAttribute('hidden', !!locked);
                const entry = OverlayStack.stack.find((item) => item.overlay === overlay);
                if (entry) entry.closeOnEscape = !locked;
            };
        }
    };

    // ------------------------------------------------------------
    // Generic action sheet / filter drawer / detail overlay
    // ------------------------------------------------------------
    const Panel = {
        instances: new Set(),
        open(options) {
            const opts = options || {};
            const kind = ['sheet', 'drawer', 'page'].includes(opts.kind) ? opts.kind : 'sheet';
            const overlay = document.createElement('div');
            const panel = document.createElement('section');
            const titleId = uid('ms-panel-title');
            const descriptionId = uid('ms-panel-description');
            const trigger = resolveElement(opts.trigger) || document.activeElement;

            overlay.className = 'ms-panel-overlay ms-panel-overlay--' + kind;
            overlay.dataset.msPanel = kind;
            overlay.setAttribute('aria-hidden', 'true');
            panel.className = 'ms-panel ms-panel--' + kind + (opts.className ? ' ' + opts.className : '');
            panel.setAttribute('role', 'dialog');
            panel.setAttribute('aria-modal', 'true');
            panel.setAttribute('aria-labelledby', titleId);
            panel.setAttribute('tabindex', '-1');
            if (opts.description) panel.setAttribute('aria-describedby', descriptionId);

            const header = document.createElement('header');
            header.className = 'ms-panel__header';
            const copy = document.createElement('div');
            copy.className = 'ms-panel__header-copy';
            const title = document.createElement('h2');
            title.id = titleId;
            title.className = 'ms-panel__title';
            title.textContent = opts.title || translate('shell.modal.title', 'Actions');
            title.setAttribute('tabindex', '-1');
            copy.appendChild(title);
            if (opts.description) {
                const description = document.createElement('p');
                description.id = descriptionId;
                description.className = 'ms-panel__description';
                description.textContent = String(opts.description);
                copy.appendChild(description);
            }
            const closeButton = createButton('', 'ms-panel__close', () => close('button'));
            closeButton.setAttribute('aria-label', opts.closeLabel || translate('shell.print.close', 'Fermer'));
            closeButton.title = closeButton.getAttribute('aria-label');
            closeButton.innerHTML = '<svg viewBox="0 0 24 24" aria-hidden="true"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>';
            header.append(copy, closeButton);

            const body = document.createElement('div');
            body.className = 'ms-panel__body';
            if (opts.content instanceof Node) body.appendChild(opts.content);
            else if (opts.html != null) body.innerHTML = String(opts.html);
            else if (opts.content != null) body.textContent = String(opts.content);

            const footer = document.createElement('footer');
            footer.className = 'ms-panel__footer';
            const actions = Array.isArray(opts.actions) ? opts.actions : [];
            actions.forEach((action) => {
                const button = createButton(action.label || '', action.className || action.cls || 'btn btn-secondary', async (event) => {
                    if (button.disabled) return;
                    let result;
                    try {
                        result = typeof action.onClick === 'function'
                            ? await action.onClick({ event, panel, body, close, handle })
                            : undefined;
                    } catch (error) {
                        console.error(error);
                        LiveRegion.announce(error?.message || translate('common.error', 'Une erreur est survenue.'), 'assertive');
                        return;
                    }
                    if (action.close !== false && result !== false) close('action');
                });
                if (action.disabled) button.disabled = true;
                if (action.type) button.type = action.type;
                footer.appendChild(button);
            });
            if (!actions.length) footer.hidden = true;

            panel.append(header, body, footer);
            overlay.appendChild(panel);
            document.body.appendChild(overlay);

            const handle = {
                overlay,
                panel,
                body,
                footer,
                close,
                setTitle(value) { title.textContent = String(value == null ? '' : value); },
                setDescription(value) {
                    let node = panel.querySelector('#' + descriptionId);
                    if (!node && value) {
                        node = document.createElement('p');
                        node.id = descriptionId;
                        node.className = 'ms-panel__description';
                        copy.appendChild(node);
                        panel.setAttribute('aria-describedby', descriptionId);
                    }
                    if (node) node.textContent = String(value == null ? '' : value);
                }
            };

            let closed = false;
            function close(reason) {
                if (closed) return;
                const before = dispatch('magestock:panelbeforeclose', { reason, handle }, overlay, true);
                if (before.defaultPrevented) return;
                closed = true;
                overlay.classList.remove('is-open');
                OverlayStack.deactivate(overlay);
                global.setTimeout(() => {
                    overlay.remove();
                    Panel.instances.delete(handle);
                    if (typeof opts.onClose === 'function') opts.onClose({ reason, handle });
                }, 190);
            }

            overlay.addEventListener('click', (event) => {
                if (event.target === overlay && opts.closeOnBackdrop !== false) close('backdrop');
            });
            panel.addEventListener('click', (event) => {
                const closer = event.target.closest('[data-ms-panel-close]');
                if (closer) {
                    event.preventDefault();
                    close('content');
                }
            });

            this.instances.add(handle);
            global.requestAnimationFrame(() => {
                overlay.classList.add('is-open');
                OverlayStack.activate({
                    overlay,
                    dialog: panel,
                    trigger,
                    close,
                    closeOnEscape: opts.closeOnEscape !== false,
                    initialFocus: opts.initialFocus
                });
            });
            if (typeof opts.onOpen === 'function') opts.onOpen(handle);
            return handle;
        },
        closeAll() {
            Array.from(this.instances).reverse().forEach((handle) => handle.close('all'));
        }
    };

    const ActionSheet = {
        open(options) { return Panel.open(Object.assign({}, options || {}, { kind: 'sheet' })); }
    };

    const FilterDrawer = {
        open(options) { return Panel.open(Object.assign({}, options || {}, { kind: 'drawer' })); }
    };

    const DetailPage = {
        open(options) { return Panel.open(Object.assign({}, options || {}, { kind: 'page' })); },
        enhance(root) {
            const scope = resolveElement(root) || document;
            scope.querySelectorAll('[data-ms-detail-page]').forEach((page) => {
                if (page.dataset.msDetailReady === '1') return;
                page.dataset.msDetailReady = '1';
                const back = page.querySelector('[data-ms-detail-back]');
                if (back) {
                    back.addEventListener('click', (event) => {
                        const emitted = dispatch('magestock:detailback', { page }, page, true);
                        if (!emitted.defaultPrevented && back instanceof HTMLAnchorElement && back.href) return;
                        event.preventDefault();
                    });
                }
            });
        }
    };

    // ------------------------------------------------------------
    // Multi-step flow controller
    // ------------------------------------------------------------
    class FlowController {
        constructor(root, options) {
            this.root = resolveElement(root);
            if (!this.root) throw new Error('MobileFlow: conteneur introuvable.');
            this.options = options || {};
            this.steps = Array.from(this.root.querySelectorAll('[data-ms-flow-step]'));
            if (!this.steps.length) throw new Error('MobileFlow: aucune étape [data-ms-flow-step].');
            this.storageKey = this.options.storageKey || this.root.dataset.msFlowStorage || '';
            this.current = this.normalizeIndex(this.options.startStep || Number(this.root.dataset.msFlowStart || 1) - 1);
            this.boundClick = (event) => this.onClick(event);
            this.boundInput = () => this.persist();
            this.root.addEventListener('click', this.boundClick);
            this.root.addEventListener('input', this.boundInput, true);
            this.root.addEventListener('change', this.boundInput, true);
            this.prepare();
            if (this.options.restore !== false) this.restore();
            this.update(false);
            this.root.msFlowController = this;
        }
        normalizeIndex(index) {
            const parsed = Number(index);
            if (!Number.isFinite(parsed)) return 0;
            return Math.max(0, Math.min(this.steps.length - 1, Math.round(parsed)));
        }
        prepare() {
            this.root.classList.add('ms-flow');
            this.root.dataset.msFlowReady = '1';
            this.steps.forEach((step, index) => {
                if (!step.id) step.id = uid('ms-flow-step');
                step.setAttribute('role', 'group');
                const heading = step.querySelector('[data-ms-flow-title], h1, h2, h3, h4');
                if (heading) {
                    if (!heading.id) heading.id = uid('ms-flow-heading');
                    heading.setAttribute('tabindex', '-1');
                    step.setAttribute('aria-labelledby', heading.id);
                } else {
                    step.setAttribute('aria-label', translate('mobile.flow.step', 'Étape') + ' ' + (index + 1));
                }
            });
            this.indicators = Array.from(this.root.querySelectorAll('[data-ms-flow-indicator]'));
            this.progress = this.root.querySelector('[data-ms-flow-progress]');
            if (this.progress) {
                this.progress.setAttribute('role', 'progressbar');
                this.progress.setAttribute('aria-valuemin', '1');
                this.progress.setAttribute('aria-valuemax', String(this.steps.length));
            }
        }
        async onClick(event) {
            const next = event.target.closest('[data-ms-flow-next]');
            const back = event.target.closest('[data-ms-flow-back]');
            const go = event.target.closest('[data-ms-flow-go]');
            const reset = event.target.closest('[data-ms-flow-reset]');
            if (next && this.root.contains(next)) {
                event.preventDefault();
                await this.next();
            } else if (back && this.root.contains(back)) {
                event.preventDefault();
                await this.back();
            } else if (go && this.root.contains(go)) {
                event.preventDefault();
                const destination = Number(go.dataset.msFlowGo || 1) - 1;
                await this.goTo(destination, { validate: go.dataset.msFlowValidate !== 'false' });
            } else if (reset && this.root.contains(reset)) {
                event.preventDefault();
                this.reset();
            }
        }
        async validateCurrent() {
            const step = this.steps[this.current];
            const controls = Array.from(step.querySelectorAll('input, select, textarea'));
            for (const control of controls) {
                if (control.disabled || control.type === 'hidden') continue;
                if (typeof control.checkValidity === 'function' && !control.checkValidity()) {
                    if (typeof control.reportValidity === 'function') control.reportValidity();
                    try { control.focus({ preventScroll: false }); } catch (_) { control.focus(); }
                    LiveRegion.announce(control.validationMessage || translate('mobile.flow.invalid', 'Vérifiez les champs obligatoires.'), 'assertive');
                    return false;
                }
            }
            const validationEvent = dispatch('magestock:flowvalidate', {
                controller: this,
                step,
                stepIndex: this.current,
                stepNumber: this.current + 1
            }, this.root, true);
            if (validationEvent.defaultPrevented) return false;
            if (typeof this.options.validateStep === 'function') {
                const result = await this.options.validateStep({
                    controller: this,
                    step,
                    stepIndex: this.current,
                    stepNumber: this.current + 1
                });
                if (result === false) return false;
            }
            return true;
        }
        async goTo(index, options) {
            const destination = this.normalizeIndex(index);
            const opts = options || {};
            if (destination === this.current) return true;
            if (destination > this.current && opts.validate !== false) {
                const valid = await this.validateCurrent();
                if (!valid) return false;
            }
            const before = dispatch('magestock:flowbeforechange', {
                controller: this,
                from: this.current,
                to: destination,
                fromStep: this.steps[this.current],
                toStep: this.steps[destination]
            }, this.root, true);
            if (before.defaultPrevented) return false;
            if (typeof this.options.beforeChange === 'function') {
                const result = await this.options.beforeChange({ controller: this, from: this.current, to: destination });
                if (result === false) return false;
            }
            this.current = destination;
            this.update(opts.focus !== false);
            this.persist();
            if (typeof this.options.onChange === 'function') this.options.onChange({ controller: this, index: this.current, step: this.steps[this.current] });
            return true;
        }
        next() { return this.goTo(this.current + 1, { validate: true }); }
        back() { return this.goTo(this.current - 1, { validate: false }); }
        update(focus) {
            this.steps.forEach((step, index) => {
                const active = index === this.current;
                step.hidden = !active;
                step.toggleAttribute('inert', !active);
                step.setAttribute('aria-hidden', active ? 'false' : 'true');
                step.dataset.msFlowState = active ? 'current' : (index < this.current ? 'complete' : 'upcoming');
            });
            this.indicators.forEach((indicator, index) => {
                const declared = Number(indicator.dataset.msFlowIndicator || index + 1) - 1;
                const state = declared === this.current ? 'current' : (declared < this.current ? 'complete' : 'upcoming');
                indicator.dataset.msFlowState = state;
                indicator.classList.toggle('is-current', state === 'current');
                indicator.classList.toggle('is-complete', state === 'complete');
                if (state === 'current') indicator.setAttribute('aria-current', 'step');
                else indicator.removeAttribute('aria-current');
                if (indicator.tagName === 'BUTTON') indicator.disabled = declared > this.current && indicator.dataset.msFlowAllowFuture !== 'true';
            });
            if (this.progress) {
                this.progress.setAttribute('aria-valuenow', String(this.current + 1));
                this.progress.setAttribute('aria-valuetext', translate('mobile.flow.progress', 'Étape {current} sur {total}', {
                    current: this.current + 1,
                    total: this.steps.length
                }).replace('{current}', String(this.current + 1)).replace('{total}', String(this.steps.length)));
                this.progress.style.setProperty('--ms-flow-progress', ((this.current + 1) / this.steps.length * 100) + '%');
            }
            this.root.dataset.msFlowCurrent = String(this.current + 1);
            const currentOutput = this.root.querySelector('[data-ms-flow-current]');
            const totalOutput = this.root.querySelector('[data-ms-flow-total]');
            if (currentOutput) currentOutput.textContent = String(this.current + 1);
            if (totalOutput) totalOutput.textContent = String(this.steps.length);
            const step = this.steps[this.current];
            dispatch('magestock:flowchange', {
                controller: this,
                index: this.current,
                stepNumber: this.current + 1,
                total: this.steps.length,
                step
            }, this.root);
            LiveRegion.announce(translate('mobile.flow.progress', 'Étape {current} sur {total}', {
                current: this.current + 1,
                total: this.steps.length
            }).replace('{current}', String(this.current + 1)).replace('{total}', String(this.steps.length)));
            if (focus) {
                global.scrollTo({ top: 0, behavior: global.matchMedia?.('(prefers-reduced-motion: reduce)').matches ? 'auto' : 'smooth' });
                global.setTimeout(() => {
                    const heading = step.querySelector('[data-ms-flow-title], h1, h2, h3, h4');
                    if (heading) {
                        try { heading.focus({ preventScroll: true }); } catch (_) { heading.focus(); }
                    }
                }, 80);
            }
        }
        collect() {
            const result = {};
            this.root.querySelectorAll('input[name], select[name], textarea[name]').forEach((control) => {
                if (!control.name || control.type === 'password' || control.type === 'file') return;
                if ((control.type === 'checkbox' || control.type === 'radio') && !control.checked) {
                    if (control.type === 'checkbox' && !(control.name in result)) result[control.name] = false;
                    return;
                }
                if (control.type === 'checkbox') {
                    if (control.value && control.value !== 'on') {
                        if (!Array.isArray(result[control.name])) result[control.name] = [];
                        result[control.name].push(control.value);
                    } else result[control.name] = true;
                } else if (control.type === 'radio') {
                    result[control.name] = control.value;
                } else if (control.multiple) {
                    result[control.name] = Array.from(control.selectedOptions).map((option) => option.value);
                } else result[control.name] = control.value;
            });
            return result;
        }
        apply(values) {
            if (!values || typeof values !== 'object') return;
            this.root.querySelectorAll('input[name], select[name], textarea[name]').forEach((control) => {
                if (!(control.name in values) || control.type === 'password' || control.type === 'file') return;
                const value = values[control.name];
                if (control.type === 'checkbox') {
                    control.checked = Array.isArray(value) ? value.map(String).includes(String(control.value)) : !!value;
                } else if (control.type === 'radio') {
                    control.checked = String(control.value) === String(value);
                } else if (control.multiple && Array.isArray(value)) {
                    Array.from(control.options).forEach((option) => { option.selected = value.map(String).includes(String(option.value)); });
                } else control.value = value == null ? '' : String(value);
            });
        }
        persist() {
            if (!this.storageKey) return;
            try {
                sessionStorage.setItem(this.storageKey, JSON.stringify({
                    version: 1,
                    step: this.current + 1,
                    values: this.collect(),
                    savedAt: new Date().toISOString()
                }));
            } catch (_) {}
        }
        restore() {
            if (!this.storageKey) return false;
            try {
                const raw = sessionStorage.getItem(this.storageKey);
                if (!raw) return false;
                const draft = JSON.parse(raw);
                this.apply(draft.values || {});
                this.current = this.normalizeIndex(Number(draft.step || 1) - 1);
                dispatch('magestock:flowrestore', { controller: this, draft }, this.root);
                return true;
            } catch (_) { return false; }
        }
        clearDraft() {
            if (!this.storageKey) return;
            try { sessionStorage.removeItem(this.storageKey); } catch (_) {}
        }
        reset() {
            this.root.querySelectorAll('form').forEach((form) => form.reset());
            this.clearDraft();
            this.current = 0;
            this.update(true);
            dispatch('magestock:flowreset', { controller: this }, this.root);
        }
        destroy() {
            this.root.removeEventListener('click', this.boundClick);
            this.root.removeEventListener('input', this.boundInput, true);
            this.root.removeEventListener('change', this.boundInput, true);
            delete this.root.msFlowController;
        }
    }

    const MobileFlow = {
        create(root, options) {
            const element = resolveElement(root);
            if (!element) throw new Error('MobileFlow: conteneur introuvable.');
            if (element.msFlowController) return element.msFlowController;
            return new FlowController(element, options);
        },
        enhance(root) {
            const scope = resolveElement(root) || document;
            const controllers = [];
            scope.querySelectorAll('[data-ms-flow]').forEach((element) => {
                if (element.msFlowController) controllers.push(element.msFlowController);
                else {
                    try { controllers.push(new FlowController(element)); }
                    catch (error) { console.warn('[MageStock MobileFlow]', error); }
                }
            });
            return controllers;
        },
        FlowController
    };

    // ------------------------------------------------------------
    // Native entity card builder
    // ------------------------------------------------------------
    const EntityCard = {
        create(options) {
            const opts = options || {};
            const article = document.createElement(opts.tagName || 'article');
            article.className = 'ms-entity-card' + (opts.className ? ' ' + opts.className : '');
            if (opts.state) article.dataset.state = String(opts.state);
            if (opts.id) article.dataset.entityId = String(opts.id);

            const header = document.createElement('header');
            header.className = 'ms-entity-card__header';
            const identity = document.createElement('div');
            identity.className = 'ms-entity-card__identity';
            if (opts.eyebrow) {
                const eyebrow = document.createElement('span');
                eyebrow.className = 'ms-entity-card__eyebrow';
                eyebrow.textContent = String(opts.eyebrow);
                identity.appendChild(eyebrow);
            }
            const title = document.createElement(opts.headingTag || 'h3');
            title.className = 'ms-entity-card__title';
            setTextOrNode(title, opts.title || '');
            identity.appendChild(title);
            if (opts.meta) {
                const meta = document.createElement('p');
                meta.className = 'ms-entity-card__meta';
                setTextOrNode(meta, opts.meta);
                identity.appendChild(meta);
            }
            header.appendChild(identity);
            if (opts.badge) {
                const badge = document.createElement('span');
                badge.className = 'ms-entity-card__badge' + (opts.badgeClass ? ' ' + opts.badgeClass : '');
                setTextOrNode(badge, opts.badge);
                header.appendChild(badge);
            }
            article.appendChild(header);

            const fields = Array.isArray(opts.fields) ? opts.fields : [];
            if (fields.length) {
                const list = document.createElement('dl');
                list.className = 'ms-entity-card__fields';
                fields.forEach((field) => {
                    if (!field || field.hidden) return;
                    const item = document.createElement('div');
                    item.className = 'ms-entity-card__field' + (field.className ? ' ' + field.className : '');
                    const term = document.createElement('dt');
                    term.textContent = field.label == null ? '' : String(field.label);
                    const definition = document.createElement('dd');
                    setTextOrNode(definition, field.value);
                    item.append(term, definition);
                    list.appendChild(item);
                });
                article.appendChild(list);
            }

            if (opts.content) {
                const content = document.createElement('div');
                content.className = 'ms-entity-card__content';
                setTextOrNode(content, opts.content);
                article.appendChild(content);
            }

            const actions = Array.isArray(opts.actions) ? opts.actions : [];
            if (actions.length) {
                const footer = document.createElement('footer');
                footer.className = 'ms-entity-card__actions';
                actions.forEach((action) => {
                    if (action instanceof Node) {
                        footer.appendChild(action);
                        return;
                    }
                    const button = createButton(action.label || '', action.className || action.cls || 'btn btn-secondary', (event) => {
                        if (typeof action.onClick === 'function') action.onClick({ event, card: article, item: opts.item });
                    });
                    if (action.ariaLabel) button.setAttribute('aria-label', action.ariaLabel);
                    footer.appendChild(button);
                });
                article.appendChild(footer);
            }
            return article;
        },
        renderList(container, items, mapper, options) {
            const root = resolveElement(container);
            if (!root) return [];
            const fragment = document.createDocumentFragment();
            const cards = [];
            (Array.isArray(items) ? items : []).forEach((item, index) => {
                const config = typeof mapper === 'function' ? mapper(item, index) : item;
                const card = config instanceof Node ? config : this.create(Object.assign({}, config || {}, { item }));
                fragment.appendChild(card);
                cards.push(card);
            });
            if (options?.append !== true) root.innerHTML = '';
            root.appendChild(fragment);
            return cards;
        }
    };

    // ------------------------------------------------------------
    // Disclosures and detail sections
    // ------------------------------------------------------------
    const Disclosure = {
        enhance(root) {
            const scope = resolveElement(root) || document;
            scope.querySelectorAll('[data-ms-disclosure]').forEach((button) => {
                if (button.dataset.msDisclosureReady === '1') return;
                const targetRef = button.getAttribute('aria-controls') || button.dataset.msDisclosure;
                const target = targetRef ? document.getElementById(targetRef.replace(/^#/, '')) || scope.querySelector(targetRef.startsWith('#') ? targetRef : '#' + targetRef) : null;
                if (!target) return;
                button.dataset.msDisclosureReady = '1';
                if (!button.hasAttribute('aria-expanded')) button.setAttribute('aria-expanded', target.hidden ? 'false' : 'true');
                button.addEventListener('click', () => {
                    const open = button.getAttribute('aria-expanded') !== 'true';
                    button.setAttribute('aria-expanded', open ? 'true' : 'false');
                    target.hidden = !open;
                    target.setAttribute('aria-hidden', open ? 'false' : 'true');
                    dispatch('magestock:disclosurechange', { button, target, open }, button);
                });
            });
        }
    };

    // ------------------------------------------------------------
    // Sticky action bars opt-in
    // ------------------------------------------------------------
    const StickyActions = {
        items: new Set(),
        observer: null,
        init() {
            if ('ResizeObserver' in global && !this.observer) {
                this.observer = new ResizeObserver(() => this.refresh());
            }
        },
        register(element) {
            const root = resolveElement(element);
            if (!root || this.items.has(root)) return root;
            root.classList.add('ms-sticky-actions');
            this.items.add(root);
            this.observer?.observe(root);
            this.refresh();
            return root;
        },
        unregister(element) {
            const root = resolveElement(element);
            if (!root) return;
            this.items.delete(root);
            this.observer?.unobserve(root);
            this.refresh();
        },
        enhance(root) {
            const scope = resolveElement(root) || document;
            scope.querySelectorAll('[data-ms-sticky-actions]').forEach((element) => this.register(element));
        },
        refresh() {
            let height = 0;
            this.items.forEach((item) => {
                if (!document.contains(item)) {
                    this.items.delete(item);
                    return;
                }
                if (isVisible(item)) height = Math.max(height, Math.round(item.getBoundingClientRect().height));
            });
            document.documentElement.style.setProperty('--ms-sticky-actions-height', height + 'px');
        }
    };

    // ------------------------------------------------------------
    // Declarative panel triggers for future page lots
    // ------------------------------------------------------------
    const DeclarativePanels = {
        initialized: false,
        init() {
            if (this.initialized) return;
            this.initialized = true;
            document.addEventListener('click', (event) => {
                const trigger = event.target.closest('[data-ms-open-panel]');
                if (!trigger) return;
                const reference = trigger.dataset.msOpenPanel;
                const template = reference ? document.querySelector(reference) : null;
                if (!template) return;
                event.preventDefault();
                const content = template instanceof HTMLTemplateElement
                    ? template.content.cloneNode(true)
                    : template.cloneNode(true);
                const kind = trigger.dataset.msPanelKind || 'sheet';
                Panel.open({
                    kind,
                    title: trigger.dataset.msPanelTitle || trigger.getAttribute('aria-label') || trigger.textContent.trim(),
                    description: trigger.dataset.msPanelDescription || '',
                    content,
                    trigger,
                    className: trigger.dataset.msPanelClass || ''
                });
            });
        }
    };

    // ------------------------------------------------------------
    // Foundation bootstrap
    // ------------------------------------------------------------
    function enhance(root) {
        MobileFlow.enhance(root);
        Disclosure.enhance(root);
        DetailPage.enhance(root);
        StickyActions.enhance(root);
    }

    function init() {
        if (document.documentElement.dataset.msMobileFoundation === VERSION) return;
        document.documentElement.dataset.msMobileFoundation = VERSION;
        document.documentElement.classList.add('ms-mobile-foundation');
        LiveRegion.init();
        StickyActions.init();
        Viewport.init();
        LegacyModalBridge.patch();
        DeclarativePanels.init();
        enhance(document);

        const observer = new MutationObserver((mutations) => {
            mutations.forEach((mutation) => {
                mutation.addedNodes.forEach((node) => {
                    if (!(node instanceof Element)) return;
                    enhance(node);
                });
            });
        });
        observer.observe(document.body, { childList: true, subtree: true });
        global.addEventListener('beforeunload', () => observer.disconnect(), { once: true });
        dispatch('magestock:mobilefoundationready', { version: VERSION, api: MobileUI });
    }

    const MobileUI = {
        version: VERSION,
        init,
        enhance,
        t: translate,
        escapeHtml,
        announce: (message, priority) => LiveRegion.announce(message, priority),
        device: Device,
        viewport: Viewport,
        overlays: OverlayStack,
        dialog: {
            patchLegacy: () => LegacyModalBridge.patch(),
            close: () => global.Modal?.close?.(),
            onClose: (callback) => global.Modal?.onClose?.(callback)
        },
        flow: MobileFlow,
        sheet: ActionSheet,
        drawer: FilterDrawer,
        panel: Panel,
        detail: DetailPage,
        card: EntityCard,
        disclosure: Disclosure,
        sticky: StickyActions
    };

    global.MageStockMobileUI = MobileUI;
    global.MobileFlow = MobileFlow;
    global.ActionSheet = ActionSheet;
    global.FilterDrawer = FilterDrawer;
    global.MobileDetailPage = DetailPage;
    global.MobileEntityCard = EntityCard;

    if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init, { once: true });
    else init();
})(window, document);
