<?php

namespace Components\Ui;

use Components\Base;
use Components\Support\Devices;

/**
 * DevicePreviewer page component.
 *
 * Generic device preview UI shared by Email preview and Web preview.
 *
 * Modes:
 * - email: keeps current EmailPreviewer behaviour (mail header/subject + images toggle + `lng_id`)
 * - web:   no mail header/subject, no images toggle, uses `lng_id` (same as email)
 *
 * Options:
 * - mode: 'email'|'web' (default 'email')
 * - url: string initial URL to load (optional)
 * - device: device id from `Devices::previewDevices()` (default 'desktop')
 * - id_prefix: string unique prefix for element ids to avoid collisions (default 'dp')
 * - modal_id: string when rendered inside a modal (optional)
 * - allow_mockup: bool (default true)
 * - allow_images_toggle: bool (default depends on mode; email=true, web=false)
 * - show_images: bool (default true)
 * - allow_mail_header: bool (default depends on mode; email=true, web=false)
 * - persona_control_html: string raw HTML for custom persona control (optional)
 */
class DevicePreviewer
{
    public static function render(array $opts = []): string
    {
        $mode = isset($opts['mode']) ? (string)$opts['mode'] : 'email';
        if ($mode !== 'web') {
            $mode = 'email';
        }

        $url = isset($opts['url']) ? (string)$opts['url'] : '';
        $device = isset($opts['device']) ? (string)$opts['device'] : 'desktop';
        $idPrefix = isset($opts['id_prefix']) && $opts['id_prefix'] !== '' ? (string)$opts['id_prefix'] : 'dp';
        $modalId = isset($opts['modal_id']) && $opts['modal_id'] !== '' ? (string)$opts['modal_id'] : '';

        // Make availability of Mockup toggle optional (default: true)
        $allowMockup = array_key_exists('allow_mockup', $opts) ? (bool)$opts['allow_mockup'] : true;

        // Make availability of Images toggle optional
        $allowImagesToggleDefault = ($mode === 'email');
        $allowImagesToggle = array_key_exists('allow_images_toggle', $opts)
            ? (bool)$opts['allow_images_toggle']
            : $allowImagesToggleDefault;

        // Option to show/hide images by default
        $showImages = array_key_exists('show_images', $opts) ? (bool)$opts['show_images'] : true;
        if ($mode === 'web') {
            // Web preview must not offer (or default to) hiding images.
            $showImages = true;
        }

        $allowPersonaSelection = ($mode === 'email' && \mh_cfg('MAILBEEZ_FILTER_ADD_GENDER_STATUS') === 'True');

        // Email-only: allow rendering an email app header/subject (outside and inside the iframe)
        $allowMailHeaderDefault = ($mode === 'email');
        $allowMailHeader = array_key_exists('allow_mail_header', $opts)
            ? (bool)$opts['allow_mail_header']
            : $allowMailHeaderDefault;

        // Language query param (keep consistent across modes)
        $languageParam = 'lng_id';

        // Optional: when rendered inside a BeezUI modal that exposes dedicated header toolbar containers,
        // the preview controls can be teleported into the right-aligned toolbar slot.
        $headerTargetId = isset($opts['header_target']) && $opts['header_target'] !== ''
            ? (string)$opts['header_target']
            : $modalId . '-header';

        // By default we target the right-hand toolbar slot exposed by the Modal header (ToolbarRight).
        $headerToolbarId = $modalId !== '' ? $modalId . '-header-toolbar-right' : '';

        // Basic sanitation for the initial URL
        $safeUrl = '';
        if ($url !== '') {
            $u = filter_var($url, FILTER_VALIDATE_URL);
            if ($u && ((strpos($u, 'http://') === 0) || (strpos($u, 'https://') === 0))) {
                $safeUrl = $u;
            }
        }

        $overlayId = $idPrefix . '-overlay';
        $idMaster = $idPrefix . '-frame-master';
        $idPlainDesktop = $idPrefix . '-frame-plain-desktop';
        $idPlainTablet = $idPrefix . '-frame-plain-tablet';
        $idPlainMobile = $idPrefix . '-frame-plain-mobile';
        $idPlainMobileSmall = $idPrefix . '-frame-plain-mobile-small';
        $idMockDesktop = $idPrefix . '-frame-mock-desktop';
        $idMockTablet = $idPrefix . '-frame-mock-tablet';
        $idMockMobile = $idPrefix . '-frame-mock-mobile';
        $idMockMobileSmall = $idPrefix . '-frame-mock-mobile-small';

        // Languages from store system
        $defaultLanguageId = \mh_session('languages_id', '2');
        $langItems = [];
        if (function_exists('mh_get_language_list')) {
            try {
                $langItems = (array)\mh_get_language_list();
            } catch (\Throwable $e) {
                $langItems = [];
            }
        }

        // Normalize
        $languages = [];
        foreach ($langItems as $li) {
            if (!is_array($li)) {
                continue;
            }
            $id = isset($li['id']) ? (int)$li['id'] : (isset($li['languages_id']) ? (int)$li['languages_id'] : 0);
            if ($id <= 0) {
                continue;
            }
            $code = isset($li['code']) ? (string)$li['code'] : '';
            $text = isset($li['text']) ? (string)$li['text'] : ($code !== '' ? strtoupper($code) : ('#' . $id));
            $languages[] = ['id' => $id, 'code' => $code, 'text' => $text];
        }
        if ($defaultLanguageId === null && !empty($languages)) {
            $defaultLanguageId = (int)$languages[0]['id'];
        }
        $languagesJson = json_encode($languages);

        // Prepare rich options for language selection using circle flag icons
        $richLanguages = [];
        foreach ($languages as $lang) {
            $richLanguages[$lang['id']] = [
                'label'      => $lang['text'],
                'label_html' => CollectionIdentity::render(
                    Base::h($lang['text']),
                    null,
                    [
                        'icon' => \Components\Form\BaseField::flagIconHtml($lang['code'], 'text-lg align-baseline', 'circle'),
                        'size' => 'xs',
                        'dot_size_class' => 'size-5',
                    ]
                )
            ];
        }

        // Shared device presets for the previewer (widths + ids)
        $previewDevices = Devices::previewDevices();
        $sizes = [];
        $firstDeviceId = 'desktop';
        foreach ($previewDevices as $devCfg) {
            if (!is_array($devCfg)) {
                continue;
            }
            $id = isset($devCfg['id']) ? (string)$devCfg['id'] : '';
            if ($id === '') {
                continue;
            }
            if ($firstDeviceId === 'desktop') {
                $firstDeviceId = $id;
            }
            $size = Devices::previewSize($id);
            $sizes[$id] = [
                'width' => $size['width'],
                'maxWidth' => $size['max_width'],
            ];
        }

        // Determine the initial device in a safe way
        $initialDevice = $device;
        if (!isset($sizes[$initialDevice])) {
            $initialDevice = isset($sizes[$firstDeviceId]) ? $firstDeviceId : (isset($sizes['desktop']) ? 'desktop' : 'desktop');
        }

        $sizesJson = json_encode($sizes);

        ob_start();
        ?>
        <div data-device-preview-root="1"
             data-email-preview-root="1"
             data-dp-mode="<?= Base::h($mode, false) ?>"
             data-dp-language-param="<?= Base::h($languageParam, false) ?>"
             data-dp-allow-mail-header="<?= Base::h($allowMailHeader ? '1' : '0', false) ?>"
             data-ep-id-prefix="<?= Base::h($idPrefix, false) ?>"
             data-ep-overlay-id="<?= Base::h($overlayId, false) ?>"
             data-ep-languages='<?= Base::h((string)$languagesJson, false) ?>'
             data-ep-default-language-id='<?= Base::h((string)($defaultLanguageId ?? 2), false) ?>'
             data-ep-allow-mockup='<?= Base::h($allowMockup ? '1' : '0', false) ?>'
             data-ep-allow-gender='<?= Base::h($allowPersonaSelection ? '1' : '0', false) ?>'
             data-ep-allow-images-toggle='<?= Base::h($allowImagesToggle ? '1' : '0', false) ?>'
             data-ep-show-images='<?= Base::h($showImages ? '1' : '0', false) ?>'
             data-ep-sizes='<?= Base::h((string)$sizesJson, false) ?>'
             data-ep-default-device='<?= Base::h($initialDevice, false) ?>'
             data-ep-mobile-header-template='<?= Base::h(($allowMailHeader ? self::mobileHeaderTemplate() : ''), false) ?>'
             data-ep-subject-skeleton='<?= Base::h(($allowMailHeader ? self::subjectSkeletonTemplate() : ''), false) ?>'
             data-ep-subject-warning-badge='<?= Base::h(($allowMailHeader ? \Components\Ui\EmailEditor::showSubjectRequiredWarning() : ''), false) ?>'
             x-data="previewer()"
             x-init="init(<?= Base::h(json_encode($safeUrl), false) ?>, <?= Base::h(json_encode($initialDevice), false) ?>)"
             class="space-y-6n relative h-full flex flex-col">

            <?php
            $personaControlHtml = $opts['persona_control_html'] ?? $opts['gender_control_html'] ?? '';
            $controlsHtml = self::renderHeaderToolbar($previewDevices, $allowPersonaSelection, $personaControlHtml, $richLanguages, $defaultLanguageId);

            if ($headerToolbarId !== '') {
                echo '<template x-teleport="#' . Base::h($headerToolbarId, false) . '">';
                echo $controlsHtml;
                echo '</template>';
            } else {
                echo $controlsHtml;
            }
            ?>

            <?php if ($allowMailHeader): ?>
            <!-- Subject bar (outside iframe when mockups are disabled) -->
            <div x-show="!isMockupActive && (subjectDisplay || awaitingMailInfo || mailInfoReceived)"
                 x-cloak
                 class="px-4 py-2 border-b border-divider bg-white/90 text-sm font-semibold text-gray-800 flex items-center gap-2 overflow-hidden">
                <span x-show="subjectDisplay" x-text="subjectDisplay" class="truncate"></span>
                <div x-show="!subjectDisplay && awaitingMailInfo" class="shrink-0" x-html="subjectSkeletonHtml"></div>
                <div x-show="!subjectDisplay && !awaitingMailInfo && mailInfoReceived" class="shrink-0">
                    <?= \Components\Ui\EmailEditor::showSubjectRequiredWarning(); ?>
                </div>
            </div>
            <?php endif; ?>

            <!-- Main preview area -->
            <div class="relative overflow-hidden flex-1">

                <!-- Pattern background -->
                <svg class="absolute inset-0 rounded-xl size-full stroke-divider/90 dark:stroke-muted/10 pointer-events-none z-0" fill="none" aria-hidden="true">
                    <defs>
                        <pattern id="pattern-<?php echo Base::h($idPrefix, false); ?>" x="0" y="0" width="8" height="8" patternUnits="userSpaceOnUse">
                            <path d="M-1 5L5 -1M3 9L8.5 3.5" stroke-width="0.5"></path>
                        </pattern>
                    </defs>
                    <rect stroke="none" fill="url(#pattern-<?php echo Base::h($idPrefix, false); ?>)" width="100%" height="100%"></rect>
                </svg>

                <!-- Loading overlay over the preview area -->
                <?php echo Base::loaderOverlay($overlayId, 'z-20', ''); ?>
                <div class="mx-auto relative z-10 h-full grid grid-cols-1 grid-rows-1" :style="previewStyle">

                    <!-- Hidden master iframe (load once per language) -->
                    <iframe id="<?= Base::h($idMaster, false) ?>" class="hidden"
                            ></iframe>

                    <!-- Skeleton Loader -->
                    <div class="col-start-1 row-start-1 h-full overflow-hidden transition-opacity duration-200 ease-out pointer-events-none"
                         x-cloak
                         :class="loading ? 'opacity-100' : 'opacity-0'">
                        <?= Base::skeleton() ?>
                    </div>

                    <!-- Plain iframes -->
                    <div x-show="!isMockupActive"
                         :class="loading ? 'invisible absolute opacity-0 pointer-events-none' : 'opacity-100'"
                         class="h-full col-start-1 row-start-1 transition-opacity duration-200 ease-out">
                        <div class="h-full grid grid-cols-1 grid-rows-1">
                            <div x-show="device==='desktop'" class="col-start-1 row-start-1 h-full">
                                <iframe id="<?= Base::h($idPlainDesktop, false) ?>" data-preview="desktop"
                                        
                                        class="block w-full h-full bg-white"></iframe>
                            </div>
                            <div x-show="device==='tablet'"  class="col-start-1 row-start-1 h-full">
                                <iframe id="<?= Base::h($idPlainTablet, false) ?>" data-preview="tablet"
                                        
                                        class="block w-full h-full bg-white"></iframe>
                            </div>
                            <div x-show="device==='mobile'"  class="col-start-1 row-start-1 h-full">
                                <iframe id="<?= Base::h($idPlainMobile, false) ?>" data-preview="mobile"
                                        
                                        class="block w-full h-full bg-white"></iframe>
                            </div>
                            <div x-show="device==='mobile-small'"  class="col-start-1 row-start-1 h-full">
                                <iframe id="<?= Base::h($idPlainMobileSmall, false) ?>" data-preview="mobile-small"
                                        
                                        class="block w-full h-full bg-white"></iframe>
                            </div>
                        </div>
                    </div>

                    <!-- Mockups -->
                    <div x-show="isMockupActive"
                         :class="loading ? 'invisible absolute opacity-0 pointer-events-none' : 'opacity-100'"
                         class="h-full col-start-1 row-start-1 transition-opacity duration-200 ease-out">
                        <div class="h-full grid grid-cols-1 grid-rows-1 items-center">
                            <div x-show="device==='mobile'"  class="col-start-1 row-start-1">
                                <?= DeviceMockups::mobile(
                                        '<iframe id="' . Base::h($idMockMobile, false) . '" data-preview="mobile"  class="w-full h-full bg-white"></iframe>'
                                ) ?>
                            </div>
                            <div x-show="device==='mobile-small'"  class="col-start-1 row-start-1">
                                <?= DeviceMockups::mobileSmall(
                                        '<iframe id="' . Base::h($idMockMobileSmall, false) . '" data-preview="mobile-small"  class="w-full h-full bg-white"></iframe>'
                                ) ?>
                            </div>
                        </div>
                    </div>

                </div>
            </div>

            <!-- frame width indicator -->
            <div class="absolute -bottom-4 opacity-30 hover:opacity-100 z-100 w-full flex justify-center shrink-0" x-cloak>
                <div class="flex items-center gap-3 w-full" :style="indicatorStyle">
                    <div class="flex-1 h-px bg-gray-400/50 relative text-gray-500/50">
                        <div class="absolute left-0 top-1/2 -translate-y-1/2">
                            <svg xmlns="http://www.w3.org/2000/svg" viewBox="8 5 6 10" stroke-width="1" fill="currentColor" class="h-2.5 w-1.5 rotate-180">
                                <path fill-rule="evenodd" d="M8.22 5.22a.75.75 0 0 1 1.06 0l4.25 4.25a.75.75 0 0 1 0 1.06l-4.25 4.25a.75.75 0 0 1-1.06-1.06L11.94 10 8.22 6.28a.75.75 0 0 1 0-1.06Z" clip-rule="evenodd"></path>
                            </svg>
                        </div>
                    </div>
                    <div class="text-[10px] font-mono text-gray-500 whitespace-nowrap bg-white/50 px-1 dark:bg-gray-800/50" x-text="frameWidthDisplay"></div>
                    <div class="flex-1 h-px bg-gray-400/50 relative text-gray-500/50">
                        <div class="absolute right-0 top-1/2 -translate-y-1/2">
                            <svg xmlns="http://www.w3.org/2000/svg" viewBox="8 5 6 10" stroke-width="1" fill="currentColor" class="h-2.5 w-1.5">
                                <path fill-rule="evenodd" d="M8.22 5.22a.75.75 0 0 1 1.06 0l4.25 4.25a.75.75 0 0 1 0 1.06l-4.25 4.25a.75.75 0 0 1-1.06-1.06L11.94 10 8.22 6.28a.75.75 0 0 1 0-1.06Z" clip-rule="evenodd"></path>
                            </svg>
                        </div>
                    </div>
                </div>
            </div>

            <script>
                function previewer() {
                    return {
                        // Mode + URL param key
                        mode: 'email',
                        languageParam: 'lng_id',
                        allowMailHeader: true,

                        // Configurable identifiers to avoid collisions when used multiple times
                        idPrefix: null,
                        overlayId: null,

                        url: '',
                        device: 'desktop',
                        gender: 'unknown',
                        allowGender: false,
                        languageId: null,
                        languages: [],
                        // When true, languageId is derived from URL param and the selector is hidden
                        languageLocked: false,
                        mailInfo: null,
                        mailInfoReceived: false,
                        lastSubject: '',
                        subjectSkeletonHtml: '',
                        subjectWarningBadgeHtml: '',

                        sizes: {},

                        showMockup: true,
                        allowMockup: true,
                        allowImagesToggle: true,
                        showImages: true,
                        loading: false,

                        _masterHandlersBound: false,
                        _lastLoadSrc: null,
                        _previewRenderSeq: 0,

                        <?= \Components\Ui\ScaleControl::jsData() ?>,
                        togglingMockup: false,
                        lastMockupActive: false,

                        get isMockupActive() {
                            return this.showMockup && this.allowMockup && (this.device === 'mobile' || this.device === 'mobile-small');
                        },

                        get subjectDisplay() {
                            try {
                                if (this.mailInfo && typeof this.mailInfo.subject !== 'undefined') {
                                    return String(this.mailInfo.subject || '').trim();
                                }
                                return String(this.lastSubject || '').trim();
                            } catch (e) {
                                return '';
                            }
                        },

                        get awaitingMailInfo() {
                            try {
                                if (!this.allowMailHeader || this.mailInfoReceived) return false;
                                const src = String(this.finalSrc || '');
                                return src !== '' && src !== 'about:blank';
                            } catch (e) {
                                return false;
                            }
                        },

                        get ids() {
                            const p = this.idPrefix || 'dp';
                            return {
                                master: p + '-frame-master',
                                plainDesktop: p + '-frame-plain-desktop',
                                plainTablet: p + '-frame-plain-tablet',
                                plainMobile: p + '-frame-plain-mobile',
                                plainMobileSmall: p + '-frame-plain-mobile-small',
                                mockDesktop: p + '-frame-mock-desktop',
                                mockTablet: p + '-frame-mock-tablet',
                                mockMobile: p + '-frame-mock-mobile',
                                mockMobileSmall: p + '-frame-mock-mobile-small',
                            };
                        },

                        get frameWidthDisplay() {
                            const size = (this.sizes && this.sizes[this.device])
                                ? this.sizes[this.device]
                                : {width: '100%', maxWidth: '100%'};
                            return size.width;
                        },

                        get finalSrc() {
                            if (!this.url) return 'about:blank';
                            try {
                                const u = new URL(this.url, window.location.href);
                                if (this.languageId) {
                                    // Always use numeric language id (align web preview with email preview)
                                    u.searchParams.delete('language');
                                    u.searchParams.set(this.languageParam || 'lng_id', String(this.languageId));
                                }
                                if (this.allowGender && this.gender) {
                                    u.searchParams.set('gnd', String(this.gender));
                                }
                                return u.toString();
                            } catch (e) {
                                return this.url;
                            }
                        },

                        get previewStyle() {
                            const s = Number(this.scale) || 1.0;
                            const size = (this.sizes && this.sizes[this.device])
                                ? this.sizes[this.device]
                                : {width: '100%', maxWidth: '100%'};

                            let width = size.width;
                            let maxWidth = size.maxWidth;
                            let height = '100%';

                            if (this.isMockupActive) {
                                width = '100%';
                                maxWidth = 'none';
                            } else {
                                if (s !== 1.0) {
                                    height = (100 / s) + '%';
                                }
                            }

                            const transitions = [];
                            if (!this.togglingMockup) {
                                transitions.push('transform 200ms ease-in-out');
                                transitions.push('width 200ms ease-in-out');
                                transitions.push('max-width 200ms ease-in-out');
                                transitions.push('height 200ms ease-in-out');
                            }

                            return {
                                width: width,
                                maxWidth: maxWidth,
                                height: height,
                                transform: `scale(${s})`,
                                transformOrigin: this.isMockupActive ? 'center center' : 'top center',
                                transition: transitions.join(', ')
                            };
                        },

                        get indicatorStyle() {
                            const s = Number(this.scale) || 1.0;
                            const ps = this.previewStyle;
                            const size = (this.sizes && this.sizes[this.device])
                                ? this.sizes[this.device]
                                : {width: '100%', maxWidth: '100%'};

                            const scaleValue = (valStr, factor) => {
                                if (!valStr || valStr === 'none') return 'none';
                                if (valStr.endsWith('%')) {
                                    return (parseFloat(valStr) * factor) + '%';
                                }
                                if (valStr.endsWith('px')) {
                                    return (parseFloat(valStr) * factor) + 'px';
                                }
                                return valStr;
                            };

                            return {
                                width: scaleValue(size.width, s),
                                maxWidth: scaleValue(size.maxWidth, s),
                                transition: ps.transition
                            };
                        },

                        init(u, d) {
                            try {
                                const root = this.$root;
                                this.idPrefix = root.getAttribute('data-ep-id-prefix') || 'dp';
                                this.overlayId = root.getAttribute('data-ep-overlay-id') || (this.idPrefix + '-overlay');
                                this.mode = root.getAttribute('data-dp-mode') || 'email';
                                this.languageParam = root.getAttribute('data-dp-language-param') || 'lng_id';
                                const allowMailAttr = root.getAttribute('data-dp-allow-mail-header') || '1';
                                this.allowMailHeader = allowMailAttr === '1' || allowMailAttr === 'true' || allowMailAttr === 'yes';

                                const allowGenderAttr = root.getAttribute('data-ep-allow-gender') || '0';
                                this.allowGender = allowGenderAttr === '1' || allowGenderAttr === 'true' || allowGenderAttr === 'yes';

                                const langsAttr = root.getAttribute('data-ep-languages') || '[]';
                                try {
                                    this.languages = JSON.parse(langsAttr) || [];
                                } catch (parseErr) {
                                    this.languages = [];
                                }
                                const defIdAttr = root.getAttribute('data-ep-default-language-id') || '0';
                                const defId = parseInt(defIdAttr, 10);
                                this.languageId = Number.isFinite(defId) && defId > 0 ? defId : (this.languages[0]?.id || null);
                                this.languageLocked = false;

                                const allowMockAttr = root.getAttribute('data-ep-allow-mockup');
                                this.allowMockup = allowMockAttr === '1' || allowMockAttr === 'true' || allowMockAttr === 'yes';
                                const allowImagesToggleAttr = root.getAttribute('data-ep-allow-images-toggle');
                                this.allowImagesToggle = allowImagesToggleAttr === '1' || allowImagesToggleAttr === 'true' || allowImagesToggleAttr === 'yes';
                                const showImagesAttr = root.getAttribute('data-ep-show-images');
                                this.showImages = showImagesAttr !== '0' && showImagesAttr !== 'false' && showImagesAttr !== 'no';

                                const sizesAttr = root.getAttribute('data-ep-sizes') || '{}';
                                try {
                                    const parsedSizes = JSON.parse(sizesAttr) || {};
                                    this.sizes = parsedSizes && typeof parsedSizes === 'object' ? parsedSizes : {};
                                } catch (eSizes) {
                                    this.sizes = {};
                                }

                                this.mobileHeaderTemplate = root.getAttribute('data-ep-mobile-header-template') || '';
                                this.subjectSkeletonHtml = root.getAttribute('data-ep-subject-skeleton') || '';
                                this.subjectWarningBadgeHtml = root.getAttribute('data-ep-subject-warning-badge') || '';
                            } catch (e) {
                                this.idPrefix = 'dp';
                                this.overlayId = 'dp-overlay';
                            }

                            // Email-only integrations for subject/header info
                            if (this.allowMailHeader) {
                                try { window.mbEmailPreviewer = this; } catch (e) {}
                                try {
                                    window.addEventListener('message', (ev) => {
                                        try {
                                            if (ev && ev.data && ev.data.type === 'mb:mail-info' && ev.data.info) {
                                                this.setMailInfo(ev.data.info);
                                            }
                                        } catch (e2) {}
                                    });
                                } catch (e3) {}
                            }

                            // Listen for external URL set events from integrations (e.g., modal)
                            try {
                                const onSetUrl = (ev) => {
                                    try {
                                        const u2 = ev && ev.detail ? (ev.detail.url || '') : '';
                                        if (u2) this.setUrlAndLoad(u2);
                                    } catch (e2) {}
                                };

                                // Legacy name kept for backwards compatibility.
                                this.$root.addEventListener('mb:email-preview:set-url', onSetUrl);
                                // Web preview modal uses a dedicated event name.
                                this.$root.addEventListener('mb:web-preview:set-url', onSetUrl);
                            } catch (e3) {}

                            this.url = u || '';

                            const search = (new URL(this.url, window.location.href)).searchParams;
                            const initialLangFromUrl = this.extractLanguageIdFromUrl(this.url);
                            if (initialLangFromUrl !== null) {
                                this.languageId = initialLangFromUrl;
                                this.languageLocked = (search.get('lock_lng') !== '0');
                            }

                            if (this.allowGender) {
                                this.gender = search.get('gnd') || 'unknown';
                            }

                            let initialDevice = null;
                            if (d && this.sizes && Object.prototype.hasOwnProperty.call(this.sizes, d)) {
                                initialDevice = d;
                            }
                            if (!initialDevice) {
                                try {
                                    const root = this.$root;
                                    const defDevAttr = root.getAttribute('data-ep-default-device') || '';
                                    if (defDevAttr && this.sizes && Object.prototype.hasOwnProperty.call(this.sizes, defDevAttr)) {
                                        initialDevice = defDevAttr;
                                    }
                                } catch (eDef) {}
                            }
                            if (!initialDevice && this.sizes && typeof this.sizes === 'object') {
                                const keys = Object.keys(this.sizes);
                                if (keys.length > 0) {
                                    initialDevice = keys[0];
                                }
                            }
                            if (!initialDevice) {
                                this.sizes = { desktop: {width: '100%', maxWidth: '100%'} };
                                initialDevice = 'desktop';
                            }

                            this.device = initialDevice;
                            if (!this.allowMockup) {
                                this.showMockup = false;
                            }
                            this.lastMockupActive = this.isMockupActive;
                            this.updateScaleForMockup();

                            this.$watch('showMockup', () => this.handleModeChange());
                            this.$watch('device', () => this.handleModeChange());

                            if (this.url) this.load(true);
                            this.$nextTick(() => this.updateActiveFrame());
                        },

                        handleModeChange() {
                            const current = this.isMockupActive;
                            if (current !== this.lastMockupActive) {
                                this.togglingMockup = true;
                                this.lastMockupActive = current;
                            }
                            this.updateScaleForMockup();
                            this.$nextTick(() => {
                                this.updateActiveFrame();
                                try { this.injectHeadersAll(); } catch (e) {}
                                this.togglingMockup = false;
                            });
                        },

                        resetPreviews() {
                            this._lastLoadSrc = null;
                            this.mailInfo = null;
                            this.lastSubject = '';
                            const master = document.getElementById(this.ids.master);
                            if (master) {
                                master.src = 'about:blank';
                            }
                            this.$root.querySelectorAll('[data-preview]').forEach(frame => {
                                try {
                                    frame.src = 'about:blank';
                                } catch (e) {}
                            });
                        },

                        updateScaleForMockup() {
                            if (!this.userScaled) {
                                this.scale = this.isMockupActive ? 0.7 : 1.0;
                            }
                        },

                        setUrlAndLoad(u) {
                            this.showLoader();
                            this.resetPreviews();
                            this.url = u || '';
                            const search = (new URL(this.url, window.location.href)).searchParams;
                            const langFromUrl = this.extractLanguageIdFromUrl(this.url);
                            if (langFromUrl !== null) {
                                this.languageId = langFromUrl;
                                this.languageLocked = (search.get('lock_lng') !== '0');
                            } else {
                                this.languageLocked = false;
                            }
                            this.load(true);
                        },

                        load(force = false) {
                            const master = document.getElementById(this.ids.master);
                            if (!master) {
                                this.hideLoader();
                                return;
                            }

                            let src = this.finalSrc;
                            if (force && src && src !== 'about:blank') {
                                try {
                                    const u = new URL(src, window.location.href);
                                    u.searchParams.set('_v', Date.now());
                                    src = u.toString();
                                } catch (e) {
                                    src += (src.indexOf('?') === -1 ? '?' : '&') + '_v=' + Date.now();
                                }
                            }

                            // Guard against duplicate `load()` calls with the same resolved URL.
                            if (!force && this._lastLoadSrc === src) return;
                            this._lastLoadSrc = src;

                            if (src && src !== 'about:blank') {
                                this.showLoader();
                                this.mailInfoReceived = false;
                                this.mailInfo = null;
                            }

                            // Bind iframe handlers only once; `load()` is called from multiple places.
                            if (!this._masterHandlersBound) {
                                this._masterHandlersBound = true;
                                master.onload = () => {
                                    const renderSeq = ++this._previewRenderSeq;
                                    Promise.resolve(this.copyMasterToPreviews())
                                        .catch(() => {})
                                        .finally(() => {
                                            if (renderSeq !== this._previewRenderSeq) return;
                                            this.hideLoader();
                                        });
                                };
                                master.onerror = () => { this.hideLoader(); };
                            }

                            master.src = src;
                        },

                        setMailInfo(info) {
                            if (!this.allowMailHeader) return;
                            this.mailInfoReceived = true;
                            try {
                                if (!info || typeof info !== 'object') return;
                                const copy = Object.assign({}, info);
                                if (copy.subject) copy.subject = this.decodeHeaderString(copy.subject);
                                if (copy.sender_name) copy.sender_name = this.decodeHeaderString(copy.sender_name);
                                this.mailInfo = copy;
                                if (copy.subject && String(copy.subject).trim() !== '') {
                                    this.lastSubject = String(copy.subject);
                                }
                                this.$nextTick(() => { try { this.injectHeadersAll(); } catch (e) {} });
                            } catch (e) {}
                        },

                        injectHeadersAll() {
                            if (!this.allowMailHeader) return;
                            this.$root.querySelectorAll('[data-preview]').forEach((frame) => {
                                const fd = frame.contentDocument;
                                if (fd) {
                                    this.injectHeader(fd, frame.getAttribute('data-preview') || '');
                                }
                            });
                        },

                        injectHeader(docOrEl, deviceType) {
                            if (!this.allowMailHeader) return;
                            try {
                                if (!docOrEl) return;

                                // Support both Document and Element targets
                                let container;
                                if (docOrEl.nodeType === 9) { // DOCUMENT_NODE
                                    container = docOrEl.body;
                                } else {
                                    // It's an element, probably documentElement
                                    container = (docOrEl.tagName === 'HTML') ? docOrEl.querySelector('body') : docOrEl;
                                }
                                if (!container) return;

                                // Always try to remove existing header first
                                const old = container.querySelector('#mb-ep-header');
                                if (old) {
                                    try { old.remove(); } catch (e) {
                                        if (old.parentNode) old.parentNode.removeChild(old);
                                    }
                                }

                                if (!this.isMockupActive || deviceType === 'desktop') return;

                                const headerHtml = this.buildInjectedHeaderHtml();
                                if (!headerHtml) return;

                                container.insertAdjacentHTML('afterbegin', headerHtml);
                            } catch (e) {}
                        },

                        buildInjectedHeaderHtml() {
                            try {
                                if (!this.allowMailHeader) return '';
                                if (!this.mobileHeaderTemplate) return '';
                                const info = this.mailInfo || {};
                                const subject = (this.subjectDisplay || '').toString();
                                const subjectHtml = subject
                                    ? this.escapeHtml(subject)
                                    : (this.awaitingMailInfo ? this.subjectSkeletonHtml : (this.mailInfoReceived ? this.subjectWarningBadgeHtml : ''));
                                const senderName = (info.sender_name || '').toString();
                                const senderEmail = (info.sender_email || '').toString();
                                const toEmail = (info.to_email || '').toString();
                                const initial = (senderName || senderEmail || '?').trim().charAt(0).toUpperCase();
                                let tpl = this.mobileHeaderTemplate;
                                tpl = tpl.replace(/\{\{subject\}\}/g, subjectHtml);
                                tpl = tpl.replace(/\{\{sender_name\}\}/g, this.escapeHtml(senderName));
                                tpl = tpl.replace(/\{\{sender_email\}\}/g, this.escapeHtml(senderEmail));
                                tpl = tpl.replace(/\{\{to_email\}\}/g, this.escapeHtml(toEmail));
                                tpl = tpl.replace(/\{\{sender_initial\}\}/g, this.escapeHtml(initial));
                                return tpl;
                            } catch (e) { return ''; }
                        },

                        escapeHtml(s) {
                            try {
                                return String(s)
                                    .replaceAll('&', '&amp;')
                                    .replaceAll('<', '&lt;')
                                    .replaceAll('>', '&gt;')
                                    .replaceAll('"', '&quot;')
                                    .replaceAll("'", '&#39;');
                            } catch (e) { return s; }
                        },

                        decodeHtmlEntities(s) {
                            try {
                                const div = document.createElement('div');
                                div.innerHTML = String(s);
                                const text = div.textContent || div.innerText || '';
                                return text;
                            } catch (e) {
                                return String(s || '');
                            }
                        },

                        decodeHeaderString(s) {
                            try {
                                let v = this.decodeMimeEncodedWords(String(s));
                                v = this.decodeHtmlEntities(v);
                                if (/%[0-9A-Fa-f]{2}/.test(v)) {
                                    try { v = decodeURIComponent(v); } catch (e2) {}
                                }
                                v = v.replace(/[\r\n]+/g, ' ').replace(/[\t ]+/g, ' ').trim();
                                return v;
                            } catch (e) {
                                return String(s);
                            }
                        },

                        decodeMimeEncodedWords(s) {
                            try {
                                if (!s) return '';
                                let str = String(s);
                                str = str.replace(/(\?=)\s+(=\?)/g, '$1$2');
                                const re = /=\?([^?]+)\?([bBqQ])\?([^?]+?)\?=/g;
                                let out = '';
                                let lastIndex = 0;
                                let m;
                                let lastWasEncoded = false;
                                while ((m = re.exec(str)) !== null) {
                                    const between = str.slice(lastIndex, m.index);
                                    if (!(lastWasEncoded && /^\s+$/.test(between))) {
                                        out += between;
                                    }
                                    const charset = m[1];
                                    const enc = m[2].toUpperCase();
                                    const data = m[3];
                                    let decoded = '';
                                    if (enc === 'B') {
                                        decoded = this.decodeB(data, charset);
                                    } else {
                                        decoded = this.decodeQ(data, charset);
                                    }
                                    out += decoded;
                                    lastIndex = re.lastIndex;
                                    lastWasEncoded = true;
                                }
                                out += str.slice(lastIndex);
                                return out;
                            } catch (e) { return String(s); }
                        },

                        normalizeCharsetLabel(chs) {
                            const c = String(chs || '').trim().toLowerCase().replace(/_/g, '-');
                            const map = {
                                'utf8': 'utf-8',
                                'utf-8': 'utf-8',
                                'us-ascii': 'us-ascii',
                                'iso8859-1': 'iso-8859-1',
                                'iso-8859-1': 'iso-8859-1',
                                'latin1': 'iso-8859-1',
                                'windows-1252': 'windows-1252',
                                'cp1252': 'windows-1252',
                                'utf8mb4': 'utf-8',
                                'utf-8mb4': 'utf-8'
                            };
                            return map[c] || c || 'utf-8';
                        },

                        decodeBytesWithCharset(bytes, chs) {
                            try {
                                const label = this.normalizeCharsetLabel(chs);
                                if (window.TextDecoder) {
                                    try {
                                        const dec = new TextDecoder(label, { fatal: false });
                                        return dec.decode(bytes);
                                    } catch (e) {
                                        const dec = new TextDecoder('utf-8', { fatal: false });
                                        return dec.decode(bytes);
                                    }
                                }
                            } catch (e) {}
                            try {
                                let s = '';
                                for (let i = 0; i < bytes.length; i++) s += String.fromCharCode(bytes[i]);
                                return s;
                            } catch (e2) { return ''; }
                        },

                        decodeB(data, chs) {
                            try {
                                const cleaned = String(data).replace(/\s+/g, '');
                                const bin = atob(cleaned);
                                const bytes = new Uint8Array(bin.length);
                                for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
                                return this.decodeBytesWithCharset(bytes, chs);
                            } catch (e) { return String(data); }
                        },

                        decodeQ(data, chs) {
                            try {
                                let str = String(data).replace(/_/g, ' ');
                                const bytes = [];
                                for (let i = 0; i < str.length; i++) {
                                    const ch = str[i];
                                    if (ch === '=' && i + 2 < str.length) {
                                        const h = str.substr(i + 1, 2);
                                        if (/^[0-9A-Fa-f]{2}$/.test(h)) {
                                            bytes.push(parseInt(h, 16));
                                            i += 2;
                                            continue;
                                        }
                                    }
                                    bytes.push(str.charCodeAt(i));
                                }
                                return this.decodeBytesWithCharset(new Uint8Array(bytes), chs);
                            } catch (e) { return String(data); }
                        },

                        copyMasterToPreviews() {
                            const master = document.getElementById(this.ids.master);
                            const doc = master ? master.contentDocument : null;
                            if (!doc) return Promise.resolve();

                            const html = doc.documentElement.cloneNode(true);
                            let doctype = '';
                            if (doc.doctype) {
                                try {
                                    doctype = new XMLSerializer().serializeToString(doc.doctype);
                                } catch (e) {
                                    doctype = '<!DOCTYPE html>';
                                }
                            }

                            if (!this.showImages) {
                                html.querySelectorAll('img, source').forEach(el => {
                                    el.removeAttribute('src');
                                    el.removeAttribute('srcset');
                                });
                                html.querySelectorAll('video').forEach(video => { video.removeAttribute('poster'); });
                                html.querySelectorAll('[background]').forEach(el => { el.removeAttribute('background'); });
                                html.querySelectorAll('[style*="background"]').forEach(el => { el.style.backgroundImage = 'none'; });

                                const styleOverride = document.createElement('style');
                                styleOverride.textContent = '* { background-image: none !important; list-style-image: none !important; }';
                                let head = html.querySelector('head');
                                if (!head) {
                                    head = document.createElement('head');
                                    html.insertBefore(head, html.firstChild);
                                }
                                head.appendChild(styleOverride);
                            }

                            const updates = [];
                            this.$root.querySelectorAll('[data-preview]').forEach(frame => {
                                const dev = frame.getAttribute('data-preview') || '';
                                const frameHtml = html.cloneNode(true);
                                this.injectHeader(frameHtml, dev);

                                updates.push(new Promise((resolve) => {
                                    let settled = false;
                                    let timeoutId = null;

                                    const finish = () => {
                                        if (settled) return;
                                        settled = true;
                                        frame.removeEventListener('load', onLoad);
                                        if (timeoutId !== null) {
                                            try { clearTimeout(timeoutId); } catch (e) {}
                                        }
                                        resolve();
                                    };

                                    const onLoad = () => { finish(); };

                                    try {
                                        frame.addEventListener('load', onLoad);
                                        timeoutId = setTimeout(finish, 800);

                                        try {
                                            // Use srcdoc to avoid Safari extension injection issues with document.write into about:blank
                                            frame.srcdoc = doctype + frameHtml.outerHTML;
                                        } catch (e) {
                                            const fd = frame.contentDocument;
                                            if (fd) {
                                                try {
                                                    fd.open();
                                                    fd.write(doctype + frameHtml.outerHTML);
                                                    fd.close();
                                                } catch (e2) {}
                                            }
                                        }
                                    } catch (e3) {
                                        finish();
                                    }
                                }));
                            });

                            return Promise.all(updates);
                        },

                        setDevice(d) { this.device = d; },

                        changeLanguage() {
                            this.load(true);
                        },

                        changeGender() {
                            this.load(true);
                        },

                        updateActiveFrame() {
                            try { this.injectHeadersAll(); } catch (e) {}
                        },

                        extractLanguageIdFromUrl(u) {
                            if (!u || typeof u !== 'string') return null;

                            const param = this.languageParam || 'lng_id';
                            let value = null;

                            try {
                                const urlObj = new URL(u, window.location.href);
                                const raw = urlObj.searchParams.get(param);
                                if (raw !== null && raw !== '') {
                                    value = raw;
                                }
                            } catch (e) {
                                try {
                                    const qIndex = u.indexOf('?');
                                    if (qIndex !== -1 && qIndex < u.length - 1) {
                                        const query = u.slice(qIndex + 1);
                                        const parts = query.split('&');
                                        for (let i = 0; i < parts.length; i++) {
                                            const kv = parts[i].split('=');
                                            if (kv[0] === param) {
                                                value = decodeURIComponent(kv[1] || '');
                                                break;
                                            }
                                        }
                                    }
                                } catch (e2) {}
                            }

                            let id = null;

                            if (value !== null) {
                                const parsed = parseInt(value, 10);
                                if (Number.isFinite(parsed) && parsed > 0) id = parsed;
                            }

                            // Backwards compatibility: if callers still pass `language=<code>`, map to id.
                            if (id === null) {
                                try {
                                    const urlObj = new URL(u, window.location.href);
                                    const legacy = urlObj.searchParams.get('language');
                                    if (legacy) {
                                        const code = String(legacy || '').trim().toLowerCase();
                                        const match = Array.isArray(this.languages)
                                            ? this.languages.find(l => (l && l.code && String(l.code).toLowerCase() === code))
                                            : null;
                                        if (match && match.id) {
                                            const parsedLegacy = parseInt(String(match.id), 10);
                                            if (Number.isFinite(parsedLegacy) && parsedLegacy > 0) id = parsedLegacy;
                                        }
                                    }
                                } catch (eLegacy) {}
                            }

                            if (id === null) return null;

                            const exists = Array.isArray(this.languages) && this.languages.some(function (lang) {
                                const lid = typeof lang.id === 'number' ? lang.id : parseInt(String(lang.id), 10);
                                return Number.isFinite(lid) && lid === id;
                            });

                            return exists ? id : null;
                        },

                        showLoader() {
                            this.loading = true;
                            const o = document.getElementById(this.overlayId);
                            if (!o) return;
                            if (window.MBLoader && MBLoader.show) MBLoader.show(o); else o.classList.remove('hidden');
                        },

                        hideLoader() {
                            this.loading = false;
                            const o = document.getElementById(this.overlayId);
                            if (!o) return;
                            if (window.MBLoader && MBLoader.hide) MBLoader.hide(o); else o.classList.add('hidden');
                        },
                    }
                }
            </script>
        </div>
        <?php
        return ob_get_clean();
    }

