<?php

declare(strict_types=1);
/* Variables: $baseUrl, $assetsUrl, $project, $user, $navItems, $pageTitle, $configJson, $pageOptions, $effects, $iconUrl */
?>
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
    <title><?= htmlspecialchars($project['name'] ?? 'Project') ?> - Mobile</title>
    <?php include constant('BASE_PATH') . '/core/views/project-head.php'; ?>
    <?= \PpPhp\Core\ModuleAssets::style('Mobile', 'Views/styles.css') ?>
</head>

<body class="mobile-body">
    <?php include constant('BASE_PATH') . '/core/views/project-navbar.php'; ?>

    <div id="mainArea" class="main-area">
        <div class="right-workspace w-100">
            <div class="mob-wrap">
                <div class="mob-head">
                    <span class="ic"><i class="bi bi-phone"></i></span>
                    <div>
                        <h1>Mobile App Experience</h1>
                        <p>Make your generated app feel like a real native mobile app. Everything here applies only on phones — desktop is never affected.</p>
                    </div>
                    <div style="margin-left:auto;display:flex;align-items:center;gap:.9rem;flex-shrink:0">
                        <span class="mob-saved" id="mobSaved"><i class="bi bi-check-circle-fill"></i> Saved</span>
                        <button type="button" class="mob-btn ghost" id="mobReset" style="margin-top:0"><i class="bi bi-arrow-counterclockwise"></i> Reset</button>
                    </div>
                </div>

                <div class="mob-layout">
                    <!-- ══ Settings column ══ -->
                    <div class="mob-settings">

                        <div class="mob-master">
                            <div style="flex:1 1 auto">
                                <div class="t">Native mobile experience<span class="mob-tip" data-tip="The master switch for everything on this page. When off, the generated app just uses its normal responsive layout on phones — no tab bar, no native skin. Desktop is never affected either way." tabindex="0" role="button" aria-label="Help">?</span></div>
                                <div class="d">Master switch. When off, the generated app just uses its normal responsive layout on phones.</div>
                            </div>
                            <label class="mob-sw" title="Turn the whole native-mobile layer on or off"><input type="checkbox" id="mobEnabled"><span class="track"></span><span class="thumb"></span></label>
                        </div>

                        <div class="mob-sec-h">Navigation<span class="mob-tip" data-tip="How users move around on phones: a bottom tab bar, edge Back/Forward buttons, and edge-swipe gestures — the core native-navigation signals." tabindex="0" role="button" aria-label="Help">?</span></div>
                        <div class="mob-grid">
                            <?php
                            $feat = static function (string $key, string $icon, string $title, string $desc, string $tip = '', bool $warn = false): void {
                                $tipText = $tip !== '' ? $tip : $desc; ?>
                                <div class="mob-card">
                                    <span class="cic"><i class="bi bi-<?= htmlspecialchars($icon) ?>"></i></span>
                                    <div class="cb"><div class="t"><?= htmlspecialchars($title) ?><span class="mob-tip<?= $warn ? ' warn' : '' ?>" data-tip="<?= htmlspecialchars($tipText) ?>" tabindex="0" role="button" aria-label="Help"><?= $warn ? '!' : '?' ?></span></div><div class="d"><?= htmlspecialchars($desc) ?></div></div>
                                    <label class="mob-sw"><input type="checkbox" data-feat="<?= htmlspecialchars($key) ?>"><span class="track"></span><span class="thumb"></span></label>
                                </div>
                            <?php };
                            $feat('tabbar', 'grid-3x3-gap', 'Bottom tab bar', 'A fixed bottom bar — the #1 native-app signal. Customize it below.');
                            $feat('edgeNav', 'chevron-bar-left', 'Back / Forward buttons', 'Small chevron tabs on the left & right screen edges.');
                            $feat('edgeSwipe', 'hand-index', 'Edge-swipe navigation', 'Swipe from the left edge = Back, right edge = Forward.');
                            ?>
                        </div>

                        <!-- ══ Tab bar customizer ══ -->
                        <div class="mob-sec-h">Bottom tab bar<span class="mob-tip" data-tip="Customise the fixed bottom bar. “Auto” turns your first menu items into tabs; “Custom” lets you set each tab’s icon, label, link and order. A “More” tab opens the full menu." tabindex="0" role="button" aria-label="Help">?</span></div>
                        <div class="mob-panel" id="mobTabPanel">
                            <div class="mob-row">
                                <div class="mob-seg" role="group" aria-label="Tab source">
                                    <button type="button" class="mob-seg-btn" data-mode="auto">Auto (from menu)</button>
                                    <button type="button" class="mob-seg-btn" data-mode="custom">Custom tabs</button>
                                </div>
                            </div>

                            <div class="mob-inline" data-when="auto">
                                <label>How many tabs
                                    <select id="mobCount"><option>2</option><option>3</option><option>4</option><option>5</option></select>
                                </label>
                                <span class="mob-hint">The first menu items become tabs; a “More” tab opens the full menu.</span>
                            </div>

                            <div data-when="custom">
                                <div class="mob-tabs-editor" id="mobTabsEditor"></div>
                                <button type="button" class="mob-btn" id="mobAddTab"><i class="bi bi-plus-lg"></i> Add tab</button>
                                <div class="mob-hint" style="margin-top:.5rem">Up to 5 tabs. Drag the handle to reorder. Pick a page or set a custom link. A “More” tab is added automatically when you have extra menu items.</div>
                            </div>

                            <div class="mob-inline" style="margin-top:1rem;border-top:1px solid var(--pp-border,#242a3a);padding-top:.9rem">
                                <label class="mob-check"><input type="checkbox" id="mobShowLabels"> Show labels under icons</label>
                                <label>Active icon effect
                                    <select id="mobEffect"></select>
                                </label>
                            </div>
                        </div>

                        <!-- ══ Back / Forward edge buttons ══ -->
                        <div class="mob-sec-h">Back / Forward buttons<span class="mob-tip" data-tip="Style the floating chevron buttons that hug the left &amp; right screen edges. Turn them on/off with the “Back / Forward buttons” toggle up in Navigation." tabindex="0" role="button" aria-label="Help">?</span></div>
                        <div class="mob-panel">
                            <div class="mob-hint" style="margin-bottom:.8rem">Turn them on/off with the <b>Back / Forward buttons</b> toggle above; style them here.</div>
                            <div class="mob-inline">
                                <label>Size <input type="range" id="mobEdgeSize" min="40" max="80" step="2"> <span class="mob-hint" id="mobEdgeSizeV"></span></label>
                                <label>Opacity <input type="range" id="mobEdgeOpacity" min="20" max="100" step="5"> <span class="mob-hint" id="mobEdgeOpV"></span></label>
                            </div>
                            <div class="mob-fields" style="grid-template-columns:1fr 1fr;margin-top:.9rem">
                                <div>
                                    <label for="mobEdgeBg">Background</label>
                                    <div style="display:flex;gap:.4rem;align-items:center"><input type="color" id="mobEdgeBg"><button type="button" class="mob-btn ghost" id="mobEdgeBgAuto" style="margin-top:0;padding:.45rem .7rem" title="Follow theme colour">Auto</button></div>
                                </div>
                                <div>
                                    <label for="mobEdgeColor">Icon color</label>
                                    <div style="display:flex;gap:.4rem;align-items:center"><input type="color" id="mobEdgeColor"><button type="button" class="mob-btn ghost" id="mobEdgeColorAuto" style="margin-top:0;padding:.45rem .7rem">Auto</button></div>
                                </div>
                            </div>
                        </div>

                        <div class="mob-sec-h">Interactions<span class="mob-tip" data-tip="Touch feedback &amp; micro-interactions: swipe a row for actions, shimmer placeholders while loading, subtle haptics, and a gentle page fade." tabindex="0" role="button" aria-label="Help">?</span></div>
                        <div class="mob-grid">
                            <?php
                            $feat('swipeActions', 'menu-app', 'Swipe row actions', 'Swipe a list card left to reveal Edit / Delete.');
                            $feat('skeleton', 'layout-text-window', 'Skeleton loaders', 'Shimmering placeholders while the next page loads.');
                            $feat('haptics', 'vibrate', 'Haptic feedback', 'A subtle vibration on taps & gestures (supported devices).');
                            $feat('pageTransition', 'transparency', 'Page transitions', 'A gentle fade-in as each page appears.');
                            ?>
                        </div>

                        <!-- ══ Components → mobile controls (Phase A) ══ -->
                        <div class="mob-sec-h">Mobile controls <span class="mob-hint" style="font-weight:500;text-transform:none;letter-spacing:0">— convert every component to a native flavour</span></div>
                        <div class="mob-grid">
                            <?php
                            $feat('cardify', 'grid-1x2', 'Card-ify tables', 'Wide data tables become stacked label/value cards on phones.');
                            $feat('inputEnhance', 'keyboard', 'Smart keyboards', 'Right keyboard per field — email, phone, number, URL — with next/done hints.');
                            $feat('nativePickers', 'calendar-week', 'Native pickers', 'Prefer the device date/time & select wheels where possible.');
                            $feat('liveSearch', 'search', 'Instant search', 'A native search bar filters the current list as you type.');
                            $feat('stickyHeaders', 'layout-text-sidebar', 'Sticky headers', 'Group & column headers pin to the top while scrolling.');
                            $feat('emptyStates', 'inbox', 'Empty states', 'A friendly illustration + call-to-action when a list is empty.');
                            $feat('toasts', 'chat-square-text', 'Native toasts', 'Snackbar notifications with an Undo action instead of desktop alerts.');
                            $feat('sheets', 'segmented-nav', 'Bottom sheets', 'Menus, filters & dialogs slide up as native bottom sheets.');
                            ?>
                        </div>

                        <!-- ══ Component → flavour matrix ══ -->
                        <div class="mob-sec-h">Component flavours <span class="mob-hint" style="font-weight:500;text-transform:none;letter-spacing:0">— pick how each family presents on phones</span></div>
                        <div class="mob-panel">
                            <div class="mob-flav">
                                <div class="mob-flav-row">
                                    <span class="cic"><i class="bi bi-table"></i></span>
                                    <div class="mob-flav-tx"><div class="t">Lists &amp; tables</div><div class="d">Wide grids as stacked cards, a scrollable table, or auto by width.</div></div>
                                    <select id="mobFlvList" class="mob-flav-sel"><option value="auto">Auto</option><option value="cards">Cards</option><option value="table">Table</option></select>
                                </div>
                                <div class="mob-flav-row">
                                    <span class="cic"><i class="bi bi-calendar-event"></i></span>
                                    <div class="mob-flav-tx"><div class="t">Date &amp; time fields</div><div class="d">Keep the custom calendar, or use the phone’s native date/time wheel.</div></div>
                                    <select id="mobFlvDates" class="mob-flav-sel"><option value="custom">Custom</option><option value="native">Native</option></select>
                                </div>
                                <div class="mob-flav-row">
                                    <span class="cic"><i class="bi bi-menu-button-wide"></i></span>
                                    <div class="mob-flav-tx"><div class="t">Dropdowns</div><div class="d">The browser’s native picker, or a full-width action sheet of options.</div></div>
                                    <select id="mobFlvSelects" class="mob-flav-sel"><option value="native">Native</option><option value="sheet">Action sheet</option></select>
                                </div>
                                <div class="mob-flav-row">
                                    <span class="cic"><i class="bi bi-123"></i></span>
                                    <div class="mob-flav-tx"><div class="t">Number fields</div><div class="d">A plain input, or a stepper with − / + buttons for quick tweaks.</div></div>
                                    <select id="mobFlvNumbers" class="mob-flav-sel"><option value="plain">Plain</option><option value="stepper">Stepper</option></select>
                                </div>
                            </div>
                        </div>

                        <!-- ══ Signature interactions (Phase B) ══ -->
                        <div class="mob-sec-h">Signature interactions<span class="mob-tip" data-tip="The premium native touches: pull-to-refresh, slide/shared-element page transitions, a swipeable KPI carousel, modal detail cards, and infinite scroll." tabindex="0" role="button" aria-label="Help">?</span></div>
                        <div class="mob-grid">
                            <?php
                            $feat('pullRefresh', 'arrow-clockwise', 'Pull to refresh', 'Drag down at the top of a list to reload it.');
                            $feat('viewTransitions', 'layers', 'Page transitions+', 'Native slide / shared-element transitions between pages.');
                            $feat('kpiCarousel', 'bar-chart', 'KPI carousel', 'Dashboard stat tiles become a swipeable snap carousel.');
                            $feat('modalDetail', 'window-stack', 'Modal detail', 'Open view/edit as a card that lifts over the list (swipe down to close).', 'View pages are fetched and shown in a sheet that lifts over the list; the OS back gesture closes it. Edit pages still navigate normally (they need their own scripts).');
                            $feat('infiniteScroll', 'infinity', 'Infinite scroll', 'Load more rows as you reach the end, instead of pager buttons.', 'Fetches the next pager page and appends its rows as you near the bottom. If a page can’t load it quietly restores the normal pager.');
                            $feat('formDrafts', 'pencil-square', 'Autosave drafts', 'Keep what was typed in a new record if the user leaves, and offer to restore it.');
                            ?>
                        </div>

                        <!-- ══ Platform depth (Phase C) ══ -->
                        <div class="mob-sec-h">Platform &amp; native bridge<span class="mob-tip warn" data-tip="Deeper OS integration — offline sync, push notifications, share target, install prompt, app badge. Several need HTTPS and the app installed to the Home Screen; some are Android-only. Check each item’s hint." tabindex="0" role="button" aria-label="Help">!</span></div>
                        <div class="mob-grid">
                            <?php
                            $feat('offline', 'wifi-off', 'Offline mode', 'Cache pages for offline reading and queue changes until reconnected.', 'A service worker caches visited pages so they open offline, and form submits are queued in the browser and replayed when the connection returns. Best tested on a real device (service workers need HTTPS and don’t run in most in-app browsers).', true);
                            $feat('push', 'bell', 'Push notifications', 'Let the installed app receive push alerts (needs setup).', 'Needs a VAPID key pair — generate it in “Push setup” below — and, on iOS, the app added to the Home Screen (iOS 16.4+). See the step-by-step guide under Push setup.', true);
                            $feat('shareTarget', 'share', 'Share target', 'The app appears in the OS share sheet to create records.', 'After you rebuild, the installed app shows up in the phone’s Share sheet; shared text/links are handed to the app. Supported on Android/Chromium.', true);
                            $feat('installPrompt', 'phone', 'Install prompt', 'Gently invite users to add the app to their home screen.', 'On Android/Chromium a native “Install” banner appears; on iOS a one-time “Add to Home Screen” hint is shown (iOS has no install API).');
                            $feat('appBadge', 'app-indicator', 'App badge', 'Show an unread/pending count on the home-screen icon.', 'Reads a count from a [data-pp-badge-count] / .pp-notif-count element and shows it on the installed icon. Supported on Android & desktop; limited on iOS.');
                            ?>
                        </div>

                        <!-- ══ Push setup (VAPID) ══ -->
                        <div class="mob-sec-h">Push setup<span class="mob-tip warn" data-tip="Web Push needs a one-time VAPID key pair and, on iOS, the app installed to the Home Screen (iOS 16.4+). Use “Generate VAPID keys” below, then follow the guide." tabindex="0" role="button" aria-label="Help">!</span> <span class="mob-hint" style="font-weight:500;text-transform:none;letter-spacing:0">— needed by the “Push notifications” toggle</span></div>
                        <div class="mob-panel">
                            <div class="mob-hint" style="margin-bottom:.7rem">Create a one-time <b>VAPID</b> key pair below (or paste your own). The public key ships to the app; the private key stays server-side to sign and send. Subscriptions are stored in <code>config/push-subs.json</code>.</div>
                            <div style="display:flex;gap:.5rem;flex-wrap:wrap;margin-bottom:.6rem">
                                <button type="button" class="mob-btn" id="mobVapidGen" style="margin-top:0"><i class="bi bi-key"></i> Generate VAPID keys</button>
                                <span class="mob-hint" id="mobVapidMsg" style="align-self:center"></span>
                            </div>
                            <div class="mob-fields" style="grid-template-columns:1fr">
                                <div><label for="mobVapidPublic">VAPID public key</label><input type="text" id="mobVapidPublic" placeholder="B… (base64url)" autocomplete="off" spellcheck="false"></div>
                                <div><label for="mobVapidPrivate">VAPID private key <span class="mob-tip warn" data-tip="Secret. It is stored on the server in config/push.php and is never sent to browsers. Keep it private." tabindex="0" role="button" aria-label="Help">!</span></label><input type="text" id="mobVapidPrivate" placeholder="… (base64url — kept server-side)" autocomplete="off" spellcheck="false"></div>
                                <div><label for="mobVapidSubject">Contact (mailto: or URL)</label><input type="text" id="mobVapidSubject" placeholder="mailto:you@example.com" autocomplete="off"></div>
                            </div>

                            <details class="mob-guide">
                                <summary><i class="bi bi-book"></i> How to set up push notifications — step by step</summary>
                                <ol>
                                    <li><b>Generate keys.</b> Click <b>Generate VAPID keys</b> above (fills both fields), or run <code>npx web-push generate-vapid-keys</code> and paste them. VAPID is how push services verify the sender.</li>
                                    <li><b>Set a contact.</b> Put a <code>mailto:</code> address or your site URL in <b>Contact</b> — push services require it so they can reach you about issues.</li>
                                    <li><b>Turn it on &amp; build.</b> Enable <b>Push notifications</b> in <i>Platform &amp; native bridge</i> above, then rebuild the app. The build emits the sender (<code>push/WebPush.php</code>), a subscription endpoint (<code>api/push-subscribe.php</code>) and server config (<code>config/push.php</code>).</li>
                                    <li><b>Install on the phone.</b> Open the app on a phone (use the QR above), then <b>Add to Home Screen</b>. On iOS this is required and needs <b>iOS 16.4+</b>; Android works installed or in Chrome. In the app, tap <b>Enable notifications</b> and allow.</li>
                                    <li><b>Send a notification.</b> From your server code call <code>pp_push(['title'=&gt;'Hi','body'=&gt;'…','url'=&gt;'./'])</code> (include <code>push/push.php</code> first) — e.g. from a record-created event. To test now: run <code>php push/send-test.php</code> on the server, or open <code>push/send-test.php?token=…</code> (the token is in <code>config/push.php</code>).</li>
                                </ol>
                                <div class="mob-guide-note"><i class="bi bi-info-circle"></i> Web Push requires <b>HTTPS</b> (localhost is fine for testing). Expired subscriptions are pruned automatically when you send.</div>
                                <div class="mob-guide-links">
                                    <a href="https://github.com/web-push-libs/web-push" target="_blank" rel="noopener">web-push (key generator) ↗</a>
                                    <a href="https://developer.mozilla.org/docs/Web/API/Push_API" target="_blank" rel="noopener">MDN: Push API ↗</a>
                                    <a href="https://web.dev/articles/push-notifications-overview" target="_blank" rel="noopener">web.dev: overview ↗</a>
                                    <a href="https://developer.apple.com/documentation/usernotifications/sending-web-push-notifications-in-web-apps-and-browsers" target="_blank" rel="noopener">Apple: web push on iOS ↗</a>
                                </div>
                            </details>
                        </div>

                        <!-- ══ Appearance ══ -->
                        <div class="mob-sec-h">Appearance<span class="mob-tip" data-tip="“Auto” flips the generated app to its dark theme when the phone is in dark mode; or force light/dark; or leave it to the app’s own theme (Off)." tabindex="0" role="button" aria-label="Help">?</span></div>
                        <div class="mob-panel">
                            <div class="mob-inline">
                                <label>Dark mode on phones
                                    <select id="mobDarkMode">
                                        <option value="off">Off (use app theme)</option>
                                        <option value="auto">Auto (follow device)</option>
                                        <option value="light">Always light</option>
                                        <option value="dark">Always dark</option>
                                    </select>
                                </label>
                                <span class="mob-hint">“Auto” flips the generated app to its dark theme when the phone is in dark mode.</span>
                            </div>
                        </div>

                        <!-- ══ Open on your phone (QR) ══ -->
                        <div class="mob-sec-h">Open on your phone<span class="mob-tip" data-tip="Scan the QR with your phone camera to open the built app on a real device and feel the native experience. Edit the URL to match where the app is deployed." tabindex="0" role="button" aria-label="Help">?</span></div>
                        <div class="mob-panel mob-qr-panel">
                            <div class="mob-qr" id="mobQr"></div>
                            <div class="mob-qr-side">
                                <div class="d">Scan with your phone camera to open the built app and feel the native experience live.</div>
                                <label for="mobPreviewUrl" style="margin-top:.7rem">App URL</label>
                                <input type="text" id="mobPreviewUrl" placeholder="<?= htmlspecialchars($appUrlGuess ?? '') ?>" autocomplete="off" spellcheck="false">
                                <a class="mob-btn ghost" id="mobQrOpen" href="#" target="_blank" rel="noopener"><i class="bi bi-box-arrow-up-right"></i> Open link</a>
                            </div>
                        </div>

                        <!-- ══ PWA + icon ══ -->
                        <div class="mob-sec-h">Install as an app (PWA)<span class="mob-tip" data-tip="Makes the app installable to the Home Screen and launchable fullscreen. Generates a manifest, service worker and icons at build time." tabindex="0" role="button" aria-label="Help">?</span></div>
                        <div class="mob-panel">
                            <div class="mob-row">
                                <span class="cic"><i class="bi bi-download"></i></span>
                                <div style="flex:1 1 auto">
                                    <div class="t" style="font-weight:700">Installable app</div>
                                    <div class="d">Users can “Add to Home Screen” and launch it fullscreen. Generates a manifest, service worker & icons at build.</div>
                                </div>
                                <label class="mob-sw"><input type="checkbox" id="mobPwa"><span class="track"></span><span class="thumb"></span></label>
                            </div>
                            <div id="mobPwaFields">
                                <div class="mob-fields">
                                    <div>
                                        <label for="mobAppName">App name on home screen</label>
                                        <input type="text" id="mobAppName" maxlength="60" placeholder="<?= htmlspecialchars($project['name'] ?? 'App') ?> (default)">
                                    </div>
                                    <div>
                                        <label for="mobThemeColor">Theme color</label>
                                        <input type="color" id="mobThemeColor" value="#4f46e5" title="Status-bar / splash color (blank = your theme's primary)">
                                    </div>
                                </div>
                                <div class="mob-icon-up">
                                    <div class="mob-icon-prev" id="mobIconPrev"><i class="bi bi-image"></i></div>
                                    <div style="flex:1 1 auto">
                                        <div class="t" style="font-weight:700;font-size:.9rem">App icon</div>
                                        <div class="d">Square PNG or SVG (512×512 recommended). Leave empty to auto-generate from your theme colour + app initial.</div>
                                        <div style="margin-top:.5rem;display:flex;gap:.5rem;flex-wrap:wrap">
                                            <label class="mob-btn"><i class="bi bi-upload"></i> Upload icon<input type="file" id="mobIconFile" accept="image/png,image/svg+xml,image/jpeg,image/webp" hidden></label>
                                            <button type="button" class="mob-btn ghost" id="mobIconRemove"><i class="bi bi-trash3"></i> Remove</button>
                                        </div>
                                        <div class="mob-hint" id="mobIconMsg"></div>
                                    </div>
                                </div>
                            </div>
                        </div>

                        <div class="mob-note">
                            <i class="bi bi-info-circle-fill"></i>
                            <div>Changes save instantly and take effect on the next <b>Build</b>. The live preview on the right updates as you edit.</div>
                        </div>
                    </div>

                    <!-- ══ Live phone preview ══ -->
                    <div class="mob-preview-col">
                        <div class="mob-phone" id="mobPhone">
                            <div class="mp-notch"></div>
                            <div class="mp-screen">
                                <div class="mp-topbar"><span class="mp-burger"><i class="bi bi-list"></i></span><span class="mp-brand" id="mpBrand">App</span><span class="mp-av">A</span></div>
                                <div class="mp-body">
                                    <div class="mp-title">Customers</div>
                                    <div class="mp-card"><div class="mp-l w60"></div><div class="mp-l w90"></div><div class="mp-l w40"></div></div>
                                    <div class="mp-card"><div class="mp-l w50"></div><div class="mp-l w80"></div><div class="mp-l w35"></div></div>
                                    <div class="mp-edge mp-edge-l" id="mpEdgeL"><i class="bi bi-chevron-left"></i></div>
                                    <div class="mp-edge mp-edge-r" id="mpEdgeR"><i class="bi bi-chevron-right"></i></div>
                                    <div class="mp-fab" id="mpFab"><i class="bi bi-plus-lg"></i></div>
                                </div>
                                <nav class="mp-tabbar" id="mpTabbar"></nav>
                            </div>
                        </div>
                        <div class="mob-preview-cap">Live preview</div>
                    </div>
                </div>
            </div>
        </div>
    </div>

    <!-- Icon picker gallery -->
    <div class="mob-iconpick" id="mobIconPick" aria-hidden="true">
        <div class="mob-iconpick-box" role="dialog" aria-modal="true" aria-label="Choose an icon">
            <div class="mob-iconpick-head"><strong>Choose an icon</strong><button type="button" class="x" data-ip-close aria-label="Close">&times;</button></div>
            <div class="mob-iconpick-search"><i class="bi bi-search"></i><input type="text" id="mobIconSearch" placeholder="Search icons… (e.g. house, cart, chart)" autocomplete="off"></div>
            <div class="mob-iconpick-grid" id="mobIconGrid"></div>
        </div>
    </div>

    <script src="<?= $assetsUrl ?>/bootstrap/js/bootstrap.bundle.min.js"></script>
    <script src="<?= $assetsUrl ?>/js/project-shell.js?v=<?= (int) @filemtime(constant('BASE_PATH') . '/public/assets/js/project-shell.js') ?>"></script>
    <script src="<?= $assetsUrl ?>/js/pp-qr.js?v=<?= (int) @filemtime(constant('BASE_PATH') . '/public/assets/js/pp-qr.js') ?>"></script>
    <script>
    (function () {
        var SLUG = <?= json_encode($project['slug'] ?? '') ?>;
        var BASE = <?= json_encode(rtrim((string) $baseUrl, '/')) ?>;
        var URLGUESS = <?= json_encode($appUrlGuess ?? '') ?>;
        var PROJNAME = <?= json_encode($project['name'] ?? 'App') ?>;
        var cfg = <?= $configJson ?: '{}' ?>;
        var PAGES = <?= $pageOptions ?: '[]' ?>;
        var EFFECTS = <?= $effects ?: '["none"]' ?>;
        var ICON_URL = <?= json_encode($iconUrl) ?>;
        cfg.features = cfg.features || {}; cfg.tabbar = cfg.tabbar || {}; cfg.pwa = cfg.pwa || {}; cfg.edgeNav = cfg.edgeNav || {}; cfg.flavours = cfg.flavours || {};
        cfg.tabbar.items = cfg.tabbar.items || [];

        var $ = function (id) { return document.getElementById(id); };
        var savedEl = $('mobSaved'), saveTimer = null;
        function toast() { savedEl.classList.add('show'); setTimeout(function () { savedEl.classList.remove('show'); }, 1500); }
        function esc(s) { return String(s == null ? '' : s).replace(/[&<>"]/g, function (c) { return ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' })[c]; }); }

        // Curated Bootstrap-icon set for the picker gallery (names without the "bi-" prefix).
        var ICONS = ('house house-door speedometer2 grid grid-3x3-gap list-ul columns-gap kanban table people person person-badge person-circle people-fill ' +
            'mortarboard briefcase building shop bag bag-check cart cart-check basket box box-seam boxes truck wallet2 credit-card cash cash-stack ' +
            'receipt calculator clipboard clipboard-check journal-text file-earmark file-earmark-text folder folder2-open archive ' +
            'bar-chart line-chart pie-chart graph-up graph-down activity clipboard-data bezier2 diagram-3 sliders2 toggles ' +
            'bell bell-fill chat chat-dots chat-left-text envelope envelope-open telephone phone geo-alt map compass pin-map ' +
            'calendar calendar-event calendar-week clock clock-history alarm hourglass-split stopwatch ' +
            'star star-fill heart heart-fill bookmark bookmark-star flag trophy award gift lightning-charge fire gem magic ' +
            'gear gear-fill tools wrench sliders shield shield-lock shield-check key lock unlock fingerprint eye ' +
            'search zoom-in funnel filter plus-circle pencil pencil-square trash3 check2-circle x-circle arrow-repeat download upload ' +
            'cloud cloud-arrow-up wifi hdd server database globe globe2 link-45deg share ' +
            'camera image images film music-note-beamed play-circle mic printer qr-code upc-scan ' +
            'tag tags palette brush droplet basket3 emoji-smile chat-square-quote megaphone broadcast rss ' +
            'moon sun stars cup-hot egg-fried book journal-bookmark newspaper mailbox tree flower1 sun-fill').split(/\s+/).filter(Boolean);

        var EFFECT_LABEL = { none: 'None', pop: 'Pop', bounce: 'Bounce', pulse: 'Pulse', glow: 'Glow', wiggle: 'Wiggle' };
        EFFECTS.forEach(function (e) { var o = document.createElement('option'); o.value = e; o.textContent = EFFECT_LABEL[e] || e; $('mobEffect').appendChild(o); });

        function linkOptionsHtml(sel) {
            var h = '';
            PAGES.forEach(function (p) { h += '<option value="' + p.link + '"' + (p.link === sel ? ' selected' : '') + '>' + p.label + '</option>'; });
            var isCustom = sel && !PAGES.some(function (p) { return p.link === sel; });
            h += '<option value="__custom__"' + (isCustom ? ' selected' : '') + '>Custom link…</option>';
            return h;
        }

        function renderTabsEditor() {
            var wrap = $('mobTabsEditor'); wrap.innerHTML = '';
            cfg.tabbar.items.forEach(function (it, i) {
                var row = document.createElement('div'); row.className = 'mob-tab-row'; row.dataset.idx = i;
                var custom = it.link && !PAGES.some(function (p) { return p.link === it.link; });
                var ic = /^bi-/.test(it.icon) ? it.icon : 'bi-' + (it.icon || 'dot');
                row.innerHTML =
                    '<span class="mt-grip" draggable="true" title="Drag to reorder"><i class="bi bi-grip-vertical"></i></span>' +
                    '<button type="button" class="mt-ico" title="Change icon"><i class="bi ' + ic + '"></i></button>' +
                    '<input type="hidden" class="mt-icon" value="' + esc(it.icon || 'bi-dot') + '">' +
                    '<div class="mt-fields">' +
                        '<input class="mt-label" placeholder="Label" value="' + esc(it.label || '') + '">' +
                        '<select class="mt-link">' + linkOptionsHtml(it.link) + '</select>' +
                        '<input class="mt-link-custom" placeholder="/path or https://…" value="' + (custom ? esc(it.link || '') : '') + '" style="' + (custom ? '' : 'display:none') + '">' +
                    '</div>' +
                    '<label class="mob-sw sm"><input type="checkbox" class="mt-enabled"' + (it.enabled !== false ? ' checked' : '') + '><span class="track"></span><span class="thumb"></span></label>' +
                    '<button type="button" class="mt-del" title="Remove">&times;</button>';
                wrap.appendChild(row);
            });
        }

        function readTabsEditor() {
            var items = [];
            $('mobTabsEditor').querySelectorAll('.mob-tab-row').forEach(function (row) {
                var linkSel = row.querySelector('.mt-link').value;
                var link = linkSel === '__custom__' ? row.querySelector('.mt-link-custom').value.trim() : linkSel;
                var icon = row.querySelector('.mt-icon').value.trim() || 'bi-dot';
                items.push({ icon: icon, label: row.querySelector('.mt-label').value.trim(), link: link, enabled: row.querySelector('.mt-enabled').checked });
            });
            return items;
        }

        function effectiveTabs() {
            var t = cfg.tabbar, out;
            if (t.mode === 'custom') {
                out = (t.items || []).filter(function (it) { return it.enabled !== false; })
                    .map(function (it) { return { icon: it.icon, label: it.label }; });
            } else {
                out = PAGES.slice(0, Math.max(2, Math.min(5, t.count || 4)))
                    .map(function (p) { return { icon: 'bi-' + (p.link.indexOf('dashboard') > -1 ? 'speedometer2' : 'table'), label: p.label }; });
            }
            out = out.slice(0, 5);
            out.push({ icon: 'bi-grid-3x3-gap', label: 'More', more: true });
            return out;
        }

        function renderPreview() {
            $('mpBrand').textContent = (cfg.pwa.appName || PROJNAME || 'App');
            var showTab = cfg.enabled !== false && cfg.features.tabbar !== false;
            var bar = $('mpTabbar'); bar.style.display = showTab ? '' : 'none';
            bar.className = 'mp-tabbar fx-' + (cfg.tabbar.effect || 'none') + (cfg.tabbar.showLabels === false ? ' no-labels' : '');
            if (showTab) {
                bar.innerHTML = '';
                effectiveTabs().forEach(function (t, i) {
                    var a = document.createElement('span'); a.className = 'mp-tab' + (i === 0 ? ' is-active' : '');
                    var ic = /^bi-/.test(t.icon) ? t.icon : 'bi-' + (t.icon || 'dot');
                    a.innerHTML = '<i class="bi ' + ic + '"></i><span>' + (t.label || '') + '</span>';
                    bar.appendChild(a);
                });
            }
            $('mpFab').style.display = (cfg.enabled !== false) ? '' : 'none';
            var edge = cfg.enabled !== false && cfg.features.edgeNav !== false;
            var en = cfg.edgeNav || {};
            var esz = Math.max(40, Math.min(80, en.size || 52));
            [$('mpEdgeL'), $('mpEdgeR')].forEach(function (el) {
                if (!el) return;
                el.style.display = edge ? '' : 'none';
                el.style.height = Math.round(esz * 0.72) + 'px';
                el.style.width = Math.round(esz * 0.42) + 'px';
                el.style.opacity = String(en.opacity != null ? en.opacity : 0.5);
                el.style.background = en.bg || '';
                el.style.color = en.iconColor || '';
            });
            $('mobPhone').classList.toggle('mp-tabless', !showTab);
        }

        function paintMode() {
            document.querySelectorAll('.mob-seg-btn').forEach(function (b) { b.classList.toggle('is-active', b.dataset.mode === cfg.tabbar.mode); });
            document.querySelectorAll('#mobTabPanel [data-when]').forEach(function (el) { el.style.display = (el.getAttribute('data-when') === cfg.tabbar.mode) ? '' : 'none'; });
        }

        function paint() {
            $('mobEnabled').checked = cfg.enabled !== false;
            document.body.classList.toggle('mob-disabled', cfg.enabled === false);
            document.querySelectorAll('[data-feat]').forEach(function (cb) { cb.checked = cfg.features[cb.getAttribute('data-feat')] !== false; });
            cfg.tabbar.mode = (cfg.tabbar.mode === 'custom') ? 'custom' : 'auto';
            $('mobCount').value = String(Math.max(2, Math.min(5, cfg.tabbar.count || 4)));
            $('mobShowLabels').checked = cfg.tabbar.showLabels !== false;
            $('mobEffect').value = cfg.tabbar.effect || 'none';
            var en = cfg.edgeNav || {};
            $('mobEdgeSize').value = String(Math.max(40, Math.min(80, en.size || 52)));
            $('mobEdgeSizeV').textContent = $('mobEdgeSize').value + 'px';
            $('mobEdgeOpacity').value = String(Math.round((en.opacity != null ? en.opacity : 0.5) * 100));
            $('mobEdgeOpV').textContent = $('mobEdgeOpacity').value + '%';
            $('mobEdgeBg').value = /^#[0-9a-fA-F]{6}$/.test(en.bg || '') ? en.bg : '#4f46e5';
            $('mobEdgeColor').value = /^#[0-9a-fA-F]{6}$/.test(en.iconColor || '') ? en.iconColor : '#ffffff';
            $('mobEdgeBgAuto').classList.toggle('is-active', !en.bg);
            $('mobEdgeColorAuto').classList.toggle('is-active', !en.iconColor);
            $('mobDarkMode').value = ['off', 'auto', 'light', 'dark'].indexOf(cfg.darkMode) > -1 ? cfg.darkMode : 'off';
            cfg.flavours = cfg.flavours || {};
            $('mobFlvList').value = ['auto', 'cards', 'table'].indexOf(cfg.flavours.list) > -1 ? cfg.flavours.list : 'auto';
            $('mobFlvDates').value = ['custom', 'native'].indexOf(cfg.flavours.dates) > -1 ? cfg.flavours.dates : 'custom';
            $('mobFlvSelects').value = ['native', 'sheet'].indexOf(cfg.flavours.selects) > -1 ? cfg.flavours.selects : 'native';
            $('mobFlvNumbers').value = ['plain', 'stepper'].indexOf(cfg.flavours.numbers) > -1 ? cfg.flavours.numbers : 'plain';
            $('mobPreviewUrl').value = cfg.previewUrl || '';
            renderQr();
            cfg.push = cfg.push || {};
            $('mobVapidPublic').value = cfg.push.vapidPublic || '';
            $('mobVapidPrivate').value = cfg.push.vapidPrivate || '';
            $('mobVapidSubject').value = cfg.push.subject || '';
            $('mobPwa').checked = cfg.pwa.enabled !== false;
            $('mobAppName').value = cfg.pwa.appName || '';
            $('mobThemeColor').value = /^#[0-9a-fA-F]{6}$/.test(cfg.pwa.themeColor || '') ? cfg.pwa.themeColor : '#4f46e5';
            $('mobPwaFields').style.opacity = $('mobPwa').checked ? '1' : '.45';
            $('mobPwaFields').style.pointerEvents = $('mobPwa').checked ? '' : 'none';
            if (ICON_URL) { $('mobIconPrev').innerHTML = '<img src="' + ICON_URL + '" alt="">'; }
            paintMode();
            renderTabsEditor();
            renderPreview();
        }

        function collect() {
            var features = {};
            document.querySelectorAll('[data-feat]').forEach(function (cb) { features[cb.getAttribute('data-feat')] = cb.checked; });
            cfg.enabled = $('mobEnabled').checked;
            cfg.darkMode = $('mobDarkMode').value || 'off';
            cfg.flavours = {
                list: $('mobFlvList').value, dates: $('mobFlvDates').value,
                selects: $('mobFlvSelects').value, numbers: $('mobFlvNumbers').value
            };
            cfg.previewUrl = ($('mobPreviewUrl').value || '').trim().slice(0, 300);
            cfg.push = {
                vapidPublic: ($('mobVapidPublic').value || '').replace(/[^A-Za-z0-9_-]/g, ''),
                vapidPrivate: ($('mobVapidPrivate').value || '').replace(/[^A-Za-z0-9_-]/g, ''),
                subject: ($('mobVapidSubject').value || '').trim().slice(0, 120)
            };
            cfg.features = features;
            cfg.tabbar = {
                mode: cfg.tabbar.mode || 'auto',
                count: parseInt($('mobCount').value, 10) || 4,
                showLabels: $('mobShowLabels').checked,
                effect: $('mobEffect').value || 'none',
                items: cfg.tabbar.mode === 'custom' ? readTabsEditor() : (cfg.tabbar.items || [])
            };
            cfg.edgeNav = {
                size: parseInt($('mobEdgeSize').value, 10) || 52,
                bg: (cfg.edgeNav && cfg.edgeNav.bg) || '',
                iconColor: (cfg.edgeNav && cfg.edgeNav.iconColor) || '',
                opacity: (parseInt($('mobEdgeOpacity').value, 10) || 50) / 100
            };
            cfg.pwa = {
                enabled: $('mobPwa').checked,
                appName: $('mobAppName').value.trim().slice(0, 60),
                themeColor: $('mobThemeColor').value || '',
                iconFile: cfg.pwa.iconFile || ''
            };
            return cfg;
        }

        function persist() {
            fetch(BASE + '/project/' + encodeURIComponent(SLUG) + '/mobile/save', {
                method: 'POST', credentials: 'same-origin', headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({ section: 'all', data: cfg })
            }).then(function (r) { return r.json(); }).then(function (j) { if (j && j.success) toast(); }).catch(function () {});
        }
        function queueSave() { collect(); renderPreview(); clearTimeout(saveTimer); saveTimer = setTimeout(persist, 350); }

        // ── QR: open the built app on a real phone ──
        function renderQr() {
            var host = $('mobQr'); if (!host || !window.PPQR) return;
            var url = (cfg.previewUrl && cfg.previewUrl.trim()) || URLGUESS || '';
            try { window.PPQR.render(host, url, { scale: 4, quiet: 2, dark: '#0b0d12', light: '#ffffff' }); }
            catch (e) { host.textContent = 'That URL is too long to encode.'; }
            var open = $('mobQrOpen'); if (open) { open.href = url; open.style.pointerEvents = url ? '' : 'none'; }
        }

        // ── Icon picker gallery ──
        var pickTarget = null;
        function openPicker(row) { pickTarget = row; $('mobIconSearch').value = ''; renderIconGrid(''); $('mobIconPick').classList.add('open'); setTimeout(function () { $('mobIconSearch').focus(); }, 30); }
        function closePicker() { $('mobIconPick').classList.remove('open'); pickTarget = null; }
        function renderIconGrid(q) {
            q = (q || '').toLowerCase().trim();
            var grid = $('mobIconGrid'); grid.innerHTML = '';
            var list = q ? ICONS.filter(function (n) { return n.indexOf(q) > -1; }) : ICONS;
            if (!list.length) { grid.innerHTML = '<div class="mob-iconpick-empty">No icons match “' + esc(q) + '”.</div>'; return; }
            list.forEach(function (n) { var b = document.createElement('button'); b.type = 'button'; b.title = n; b.dataset.icon = 'bi-' + n; b.innerHTML = '<i class="bi bi-' + n + '"></i>'; grid.appendChild(b); });
        }
        $('mobIconSearch').addEventListener('input', function () { renderIconGrid(this.value); });
        $('mobIconGrid').addEventListener('click', function (e) {
            var b = e.target.closest('button[data-icon]'); if (!b || !pickTarget) return;
            var icon = b.dataset.icon;
            pickTarget.querySelector('.mt-icon').value = icon;
            pickTarget.querySelector('.mt-ico i').className = 'bi ' + icon;
            closePicker(); cfg.tabbar.items = readTabsEditor(); queueSave();
        });
        $('mobIconPick').addEventListener('click', function (e) { if (e.target === this || e.target.closest('[data-ip-close]')) closePicker(); });
        document.addEventListener('keydown', function (e) { if (e.key === 'Escape') closePicker(); });

        // ── Drag-to-reorder (custom tabs) ──
        (function () {
            var editor = $('mobTabsEditor'), dragRow = null;
            editor.addEventListener('dragstart', function (e) { var g = e.target.closest('.mt-grip'); if (!g) { e.preventDefault(); return; } dragRow = g.closest('.mob-tab-row'); dragRow.classList.add('dragging'); e.dataTransfer.effectAllowed = 'move'; try { e.dataTransfer.setData('text/plain', ''); } catch (x) {} });
            editor.addEventListener('dragover', function (e) { if (!dragRow) return; e.preventDefault(); var row = e.target.closest('.mob-tab-row'); if (!row || row === dragRow) return; var r = row.getBoundingClientRect(); editor.insertBefore(dragRow, ((e.clientY - r.top) / r.height > 0.5) ? row.nextSibling : row); });
            editor.addEventListener('drop', function (e) { e.preventDefault(); });
            editor.addEventListener('dragend', function () { if (!dragRow) return; dragRow.classList.remove('dragging'); dragRow = null; cfg.tabbar.items = readTabsEditor(); renderTabsEditor(); queueSave(); });
        })();

        // ── Events ──
        document.addEventListener('change', function (e) {
            var t = e.target;
            if (t.id === 'mobEnabled') document.body.classList.toggle('mob-disabled', !t.checked);
            if (t.id === 'mobPwa') { $('mobPwaFields').style.opacity = t.checked ? '1' : '.45'; $('mobPwaFields').style.pointerEvents = t.checked ? '' : 'none'; }
            if (t.id === 'mobEdgeBg') { cfg.edgeNav = cfg.edgeNav || {}; cfg.edgeNav.bg = t.value; $('mobEdgeBgAuto').classList.remove('is-active'); }
            if (t.id === 'mobEdgeColor') { cfg.edgeNav = cfg.edgeNav || {}; cfg.edgeNav.iconColor = t.value; $('mobEdgeColorAuto').classList.remove('is-active'); }
            if (t.closest && t.closest('.mob-tab-row')) {
                var row = t.closest('.mob-tab-row');
                if (t.classList.contains('mt-link')) row.querySelector('.mt-link-custom').style.display = (t.value === '__custom__') ? '' : 'none';
                cfg.tabbar.items = readTabsEditor();
            }
            queueSave();
        });
        document.addEventListener('input', function (e) {
            if (e.target.id === 'mobAppName') { renderPreview(); queueSave(); return; }
            if (e.target.id === 'mobEdgeSize') { $('mobEdgeSizeV').textContent = e.target.value + 'px'; queueSave(); return; }
            if (e.target.id === 'mobEdgeOpacity') { $('mobEdgeOpV').textContent = e.target.value + '%'; queueSave(); return; }
            if (e.target.id === 'mobPreviewUrl') { cfg.previewUrl = e.target.value.trim().slice(0, 300); renderQr(); queueSave(); return; }
            if (e.target.classList && (e.target.classList.contains('mt-label') || e.target.classList.contains('mt-link-custom'))) { cfg.tabbar.items = readTabsEditor(); queueSave(); }
        });
        document.addEventListener('click', function (e) {
            if (e.target.closest('#mobEdgeBgAuto')) { cfg.edgeNav = cfg.edgeNav || {}; cfg.edgeNav.bg = ''; $('mobEdgeBgAuto').classList.add('is-active'); queueSave(); return; }
            if (e.target.closest('#mobEdgeColorAuto')) { cfg.edgeNav = cfg.edgeNav || {}; cfg.edgeNav.iconColor = ''; $('mobEdgeColorAuto').classList.add('is-active'); queueSave(); return; }
            var seg = e.target.closest('.mob-seg-btn');
            if (seg) {
                cfg.tabbar.mode = seg.dataset.mode;
                if (cfg.tabbar.mode === 'custom' && (!cfg.tabbar.items || !cfg.tabbar.items.length)) {
                    cfg.tabbar.items = PAGES.slice(0, 4).map(function (p) { return { icon: p.link.indexOf('dashboard') > -1 ? 'bi-speedometer2' : 'bi-table', label: p.label, link: p.link, enabled: true }; });
                }
                paintMode(); renderTabsEditor(); queueSave(); return;
            }
            if (e.target.closest('#mobAddTab')) {
                cfg.tabbar.items = readTabsEditor();
                if (cfg.tabbar.items.length < 5) { cfg.tabbar.items.push({ icon: 'bi-star', label: 'Tab', link: (PAGES[0] && PAGES[0].link) || '/', enabled: true }); renderTabsEditor(); queueSave(); }
                return;
            }
            var iconBtn = e.target.closest('.mt-ico');
            if (iconBtn) { openPicker(iconBtn.closest('.mob-tab-row')); return; }
            var delBtn = e.target.closest('.mt-del');
            if (delBtn) { var row = delBtn.closest('.mob-tab-row'); var idx = parseInt(row.dataset.idx, 10); cfg.tabbar.items = readTabsEditor(); cfg.tabbar.items.splice(idx, 1); renderTabsEditor(); queueSave(); }
        });

        // ── Reset to defaults ──
        $('mobReset').addEventListener('click', function () {
            if (!window.confirm('Reset all mobile settings to defaults? (Your uploaded icon is kept.)')) return;
            cfg = {
                enabled: true,
                darkMode: 'off',
                features: {
                    tabbar: true, edgeNav: true, edgeSwipe: true, swipeActions: true, skeleton: true, haptics: true, pageTransition: true,
                    inputEnhance: true, nativePickers: true, cardify: true, liveSearch: true, stickyHeaders: true, emptyStates: true, toasts: true, sheets: true,
                    pullRefresh: true, infiniteScroll: false, modalDetail: false, viewTransitions: true, kpiCarousel: true,
                    offline: false, push: false, shareTarget: false, installPrompt: true, appBadge: true, formDrafts: true
                },
                tabbar: { mode: 'auto', count: 4, showLabels: true, effect: 'none', items: [] },
                edgeNav: { size: 52, bg: '', iconColor: '', opacity: 0.5 },
                flavours: { list: 'auto', dates: 'custom', selects: 'native', numbers: 'plain' },
                previewUrl: (cfg.previewUrl || ''),
                pwa: { enabled: true, appName: '', themeColor: '', iconFile: (cfg.pwa && cfg.pwa.iconFile) || '' },
                push: { vapidPublic: '', vapidPrivate: '', subject: '' }
            };
            paint(); persist();
        });

        // ── Icon upload ──
        $('mobIconFile').addEventListener('change', function () {
            var f = this.files && this.files[0]; if (!f) return;
            $('mobIconMsg').textContent = 'Uploading…';
            var fd = new FormData(); fd.append('icon', f);
            fetch(BASE + '/project/' + encodeURIComponent(SLUG) + '/mobile/icon', { method: 'POST', credentials: 'same-origin', body: fd })
                .then(function (r) { return r.json(); }).then(function (j) {
                    if (j && j.success) { ICON_URL = j.url; $('mobIconPrev').innerHTML = '<img src="' + j.url + '" alt="">'; cfg.pwa.iconFile = 'set'; $('mobIconMsg').textContent = 'Icon updated.'; }
                    else { $('mobIconMsg').textContent = (j && j.message) || 'Upload failed.'; }
                }).catch(function () { $('mobIconMsg').textContent = 'Upload failed.'; });
            this.value = '';
        });
        $('mobIconRemove').addEventListener('click', function () {
            fetch(BASE + '/project/' + encodeURIComponent(SLUG) + '/mobile/icon/remove', { method: 'POST', credentials: 'same-origin' })
                .then(function (r) { return r.json(); }).then(function () { ICON_URL = ''; cfg.pwa.iconFile = ''; $('mobIconPrev').innerHTML = '<i class="bi bi-image"></i>'; $('mobIconMsg').textContent = 'Removed — will auto-generate at build.'; }).catch(function () {});
        });

        // ── Generate VAPID keys ──
        $('mobVapidGen').addEventListener('click', function () {
            var btn = this; btn.disabled = true; $('mobVapidMsg').textContent = 'Generating…';
            fetch(BASE + '/project/' + encodeURIComponent(SLUG) + '/mobile/vapid', { method: 'POST', credentials: 'same-origin' })
                .then(function (r) { return r.json(); }).then(function (j) {
                    if (j && j.success) {
                        $('mobVapidPublic').value = j.publicKey; $('mobVapidPrivate').value = j.privateKey;
                        cfg.push = cfg.push || {}; cfg.push.vapidPublic = j.publicKey; cfg.push.vapidPrivate = j.privateKey;
                        $('mobVapidMsg').textContent = 'Keys generated & saved.'; queueSave();
                    } else { $('mobVapidMsg').textContent = (j && j.message) || 'Could not generate keys.'; }
                }).catch(function () { $('mobVapidMsg').textContent = 'Could not generate keys.'; })
                .finally(function () { btn.disabled = false; });
        });

        // ── Hint tooltips (?/!): tap to toggle on touch, click-away to close ──
        document.addEventListener('click', function (e) {
            var tip = e.target.closest('.mob-tip');
            document.querySelectorAll('.mob-tip.show').forEach(function (t) { if (t !== tip) t.classList.remove('show'); });
            if (tip) { e.preventDefault(); tip.classList.toggle('show'); }
        });
        document.addEventListener('keydown', function (e) {
            if ((e.key === 'Enter' || e.key === ' ') && e.target.classList && e.target.classList.contains('mob-tip')) { e.preventDefault(); e.target.classList.toggle('show'); }
        });

        paint();
    })();
    </script>
</body>

</html>