    private static function renderHeaderToolbar(array $previewDevices, bool $allowPersonaSelection = false, string $personaControlHtml = '', array $languages = [], $defaultLanguageId = null): string
    {
        ob_start();
        ?>
        <header class="z-20 mr-2 h-9 flex items-center justify-end gap-4">
            <div class="flex items-center gap-4 opacity-60 hover:opacity-100">
                <?php if ($allowPersonaSelection): ?>
                        <?= $personaControlHtml ?>
                <?php endif; ?>

                <div class="hidden sm:block" x-show="!languageLocked" x-cloak>
                    <?php
                    echo \Components\Form\RichSelect::render('languageId', '', $languages, $defaultLanguageId, [
                        'no_label'        => true,
                        'variant'         => 'ghost',
                        'container_class' => 'w-40',
                        'input_attrs'     => 'x-model.number="languageId" @change="changeLanguage()"',
                    ]);
                    ?>
                </div>

                <div class="sm:hidden" x-show="!languageLocked" x-cloak>
                    <?php
                    // Mobile version: more compact, no fixed width
                    echo \Components\Form\RichSelect::render('languageIdMobile', '', $languages, $defaultLanguageId, [
                        'no_label'    => true,
                        'variant'     => 'ghost',
                        'input_attrs' => 'x-model.number="languageId" @change="changeLanguage()"',
                    ]);
                    ?>
                </div>

                <?= \Components\Ui\ScaleControl::render(); ?>
                <?= self::renderDeviceButtonGroup($previewDevices, self::renderToggleButtons()); ?>
            </div>
        </header>
        <?php
        return ob_get_clean();
    }

    public static function renderDeviceButtonGroup(array $devices, string $extraHtml = ''): string
    {
        $items = [];
        foreach ($devices as $devCfg) {
            if (!is_array($devCfg)) continue;
            $id = isset($devCfg['id']) ? (string)$devCfg['id'] : '';
            if ($id === '') continue;
            $label = isset($devCfg['label']) && $devCfg['label'] !== '' ? (string)$devCfg['label'] : ucfirst($id);
            $items[] = [
                'id'        => $id,
                'label'     => $label,
                'icon_html' => Devices::iconHtml($devCfg),
            ];
        }
        return self::renderPillIconButtonGroup($items, 'device', "setDevice('__ID__')", $extraHtml);
    }

    /**
     * Generic pill-style icon button group, reused for devices and gender.
     *
     * Each item requires:
     * - id: string unique value
     * - label: string aria-label
     * - icon_html: string pre-rendered SVG markup
     *
     * @param array $items            List of items
     * @param string $selectedVar     Alpine var name to compare against item id
     * @param string $onClickTemplate JS expression template; use '__ID__' placeholder for the id
     * @param string $extraHtml       Optional trailing HTML inside the group (e.g., dividers/toggles)
     * @param string $tag             The HTML tag to use for items (default 'button')
     */
    public static function renderPillIconButtonGroup(array $items, string $selectedVar, string $onClickTemplate, string $extraHtml = '', string $tag = 'button'): string
    {
        ob_start();
        ?>
        <div class="flex gap-1 rounded-full bg-muted p-1 dark:bg-muted shrink-0" x-cloak>
            <?php foreach ($items as $it):
                if (!is_array($it)) continue;
                $id    = isset($it['id']) ? (string)$it['id'] : '';
                if ($id === '') continue;
                $label = isset($it['label']) ? (string)$it['label'] : $id;
                $icon  = isset($it['icon_html']) ? (string)$it['icon_html'] : '';
                $click = str_replace('__ID__', $id, $onClickTemplate);
                $buttonAttrs = [
                    'class'          => 'group flex items-center rounded-full p-1 text-sm/7 font-medium data-selected:bg-white hover:bg-white data-selected:ring data-selected:ring-muted/5 dark:data-selected:bg-gray-600 dark:data-selected:ring-0 dark:data-selected:inset-ring dark:data-selected:inset-ring-white/10',
                    ':data-selected' => $selectedVar . "==='" . $id . "' ? '' : null",
                    '@click'         => $click,
                    'aria-label'     => $label,
                    'title'          => $label,
                ];
                if ($tag === 'button') {
                    $buttonAttrs['type'] = 'button';
                } else {
                    $buttonAttrs['tabindex'] = '0';
                    $buttonAttrs['role'] = 'button';
                }
                echo '<' . $tag . ' ' . Base::attrs($buttonAttrs) . '>' . $icon . '</' . $tag . '>';
            endforeach;

            if ($extraHtml !== '') {
                echo $extraHtml;
            }
            ?>
        </div>
        <?php
        return ob_get_clean();
    }

    /**
     * Gender selection button group using icons.
     *
     * @param string $selectedVar Alpine variable holding active gender (e.g., 'gender' or 'activeGnd')
     * @param string $onClickTemplate JS template; use '__ID__' placeholder for id
     */
    public static function renderGenderButtonGroup(string $selectedVar = 'gender', string $onClickTemplate = ""): string
    {
        $items = [
            [ 'id' => 'm',        'label' => (string)mh_lng('MAILBEEZ_GENDER_MALE', 'Male'),        'icon_html' => Base::SVG_GENDER_M ],
            [ 'id' => 'f',        'label' => (string)mh_lng('MAILBEEZ_GENDER_FEMALE', 'Female'),    'icon_html' => Base::SVG_GENDER_F ],
            [ 'id' => 'unknown',  'label' => (string)mh_lng('MAILBEEZ_GENDER_UNKNOWN', 'Unknown'), 'icon_html' => Base::SVG_GENDER_NOTSPECIFIED ],
        ];
        if ($onClickTemplate === '') {
            $onClickTemplate = "{$selectedVar}='__ID__'";
        }
        return self::renderPillIconButtonGroup($items, $selectedVar, $onClickTemplate, '', 'div');
    }

    /**
     * Render a group of persona options as large buttons with icons and labels.
     *
     * @param array $items Array of items [id, label, icon_html]
     * @param string $selectedVar Alpine variable holding active selection
     * @param string $onClickTemplate JS template; use '__ID__' placeholder for id
     */
    public static function renderPersonaOptionGroup(array $items, string $selectedVar, string $onClickTemplate): string
    {
        ob_start();
        ?>
        <div class="grid grid-cols-3 gap-2" x-cloak>
            <?php foreach ($items as $it):
                $click = str_replace('__ID__', $it['id'], $onClickTemplate);
            ?>
            <button type="button"
                    @click="<?= $click ?>"
                    class="flex flex-col items-center justify-center gap-1 p-3 rounded-lg border border-divider hover:bg-gray-50 dark:hover:bg-white/5 transition-colors group"
                    :class="<?= $selectedVar ?> === '<?= $it['id'] ?>' ? 'bg-brand/5 border-brand ring-1 ring-brand' : 'bg-white dark:bg-muted'"
            >
                <span class="opacity-70 group-hover:opacity-100" :class="<?= $selectedVar ?> === '<?= $it['id'] ?>' ? 'text-brand opacity-100' : ''"><?= $it['icon_html'] ?></span>
                <span class="text-xs font-medium leading-tight" :class="<?= $selectedVar ?> === '<?= $it['id'] ?>' ? 'text-brand' : ''"><?= Base::h($it['label']) ?></span>
            </button>
            <?php endforeach; ?>
        </div>
        <?php
        return ob_get_clean();
    }

    private static function renderToggleButtons(): string
    {
        ob_start();
        ?>
        <div class="border-l border-divider/50 h-4 self-center mx-1 dark:border-white/10" x-cloak></div>

        <button type="button"
                @click="showMockup = !showMockup; updateScale()"
                class="group flex items-center rounded-full p-1 text-sm/7 font-medium data-selected:bg-white hover:bg-white data-selected:ring data-selected:ring-muted/5 dark:data-selected:bg-gray-600 dark:data-selected:ring-0 dark:data-selected:inset-ring dark:data-selected:inset-ring-white/10"
                :data-selected="showMockup ? '' : null"
                aria-label="Toggle mockup"
                x-cloak>
            <div class="relative flex items-center justify-center" :class="!showMockup ? 'text-gray-400' : 'text-gray-500 group-hover:text-gray-700 dark:text-gray-400 dark:group-hover:text-gray-200'">
                <div class="relative size-5">
                    <?= Base::SVG_DEVICE_PHONE ?>
                    <div class="absolute -right-0.5 -bottom-0.5 bg-white dark:bg-gray-900 rounded-xs group-data-selected:bg-white dark:group-data-selected:bg-gray-600 group-hover:bg-white">
                        <?= str_replace(['size-4', 'm-0.5'], ['size-3', 'm-0'], Base::SVG_DEVICE_PHONE_SMALL) ?>
                    </div>
                    <template x-if="!showMockup">
                        <span class="absolute -top-0.5 -right-0.5 text-red-500 bg-white dark:bg-gray-900 rounded-full group-hover:bg-white">
                            <?= Base::SVG_MICRO_X_MARK ?>
                        </span>
                    </template>
                </div>
            </div>
        </button>

        <button type="button"
                x-show="allowImagesToggle"
                @click="showImages = !showImages; copyMasterToPreviews()"
                class="group flex items-center rounded-full p-1 text-sm/7 font-medium data-selected:bg-white hover:bg-white data-selected:ring data-selected:ring-muted/5 dark:data-selected:bg-gray-600 dark:data-selected:ring-0 dark:data-selected:inset-ring dark:data-selected:inset-ring-white/10"
                :data-selected="showImages ? '' : null"
                aria-label="Toggle images"
                x-cloak>
            <div class="relative">
                <span :class="!showImages ? 'text-gray-400' : 'text-gray-500 group-hover:text-gray-700 dark:text-gray-400 dark:group-hover:text-gray-200'" class="block">
                    <?= str_replace('size-6', 'size-5', Base::SVG_PHOTO_OUTLINE) ?>
                </span>
                <template x-if="!showImages">
                    <span class="absolute -top-0.5 -right-0.5 text-red-500 bg-white dark:bg-gray-900 rounded-full group-hover:bg-white">
                        <?= Base::SVG_MICRO_X_MARK ?>
                    </span>
                </template>
            </div>
        </button>
        <?php
        return ob_get_clean();
    }

    private static function mobileHeaderTemplate(): string
    {
        ob_start(); ?>
<div id="mb-ep-header" style="all: initial; box-sizing: border-box; display: block; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; background: #ffffff; color: #111827; border-bottom: 1px solid #E5E7EB;">
  <div style="padding: 100px 14px 8px 14px;">
    <div style="display:flex; align-items:center; gap:10px; margin-bottom:16px;">
      <div style="width:36px;height:36px;border-radius:9999px;background:#E5E7EB;color:#111827;display:flex;align-items:center;justify-content:center;font-weight:600;font-size:20px;line-height:32px;text-transform:uppercase;overflow: hidden">{{sender_initial}}</div>
      <div style="display:flex;flex-direction:column;">
        <div style="font-weight:600;font-size:14px;line-height:1.2;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:290px;">{{sender_name}}</div>
        <div style="font-size:12px;color:#6B7280;line-height:1.2;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:290px;">To: {{to_email}}</div>
      </div>
    </div>
    <div style="font-weight:700;font-size:18px;line-height:1.3;margin:0;white-space:normal;word-break:break-word; padding-top: 8px; padding-bottom: 8px; border-top: 1px solid #E5E7EB;">{{subject}}</div>
  </div>
</div>
<?php return ob_get_clean();
    }

    private static function subjectSkeletonTemplate(): string
    {
        return '<span aria-hidden="true" class="inline-block align-middle h-4 w-56 max-w-full rounded-md bg-gray-200/70 dark:bg-zinc-700/60 animate-pulse"></span>';
    }
}
