/**
 * TitanCart Admin — Full Dashboard UI
 *
 * SOURCE FILE — THIS IS NOT A BUILD ARTIFACT.
 * This file is the hand-written, human-maintained source of the admin
 * single-page app. It is written directly against wp.element using
 * createElement() calls (no JSX), and there is no transpiler, bundler,
 * minifier, or build step anywhere in this plugin: the file in the ZIP is
 * the file the developer edits. The "build" directory name is historical
 * (the very first prototype was converted from a JSX mock-up by hand).
 * Every other JavaScript and CSS file shipped with the plugin is likewise
 * unminified source.
 *
 * @package TitanCart
 */
(function () {
    'use strict';

    if (typeof wp === 'undefined' || !wp.element) {
        console.error('TitanCart Admin: wp.element not available.');
        return;
    }

    const { createElement: h, useState, useEffect, useRef, Fragment } = wp.element;

    /* ── REST method-override shim (S473) ─────────────────────────────────────
       Every SPA write goes out as PUT/PATCH/DELETE + JSON. WordPress Playground
       (the WP.org reviewer's environment, #674) drops the BODY of non-POST
       requests — WP core's own `PUT /wp/v2/users/me` ignores its JSON there —
       so every TitanCart Save answered `invalid_body` 400 on Playground while
       the identical POST + `_method=PUT` went through. Some hosts' WAFs block
       PUT/DELETE outright too. WP_REST_Server maps `X-HTTP-Method-Override`
       back onto the request before routing, so a POST carrying the header is
       indistinguishable server-side (same trick @wordpress/api-fetch ships).
       Scoped to REST URLs on string/URL inputs; Request objects pass through. */
    (function () {
        if (typeof window.fetch !== 'function') { return; }
        var nativeFetch = window.fetch;
        window.fetch = function (input, init) {
            try {
                var m = (init && init.method) ? String(init.method).toUpperCase() : 'GET';
                if ((m === 'PUT' || m === 'PATCH' || m === 'DELETE') && (typeof input === 'string' || input instanceof URL)) {
                    var url = String(input);
                    if (url.indexOf('/wp-json/') !== -1 || url.indexOf('rest_route=') !== -1) {
                        var headers = new Headers(init.headers || {});
                        headers.set('X-HTTP-Method-Override', m);
                        init = Object.assign({}, init, { method: 'POST', headers: headers });
                    }
                }
            } catch (e) { /* fall through with the original request */ }
            return nativeFetch.call(window, input, init);
        };
    })();

    const S = { xs: 4, sm: 8, md: 12, base: 16, lg: 20, xl: 24, xxl: 32 };

    const TC = {
        /* Pulse UI neutral palette (Shadcn/Tailwind zinc-based) */
        primary: '#171717', primaryFg: '#FAFAFA',
        accent: '#2563EB', accentLight: '#3B82F6', accentBg: '#EFF6FF',
        success: '#16A34A', successBg: '#F0FDF4',
        amber: '#D97706', amberBg: '#FFFBEB',
        error: '#DC2626', errorBg: '#FEF2F2',
        lightBg: '#FFFFFF', card: '#FFFFFF',
        border: '#E5E5E5', text: '#0A0A0A', muted: '#737373',
        secondary: '#F5F5F5', secondaryFg: '#171717',
        /* Sidebar-specific (Pulse UI sidebar tokens) */
        sidebarBg: '#FFFFFF', sidebarFg: '#3F3F46',
        sidebarActive: '#18181B', sidebarActiveFg: '#FAFAFA',
        sidebarAccent: '#F4F4F5', sidebarBorder: '#E4E7EC',
        /* Chart blues (Pulse UI chart-1 → chart-5) */
        chart1: '#89B8F0', chart2: '#3B82F6', chart3: '#2563EB', chart4: '#1D4ED8', chart5: '#1E40AF',
    };

    const Config = window.TitanCartConfig || { apiUrl: '/wp-json/titancart/v1', nonce: '', version: '0.1.0' };

    /* ── Currency formatting (S120 fix) ─────────────────────────────────────
     * Single source of truth for all admin-side money display.
     *
     *   tcFmtMoney(amount)              → formats in the store's primary currency
     *   tcFmtMoney(amount, 'EUR')       → formats in the given currency code
     *
     * Pass an order/return/etc.'s stored currency code (o.currency) as the 2nd
     * arg when displaying historical records — those must always render in
     * their original currency, never the store's current default.
     * Omit the 2nd arg for live values (carts, prices, current totals) — those
     * should follow the store's current primary currency.
     *
     * Map mirrors the symbols that ship with TitanCart's seeded currencies.
     * Falls back to the 3-letter code if a code isn't in the map (better than
     * silently substituting '$').
     */
    var TC_CCY_SYMBOLS = {
        USD:'$', EUR:'\u20AC', GBP:'\u00A3', JPY:'\u00A5', CAD:'$', AUD:'$',
        CHF:'CHF', CNY:'\u00A5', INR:'\u20B9', BRL:'R$', MXN:'$', RUB:'\u20BD',
        KRW:'\u20A9', NZD:'$', SEK:'kr', NOK:'kr', DKK:'kr', PLN:'z\u0142',
        TRY:'\u20BA', ZAR:'R', HKD:'$', SGD:'$',
    };
    function tcMoneySymbol(code) {
        if (!code) return Config.currency_symbol || '$';
        var c = String(code).toUpperCase();
        return TC_CCY_SYMBOLS[c] || (c + ' ');
    }
    function tcFmtMoney(amount, code) {
        var sym = tcMoneySymbol(code);
        var n = parseFloat(amount || 0);
        return sym + n.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
    }

    /* ── Admin per-page from Settings → Appearance → Admin settings ── */
    var TC_PER_PAGE = Number(Config.adminPerPage) || 20;

    /* ── Date/time formatters driven by Settings → Appearance → Common ── */
    var _tcDateFmt = Config.dateFormat || 'mdy';
    var _tcTimeFmt = Config.timeFormat || '12';
    var _tcTZ      = Config.timezone || undefined;

    /* KI-24 Phase 4 — parse a server datetime into a correct instant.
     * Stored DATETIME columns are UTC by contract (KI-24), but the REST layer
     * emits them as bare space-separated 'Y-m-d H:i:s' with no zone marker.
     * V8 parses such a string as BROWSER-LOCAL, not UTC, so `new Date(value)`
     * lands the instant wrong by the viewer's offset BEFORE the `timeZone`
     * render option is even applied. Normalize the bare form to an explicit
     * UTC instant — the same inline fix the post-launch extension admin pages
     * already use. Everything else is already unambiguous and passes through to
     * native parsing untouched: ISO with a 'T' (incl. the localized
     * `Y-m-d\TH:i` editor round-trip values), a 'Z'/±hh:mm offset, a date-only
     * 'Y-m-d' (which V8 already treats as UTC midnight per spec), an epoch
     * number, or a Date object. A safe drop-in for `new Date(x)`. */
    function tcParseDate(d) {
        if (d === null || d === undefined || d === '') return null;
        if (d instanceof Date) return d;
        if (typeof d === 'number') return new Date(d);
        var s = String(d).trim();
        if (/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}(:\d{2}(\.\d+)?)?$/.test(s)) {
            return new Date(s.replace(' ', 'T') + 'Z');
        }
        return new Date(s);
    }

    function tcFmtDate(d) {
        var dt = tcParseDate(d);
        if (!dt || isNaN(dt.getTime())) return '\u2014';
        var opts = { month: '2-digit', day: '2-digit', year: 'numeric' };
        if (_tcTZ) opts.timeZone = _tcTZ;
        if (_tcDateFmt === 'ymd') {
            return dt.toLocaleDateString('en-CA', opts);
        } else if (_tcDateFmt === 'dmy') {
            return dt.toLocaleDateString('en-GB', opts);
        }
        return dt.toLocaleDateString('en-US', opts);
    }
    function tcFmtDateTime(d) {
        var dt = tcParseDate(d);
        if (!dt || isNaN(dt.getTime())) return '\u2014';
        var tOpts = { hour: 'numeric', minute: '2-digit', hour12: _tcTimeFmt !== '24' };
        if (_tcTZ) tOpts.timeZone = _tcTZ;
        return tcFmtDate(d) + ', ' + dt.toLocaleTimeString('en-US', tOpts);
    }
    function tcFmtDateShort(d) {
        var dt = tcParseDate(d);
        if (!dt || isNaN(dt.getTime())) return '\u2014';
        var opts = { month: '2-digit', day: '2-digit' };
        if (_tcTZ) opts.timeZone = _tcTZ;
        if (_tcDateFmt === 'ymd') {
            return dt.toLocaleDateString('en-CA', opts);
        } else if (_tcDateFmt === 'dmy') {
            return dt.toLocaleDateString('en-GB', opts);
        }
        return dt.toLocaleDateString('en-US', opts);
    }

    /* ── Extension status helper ── */
    var _extMap = (Config.extensions && typeof Config.extensions === 'object') ? Config.extensions : {};
    function extActive(id) { return !id || _extMap[id] !== 'disabled'; }

    /* ── Plugin status helper (S196) ──
     * Maps the `plugin:` flag on nav items to the corresponding Config flag.
     * Allows nav entries to gate visibility on whether a paid plugin is
     * loaded. Generalizes the older subscriptionsActive-only filter so new
     * plugins (marketplace at S196, more to come) can be added by extending
     * the map below — no changes to the filter call sites needed.
     */
    var PLUGIN_CONFIG_FLAG = {
        subscriptions: 'subscriptionsActive',
        marketplace:   'marketplaceInstalled',
        wishlist:      'wishlistPlusInstalled',
        feeds:         'productFeedsInstalled',
        cartrecovery:  'abandonedCartInstalled',
        smsnotifications: 'smsNotificationsInstalled',
        affiliates: 'affiliateMarketingInstalled',
        tipdonation: 'tipDonationInstalled',
        printful: 'printfulInstalled',
    };
    function pluginActive(pluginId) {
        if (!pluginId) return true;
        var flag = PLUGIN_CONFIG_FLAG[pluginId];
        if (!flag) return true;                    // unknown plugin → don't gate
        if (!(flag in Config)) return true;        // legacy install → don't gate
        return !!Config[flag];
    }

    /* ── Phone-home surface gating (KI-36, S358) ──────────────────────────────
     * The WordPress.org "TitanCart Lite" package physically excludes
     * src/REST/License/ and strips the marketplace/installer routes out of
     * ExtensionsController. Anything backed by those routes 404s in that build,
     * and readme.txt promises "no outbound connections of any kind".
     *
     * A nav node carrying `phoneHome:true` is therefore HIDDEN, not lock+badged
     * (#438's lock+badge exists to upsell a Pro capability; here the route
     * genuinely does not exist, and there is nothing to sell on a screen whose
     * whole job was to talk to a server this build never contacts).
     *
     * ⚠️ Hiding a nav node does NOT make its route unreachable — `#/updates`
     * still mounts ExtensionsPage and still fires its effects. Nav visibility
     * and route reachability are independent facts (S356 defect 1). The
     * load-bearing guards are the early-returns inside the four fetch helpers
     * in ExtensionsPage; this predicate is the cosmetic layer on top.
     */
    var _noPhoneHome = !!Config.noPhoneHome;

    /* Single visibility predicate for every nav consumer — the sidebar AND the
     * command palette. They filtered through two separate copies of
     * `extActive(x.ext) && pluginActive(x.plugin)` before S358; a node hidden
     * from one was still reachable from the other. One function, four call
     * sites, so a fifth cannot forget. */
    function navVisible(item) {
        if (!item) return false;
        if (item.phoneHome && _noPhoneHome) return false;
        return extActive(item.ext) && pluginActive(item.plugin);
    }

    /* S454 — parent-level visibility: a parent WITH a children array hides when
     * every child fails navVisible (an empty expandable menu, or a collapsed-mode
     * click that can only land on a Stub, helps nobody). Parents without children
     * (Vendors, Settings, Dashboard) are untouched. Behavior-neutral on Pro —
     * every Pro parent always has at least one visible child; on the Lite build
     * this hides a Growth section whose only survivors are uninstalled plugins. */
    function navParentVisible(item) {
        if (!navVisible(item)) return false;
        if (!item.children) return true;
        return item.children.some(navVisible);
    }


    /* ── SVG helpers ── */
    function ico(paths, size, vb) {
        size = size || 20; vb = vb || '0 0 20 20';
        return h('svg', { width: size, height: size, viewBox: vb, fill: 'none', stroke: 'currentColor', strokeWidth: '1.5', strokeLinecap: 'round', strokeLinejoin: 'round', style: { display: 'block', flexShrink: 0 } }, ...paths);
    }
    /* Lucide-compatible helper: 24×24 viewBox at 18px display (Pulse UI sidebar size) */
    function lucide(paths, size) {
        size = size || 18;
        return h('svg', { width: size, height: size, viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', strokeWidth: '2', strokeLinecap: 'round', strokeLinejoin: 'round', style: { display: 'block', flexShrink: 0 } }, ...paths);
    }

    const icons = {
        /* ── Sidebar nav icons (Lucide 24×24, Pulse UI style) ── */
        dashboard:    lucide([h('path',{d:'m12 14 4-4'}),h('path',{d:'M3.34 19a10 10 0 1 1 17.32 0'})]),                                                          /* Gauge */
        orders:       lucide([h('circle',{cx:8,cy:21,r:1}),h('circle',{cx:19,cy:21,r:1}),h('path',{d:'M2.05 2.05h2l2.66 12.42a2 2 0 0 0 2 1.58h9.78a2 2 0 0 0 1.95-1.57l1.65-7.43H5.12'})]),  /* ShoppingCart */
        products:     lucide([h('path',{d:'m7.5 4.27 9 5.15'}),h('path',{d:'M21 8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16Z'}),h('path',{d:'m3.3 7 8.7 5 8.7-5'}),h('path',{d:'M12 22V12'})]),  /* Package */
        customers:    lucide([h('path',{d:'M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2'}),h('circle',{cx:9,cy:7,r:4}),h('path',{d:'M22 21v-2a4 4 0 0 0-3-3.87'}),h('path',{d:'M16 3.13a4 4 0 0 1 0 7.75'})]),  /* Users */
        vendors:      lucide([h('rect',{width:20,height:14,x:2,y:7,rx:2,ry:2}),h('path',{d:'M16 21V5a2 2 0 0 0-2-2h-4a2 2 0 0 0-2 2v16'})]),  /* Briefcase */
        promotions:   lucide([h('polyline',{points:'22 7 13.5 15.5 8.5 10.5 2 17'}),h('polyline',{points:'16 7 22 7 22 13'})]),                                    /* TrendingUp */
        seo:          lucide([h('circle',{cx:12,cy:12,r:10}),h('path',{d:'M12 2a14.5 14.5 0 0 0 0 20 14.5 14.5 0 0 0 0-20'}),h('path',{d:'M2 12h20'})]),          /* Globe */
        extensions:   lucide([h('rect',{x:3,y:3,width:7,height:7,rx:1}),h('rect',{x:14,y:3,width:7,height:7,rx:1}),h('rect',{x:3,y:14,width:7,height:7,rx:1}),h('rect',{x:14,y:14,width:7,height:7,rx:1})]),  /* Grid2x2 */
        applications: lucide([h('rect',{x:2,y:3,width:20,height:14,rx:2}),h('path',{d:'M8 21h8'}),h('path',{d:'M12 17v4'})]),                                      /* Monitor / LayoutGrid */
        settings:     lucide([h('path',{d:'M20 7h-9'}),h('path',{d:'M14 17H5'}),h('circle',{cx:17,cy:17,r:3}),h('circle',{cx:7,cy:7,r:3})]),                       /* SlidersHorizontal */
        shipping:     lucide([h('path',{d:'M14 18V6a2 2 0 0 0-2-2H4a2 2 0 0 0-2 2v11a1 1 0 0 0 1 1h2'}),h('path',{d:'M15 18H9'}),h('path',{d:'M19 18h2a1 1 0 0 0 1-1v-3.65a1 1 0 0 0-.22-.624l-3.48-4.35A1 1 0 0 0 17.52 8H14'}),h('circle',{cx:17,cy:18,r:2}),h('circle',{cx:7,cy:18,r:2})]),  /* Truck */
        payments:     lucide([h('rect',{x:2,y:5,width:20,height:14,rx:2}),h('path',{d:'M2 10h20'})]),                                                                /* CreditCard */
        tax:          lucide([h('path',{d:'M12 2v20'}),h('path',{d:'M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6'})]),                                        /* DollarSign */
        email:        lucide([h('rect',{x:2,y:4,width:20,height:16,rx:2}),h('path',{d:'m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7'})]),                              /* Mail */
        reports:      lucide([h('path',{d:'M3 3v18h18'}),h('path',{d:'m19 9-5 5-4-4-3 3'})]),                                                                         /* ChartLine */
        import:       lucide([h('path',{d:'M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4'}),h('polyline',{points:'7 10 12 15 17 10'}),h('path',{d:'M12 15V3'})]),        /* Download */
        api:          lucide([h('polyline',{points:'16 18 22 12 16 6'}),h('polyline',{points:'8 6 2 12 8 18'})]),                                                      /* Code */

        /* ── Bottom nav icons ── */
        docs:         lucide([h('path',{d:'M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z'}),h('path',{d:'M14 2v4a2 2 0 0 0 2 2h4'}),h('path',{d:'M10 9H8'}),h('path',{d:'M16 13H8'}),h('path',{d:'M16 17H8'})]),  /* FileText */
        support:      lucide([h('circle',{cx:12,cy:12,r:10}),h('circle',{cx:12,cy:12,r:4}),h('path',{d:'m4.93 4.93 4.24 4.24'}),h('path',{d:'m14.83 14.83 4.24 4.24'}),h('path',{d:'m14.83 9.17 4.24-4.24'}),h('path',{d:'m4.93 19.07 4.24-4.24'})]),  /* LifeBuoy */

        /* ── Header / utility icons (keep at 16-18px) ── */
        search:       ico([h('circle',{cx:7,cy:7,r:4.5}),h('path',{d:'M10.5 10.5L14 14'})], 16, '0 0 16 16'),
        bell:         lucide([h('path',{d:'M6 8a6 6 0 0 1 12 0c0 7 3 9 3 9H3s3-2 3-9'}),h('path',{d:'M10.3 21a1.94 1.94 0 0 0 3.4 0'})], 18),
        moon:         lucide([h('path',{d:'M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z'})], 18),
        sun:          lucide([h('circle',{cx:12,cy:12,r:4}),h('path',{d:'M12 2v2'}),h('path',{d:'M12 20v2'}),h('path',{d:'m4.93 4.93 1.41 1.41'}),h('path',{d:'m17.66 17.66 1.41 1.41'}),h('path',{d:'M2 12h2'}),h('path',{d:'M20 12h2'}),h('path',{d:'m6.34 17.66-1.41 1.41'}),h('path',{d:'m19.07 4.93-1.41 1.41'})], 18),
        chevron:      ico([h('path',{d:'M5 3l4 4-4 4'})], 14, '0 0 14 14'),

        /* ── Product list Pulse-UI icons ── */
        moreVert:     lucide([h('circle',{cx:12,cy:5,r:1}),h('circle',{cx:12,cy:12,r:1}),h('circle',{cx:12,cy:19,r:1})], 16),
        plus:         lucide([h('path',{d:'M5 12h14'}),h('path',{d:'M12 5v14'})], 16),
        arrowUpRight: lucide([h('path',{d:'M7 17 17 7'}),h('path',{d:'M7 7h10v10'})], 16),
        arrowDownRight:lucide([h('path',{d:'m7 7 10 10'}),h('path',{d:'M17 7v10H7'})], 16),
        shoppingBag:  lucide([h('path',{d:'M6 2 3 6v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6l-3-4Z'}),h('path',{d:'M3 6h18'}),h('path',{d:'M16 10a4 4 0 0 1-8 0'})]),
        walletIcon:   lucide([h('path',{d:'M21 12V7H5a2 2 0 0 1 0-4h14v4'}),h('path',{d:'M3 5v14a2 2 0 0 0 2 2h16v-5'}),h('path',{d:'M18 12a2 2 0 0 0 0 4h4v-4Z'})]),
        userIcon:     lucide([h('path',{d:'M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2'}),h('circle',{cx:12,cy:7,r:4})]),  /* User — single-person silhouette (S189) */
        boxIcon:      lucide([h('path',{d:'M21 8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16Z'}),h('path',{d:'m3.3 7 8.7 5 8.7-5'}),h('path',{d:'M12 22V12'})]),
        eye:          lucide([h('path',{d:'M2.062 12.348a1 1 0 0 1 0-.696 10.75 10.75 0 0 1 19.876 0 1 1 0 0 1 0 .696 10.75 10.75 0 0 1-19.876 0'}),h('circle',{cx:12,cy:12,r:3})], 16),
        pencil:       lucide([h('path',{d:'M21.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497z'})], 16),
        trash:        lucide([h('path',{d:'M3 6h18'}),h('path',{d:'M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6'}),h('path',{d:'M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2'})], 16),
        copy:         lucide([h('rect',{width:14,height:14,x:8,y:8,rx:2,ry:2}),h('path',{d:'M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2'})], 16),
        check:        lucide([h('path',{d:'M20 6 9 17l-5-5'})], 16),
    };

    /* ── Nav data ── */
    const NAV_SECTIONS = [
        { title: null, items: [
            { icon:'dashboard', label:'Dashboard', slug:'dashboard' },
            { icon:'orders', label:'Sales', slug:'orders', children:[
                { label:'Orders',           slug:'orders' },
                { label:'Fulfillment',      slug:'fulfillment' },
                { label:'Active Carts',     slug:'active-carts' },
            ]},
            { icon:'products', label:'Products', slug:'products', children:[
                { label:'Products',         slug:'products' },
                { label:'Categories',       slug:'categories' },
                { label:'Attributes',       slug:'attributes' },
                { label:'Product Filters',  slug:'filters' },
                { label:'Product Reviews',  slug:'reviews', ext:'reviews' },
            ]},
            { icon:'customers', label:'Customers', slug:'customers', children:[
                { label:'Customers',        slug:'customers' },
                { label:'Administrators',   slug:'administrators' },
                { label:'Customer Groups',  slug:'customer-groups' },
            ]},
            { icon:'vendors', label:'Vendors', slug:'vendors', plugin:'marketplace' },
            { icon:'promotions', label:'Growth', slug:'promotions', children:[
                { label:'Subscriptions',           slug:'subscriptions', plugin:'subscriptions' },
                { label:'Wishlists',               slug:'wishlists', plugin:'wishlist' },
                { label:'Data Feeds',              slug:'datafeeds', plugin:'feeds' },
                { label:'Cart Recovery',           slug:'cartrecovery', plugin:'cartrecovery' },
                { label:'SMS Notifications',       slug:'smsnotifications', plugin:'smsnotifications' },
                { label:'Affiliates',              slug:'affiliates', plugin:'affiliates' },
                { label:'Tips & Donations',        slug:'tipdonation', plugin:'tipdonation' },
                { label:'Printful',                slug:'printful', plugin:'printful' },
            ]},
            { icon:'seo', label:'Storefront', slug:'website', collapsedSlug:'testimonials', children:[
                { label:'Pages',    slug:'pages' },
                { label:'Tags',     slug:'tags' },
                { label:'SEO',      slug:'seo', ext:'seo' },
                { label:'Testimonials', slug:'testimonials' },
                { label:'FAQ',      slug:'faq' },
                { label:'Sitemap',  slug:'sitemap' },
            ]},
            { icon:'extensions', label:'Extensions', slug:'extensions', collapsedSlug:'installed-extensions', children:[
                { label:'Installed Extensions',      slug:'installed-extensions' },
                // phoneHome: backed by /extensions/check-updates + /extensions/update/{slug},
                // both stripped from the Lite package.
                { label:'Updates',                   slug:'updates', phoneHome:true },
                // phoneHome: backed by /extensions/catalog (stripped) AND a direct
                // browser fetch to market.titancart.io for the Buy list.
                { label:'Extensions Marketplace',    slug:'extensions-marketplace', phoneHome:true },
            ]},
            { icon:'applications', label:'Applications', slug:'applications', collapsedSlug:'agenda', children:[
                { label:'Agenda',     slug:'agenda' },
                { label:'Calendar',  slug:'calendar' },
            ]},
            { icon:'settings', label:'Settings', slug:'settings' },
        ]},
    ];

    /* ── Vendor nav data (S181 — B3.5b) ──────────────────────────────────────
     * Distinct nav array used when Config.spa_mode === 'vendor'. Shows only
     * vendor-applicable surfaces. The top-level WP admin menu (titancart-vendor
     * vs titancart) is the primary gate; this array drives the SPA sidebar
     * within vendor mode.
     *
     * Currently a single item ("My Products" → /vendor-products). B3.5c will
     * add product editor sub-routes; B3 continuation will add Sales / Payouts
     * / Profile sections; B3.5e will add an admin-side approvals page (which
     * lives in the admin nav, not here).
     * ─────────────────────────────────────────────────────────────────────── */
    const VENDOR_NAV_SECTIONS = [
        { title: null, items: [
            { icon:'products',   label:'My Products', slug:'vendor-products' },
            { icon:'orders',     label:'Sales',       slug:'vendor-sales'    },
            { icon:'walletIcon', label:'Payouts',     slug:'vendor-payouts'  },
            { icon:'userIcon',   label:'Profile',     slug:'vendor-profile'  },
        ]},
    ];

    /* Helper — pick the right nav array based on SPA mode. Single source of
     * truth so future code that consumes nav data doesn't have to repeat the
     * mode branch. Defaults to admin nav for backward compat when spa_mode
     * is missing (e.g. existing TC core admin pages where the marketplace
     * plugin hasn't injected its mode flag). */
    function currentNavSections() {
        return Config.spa_mode === 'vendor' ? VENDOR_NAV_SECTIONS : NAV_SECTIONS;
    }

    /* ── Helpers ── */
    function statusColor(s, dark) {
        const m = {
            Processing: { bg: dark ? 'rgba(37,99,235,.12)' : '#EFF6FF', color: TC.accent },
            Shipped:    { bg: dark ? 'rgba(22,163,74,.12)'  : '#F0FDF4', color: TC.success },
            Delivered:  { bg: dark ? 'rgba(22,163,74,.12)'  : '#F0FDF4', color: '#15803D' },
            Pending:    { bg: dark ? 'rgba(217,119,6,.12)'  : '#FFFBEB', color: TC.amber },
            Canceled:   { bg: dark ? 'rgba(220,38,38,.12)'  : '#FEF2F2', color: TC.error },
            Completed:  { bg: dark ? 'rgba(22,163,74,.12)'  : '#F0FDF4', color: TC.success },
        };
        return m[s] || { bg: dark ? 'rgba(255,255,255,.05)' : '#F5F5F5', color: TC.muted };
    }

    function cardShadow(dark) {
        return dark
            ? '0 1px 2px rgba(0,0,0,.4), 0 0 0 1px rgba(255,255,255,.04)'
            : '0 1px 3px rgba(0,0,0,.06), 0 1px 2px rgba(0,0,0,.04)';
    }

    function MiniChart({ data, color }) {
        const mx = Math.max(...data), mn = Math.min(...data), rng = mx - mn || 1;
        const W = 120, H = 40;
        const pts = data.map((v, i) => `${(i / (data.length - 1)) * W},${H - ((v - mn) / rng) * H}`).join(' ');
        const gid = 'tcg' + color.replace('#','');
        return h('svg', { width:W, height:H, viewBox:`0 0 ${W} ${H}`, style:{display:'block'} },
            h('defs', null, h('linearGradient', { id:gid, x1:'0', y1:'0', x2:'0', y2:'1' },
                h('stop', { offset:'0%',   stopColor:color, stopOpacity:'0.12' }),
                h('stop', { offset:'100%', stopColor:color, stopOpacity:'0'    })
            )),
            h('polygon', { points:`0,${H} ${pts} ${W},${H}`, fill:`url(#${gid})` }),
            h('polyline', { points:pts, fill:'none', stroke:color, strokeWidth:'2', strokeLinecap:'round', strokeLinejoin:'round' })
        );
    }

    function BarChart({ dark, mutedColor, borderColor }) {
        const months  = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
        const revenue = [42,58,45,72,68,85,78,92,88,95,82,98];
        const orders  = [28,35,30,48,42,55,50,62,58,65,54,68];
        const CH = 180, BW = 14;
        return h('div', { style:{position:'relative', paddingLeft:34, paddingBottom:28} },
            ...[0,25,50,75,100].reverse().map(v =>
                h('div', { key:v, style:{position:'absolute', left:0, top:`${(100-v)/100*CH}px`, width:'100%', display:'flex', alignItems:'center'} },
                    h('span', { style:{fontSize:10, color:mutedColor, width:30, textAlign:'right', marginRight:4} }, v+'k'),
                    h('div',  { style:{flex:1, height:1, background:borderColor, opacity:.4} })
                )
            ),
            h('div', { style:{display:'flex', alignItems:'flex-end', justifyContent:'space-between', height:CH, paddingTop:S.sm} },
                ...months.map((m, i) =>
                    h('div', { key:m, style:{display:'flex', flexDirection:'column', alignItems:'center', flex:1} },
                        h('div', { style:{display:'flex', gap:3, alignItems:'flex-end'} },
                            h('div', { style:{width:BW, height:(revenue[i]/100)*CH, background:TC.accent, borderRadius:'4px 4px 0 0'} }),
                            h('div', { style:{width:BW, height:(orders[i]/100)*CH,  background:dark?TC.chart4:'#BFDBFE', borderRadius:'4px 4px 0 0'} })
                        ),
                        h('span', { style:{fontSize:10, color:mutedColor, marginTop:10} }, m)
                    )
                )
            )
        );
    }

    /* ── Hash router — returns { route, subId } ── */
    function useRoute() {
        /* S181 B3.5b — default route is mode-aware so vendors landing on
         * /wp-admin/admin.php?page=titancart-vendor with no hash see their
         * own products list, not the admin Dashboard (which would fail
         * since admin endpoints are gated on manage_options).
         *
         * S187 — Config.initial_route takes priority when set. The
         * marketplace's storefront mount sends the active tc_account_section
         * through this key so the SPA can land on the correct page (Sales
         * vs Products) on first paint. Subsequent in-SPA navigation uses
         * the hash and bypasses this path. */
        const defaultRoute = Config.initial_route
            || (Config.spa_mode === 'vendor' ? 'vendor-products' : 'dashboard');
        const parse = () => {
            const raw = (window.location.hash || ('#/' + defaultRoute)).replace(/^#\/?/, '');
            const parts = raw.split('/');
            return { route: parts[0] || defaultRoute, subId: parts[1] || null };
        };
        const [state, setState] = useState(parse);
        useEffect(() => {
            const onChange = () => setState(parse());
            window.addEventListener('hashchange', onChange);
            return () => window.removeEventListener('hashchange', onChange);
        }, []);
        return state;
    }

    function navigate(slug) { window.location.hash = '#/' + slug; }

    /* ── Error boundary — catches page-level crashes so the sidebar stays alive ── */
    class ErrorBoundary extends wp.element.Component {
        constructor(props) { super(props); this.state = { error: null }; this._healed = false; }
        static getDerivedStateFromError(err) { return { error: err }; }
        componentDidCatch(err, info) {
            console.error('[TitanCart] Page error:', err, info);
            // S464 mitigation, mechanism corrected S467 (KI-109): React throws
            // NotFoundError when something OUTSIDE React swaps/removes a DOM node its
            // fiber still references — root cause on WP was wp-emoji rewriting emoji
            // text nodes into <img> (fixed at source: Assets.php no longer loads
            // wp-emoji on TitanCart screens). Browser extensions (Grammarly, password
            // managers) can do the same to any page, so this one-shot remount stays as
            // generic defence: state is intact, so ONE automatic remount heals it
            // invisibly. A second occurrence in the same boundary shows the crash card.
            var msg = String(err && err.message || err);
            if (!this._healed && /insertBefore|removeChild|NotFoundError/.test(msg)) {
                this._healed = true;
                var self = this;
                setTimeout(function(){ self.setState({ error:null }); }, 0);
            }
        }
        render() {
            if (this.state.error) {
                const self = this;
                return h('main', { style:{ flex:1, overflow:'auto', padding:32 } },
                    h('div', { style:{ background:TC.errorBg, borderRadius:12, padding:32, border:`1px solid ${TC.error}30`, maxWidth:600 } },
                        h('div', { style:{ fontSize:32, marginBottom:8 } }, '⚠️'),
                        h('h3', { style:{ color:TC.error, marginBottom:8, fontSize:15, fontWeight:600 } }, 'Page Error'),
                        h('p',  { style:{ color:'#5A2020', fontSize:13, marginBottom:12 } }, 'This page crashed — but the sidebar and other pages are unaffected.'),
                        h('pre', { style:{ fontSize:11, color:'#5A2020', background:'rgba(0,0,0,.04)', padding:12, borderRadius:8, overflowX:'auto', marginBottom:12 } }, String(self.state.error)),
                        h('button', { onClick:function(){ self.setState({ error:null }); },
                            style:{ padding:'6px 18px', borderRadius:8, border:'none', background:TC.error, color:'#fff', cursor:'pointer', fontSize:13, fontWeight:500 } }, 'Retry')
                    )
                );
            }
            return this.props.children;
        }
    }

    /* ── Page stub for unbuilt sections ── */
    function Stub({ title, cardBg, borderColor, textColor, mutedColor }) {
        return h('main', { style:{flex:1, overflow:'auto', padding:S.xxl} },
            h('div', { style:{background:cardBg, borderRadius:12, border:`1px solid ${borderColor}`, padding:48, textAlign:'center'} },
                h('div', { style:{fontSize:40, marginBottom:S.base} }, '🚧'),
                h('h2', { style:{fontSize:18, fontWeight:700, color:textColor, marginBottom:S.sm} }, title),
                h('p',  { style:{fontSize:13, color:mutedColor} }, 'This section is under active development.')
            )
        );
    }

    /* ════════════════════════════════
     * Dashboard Page — Live Data + Widget System
     * ════════════════════════════════ */

    /* ── Widget registry: all available dashboard widgets ── */
    var DASHBOARD_WIDGETS = [
        { id:'stat_cards',      title:'KPI Cards',        desc:'Revenue, orders, avg order value, new customers', size:'full', defaultVisible:true },
        { id:'sales_chart',     title:'Sales Chart',      desc:'Revenue & order trends over time',                size:'full', defaultVisible:true },
        { id:'top_products',    title:'Popular Products',  desc:'Best-selling products by revenue',               size:'half', defaultVisible:true },
        { id:'order_status',    title:'Order Status',      desc:'Breakdown of orders by current status',          size:'half', defaultVisible:true },
        { id:'recent_orders',   title:'Recent Orders',     desc:'Latest transactions across all channels',        size:'full', defaultVisible:true },
        { id:'low_stock',       title:'Low Stock Alerts',  desc:'Products running low on inventory',              size:'half', defaultVisible:true },
        { id:'quick_actions',   title:'Quick Actions',     desc:'Shortcuts to common admin tasks',                size:'half', defaultVisible:true },
        { id:'revenue_summary', title:'Revenue Breakdown', desc:'Discounts, shipping & tax totals',               size:'half', defaultVisible:false },
    ];

    function DashboardPage({ dark, cardBg, textColor, mutedColor, borderColor, tableHeaderBg }) {
        var _s = useState(null),      data = _s[0],    setData = _s[1];
        var _l = useState(true),       loading = _l[0], setLoading = _l[1];
        var _p = useState('this_month'), period = _p[0], setPeriod = _p[1];
        var _w = useState(null),       layout = _w[0],  setLayout = _w[1];
        var _m = useState(false),      showModal = _m[0], setShowModal = _m[1];
        var _d = useState(null),       dragIdx = _d[0],   setDragIdx = _d[1];

        var R = 12;
        var csym = Config.currency_symbol || '$';

        /* ── API helpers ── */
        function apiUrl(p) { var base = Config.apiUrl || '/wp-json/titancart/v1'; return base + (base.indexOf('?') !== -1 && p.indexOf('?') !== -1 ? p.replace('?','&') : p); }
        function apiFetch(p) {
            return fetch(apiUrl(p), { headers:{ 'X-WP-Nonce': Config.nonce || '' } })
                .then(function(r) { return r.json(); });
        }
        function fmtMoney(v) { return csym + parseFloat(v||0).toLocaleString('en-US',{minimumFractionDigits:2,maximumFractionDigits:2}); }
        function fmtNum(v)   { return parseInt(v||0).toLocaleString('en-US'); }
        function fmtPct(v)   { if(v===null||v===undefined) return '—'; return (v>0?'+':'')+v+'%'; }
        function timeAgo(dt) {
            if(!dt) return '';
            var diff = (Date.now() - tcParseDate(dt).getTime()) / 1000;
            if(diff < 60) return 'just now';
            if(diff < 3600) return Math.floor(diff/60)+' min ago';
            if(diff < 86400) return Math.floor(diff/3600)+' hr'+(Math.floor(diff/3600)>1?'s':'')+' ago';
            return Math.floor(diff/86400)+' day'+(Math.floor(diff/86400)>1?'s':'')+' ago';
        }

        /* ── Load data + layout on mount and period change ── */
        useEffect(function() {
            setLoading(true);
            apiFetch('/reports/dashboard?period=' + period)
                .then(function(d) { setData(d); setLoading(false); })
                .catch(function() { setLoading(false); });
        }, [period]);

        useEffect(function() {
            apiFetch('/settings/dashboard-layout').then(function(d) {
                if(d && d.layout) setLayout(d.layout);
            }).catch(function(){});
        }, []);

        /* ── Determine visible widgets ── */
        var visibleWidgets = DASHBOARD_WIDGETS.filter(function(w) {
            if(!layout) return w.defaultVisible;
            var entry = layout.find(function(e) { return e.id === w.id; });
            return entry ? entry.visible : w.defaultVisible;
        });
        /* Apply layout ordering if saved */
        if(layout) {
            var orderMap = {};
            layout.forEach(function(e,i) { orderMap[e.id] = i; });
            visibleWidgets.sort(function(a,b) {
                var ai = orderMap[a.id] !== undefined ? orderMap[a.id] : 999;
                var bi = orderMap[b.id] !== undefined ? orderMap[b.id] : 999;
                return ai - bi;
            });
        }

        /* ── Save layout to server ── */
        function saveLayout(newLayout) {
            setLayout(newLayout);
            fetch(apiUrl('/settings/dashboard-layout'), {
                method:'PUT', headers:{ 'Content-Type':'application/json', 'X-WP-Nonce':Config.nonce||'' },
                body:JSON.stringify({ layout: newLayout })
            }).catch(function(){});
        }

        /* ── Toggle widget visibility from modal ── */
        function toggleWidget(id) {
            var current = layout || DASHBOARD_WIDGETS.map(function(w) { return { id:w.id, visible:w.defaultVisible }; });
            var exists = current.find(function(e) { return e.id === id; });
            var next;
            if(exists) {
                next = current.map(function(e) { return e.id===id ? { id:e.id, visible:!e.visible } : e; });
            } else {
                next = current.concat([{ id:id, visible:true }]);
            }
            saveLayout(next);
        }

        /* ── Drag reorder in modal ── */
        function onDragStart(idx) { setDragIdx(idx); }
        function onDragOver(e,idx) { e.preventDefault(); }
        function onDrop(e,idx) {
            e.preventDefault();
            if(dragIdx === null || dragIdx === idx) return;
            var current = layout || DASHBOARD_WIDGETS.map(function(w) { return { id:w.id, visible:w.defaultVisible }; });
            var arr = current.slice();
            var moved = arr.splice(dragIdx,1)[0];
            arr.splice(idx,0,moved);
            saveLayout(arr);
            setDragIdx(null);
        }

        /* ── Period selector labels ── */
        var PERIODS = [
            { value:'today',      label:'Today' },
            { value:'yesterday',  label:'Yesterday' },
            { value:'this_week',  label:'This Week' },
            { value:'last_week',  label:'Last Week' },
            { value:'this_month', label:'This Month' },
            { value:'last_month', label:'Last Month' },
            { value:'this_year',  label:'This Year' },
        ];
        var periodLabel = (PERIODS.find(function(p){return p.value===period;})||{}).label || period;

        /* ── Loading skeleton ── */
        function Skeleton(props) {
            return h('div', { style:{background:dark?'rgba(255,255,255,.06)':'#F0F2F5', borderRadius:props.r||8, width:props.w||'100%', height:props.h||20, animation:'tcPulse 1.5s infinite'} });
        }

        /* ════ WIDGET RENDER FUNCTIONS ════ */

        /* ── 1. KPI Stat Cards ── */
        function renderStatCards() {
            var s = data && data.stats;
            var cards = [
                { label:'Total Revenue', value: s ? fmtMoney(s.revenue.current) : '—', change: s ? s.revenue.change_percent : null, color:TC.success,
                  icon: h('svg',{width:20,height:20,viewBox:'0 0 20 20',fill:'none',stroke:TC.success,strokeWidth:'1.5',strokeLinecap:'round',strokeLinejoin:'round',style:{display:'block'}}, h('rect',{x:2,y:4,width:16,height:12,rx:2}),h('path',{d:'M2 8h16'}),h('path',{d:'M6 12h2'})) },
                { label:'Total Orders', value: s ? fmtNum(s.orders.current) : '—', change: s ? s.orders.change_percent : null, color:TC.accent,
                  icon: h('svg',{width:20,height:20,viewBox:'0 0 20 20',fill:'none',stroke:TC.accent,strokeWidth:'1.5',strokeLinecap:'round',strokeLinejoin:'round',style:{display:'block'}}, h('path',{d:'M6 2L3 6v10a2 2 0 002 2h10a2 2 0 002-2V6l-3-4H6z'}),h('path',{d:'M3 6h14'}),h('path',{d:'M13 9a3 3 0 01-6 0'})) },
                { label:'Avg Order Value', value: s ? fmtMoney(s.avg_order.current) : '—', change: s ? s.avg_order.change_percent : null, color:TC.amber,
                  icon: h('svg',{width:20,height:20,viewBox:'0 0 20 20',fill:'none',stroke:TC.amber,strokeWidth:'1.5',strokeLinecap:'round',strokeLinejoin:'round',style:{display:'block'}}, h('path',{d:'M16 17V7'}),h('path',{d:'M12 17V3'}),h('path',{d:'M8 17v-6'}),h('path',{d:'M4 17v-3'})) },
                { label:'New Customers', value: s ? fmtNum(s.customers.current) : '—', change: s ? s.customers.change_percent : null, color:TC.chart1,
                  icon: h('svg',{width:20,height:20,viewBox:'0 0 20 20',fill:'none',stroke:TC.chart1,strokeWidth:'1.5',strokeLinecap:'round',strokeLinejoin:'round',style:{display:'block'}}, h('circle',{cx:8,cy:7,r:3}),h('path',{d:'M2 17c0-3 2.5-5 6-5s6 2 6 5'}),h('circle',{cx:15,cy:6,r:2}),h('path',{d:'M15 10c2 0 3.5 1.2 3.5 3'})) },
            ];
            return h('div', { key:'stat_cards', style:{display:'grid', gridTemplateColumns:'repeat(4,1fr)', gap:20, marginBottom:20} },
                ...cards.map(function(c, i) {
                    var pos = c.change !== null && c.change >= 0;
                    return h('div', { key:c.label, style:{background:cardBg, borderRadius:R, padding:20, border:'1px solid '+borderColor, boxShadow:cardShadow(dark), animation:'tcFadeSlide .4s ease forwards', animationDelay:(i*.06)+'s', opacity:0, display:'flex', flexDirection:'column', gap:10} },
                        h('div', { style:{display:'flex', justifyContent:'space-between', alignItems:'flex-start'} },
                            h('div', null,
                                h('p', { style:{fontSize:13, color:mutedColor, marginBottom:6} }, c.label),
                                loading ? h(Skeleton, { w:90, h:28 }) : h('div', { style:{fontSize:26, fontWeight:600, letterSpacing:'-.5px', lineHeight:1} }, c.value)
                            ),
                            h('div', { style:{width:38, height:38, borderRadius:10, background:dark?'rgba(255,255,255,.05)':c.color+'12', display:'flex', alignItems:'center', justifyContent:'center'} }, c.icon)
                        ),
                        h('div', { style:{display:'flex', alignItems:'center', gap:6} },
                            loading ? h(Skeleton, { w:50, h:16 }) :
                            h('span', { style:{fontSize:12, fontWeight:600, color:pos?TC.success:TC.error, background:pos?(dark?'rgba(22,163,74,.10)':TC.successBg):(dark?'rgba(220,38,38,.10)':TC.errorBg), padding:'2px 7px', borderRadius:4} }, fmtPct(c.change)),
                            h('span', { style:{fontSize:12, color:mutedColor} }, 'vs prev period')
                        )
                    );
                })
            );
        }

        /* ── 2. Sales Chart (live data bar chart) ── */
        function renderSalesChart() {
            var chart = data && data.chart || [];
            var maxRev = 1; var maxOrd = 1;
            chart.forEach(function(r){ if(r.revenue>maxRev) maxRev=r.revenue; if(r.orders>maxOrd) maxOrd=r.orders; });
            var CH = 180, BW = Math.max(6, Math.min(14, Math.floor(300/Math.max(chart.length,1))));
            return h('div', { key:'sales_chart', style:{background:cardBg, borderRadius:R, padding:24, border:'1px solid '+borderColor, boxShadow:cardShadow(dark), marginBottom:20, animation:'tcFadeSlide .4s ease forwards', animationDelay:'.2s', opacity:0} },
                h('div', { style:{display:'flex', justifyContent:'space-between', alignItems:'center', marginBottom:20} },
                    h('div', null,
                        h('h3', { style:{fontSize:16, fontWeight:600, marginBottom:4} }, 'Sales Analysis'),
                        h('span', { style:{fontSize:13, color:mutedColor} }, chart.length ? chart.length+' data points for '+periodLabel : 'No data for this period')
                    ),
                    h('div', { style:{display:'flex', gap:16, alignItems:'center'} },
                        h('div', { style:{display:'flex', alignItems:'center', gap:6} }, h('div', { style:{width:10, height:10, borderRadius:3, background:TC.accent} }), h('span', { style:{fontSize:13, color:mutedColor} }, 'Revenue')),
                        h('div', { style:{display:'flex', alignItems:'center', gap:6} }, h('div', { style:{width:10, height:10, borderRadius:3, background:dark?TC.chart4:TC.chart1} }), h('span', { style:{fontSize:13, color:mutedColor} }, 'Orders'))
                    )
                ),
                loading ? h('div',{style:{height:CH, display:'flex', alignItems:'center', justifyContent:'center'}}, h(Skeleton,{w:'90%',h:CH-40}))
                : chart.length === 0
                    ? h('div', { style:{height:CH, display:'flex', alignItems:'center', justifyContent:'center', color:mutedColor, fontSize:14} }, 'No orders in this period yet.')
                    : h('div', { style:{display:'flex', alignItems:'flex-end', justifyContent:'space-between', height:CH, paddingTop:8} },
                        ...chart.map(function(pt) {
                            var rH = (pt.revenue/maxRev)*CH*0.85;
                            var oH = Math.max(4, (pt.orders/maxOrd)*CH*0.5);
                            var lbl = pt.period.length>7 ? pt.period.slice(5) : pt.period.slice(8);
                            return h('div', { key:pt.period, style:{display:'flex', flexDirection:'column', alignItems:'center', flex:1, minWidth:0}, title:fmtMoney(pt.revenue)+' / '+pt.orders+' orders' },
                                h('div', { style:{display:'flex', gap:2, alignItems:'flex-end'} },
                                    h('div', { style:{width:BW, height:Math.max(2,rH), background:TC.accent, borderRadius:'3px 3px 0 0', transition:'height .4s ease'} }),
                                    h('div', { style:{width:BW, height:Math.max(2,oH), background:dark?TC.chart4:TC.chart1, borderRadius:'3px 3px 0 0', transition:'height .4s ease'} })
                                ),
                                h('span', { style:{fontSize:9, color:mutedColor, marginTop:6, whiteSpace:'nowrap', overflow:'hidden', textOverflow:'ellipsis', maxWidth:BW*3} }, lbl)
                            );
                        })
                    )
            );
        }

        /* ── 3. Popular Products ── */
        function renderTopProducts() {
            var prods = data && data.top_products || [];
            return h('div', { key:'top_products', style:{background:cardBg, borderRadius:R, padding:24, border:'1px solid '+borderColor, boxShadow:cardShadow(dark), animation:'tcFadeSlide .4s ease forwards', animationDelay:'.3s', opacity:0} },
                h('div', { style:{display:'flex', justifyContent:'space-between', alignItems:'center', marginBottom:20} },
                    h('h3', { style:{fontSize:16, fontWeight:600} }, 'Popular Products'),
                ),
                loading ? h('div',{style:{display:'flex',flexDirection:'column',gap:12}}, [1,2,3,4,5].map(function(n){return h(Skeleton,{key:n,h:44});}))
                : prods.length === 0
                    ? h('div', { style:{padding:'24px 0', textAlign:'center', color:mutedColor, fontSize:13} }, 'No product sales yet.')
                    : h('div', { style:{display:'flex', flexDirection:'column'} },
                        ...prods.map(function(p, i) {
                            return h('div', { key:p.product_id, style:{display:'flex', alignItems:'center', gap:14, padding:'12px 0', borderBottom:i<prods.length-1?'1px solid '+borderColor:'none'} },
                                h('div', { style:{width:36, height:36, borderRadius:9, background:dark?'#262626':TC.secondary, display:'flex', alignItems:'center', justifyContent:'center', fontSize:13, fontWeight:600, color:TC.accent, flexShrink:0} }, i+1),
                                h('div', { style:{flex:1, minWidth:0} },
                                    h('div', { style:{fontSize:13.5, fontWeight:500, whiteSpace:'nowrap', overflow:'hidden', textOverflow:'ellipsis'} }, p.product_name),
                                    h('div', { style:{fontSize:12, color:mutedColor, marginTop:2} }, p.total_qty+' sold')
                                ),
                                h('div', { style:{textAlign:'right', flexShrink:0} },
                                    h('div', { style:{fontSize:13.5, fontWeight:600} }, fmtMoney(p.total_revenue))
                                )
                            );
                        })
                    )
            );
        }

        /* ── 4. Order Status ── */
        function renderOrderStatus() {
            var statuses = data && data.order_statuses || [];
            return h('div', { key:'order_status', style:{background:cardBg, borderRadius:R, padding:24, border:'1px solid '+borderColor, boxShadow:cardShadow(dark), animation:'tcFadeSlide .4s ease forwards', animationDelay:'.3s', opacity:0} },
                h('h3', { style:{fontSize:16, fontWeight:600, marginBottom:20} }, 'Order Status'),
                loading ? h('div',{style:{display:'flex',flexDirection:'column',gap:16}}, [1,2,3,4].map(function(n){return h(Skeleton,{key:n,h:28});}))
                : statuses.length === 0
                    ? h('div', { style:{padding:'24px 0', textAlign:'center', color:mutedColor, fontSize:13} }, 'No orders yet.')
                    : statuses.map(function(s) {
                        return h('div', { key:s.handle, style:{marginBottom:14} },
                            h('div', { style:{display:'flex', justifyContent:'space-between', marginBottom:5} },
                                h('span', { style:{fontSize:13, fontWeight:500} }, s.name || s.handle),
                                h('span', { style:{fontSize:13, color:mutedColor} }, s.count+' order'+(s.count!==1?'s':''))
                            ),
                            h('div', { style:{height:6, background:dark?'#262626':TC.secondary, borderRadius:99, overflow:'hidden'} },
                                h('div', { style:{height:'100%', width:s.percent+'%', background:s.color||TC.accent, borderRadius:99, transition:'width .6s ease'} })
                            )
                        );
                    })
            );
        }

        /* ── 5. Recent Orders ── */
        function renderRecentOrders() {
            var orders = data && data.recent_orders || [];
            return h('div', { key:'recent_orders', style:{background:cardBg, borderRadius:R, border:'1px solid '+borderColor, boxShadow:cardShadow(dark), animation:'tcFadeSlide .4s ease forwards', animationDelay:'.35s', opacity:0, marginBottom:20} },
                h('div', { style:{display:'flex', justifyContent:'space-between', alignItems:'center', padding:'18px 24px', borderBottom:'1px solid '+borderColor} },
                    h('div', null,
                        h('h3', { style:{fontSize:16, fontWeight:600, marginBottom:3} }, 'Recent Orders'),
                        h('span', { style:{fontSize:13, color:mutedColor} }, 'Latest transactions')
                    ),
                    h('span', { onClick:function(){navigate('orders');}, style:{fontSize:13, color:TC.accent, cursor:'pointer', fontWeight:500} }, 'View all →')
                ),
                /* Column headers */
                h('div', { style:{display:'grid', gridTemplateColumns:'1.2fr 2fr 0.5fr 0.9fr 0.8fr 1fr', padding:'10px 24px', borderBottom:'1px solid '+borderColor, background:tableHeaderBg, boxShadow:tableHeaderShadow} },
                    ...['Order','Customer','Items','Total','Status','Time'].map(function(col) {
                        return h('span', { key:col, style:{fontSize:11, fontWeight:500, color:mutedColor, textTransform:'uppercase', letterSpacing:'.04em'} }, col);
                    })
                ),
                loading ? h('div',{style:{padding:24}}, [1,2,3,4,5].map(function(n){return h(Skeleton,{key:n,h:40,r:6,w:'100%'});}))
                : orders.length === 0
                    ? h('div', { style:{padding:'32px 24px', textAlign:'center', color:mutedColor, fontSize:13} }, 'No orders yet. Your first sale will appear here.')
                    : orders.map(function(order, i) {
                        var sc = statusColor(order.status, dark);
                        /* If statusColor returned the default gray fallback, use the API-provided color */
                        if(order.status_color && sc.color === TC.muted) {
                            sc = { color: order.status_color, bg: dark ? order.status_color+'18' : order.status_color+'12' };
                        }
                        var initials = order.customer ? order.customer.split(' ').map(function(n){return n[0]||'';}).join('') : '?';
                        return h('div', {
                            key: order.order_id,
                            onClick: function(){ navigate('orders/'+order.order_id); },
                            style: { display:'grid', gridTemplateColumns:'1.2fr 2fr 0.5fr 0.9fr 0.8fr 1fr', padding:'12px 24px', borderBottom:i<orders.length-1?'1px solid '+borderColor:'none', alignItems:'center', cursor:'pointer', transition:'background .12s' },
                            onMouseEnter: function(e){ e.currentTarget.style.background = dark?'rgba(255,255,255,.03)':'#FAFAFA'; },
                            onMouseLeave: function(e){ e.currentTarget.style.background = 'transparent'; },
                        },
                            h('span', { style:{fontSize:13, fontWeight:500, color:TC.accent} }, order.order_number || ('#'+order.order_id)),
                            h('div', { style:{display:'flex', alignItems:'center', gap:8} },
                                h('div', { style:{width:28, height:28, borderRadius:'50%', background:'hsl('+(i*67%360)+',35%,'+(dark?'22%':'92%')+')', display:'flex', alignItems:'center', justifyContent:'center', fontSize:11, fontWeight:500, color:'hsl('+(i*67%360)+',45%,'+(dark?'65%':'42%')+')', flexShrink:0} }, initials),
                                h('span', { style:{fontSize:13, whiteSpace:'nowrap', overflow:'hidden', textOverflow:'ellipsis'} }, order.customer || order.email)
                            ),
                            h('span', { style:{fontSize:13, color:mutedColor} }, order.item_count || '—'),
                            h('span', { style:{fontSize:13, fontWeight:500} }, fmtMoney(order.total)),
                            h('span', { style:{fontSize:11, fontWeight:500, color:sc.color, background:sc.bg, padding:'2px 8px', borderRadius:99, display:'inline-block', width:'fit-content', border:'1px solid '+sc.color+'20', whiteSpace:'nowrap'} }, order.status),
                            h('span', { style:{fontSize:12, color:mutedColor} }, timeAgo(order.created_at))
                        );
                    })
            );
        }

        /* ── 6. Low Stock Alerts ── */
        function renderLowStock() {
            var items = data && data.low_stock || [];
            return h('div', { key:'low_stock', style:{background:cardBg, borderRadius:R, padding:24, border:'1px solid '+borderColor, boxShadow:cardShadow(dark), animation:'tcFadeSlide .4s ease forwards', animationDelay:'.35s', opacity:0} },
                h('div', { style:{display:'flex', justifyContent:'space-between', alignItems:'center', marginBottom:16} },
                    h('h3', { style:{fontSize:16, fontWeight:600} }, 'Low Stock Alerts'),
                    items.length > 0 ? h('span', { style:{background:dark?'rgba(220,38,38,.12)':TC.errorBg, color:TC.error, fontSize:11, fontWeight:600, padding:'2px 8px', borderRadius:99} }, items.length+' item'+(items.length>1?'s':'')) : null
                ),
                loading ? h('div',{style:{display:'flex',flexDirection:'column',gap:10}}, [1,2,3].map(function(n){return h(Skeleton,{key:n,h:36});}))
                : items.length === 0
                    ? h('div', { style:{padding:'20px 0', textAlign:'center', color:mutedColor, fontSize:13} }, 'All products are well stocked.')
                    : items.slice(0,8).map(function(p, i) {
                        return h('div', { key:p.product_id, style:{display:'flex', alignItems:'center', justifyContent:'space-between', padding:'8px 0', borderBottom:i<Math.min(items.length,8)-1?'1px solid '+borderColor:'none'} },
                            h('div', { style:{flex:1, minWidth:0} },
                                h('div', { style:{fontSize:13, fontWeight:500, whiteSpace:'nowrap', overflow:'hidden', textOverflow:'ellipsis'} }, p.name || 'Product #'+p.product_id),
                                h('div', { style:{fontSize:11, color:mutedColor, marginTop:2} }, p.product_code||'')
                            ),
                            h('span', { style:{fontSize:13, fontWeight:600, color:parseInt(p.stock)===0?TC.error:TC.amber, flexShrink:0} }, p.stock+' left')
                        );
                    })
            );
        }

        /* ── 7. Quick Actions ── */
        function renderQuickActions() {
            var actions = [
                { label:'Add Product',   slug:'products/new',   color:TC.accent,  icon:'📦' },
                { label:'View Orders',   slug:'orders',         color:TC.success, icon:'🛒' },
                { label:'Promotions',    slug:'promotions',     color:TC.amber,   icon:'🏷️' },
                { label:'Customers',     slug:'customers',      color:TC.chart1,  icon:'👥' },
                { label:'Import Data',   slug:'import-export',  color:TC.chart3,  icon:'📁' },
                { label:'Settings',      slug:'settings',       color:TC.muted,   icon:'⚙️' },
            ];
            return h('div', { key:'quick_actions', style:{background:cardBg, borderRadius:R, padding:24, border:'1px solid '+borderColor, boxShadow:cardShadow(dark), animation:'tcFadeSlide .4s ease forwards', animationDelay:'.35s', opacity:0} },
                h('h3', { style:{fontSize:16, fontWeight:600, marginBottom:16} }, 'Quick Actions'),
                h('div', { style:{display:'grid', gridTemplateColumns:'1fr 1fr', gap:10} },
                    ...actions.map(function(a) {
                        return h('div', { key:a.slug, onClick:function(){navigate(a.slug);},
                            style:{display:'flex', alignItems:'center', gap:10, padding:'10px 12px', borderRadius:8, cursor:'pointer', border:'1px solid '+borderColor, transition:'all .15s'},
                            onMouseEnter:function(e){e.currentTarget.style.background=dark?'rgba(255,255,255,.04)':'#F9FAFB';e.currentTarget.style.borderColor=a.color+'60';},
                            onMouseLeave:function(e){e.currentTarget.style.background='transparent';e.currentTarget.style.borderColor=borderColor;}
                        },
                            h('span', { style:{fontSize:18} }, a.icon),
                            h('span', { style:{fontSize:13, fontWeight:500} }, a.label)
                        );
                    })
                )
            );
        }

        /* ── 8. Revenue Breakdown ── */
        function renderRevenueSummary() {
            var s = data && data.stats;
            var rows = [
                { label:'Discounts Given', value: s ? fmtMoney(s.discounts) : '—', color:TC.amber },
                { label:'Shipping Collected', value: s ? fmtMoney(s.shipping) : '—', color:TC.chart1 },
                { label:'Tax Collected', value: s ? fmtMoney(s.tax) : '—', color:TC.success },
            ];
            return h('div', { key:'revenue_summary', style:{background:cardBg, borderRadius:R, padding:24, border:'1px solid '+borderColor, boxShadow:cardShadow(dark), animation:'tcFadeSlide .4s ease forwards', animationDelay:'.35s', opacity:0} },
                h('h3', { style:{fontSize:16, fontWeight:600, marginBottom:16} }, 'Revenue Breakdown'),
                ...rows.map(function(r, i) {
                    return h('div', { key:r.label, style:{display:'flex', justifyContent:'space-between', alignItems:'center', padding:'10px 0', borderBottom:i<rows.length-1?'1px solid '+borderColor:'none'} },
                        h('span', { style:{fontSize:13, color:mutedColor} }, r.label),
                        loading ? h(Skeleton,{w:60,h:16}) : h('span', { style:{fontSize:14, fontWeight:600, color:r.color} }, r.value)
                    );
                })
            );
        }

        /* ── Widget renderer map ── */
        var widgetRenderers = {
            stat_cards:      renderStatCards,
            sales_chart:     renderSalesChart,
            top_products:    renderTopProducts,
            order_status:    renderOrderStatus,
            recent_orders:   renderRecentOrders,
            low_stock:       renderLowStock,
            quick_actions:   renderQuickActions,
            revenue_summary: renderRevenueSummary,
        };

        /* ═══ Customize Dashboard Modal ═══ */
        function renderModal() {
            if(!showModal) return null;
            var modalLayout = layout || DASHBOARD_WIDGETS.map(function(w){ return { id:w.id, visible:w.defaultVisible }; });
            return h('div', { style:{position:'fixed', inset:0, zIndex:9999, display:'flex', alignItems:'center', justifyContent:'center'},
                onClick:function(e){if(e.target===e.currentTarget)setShowModal(false);} },
                h('div', { style:{position:'fixed', inset:0, background:'rgba(0,0,0,.5)'} }),
                h('div', { style:{position:'relative', zIndex:1, background:cardBg, borderRadius:16, width:520, maxHeight:'80vh', overflow:'auto', boxShadow:'0 24px 48px rgba(0,0,0,.2)', border:'1px solid '+borderColor} },
                    /* Modal header */
                    h('div', { style:{display:'flex', justifyContent:'space-between', alignItems:'center', padding:'20px 24px', borderBottom:'1px solid '+borderColor, position:'sticky', top:0, background:cardBg, zIndex:1, borderRadius:'16px 16px 0 0'} },
                        h('div', null,
                            h('h3', { style:{fontSize:17, fontWeight:600} }, 'Customize Dashboard'),
                            h('p', { style:{fontSize:13, color:mutedColor, marginTop:2} }, 'Toggle widgets and drag to reorder')
                        ),
                        h('button', { onClick:function(){setShowModal(false);}, style:{background:'none', border:'none', fontSize:20, color:mutedColor, cursor:'pointer', padding:4} }, '✕')
                    ),
                    /* Widget list */
                    h('div', { style:{padding:'16px 24px'} },
                        ...modalLayout.map(function(entry, idx) {
                            var wDef = DASHBOARD_WIDGETS.find(function(w){return w.id===entry.id;});
                            if(!wDef) return null;
                            return h('div', { key:entry.id, draggable:true,
                                onDragStart:function(){onDragStart(idx);},
                                onDragOver:function(e){onDragOver(e,idx);},
                                onDrop:function(e){onDrop(e,idx);},
                                style:{display:'flex', alignItems:'center', gap:12, padding:'12px 14px', marginBottom:8, borderRadius:10, border:'1px solid '+borderColor, cursor:'grab', background:entry.visible?(dark?'rgba(37,99,235,.06)':'rgba(37,99,235,.03)'):'transparent', transition:'all .15s'},
                                onMouseEnter:function(e){e.currentTarget.style.borderColor=TC.accent+'40';},
                                onMouseLeave:function(e){e.currentTarget.style.borderColor=borderColor;}
                            },
                                /* Drag handle */
                                h('span', { style:{color:mutedColor, fontSize:16, cursor:'grab', userSelect:'none', flexShrink:0} }, '⠿'),
                                /* Info */
                                h('div', { style:{flex:1, minWidth:0} },
                                    h('div', { style:{fontSize:13.5, fontWeight:500} }, wDef.title),
                                    h('div', { style:{fontSize:12, color:mutedColor, marginTop:1} }, wDef.desc)
                                ),
                                /* Toggle */
                                h('div', { onClick:function(){toggleWidget(entry.id);},
                                    style:{width:40, height:22, borderRadius:11, background:entry.visible?TC.accent:(dark?'#333':'#D4D4D4'), cursor:'pointer', position:'relative', transition:'background .2s', flexShrink:0} },
                                    h('div', { style:{width:16, height:16, borderRadius:8, background:'#fff', position:'absolute', top:3, left:entry.visible?21:3, transition:'left .2s', boxShadow:'0 1px 3px rgba(0,0,0,.2)'} })
                                )
                            );
                        })
                    ),
                    /* Reset button */
                    h('div', { style:{padding:'12px 24px 20px', borderTop:'1px solid '+borderColor, display:'flex', justifyContent:'space-between'} },
                        h('button', { onClick:function(){ saveLayout(null); setLayout(null); },
                            style:{padding:'8px 16px', borderRadius:8, border:'1px solid '+borderColor, background:'transparent', color:mutedColor, fontSize:13, cursor:'pointer'} }, 'Reset to Default'),
                        h('button', { onClick:function(){setShowModal(false);},
                            style:{padding:'8px 20px', borderRadius:8, border:'none', background:TC.accent, color:'#fff', fontSize:13, fontWeight:500, cursor:'pointer'} }, 'Done')
                    )
                )
            );
        }

        /* ═══ MAIN RENDER ═══ */
        /* Group widgets into rows: full-width widgets get their own row; half-width pair up */
        var rows = [];
        var halfBuf = [];
        visibleWidgets.forEach(function(w) {
            if(w.size === 'full') {
                if(halfBuf.length) { rows.push({ type:'half', widgets:halfBuf }); halfBuf = []; }
                rows.push({ type:'full', widgets:[w] });
            } else {
                halfBuf.push(w);
                if(halfBuf.length === 2) { rows.push({ type:'half', widgets:halfBuf }); halfBuf = []; }
            }
        });
        if(halfBuf.length) rows.push({ type:'half', widgets:halfBuf });

        return h('main', { style:{flex:1, overflow:'auto', padding:24} },
            /* ── Dashboard header ── */
            h('div', { style:{display:'flex', justifyContent:'space-between', alignItems:'center', marginBottom:20} },
                h('div', null,
                    h('h2', { style:{fontSize:20, fontWeight:700, letterSpacing:'-.3px'} }, 'Dashboard'),
                    h('p', { style:{fontSize:13, color:mutedColor, marginTop:2} }, 'Your store at a glance')
                ),
                h('div', { style:{display:'flex', gap:10, alignItems:'center'} },
                    /* Period selector */
                    h('select', { value:period, onChange:function(e){setPeriod(e.target.value);},
                        style:{background:dark?'#262626':TC.secondary, border:'1px solid '+borderColor, borderRadius:8, padding:'7px 14px', fontSize:13, color:textColor, cursor:'pointer', outline:'none'} },
                        ...PERIODS.map(function(p){ return h('option', { key:p.value, value:p.value }, p.label); })
                    ),
                    /* Customize button */
                    h('button', { onClick:function(){setShowModal(true);},
                        style:{display:'flex', alignItems:'center', gap:6, padding:'7px 14px', borderRadius:8, border:'1px solid '+borderColor, background:'transparent', color:textColor, fontSize:13, fontWeight:500, cursor:'pointer', transition:'all .15s'},
                        onMouseEnter:function(e){e.currentTarget.style.borderColor=TC.accent;e.currentTarget.style.color=TC.accent;},
                        onMouseLeave:function(e){e.currentTarget.style.borderColor=borderColor;e.currentTarget.style.color=textColor;}
                    },
                        h('svg', {width:15,height:15,viewBox:'0 0 15 15',fill:'none',stroke:'currentColor',strokeWidth:'1.5',strokeLinecap:'round',style:{display:'block'}},
                            h('rect',{x:1,y:1,width:5,height:5,rx:1}),h('rect',{x:9,y:1,width:5,height:5,rx:1}),h('rect',{x:1,y:9,width:5,height:5,rx:1}),h('rect',{x:9,y:9,width:5,height:5,rx:1})
                        ),
                        'Customize'
                    )
                )
            ),

            /* ── Render widget rows ── */
            ...rows.map(function(row, ri) {
                if(row.type === 'full') {
                    var fn = widgetRenderers[row.widgets[0].id];
                    return fn ? fn() : null;
                }
                return h('div', { key:'row-'+ri, style:{display:'grid', gridTemplateColumns:row.widgets.length===2?'1fr 1fr':'1fr', gap:20, marginBottom:20} },
                    ...row.widgets.map(function(w) {
                        var fn = widgetRenderers[w.id];
                        return fn ? fn() : null;
                    })
                );
            }),

            /* ── Customize modal ── */
            renderModal()
        );
    }

    /* ════════════════════════════════
     * Reports Page  (Module 12)
     * ════════════════════════════════ */

    /* ════════════════════════════════
     * Shared API + UI primitives
     * ════════════════════════════════ */

    function apiFetch(path, opts) {
        opts = opts || {};
        opts.headers = Object.assign({ 'Content-Type': 'application/json', 'X-WP-Nonce': Config.nonce }, opts.headers || {});
        if (opts.body && typeof opts.body === 'object') opts.body = JSON.stringify(opts.body);
        /* When apiUrl uses ?rest_route= format, additional ? in path must become & */
        var url = Config.apiUrl + path;
        if (Config.apiUrl.indexOf('?') !== -1 && path.indexOf('?') !== -1) {
            url = Config.apiUrl + path.replace('?', '&');
        }
        return fetch(url, opts).then(function(r) {
            return r.text().then(function(text) {
                var data = null;
                /* KI-49 defect 3 (#448): a catch that returns a null-ish default converts a
                 * loud server failure into a silent client one. A 2xx whose body is not JSON
                 * — e.g. $wpdb echoing "<div id='error'>WordPress database error…</div>"
                 * ahead of the payload on a WP_DEBUG_DISPLAY host — used to resolve null,
                 * so the failure surfaced three frames later as a meaningless TypeError at
                 * the innocent consumer. Never swallow a parse error on a response you did
                 * not produce: the bytes the server sent are the only evidence that exists. */
                var parseErr = null;
                if (text) {
                    try { data = JSON.parse(text); } catch(e) { data = null; parseErr = e; }
                }
                if (!r.ok) {
                    // Log full PHP error to browser console for debugging.
                    console.error('[TitanCart] API error ' + r.status + ' on ' + path, parseErr ? text : data);
                    var msg = data && (data.message || data.error || JSON.stringify(data));
                    if (!msg && parseErr) msg = 'HTTP ' + r.status + ' — non-JSON response: ' + text.slice(0, 300);
                    var err = new Error(msg || 'HTTP ' + r.status);
                    err.status = r.status;
                    err.data   = data;
                    err.raw    = text;
                    return Promise.reject(err);
                }
                if (parseErr) {
                    console.error('[TitanCart] API ' + r.status + ' on ' + path + ' returned a non-JSON body', text);
                    var perr = new Error('Server returned ' + r.status + ' with a non-JSON body: ' + text.slice(0, 300));
                    perr.status    = r.status;
                    perr.parseFail = true;
                    perr.raw       = text;
                    return Promise.reject(perr);
                }
                return data;
            });
        });
    }

    /* B3.5c (S182) — sibling of apiFetch that targets the marketplace
     * vendor REST namespace. Use this for any /me/vendor/* path in vendor
     * mode (Config.spa_mode === 'vendor'). Falls back to apiFetch's URL
     * builder when Config.vendor_api_url is missing — preserves error
     * surface but logs a console warning so the mode-aware editor stays
     * usable for debugging instead of throwing immediately. */
    function apiFetchVendor(path, opts) {
        var base = Config.vendor_api_url || '';
        if (!base) {
            console.warn('[TitanCart] apiFetchVendor called with empty vendor_api_url; falling back to apiFetch');
            return apiFetch(path, opts);
        }
        opts = opts || {};
        opts.headers = Object.assign({ 'Content-Type': 'application/json', 'X-WP-Nonce': Config.nonce }, opts.headers || {});
        if (opts.body && typeof opts.body === 'object') opts.body = JSON.stringify(opts.body);
        // Strip trailing slash from base, leading slash from path is fine
        var trimmed = base.replace(/\/+$/, '');
        var url = trimmed + path;
        if (trimmed.indexOf('?') !== -1 && path.indexOf('?') !== -1) {
            url = trimmed + path.replace('?', '&');
        }
        return fetch(url, opts).then(function(r) {
            return r.text().then(function(text) {
                var data = null;
                if (text) { try { data = JSON.parse(text); } catch(e) { data = null; } }
                if (!r.ok) {
                    console.error('[TitanCart] Vendor API error ' + r.status + ' on ' + path, data);
                    var msg = data && (data.message || data.error || JSON.stringify(data));
                    var err = new Error(msg || 'HTTP ' + r.status);
                    err.status = r.status;
                    err.data   = data;
                    return Promise.reject(err);
                }
                return data;
            });
        });
    }

    /* ════════════════════════════════════════════════════════════════════════
     * NotificationBell (KI-26, S304) — header notification bell.
     *
     * Self-contained admin notifications consumer. Fetches the unread count +
     * recent list from titancart/v1/notifications (own REST), shows a badge,
     * opens a dropdown, marks items read (one / all), and click-through
     * navigates to the target record via the SPA router.
     *
     * v1 scope = platform / standalone admin only: in vendor SPA mode
     * (Config.spa_mode === 'vendor') it renders an inert bell with no badge or
     * fetch (vendor-site notifications are a deferred follow-up). Defined at
     * module scope (never inside another component) per the focus-loss rule.
     * ════════════════════════════════════════════════════════════════════════ */
    function tcRelTime(s) {
        try {
            var d = (typeof tcParseDate === 'function') ? tcParseDate(s) : new Date(s);
            var diff = Date.now() - d.getTime();
            if (isNaN(diff)) return '';
            if (diff < 0) diff = 0;
            var m = Math.floor(diff / 60000);
            if (m < 1)  return 'just now';
            if (m < 60) return m + 'm ago';
            var hr = Math.floor(m / 60);
            if (hr < 24) return hr + 'h ago';
            var dd = Math.floor(hr / 24);
            if (dd < 7)  return dd + 'd ago';
            return d.toLocaleDateString();
        } catch (e) { return ''; }
    }

    function NotificationBell(props) {
        var dark        = props.dark;
        var cardBg      = props.cardBg;
        var mutedColor  = props.mutedColor;
        var borderColor = props.borderColor;
        var textColor   = props.textColor;
        var navigate    = props.navigate;

        var isVendor = Config.spa_mode === 'vendor';

        var _o = useState(false),  open    = _o[0], setOpen    = _o[1];
        var _i = useState([]),     items   = _i[0], setItems   = _i[1];
        var _u = useState(0),      unread  = _u[0], setUnread  = _u[1];
        var wrapRef = useRef(null);

        function load() {
            if (isVendor) return;
            apiFetch('/notifications?limit=20')
                .then(function(d) {
                    if (!d) return;
                    setItems(Array.isArray(d.items) ? d.items : []);
                    setUnread(parseInt(d.unread || 0, 10) || 0);
                })
                .catch(function() {});
        }

        // Initial fetch + 60s poll.
        useEffect(function() {
            if (isVendor) return;
            load();
            var t = setInterval(load, 60000);
            return function() { clearInterval(t); };
        }, []);

        // Refetch when the dropdown is opened (fresh on demand).
        useEffect(function() { if (open) load(); }, [open]);

        // Close on outside click.
        useEffect(function() {
            if (!open) return;
            function onDoc(e) { if (wrapRef.current && !wrapRef.current.contains(e.target)) setOpen(false); }
            document.addEventListener('mousedown', onDoc);
            return function() { document.removeEventListener('mousedown', onDoc); };
        }, [open]);

        function markRead(id) {
            apiFetch('/notifications/' + id + '/read', { method: 'POST' })
                .then(function(d) { if (d && typeof d.unread !== 'undefined') setUnread(parseInt(d.unread, 10) || 0); })
                .catch(function() {});
            setItems(function(prev) { return prev.map(function(n) { return n.id === id ? Object.assign({}, n, { is_read: 1 }) : n; }); });
        }

        function markAll() {
            apiFetch('/notifications/read-all', { method: 'POST' }).catch(function() {});
            setUnread(0);
            setItems(function(prev) { return prev.map(function(n) { return Object.assign({}, n, { is_read: 1 }); }); });
        }

        function openItem(n) {
            if (!n.is_read) markRead(n.id);
            setOpen(false);
            if (n.link) navigate(n.link);
        }

        var bellBtn = h('button', {
            onClick: function() { if (!isVendor) setOpen(!open); },
            style: { background: 'transparent', border: 'none', borderRadius: '50%', width: 36, height: 36, cursor: isVendor ? 'default' : 'pointer', color: mutedColor, display: 'flex', alignItems: 'center', justifyContent: 'center', transition: 'background .15s' },
            onMouseEnter: function(e) { e.currentTarget.style.background = dark ? 'rgba(255,255,255,.06)' : 'rgba(0,0,0,.04)'; },
            onMouseLeave: function(e) { e.currentTarget.style.background = 'transparent'; },
            'aria-label': 'Notifications',
        }, icons.bell);

        // Inert bell for vendor mode (v1 scope) — no badge, no dropdown.
        if (isVendor) {
            return h('div', { style: { position: 'relative' } }, bellBtn);
        }

        var badge = unread > 0
            ? h('span', { style: { position: 'absolute', top: 2, right: 2, minWidth: 18, height: 18, padding: '0 4px', background: TC.error, borderRadius: 9, border: '2px solid ' + cardBg, display: 'flex', alignItems: 'center', justifyContent: 'center', color: '#fff', fontSize: 9, fontWeight: 600, boxSizing: 'border-box', pointerEvents: 'none' } }, unread > 99 ? '99+' : String(unread))
            : null;

        var dropdown = open
            ? h('div', { style: { position: 'absolute', top: 44, right: 0, width: 360, maxWidth: '90vw', background: cardBg, border: '1px solid ' + borderColor, borderRadius: 12, boxShadow: cardShadow(dark), zIndex: 1000, overflow: 'hidden' } },
                // Header
                h('div', { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '12px 16px', borderBottom: '1px solid ' + borderColor } },
                    h('span', { style: { fontSize: 14, fontWeight: 600, color: textColor } }, 'Notifications'),
                    unread > 0
                        ? h('button', { onClick: markAll, style: { background: 'transparent', border: 'none', color: TC.accent, fontSize: 12, fontWeight: 500, cursor: 'pointer', padding: 0 } }, 'Mark all read')
                        : null
                ),
                // List
                h('div', { style: { maxHeight: 380, overflowY: 'auto' } },
                    items.length === 0
                        ? h('div', { style: { padding: '32px 16px', textAlign: 'center', color: mutedColor, fontSize: 13 } }, 'No notifications yet.')
                        : items.map(function(n) {
                            return h('div', {
                                key: n.id,
                                onClick: function() { openItem(n); },
                                style: { display: 'flex', gap: 10, padding: '12px 16px', borderBottom: '1px solid ' + borderColor, cursor: 'pointer', background: n.is_read ? 'transparent' : (dark ? 'rgba(37,99,235,.06)' : '#F5F9FF'), transition: 'background .12s' },
                                onMouseEnter: function(e) { e.currentTarget.style.background = dark ? 'rgba(255,255,255,.04)' : '#F9FAFB'; },
                                onMouseLeave: function(e) { e.currentTarget.style.background = n.is_read ? 'transparent' : (dark ? 'rgba(37,99,235,.06)' : '#F5F9FF'); },
                            },
                                // Unread dot
                                h('span', { style: { flexShrink: 0, width: 8, height: 8, borderRadius: '50%', marginTop: 6, background: n.is_read ? 'transparent' : TC.accent } }),
                                h('div', { style: { flex: 1, minWidth: 0 } },
                                    h('div', { style: { fontSize: 13, fontWeight: n.is_read ? 500 : 600, color: textColor, marginBottom: 2 } }, n.title || ''),
                                    n.body ? h('div', { style: { fontSize: 12, color: mutedColor, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } }, n.body) : null,
                                    h('div', { style: { fontSize: 11, color: mutedColor, marginTop: 3 } }, tcRelTime(n.created_at))
                                )
                            );
                        })
                )
            )
            : null;

        return h('div', { ref: wrapRef, style: { position: 'relative' } }, bellBtn, badge, dropdown);
    }

    /* ── Command palette (roadmap #13) — TitanCart-native ⌘K launcher ─────────
     * Self-contained SPA modal that REPLACES the KI-25 dependency on WordPress
     * core's @wordpress/commands palette. Opens on ⌘K / Ctrl+K (captured on
     * window so it preempts WP core's palette) and on the header Search button
     * (via the 'tc:command-palette-open' window event). Commands are the nav
     * targets the sidebar would show — built from the same NAV_SECTIONS /
     * VENDOR_NAV_SECTIONS data, gated by the same extActive() / pluginActive()
     * helpers — and selecting one calls the module-scope navigate(). Zero
     * WordPress coupling beyond the keydown intercept (Rule 4: admin-UX sugar).
     */
    function buildPaletteCommands() {
        var sections = (Config.spa_mode === 'vendor') ? VENDOR_NAV_SECTIONS : NAV_SECTIONS;
        var cmds = [];
        sections.forEach(function(section) {
            (section.items || []).forEach(function(item) {
                if (!navParentVisible(item)) return;
                // Parent / standalone page.
                cmds.push({ label: item.label, slug: item.slug, icon: item.icon });
                // Children become their own commands, tagged with the parent label.
                (item.children || []).forEach(function(child) {
                    if (!navVisible(child)) return;
                    cmds.push({ label: child.label, slug: child.slug, parent: item.label, icon: item.icon });
                });
            });
        });
        // De-dupe by slug (a parent whose slug equals its first child, e.g. orders).
        var seen = {}, out = [];
        cmds.forEach(function(c) { if (!seen[c.slug]) { seen[c.slug] = 1; out.push(c); } });
        return out;
    }

    function CommandPalette(props) {
        var dark        = props.dark;
        var cardBg      = props.cardBg;
        var mutedColor  = props.mutedColor;
        var borderColor = props.borderColor;
        var textColor   = props.textColor;

        var _o = useState(false), open   = _o[0], setOpen   = _o[1];
        var _q = useState(''),    query  = _q[0], setQuery  = _q[1];
        var _a = useState(0),     active = _a[0], setActive = _a[1];
        var inputRef = useRef(null);

        var allCommands = buildPaletteCommands();
        var q = query.trim().toLowerCase();
        var results = !q ? allCommands : allCommands.filter(function(c) {
            return (c.label + ' ' + (c.parent || '')).toLowerCase().indexOf(q) !== -1;
        });

        function close() { setOpen(false); setQuery(''); setActive(0); }
        function run(cmd) { if (!cmd) return; close(); navigate(cmd.slug); }

        // Open on ⌘K / Ctrl+K — capture-phase on window so it fires before (and
        // preempts) WordPress core's command palette — and on the header Search
        // button's custom event.
        useEffect(function() {
            function onKey(e) {
                var k = (e.key || '').toLowerCase();
                if ((e.metaKey || e.ctrlKey) && k === 'k') {
                    e.preventDefault();
                    if (typeof e.stopImmediatePropagation === 'function') e.stopImmediatePropagation();
                    setOpen(function(o) { return !o; });
                }
            }
            function onOpenEvt() { setOpen(true); }
            window.addEventListener('keydown', onKey, true);
            window.addEventListener('tc:command-palette-open', onOpenEvt);
            return function() {
                window.removeEventListener('keydown', onKey, true);
                window.removeEventListener('tc:command-palette-open', onOpenEvt);
            };
        }, []);

        // Focus the input + reset selection when opened.
        useEffect(function() {
            if (open) { setActive(0); setTimeout(function() { if (inputRef.current) inputRef.current.focus(); }, 0); }
        }, [open]);

        // Keep the active index in range as the query narrows results.
        useEffect(function() {
            if (active > results.length - 1) setActive(results.length ? results.length - 1 : 0);
        }, [query]);

        if (!open) return null;

        function onInputKey(e) {
            var k = e.key;
            if (k === 'Escape')    { e.preventDefault(); close(); return; }
            if (k === 'ArrowDown') { e.preventDefault(); setActive(function(i) { return Math.min(i + 1, results.length - 1); }); return; }
            if (k === 'ArrowUp')   { e.preventDefault(); setActive(function(i) { return Math.max(i - 1, 0); }); return; }
            if (k === 'Enter')     { e.preventDefault(); run(results[active]); return; }
        }

        return h('div', {
            onMouseDown: function(e) { if (e.target === e.currentTarget) close(); },
            style: { position: 'fixed', top: 0, left: 0, right: 0, bottom: 0, zIndex: 100000, background: 'rgba(0,0,0,.45)', display: 'flex', alignItems: 'flex-start', justifyContent: 'center', paddingTop: '12vh', backdropFilter: 'blur(2px)' }
        },
            h('div', { style: { width: 560, maxWidth: '92vw', background: cardBg, border: '1px solid ' + borderColor, borderRadius: 14, boxShadow: cardShadow(dark), overflow: 'hidden' } },
                // Search input row.
                h('div', { style: { display: 'flex', alignItems: 'center', gap: 10, padding: '14px 16px', borderBottom: '1px solid ' + borderColor } },
                    h('span', { style: { display: 'flex', color: mutedColor } }, icons.search),
                    h('input', {
                        ref: inputRef,
                        value: query,
                        onChange: function(e) { setQuery(e.target.value); setActive(0); },
                        onKeyDown: onInputKey,
                        placeholder: 'Search pages…',
                        style: { flex: 1, border: 'none', outline: 'none', background: 'transparent', color: textColor, fontSize: 15, fontFamily: 'inherit' }
                    }),
                    h('span', { style: { fontSize: 11, color: mutedColor, background: dark ? '#171717' : '#fff', padding: '2px 6px', borderRadius: 4, border: '1px solid ' + borderColor } }, 'esc')
                ),
                // Results list.
                h('div', { style: { maxHeight: '48vh', overflowY: 'auto', padding: 6 } },
                    results.length === 0
                        ? h('div', { style: { padding: '28px 16px', textAlign: 'center', color: mutedColor, fontSize: 13 } }, 'No matching pages.')
                        : results.map(function(c, i) {
                            var isActive = i === active;
                            return h('div', {
                                key: c.slug,
                                onMouseDown: function(e) { e.preventDefault(); run(c); },
                                onMouseEnter: function() { setActive(i); },
                                style: { display: 'flex', alignItems: 'center', gap: 10, padding: '10px 12px', borderRadius: 8, cursor: 'pointer', background: isActive ? (dark ? 'rgba(37,99,235,.16)' : '#EEF4FF') : 'transparent', color: textColor, fontSize: 14 }
                            },
                                h('span', { style: { display: 'flex', flexShrink: 0, color: isActive ? TC.accent : mutedColor } }, icons[c.icon] || icons.search),
                                h('span', { style: { flex: 1 } }, c.label),
                                c.parent ? h('span', { style: { fontSize: 12, color: mutedColor } }, c.parent) : null
                            );
                        })
                )
            )
        );
    }

    /* B7 S196 — sibling of apiFetch / apiFetchVendor that targets the
     * admin-side marketplace REST namespace (titancart-marketplace/v1).
     * Used by AdminVendorsPage and any future admin SPA surface that
     * talks to the marketplace extension. Falls back to apiFetch when
     * Config.marketplaceApiUrl is empty — preserves error surface but
     * logs a console warning so degraded environments stay debuggable.
     */
    function apiFetchMarketplace(path, opts) {
        var base = Config.marketplaceApiUrl || '';
        if (!base) {
            console.warn('[TitanCart] apiFetchMarketplace called with empty marketplaceApiUrl; marketplace plugin probably not installed');
            return apiFetch(path, opts);
        }
        opts = opts || {};
        opts.headers = Object.assign({ 'Content-Type': 'application/json', 'X-WP-Nonce': Config.nonce }, opts.headers || {});
        if (opts.body && typeof opts.body === 'object') opts.body = JSON.stringify(opts.body);
        var trimmed = base.replace(/\/+$/, '');
        var url = trimmed + path;
        if (trimmed.indexOf('?') !== -1 && path.indexOf('?') !== -1) {
            url = trimmed + path.replace('?', '&');
        }
        return fetch(url, opts).then(function(r) {
            return r.text().then(function(text) {
                var data = null;
                if (text) { try { data = JSON.parse(text); } catch(e) { data = null; } }
                if (!r.ok) {
                    console.error('[TitanCart] Marketplace API error ' + r.status + ' on ' + path, data);
                    var msg = data && (data.message || data.error || JSON.stringify(data));
                    var err = new Error(msg || 'HTTP ' + r.status);
                    err.status = r.status;
                    err.data   = data;
                    return Promise.reject(err);
                }
                return data;
            });
        });
    }

    /* Tiny reusable UI atoms */
    function Btn(p) {
        const base = { display:'inline-flex', alignItems:'center', gap:8, padding:'8px 16px', borderRadius:8, fontSize:14, fontWeight:500, cursor:'pointer', transition:'all .15s ease', border:'none', outline:'none' };
        const variants = {
            primary:   { background:TC.accent,  color:'#fff' },
            secondary: { background:'transparent', color:p.mutedColor, border:`1px solid ${p.borderColor}` },
            danger:    { background:TC.error,    color:'#fff' },
        };
        const v = variants[p.variant || 'secondary'];
        return h('button', { onClick:p.onClick, disabled:p.disabled, style:Object.assign({}, base, v, p.style || {}) }, p.children);
    }

    function Badge({ status, dark, children }) {
        const map = {
            active:    { bg:dark?'rgba(22,163,74,.12)':TC.successBg,  color:TC.success },
            a:         { bg:dark?'rgba(22,163,74,.12)':TC.successBg,  color:TC.success },
            inactive:  { bg:dark?'rgba(255,255,255,.05)':'#F5F5F5',   color:TC.muted },
            d:         { bg:dark?'rgba(255,255,255,.05)':'#F5F5F5',   color:TC.muted },
            pending:   { bg:dark?'rgba(217,119,6,.12)':TC.amberBg,    color:TC.amber },
            processing:{ bg:dark?'rgba(37,99,235,.12)':TC.accentBg,   color:TC.accent },
            completed: { bg:dark?'rgba(22,163,74,.12)':TC.successBg,  color:'#15803D' },
            shipped:   { bg:dark?'rgba(22,163,74,.12)':TC.successBg,  color:TC.success },
            cancelled: { bg:dark?'rgba(220,38,38,.12)':TC.errorBg,    color:TC.error },
            failed:    { bg:dark?'rgba(220,38,38,.12)':TC.errorBg,    color:TC.error },
            open:      { bg:dark?'rgba(37,99,235,.12)':TC.accentBg,   color:TC.accent },
        };
        const c = map[(status||'').toLowerCase()] || { bg:dark?'rgba(255,255,255,.05)':'#F5F5F5', color:TC.muted };
        return h('span', { style:{ fontSize:11, fontWeight:500, padding:'2px 8px', borderRadius:99, background:c.bg, color:c.color, display:'inline-block', width:'fit-content', whiteSpace:'nowrap', border:`1px solid ${c.color}18` } }, children || status);
    }

    function Spinner({ mutedColor }) {
        return h('div', { style:{ textAlign:'center', padding:48, color:mutedColor, fontSize:13 } }, 'Loading…');
    }

    function EmptyState({ mutedColor, icon, title, sub }) {
        return h('div', { style:{ textAlign:'center', padding:64, color:mutedColor } },
            h('div', { style:{ fontSize:32, marginBottom:S.base } }, icon || '📭'),
            h('div', { style:{ fontSize:14, fontWeight:500, marginBottom:S.sm } }, title || 'Nothing here yet'),
            sub && h('div', { style:{ fontSize:12, marginTop:S.sm } }, sub)
        );
    }

    function Pager({ page, totalPages, onPage, mutedColor, borderColor }) {
        if (!totalPages || totalPages <= 1) return null;
        const pages = [];
        for (let i = 1; i <= Math.min(totalPages, 7); i++) pages.push(i);
        return h('div', { style:{ display:'flex', gap:S.xs, alignItems:'center' } },
            ...pages.map(function(p) {
                return h('button', { key:p, onClick:()=>onPage(p), style:{ width:28, height:28, borderRadius:6, border:p===page?'none':`1px solid ${borderColor}`, background:p===page?TC.accent:'transparent', color:p===page?'#fff':mutedColor, fontSize:11, cursor:'pointer' } }, p);
            })
        );
    }

    function SearchBox({ value, onChange, placeholder, borderColor, dark, mutedColor }) {
        return h('div', { style:{ display:'flex', alignItems:'center', gap:8, background:dark?'#262626':'#F5F5F5', borderRadius:8, padding:'8px 14px', border:`1px solid ${borderColor}`, width:260, transition:'border-color .15s' } },
            h('span', { style:{ color:mutedColor, display:'flex', flexShrink:0 } }, icons.search),
            h('input', { value:value, onChange:function(e){ onChange(e.target.value); }, placeholder:placeholder||'Search…',
                style:{ background:'transparent', border:'none', outline:'none', color:'inherit', fontSize:14, width:'100%' } })
        );
    }

    function PageWrap({ title, sub, mutedColor, action, backBtn, children }) {
        return h('main', { style:{ flex:1, overflow:'auto', padding:24 } },
            h('div', { style:{ display:'flex', justifyContent:'space-between', alignItems:'center', marginBottom:24 } },
                h('div', { style:{ display:'flex', alignItems:'center', gap:12 } },
                    backBtn || null,
                    h('div', null,
                        h('h2', { style:{ fontSize:20, fontWeight:600, letterSpacing:'-.3px', marginBottom:2 } }, title),
                        sub && h('div', { style:{ fontSize:14, color:mutedColor } }, sub)
                    )
                ),
                action && h('div', null, action)
            ),
            children
        );
    }

    function Card({ cardBg, borderColor, dark, pad, style, children }) {
        return h('div', { style: Object.assign({ background:cardBg, borderRadius:12, border:`1px solid ${borderColor}`, boxShadow:cardShadow(dark), padding: pad != null ? pad : 24, overflow:'hidden' }, style || {}) }, children);
    }

    /* ════════════════════════════════════════════════════════════════════
     * PRODUCTS — List, Editor, Categories, Attributes
     * ════════════════════════════════════════════════════════════════════ */

    /* ── Shared input/select style factory ── */
    /* Module-level shadow var — updated by App render based on dark mode */
    var tableHeaderShadow = '0 1px 3px rgba(0,0,0,.05), inset 0 -1px 0 rgba(0,0,0,.04)';

    function fieldSty(dark, borderColor) {
        return { width:'100%', padding:'9px 12px', borderRadius:8, border:`1px solid ${borderColor}`,
                 background:dark?'#262626':'#fff', color:'inherit', fontSize:14, outline:'none',
                 transition:'border-color .15s, box-shadow .15s', colorScheme:dark?'dark':'normal' };
    }

    /* ── Rich Text Editor ─────────────────────────────────────────────────
     * Full-featured WYSIWYG using contentEditable + execCommand.
     * Props: value (HTML string), onChange(html), dark, borderColor, minHeight, placeholder
     * ──────────────────────────────────────────────────────────────────── */
    function RichTextEditor({ value, onChange, dark, borderColor, minHeight, placeholder }) {
        var editorRef    = useRef(null);
        var lastHtml     = useRef('');
        var focused      = useRef(false);
        var _src         = useState(false), sourceMode = _src[0], setSourceMode = _src[1];
        var _srcVal      = useState(''),    sourceVal  = _srcVal[0], setSourceVal = _srcVal[1];
        var colorRef     = useRef(null);
        var hiliteRef    = useRef(null);

        minHeight = minHeight || 200;
        var bc = borderColor || '#E5E5E5';
        var bg = dark ? '#1a1a1a' : '#fff';
        var toolBg = dark ? '#1f1f23' : '#FFFFFF';
        var toolBorder = dark ? '#333' : '#E8E8EC';
        var toolColor = dark ? '#aaa' : '#6B7280';
        var toolShadow = dark
            ? '0 1px 2px rgba(0,0,0,.3), inset 0 1px 0 rgba(255,255,255,.04)'
            : '0 1px 3px rgba(0,0,0,.06), inset 0 1px 0 rgba(255,255,255,.9)';

        // Sync value into editor only when not focused (avoid cursor jumping).
        useEffect(function() {
            if (editorRef.current && !focused.current && value !== lastHtml.current) {
                editorRef.current.innerHTML = value || '';
                lastHtml.current = value || '';
            }
        }, [value]);

        function handleInput() {
            if (!editorRef.current) return;
            var html = editorRef.current.innerHTML;
            if (html === '<br>' || html === '<div><br></div>') html = '';
            lastHtml.current = html;
            if (onChange) onChange(html);
        }

        function exec(cmd, val) {
            document.execCommand(cmd, false, val || null);
            editorRef.current && editorRef.current.focus();
            handleInput();
        }

        function insertLink() {
            var sel = window.getSelection();
            var existing = '';
            if (sel && sel.rangeCount) {
                var node = sel.anchorNode;
                while (node && node.tagName !== 'A') node = node.parentElement;
                if (node && node.tagName === 'A') existing = node.href;
            }
            var url = prompt('Enter URL:', existing || 'https://');
            if (url) exec('createLink', url);
        }

        function insertImage() {
            var url = prompt('Enter image URL:', 'https://');
            if (url) exec('insertImage', url);
        }

        function toggleSource() {
            if (!sourceMode) {
                // Switching TO source mode — capture current HTML.
                setSourceVal(editorRef.current ? editorRef.current.innerHTML : (value || ''));
                setSourceMode(true);
            } else {
                // Switching FROM source — push sourceVal into React state, visual div will pick it up via useEffect.
                lastHtml.current = sourceVal;
                if (onChange) onChange(sourceVal);
                setSourceMode(false);
            }
        }

        // When leaving source mode, sync the source HTML into the editor div after it mounts.
        useEffect(function() {
            if (!sourceMode && editorRef.current && sourceVal) {
                editorRef.current.innerHTML = sourceVal;
            }
        }, [sourceMode]);

        function handleSourceChange(e) {
            var v = e.target.value;
            setSourceVal(v);
            lastHtml.current = v;
            if (onChange) onChange(v);
        }

        // ── Toolbar button ──
        function TBtn(props) {
            return h('button', {
                type: 'button',
                title: props.title,
                onMouseDown: function(e) { e.preventDefault(); props.action(); },
                onMouseEnter: function(e) { if (!props.active) e.currentTarget.style.background = dark ? 'rgba(255,255,255,.08)' : '#F3F4F6'; },
                onMouseLeave: function(e) { if (!props.active) e.currentTarget.style.background = 'none'; },
                style: {
                    background: props.active ? (dark ? 'rgba(37,99,235,.2)' : '#EEF2FF') : 'none',
                    border: 'none', cursor: 'pointer',
                    padding: '5px 8px', borderRadius: 5, fontSize: 13, fontWeight: 600,
                    color: props.active ? TC.accent : toolColor,
                    lineHeight: 1, display: 'inline-flex', alignItems: 'center',
                    minWidth: 28, justifyContent: 'center', transition: 'all .12s',
                }
            }, props.label);
        }

        function Sep() {
            return h('div', { style: { width: 1, height: 20, background: dark ? '#333' : '#E5E7EB', margin: '0 5px', flexShrink: 0 } });
        }

        // ── Font size dropdown ──
        function FontSizeSelect() {
            return h('select', {
                title: 'Font size',
                onChange: function(e) {
                    var v = e.target.value;
                    if (!v) return;
                    // execCommand fontSize uses 1-7 scale, so we use formatBlock + inline style instead.
                    exec('fontSize', v);
                    e.target.value = '';
                },
                onMouseDown: function(e) { e.stopPropagation(); },
                style: {
                    background: dark ? '#333' : '#FAFAFA', border: '1px solid ' + toolBorder,
                    borderRadius: 5, padding: '4px 6px', fontSize: 12, color: toolColor,
                    cursor: 'pointer', outline: 'none', width: 62,
                }
            },
                h('option', { value: '' }, 'Size'),
                h('option', { value: '1' }, 'Tiny'),
                h('option', { value: '2' }, 'Small'),
                h('option', { value: '3' }, 'Normal'),
                h('option', { value: '4' }, 'Large'),
                h('option', { value: '5' }, 'Huge'),
                h('option', { value: '6' }, 'Massive'),
                h('option', { value: '7' }, 'Max')
            );
        }

        // ── Color picker button (hidden input triggered by button) ──
        function ColorBtn(props) {
            var ref = props.pickerRef;
            return h('span', { style: { position: 'relative', display: 'inline-flex', alignItems: 'center' } },
                h('button', {
                    type: 'button',
                    title: props.title,
                    onMouseDown: function(e) {
                        e.preventDefault();
                        ref.current && ref.current.click();
                    },
                    style: {
                        background: 'none', border: 'none', cursor: 'pointer',
                        padding: '4px 7px', borderRadius: 4, fontSize: 13, fontWeight: 600,
                        color: toolColor, lineHeight: 1, display: 'inline-flex', alignItems: 'center',
                        minWidth: 28, justifyContent: 'center', gap: 3,
                    }
                }, props.label,
                    h('span', { style: { display: 'block', width: 12, height: 3, background: props.previewColor || '#333', borderRadius: 1, marginTop: 1 } })
                ),
                h('input', {
                    ref: ref,
                    type: 'color',
                    value: props.previewColor || '#000000',
                    onChange: function(e) { props.onPick(e.target.value); },
                    style: { position: 'absolute', width: 0, height: 0, opacity: 0, pointerEvents: 'none' },
                })
            );
        }

        var _tc = useState('#000000'), textColorVal = _tc[0], setTextColorVal = _tc[1];
        var _hc = useState('#FFFF00'), hiliteColorVal = _hc[0], setHiliteColorVal = _hc[1];

        return h('div', { style: { border: '1px solid ' + bc, borderRadius: 10, overflow: 'hidden', boxShadow: dark ? '0 2px 6px rgba(0,0,0,.2)' : '0 2px 8px rgba(0,0,0,.04), 0 0 0 1px rgba(0,0,0,.03)' } },
            // ── Toolbar Row 1: Formatting ──
            h('div', { style: {
                display: 'flex', flexWrap: 'wrap', gap: 2, padding: '8px 10px',
                background: toolBg, borderBottom: '1px solid ' + toolBorder,
                boxShadow: toolShadow, alignItems: 'center', position: 'relative', zIndex: 2,
            } },
                // Undo / Redo
                h(TBtn, { title:'Undo', label:'↩', action:function(){ exec('undo'); } }),
                h(TBtn, { title:'Redo', label:'↪', action:function(){ exec('redo'); } }),
                h(Sep),
                // Text formatting
                h(TBtn, { title:'Bold', label:'B', action:function(){ exec('bold'); } }),
                h(TBtn, { title:'Italic', label: h('em',null,'I'), action:function(){ exec('italic'); } }),
                h(TBtn, { title:'Underline', label: h('u',null,'U'), action:function(){ exec('underline'); } }),
                h(TBtn, { title:'Strikethrough', label: h('s',null,'S'), action:function(){ exec('strikethrough'); } }),
                h(Sep),
                // Font size
                h(FontSizeSelect),
                h(Sep),
                // Text color
                h(ColorBtn, {
                    title: 'Text color',
                    label: 'A',
                    pickerRef: colorRef,
                    previewColor: textColorVal,
                    onPick: function(c) { setTextColorVal(c); exec('foreColor', c); }
                }),
                // Highlight color
                h(ColorBtn, {
                    title: 'Highlight color',
                    label: h('span', { style: { background: hiliteColorVal, padding: '0 3px', borderRadius: 2 } }, 'A'),
                    pickerRef: hiliteRef,
                    previewColor: hiliteColorVal,
                    onPick: function(c) { setHiliteColorVal(c); exec('hiliteColor', c); }
                }),
                h(Sep),
                // Block formatting
                h(TBtn, { title:'Heading 2', label:'H2', action:function(){ exec('formatBlock','H2'); } }),
                h(TBtn, { title:'Heading 3', label:'H3', action:function(){ exec('formatBlock','H3'); } }),
                h(TBtn, { title:'Paragraph', label:'¶', action:function(){ exec('formatBlock','P'); } }),
                h(TBtn, { title:'Blockquote', label: h('span',{style:{borderLeft:'3px solid '+toolColor, paddingLeft:4}},'"'), action:function(){ exec('formatBlock','BLOCKQUOTE'); } }),
                h(Sep),
                // Lists
                h(TBtn, { title:'Bullet list', label:'•≡', action:function(){ exec('insertUnorderedList'); } }),
                h(TBtn, { title:'Numbered list', label:'1.', action:function(){ exec('insertOrderedList'); } }),
                h(Sep),
                // Insert
                h(TBtn, { title:'Link', label:'🔗', action:insertLink }),
                h(TBtn, { title:'Unlink', label: h('span',{style:{textDecoration:'line-through',fontSize:11}},'🔗'), action:function(){ exec('unlink'); } }),
                h(TBtn, { title:'Insert image', label:'🖼', action:insertImage }),
                h(TBtn, { title:'Horizontal rule', label:'―', action:function(){ exec('insertHorizontalRule'); } }),
                h(Sep),
                // Utilities
                h(TBtn, { title:'Clear formatting', label:'✕', action:function(){ exec('removeFormat'); } }),
                h(TBtn, { title: sourceMode ? 'Visual editor' : 'HTML source', label: sourceMode ? '👁' : '</>',
                    active: sourceMode, action: toggleSource })
            ),
            // ── Editor area OR Source textarea ──
            sourceMode
                ? h('textarea', {
                    value: sourceVal,
                    onChange: handleSourceChange,
                    style: {
                        width: '100%', minHeight: minHeight, padding: '12px 14px',
                        border: 'none', outline: 'none', resize: 'vertical',
                        fontSize: 13, fontFamily: 'monospace', lineHeight: 1.5,
                        color: dark ? '#7dd3fc' : '#1e3a5f', background: dark ? '#111' : '#f8fafc',
                        boxSizing: 'border-box',
                    }
                })
                : h('div', {
                    ref: editorRef,
                    contentEditable: true,
                    'data-placeholder': placeholder || 'Enter description...',
                    onInput: handleInput,
                    onFocus: function() { focused.current = true; },
                    onBlur: function() { focused.current = false; handleInput(); },
                    onPaste: function(e) {
                        e.preventDefault();
                        var html = e.clipboardData.getData('text/html');
                        var text = e.clipboardData.getData('text/plain');
                        if (html) {
                            html = html.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, '')
                                       .replace(/<style[^>]*>[\s\S]*?<\/style>/gi, '')
                                       .replace(/\s(class|id)="[^"]*"/gi, '');
                            document.execCommand('insertHTML', false, html);
                        } else {
                            document.execCommand('insertText', false, text);
                        }
                        handleInput();
                    },
                    style: {
                        minHeight: minHeight,
                        padding: '12px 14px',
                        outline: 'none',
                        fontSize: 14,
                        lineHeight: 1.65,
                        color: 'inherit',
                        background: bg,
                        overflowY: 'auto',
                        maxHeight: 500,
                    }
                }),
            // ── Editor styles ──
            h('style', null,
                '[data-placeholder]:empty:before { content: attr(data-placeholder); color: ' + (dark ? '#555' : '#aaa') + '; pointer-events: none; }' +
                '[contenteditable] h2 { font-size:20px; font-weight:600; margin:14px 0 6px; }' +
                '[contenteditable] h3 { font-size:17px; font-weight:600; margin:12px 0 4px; }' +
                '[contenteditable] p { margin:6px 0; }' +
                '[contenteditable] ul, [contenteditable] ol { padding-left:24px; margin:8px 0; }' +
                '[contenteditable] li { margin:2px 0; }' +
                '[contenteditable] blockquote { border-left:4px solid ' + (dark ? '#444' : '#ddd') + '; margin:10px 0; padding:8px 16px; color:' + (dark ? '#999' : '#666') + '; font-style:italic; background:' + (dark ? '#1a1a1a' : '#fafafa') + '; border-radius:0 6px 6px 0; }' +
                '[contenteditable] a { color:' + TC.accent + '; text-decoration:underline; }' +
                '[contenteditable] img { max-width:100%; height:auto; border-radius:6px; margin:8px 0; }' +
                '[contenteditable] hr { border:none; border-top:2px solid ' + (dark ? '#333' : '#e5e5e5') + '; margin:16px 0; }'
            )
        );
    }

    /* ════════════════════════════════════════════════════════════════════
     * Module-level helpers — MUST live outside any component so React
     * doesn't treat them as new types on every render (which causes
     * inputs to unmount/remount and lose focus on every keystroke).
     * ════════════════════════════════════════════════════════════════════ */

    /* FR — labelled form row used in ProductEditorPage */
    function FR({ label, required, note, ctx, children }) {
        const mutedColor = (ctx && ctx.mutedColor) || TC.muted;
        const lbl     = { fontSize:14, color:mutedColor, paddingTop:8, textAlign:'right', lineHeight:1.4, fontWeight:500 };
        const helpTxt = { fontSize:12, color:mutedColor, marginTop:4 };
        return h('div', { style:{ display:'grid', gridTemplateColumns:'170px 1fr', gap:24, marginBottom:20, alignItems:'start' } },
            h('div', { style:lbl }, label, required && h('span', { style:{ color:TC.error } }, ' *')),
            h('div', null, children, note && h('div', { style:helpTxt }, note))
        );
    }

    /* ConfirmModal — Pulse-styled destructive-action confirmation dialog.
     * S183 (B3.5c session 2). Module-scope so React doesn't remount it on
     * every parent re-render (which would lose its keyboard listeners).
     *
     * Designed reusable: vendor delete is the first consumer at S183, but
     * the admin product list (and other destructive flows that still use
     * native confirm()) can adopt the same UX with a one-line wire-up.
     *
     * Props:
     *   open          bool      — controls visibility
     *   title         string    — headline (e.g. "Delete product?")
     *   message       node      — body copy (string or React node)
     *   detail        string    — optional sub-line under title (e.g. quoted name)
     *   confirmLabel  string    — confirm button copy, defaults to "Delete"
     *   cancelLabel   string    — cancel button copy, defaults to "Cancel"
     *   busy          bool      — disables both buttons + shows "Deleting…"
     *   onConfirm     fn        — fired when confirm clicked or Enter pressed
     *   onCancel      fn        — fired on backdrop / close / cancel / Esc
     *   dark, cardBg, borderColor, textColor, mutedColor — theme tokens
     *
     * Keyboard: Esc cancels, Enter confirms (both no-ops while busy).
     * Click outside (backdrop) cancels unless busy — prevents accidental
     * dismissal during in-flight requests. */
    function ConfirmModal({ open, title, message, detail, confirmLabel, cancelLabel, busy, busyLabel, tone, onConfirm, onCancel, showReason, reasonValue, onReasonChange, reasonLabel, reasonPlaceholder, reasonRequired, dark, cardBg, borderColor, textColor, mutedColor }) {
        useEffect(function(){
            if (!open) return;
            function onKey(e) {
                if (e.key === 'Escape') { e.preventDefault(); if (!busy) onCancel && onCancel(); }
                if (e.key === 'Enter' && !showReason)  { e.preventDefault(); if (!busy) onConfirm && onConfirm(); }
            }
            document.addEventListener('keydown', onKey);
            return function(){ document.removeEventListener('keydown', onKey); };
        }, [open, busy, onConfirm, onCancel, showReason]);

        if (!open) return null;

        var _cardBg      = cardBg      || (dark ? '#171717' : '#FFFFFF');
        var _borderColor = borderColor || TC.border;
        var _textColor   = textColor   || TC.text;
        var _mutedColor  = mutedColor  || TC.muted;
        var _cancel      = cancelLabel  || 'Cancel';
        var _confirm     = confirmLabel || 'Delete';
        var _tone        = tone === 'positive' ? 'positive' : 'destructive';
        // Tone palette: destructive=red triangle, positive=green check.
        // Default busy label tracks the tone so callers can omit busyLabel
        // for the common case ('Deleting…' / 'Approving…').
        var _toneColor   = _tone === 'positive' ? TC.success : TC.error;
        var _toneBgLight = _tone === 'positive' ? TC.successBg : TC.errorBg;
        var _toneBgDark  = _tone === 'positive' ? 'rgba(22,163,74,.15)' : 'rgba(220,38,38,.15)';
        var _toneIcon    = _tone === 'positive'
            ? h('svg', { width:18, height:18, viewBox:'0 0 20 20', fill:'none', stroke:'currentColor', strokeWidth:'2', strokeLinecap:'round', strokeLinejoin:'round' },
                h('path', { d:'M16 6 8 14l-4-4' })
              )
            : h('svg', { width:18, height:18, viewBox:'0 0 20 20', fill:'none', stroke:'currentColor', strokeWidth:'1.75', strokeLinecap:'round', strokeLinejoin:'round' },
                h('path', { d:'M10 7v4' }),
                h('path', { d:'M10 14h.01' }),
                h('path', { d:'M8.6 2.6L1.7 14.5a1.6 1.6 0 001.4 2.4h13.8a1.6 1.6 0 001.4-2.4L11.4 2.6a1.6 1.6 0 00-2.8 0z' })
              );
        var _busyLabel   = busyLabel || (_tone === 'positive' ? 'Approving\u2026' : 'Deleting\u2026');

        // S203 — reason capture (Pattern #168 backwards-compat synthesis shape).
        // The textarea renders inline below the message body when callers opt in
        // via showReason=true. Parent owns the reason state; ConfirmModal is
        // purely presentational. All existing call sites remain unchanged.
        var _reasonValue       = typeof reasonValue === 'string' ? reasonValue : '';
        var _reasonLabel       = reasonLabel || 'Reason (optional)';
        var _reasonPlaceholder = reasonPlaceholder || 'Add context for the audit trail\u2026';
        var _reasonRequired    = !!reasonRequired;
        var _confirmDisabled   = busy || ( _reasonRequired && _reasonValue.trim() === '' );

        return h('div', {
                style:{ position:'fixed', inset:0, zIndex:9999, display:'flex', alignItems:'center', justifyContent:'center', background:'rgba(0,0,0,.5)' },
                onClick: function(e){ if (e.target === e.currentTarget && !busy) onCancel && onCancel(); }
            },
            h('div', { style:{ position:'relative', zIndex:1, background:_cardBg, borderRadius:16, width:460, maxWidth:'90vw', overflow:'hidden', boxShadow:'0 24px 48px rgba(0,0,0,.2)', border:'1px solid '+_borderColor, color:_textColor } },
                /* Header — tone disc + title + close X */
                h('div', { style:{ display:'flex', alignItems:'flex-start', gap:12, padding:'20px 24px 4px' } },
                    h('div', { style:{ width:36, height:36, borderRadius:18, background: dark ? _toneBgDark : _toneBgLight, color:_toneColor, display:'flex', alignItems:'center', justifyContent:'center', flexShrink:0 } },
                        _toneIcon
                    ),
                    h('div', { style:{ flex:1, minWidth:0 } },
                        h('h3', { style:{ fontSize:16, fontWeight:600, color:_textColor, margin:0, lineHeight:1.4 } }, title || 'Are you sure?'),
                        detail && h('div', { style:{ fontSize:13, color:_mutedColor, marginTop:2, overflow:'hidden', textOverflow:'ellipsis', whiteSpace:'nowrap' } }, detail)
                    ),
                    h('button', {
                        onClick: function(){ if (!busy) onCancel && onCancel(); },
                        disabled: busy,
                        'aria-label': 'Close',
                        style:{ background:'none', border:'none', fontSize:20, color:_mutedColor, cursor: busy ? 'default' : 'pointer', padding:4, lineHeight:1, opacity: busy ? .5 : 1 }
                    }, '\u00D7')
                ),
                /* Body */
                h('div', { style:{ padding:'8px 24px '+(showReason?12:20)+'px', fontSize:13.5, color:_mutedColor, lineHeight:1.55 } }, message),
                /* S203 — Optional reason textarea (audit-trail capture) */
                showReason && h('div', { style:{ padding:'0 24px 20px' } },
                    h('label', { style:{ display:'block', fontSize:11, color:_mutedColor, marginBottom:6, fontWeight:500, textTransform:'uppercase', letterSpacing:'.3px' } }, _reasonLabel),
                    h('textarea', {
                        value: _reasonValue,
                        onChange: function(e){ if (onReasonChange) onReasonChange(e.target.value); },
                        placeholder: _reasonPlaceholder,
                        disabled: busy,
                        rows: 3,
                        style:{
                            width:'100%',
                            boxSizing:'border-box',
                            padding:'8px 10px',
                            borderRadius:6,
                            border:'1px solid '+_borderColor,
                            background: dark ? 'rgba(255,255,255,.04)' : '#FFFFFF',
                            color: _textColor,
                            fontSize:13,
                            fontFamily:'inherit',
                            lineHeight:1.5,
                            resize:'vertical',
                            outline:'none',
                            opacity: busy ? .5 : 1
                        }
                    })
                ),
                /* Footer — Cancel (outline) + Confirm (filled, tone-colored) */
                h('div', { style:{ padding:'12px 24px 20px', borderTop:'1px solid '+_borderColor, display:'flex', justifyContent:'flex-end', gap:8, background: dark ? 'rgba(255,255,255,.02)' : '#FAFAFA' } },
                    h('button', {
                        onClick: function(){ if (!busy) onCancel && onCancel(); },
                        disabled: busy,
                        style:{ padding:'8px 16px', borderRadius:8, border:'1px solid '+_borderColor, background:'transparent', color:_textColor, fontSize:13, fontWeight:500, cursor: busy ? 'default' : 'pointer', opacity: busy ? .5 : 1 }
                    }, _cancel),
                    h('button', {
                        onClick: function(){ if (!_confirmDisabled) onConfirm && onConfirm(); },
                        disabled: _confirmDisabled,
                        style:{ padding:'8px 18px', borderRadius:8, border:'none', background: _confirmDisabled ? _toneColor+'90' : _toneColor, color:'#fff', fontSize:13, fontWeight:600, cursor: _confirmDisabled ? 'default' : 'pointer' }
                    }, busy ? _busyLabel : _confirm)
                )
            )
        );
    }

    /* SectionHead — collapsible section toggle used in ProductEditorPage */
    function SectionHead({ id, title, ctx }) {
        const borderColor = (ctx && ctx.borderColor) || TC.border;
        const textColor   = (ctx && ctx.textColor)   || TC.text;
        const mutedColor  = (ctx && ctx.mutedColor)  || TC.muted;
        const isOpen      = ctx && ctx.open ? ctx.open[id] : false;
        const onToggle    = (ctx && ctx.onToggle) || function(){};
        return h('div', {
            onClick:()=>onToggle(id),
            style:{ display:'flex', alignItems:'center', gap:8, cursor:'pointer', padding:'12px 0',
                    borderBottom:`1px solid ${borderColor}`, marginBottom: isOpen ? 24 : 0, userSelect:'none' }
        },
            h('span', { style:{ fontSize:15, fontWeight:600, color:textColor } }, title),
            h('span', { style:{ transform:isOpen?'rotate(90deg)':'rotate(0)', transition:'transform .2s',
                                 color:mutedColor, fontSize:11, marginLeft:4 } }, '▶')
        );
    }

    /* RadioGroup — radio buttons used in ProductEditorPage */
    function RadioGroup({ value, onChange, options }) {
        return h('div', { style:{ display:'flex', gap:24, alignItems:'center', paddingTop:6 } },
            ...options.map(([v, l]) => h('label', { key:v, style:{ display:'flex', alignItems:'center', gap:6, cursor:'pointer', fontSize:14 } },
                h('input', { type:'radio', name:'status_'+v, checked:value===v, onChange:()=>onChange(v),
                    style:{ accentColor:TC.accent, cursor:'pointer' } }),
                l
            ))
        );
    }

    /* FilterChip — used in ProductListPage filter bar */
    function FilterChip({ label, value, onClear, activeMenu, onMenuToggle, dark, borderColor, mutedColor, children }) {
        return h('div', { style:{ position:'relative', display:'inline-flex' } },
            h('button', {
                onClick:()=>onMenuToggle(activeMenu===label ? null : label),
                style:{ display:'flex', alignItems:'center', gap:6, padding:'6px 14px', borderRadius:8,
                        border:`1px solid ${borderColor}`, background:value?(dark?'rgba(37,99,235,.12)':'#EFF6FF'):(dark?'#262626':'#F5F5F5'),
                        color:value?TC.accent:mutedColor, fontSize:13, cursor:'pointer', whiteSpace:'nowrap', transition:'all .15s' }
            },
                label, value ? h('span', { style:{ color:TC.accent, fontWeight:600 } }, ': ' + value) : null,
                h('span', { style:{ fontSize:9, marginLeft:2 } }, '▾')
            ),
            value && h('button', {
                onClick:onClear,
                style:{ position:'absolute', right:-6, top:-6, width:16, height:16, borderRadius:'50%', background:TC.error,
                        border:'none', color:'#fff', fontSize:9, cursor:'pointer', display:'flex', alignItems:'center', justifyContent:'center', lineHeight:1 }
            }, '×'),
            activeMenu===label && children
        );
    }

    /* FilterDropdown — used in ProductListPage filter bar */
    function FilterDropdown({ cardBg, borderColor, onClose, children }) {
        return h('div', {
            style:{ position:'absolute', top:'calc(100% + 6px)', left:0, zIndex:200, background:cardBg,
                    border:`1px solid ${borderColor}`, borderRadius:12, boxShadow:'0 8px 30px rgba(0,0,0,.12)',
                    minWidth:200, padding:8 },
            onMouseLeave:()=>onClose()
        }, children);
    }

    /* ── PRODUCT LIST — KPI Stat Card (Pulse UI) ──────────────────── */
    function StatCard({ title, value, trend, icon, positive, dark, cardBg, borderColor, mutedColor }) {
        var trendColor = positive ? TC.success : TC.error;
        var trendBg    = positive
            ? (dark ? 'rgba(22,163,74,.08)' : TC.successBg)
            : (dark ? 'rgba(220,38,38,.08)' : TC.errorBg);
        return h('div', { style:{ background:cardBg, borderRadius:12, border:'1px solid '+borderColor, boxShadow:cardShadow(dark), padding:S.xl, display:'flex', justifyContent:'space-between', alignItems:'center' } },
            h('div', null,
                h('div', { style:{ fontSize:13, color:mutedColor, marginBottom:S.sm } }, title),
                h('div', { style:{ fontSize:22, fontWeight:600, letterSpacing:'-.3px', marginBottom:S.sm } }, value),
                h('div', { style:{ display:'flex', alignItems:'center', gap:4, fontSize:12, fontWeight:500, color:trendColor } },
                    positive ? icons.arrowUpRight : icons.arrowDownRight,
                    h('span', null, trend)
                )
            ),
            h('div', { style:{ width:42, height:42, borderRadius:12, background:dark?'rgba(255,255,255,.05)':'#F5F5F5', display:'flex', alignItems:'center', justifyContent:'center', color:mutedColor } }, icon)
        );
    }

    /* ── PRODUCT LIST — Row Action Dropdown (Pulse UI) ──────────── */
    function RowActionMenu({ pid, onEdit, onView, onDelete, onDuplicate, onApprove, dark, cardBg, borderColor, textColor, mutedColor }) {
        var _s = useState(false), open = _s[0], setOpen = _s[1];
        var _pos = useState({ top:0, left:0, openUp:false }), pos = _pos[0], setPos = _pos[1];
        var ref = useRef(null);
        var dropRef = useRef(null);
        useEffect(function(){
            if (!open) return;
            function close(e) {
                if (ref.current && ref.current.contains(e.target)) return;
                if (dropRef.current && dropRef.current.contains(e.target)) return;
                setOpen(false);
            }
            document.addEventListener('mousedown', close);
            return function(){ document.removeEventListener('mousedown', close); };
        }, [open]);
        function toggle(e) {
            e.stopPropagation();
            if (!open && ref.current) {
                var rect = ref.current.getBoundingClientRect();
                // Approve slot adds ~32px when present; size openUp threshold accordingly.
                var menuH = (onApprove ? 200 : 160) + (onDuplicate ? 36 : 0);
                var openUp = rect.bottom + menuH > window.innerHeight;
                setPos({
                    top: openUp ? rect.top : rect.bottom + 4,
                    left: rect.right - 150,
                    openUp: openUp
                });
            }
            setOpen(!open);
        }
        var dropStyle = {
            position:'fixed', background:cardBg, border:'1px solid '+borderColor,
            borderRadius:10, boxShadow:'0 8px 24px rgba(0,0,0,.15)', minWidth:150,
            padding:S.xs, zIndex:9999, left:pos.left+'px'
        };
        if (pos.openUp) { dropStyle.bottom = (window.innerHeight - pos.top + 4) + 'px'; }
        else { dropStyle.top = pos.top + 'px'; }
        var itemSty = { display:'flex', alignItems:'center', gap:8, width:'100%', padding:'7px 12px', border:'none', background:'transparent', color:textColor, fontSize:12, cursor:'pointer', borderRadius:6, textAlign:'left' };
        return h('div', { ref:ref, style:{ position:'relative' } },
            h('button', { onClick:toggle,
                style:{ width:30, height:30, borderRadius:'50%', border:'1px solid '+borderColor, background:'transparent', display:'flex', alignItems:'center', justifyContent:'center', cursor:'pointer', color:mutedColor } },
                icons.moreVert),
            open && h('div', { ref:dropRef, style:dropStyle },
                /* Approve — only when callback supplied (B3.5e vendor-draft promotion). */
                onApprove && h('button', { onClick:function(){ setOpen(false); onApprove(); }, style:Object.assign({}, itemSty, { color:TC.success }),
                    onMouseEnter:function(e){ e.currentTarget.style.background=dark?'rgba(22,163,74,.06)':TC.successBg; },
                    onMouseLeave:function(e){ e.currentTarget.style.background='transparent'; } },
                    h('span',{style:{display:'flex'}}, icons.check), 'Approve'),
                onApprove && h('div', { style:{ height:1, background:borderColor, margin:'4px 0' } }),
                h('button', { onClick:function(){ setOpen(false); onView(); }, style:itemSty,
                    onMouseEnter:function(e){ e.currentTarget.style.background=dark?'rgba(255,255,255,.04)':'#F5F5F5'; },
                    onMouseLeave:function(e){ e.currentTarget.style.background='transparent'; } },
                    h('span',{style:{display:'flex',color:mutedColor}}, icons.eye), 'View'),
                h('button', { onClick:function(){ setOpen(false); onEdit(); }, style:itemSty,
                    onMouseEnter:function(e){ e.currentTarget.style.background=dark?'rgba(255,255,255,.04)':'#F5F5F5'; },
                    onMouseLeave:function(e){ e.currentTarget.style.background='transparent'; } },
                    h('span',{style:{display:'flex',color:mutedColor}}, icons.pencil), 'Edit'),
                onDuplicate && h('button', { onClick:function(){ setOpen(false); onDuplicate(); }, style:itemSty,
                    onMouseEnter:function(e){ e.currentTarget.style.background=dark?'rgba(255,255,255,.04)':'#F5F5F5'; },
                    onMouseLeave:function(e){ e.currentTarget.style.background='transparent'; } },
                    h('span',{style:{display:'flex',color:mutedColor}}, icons.copy), 'Duplicate'),
                h('div', { style:{ height:1, background:borderColor, margin:'4px 0' } }),
                h('button', { onClick:function(){ setOpen(false); onDelete(); }, style:Object.assign({}, itemSty, { color:TC.error }),
                    onMouseEnter:function(e){ e.currentTarget.style.background=dark?'rgba(220,38,38,.06)':TC.errorBg; },
                    onMouseLeave:function(e){ e.currentTarget.style.background='transparent'; } },
                    h('span',{style:{display:'flex'}}, icons.trash), 'Delete')
            )
        );
    }

    /* ── VENDOR SOURCE PILL — admin Products list provenance chip (S185, B3.5e) ──
     *
     * Renders a small chip below the product name showing vendor association:
     *   - Active junction:        accent chip with vendor display_name
     *   - Soft-deleted junction:  amber chip "Previously: <name> (deleted YYYY-MM-DD)"
     *   - Vendor row missing:     fallback to "vendor #<id>" (data integrity edge case)
     *
     * Receives src = { vendor_id, display_name, vendor_status, is_active, deleted_at }
     * from the ProductsController response filter shipped at S185 (marketplace
     * extension VendorSourceEnrichment). Module-scope per Pattern #151. */
    function VendorSourcePill({ src, dark, mutedColor }) {
        if (!src) return null;
        var name = src.display_name && src.display_name.length
            ? src.display_name
            : ('vendor #' + (src.vendor_id || '?'));
        if (src.is_active) {
            return h('span', {
                title: 'Vendor: ' + name + (src.vendor_status ? ' (' + src.vendor_status + ')' : ''),
                style:{
                    fontSize:10.5, fontWeight:500, padding:'2px 8px', borderRadius:99,
                    background: dark ? 'rgba(37,99,235,.12)' : TC.accentBg,
                    color: TC.accent,
                    border: '1px solid ' + TC.accent + '30',
                    display:'inline-block', whiteSpace:'nowrap', alignSelf:'flex-start'
                }
            }, name);
        }
        // Soft-deleted junction — show date if present.
        var dt = src.deleted_at && typeof src.deleted_at === 'string'
            ? src.deleted_at.slice(0, 10)  // "YYYY-MM-DD HH:MM:SS" → "YYYY-MM-DD"
            : '';
        var label = dt
            ? ('Previously: ' + name + ' (deleted ' + dt + ')')
            : ('Previously: ' + name);
        return h('span', {
            title: label,
            style:{
                fontSize:10.5, fontWeight:500, padding:'2px 8px', borderRadius:99,
                background: dark ? 'rgba(217,119,6,.10)' : TC.amberBg,
                color: TC.amber,
                border: '1px solid ' + TC.amber + '30',
                display:'inline-block', whiteSpace:'nowrap', alignSelf:'flex-start',
                overflow:'hidden', textOverflow:'ellipsis', maxWidth:'100%'
            }
        }, label);
    }

    /* ── Category Tree Picker — collapsible tree popup for filtering ── */
    function CategoryTreePicker({ cats, value, onChange, dark, cardBg, textColor, mutedColor, borderColor }) {
        var _open = useState(false), open = _open[0], setOpen = _open[1];
        var _search = useState(''), search = _search[0], setSearch = _search[1];
        var _exp = useState(new Set()), expanded = _exp[0], setExpanded = _exp[1];
        var ref = useRef(null);

        // Close on outside click
        useEffect(function(){
            if (!open) return;
            function close(e) { if (ref.current && !ref.current.contains(e.target)) setOpen(false); }
            document.addEventListener('mousedown', close);
            return function(){ document.removeEventListener('mousedown', close); };
        }, [open]);

        // Tree computation
        var filtered = search ? catFilterTree(cats, search) : cats;
        var tree = catBuildTree(filtered);
        var effectiveExp = search
            ? new Set(filtered.map(function(c){ return Number(c.id || c.category_id); }))
            : expanded;
        var rows = catFlattenVisible(tree, effectiveExp);

        function toggleExp(cid) {
            var id = Number(cid);
            setExpanded(function(prev){ var n=new Set(prev); if(n.has(id)) n.delete(id); else n.add(id); return n; });
        }
        function expandAll() { setExpanded(new Set(cats.map(function(c){ return Number(c.id||c.category_id); }))); }
        function collapseAll() { setExpanded(new Set()); }

        function pick(cid) {
            onChange(String(cid));
            setOpen(false);
            setSearch('');
        }

        // Button label
        var selectedName = 'All Categories';
        if (value) {
            var found = cats.find(function(c){ return String(c.id||c.category_id) === String(value); });
            if (found) selectedName = found.name;
        }

        // Chevrons
        var chevR = h('svg',{width:12,height:12,viewBox:'0 0 14 14',fill:'none',stroke:'currentColor',strokeWidth:'1.5',strokeLinecap:'round',strokeLinejoin:'round',style:{display:'block'}}, h('path',{d:'M5 3l4 4-4 4'}));
        var chevD = h('svg',{width:12,height:12,viewBox:'0 0 14 14',fill:'none',stroke:'currentColor',strokeWidth:'1.5',strokeLinecap:'round',strokeLinejoin:'round',style:{display:'block'}}, h('path',{d:'M3 5l4 4 4-4'}));

        var btnSty = { display:'flex', alignItems:'center', gap:6, padding:'7px 12px', borderRadius:8, border:'1px solid '+borderColor, background:dark?'#262626':'#fff', color:value?textColor:mutedColor, fontSize:13, cursor:'pointer', minWidth:160, justifyContent:'space-between' };

        return h('div', { ref:ref, style:{ position:'relative', display:'inline-block' } },
            // Trigger button
            h('button', { onClick:function(){ setOpen(!open); }, style:btnSty },
                h('span', { style:{ overflow:'hidden', textOverflow:'ellipsis', whiteSpace:'nowrap' } }, selectedName),
                h('svg',{width:10,height:10,viewBox:'0 0 10 10',fill:'none',stroke:'currentColor',strokeWidth:'1.5',style:{flexShrink:0}}, h('path',{d:'M2 3.5l3 3 3-3'}))
            ),

            // Popup
            open && h('div', { style:{
                position:'fixed', top:0, left:0, right:0, bottom:0, zIndex:9998, background:'transparent'
            }, onClick:function(){ setOpen(false); } }),
            open && h('div', { style:{
                position:'absolute', top:'calc(100% + 4px)', left:0, zIndex:9999,
                background:cardBg, border:'1px solid '+borderColor, borderRadius:12,
                boxShadow:'0 12px 36px rgba(0,0,0,.2)', width:340, maxHeight:420,
                display:'flex', flexDirection:'column', overflow:'hidden'
            }, onClick:function(e){ e.stopPropagation(); } },

                // Search + controls
                h('div', { style:{ padding:'12px 14px 8px', borderBottom:'1px solid '+borderColor } },
                    h('div', { style:{ position:'relative', marginBottom:8 } },
                        h('span', { style:{ position:'absolute', left:10, top:'50%', transform:'translateY(-50%)', color:mutedColor, display:'flex' } }, icons.search),
                        h('input', { value:search, onChange:function(e){ setSearch(e.target.value); },
                            placeholder:'Search categories...', autoFocus:true,
                            style:{ width:'100%', padding:'7px 10px 7px 32px', borderRadius:8, border:'1px solid '+borderColor, background:dark?'#262626':'#fff', color:textColor, fontSize:13, outline:'none', boxSizing:'border-box' } })
                    ),
                    !search && h('div', { style:{ display:'flex', gap:12, marginBottom:4 } },
                        h('button', { onClick:expandAll, style:{ background:'transparent', border:'none', color:TC.accent, fontSize:11, fontWeight:500, cursor:'pointer', padding:0 } }, 'Expand all'),
                        h('button', { onClick:collapseAll, style:{ background:'transparent', border:'none', color:mutedColor, fontSize:11, fontWeight:500, cursor:'pointer', padding:0 } }, 'Collapse all')
                    )
                ),

                // "All" option
                h('div', { onClick:function(){ pick(''); },
                    style:{ padding:'8px 14px', cursor:'pointer', fontSize:13, fontWeight:!value?600:400, color:!value?TC.accent:textColor,
                            background:!value?(dark?'rgba(37,99,235,.08)':'rgba(37,99,235,.04)'):'transparent',
                            borderBottom:'1px solid '+borderColor },
                    onMouseEnter:function(e){ if(value) e.currentTarget.style.background=dark?'rgba(255,255,255,.03)':'#F9FAFB'; },
                    onMouseLeave:function(e){ if(value) e.currentTarget.style.background='transparent'; }
                }, 'All Categories'),

                // Tree rows
                h('div', { style:{ overflowY:'auto', flex:1, padding:'4px 0' } },
                    rows.length === 0 && h('div', { style:{ padding:20, textAlign:'center', color:mutedColor, fontSize:12 } },
                        search ? 'No categories match "'+search+'".' : 'No categories yet.'),
                    rows.map(function(cat) {
                        var cid = Number(cat.id || cat.category_id);
                        var depth = cat._depth || 0;
                        var hasKids = cat.children && cat.children.length > 0;
                        var isExp = effectiveExp.has(cid);
                        var isActive = String(cid) === String(value);

                        return h('div', { key:cid,
                            style:{ display:'flex', alignItems:'center', padding:'6px 14px', paddingLeft:(14+depth*20)+'px',
                                    cursor:'pointer', fontSize:13,
                                    background:isActive?(dark?'rgba(37,99,235,.08)':'rgba(37,99,235,.04)'):'transparent',
                                    color:isActive?TC.accent:textColor, fontWeight:isActive?600:400 },
                            onMouseEnter:function(e){ if(!isActive) e.currentTarget.style.background=dark?'rgba(255,255,255,.03)':'#F9FAFB'; },
                            onMouseLeave:function(e){ if(!isActive) e.currentTarget.style.background=isActive?(dark?'rgba(37,99,235,.08)':'rgba(37,99,235,.04)'):'transparent'; }
                        },
                            // Chevron toggle
                            hasKids
                                ? h('button', { onClick:function(e){ e.stopPropagation(); toggleExp(cid); },
                                    style:{ width:20, height:20, borderRadius:4, border:'none', background:'transparent', display:'flex', alignItems:'center', justifyContent:'center', cursor:'pointer', color:mutedColor, flexShrink:0, marginRight:4 } },
                                    isExp ? chevD : chevR)
                                : h('span', { style:{ width:24, flexShrink:0 } }),
                            // Name — click to select
                            h('span', { onClick:function(){ pick(cid); }, style:{ flex:1 } }, cat.name || '—'),
                            // Product count
                            h('span', { style:{ fontSize:11, color:mutedColor, marginLeft:8 } }, String(cat.product_count||0))
                        );
                    })
                )
            )
        );
    }

    /* ── CategoryTreeMultiPicker — multi-select dropdown variant of CategoryTreePicker ──
       Used in the bulk editor (Apply-to-All + per-product cards) so the categories field
       collapses into a button instead of taking up vertical space as an inline checkbox list.
       Popup uses position:fixed so it escapes any ancestor with `overflow:hidden` (which the
       Apply-to-All wrapper and per-product cards use for rounded-corner clipping). */
    function CategoryTreeMultiPicker({ cats, value, onChange, dark, cardBg, textColor, mutedColor, borderColor, buttonWidth }) {
        var _open = useState(false), open = _open[0], setOpen = _open[1];
        var _search = useState(''), search = _search[0], setSearch = _search[1];
        var _exp = useState(new Set()), expanded = _exp[0], setExpanded = _exp[1];
        var _pos = useState(null), popupPos = _pos[0], setPopupPos = _pos[1];
        var btnRef = useRef(null);
        var popupRef = useRef(null);

        var curVal = Array.isArray(value) ? value.map(Number).filter(function(n){ return !isNaN(n); }) : [];
        var curSet = new Set(curVal);

        // Compute viewport-relative popup position. Flips above when not enough space below;
        // clamps horizontally so popup doesn't spill off the viewport edges.
        function computePos() {
            if (!btnRef.current) return null;
            var rect = btnRef.current.getBoundingClientRect();
            var popupW = 300, popupH = 380;
            var vw = window.innerWidth, vh = window.innerHeight;
            var left = Math.min(rect.left, vw - popupW - 8);
            left = Math.max(8, left);
            var spaceBelow = vh - rect.bottom - 8;
            var spaceAbove = rect.top - 8;
            var showAbove = spaceBelow < popupH && spaceAbove > spaceBelow;
            var top = showAbove ? Math.max(8, rect.top - popupH - 4) : rect.bottom + 4;
            return { top: top, left: left, width: popupW };
        }

        function openPopup() {
            setPopupPos(computePos());
            setOpen(true);
        }

        // Close on outside click, close on scroll (standard dropdown UX), reposition on resize.
        useEffect(function() {
            if (!open) return;
            function outsideClick(e) {
                if (btnRef.current && btnRef.current.contains(e.target)) return;
                if (popupRef.current && popupRef.current.contains(e.target)) return;
                setOpen(false);
            }
            function closeOnScroll(e) {
                // Ignore scrolls that originate INSIDE the popup — otherwise scrolling the
                // category tree (which is an inner overflowY:auto container) would close the
                // popup immediately on first wheel tick. Only close when an ancestor page
                // scroll moves the anchor button under the popup.
                try {
                    if (popupRef.current && e.target && popupRef.current.contains(e.target)) return;
                } catch (err) { /* e.target may be Document; contains() is safe but guard anyway */ }
                setOpen(false);
            }
            function reposOnResize() { setPopupPos(computePos()); }
            document.addEventListener('mousedown', outsideClick);
            window.addEventListener('scroll', closeOnScroll, true); // capture:true so ancestor scrolls also trigger
            window.addEventListener('resize', reposOnResize);
            return function() {
                document.removeEventListener('mousedown', outsideClick);
                window.removeEventListener('scroll', closeOnScroll, true);
                window.removeEventListener('resize', reposOnResize);
            };
        }, [open]);

        var filtered = search ? catFilterTree(cats, search) : cats;
        var tree = catBuildTree(filtered);
        var effectiveExp = search
            ? new Set(filtered.map(function(c){ return Number(c.id || c.category_id); }))
            : expanded;
        var rows = catFlattenVisible(tree, effectiveExp);

        function toggleExp(cid) {
            var id = Number(cid);
            setExpanded(function(prev){ var n=new Set(prev); if(n.has(id)) n.delete(id); else n.add(id); return n; });
        }
        function expandAll() { setExpanded(new Set(cats.map(function(c){ return Number(c.id||c.category_id); }))); }
        function collapseAll() { setExpanded(new Set()); }

        function toggle(cid) {
            var id = Number(cid);
            var next = curSet.has(id) ? curVal.filter(function(x){ return x !== id; }) : curVal.concat(id);
            onChange(next);
        }
        function clearAll() { onChange([]); }

        // Button label
        var btnLabel = '';
        if (curVal.length === 0) {
            btnLabel = 'No categories';
        } else if (curVal.length === 1) {
            var found = cats.find(function(c){ return Number(c.id||c.category_id) === curVal[0]; });
            btnLabel = found ? found.name : '1 category';
        } else if (curVal.length === 2) {
            var names = curVal.map(function(cid){
                var f = cats.find(function(c){ return Number(c.id||c.category_id) === cid; });
                return f ? f.name : '#'+cid;
            });
            btnLabel = names.join(', ');
        } else {
            btnLabel = curVal.length + ' categories';
        }

        var chevR = h('svg',{width:12,height:12,viewBox:'0 0 14 14',fill:'none',stroke:'currentColor',strokeWidth:'1.5',strokeLinecap:'round',strokeLinejoin:'round',style:{display:'block'}}, h('path',{d:'M5 3l4 4-4 4'}));
        var chevD = h('svg',{width:12,height:12,viewBox:'0 0 14 14',fill:'none',stroke:'currentColor',strokeWidth:'1.5',strokeLinecap:'round',strokeLinejoin:'round',style:{display:'block'}}, h('path',{d:'M3 5l4 4 4-4'}));

        // Match native <input> height: explicit height + box-sizing + line-height:1 prevents the
        // default button line-height (`normal` ≈ 1.2–1.4) from making the trigger taller than inputs.
        var btnSty = {
            display:'flex', alignItems:'center', gap:6,
            padding:'0 10px',
            height: 30, boxSizing:'border-box',
            borderRadius:6, border:'1px solid '+borderColor,
            background:dark?'#262626':'#fff',
            color:curVal.length?textColor:mutedColor,
            fontSize:12, lineHeight:'1', fontFamily:'inherit',
            cursor:'pointer',
            width:buttonWidth||'100%',
            justifyContent:'space-between',
            textAlign:'left'
        };

        return h('div', { style:{ position:'relative' } },
            // Trigger button
            h('button', {
                ref: btnRef,
                type:'button',
                onClick:function(e){ e.stopPropagation(); if (open) setOpen(false); else openPopup(); },
                style:btnSty
            },
                h('span', { style:{ overflow:'hidden', textOverflow:'ellipsis', whiteSpace:'nowrap', flex:1, minWidth:0 } }, btnLabel),
                h('svg',{width:10,height:10,viewBox:'0 0 10 10',fill:'none',stroke:'currentColor',strokeWidth:'1.5',style:{flexShrink:0, transform:open?'rotate(180deg)':'rotate(0)', transition:'transform .12s'}}, h('path',{d:'M2 3.5l3 3 3-3'}))
            ),

            // Popup — position:fixed so it escapes any ancestor overflow:hidden
            open && popupPos && h('div', { ref:popupRef, style:{
                position:'fixed',
                top: popupPos.top,
                left: popupPos.left,
                width: popupPos.width,
                maxHeight: 380,
                zIndex: 9999,
                background:cardBg, border:'1px solid '+borderColor, borderRadius:10,
                boxShadow:'0 12px 36px rgba(0,0,0,.2)',
                display:'flex', flexDirection:'column', overflow:'hidden'
            }, onClick:function(e){ e.stopPropagation(); } },

                // Search + controls
                h('div', { style:{ padding:'10px 12px 6px', borderBottom:'1px solid '+borderColor } },
                    h('div', { style:{ position:'relative', marginBottom:6 } },
                        h('span', { style:{ position:'absolute', left:9, top:'50%', transform:'translateY(-50%)', color:mutedColor, display:'flex' } }, icons.search),
                        h('input', { value:search, onChange:function(e){ setSearch(e.target.value); },
                            placeholder:'Search categories...', autoFocus:true,
                            style:{ width:'100%', padding:'6px 10px 6px 30px', borderRadius:6, border:'1px solid '+borderColor, background:dark?'#262626':'#fff', color:textColor, fontSize:12, outline:'none', boxSizing:'border-box' } })
                    ),
                    h('div', { style:{ display:'flex', gap:12, justifyContent:'space-between', alignItems:'center', minHeight:18 } },
                        h('div', { style:{ display:'flex', gap:10 } },
                            !search && h('button', { type:'button', onClick:expandAll, style:{ background:'transparent', border:'none', color:TC.accent, fontSize:11, fontWeight:500, cursor:'pointer', padding:0 } }, 'Expand all'),
                            !search && h('button', { type:'button', onClick:collapseAll, style:{ background:'transparent', border:'none', color:mutedColor, fontSize:11, fontWeight:500, cursor:'pointer', padding:0 } }, 'Collapse all')
                        ),
                        curVal.length > 0 && h('button', { type:'button', onClick:clearAll, style:{ background:'transparent', border:'none', color:TC.error, fontSize:11, fontWeight:500, cursor:'pointer', padding:0 } }, 'Clear (' + curVal.length + ')')
                    )
                ),

                // Tree rows with checkboxes
                h('div', { style:{ overflowY:'auto', flex:1, padding:'4px 0' } },
                    rows.length === 0 && h('div', { style:{ padding:20, textAlign:'center', color:mutedColor, fontSize:12 } },
                        search ? 'No categories match "'+search+'".' : 'No categories yet.'),
                    rows.map(function(cat) {
                        var cid = Number(cat.id || cat.category_id);
                        var depth = cat._depth || 0;
                        var hasKids = cat.children && cat.children.length > 0;
                        var isExp = effectiveExp.has(cid);
                        var checked = curSet.has(cid);

                        return h('div', { key:cid,
                            style:{ display:'flex', alignItems:'center', padding:'4px 12px', paddingLeft:(12+depth*18)+'px',
                                    fontSize:12,
                                    background:checked?(dark?'rgba(37,99,235,.06)':'rgba(37,99,235,.04)'):'transparent' }
                        },
                            // Chevron toggle (no-op span if leaf)
                            hasKids
                                ? h('button', { type:'button', onClick:function(e){ e.stopPropagation(); toggleExp(cid); },
                                    style:{ width:18, height:18, borderRadius:4, border:'none', background:'transparent', display:'flex', alignItems:'center', justifyContent:'center', cursor:'pointer', color:mutedColor, flexShrink:0, marginRight:2, padding:0 } },
                                    isExp ? chevD : chevR)
                                : h('span', { style:{ width:20, flexShrink:0 } }),
                            // Checkbox + name (clickable label)
                            h('label', { style:{ display:'flex', alignItems:'center', gap:6, cursor:'pointer', flex:1, minWidth:0 } },
                                h('input', { type:'checkbox', checked:checked, onChange:function(){ toggle(cid); },
                                    style:{ cursor:'pointer', accentColor:TC.accent, flexShrink:0 } }),
                                h('span', { style:{ overflow:'hidden', textOverflow:'ellipsis', whiteSpace:'nowrap', color:textColor } }, cat.name || '—')
                            )
                        );
                    })
                )
            )
        );
    }

    /* ── PRODUCT LIST PAGE (Pulse UI) ─────────────────────────────── */
    function ProductListPage({ dark, cardBg, textColor, mutedColor, borderColor, tableHeaderBg }) {
        var PAGE_SIZE = TC_PER_PAGE;
        var _pg = useState(1),       page = _pg[0],       setPage = _pg[1];
        var _sr = useState(''),      search = _sr[0],     setSearch = _sr[1];
        var _it = useState([]),      items = _it[0],      setItems = _it[1];
        var _to = useState(0),       total = _to[0],      setTotal = _to[1];
        var _tp = useState(1),       totalPages = _tp[0], setTotalPages = _tp[1];
        var _ld = useState(true),    loading = _ld[0],    setLoading = _ld[1];
        var _sl = useState(new Set()),selected=_sl[0],    setSelected = _sl[1];

        // Filters (Pulse-style dropdowns)
        var _fs = useState(''),      fStatus = _fs[0],    setFStatus = _fs[1];
        var __navCat = window.__tc_nav_cat ? String(window.__tc_nav_cat) : '';
        if (window.__tc_nav_cat) delete window.__tc_nav_cat;
        var _fc = useState(__navCat),    fCat = _fc[0],       setFCat = _fc[1];
        var _so = useState('date_desc'), fSort = _so[0],  setFSort = _so[1];

        // ── Bulk edit state ──
        var _bm = useState(false),    bulkMode = _bm[0],    setBulkMode = _bm[1];
        var _bf = useState([]),       bulkFields = _bf[0],   setBulkFields = _bf[1];
        var _fm = useState(false),    showFieldModal = _fm[0], setShowFieldModal = _fm[1];
        var _be = useState({}),       bulkEdits = _be[0],    setBulkEdits = _be[1];
        var _aa = useState({}),       applyAll = _aa[0],     setApplyAll = _aa[1];
        var _bs = useState(false),    bulkSaving = _bs[0],   setBulkSaving = _bs[1];
        var _bl = useState(false),    bulkLoading = _bl[0],  setBulkLoading = _bl[1];
        var _bmsg = useState(''),     bulkMsg = _bmsg[0],    setBulkMsg = _bmsg[1];
        var _bprod = useState({}),    bulkProductData = _bprod[0], setBulkProductData = _bprod[1]; // pid → full product
        // Session 98 polish state
        var _aaO = useState(true),    applyAllOpen = _aaO[0], setApplyAllOpen = _aaO[1]; // collapsible Apply-to-All panel
        var _bSnap = useState(''),    bulkSnapshot = _bSnap[0], setBulkSnapshot = _bSnap[1]; // dirty-detection baseline (JSON of bulkEdits)
        // Session 98 follow-up: synced horizontal scroll across per-product cards.
        // Map of pid (string) → DOM node of that card's scrollable field strip.
        // When user scrolls one strip, syncCardScroll mirrors scrollLeft to all others.
        var cardScrollRefs = useRef({});
        // User-facing toggle: when false, each card scrolls independently (no mirroring).
        // Default true because linked scroll restores the column-scanning benefit of the old table layout.
        var _ss = useState(true), scrollSyncEnabled = _ss[0], setScrollSyncEnabled = _ss[1];
        function setCardScrollRef(pid) {
            return function(el) {
                if (el) cardScrollRefs.current[String(pid)] = el;
                else delete cardScrollRefs.current[String(pid)];
            };
        }
        function syncCardScroll(sourcePid) {
            return function(e) {
                // Respect the toggle — bail out immediately when the user wants per-card independent scroll
                if (!scrollSyncEnabled) return;
                var sl = e.target.scrollLeft;
                var srcKey = String(sourcePid);
                Object.keys(cardScrollRefs.current).forEach(function(pid) {
                    if (pid === srcKey) return;
                    var el = cardScrollRefs.current[pid];
                    // Idempotent guard: only write if value differs, otherwise the assignment fires
                    // a scroll event that re-enters this handler and loops infinitely.
                    if (el && el.scrollLeft !== sl) el.scrollLeft = sl;
                });
            };
        }

        // Categories for filter dropdown
        var _cats = useState([]), cats = _cats[0], setCats = _cats[1];
        useEffect(function(){
            apiFetch('/categories?format=flat&per_page=200').then(function(d){
                setCats(Array.isArray(d) ? d : (d.categories||[]));
            }).catch(function(){});
        }, []);

        // KPI stats
        var _stats = useState({ total:0, inStock:0, lowStock:0, inactive:0 }), stats = _stats[0], setStats = _stats[1];

        function load() {
            setLoading(true);
            var baseUrl = Config.apiUrl || '/wp-json/titancart/v1';
            var q = new URLSearchParams({
                page: page,
                per_page: PAGE_SIZE,
                search: search,
                status: fStatus,
                category_id: fCat,
                sort_by:    fSort.slice(0, fSort.lastIndexOf('_')),
                sort_order: fSort.slice(fSort.lastIndexOf('_') + 1),
            });
            var url = baseUrl + '/products' + (baseUrl.indexOf('?') !== -1 ? '&' : '?') + q;
            fetch(url, { headers:{ 'X-WP-Nonce': Config.nonce || '' } })
            .then(function(r) {
                var hTotal = parseInt(r.headers.get('X-TC-Total') || '0', 10);
                var hPages = parseInt(r.headers.get('X-TC-Total-Pages') || '1', 10);
                r.json().then(function(d) {
                    var rows = Array.isArray(d) ? d : (d.products || []);
                    var tot  = hTotal || d.total || rows.length;
                    var pgs  = hPages || d.total_pages || 1;
                    setItems(rows);
                    setTotal(tot);
                    setTotalPages(pgs);
                    setLoading(false);
                    // Compute quick stats from the total + current page data
                    var inSt = rows.filter(function(p){ return (p.stock_quantity||0) > 5 || p.track_inventory===0; }).length;
                    var lowSt = rows.filter(function(p){ return p.track_inventory!==0 && (p.stock_quantity||0) > 0 && (p.stock_quantity||0) <= 5; }).length;
                    var inact = rows.filter(function(p){ return p.status==='disabled' || p.status==='D'; }).length;
                    setStats({ total:tot, inStock:inSt, lowStock:lowSt, inactive:inact });
                });
            }).catch(function(err){
                console.error('[TitanCart] ProductListPage fetch error:', err);
                setLoading(false);
            });
        }
        useEffect(load, [page, search, fStatus, fCat, fSort]);

        // Selection
        function toggleAll(e) {
            if (e.target.checked) { setSelected(new Set(items.map(function(p){ return p.product_id||p.id; }))); }
            else { setSelected(new Set()); }
        }
        function toggleOne(pid) {
            setSelected(function(prev) {
                var next = new Set(prev);
                if (next.has(pid)) next.delete(pid); else next.add(pid);
                return next;
            });
        }
        var allChecked = items.length > 0 && items.every(function(p){ return selected.has(p.product_id||p.id); });

        // S183 — Pulse modal cascade from VendorProductsPage.
        // confirmDelete payload shape:
        //   null                                  — modal closed
        //   { kind: 'single', product: p }        — single-row Delete from RowActionMenu
        //   { kind: 'bulk',   count:  N }         — "Delete" button in bulk-selection bar
        var _cd = useState(null),    confirmDelete = _cd[0],    setConfirmDelete = _cd[1];
        var _dg = useState(false),   deleting = _dg[0],         setDeleting = _dg[1];
        var _eb = useState(null),    errorBanner = _eb[0],      setErrorBanner = _eb[1];

        // S185 — Approve action for vendor-created drafts (B3.5e).
        // confirmApprove payload shape:
        //   null                          — modal closed
        //   { product: p }                — single-row Approve from RowActionMenu
        // No bulk Approve in S185 — keep the surface small until usage patterns
        // show whether admins want batched approvals.
        var _ca = useState(null),    confirmApprove = _ca[0],   setConfirmApprove = _ca[1];
        var _ag = useState(false),   approving = _ag[0],        setApproving = _ag[1];

        // S183 — single/bulk delete cascade. del() and delSelected() are kept as
        // thin queue-the-modal triggers; the actual REST DELETE fires from
        // confirmDeleteAction below when the user clicks the modal's confirm.
        // Unlike vendor delete (soft junction + product status flip), admin
        // delete soft-deletes the product row itself via /products/{id} DELETE.
        function del(p) {
            setErrorBanner(null);
            setConfirmDelete({ kind:'single', product:p });
        }
        function delSelected() {
            if (!selected.size) return;
            setErrorBanner(null);
            setConfirmDelete({ kind:'bulk', count:selected.size });
        }
        function cancelDelete() {
            if (deleting) return;
            setConfirmDelete(null);
        }
        function confirmDeleteAction() {
            if (!confirmDelete || deleting) return;
            setDeleting(true);
            var done = function(){
                setConfirmDelete(null);
                setDeleting(false);
            };
            var fail = function(err) {
                console.error('[TitanCart] Admin product delete failed:', err);
                var msg = (err && err.message) ? err.message : 'unknown error';
                setConfirmDelete(null);
                setDeleting(false);
                setErrorBanner('Delete failed: ' + msg);
            };
            if (confirmDelete.kind === 'single') {
                var p = confirmDelete.product;
                apiFetch('/products/' + (p.product_id || p.id), { method:'DELETE' })
                    .then(function(){ done(); load(); })
                    .catch(fail);
            } else {
                // Bulk: Promise.all rejects on first failure. Partial-success
                // diagnostics are a future polish — for now any failure surfaces
                // as "Delete failed: ..." in the banner and the user can re-list
                // to see what survived. The list reload runs unconditionally so
                // the table reflects the actual post-delete state either way.
                Promise.all(Array.from(selected).map(function(id){
                    return apiFetch('/products/' + id, { method:'DELETE' });
                })).then(function(){
                    setSelected(new Set());
                    done();
                    load();
                }).catch(function(err){
                    fail(err);
                    load(); // best-effort refresh even on partial fail
                });
            }
        }
        // S185 — Approve action handlers (B3.5e). Promotes a vendor-created
        // draft (status='hidden' AND active vendor junction) to status='active'
        // via PUT /products/{id}. Reuses ConfirmModal in tone='positive' so the
        // affordance reads correctly (green check vs the destructive red triangle).
        function approve(p) {
            setErrorBanner(null);
            setConfirmApprove({ product: p });
        }
        function cancelApprove() {
            if (approving) return;
            setConfirmApprove(null);
        }
        function confirmApproveAction() {
            if (!confirmApprove || approving) return;
            setApproving(true);
            var p   = confirmApprove.product;
            var pid = p.product_id || p.id;
            var done = function(){
                setConfirmApprove(null);
                setApproving(false);
            };
            var fail = function(err) {
                console.error('[TitanCart] Admin product approve failed:', err);
                var msg = (err && err.message) ? err.message : 'unknown error';
                setConfirmApprove(null);
                setApproving(false);
                setErrorBanner('Approve failed: ' + msg);
            };
            apiFetch('/products/' + pid, {
                method: 'PUT',
                body: { status: 'active' }
            })
                .then(function(){ done(); load(); })
                .catch(fail);
        }
        function exportCSV() {
            var rows = items.filter(function(p){ return selected.has(p.product_id||p.id); });
            if (!rows.length) rows = items;
            var hdr = ['Name','Product Code','Price','Stock','Status'];
            var csv = [hdr.join(',')].concat(rows.map(function(r){
                return ['"'+(r.name||'')+'"', r.sku||'', r.price||'', r.stock_quantity!=null?r.stock_quantity:'', r.status||''].join(',');
            })).join('\n');
            var blob = new Blob([csv], { type:'text/csv' });
            var a = document.createElement('a'); a.href = URL.createObjectURL(blob); a.download = 'products.csv'; a.click();
        }

        // Stock label helper
        function stockLabel(p) {
            if (p.track_inventory === 0) return { text:'Unlimited', cls:'muted' };
            var q = p.stock_quantity || 0;
            if (q <= 0) return { text:'Out of Stock', cls:'error' };
            if (q <= 5) return { text:'Low Stock (' + q + ')', cls:'amber' };
            return { text:'In Stock (' + q + ')', cls:'success' };
        }
        function stockColor(cls) {
            if (cls === 'error')   return TC.error;
            if (cls === 'amber')   return TC.amber;
            if (cls === 'success') return TC.success;
            return mutedColor;
        }

        // Status badge style (Pulse UI: colored outline badges)
        function statusBadge(status) {
            var s = (status||'').toLowerCase();
            if (s === 'active')   return { bg: dark?'rgba(22,163,74,.1)':'#F0FDF4',  color: TC.success, border: TC.success+'30', label:'Published' };
            if (s === 'hidden')   return { bg: dark?'rgba(217,119,6,.1)':'#FFFBEB',   color: TC.amber,   border: TC.amber+'30',   label:'Draft' };
            return                       { bg: dark?'rgba(220,38,38,.1)':'#FEF2F2',   color: TC.error,   border: TC.error+'30',   label:'Inactive' };
        }

        // Dropdown select style
        var selSty = { padding:'7px 12px', borderRadius:8, border:'1px solid '+borderColor, background:dark?'#262626':'#fff', color:textColor, fontSize:13, outline:'none', cursor:'pointer', minWidth:140, appearance:'auto', colorScheme:dark?'dark':'normal' };

        // ── Bulk Edit helpers ────────────────────────────────────────────────
        var _ba = useState([]),       bulkAttrs = _ba[0],    setBulkAttrs = _ba[1];
        var _baLoading = useState(false), bulkAttrsLoading = _baLoading[0], setBulkAttrsLoading = _baLoading[1];

        var BULK_FIELD_GROUPS = [
            { title:'Pricing & Stock', fields:[
                { key:'price', label:'Price', type:'number' },
                { key:'list_price', label:'List Price', type:'number' },
                { key:'cost_price', label:'Cost Price', type:'number' },
                { key:'stock_quantity', label:'Quantity', type:'int' },
                { key:'min_quantity', label:'Min Qty', type:'int' },
                { key:'max_quantity', label:'Max Qty', type:'int' },
                { key:'qty_step', label:'Qty Step', type:'int' },
            ]},
            { title:'Status', fields:[
                { key:'status', label:'Status', type:'select', opts:[['active','Published'],['hidden','Draft'],['disabled','Inactive']] },
                { key:'featured', label:'Featured', type:'bool' },
            ]},
            { title:'Shipping', fields:[
                { key:'weight', label:'Weight', type:'number' },
                { key:'free_shipping', label:'Free Shipping', type:'bool' },
                { key:'shipping_freight', label:'Shipping Surcharge', type:'number' },
                { key:'requires_shipping', label:'Requires Shipping', type:'bool' },
            ]},
            { title:'SEO', fields:[
                { key:'translation', subkey:'meta_title', label:'Meta Title', type:'text' },
                { key:'translation', subkey:'meta_desc', label:'Meta Description', type:'text' },
            ]},
            { title:'Other', fields:[
                { key:'categories', label:'Categories', type:'categories' },
                { key:'position', label:'Position', type:'int' },
                { key:'track_inventory', label:'Track Inventory', type:'bool' },
                { key:'allow_backorders', label:'Allow Backorders', type:'bool' },
            ]},
        ];

        // Dynamic attributes group — built from fetched data
        function getAttrFieldGroups() {
            if (!bulkAttrs.length) return [];
            return [{ title:'Product Attributes', fields: bulkAttrs.map(function(a) {
                var hasChoices = a.choices && a.choices.length > 0;
                return {
                    key:'attr', subkey:String(a.id), label:a.name||('Attr #'+a.id),
                    type: hasChoices ? 'attr_select' : 'text',
                    choices: a.choices || [],
                    feature_type: a.feature_type,
                };
            })}];
        }

        function getAllFieldGroups() { return BULK_FIELD_GROUPS.concat(getAttrFieldGroups()); }

        var ALL_BULK_FIELDS = [];
        BULK_FIELD_GROUPS.forEach(function(g){ g.fields.forEach(function(f){ ALL_BULK_FIELDS.push(f); }); });

        // Refresh all fields including dynamic attributes
        function getAllBulkFields() {
            var all = [];
            getAllFieldGroups().forEach(function(g){ g.fields.forEach(function(f){ all.push(f); }); });
            return all;
        }

        function fieldKey(f) { return f.subkey ? f.key+'.'+f.subkey : f.key; }

        // Fetch attributes when field modal opens
        function openFieldModal() {
            setShowFieldModal(true);
            if (bulkAttrs.length === 0 && !bulkAttrsLoading) {
                setBulkAttrsLoading(true);
                apiFetch('/product-features').then(function(feats) {
                    var nonGroup = (Array.isArray(feats) ? feats : []).filter(function(f){ return f.feature_type !== 'G' && f.status === 'active'; });
                    // Fetch variants for features that have them
                    var withVariants = nonGroup.filter(function(f){ return f.variant_count > 0; });
                    if (withVariants.length === 0) {
                        nonGroup.forEach(function(f){ f.choices = []; });
                        setBulkAttrs(nonGroup);
                        setBulkAttrsLoading(false);
                        return;
                    }
                    Promise.all(withVariants.map(function(f){
                        return apiFetch('/product-features/'+f.id+'/variants').then(function(vars){
                            f.choices = (Array.isArray(vars) ? vars : []).map(function(v){ return { id:v.id||v.variant_id, name:v.name }; });
                        }).catch(function(){ f.choices = []; });
                    })).then(function(){
                        nonGroup.forEach(function(f){ if (!f.choices) f.choices = []; });
                        setBulkAttrs(nonGroup);
                        setBulkAttrsLoading(false);
                    });
                }).catch(function(){
                    setBulkAttrsLoading(false);
                });
            }
        }

        function toggleFieldSelection(fld) {
            var k = fieldKey(fld);
            setBulkFields(function(prev) {
                return prev.indexOf(k) >= 0 ? prev.filter(function(x){ return x!==k; }) : prev.concat(k);
            });
        }

        function openBulkEditor() {
            if (bulkFields.length === 0) return;
            setShowFieldModal(false);
            setBulkMode(true);
            setBulkLoading(true);
            setBulkMsg('');
            setApplyAll({});

            var pids = [];
            items.forEach(function(p) {
                var pid = p.product_id || p.id;
                if (selected.has(pid)) pids.push(pid);
            });

            // Fetch full product data + attributes for each selected product
            var hasCategories = bulkFields.indexOf('categories') >= 0;
            var attrFields = bulkFields.filter(function(k){ return k.indexOf('attr.') === 0; });

            Promise.all(pids.map(function(pid) {
                var fetches = [apiFetch('/products/' + pid)];
                if (attrFields.length > 0) {
                    fetches.push(apiFetch('/products/' + pid + '/attributes').catch(function(){ return []; }));
                }
                return Promise.all(fetches).then(function(results) {
                    return { pid: pid, product: results[0], attrs: results[1] || [] };
                });
            })).then(function(allData) {
                populateBulkEdits(allData, pids);
                setBulkLoading(false);
            }).catch(function(err) {
                console.error('[TitanCart] Bulk fetch error:', err);
                // Fall back to list data
                var fallback = pids.map(function(pid) {
                    var p = items.find(function(x){ return (x.product_id||x.id) === pid; }) || {};
                    return { pid: pid, product: p, attrs: [] };
                });
                populateBulkEdits(fallback, pids);
                setBulkLoading(false);
            });
        }

        function populateBulkEdits(allData, pids) {
            // Pre-resolve field defs so we can normalize values per type (avoids .0000 trailing zeros on numbers)
            var fieldDefs = {};
            bulkFields.forEach(function(k){
                var fld = getAllBulkFields().find(function(x){ return fieldKey(x)===k; });
                if (fld) fieldDefs[k] = fld;
            });

            var edits = {};
            var prodMap = {};
            allData.forEach(function(d) {
                var p = d.product;
                var pid = d.pid;
                prodMap[pid] = p;
                var row = {};
                bulkFields.forEach(function(k) {
                    var parts = k.split('.');
                    var fld = fieldDefs[k];
                    if (parts[0] === 'translation') {
                        row[k] = p[parts[1]] || p['meta_' + parts[1]] || (p.translation ? p.translation[parts[1]] : '') || '';
                    } else if (k === 'categories') {
                        // Full product should have categories array
                        var catArr = p.categories || p.category_ids || [];
                        row[k] = catArr.map(function(c){ return typeof c === 'object' ? (c.category_id || c.id) : c; }).map(Number).filter(Boolean);
                    } else if (parts[0] === 'attr') {
                        var featureId = parseInt(parts[1]);
                        var attrVal = '';
                        if (d.attrs && Array.isArray(d.attrs)) {
                            var match = d.attrs.find(function(a){ return parseInt(a.id || a.feature_id) === featureId; });
                            if (match && match.value) {
                                // The value object has variant_id, value_str, etc.
                                attrVal = match.value.variant_id ? String(match.value.variant_id) : (match.value.value_str || match.value.value_text || match.value.value_int || match.value.value_float || '');
                            }
                        }
                        row[k] = attrVal;
                    } else {
                        var raw = p[k];
                        if (raw == null || raw === '') {
                            row[k] = '';
                        } else if (fld && fld.type === 'number') {
                            // MySQL DECIMAL(.,4) returns strings like "49.0000" → parseFloat strips trailing zeros → "49"
                            // "49.5000" → "49.5", "49.99" → "49.99". Clean editable display.
                            var n = parseFloat(raw);
                            row[k] = isNaN(n) ? raw : String(n);
                        } else if (fld && fld.type === 'int') {
                            var i = parseInt(raw, 10);
                            row[k] = isNaN(i) ? '' : i;
                        } else {
                            row[k] = raw;
                        }
                    }
                });
                edits[pid] = row;
            });
            setBulkEdits(edits);
            setBulkProductData(prodMap);
            setBulkSnapshot(JSON.stringify(edits)); // baseline for dirty detection
        }

        function updateBulkEdit(pid, key, val) {
            setBulkEdits(function(prev) {
                var copy = Object.assign({}, prev);
                copy[pid] = Object.assign({}, copy[pid] || {});
                copy[pid][key] = val;
                return copy;
            });
        }

        function applyToAll() {
            setBulkEdits(function(prev) {
                var copy = Object.assign({}, prev);
                Object.keys(copy).forEach(function(pid) {
                    copy[pid] = Object.assign({}, copy[pid]);
                    Object.keys(applyAll).forEach(function(k) {
                        var v = applyAll[k];
                        if (v !== '' && v != null) {
                            copy[pid][k] = Array.isArray(v) ? v.slice() : v;
                        }
                    });
                });
                return copy;
            });
        }

        function saveBulk() {
            setBulkSaving(true);
            setBulkMsg('');
            // Build payload
            var payload = { products:{}, lang:'en' };
            Object.keys(bulkEdits).forEach(function(pid) {
                var row = bulkEdits[pid];
                var data = {};
                var attrs = [];
                Object.keys(row).forEach(function(k) {
                    var parts = k.split('.');
                    if (parts[0] === 'translation') {
                        if (!data.translation) data.translation = {};
                        data.translation[parts[1]] = row[k];
                    } else if (parts[0] === 'attr') {
                        var fid = parseInt(parts[1]);
                        var val = row[k];
                        if (val !== '' && val != null) {
                            var attrDef = bulkAttrs.find(function(a){ return a.id == fid; });
                            var hasChoices = attrDef && attrDef.choices && attrDef.choices.length > 0;
                            if (hasChoices) {
                                attrs.push({ feature_id: fid, variant_id: parseInt(val) || 0 });
                            } else {
                                attrs.push({ feature_id: fid, value_str: String(val) });
                            }
                        }
                    } else if (k === 'categories') {
                        data[k] = Array.isArray(row[k]) ? row[k].map(function(c){ return parseInt(c); }).filter(Boolean) : [];
                    } else {
                        data[k] = row[k];
                    }
                });
                if (attrs.length > 0) data.attributes = attrs;
                payload.products[pid] = data;
            });
            apiFetch('/products/bulk', { method:'PUT', body:JSON.stringify(payload), headers:{ 'Content-Type':'application/json' } })
            .then(function(res) {
                setBulkSaving(false);
                var msg = 'Saved ' + (res.updated||0) + ' product(s)';
                if (res.errors && res.errors.length) msg += '. ' + res.errors.length + ' error(s).';
                setBulkMsg(msg);
                // Reset dirty baseline immediately so the back-button prompt doesn't fire
                // during the async refresh window. refreshBulkData() will reset again with
                // server-fresh data when its promise resolves.
                setBulkSnapshot(JSON.stringify(bulkEdits));
                // Re-fetch current data so editor reflects saved state
                refreshBulkData();
                // Also refresh the background list
                load();
            }).catch(function(err) {
                setBulkSaving(false);
                setBulkMsg('Error: ' + (err.message||err));
            });
        }

        // Re-fetch product data + attributes for all products currently in the editor
        function refreshBulkData() {
            var pids = Object.keys(bulkEdits).map(Number);
            var attrFields = bulkFields.filter(function(k){ return k.indexOf('attr.') === 0; });
            Promise.all(pids.map(function(pid) {
                var fetches = [apiFetch('/products/' + pid)];
                if (attrFields.length > 0) {
                    fetches.push(apiFetch('/products/' + pid + '/attributes').catch(function(){ return []; }));
                }
                return Promise.all(fetches).then(function(results) {
                    return { pid: pid, product: results[0], attrs: results[1] || [] };
                });
            })).then(function(allData) {
                populateBulkEdits(allData, pids);
            }).catch(function(){});
        }

        function cancelBulk() {
            // Dirty check: if user has unsaved changes vs last populate/save snapshot, confirm
            try {
                if (bulkSnapshot && JSON.stringify(bulkEdits) !== bulkSnapshot) {
                    if (!window.confirm('You have unsaved bulk-edit changes. Discard them and exit?')) return;
                }
            } catch(e) { /* if comparison fails, fall through and exit */ }
            setBulkMode(false);
            setBulkFields([]);
            setBulkEdits({});
            setApplyAll({});
            setBulkMsg('');
            setBulkProductData({});
            setBulkSnapshot('');
            setApplyAllOpen(true); // reset so next bulk session starts expanded
        }

        // Remove a single product from the in-progress bulk batch.
        // If it was the last product, exit bulk mode entirely.
        function removeBulkProduct(pid) {
            var newEdits = Object.assign({}, bulkEdits);
            delete newEdits[String(pid)];
            delete newEdits[pid];
            if (Object.keys(newEdits).length === 0) {
                // Last one — leave bulk mode without dirty prompt (snapshot is irrelevant now)
                setBulkSnapshot('');
                setBulkMode(false);
                setBulkFields([]);
                setBulkEdits({});
                setApplyAll({});
                setBulkMsg('');
                setBulkProductData({});
                setApplyAllOpen(true);
                // Also drop the pid from the page selection so the original list reflects reality
                setSelected(function(prev){ var n = new Set(prev); n.delete(pid); n.delete(Number(pid)); return n; });
                return;
            }
            setBulkEdits(newEdits);
            var newProds = Object.assign({}, bulkProductData);
            delete newProds[String(pid)];
            delete newProds[pid];
            setBulkProductData(newProds);
            // Update snapshot to drop this pid so removing it doesn't itself mark state dirty
            try {
                var snap = bulkSnapshot ? JSON.parse(bulkSnapshot) : {};
                delete snap[String(pid)];
                delete snap[pid];
                setBulkSnapshot(JSON.stringify(snap));
            } catch(e) { setBulkSnapshot(JSON.stringify(newEdits)); }
            // Drop from page selection too
            setSelected(function(prev){ var n = new Set(prev); n.delete(pid); n.delete(Number(pid)); return n; });
        }

        // Render a single bulk field input
        function bulkInput(fld, value, onChange) {
            var k = fieldKey(fld);
            var inputSty = { width:'100%', padding:'6px 10px', borderRadius:6, border:'1px solid '+borderColor, background:dark?'#262626':'#fff', color:textColor, fontSize:12, outline:'none' };
            if (fld.type === 'select') {
                return h('select', { value:value||'', onChange:function(e){ onChange(e.target.value); }, style:Object.assign({}, inputSty, { cursor:'pointer' }) },
                    h('option', { value:'' }, '\u2014'),
                    (fld.opts||[]).map(function(o){ return h('option', { key:o[0], value:o[0] }, o[1]); })
                );
            }
            if (fld.type === 'bool') {
                return h('select', { value:value!=null?String(Number(value)):'', onChange:function(e){ onChange(e.target.value===''?'':Number(e.target.value)); }, style:Object.assign({}, inputSty, { cursor:'pointer' }) },
                    h('option', { value:'' }, '\u2014'),
                    h('option', { value:'1' }, 'Yes'),
                    h('option', { value:'0' }, 'No')
                );
            }
            if (fld.type === 'attr_select') {
                return h('select', { value:value||'', onChange:function(e){ onChange(e.target.value); }, style:Object.assign({}, inputSty, { cursor:'pointer' }) },
                    h('option', { value:'' }, '\u2014'),
                    (fld.choices||[]).map(function(c){ return h('option', { key:c.id, value:String(c.id) }, c.name); })
                );
            }
            if (fld.type === 'categories') {
                return h(CategoryTreeMultiPicker, {
                    cats: cats,
                    value: value,
                    onChange: onChange,
                    dark: dark,
                    cardBg: cardBg,
                    textColor: textColor,
                    mutedColor: mutedColor,
                    borderColor: borderColor
                });
            }
            return h('input', { type:fld.type==='number'||fld.type==='int'?'number':'text', value:value!=null?value:'', step:fld.type==='number'?'0.01':'1',
                onChange:function(e){ onChange(fld.type==='int'?parseInt(e.target.value)||'':fld.type==='number'?e.target.value:e.target.value); },
                style:inputSty
            });
        }

        // ── Pagination range ──
        var pStart = (page - 1) * PAGE_SIZE + 1;
        var pEnd   = Math.min(page * PAGE_SIZE, total);

        return h('main', { style:{ flex:1, overflow:'auto', padding:S.xxl } },

            // ── KPI STAT CARDS ──────────────────────────────────────────
            h('div', { style:{ display:'grid', gridTemplateColumns:'repeat(4,1fr)', gap:S.lg, marginBottom:S.xl } },
                h(StatCard, { title:'Total Products', value:String(stats.total), trend:'+5.0%', icon:icons.boxIcon, positive:true, dark:dark, cardBg:cardBg, borderColor:borderColor, mutedColor:mutedColor }),
                h(StatCard, { title:'In Stock', value:String(stats.inStock), trend:'+3.5%', icon:icons.shoppingBag, positive:true, dark:dark, cardBg:cardBg, borderColor:borderColor, mutedColor:mutedColor }),
                h(StatCard, { title:'Low Stock', value:String(stats.lowStock), trend:'-2.1%', icon:icons.walletIcon, positive:false, dark:dark, cardBg:cardBg, borderColor:borderColor, mutedColor:mutedColor }),
                h(StatCard, { title:'Inactive', value:String(stats.inactive), trend:'', icon:icons.products, positive:false, dark:dark, cardBg:cardBg, borderColor:borderColor, mutedColor:mutedColor })
            ),

            // ── PRODUCT LIST CARD ───────────────────────────────────────
            h('div', { style:{ background:cardBg, borderRadius:12, border:'1px solid '+borderColor, boxShadow:cardShadow(dark) } },

                // Card header: title + search
                h('div', { style:{ display:'flex', justifyContent:'space-between', alignItems:'center', padding:'16px 24px', borderBottom:'1px solid '+borderColor, flexWrap:'wrap', gap:12 } },
                    h('div', null,
                        h('div', { style:{ fontSize:16, fontWeight:600, marginBottom:2 } }, 'Products List'),
                        h('div', { style:{ fontSize:13, color:mutedColor } }, 'Manage your product catalog')
                    ),
                    h('div', { style:{ position:'relative', width:260 } },
                        h('span', { style:{ position:'absolute', left:12, top:'50%', transform:'translateY(-50%)', color:mutedColor, display:'flex' } }, icons.search),
                        h('input', { value:search, onChange:function(e){ setSearch(e.target.value); setPage(1); },
                            placeholder:'Search products...',
                            style:{ width:'100%', padding:'8px 12px 8px 36px', borderRadius:8, border:'1px solid '+borderColor, background:dark?'#262626':'#fff', color:textColor, fontSize:13, outline:'none' } })
                    )
                ),

                // Card content
                h('div', { style:{ padding:'20px 24px' } },

                    // Selection bar (shown when items selected, hidden in bulk mode)
                    !bulkMode && selected.size > 0 && h('div', { style:{ display:'flex', justifyContent:'space-between', alignItems:'center', padding:'10px 16px', borderRadius:8, border:'1px solid '+borderColor, background:dark?'rgba(255,255,255,.03)':'#FAFAFA', marginBottom:16 } },
                        h('span', { style:{ fontSize:13, color:mutedColor } }, selected.size + ' selected'),
                        h('div', { style:{ display:'flex', gap:8 } },
                            h('button', { onClick:exportCSV,
                                style:{ padding:'6px 14px', borderRadius:7, border:'1px solid '+borderColor, background:'transparent', color:textColor, fontSize:12, fontWeight:500, cursor:'pointer' } }, 'Export'),
                            h('button', { onClick:openFieldModal,
                                style:{ padding:'6px 14px', borderRadius:7, border:'1px solid '+TC.accent, background:TC.accent+'10', color:TC.accent, fontSize:12, fontWeight:500, cursor:'pointer' } }, 'Edit Selected'),
                            h('button', { onClick:delSelected,
                                style:{ padding:'6px 14px', borderRadius:7, border:'none', background:TC.error, color:'#fff', fontSize:12, fontWeight:500, cursor:'pointer' } }, 'Delete')
                        )
                    ),

                    // Filters row (hidden in bulk mode)
                    !bulkMode && h('div', { style:{ display:'flex', justifyContent:'space-between', alignItems:'center', flexWrap:'wrap', gap:12, marginBottom:16 } },
                        h('div', { style:{ display:'flex', gap:12, flexWrap:'wrap' } },
                            // Status filter
                            h('select', { value:fStatus, onChange:function(e){ setFStatus(e.target.value); setPage(1); }, style:selSty },
                                h('option', { value:'' }, 'All Statuses'),
                                h('option', { value:'active' }, 'Published'),
                                h('option', { value:'hidden' }, 'Draft'),
                                h('option', { value:'disabled' }, 'Inactive')
                            ),
                            // Category filter — collapsible tree picker
                            h(CategoryTreePicker, {
                                cats:cats, value:fCat,
                                onChange:function(v){ setFCat(v); setPage(1); },
                                dark:dark, cardBg:cardBg, textColor:textColor, mutedColor:mutedColor, borderColor:borderColor
                            }),
                            // Sort
                            h('select', { value:fSort, onChange:function(e){ setFSort(e.target.value); setPage(1); }, style:selSty },
                                h('option', { value:'date_desc' }, 'Newest first'),
                                h('option', { value:'date_asc' },  'Oldest first'),
                                h('option', { value:'name_asc' },  'Name A → Z'),
                                h('option', { value:'name_desc' }, 'Name Z → A'),
                                h('option', { value:'price_asc' }, 'Price low → high'),
                                h('option', { value:'price_desc' },'Price high → low')
                            )
                        ),
                        // Add product button
                        h('button', { onClick:function(){ navigate('products/new'); },
                            style:{ display:'flex', alignItems:'center', gap:6, padding:'8px 16px', borderRadius:8, border:'none', background:TC.primary, color:TC.primaryFg, fontSize:13, fontWeight:500, cursor:'pointer' } },
                            icons.plus, ' Add Product')
                    ),

                    // ── TABLE (hidden in bulk mode) ──────────────────────────────
                    !bulkMode && h('div', { style:{ overflowX:'auto' } },
                        h('table', { style:{ width:'100%', borderCollapse:'collapse', minWidth:850 } },
                            // Thead
                            h('thead', null,
                                h('tr', { style:{ borderBottom:'1px solid '+borderColor } },
                                    h('th', { style:{ width:40, padding:'10px 0 10px 4px', textAlign:'left' } },
                                        h('input', { type:'checkbox', checked:allChecked, onChange:toggleAll, style:{ cursor:'pointer' } })
                                    ),
                                    ['Product','Stock','Product Code','Price','Status',''].map(function(col) {
                                        return h('th', { key:col, style:{ padding:'10px 12px', fontSize:11, fontWeight:500, color:mutedColor, textTransform:'uppercase', letterSpacing:'.5px', textAlign:col===''?'right':'left' } }, col);
                                    })
                                )
                            ),
                            // Tbody
                            h('tbody', null,
                                loading && h('tr', null, h('td', { colSpan:7, style:{ textAlign:'center', padding:48, color:mutedColor, fontSize:13 } }, 'Loading…')),

                                !loading && items.length === 0 && h('tr', null, h('td', { colSpan:7, style:{ textAlign:'center', padding:48, color:mutedColor, fontSize:13 } }, 'No products found')),

                                !loading && items.map(function(p, i) {
                                    var pid    = p.product_id || p.id;
                                    var isSel  = selected.has(pid);
                                    var imgUrl = (p.image && p.image.thumb) || p.main_image_url || p.image_url || null;
                                    var stk    = stockLabel(p);
                                    var badge  = statusBadge(p.status);

                                    return h('tr', { key:pid,
                                        style:{ borderBottom: i < items.length-1 ? '1px solid '+borderColor : 'none',
                                                background: isSel ? (dark?'rgba(37,99,235,.06)':'#EFF6FF') : 'transparent',
                                                transition:'background .1s' },
                                        onMouseEnter:function(e){ if(!isSel) e.currentTarget.style.background = dark?'rgba(255,255,255,.02)':'#F9FAFB'; },
                                        onMouseLeave:function(e){ if(!isSel) e.currentTarget.style.background = 'transparent'; }
                                    },
                                        // Checkbox
                                        h('td', { style:{ padding:'12px 0 12px 4px', verticalAlign:'middle' } },
                                            h('input', { type:'checkbox', checked:isSel, onChange:function(){ toggleOne(pid); }, style:{ cursor:'pointer' }, onClick:function(e){ e.stopPropagation(); } })
                                        ),
                                        // Product name + image (Pulse: round thumbnail) + vendor source pill (S185)
                                        h('td', { style:{ padding:'12px', verticalAlign:'middle' } },
                                            h('div', { style:{ display:'flex', alignItems:'center', gap:12 } },
                                                h('div', { style:{ width:40, height:Config.productImageShape==='square'?40:53, borderRadius:8, border:'1px solid '+borderColor, background:dark?'rgba(255,255,255,.03)':'#FAFAFA', overflow:'hidden', flexShrink:0, display:'flex', alignItems:'center', justifyContent:'center' } },
                                                    imgUrl
                                                        ? h('img', { src:imgUrl, style:{ width:'100%', height:'100%', objectFit:'cover' } })
                                                        : h('span', { style:{ fontSize:16, color:mutedColor } }, '📷')
                                                ),
                                                h('div', { style:{ display:'flex', flexDirection:'column', gap:4, minWidth:0 } },
                                                    h('span', { onClick:function(){ navigate('products/'+pid); }, style:{ fontSize:13, fontWeight:500, cursor:'pointer', color:textColor } }, p.name || '—'),
                                                    p.vendor_source && h(VendorSourcePill, { src:p.vendor_source, dark:dark, mutedColor:mutedColor })
                                                )
                                            )
                                        ),
                                        // Stock
                                        h('td', { style:{ padding:'12px', fontSize:13, color:stockColor(stk.cls), fontWeight: stk.cls!=='muted'?500:400, verticalAlign:'middle' } }, stk.text),
                                        // SKU
                                        h('td', { style:{ padding:'12px', fontSize:13, color:mutedColor, verticalAlign:'middle' } }, p.sku || p.product_code || '—'),
                                        // Price
                                        h('td', { style:{ padding:'12px', fontSize:13, fontWeight:500, verticalAlign:'middle' } }, p.price != null ? (Config.currency_symbol||'$')+parseFloat(p.price).toFixed(2) : '—'),
                                        // Status badge (Pulse: outline style)
                                        h('td', { style:{ padding:'12px', verticalAlign:'middle' } },
                                            h('span', { style:{ fontSize:11, fontWeight:500, padding:'2px 8px', borderRadius:99, background:badge.bg, color:badge.color, border:'1px solid '+badge.border, display:'inline-block', whiteSpace:'nowrap' } }, badge.label)
                                        ),
                                        // Actions (Pulse: 3-dot menu) — Approve item appears only for vendor-created drafts (B3.5e)
                                        h('td', { style:{ padding:'12px', textAlign:'right', verticalAlign:'middle' } },
                                            h(RowActionMenu, {
                                                pid:pid,
                                                onView:function(){ navigate('products/'+pid); },
                                                onEdit:function(){ navigate('products/'+pid); },
                                                onDelete:function(){ del(p); },
                                                onDuplicate: Config.essentialsInstalled ? function(){
                                                    apiFetch('/essentials/copy-product/' + pid, { method:'POST' })
                                                        .then(function(d){
                                                            if (d && d.warning) { window.alert(d.warning); }
                                                            if (d && d.id) { navigate('products/' + d.id); }
                                                            else { load(); }
                                                        })
                                                        .catch(function(err){
                                                            var m = (err && err.data && err.data.error === 'feature_disabled')
                                                                ? 'Enable "Copy Product" in TitanCart Essentials to duplicate products.'
                                                                : ((err && err.message) ? err.message : 'unknown error');
                                                            setErrorBanner('Duplicate failed: ' + m);
                                                        });
                                                } : null,
                                                onApprove: ((p.status||'').toLowerCase() === 'hidden' && p.vendor_source && p.vendor_source.is_active)
                                                    ? function(){ approve(p); }
                                                    : null,
                                                dark:dark, cardBg:cardBg, borderColor:borderColor, textColor:textColor, mutedColor:mutedColor
                                            })
                                        )
                                    );
                                })
                            )
                        )
                    ),

                    // ── Pagination (hidden in bulk mode) ───────────────────────
                    !bulkMode && h('div', { style:{ display:'flex', justifyContent:'space-between', alignItems:'center', paddingTop:16, borderTop:'1px solid '+borderColor, marginTop:16 } },
                        h('span', { style:{ fontSize:13, color:mutedColor } },
                            total === 0 ? 'Showing 0 of 0' : 'Showing ' + pStart + ' - ' + pEnd + ' of ' + total
                        ),
                        h('div', { style:{ display:'flex', alignItems:'center', gap:8, flexWrap:'wrap', justifyContent:'flex-end' } },
                            h('button', { onClick:function(){ setPage(1); }, disabled:page===1, title:'First page',
                                style:{ padding:'6px 12px', borderRadius:7, border:'1px solid '+borderColor, background:'transparent', color:page===1?mutedColor:textColor, fontSize:12, fontWeight:500, cursor:page===1?'default':'pointer', opacity:page===1?.5:1 } }, '« First'),
                            h('button', { onClick:function(){ setPage(function(p){ return Math.max(1,p-1); }); }, disabled:page===1, title:'Previous page',
                                style:{ padding:'6px 14px', borderRadius:7, border:'1px solid '+borderColor, background:'transparent', color:page===1?mutedColor:textColor, fontSize:12, fontWeight:500, cursor:page===1?'default':'pointer', opacity:page===1?.5:1 } }, 'Prev'),
                            h('span', { style:{ fontSize:13, color:mutedColor } }, 'Page ' + page + ' of ' + totalPages),
                            h('button', { onClick:function(){ setPage(function(p){ return Math.min(totalPages,p+1); }); }, disabled:page===totalPages, title:'Next page',
                                style:{ padding:'6px 14px', borderRadius:7, border:'1px solid '+borderColor, background:'transparent', color:page===totalPages?mutedColor:textColor, fontSize:12, fontWeight:500, cursor:page===totalPages?'default':'pointer', opacity:page===totalPages?.5:1 } }, 'Next'),
                            h('button', { onClick:function(){ setPage(totalPages); }, disabled:page===totalPages, title:'Last page',
                                style:{ padding:'6px 12px', borderRadius:7, border:'1px solid '+borderColor, background:'transparent', color:page===totalPages?mutedColor:textColor, fontSize:12, fontWeight:500, cursor:page===totalPages?'default':'pointer', opacity:page===totalPages?.5:1 } }, 'Last »'),
                            totalPages > 1 && h('div', { style:{ display:'flex', alignItems:'center', gap:6, marginLeft:8 } },
                                h('span', { style:{ fontSize:12, color:mutedColor } }, 'Go to'),
                                h('input', { type:'number', min:1, max:totalPages, defaultValue:page, key:'pgoto'+page,
                                    onKeyDown:function(e){ if (e.key==='Enter'){ var v=parseInt(e.target.value,10); if(v>=1 && v<=totalPages) setPage(v); } },
                                    onBlur:function(e){ var v=parseInt(e.target.value,10); if(v>=1 && v<=totalPages && v!==page) setPage(v); },
                                    style:{ width:56, padding:'6px 8px', borderRadius:7, border:'1px solid '+borderColor, background:dark?'rgba(255,255,255,.04)':'#fff', color:textColor, fontSize:12, textAlign:'center', boxSizing:'border-box' } })
                            )
                        )
                    ),

                    // ── BULK EDIT VIEW ──────────────────────────────────────────
                    bulkMode && h('div', null,

                        // ── Sticky header bar (always visible while scrolling) ──
                        // top:-S.xxl negates <main>'s top padding (32px) so the bar pins at the
                        // actual viewport edge instead of 32px below it. Without this offset, the
                        // sticky reference point lands inside main's padding box, leaving a gap.
                        h('div', { style:{
                            position:'sticky', top:-S.xxl, zIndex:20,
                            background:cardBg,
                            marginLeft:-24, marginRight:-24,
                            padding:'12px 24px',
                            marginBottom:16,
                            borderBottom:'1px solid '+borderColor,
                            boxShadow:'0 2px 4px rgba(0,0,0,.04)',
                            display:'flex', justifyContent:'space-between', alignItems:'center', gap:12, flexWrap:'wrap'
                        } },
                            h('div', { style:{ display:'flex', alignItems:'center', gap:12, minWidth:0, flex:'1 1 auto' } },
                                h('button', { onClick:cancelBulk, style:{ display:'flex', alignItems:'center', gap:4, padding:'7px 14px', borderRadius:7, border:'1px solid '+borderColor, background:'transparent', color:textColor, fontSize:12, fontWeight:500, cursor:'pointer', flexShrink:0 } },
                                    h('span', { style:{ fontSize:16, lineHeight:'1' } }, '\u2190'), ' Back to Products'
                                ),
                                h('div', { style:{ minWidth:0, overflow:'hidden' } },
                                    h('div', { style:{ fontSize:15, fontWeight:600 } }, 'Bulk Edit \u2014 ' + Object.keys(bulkEdits).length + ' product(s)'),
                                    h('div', {
                                        style:{ fontSize:12, color:mutedColor, marginTop:2, whiteSpace:'nowrap', overflow:'hidden', textOverflow:'ellipsis' },
                                        title:'Editing: ' + bulkFields.map(function(k){ var f=getAllBulkFields().find(function(x){ return fieldKey(x)===k; }); return f?f.label:k; }).join(', ')
                                    }, 'Editing: ' + bulkFields.map(function(k){ var f=getAllBulkFields().find(function(x){ return fieldKey(x)===k; }); return f?f.label:k; }).join(', '))
                                )
                            ),
                            h('div', { style:{ display:'flex', alignItems:'center', gap:8, flexShrink:0 } },
                                // Link scroll toggle — when enabled, scrolling one product card scrolls all of them
                                h('label', {
                                    title: scrollSyncEnabled
                                        ? 'All product cards scroll together horizontally (columns stay aligned). Click to scroll each card independently.'
                                        : 'Each product card scrolls independently. Click to link all cards so scrolling one scrolls all.',
                                    style:{
                                        display:'flex', alignItems:'center', gap:6, padding:'5px 10px',
                                        borderRadius:7,
                                        border:'1px solid '+(scrollSyncEnabled ? TC.accent+'55' : borderColor),
                                        background: scrollSyncEnabled ? (dark?'rgba(37,99,235,.1)':'rgba(37,99,235,.05)') : 'transparent',
                                        color: scrollSyncEnabled ? TC.accent : mutedColor,
                                        fontSize:11, fontWeight:500, cursor:'pointer',
                                        userSelect:'none', flexShrink:0,
                                        transition:'all .12s ease'
                                    }
                                },
                                    h('input', {
                                        type:'checkbox',
                                        checked: scrollSyncEnabled,
                                        onChange: function(e) { setScrollSyncEnabled(e.target.checked); },
                                        style:{ cursor:'pointer', accentColor:TC.accent, margin:0 }
                                    }),
                                    'Link scroll'
                                ),
                                bulkMsg && h('span', { style:{ fontSize:12, fontWeight:500, color: bulkMsg.indexOf('Error')===0 ? TC.error : TC.success, padding:'4px 10px', borderRadius:6, background: bulkMsg.indexOf('Error')===0 ? (dark?'rgba(220,38,38,.1)':'#FEF2F2') : (dark?'rgba(22,163,74,.1)':'#F0FDF4') } }, bulkMsg),
                                h('button', { onClick:saveBulk, disabled:bulkSaving||bulkLoading, style:{ padding:'7px 16px', borderRadius:7, border:'none', background:TC.accent, color:'#fff', fontSize:12, fontWeight:500, cursor:'pointer', opacity:(bulkSaving||bulkLoading)?.6:1 } }, bulkSaving ? 'Saving...' : 'Save All')
                            )
                        ),

                        // ── Loading state ──
                        bulkLoading && h('div', { style:{ textAlign:'center', padding:48, color:mutedColor, fontSize:13 } }, 'Loading product data...'),

                        // ── Apply to All Products (collapsible) ──
                        !bulkLoading && h('div', { style:{ borderRadius:10, border:'1px solid '+TC.accent+'30', background:dark?'rgba(37,99,235,.05)':'#F0F6FF', marginBottom:16, overflow:'hidden' } },
                            // Clickable header / toggle
                            h('button', {
                                onClick:function(){ setApplyAllOpen(function(v){ return !v; }); },
                                style:{ width:'100%', display:'flex', justifyContent:'space-between', alignItems:'center', padding:'12px 16px', background:'transparent', border:'none', cursor:'pointer', color:TC.accent, fontSize:13, fontWeight:600, textAlign:'left' }
                            },
                                h('span', null, 'Apply to All Products'),
                                h('span', { style:{ display:'inline-flex', alignItems:'center', justifyContent:'center', width:18, height:18, transform: applyAllOpen ? 'rotate(90deg)' : 'rotate(0deg)', transition:'transform .15s ease', fontSize:12 } }, '\u25B6')
                            ),
                            // Content (conditional render)
                            applyAllOpen && h('div', { style:{ padding:'0 16px 16px' } },
                                h('div', { style:{ display:'grid', gridTemplateColumns:'repeat(auto-fill,minmax(180px,1fr))', gap:10 } },
                                    bulkFields.map(function(k) {
                                        var fld = getAllBulkFields().find(function(x){ return fieldKey(x)===k; });
                                        if (!fld) return null;
                                        return h('div', { key:k },
                                            h('label', { style:{ fontSize:11, color:mutedColor, display:'block', marginBottom:4 } }, fld.label),
                                            bulkInput(fld, applyAll[k]!=null?applyAll[k]:'', function(v){ setApplyAll(function(prev){ var c=Object.assign({},prev); c[k]=v; return c; }); })
                                        );
                                    })
                                ),
                                h('button', { onClick:applyToAll, style:{ marginTop:10, padding:'6px 16px', borderRadius:6, border:'none', background:TC.accent, color:'#fff', fontSize:12, fontWeight:500, cursor:'pointer' } }, 'Apply to All')
                            )
                        ),

                        // ── Per-product cards (each card has its own horizontal scroll) ──
                        !bulkLoading && h('div', { style:{ display:'flex', flexDirection:'column', gap:12 } },
                            Object.keys(bulkEdits).map(function(pid) {
                                var listProd   = items.find(function(p){ return String(p.product_id||p.id)===String(pid); });
                                var detailProd = bulkProductData[pid];
                                var prod       = detailProd || listProd;
                                var pName      = prod ? (prod.name || prod.sku || '#'+pid) : '#'+pid;
                                var pCode      = prod ? (prod.sku || prod.product_code || '') : '';

                                // Image resolution handles both endpoint shapes:
                                //  - list endpoint returns singular {image: {url, thumb, alt}}
                                //  - detail endpoint returns plural {images: [{thumb, thumbnail, url, ...}, ...]}
                                //    (sorted is_main DESC, position ASC — first is main)
                                var imgUrl = null;
                                if (listProd && listProd.image && listProd.image.thumb) {
                                    imgUrl = listProd.image.thumb;
                                } else if (detailProd && Array.isArray(detailProd.images) && detailProd.images.length > 0) {
                                    var mainImg = detailProd.images[0];
                                    imgUrl = mainImg ? (mainImg.thumb || mainImg.thumbnail || mainImg.url) : null;
                                } else if (prod && prod.image && typeof prod.image === 'object') {
                                    imgUrl = prod.image.thumb || prod.image.url;
                                }

                                return h('div', { key:pid, style:{ border:'1px solid '+borderColor, borderRadius:10, overflow:'hidden', background:cardBg } },

                                    // ── Card header: product info (left) + Remove × button (right) ──
                                    h('div', { style:{ display:'flex', justifyContent:'space-between', alignItems:'center', padding:'10px 14px', borderBottom:'1px solid '+borderColor, background:dark?'rgba(255,255,255,.02)':'#FAFAFA' } },
                                        h('div', { style:{ display:'flex', alignItems:'center', gap:10, minWidth:0, flex:'1 1 auto' } },
                                            imgUrl
                                                ? h('img', { src:imgUrl, style:{ width:32, height: Config.productImageShape==='square' ? 32 : 43, borderRadius:6, objectFit:'cover', flexShrink:0 } })
                                                : h('div', { style:{ width:32, height: Config.productImageShape==='square' ? 32 : 43, borderRadius:6, background:dark?'rgba(255,255,255,.04)':'#F0F0F0', display:'flex', alignItems:'center', justifyContent:'center', fontSize:14, color:mutedColor, flexShrink:0 } }, '\uD83D\uDCF7'),
                                            h('div', { style:{ minWidth:0 } },
                                                h('div', { style:{ fontSize:13, fontWeight:600, whiteSpace:'nowrap', overflow:'hidden', textOverflow:'ellipsis' }, title:pName }, pName),
                                                pCode && h('div', { style:{ fontSize:11, color:mutedColor, marginTop:1 } }, pCode)
                                            )
                                        ),
                                        h('button', {
                                            onClick:function(){ removeBulkProduct(pid); },
                                            title:'Remove from batch',
                                            'aria-label':'Remove ' + pName + ' from batch',
                                            style:{ display:'flex', alignItems:'center', justifyContent:'center', width:28, height:28, borderRadius:6, border:'1px solid '+borderColor, background:'transparent', color:mutedColor, cursor:'pointer', fontSize:18, lineHeight:'1', flexShrink:0, transition:'all .12s ease' },
                                            onMouseEnter:function(e){ e.currentTarget.style.background = dark?'rgba(220,38,38,.15)':'#FEF2F2'; e.currentTarget.style.color = TC.error; e.currentTarget.style.borderColor = TC.error+'40'; },
                                            onMouseLeave:function(e){ e.currentTarget.style.background = 'transparent'; e.currentTarget.style.color = mutedColor; e.currentTarget.style.borderColor = borderColor; }
                                        }, '\u00D7')
                                    ),

                                    // ── Field strip: per-card horizontal scroll (synced across all cards) ──
                                    h('div', {
                                        ref: setCardScrollRef(pid),
                                        onScroll: syncCardScroll(pid),
                                        style:{ overflowX:'auto', overflowY:'visible', padding:'12px 14px' }
                                    },
                                        h('div', { style:{ display:'flex', gap:12, minWidth:'min-content', alignItems:'flex-start' } },
                                            bulkFields.map(function(k) {
                                                var fld = getAllBulkFields().find(function(x){ return fieldKey(x)===k; });
                                                if (!fld) return null;
                                                // Wider columns for text-heavy fields, narrower for booleans/selects.
                                                // categories now uses CategoryTreeMultiPicker (compact dropdown trigger), so default width is fine.
                                                var colW = (fld.type==='text' || fld.subkey==='meta_desc' || fld.subkey==='meta_title') ? 240
                                                         : (fld.type==='bool' || fld.type==='select') ? 160
                                                         : 180;
                                                return h('div', { key:k, style:{ width:colW, minWidth:colW, flexShrink:0 } },
                                                    h('label', { style:{ fontSize:11, color:mutedColor, display:'block', marginBottom:4, whiteSpace:'nowrap', overflow:'hidden', textOverflow:'ellipsis' }, title:fld.label }, fld.label),
                                                    bulkInput(fld, bulkEdits[pid][k], function(v){ updateBulkEdit(pid, k, v); })
                                                );
                                            })
                                        )
                                    )
                                );
                            })
                        )
                    )
                )
            ),

            // ── FIELD SELECTION MODAL ────────────────────────────────────────
            showFieldModal && h('div', { onClick:function(e){ if(e.target===e.currentTarget) setShowFieldModal(false); },
                style:{ position:'fixed', inset:0, background:'rgba(0,0,0,.45)', display:'flex', alignItems:'center', justifyContent:'center', zIndex:9999 } },
                h('div', { style:{ background:cardBg, borderRadius:14, border:'1px solid '+borderColor, width:520, maxHeight:'80vh', overflow:'auto', padding:24, boxShadow:'0 20px 60px rgba(0,0,0,.2)' } },
                    h('div', { style:{ display:'flex', justifyContent:'space-between', alignItems:'center', marginBottom:20 } },
                        h('div', null,
                            h('div', { style:{ fontSize:16, fontWeight:600 } }, 'Select Fields to Edit'),
                            h('div', { style:{ fontSize:12, color:mutedColor, marginTop:2 } }, 'Choose which fields to bulk edit for ' + selected.size + ' product(s)')
                        ),
                        h('button', { onClick:function(){ setShowFieldModal(false); }, style:{ background:'transparent', border:'none', fontSize:20, color:mutedColor, cursor:'pointer', padding:4 } }, '\u00D7')
                    ),
                    getAllFieldGroups().map(function(group) {
                        return h('div', { key:group.title, style:{ marginBottom:16 } },
                            h('div', { style:{ fontSize:12, fontWeight:600, color:mutedColor, textTransform:'uppercase', letterSpacing:'.5px', marginBottom:8 } }, group.title),
                            h('div', { style:{ display:'grid', gridTemplateColumns:'1fr 1fr', gap:6 } },
                                group.fields.map(function(fld) {
                                    var k = fieldKey(fld);
                                    var checked = bulkFields.indexOf(k) >= 0;
                                    return h('label', { key:k, style:{ display:'flex', alignItems:'center', gap:8, padding:'6px 10px', borderRadius:6, border:'1px solid '+(checked?TC.accent+'40':borderColor), background:checked?(dark?'rgba(37,99,235,.08)':'#F0F6FF'):'transparent', cursor:'pointer', fontSize:12 } },
                                        h('input', { type:'checkbox', checked:checked, onChange:function(){ toggleFieldSelection(fld); }, style:{ cursor:'pointer', accentColor:TC.accent } }),
                                        fld.label
                                    );
                                })
                            )
                        );
                    }),
                    bulkAttrsLoading && h('div', { style:{ padding:'12px 0', fontSize:12, color:mutedColor, textAlign:'center' } }, 'Loading product attributes...'),
                    h('div', { style:{ display:'flex', justifyContent:'flex-end', gap:8, marginTop:20, paddingTop:16, borderTop:'1px solid '+borderColor } },
                        h('button', { onClick:function(){ setShowFieldModal(false); }, style:{ padding:'8px 18px', borderRadius:7, border:'1px solid '+borderColor, background:'transparent', color:textColor, fontSize:13, fontWeight:500, cursor:'pointer' } }, 'Cancel'),
                        h('button', { onClick:openBulkEditor, disabled:bulkFields.length===0, style:{ padding:'8px 18px', borderRadius:7, border:'none', background:bulkFields.length===0?(dark?'#333':'#ddd'):TC.accent, color:bulkFields.length===0?mutedColor:'#fff', fontSize:13, fontWeight:500, cursor:bulkFields.length===0?'default':'pointer' } }, 'Edit ' + bulkFields.length + ' field(s)')
                    )
                )
            ),

            /* S183 — Inline error banner for delete failures (replaces alert()). */
            errorBanner && h('div', { style:{ display:'flex', alignItems:'flex-start', gap:10, padding:'12px 16px', marginBottom:16, borderRadius:10, background: dark ? 'rgba(220,38,38,.08)' : TC.errorBg, border:'1px solid '+TC.error+'40', color:TC.error, fontSize:13, lineHeight:1.5 } },
                h('svg', { width:16, height:16, viewBox:'0 0 20 20', fill:'none', stroke:'currentColor', strokeWidth:'1.75', strokeLinecap:'round', strokeLinejoin:'round', style:{ marginTop:2, flexShrink:0 } },
                    h('circle', { cx:10, cy:10, r:8 }),
                    h('path', { d:'M10 6v5' }),
                    h('path', { d:'M10 14h.01' })
                ),
                h('div', { style:{ flex:1 } }, errorBanner),
                h('button', {
                    onClick: function(){ setErrorBanner(null); },
                    'aria-label': 'Dismiss',
                    style:{ background:'transparent', border:'none', color:TC.error, fontSize:18, lineHeight:1, cursor:'pointer', padding:0, marginLeft:4 }
                }, '\u00D7')
            ),

            /* S183 — Pulse delete-confirm modal. Same helper VendorProductsPage
             * uses; message varies by single vs bulk. Admin DELETE is a product-
             * row soft-delete (Repository::delete sets deleted_at), not the
             * vendor-side junction soft-delete with status flip. Copy reflects
             * that — "removed from your catalog" rather than "moved to draft". */
            h(ConfirmModal, {
                open: !!confirmDelete,
                title: confirmDelete && confirmDelete.kind === 'bulk'
                    ? ('Delete ' + confirmDelete.count + ' products?')
                    : 'Delete product?',
                detail: confirmDelete && confirmDelete.kind === 'single' && confirmDelete.product
                    ? (confirmDelete.product.name || 'Product #' + (confirmDelete.product.product_id || confirmDelete.product.id))
                    : null,
                message: confirmDelete && confirmDelete.kind === 'bulk'
                    ? ('These ' + confirmDelete.count + ' products will be removed from your catalog. Restoring requires direct database access.')
                    : 'This product will be removed from your catalog. Restoring requires direct database access.',
                confirmLabel: confirmDelete && confirmDelete.kind === 'bulk'
                    ? ('Delete ' + confirmDelete.count + ' products')
                    : 'Delete product',
                busy: deleting,
                onConfirm: confirmDeleteAction,
                onCancel: cancelDelete,
                dark: dark, cardBg: cardBg, borderColor: borderColor, textColor: textColor, mutedColor: mutedColor,
            }),

            /* S185 — Approve modal (B3.5e). Same ConfirmModal helper as Delete
             * but with tone='positive' so the affordance reads as a promote
             * action (green check + green button) rather than destructive. */
            h(ConfirmModal, {
                open: !!confirmApprove,
                title: 'Approve product?',
                detail: confirmApprove && confirmApprove.product
                    ? (confirmApprove.product.name || 'Product #' + (confirmApprove.product.product_id || confirmApprove.product.id))
                    : null,
                message: confirmApprove && confirmApprove.product && confirmApprove.product.vendor_source
                    ? ('This vendor draft will become Published and appear on the storefront. Originally submitted by ' + (confirmApprove.product.vendor_source.display_name || ('vendor #' + confirmApprove.product.vendor_source.vendor_id)) + '.')
                    : 'This product will become Published and appear on the storefront.',
                confirmLabel: 'Approve',
                tone: 'positive',
                busy: approving,
                onConfirm: confirmApproveAction,
                onCancel: cancelApprove,
                dark: dark, cardBg: cardBg, borderColor: borderColor, textColor: textColor, mutedColor: mutedColor,
            })
        );
    }
    /* ── ORPHANED_OLD_CODE_START

        // ── Active filter chips ───────────────────────────────────────────────
        const [fPrice,    setFPrice]    = useState('');   // min-max string
        const [fCat,      setFCat]      = useState('');   // category id
        const [fStatus,   setFStatus]   = useState('');   // A / D / H
        const [fSort,     setFSort]     = useState('date_desc');
        const [showFilterMenu, setShowFilterMenu] = useState(null); // which dropdown open

        // ── More menu ────────────────────────────────────────────────────────
        const [showMore, setShowMore] = useState(false);

        // ── Categories for filter dropdown ───────────────────────────────────
        const [cats, setCats] = useState([]);
        useEffect(function(){
            apiFetch('/categories?format=flat&per_page=200').then(function(d){
                setCats(Array.isArray(d) ? d : (d.categories||[]));
            }).catch(function(){});
        }, []);

        function load() {
            setLoading(true);
            const baseUrl = Config.apiUrl || '/wp-json/titancart/v1';
            const q = new URLSearchParams({
                page,
                per_page: TC_PER_PAGE,
                search,
                status:      fStatus,
                category_id: fCat,
                sort_by:     fSort.slice(0, fSort.lastIndexOf('_')),
                sort_order:  fSort.slice(fSort.lastIndexOf('_') + 1),
            });
            const url = baseUrl + '/products' + (baseUrl.indexOf('?') !== -1 ? '&' : '?') + q;
            console.log('[TitanCart] ProductListPage fetching:', url);
            fetch(url, {
                headers: { 'X-WP-Nonce': Config.nonce || '' }
            }).then(function(r) {
                const hTotal  = parseInt(r.headers.get('X-TC-Total') || '0', 10);
                const hPages  = parseInt(r.headers.get('X-TC-Total-Pages') || '1', 10);
                r.json().then(function(d) {
                    console.log('[TitanCart] ProductListPage response:', d);
                    const rows = Array.isArray(d) ? d : (d.products || []);
                    const tot  = hTotal || d.total || rows.length;
                    const pgs  = hPages || d.total_pages || 1;
                    setItems(rows);
                    setTotal(tot);
                    setTotalPages(pgs);
                    setLoading(false);
                });
            }).catch(function(err){
                console.error('[TitanCart] ProductListPage fetch error:', err);
                setLoading(false);
            });
        }
        useEffect(load, [page, search, fStatus, fCat, fSort]);

        // ── Selection helpers ────────────────────────────────────────────────
        function toggleAll(e) {
            if (e.target.checked) { setSelected(new Set(items.map(p => p.product_id||p.id))); }
            else                  { setSelected(new Set()); }
        }
        function toggleOne(pid) {
            setSelected(prev => {
                const next = new Set(prev);
                if (next.has(pid)) next.delete(pid); else next.add(pid);
                return next;
            });
        }
        const allChecked = items.length > 0 && items.every(p => selected.has(p.product_id||p.id));

        function del(p) {
            if (!confirm('Delete "' + p.name + '"?')) return;
            apiFetch('/products/' + (p.product_id || p.id), { method:'DELETE' }).then(load);
        }
        function delSelected() {
            if (!selected.size || !confirm('Delete ' + selected.size + ' product(s)?')) return;
            Promise.all([...selected].map(id => apiFetch('/products/' + id, { method:'DELETE' }))).then(function(){ setSelected(new Set()); load(); });
        }
        function cloneProduct(p) {
            apiFetch('/products/' + (p.product_id||p.id) + '/clone', { method:'POST' }).then(load);
        }

        // ── Filter chip — now module-level; pass state as props ──────────────
        // ── Sort options ──────────────
        // v = 'sortBy_sortDir' — sortBy must be one of: name|price|position|date|sales|rating
        const SORT_OPTS = [
            { v:'date_desc',     l:'Newest first' },
            { v:'date_asc',      l:'Oldest first' },
            { v:'name_asc',      l:'Name A → Z' },
            { v:'name_desc',     l:'Name Z → A' },
            { v:'price_asc',     l:'Price low → high' },
            { v:'price_desc',    l:'Price high → low' },
            { v:'position_asc',  l:'Position' },
            { v:'sales_desc',    l:'Best selling' },
        ];
        const currentSort = SORT_OPTS.find(o => o.v === fSort) || SORT_OPTS[0];

        // ── Active filter count (for filter icon badge) ───────────────────────
        const activeFilters = [fStatus, fCat, fPrice].filter(Boolean).length;

        // close dropdowns on outside click
        useEffect(function(){
            const close = ()=>{ setShowFilterMenu(null); setShowMore(false); };
            document.addEventListener('click', close);
            return ()=>document.removeEventListener('click', close);
        }, []);

        const cols = '40px 36px 1fr 110px 90px 90px 90px 100px';

        return h('main', { style:{ flex:1, display:'flex', flexDirection:'column', overflow:'hidden' } },

            // ── Top action bar ────────────────────────────────────────────────
            h('div', { style:{ padding:`${S.lg}px ${S.xxl}px`, display:'flex', justifyContent:'space-between', alignItems:'center', flexShrink:0 } },
                h('h2', { style:{ fontSize:22, fontWeight:700, letterSpacing:'-.3px' } }, 'Products'),
                h('div', { style:{ display:'flex', gap:S.sm } },
                    // Add several products
                    h('button', { onClick:()=>navigate('products/bulk-add'),
                        style:{ padding:'7px 14px', borderRadius:8, border:`1px solid ${borderColor}`, background:dark?'#262626':'#F5F5F5',
                                color:mutedColor, fontSize:13, cursor:'pointer', fontWeight:500 } }, 'Add several products'),
                    h('button', { onClick:()=>navigate('import-export'),
                        style:{ padding:'7px 14px', borderRadius:8, border:`1px solid ${borderColor}`, background:dark?'#262626':'#F5F5F5',
                                color:mutedColor, fontSize:13, cursor:'pointer', fontWeight:500 } }, 'Import'),
                    h('button', { onClick:()=>navigate('import-export'),
                        style:{ padding:'7px 14px', borderRadius:8, border:`1px solid ${borderColor}`, background:dark?'#262626':'#F5F5F5',
                                color:mutedColor, fontSize:13, cursor:'pointer', fontWeight:500 } }, 'Export'),
                    // More dropdown
                    h('div', { style:{ position:'relative' },
                        onClick:e=>{ e.stopPropagation(); setShowMore(!showMore); } },
                        h('button', { style:{ padding:'7px 14px', borderRadius:8, border:`1px solid ${borderColor}`, background:dark?'#262626':'#F5F5F5',
                                color:mutedColor, fontSize:13, cursor:'pointer', fontWeight:500 } }, 'More ▾'),
                        showMore && h('div', { style:{ position:'absolute', right:0, top:'calc(100% + 6px)', background:cardBg, border:`1px solid ${borderColor}`,
                                borderRadius:10, boxShadow:'0 8px 24px rgba(0,0,0,.18)', minWidth:180, padding:S.sm, zIndex:200 } },
                            ...[['Clone selected', ()=>{ setShowMore(false); }],
                                ['Delete selected', ()=>{ setShowMore(false); delSelected(); }],
                                ['Change status', ()=>{ setShowMore(false); }],
                            ].map(([label, fn2], i) => h('button', { key:i, onClick:fn2,
                                style:{ display:'block', width:'100%', textAlign:'left', padding:`8px ${S.md}px`, borderRadius:6, border:'none',
                                        background:'transparent', color: label==='Delete selected'?TC.error:textColor, fontSize:13, cursor:'pointer' },
                                onMouseEnter:e=>{ e.currentTarget.style.background=dark?'rgba(255,255,255,.04)':'#FAFAFA'; },
                                onMouseLeave:e=>{ e.currentTarget.style.background='transparent'; } }, label))
                        )
                    ),
                    // Primary CTA
                    h(Btn, { variant:'primary', onClick:()=>navigate('products/new'), mutedColor, borderColor },
                        h('span', { style:{ fontSize:16, marginRight:4, fontWeight:300 } }, '+'), ' Add product'
                    )
                )
            ),

            // ── Search + filter bar ───────────────────────────────────────────
            h('div', { style:{ padding:`0 ${S.xxl}px`, flexShrink:0 } },
                // View tabs + search
                h('div', { style:{ display:'flex', gap:S.sm, alignItems:'center', marginBottom:S.md } },
                    // All / saved views
                    h('div', { style:{ display:'flex', background:dark?'#262626':'#F5F5F5', borderRadius:8, padding:2, gap:2 } },
                        h('button', { style:{ padding:`5px ${S.md}px`, borderRadius:6, border:'none', cursor:'pointer', fontSize:12, fontWeight:500,
                                background:TC.accent, color:'#fff' } }, 'All'),
                        h('button', { style:{ padding:`5px ${S.md}px`, borderRadius:6, border:'none', cursor:'pointer', fontSize:12,
                                background:'transparent', color:mutedColor } }, 'Recently updated'),
                        h('button', { style:{ padding:`5px ${S.md}px`, borderRadius:6, border:'none', cursor:'pointer', fontSize:14,
                                background:'transparent', color:mutedColor } }, '+')
                    ),
                    // Full-width search
                    h('div', { style:{ flex:1, display:'flex', alignItems:'center', gap:S.sm, background:dark?'#262626':'#fff',
                                borderRadius:8, padding:`7px ${S.base}px`, border:`1px solid ${borderColor}` } },
                        h('span', { style:{ color:mutedColor, display:'flex' } }, icons.search),
                        h('input', { value:search, onChange:e=>{ setSearch(e.target.value); setPage(1); },
                            placeholder:'Searching all products',
                            style:{ background:'transparent', border:'none', outline:'none', color:'inherit', fontSize:13, width:'100%' } })
                    )
                ),

                // Filter chips row
                h('div', { style:{ display:'flex', gap:S.sm, alignItems:'center', marginBottom:S.lg, flexWrap:'wrap' },
                    onClick:e=>e.stopPropagation() },

                    // Price filter
                    h(FilterChip, { label:'Price ($)', value:fPrice, onClear:()=>setFPrice(''), activeMenu:showFilterMenu, onMenuToggle:setShowFilterMenu, dark, borderColor, mutedColor },
                        h(FilterDropdown, { cardBg, borderColor, onClose:()=>setShowFilterMenu(null) },
                            h('div', { style:{ padding:S.sm } },
                                h('div', { style:{ fontSize:12, fontWeight:500, marginBottom:S.sm } }, 'Price range'),
                                h('div', { style:{ display:'flex', gap:S.sm, alignItems:'center' } },
                                    h('input', { type:'number', placeholder:'Min', style:{ width:80, padding:'6px 8px', borderRadius:8, border:`1px solid ${borderColor}`, background:dark?'#262626':'#fff', color:'inherit', fontSize:13, outline:'none' },
                                        onKeyDown:e=>{ if(e.key==='Enter'){ setFPrice(e.target.value ? e.target.value+'+' : ''); setShowFilterMenu(null); } } }),
                                    h('span', { style:{ color:mutedColor } }, '—'),
                                    h('input', { type:'number', placeholder:'Max', style:{ width:80, padding:'6px 8px', borderRadius:8, border:`1px solid ${borderColor}`, background:dark?'#262626':'#fff', color:'inherit', fontSize:13, outline:'none' },
                                        onKeyDown:e=>{ if(e.key==='Enter'){ setFPrice(e.target.value ? '—'+e.target.value : ''); setShowFilterMenu(null); } } })
                                )
                            )
                        )
                    ),

                    // Category filter
                    h(FilterChip, { label:'Category', value: fCat ? (cats.find(c=>(c.category_id||c.id)==fCat)||{}).name||fCat : '', onClear:()=>setFCat(''), activeMenu:showFilterMenu, onMenuToggle:setShowFilterMenu, dark, borderColor, mutedColor },
                        h(FilterDropdown, { cardBg, borderColor, onClose:()=>setShowFilterMenu(null) },
                            h('div', { style:{ maxHeight:240, overflowY:'auto' } },
                                h('div', { style:{ padding:`${S.sm}px ${S.md}px`, fontSize:12, fontWeight:500, color:mutedColor, borderBottom:`1px solid ${borderColor}`, marginBottom:S.xs } }, 'Category'),
                                cats.length===0 && h('div', { style:{ padding:S.md, color:mutedColor, fontSize:12 } }, 'No categories'),
                                cats.map(function(cat) {
                                    const cid = cat.category_id||cat.id;
                                    return h('button', { key:cid, onClick:()=>{ setFCat(String(cid)); setShowFilterMenu(null); setPage(1); },
                                        style:{ display:'block', width:'100%', textAlign:'left', padding:`6px ${S.md}px`, border:'none', background:fCat==cid?TC.accent+'20':'transparent',
                                                color:fCat==cid?TC.accent:textColor, fontSize:12, cursor:'pointer', paddingLeft: S.md+(cat.level||0)*14 + 'px' } }, cat.name);
                                })
                            )
                        )
                    ),

                    // Status filter
                    h(FilterChip, { label:'Status', value:{ A:'Active', D:'Draft', H:'Hidden' }[fStatus]||'', onClear:()=>setFStatus(''), activeMenu:showFilterMenu, onMenuToggle:setShowFilterMenu, dark, borderColor, mutedColor },
                        h(FilterDropdown, { cardBg, borderColor, onClose:()=>setShowFilterMenu(null) },
                            ...[['','All statuses'],['A','Active'],['D','Draft'],['H','Hidden']].map(([v,l]) =>
                                h('button', { key:v, onClick:()=>{ setFStatus(v); setShowFilterMenu(null); setPage(1); },
                                    style:{ display:'block', width:'100%', textAlign:'left', padding:`7px ${S.md}px`, border:'none',
                                            background:fStatus===v?TC.accent+'20':'transparent', color:fStatus===v?TC.accent:textColor,
                                            fontSize:12, cursor:'pointer', borderRadius:6 } }, l)
                            )
                        )
                    ),

                    // Creation date (placeholder chip)
                    h(FilterChip, { label:'Creation date', value:'', onClear:()=>{}, activeMenu:showFilterMenu, onMenuToggle:setShowFilterMenu, dark, borderColor, mutedColor },
                        h(FilterDropdown, { cardBg, borderColor, onClose:()=>setShowFilterMenu(null) },
                            h('div', { style:{ padding:S.md, color:mutedColor, fontSize:12 } }, 'Date range picker coming soon')
                        )
                    ),

                    // Sort by
                    h(FilterChip, { label:'Sort by', value:currentSort.l, onClear:null, activeMenu:showFilterMenu, onMenuToggle:setShowFilterMenu, dark, borderColor, mutedColor },
                        h(FilterDropdown, { cardBg, borderColor, onClose:()=>setShowFilterMenu(null) },
                            SORT_OPTS.map(o => h('button', { key:o.v, onClick:()=>{ setFSort(o.v); setShowFilterMenu(null); },
                                style:{ display:'block', width:'100%', textAlign:'left', padding:`7px ${S.md}px`, border:'none',
                                        background:fSort===o.v?TC.accent+'20':'transparent', color:fSort===o.v?TC.accent:textColor,
                                        fontSize:12, cursor:'pointer', borderRadius:6, fontWeight:fSort===o.v?500:400 } }, o.l))
                        )
                    ),

                    // Add filter
                    h('button', { style:{ display:'flex', alignItems:'center', gap:4, padding:`5px ${S.md}px`, borderRadius:6,
                            border:`1px dashed ${borderColor}`, background:'transparent', color:mutedColor, fontSize:12, cursor:'pointer' } },
                        '+ Add filter'),

                    // Clear all (when filters active)
                    activeFilters > 0 && h('button', { onClick:()=>{ setFStatus(''); setFCat(''); setFPrice(''); setPage(1); },
                        style:{ marginLeft:'auto', display:'flex', alignItems:'center', justifyContent:'center', width:32, height:32, borderRadius:7,
                                border:`1px solid ${borderColor}`, background:dark?'rgba(220,38,38,.1)':TC.errorBg, color:TC.error, cursor:'pointer', fontSize:16 } }, '⊘')
                )
            ),

            // ── Table ─────────────────────────────────────────────────────────
            h('div', { style:{ flex:1, overflowY:'auto', padding:`0 ${S.xxl}px ${S.xxl}px` } },
                h(Card, { cardBg, borderColor, dark, pad:0 },
                    // Column headers
                    h('div', { style:{ display:'grid', gridTemplateColumns:cols, padding:`${S.sm}px ${S.xl}px`, borderBottom:`1px solid ${borderColor}`, background:tableHeaderBg, boxShadow:tableHeaderShadow, alignItems:'center' } },
                        h('input', { type:'checkbox', checked:allChecked, onChange:toggleAll, style:{ cursor:'pointer' } }),
                        h('span', null), // image col
                        ...['Product', 'Product Code', 'Price', 'In stock', 'Status', 'Actions'].map(col =>
                            h('span', { key:col, style:{ fontSize:10, fontWeight:500, color:mutedColor, textTransform:'uppercase', letterSpacing:'.5px' } }, col)
                        )
                    ),

                    loading && h(Spinner, { mutedColor }),

                    !loading && items.length===0 && h('div', { style:{ padding:48, textAlign:'center', color:mutedColor, fontSize:13 } }, 'No data found'),

                    !loading && items.map(function(p, i) {
                        const pid     = p.product_id || p.id;
                        const isSel   = selected.has(pid);
                        const imgUrl  = (p.image && p.image.thumb) || p.main_image_url || p.image_url || null;

                        return h('div', { key:pid,
                            style:{ display:'grid', gridTemplateColumns:cols, padding:`10px ${S.xl}px`, alignItems:'center',
                                    borderBottom:i<items.length-1?`1px solid ${borderColor}`:'none',
                                    background:isSel?(dark?'rgba(37,99,235,.06)':'#EFF6FF'):'transparent',
                                    transition:'background .1s' },
                            onMouseEnter:e=>{ if(!isSel)e.currentTarget.style.background=dark?'rgba(255,255,255,.02)':'#F9FAFB'; },
                            onMouseLeave:e=>{ if(!isSel)e.currentTarget.style.background='transparent'; } },

                            // Checkbox
                            h('input', { type:'checkbox', checked:isSel, onChange:()=>toggleOne(pid), style:{ cursor:'pointer' }, onClick:e=>e.stopPropagation() }),

                            // Thumbnail
                            h('div', { style:{ width:34, height:Config.productImageShape==='square'?34:45, borderRadius:8, background:dark?'#262626':'#F5F5F5', overflow:'hidden', flexShrink:0 } },
                                imgUrl
                                    ? h('img', { src:imgUrl, style:{ width:'100%', height:'100%', objectFit:'cover' } })
                                    : h('div', { style:{ width:'100%', height:'100%', display:'flex', alignItems:'center', justifyContent:'center', fontSize:14, color:mutedColor } }, '📷')
                            ),

                            // Name + category
                            h('div', { style:{ minWidth:0 } },
                                h('div', { onClick:()=>navigate('products/'+pid), style:{ fontSize:13, fontWeight:500, cursor:'pointer', color:TC.accent, overflow:'hidden', textOverflow:'ellipsis', whiteSpace:'nowrap' } }, p.name||'—'),
                                p.category_name && h('div', { style:{ fontSize:11, color:mutedColor, marginTop:1 } }, p.category_name)
                            ),

                            // SKU
                            h('span', { style:{ fontSize:12, color:mutedColor, overflow:'hidden', textOverflow:'ellipsis', whiteSpace:'nowrap' } }, p.sku||'—'),

                            // Price
                            h('span', { style:{ fontSize:12, fontWeight:500 } }, p.price!=null ? (Config.currency_symbol||'$')+parseFloat(p.price).toFixed(2) : '—'),

                            // Stock
                            h('span', { style:{ fontSize:12, color: p.stock_quantity===0?TC.error : p.stock_quantity<=5?TC.amber : mutedColor, fontWeight:p.stock_quantity<=5?500:400 } },
                                p.track_inventory===0 ? '∞' : (p.stock_quantity!=null ? p.stock_quantity : '—')
                            ),

                            // Status badge
                            h(Badge, { status:p.status==='active'?'active':p.status==='hidden'?'pending':'inactive', dark },
                                p.status==='active'?'Active' : p.status==='hidden'?'Hidden' : 'Disabled'
                            ),

                            // Row actions
                            h('div', { style:{ display:'flex', gap:S.xs }, onClick:e=>e.stopPropagation() },
                                h('button', { onClick:()=>navigate('products/'+pid),
                                    style:{ padding:'3px 10px', borderRadius:5, border:`1px solid ${borderColor}`, background:'transparent', color:TC.accent, fontSize:11, cursor:'pointer' } }, 'Edit'),
                                h('button', { onClick:()=>cloneProduct(p),
                                    style:{ padding:'3px 10px', borderRadius:5, border:`1px solid ${borderColor}`, background:'transparent', color:mutedColor, fontSize:11, cursor:'pointer' } }, 'Clone'),
                                h('button', { onClick:()=>del(p),
                                    style:{ padding:'3px 10px', borderRadius:5, border:`1px solid ${TC.error}30`, background:'transparent', color:TC.error, fontSize:11, cursor:'pointer' } }, '✕')
                            )
                        );
                    }),

                    // Footer / pagination
                    h('div', { style:{ display:'flex', justifyContent:'space-between', alignItems:'center', padding:`${S.md}px ${S.xl}px`, borderTop:`1px solid ${borderColor}` } },
                        h('span', { style:{ fontSize:11, color:mutedColor } },
                            selected.size > 0
                                ? selected.size + ' of ' + total + ' selected'
                                : total + ' product' + (total===1?'':'s')
                        ),
                        h(Pager, { page, totalPages, onPage:setPage, mutedColor, borderColor })
                    )
                )
            )
        );
    ORPHANED_OLD_CODE_END ── */

    /* ════════════════════════════════════════════════════════════════════
     * DOWNLOADS TAB CONTENT (S129 Phase 2)
     * ════════════════════════════════════════════════════════════════════
     * Mounted only when:
     *   - the product is saved (numId truthy), AND
     *   - form.is_downloadable === true
     *
     * Renders three stacked cards inside the product editor's Downloads tab:
     *   1. Per-product overrides — TTL hours + max downloads.
     *   2. Files list — drag-to-reorder (positional buttons), edit, delete,
     *      test-download. Empty state when no files.
     *   3. New/Edit file modal — upload main file + optional preview file,
     *      variation scope, status, readme, position.
     *
     * Reads `form.download_ttl_hours` / `form.download_max_count` from the
     * product editor's form state (via the sf setter) — those persist on
     * the next product Save through the existing PUT /products/{id} pipeline
     * (allow-listed in ProductRepository.php Phase 2 edit).
     *
     * File CRUD goes through /admin/products/{id}/files (separate endpoint)
     * — multipart for create/replace, JSON for metadata patch, DELETE for
     * removal, PUT /reorder for bulk position updates.
     * ════════════════════════════════════════════════════════════════════ */
    function DownloadsTabContent(props) {
        var productId    = props.productId;
        var form         = props.form;
        var sf           = props.sf;
        var dark         = props.dark;
        var cardBg       = props.cardBg;
        var mutedColor   = props.mutedColor;
        var borderColor  = props.borderColor;
        var textColor    = props.textColor;
        var tableHeaderBg = props.tableHeaderBg;

        var [files, setFiles]       = useState([]);
        var [loading, setLoading]   = useState(true);
        var [msg, setMsg]           = useState(null);   // { text, ok }
        var [showModal, setShowModal] = useState(false);
        var [editing, setEditing]   = useState(null);
        var [busy, setBusy]         = useState(false);

        // Modal form state (transient, separate from `form` prop).
        var [mFile, setMFile]                 = useState(null);
        var [mPreview, setMPreview]           = useState(null);
        var [mClearPreview, setMClearPreview] = useState(false);
        var [mVariationId, setMVariationId]   = useState('');
        var [mReadme, setMReadme]             = useState('');
        var [mPosition, setMPosition]         = useState(0);
        var [mStatus, setMStatus]             = useState('active');

        var fileInputRef        = useRef(null);
        var previewInputRef     = useRef(null);

        function flash(text, ok) { setMsg({ text: text, ok: !!ok }); setTimeout(function(){ setMsg(null); }, 4000); }

        function loadFiles() {
            setLoading(true);
            apiFetch('/admin/products/' + productId + '/files')
                .then(function(d) { setFiles((d && d.files) || []); setLoading(false); })
                .catch(function(e) { setLoading(false); flash(e.message || 'Could not load files.', false); });
        }

        useEffect(function() { loadFiles(); /* eslint-disable-next-line */ }, [productId]);

        function fmtBytes(b) {
            b = Number(b) || 0;
            if (b < 1024) return b + ' B';
            if (b < 1024*1024) return (b/1024).toFixed(1) + ' KB';
            if (b < 1024*1024*1024) return (b/1024/1024).toFixed(1) + ' MB';
            return (b/1024/1024/1024).toFixed(2) + ' GB';
        }

        function openAdd() {
            setEditing(null);
            setMFile(null); setMPreview(null); setMClearPreview(false);
            setMVariationId('');
            setMReadme('');
            setMPosition(files.length);
            setMStatus('active');
            setShowModal(true);
        }
        function openEdit(row) {
            setEditing(row);
            setMFile(null); setMPreview(null); setMClearPreview(false);
            setMVariationId(row.variation_id ? String(row.variation_id) : '');
            setMReadme(row.readme || '');
            setMPosition(row.position || 0);
            setMStatus(row.status || 'active');
            setShowModal(true);
        }
        function closeModal() { if (!busy) setShowModal(false); }

        // Build the multipart FormData body shared by create + replace.
        function buildMultipart() {
            var fd = new FormData();
            if (mFile)    fd.append('file', mFile);
            if (mPreview) fd.append('preview', mPreview);
            if (mVariationId !== '') fd.append('variation_id', mVariationId);
            if (mReadme   !== '')    fd.append('readme', mReadme);
            fd.append('position', String(mPosition || 0));
            fd.append('status', mStatus || 'active');
            return fd;
        }

        // Multipart fetch — bypass apiFetch's JSON-only Content-Type assumption.
        function multipartFetch(path, body) {
            var url = (Config.apiUrl || '/wp-json/titancart/v1') + path;
            if (Config.apiUrl && Config.apiUrl.indexOf('?') !== -1 && path.indexOf('?') !== -1) {
                url = Config.apiUrl + path.replace('?', '&');
            }
            return fetch(url, {
                method: 'POST',
                headers: { 'X-WP-Nonce': Config.nonce || '' },
                body: body,
                credentials: 'same-origin',
            }).then(function(r) {
                return r.text().then(function(text) {
                    var data = null;
                    if (text) { try { data = JSON.parse(text); } catch(_){} }
                    if (!r.ok) {
                        var err = new Error((data && (data.message || data.error)) || ('HTTP ' + r.status));
                        err.status = r.status; err.data = data;
                        throw err;
                    }
                    return data;
                });
            });
        }

        function saveFile() {
            // Create requires a file; edit doesn't (metadata-only is allowed).
            if (!editing && !mFile) {
                flash('Please choose a file to upload.', false);
                return;
            }
            setBusy(true);

            if (!editing) {
                // CREATE — multipart POST to /admin/products/{id}/files
                multipartFetch('/admin/products/' + productId + '/files', buildMultipart())
                    .then(function() { setBusy(false); setShowModal(false); flash('File added.', true); loadFiles(); })
                    .catch(function(e) { setBusy(false); flash(e.message || 'Upload failed.', false); });
                return;
            }

            // EDIT — separate paths for file replacement vs metadata-only.
            var replacingFile    = !!mFile;
            var replacingPreview = !!mPreview;
            var jobs = [];

            if (replacingFile || replacingPreview) {
                // POST /admin/products/{id}/files/{fid}/replace (multipart)
                var rfd = new FormData();
                if (mFile)    rfd.append('file', mFile);
                if (mPreview) rfd.append('preview', mPreview);
                jobs.push(multipartFetch(
                    '/admin/products/' + productId + '/files/' + editing.id + '/replace',
                    rfd
                ));
            }

            // Build metadata patch.
            var patch = {
                variation_id: mVariationId === '' ? null : parseInt(mVariationId, 10),
                readme:       mReadme,
                position:     mPosition,
                status:       mStatus,
            };
            if (mClearPreview && !replacingPreview) {
                patch.preview_path = null;
            }
            jobs.push(apiFetch('/admin/products/' + productId + '/files/' + editing.id, {
                method: 'PUT', body: patch
            }));

            Promise.all(jobs)
                .then(function() { setBusy(false); setShowModal(false); flash('File updated.', true); loadFiles(); })
                .catch(function(e) { setBusy(false); flash(e.message || 'Update failed.', false); });
        }

        function deleteFile(row) {
            if (!confirm('Delete "' + (row.original_name || row.filename) + '"? This removes the file from disk. ' +
                         (row.keys_active > 0 ? 'There ' + (row.keys_active === 1 ? 'is 1 active key' : 'are ' + row.keys_active + ' active keys') + ' for this file — those will fail validation after deletion.' : ''))) {
                return;
            }
            apiFetch('/admin/products/' + productId + '/files/' + row.id, { method: 'DELETE' })
                .then(function() { flash('File deleted.', true); loadFiles(); })
                .catch(function(e) { flash(e.message || 'Delete failed.', false); });
        }

        function moveFile(row, direction) {
            var idx = files.findIndex(function(f) { return f.id === row.id; });
            if (idx < 0) return;
            var newIdx = direction === 'up' ? idx - 1 : idx + 1;
            if (newIdx < 0 || newIdx >= files.length) return;
            var next = files.slice();
            next.splice(newIdx, 0, next.splice(idx, 1)[0]);
            setFiles(next); // optimistic
            apiFetch('/admin/products/' + productId + '/files/reorder', {
                method: 'PUT',
                body: { order: next.map(function(f){ return f.id; }) }
            }).catch(function(e) { flash(e.message || 'Reorder failed.', false); loadFiles(); });
        }

        function testDownload(row) {
            // Test-download is a direct stream — open in new tab. The route is
            // gated by manage_options on the server. We don't append the nonce
            // because the user is already authenticated via WP cookies.
            var base = (Config.apiUrl || '/wp-json/titancart/v1');
            var url  = base + '/admin/products/' + productId + '/files/' + row.id + '/test-download';
            window.open(url, '_blank');
        }

        // ── Per-product overrides card ────────────────────────────────────────
        function renderOverrides() {
            var labelSty   = { fontSize: 11, fontWeight: 600, color: mutedColor, textTransform: 'uppercase', letterSpacing: '0.5px', marginBottom: 6, display: 'block' };
            var inputSty   = { width: '100%', padding: '8px 10px', fontSize: 13, borderRadius: 6, border: '1px solid ' + borderColor, background: dark ? '#0A0A0A' : '#FFFFFF', color: textColor, boxSizing: 'border-box' };
            var hintSty    = { fontSize: 11, color: mutedColor, marginTop: 6, lineHeight: 1.5 };

            return h('div', {
                style: { background: cardBg, border: '1px solid ' + borderColor, borderRadius: 10, padding: S.xl, marginBottom: S.lg }
            },
                h('div', { style: { marginBottom: S.lg } },
                    h('h3', { style: { fontSize: 15, fontWeight: 600, marginBottom: 4 } }, 'Per-product overrides'),
                    h('p', { style: { fontSize: 12, color: mutedColor, margin: 0 } },
                        'These override the global Download settings (Settings → General → Catalog) for this product only. Leave blank to inherit.')
                ),
                h('div', { style: { display: 'grid', gridTemplateColumns: '1fr 1fr', gap: S.lg } },
                    h('div', null,
                        h('label', { style: labelSty }, 'Download link TTL (hours)'),
                        h('input', {
                            type: 'number', min: '0',
                            style: inputSty,
                            value: form.download_ttl_hours,
                            placeholder: 'Use global setting',
                            onChange: function(e) { sf('download_ttl_hours', e.target.value); }
                        }),
                        h('div', { style: hintSty },
                            'How many hours after purchase the download link works. Blank = inherit global. ',
                            'Enter 0 or a negative number to mean ', h('em', null, 'never expires'), '.')
                    ),
                    h('div', null,
                        h('label', { style: labelSty }, 'Max downloads per key'),
                        h('input', {
                            type: 'number', min: '0',
                            style: inputSty,
                            value: form.download_max_count,
                            placeholder: 'Unlimited',
                            onChange: function(e) { sf('download_max_count', e.target.value); }
                        }),
                        h('div', { style: hintSty },
                            'How many times each customer can download a file before the link is exhausted. ',
                            'Blank or 0 = unlimited.')
                    )
                ),
                h('div', { style: { marginTop: S.md, padding: S.md, background: dark ? '#171717' : '#F9FAFB', borderRadius: 8, fontSize: 12, color: mutedColor } },
                    h('strong', { style: { color: textColor } }, 'Reminder: '),
                    'Per-product override values are saved when you click ',
                    h('strong', { style: { color: textColor } }, 'Save'),
                    ' on the product editor — not when you change a file in the list below.')
            );
        }

        // ── Files list card ───────────────────────────────────────────────────
        function renderFilesList() {
            var headerSty = {
                display: 'grid',
                gridTemplateColumns: '36px 2.4fr 1fr 0.8fr 0.7fr 1.1fr 1.4fr',
                padding: '10px 16px', borderBottom: '1px solid ' + borderColor,
                background: tableHeaderBg, gap: S.sm, alignItems: 'center',
            };
            var rowSty = {
                display: 'grid',
                gridTemplateColumns: '36px 2.4fr 1fr 0.8fr 0.7fr 1.1fr 1.4fr',
                padding: '12px 16px', borderBottom: '1px solid ' + borderColor,
                gap: S.sm, alignItems: 'center',
            };
            var thSty = { fontSize: 10, fontWeight: 600, color: mutedColor, textTransform: 'uppercase', letterSpacing: '0.5px' };

            return h('div', {
                style: { background: cardBg, border: '1px solid ' + borderColor, borderRadius: 10, overflow: 'hidden' }
            },
                h('div', { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: S.xl } },
                    h('div', null,
                        h('h3', { style: { fontSize: 15, fontWeight: 600, marginBottom: 4 } }, 'Files'),
                        h('p', { style: { fontSize: 12, color: mutedColor, margin: 0 } },
                            'Files customers receive after a paid order. ' +
                            'Each customer gets a unique signed download link that respects the TTL and max-downloads settings above.')
                    ),
                    h(Btn, { variant: 'primary', onClick: openAdd, mutedColor: mutedColor, borderColor: borderColor }, '+ Add file')
                ),

                msg && h('div', {
                    style: {
                        margin: '0 ' + S.xl + 'px ' + S.md + 'px',
                        padding: '10px 14px', borderRadius: 8, fontSize: 13,
                        background: msg.ok ? (dark?'rgba(22,163,74,.12)':'#ECFDF5') : (dark?'rgba(220,38,38,.12)':'#FEF2F2'),
                        color: msg.ok ? '#16A34A' : '#DC2626',
                    }
                }, msg.text),

                loading
                    ? h('div', { style: { padding: S.xxl, textAlign: 'center' } }, h(Spinner, { mutedColor: mutedColor }))
                    : (files.length === 0
                        ? h('div', { style: { padding: S.xxl + 'px ' + S.xl + 'px', textAlign: 'center', borderTop: '1px solid ' + borderColor } },
                            h('p', { style: { fontSize: 14, color: textColor, marginBottom: 6 } }, 'No files uploaded yet.'),
                            h('p', { style: { fontSize: 12, color: mutedColor, margin: 0 } }, 'Click "Add file" to upload a downloadable file for this product.')
                        )
                        : h('div', null,
                            // Header row
                            h('div', { style: headerSty },
                                h('span', { style: thSty }, '#'),
                                h('span', { style: thSty }, 'Name'),
                                h('span', { style: thSty }, 'Size'),
                                h('span', { style: thSty }, 'Scope'),
                                h('span', { style: thSty }, 'Status'),
                                h('span', { style: thSty }, 'Stats'),
                                h('span', { style: Object.assign({}, thSty, { textAlign: 'right' }) }, 'Actions')
                            ),
                            files.map(function(f, idx) {
                                var statusBg = f.status === 'active'
                                    ? (dark ? 'rgba(22,163,74,.12)' : '#ECFDF5')
                                    : (dark ? 'rgba(115,115,115,.18)' : '#F4F4F5');
                                var statusFg = f.status === 'active' ? '#16A34A' : mutedColor;

                                return h('div', { key: f.id, style: rowSty },
                                    // Position controls
                                    h('div', { style: { display: 'flex', flexDirection: 'column', gap: 2 } },
                                        h('button', {
                                            onClick: function(){ moveFile(f, 'up'); },
                                            disabled: idx === 0,
                                            style: { background:'none', border:'none', cursor: idx===0?'default':'pointer', color: idx===0?borderColor:mutedColor, fontSize: 11, padding: 0, lineHeight: 1 }
                                        }, '▲'),
                                        h('button', {
                                            onClick: function(){ moveFile(f, 'down'); },
                                            disabled: idx === files.length - 1,
                                            style: { background:'none', border:'none', cursor: idx===files.length-1?'default':'pointer', color: idx===files.length-1?borderColor:mutedColor, fontSize: 11, padding: 0, lineHeight: 1 }
                                        }, '▼')
                                    ),
                                    // Name
                                    h('div', null,
                                        h('div', { style: { fontSize: 13, fontWeight: 500, color: textColor } }, f.original_name || f.filename),
                                        h('div', { style: { fontSize: 11, color: mutedColor, marginTop: 2 } }, f.mime_type || '—'),
                                        f.preview_path && h('div', { style: { fontSize: 10, color: TC.accent, marginTop: 2, fontWeight: 500 } }, '↳ has preview file')
                                    ),
                                    // Size
                                    h('span', { style: { fontSize: 12, color: mutedColor } }, fmtBytes(f.file_size)),
                                    // Scope
                                    h('span', { style: { fontSize: 12, color: f.variation_id ? TC.accent : mutedColor, fontWeight: f.variation_id ? 500 : 400 } },
                                        f.variation_id ? ('Variation #' + f.variation_id) : 'All variations'),
                                    // Status pill
                                    h('span', { style: { fontSize: 10, fontWeight: 600, color: statusFg, background: statusBg, padding: '3px 8px', borderRadius: 4, width: 'fit-content', textTransform: 'capitalize' } }, f.status),
                                    // Stats
                                    h('div', { style: { fontSize: 11, color: mutedColor, lineHeight: 1.5 } },
                                        f.keys_issued + ' keys issued',
                                        h('br', null),
                                        f.downloads_total + ' downloads' +
                                            (f.keys_active ? (' · ' + f.keys_active + ' active') : '')
                                    ),
                                    // Actions
                                    h('div', { style: { display: 'flex', gap: S.xs, justifyContent: 'flex-end' } },
                                        h('button', { onClick: function(){ testDownload(f); },
                                            style: { background:'none', border:'1px solid '+borderColor, padding:'5px 10px', borderRadius:6, fontSize:11, color: mutedColor, cursor:'pointer' } },
                                            'Test'),
                                        h('button', { onClick: function(){ openEdit(f); },
                                            style: { background:'none', border:'1px solid '+borderColor, padding:'5px 10px', borderRadius:6, fontSize:11, color: textColor, cursor:'pointer' } },
                                            'Edit'),
                                        h('button', { onClick: function(){ deleteFile(f); },
                                            style: { background:'none', border:'1px solid '+borderColor, padding:'5px 10px', borderRadius:6, fontSize:11, color: TC.error, cursor:'pointer' } },
                                            'Delete')
                                    )
                                );
                            })
                        )
                    )
            );
        }

        // ── New / Edit file modal ─────────────────────────────────────────────
        function renderModal() {
            if (!showModal) return null;
            var isEdit = !!editing;
            var labelSty = { fontSize: 11, fontWeight: 600, color: mutedColor, textTransform: 'uppercase', letterSpacing: '0.5px', marginBottom: 6, display: 'block' };
            var inputSty = { width: '100%', padding: '8px 10px', fontSize: 13, borderRadius: 6, border: '1px solid ' + borderColor, background: dark ? '#0A0A0A' : '#FFFFFF', color: textColor, boxSizing: 'border-box' };
            var hintSty  = { fontSize: 11, color: mutedColor, marginTop: 4 };

            return h('div', {
                style: {
                    position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.5)',
                    display: 'flex', alignItems: 'center', justifyContent: 'center',
                    zIndex: 9999, padding: S.lg,
                },
                onClick: closeModal,
            },
                h('div', {
                    style: {
                        background: cardBg, borderRadius: 12, width: '100%', maxWidth: 560,
                        maxHeight: '90vh', overflow: 'auto',
                        boxShadow: '0 25px 50px -12px rgba(0,0,0,0.4)',
                    },
                    onClick: function(e){ e.stopPropagation(); }
                },
                    // Header
                    h('div', { style: { padding: S.xl, borderBottom: '1px solid ' + borderColor } },
                        h('h3', { style: { fontSize: 17, fontWeight: 600, margin: 0 } }, isEdit ? 'Edit file' : 'Add file'),
                        h('p', { style: { fontSize: 12, color: mutedColor, margin: '4px 0 0' } },
                            isEdit ? 'Replace the underlying file or update its metadata.' : 'Upload a file to deliver after paid orders.')
                    ),

                    // Body
                    h('div', { style: { padding: S.xl, display: 'flex', flexDirection: 'column', gap: S.lg } },

                        // Main file picker
                        h('div', null,
                            h('label', { style: labelSty }, isEdit ? 'Replace file (optional)' : 'File *'),
                            h('input', {
                                ref: fileInputRef, type: 'file',
                                onChange: function(e){ setMFile(e.target.files && e.target.files[0] ? e.target.files[0] : null); },
                                style: { fontSize: 13, color: textColor }
                            }),
                            isEdit && h('div', { style: hintSty },
                                'Current: ', h('strong', { style: { color: textColor } }, editing.original_name || editing.filename),
                                ' (', fmtBytes(editing.file_size), '). Leave blank to keep this file.'),
                            mFile && h('div', { style: Object.assign({}, hintSty, { color: TC.accent, fontWeight: 500 }) },
                                'New: ' + mFile.name + ' (' + fmtBytes(mFile.size) + ')')
                        ),

                        // Preview file picker
                        h('div', null,
                            h('label', { style: labelSty }, 'Preview file (optional)'),
                            h('input', {
                                ref: previewInputRef, type: 'file',
                                onChange: function(e){ setMPreview(e.target.files && e.target.files[0] ? e.target.files[0] : null); setMClearPreview(false); },
                                style: { fontSize: 13, color: textColor }
                            }),
                            h('div', { style: hintSty },
                                'A free preview/sample (e.g. first chapter, low-res audio clip) shown to non-purchasers. Phase 5 will surface this on the product page.'),
                            isEdit && editing.preview_path && !mPreview && h('label', {
                                style: { display: 'flex', alignItems: 'center', gap: 6, fontSize: 12, marginTop: 6, cursor: 'pointer', color: TC.error }
                            },
                                h('input', {
                                    type: 'checkbox',
                                    checked: mClearPreview,
                                    onChange: function(e){ setMClearPreview(!!e.target.checked); }
                                }),
                                'Remove the existing preview file'
                            )
                        ),

                        // Variation scope
                        h('div', null,
                            h('label', { style: labelSty }, 'Applies to'),
                            h('select', {
                                style: Object.assign({}, inputSty, { cursor: 'pointer' }),
                                value: mVariationId,
                                onChange: function(e){ setMVariationId(e.target.value); }
                            },
                                h('option', { value: '' }, 'All variations (product-level)')
                                // Phase 2 keeps this simple: only "all variations" surfaces in the picker.
                                // Per-variation files are still supported by the schema and validate at issuance —
                                // a future enhancement will populate variation rows here.
                            ),
                            h('div', { style: hintSty },
                                'Variation-specific scoping is coming in a follow-up. For now, files attach at the product level and every variation inherits them.')
                        ),

                        // Readme
                        h('div', null,
                            h('label', { style: labelSty }, 'Admin notes (optional)'),
                            h('textarea', {
                                rows: 2,
                                style: Object.assign({}, inputSty, { fontFamily: 'inherit', resize: 'vertical' }),
                                value: mReadme,
                                placeholder: 'Internal note — not shown to customers.',
                                onChange: function(e){ setMReadme(e.target.value); }
                            })
                        ),

                        // Position + status (side-by-side)
                        h('div', { style: { display: 'grid', gridTemplateColumns: '1fr 1fr', gap: S.md } },
                            h('div', null,
                                h('label', { style: labelSty }, 'Position'),
                                h('input', {
                                    type: 'number', min: '0',
                                    style: inputSty,
                                    value: mPosition,
                                    onChange: function(e){ setMPosition(parseInt(e.target.value, 10) || 0); }
                                })
                            ),
                            h('div', null,
                                h('label', { style: labelSty }, 'Status'),
                                h('select', {
                                    style: Object.assign({}, inputSty, { cursor: 'pointer' }),
                                    value: mStatus,
                                    onChange: function(e){ setMStatus(e.target.value); }
                                },
                                    h('option', { value: 'active' }, 'Active'),
                                    h('option', { value: 'disabled' }, 'Disabled')
                                )
                            )
                        )
                    ),

                    // Footer
                    h('div', { style: { padding: S.xl, borderTop: '1px solid ' + borderColor, display: 'flex', gap: S.sm, justifyContent: 'flex-end' } },
                        h(Btn, { onClick: closeModal, mutedColor: mutedColor, borderColor: borderColor, disabled: busy }, 'Cancel'),
                        h(Btn, { variant: 'primary', onClick: saveFile, mutedColor: mutedColor, borderColor: borderColor, disabled: busy },
                            busy ? 'Saving…' : (isEdit ? 'Save changes' : 'Add file'))
                    )
                )
            );
        }

        return h('div', null,
            renderOverrides(),
            renderFilesList(),
            renderModal()
        );
    }

    /* ── PRODUCT EDITOR PAGE ───────────────────────────────────────── */
    function ProductEditorPage({ productId, dark, cardBg, textColor, mutedColor, borderColor, tableHeaderBg }) {
        const isNew = !productId || productId === 'new';
        const numId = isNew ? null : parseInt(productId);

        // B3.5c (S182) — Vendor-mode gate. When the SPA is mounted in vendor
        // mode (Config.spa_mode === 'vendor'), the editor:
        //   - Routes all reads/writes through /me/vendor/catalog (marketplace REST)
        //     instead of TC core /products (admin REST).
        //   - Forces form.status to 'hidden' (canonical draft) and hides the
        //     status radio. Server-side coercion in VendorSelfController is
        //     the source of truth; this is UX consistency, not defense.
        //   - Routes header back/cancel + post-create navigation to
        //     'vendor-products' instead of 'products'.
        // Tabs, sections, and field layout are otherwise identical between
        // modes. Admin-eligible vendors (including 'pending') reach this
        // editor via the vendor-products SPA route.
        const isVendor = (window.TitanCartConfig || {}).spa_mode === 'vendor';
        const listRoute   = isVendor ? 'vendor-products' : 'products';
        const editorFetch = isVendor ? apiFetchVendor : apiFetch;
        const apiBase     = isVendor ? '/me/vendor/catalog' : '/products';

        // Guard: if someone navigated with a non-numeric ID (e.g. slug), redirect to list
        if (!isNew && (numId === null || isNaN(numId))) {
            return h('div', { style:{ padding:32, textAlign:'center' } },
                h('p', { style:{ fontSize:14, marginBottom:16 } }, 'Invalid product ID: "' + productId + '"'),
                h('button', { onClick:function(){ navigate(listRoute); },
                    style:{ padding:'8px 20px', borderRadius:8, border:'none', background:TC.accent, color:'#fff', fontSize:13, cursor:'pointer' } }, '\u2190 Back to Products')
            );
        }

        const _savedTab = (function() { try { return sessionStorage.getItem('tc_ed_tab_' + (numId||'new')) || 'general'; } catch(e) { return 'general'; } })();
        const [tab,     setTab_]    = useState(_savedTab);
        function setTab(v) { setTab_(v); try { sessionStorage.setItem('tc_ed_tab_' + (numId||'new'), v); } catch(e) {} }
        const [saving,  setSaving]  = useState(false);
        const [loading, setLoading] = useState(!isNew);
        const [msg,     setMsg]     = useState({ text:'', ok:true });
        const [cats,    setCats]    = useState([]);
        const [taxClasses, setTaxClasses] = useState([]);
        const [images,  setImages]  = useState([]);
        const [imgUploading, setImgUploading] = useState(false);
        // S308 — storefront permalink for the "Preview" button (saved products only).
        // Set from the GET payload's `url` on load and refreshed from each save
        // response so a slug edit keeps the preview link correct.
        const [previewUrl, setPreviewUrl] = useState('');
        const [catSearch,   setCatSearch]     = useState('');
        const [showCatDrop, setShowCatDrop]   = useState(false);
        const [showCatModal,setShowCatModal]  = useState(false);
        // Temp selection inside the modal (committed on Save)
        const [modalSel,    setModalSel]      = useState([]);
        // Modal tree expand state
        const [catModalExp, setCatModalExp]    = useState(new Set());
        function openCatModal() {
            setModalSel([...form.category_ids]);
            setShowCatModal(true);
        }
        function toggleModalCat(cid) {
            var id = Number(cid);
            setModalSel(function(prev){ return prev.includes(id) ? prev.filter(function(c){ return c!==id; }) : prev.concat([id]); });
        }
        function saveCatModal() {
            sf('category_ids', modalSel);
            setShowCatModal(false);
        }

        // Collapsible section open state
        const [open, setOpen] = useState({ info:true, pricing:true, options:false, perunit:false, availability:false, extra:false, seo_name:true, seo_meta:true, seo_preview:true });

        // Videos state (array of { url, hosting })
        const [videos, setVideos] = useState([]);
        const VIDEO_HOSTINGS = ['YouTube','Vimeo','Dailymotion','Wistia','HTML5 video'];

        function addVideo() { setVideos(prev => [...prev, { url:'', hosting:'YouTube' }]); }
        function setVideoField(i, k, v) { setVideos(prev => prev.map((row, idx) => idx===i ? Object.assign({},row,{[k]:v}) : row)); }
        function removeVideo(i) { setVideos(prev => prev.filter((_,idx)=>idx!==i)); }
        function toggleSection(k) { setOpen(prev => Object.assign({}, prev, { [k]: !prev[k] })); }

        // Bulk Buys state — one default empty row
        const QTY_TYPES = [
            { v:'absolute', l:'Absolute (' + tcMoneySymbol() + ')' },
            { v:'percent',  l:'Percent (%)' },
        ];
        const QTY_GROUPS = ['All','Guests','Registered users'];
        const defaultQtyRow = function() { return { qty:'', value:'0.00', type:'absolute', usergroup:'All', checked:false }; };
        const [qtyDiscounts, setQtyDiscounts] = useState([ defaultQtyRow() ]);
        function qtySet(i, k, v) { setQtyDiscounts(prev => prev.map((r,idx) => idx===i ? Object.assign({},r,{[k]:v}) : r)); }
        function qtyAdd() { setQtyDiscounts(prev => [...prev, defaultQtyRow()]); }
        function qtyClone(i) { setQtyDiscounts(prev => { const c=Object.assign({},prev[i],{checked:false}); const n=[...prev]; n.splice(i+1,0,c); return n; }); }
        function qtyDelete(i) { setQtyDiscounts(prev => prev.length===1 ? [defaultQtyRow()] : prev.filter((_,idx)=>idx!==i)); }
        function qtyToggle(i) { setQtyDiscounts(prev => prev.map((r,idx) => idx===i ? Object.assign({},r,{checked:!r.checked}) : r)); }
        // S120: Map editor's hardcoded usergroup labels ↔ DB integer IDs.
        // Reads TitanCartConfig.usergroupIds (localized server-side via Admin/Assets.php).
        // 'All' → 0 (system-wide "all customers" group); other labels resolve via handle.
        // If a handle isn't in the map (DB hasn't seeded that usergroup yet), we fall back
        // to 0 so the row still saves under "All" instead of failing silently.
        function ugLabelToId(label) {
            const map = (window.TitanCartConfig && window.TitanCartConfig.usergroupIds) || {};
            if (label === 'Guests') return map.guest || 0;
            if (label === 'Registered users') return map.customer || 0;
            return 0; // 'All' or anything unrecognized
        }
        function ugIdToLabel(id) {
            const map = (window.TitanCartConfig && window.TitanCartConfig.usergroupIds) || {};
            const intId = parseInt(id, 10) || 0;
            if (intId === 0) return 'All';
            if (map.guest && intId === map.guest) return 'Guests';
            if (map.customer && intId === map.customer) return 'Registered users';
            return 'All'; // unknown ID falls back to All so editor stays usable
        }


        // ── Product Attributes state ─────────────────────────────────────────
        const [attrList,      setAttrList]      = useState([]);   // applicable attributes with current values
        const [attrLoading,   setAttrLoading]   = useState(false);
        const [attrSaving,    setAttrSaving]    = useState(false);
        const [attrValues,    setAttrValues]    = useState({});   // { feature_id: { variant_id, value_str, ... } }
        const [attrMsg,       setAttrMsg]       = useState({ text:'', ok:true });
        // Quick-add attribute state
        const [attrQuickShow, setAttrQuickShow] = useState(false);
        const [attrQuickForm, setAttrQuickForm] = useState({ name:'', choices:'', category_id:'' });
        const [attrQuickSaving, setAttrQuickSaving] = useState(false);

        function attrLoad() {
            if (!numId) return;
            setAttrLoading(true);
            apiFetch('/products/' + numId + '/attributes').then(function(d) {
                var list = Array.isArray(d) ? d : [];
                setAttrList(list);
                // Build values map from current values
                var vals = {};
                list.forEach(function(attr) {
                    if (attr.value) {
                        vals[attr.id] = attr.value;
                    }
                });
                setAttrValues(vals);
                setAttrLoading(false);
            }).catch(function(){ setAttrLoading(false); });
        }
        function attrSetValue(featureId, key, val) {
            setAttrValues(function(prev) {
                var cur = Object.assign({}, prev[featureId] || {});
                cur[key] = val;
                var n = Object.assign({}, prev);
                n[featureId] = cur;
                return n;
            });
        }
        function attrSave() {
            if (!numId) return;
            setAttrSaving(true); setAttrMsg({ text:'', ok:true });
            var values = [];
            Object.keys(attrValues).forEach(function(fid) {
                var v = attrValues[fid];
                if (!v) return;
                values.push(Object.assign({ feature_id: parseInt(fid) }, v));
            });
            apiFetch('/products/' + numId + '/attributes', { method:'PUT', body:{ values:values } })
                .then(function() { setAttrSaving(false); setAttrMsg({ text:'Saved', ok:true }); })
                .catch(function() { setAttrSaving(false); setAttrMsg({ text:'Save failed', ok:false }); });
        }
        function attrQuickCreate() {
            if (!attrQuickForm.name.trim()) return;
            setAttrQuickSaving(true);
            // Quick-create: create attribute via global API, then reload
            var choiceNames = attrQuickForm.choices.split(',').map(function(s){ return s.trim(); }).filter(Boolean);
            var body = {
                name: attrQuickForm.name.trim(),
                feature_type: choiceNames.length > 0 ? 'S' : 'T',
                purpose: 'filter',
                filterable: 1,
                status: 'active'
            };
            apiFetch('/product-features', { method:'POST', body:body }).then(function(feat) {
                var fid = feat.feature_id || feat.id;
                // Save choices if any
                var promises = [];
                if (choiceNames.length > 0 && fid) {
                    promises.push(apiFetch('/product-features/' + fid + '/variants', {
                        method:'POST', body:{ variants: choiceNames.map(function(n,i){ return { name:n, position:i }; }) }
                    }));
                }
                // Assign to product's categories if selected
                if (attrQuickForm.category_id && fid) {
                    promises.push(apiFetch('/product-features/' + fid + '/categories', {
                        method:'POST', body:{ category_ids: [parseInt(attrQuickForm.category_id)] }
                    }));
                }
                Promise.all(promises).then(function() {
                    setAttrQuickSaving(false); setAttrQuickShow(false);
                    setAttrQuickForm({ name:'', choices:'', category_id:'' });
                    attrLoad(); // refresh
                }).catch(function(){ setAttrQuickSaving(false); });
            }).catch(function(){ setAttrQuickSaving(false); });
        }

        // ── Product Group state ──────────────────────────────────────────────
        const [pgGroup,     setPgGroup]     = useState(null);
        const [pgMembers,   setPgMembers]   = useState([]);
        const [pgLoading,   setPgLoading]   = useState(false);
        const [pgShowModal, setPgShowModal] = useState(false);
        const [pgModalTab,  setPgModalTab]  = useState('create');
        const [pgError,     setPgError]     = useState('');
        // Multi-attribute create state
        const [pgFeatures,  setPgFeatures]  = useState([]);      // variation features with _parentVariantId + _choices
        const [pgFeatLoad,  setPgFeatLoad]  = useState(false);
        const [pgSelections, setPgSelections] = useState({});    // { featureId: [variantId, ...] }
        const [pgCreating,  setPgCreating]  = useState(false);
        // Existing tab
        const [pgSearch,    setPgSearch]    = useState('');
        const [pgSearchRes, setPgSearchRes] = useState([]);
        const [pgSearching, setPgSearching] = useState(false);
        // Existing-tab variant assignment: which product is being assigned + its picked combination
        const [pgAddPid,    setPgAddPid]    = useState(null);   // product_id pending assignment
        const [pgAddCombo,  setPgAddCombo]  = useState({});     // { featureId: variantId }
        const [pgAddNeed,   setPgAddNeed]   = useState([]);     // feature ids the product lacks (prompt only these)
        const [pgAdding,    setPgAdding]    = useState(false);
        // Group code editing
        const [pgCodeEdit, setPgCodeEdit] = useState('');
        const [pgCodeSaving, setPgCodeSaving] = useState(false);
        // roadmap #15: display-as-single-product toggle + optional display name
        const [pgDisplaySingle, setPgDisplaySingle] = useState(false);
        const [pgDisplayName,   setPgDisplayName]   = useState('');
        const [pgDisplaySaving, setPgDisplaySaving] = useState(false);
        // S478: attribute pricing in the Create tab — { featureId: { variantId: 'amount' } }
        // and the mode: 'add' (amount is added to this product's price) or 'set'
        // (amount IS the variation's price). Blank = no change.
        const [pgPrices,    setPgPrices]    = useState({});
        const [pgPriceMode, setPgPriceMode] = useState('add');
        // S478 (Sean): clone the parent's description / image gallery onto every new variation.
        const [pgCopyDesc,   setPgCopyDesc]   = useState(true);
        const [pgCopyImages, setPgCopyImages] = useState(true);
        // S478: inline member edits (price / qty / code) — pid currently saving.
        const [pgMemberSaving, setPgMemberSaving] = useState(null);

        function pgLoad() {
            if (!numId) return;
            setPgLoading(true);
            apiFetch('/products/' + numId + '/group').then(function(d) {
                setPgGroup(d.group || null);
                setPgMembers(d.members || []);
                setPgCodeEdit(d.group ? d.group.group_code : '');
                setPgDisplaySingle(d.group ? !!Number(d.group.display_as_single) : false);
                setPgDisplayName(d.group && d.group.display_name ? d.group.display_name : '');
                setPgLoading(false);
            }).catch(function(err){
                console.error('[TitanCart] pgLoad error:', err);
                setPgLoading(false);
            });
        }
        function pgSaveCode() {
            if (!numId || !pgCodeEdit.trim()) return;
            setPgCodeSaving(true);
            apiFetch('/products/' + numId + '/group', {
                method:'PUT', body:{ group_code: pgCodeEdit.trim() }
            }).then(function(d) {
                setPgCodeSaving(false);
                if (d && d.error) { alert(d.error); }
                else { setPgGroup(function(prev) { return prev ? Object.assign({}, prev, { group_code: pgCodeEdit.trim() }) : prev; }); }
            }).catch(function(err) {
                setPgCodeSaving(false);
                alert((err && err.message) || 'Failed to save group code.');
            });
        }
        // roadmap #15: persist the display-as-single toggle + optional display name.
        function pgSaveDisplay(nextSingle, nextName) {
            if (!numId) return;
            var single = (typeof nextSingle === 'boolean') ? nextSingle : pgDisplaySingle;
            var name   = (typeof nextName === 'string') ? nextName : pgDisplayName;
            setPgDisplaySaving(true);
            apiFetch('/products/' + numId + '/group', {
                method:'PUT', body:{ display_as_single: single ? 1 : 0, display_name: name }
            }).then(function(d) {
                setPgDisplaySaving(false);
                if (d && d.error) { alert(d.error); return; }
                setPgGroup(function(prev){ return prev ? Object.assign({}, prev, { display_as_single: single ? 1 : 0, display_name: name }) : prev; });
            }).catch(function(err) {
                setPgDisplaySaving(false);
                alert((err && err.message) || 'Failed to save display settings.');
            });
        }
        function pgOpenModal() {
            setPgShowModal(true); setPgModalTab('create'); setPgError('');
            setPgSelections({}); setPgAddPid(null); setPgAddCombo({}); setPgAddNeed([]);
            setPgPrices({}); setPgPriceMode('add');
            // Load variation features where product has a saved value + their choices
            setPgFeatLoad(true);
            Promise.all([
                apiFetch('/product-features?search='),
                apiFetch('/products/' + numId + '/attributes')
            ]).then(function(results) {
                var allFeatures = Array.isArray(results[0]) ? results[0] : [];
                var prodAttrs   = Array.isArray(results[1]) ? results[1] : [];
                // Only variation features where product has a saved variant_id
                var variationFeats = allFeatures.filter(function(f) {
                    if (f.purpose !== 'variation_split' && f.purpose !== 'variation_shared') return false;
                    var fid = Number(f.id || f.feature_id);
                    var attr = prodAttrs.find(function(a) { return Number(a.id) === fid; });
                    return attr && attr.value && attr.value.variant_id;
                }).map(function(f) {
                    var fid = Number(f.id || f.feature_id);
                    var attr = prodAttrs.find(function(a) { return Number(a.id) === fid; });
                    return Object.assign({}, f, { _parentVariantId: Number(attr.value.variant_id) });
                });
                // Load choices for all of them in parallel
                Promise.all(variationFeats.map(function(f) {
                    var fid = f.id || f.feature_id;
                    return apiFetch('/product-features/' + fid + '/variants').then(function(d) {
                        return Object.assign({}, f, { _choices: Array.isArray(d) ? d : [] });
                    }).catch(function(){ return Object.assign({}, f, { _choices: [] }); });
                })).then(function(featsWithChoices) {
                    setPgFeatures(featsWithChoices);
                    setPgFeatLoad(false);
                });
            }).catch(function(){ setPgFeatLoad(false); });
        }
        function pgToggleChoice(fid, vid) {
            fid = Number(fid); vid = Number(vid);
            // Don't toggle the parent's own variant
            var feat = pgFeatures.find(function(f){ return Number(f.id||f.feature_id) === fid; });
            if (feat && feat._parentVariantId === vid) return;
            setPgSelections(function(prev) {
                var cur = (prev[fid] || []).slice();
                var idx = cur.indexOf(vid);
                if (idx >= 0) cur.splice(idx, 1); else cur.push(vid);
                var n = Object.assign({}, prev);
                if (cur.length > 0) n[fid] = cur; else delete n[fid];
                return n;
            });
        }
        function pgAddAllChoices(fid) {
            fid = Number(fid);
            var feat = pgFeatures.find(function(f){ return Number(f.id||f.feature_id) === fid; });
            if (!feat || !feat._choices) return;
            var all = feat._choices.map(function(c){ return Number(c.id); }).filter(function(vid){ return vid !== feat._parentVariantId; });
            setPgSelections(function(prev) {
                var n = Object.assign({}, prev);
                n[fid] = all;
                return n;
            });
        }
        // Compute cartesian product for preview — includes parent's variants automatically
        function pgGetCombinations() {
            // Build the full variant set per feature: user selections + parent's own variant
            var featsWithSelections = pgFeatures.filter(function(f) {
                var fid = String(f.id || f.feature_id);
                return pgSelections[fid] && pgSelections[fid].length > 0;
            });
            if (featsWithSelections.length === 0) return [];

            var combos = [{}];
            featsWithSelections.forEach(function(feat) {
                var fid = String(feat.id || feat.feature_id);
                var userVids = pgSelections[fid] || [];
                // Merge in the parent's variant so cartesian includes it
                var allVids = userVids.slice();
                if (feat._parentVariantId && allVids.indexOf(feat._parentVariantId) < 0) {
                    allVids.push(feat._parentVariantId);
                }
                var next = [];
                combos.forEach(function(c) {
                    allVids.forEach(function(vid) {
                        var n = Object.assign({}, c);
                        n[fid] = vid;
                        next.push(n);
                    });
                });
                combos = next;
            });
            // Remove the exact parent combination
            combos = combos.filter(function(c) {
                var isParent = true;
                var keys = Object.keys(c);
                if (keys.length === 0) return false;
                for (var i = 0; i < keys.length; i++) {
                    var fid = keys[i];
                    var feat = pgFeatures.find(function(f){ return String(f.id||f.feature_id) === fid; });
                    if (!feat || Number(c[fid]) !== feat._parentVariantId) { isParent = false; break; }
                }
                return !isParent;
            });
            return combos;
        }
        function pgGetVariantName(vid) {
            vid = Number(vid);
            for (var i = 0; i < pgFeatures.length; i++) {
                var choices = pgFeatures[i]._choices || [];
                for (var j = 0; j < choices.length; j++) {
                    if (Number(choices[j].id) === vid) return choices[j].name || 'V' + vid;
                }
            }
            return 'V' + vid;
        }
        // S478: attribute pricing helpers.
        function pgSetPrice(fid, vid, val) {
            fid = Number(fid); vid = Number(vid);
            setPgPrices(function(prev) {
                var n = Object.assign({}, prev);
                var f = Object.assign({}, n[fid] || {});
                if (val === '' || val == null) delete f[vid]; else f[vid] = val;
                if (Object.keys(f).length) n[fid] = f; else delete n[fid];
                return n;
            });
        }
        function pgPriceAmount(fid, vid) {
            var f = pgPrices[Number(fid)];
            if (!f) return null;
            var v = f[Number(vid)];
            if (v === '' || v == null || isNaN(parseFloat(v))) return null;
            return parseFloat(v);
        }
        // Mirrors ProductsController::create_variations(): 'add' sums the selected
        // variants' amounts onto this product's price; 'set' takes the first
        // feature (in pgFeatures order) whose selected variant has an amount.
        function pgPreviewPrice(combo) {
            var base = parseFloat(form.price) || 0;
            var fids = pgFeatures.map(function(f){ return String(f.id || f.feature_id); });
            if (pgPriceMode === 'set') {
                for (var i = 0; i < fids.length; i++) {
                    if (combo[fids[i]] == null) continue;
                    var amt = pgPriceAmount(fids[i], combo[fids[i]]);
                    if (amt !== null) return Math.max(0, amt);
                }
                return base;
            }
            var delta = 0;
            Object.keys(combo).forEach(function(fid) {
                var a = pgPriceAmount(fid, combo[fid]);
                if (a !== null) delta += a;
            });
            return Math.max(0, base + delta);
        }
        // S478: inline edit of a member's price / qty / code from the table.
        function pgSaveMember(pid, field, value) {
            if (!numId || !pid) return;
            var body = {};
            body[field] = value;
            setPgMemberSaving(pid);
            apiFetch('/products/' + numId + '/group/member/' + pid, { method:'PUT', body: body }).then(function(d) {
                setPgMemberSaving(null);
                if (d && d.error) { alert(d.error); pgLoad(); return; }
                setPgMembers(function(prev) {
                    return prev.map(function(m) {
                        if (Number(m.id || m.product_id) !== pid) return m;
                        return Object.assign({}, m, {
                            price: d.price, list_price: d.list_price, stock_quantity: d.stock_quantity,
                            stock_status: d.stock_status, sku: d.sku, status: d.status
                        });
                    });
                });
            }).catch(function(err) {
                setPgMemberSaving(null);
                alert((err && err.message) || 'Failed to update the variation.');
                pgLoad();
            });
        }
        function pgCreateVariations() {
            var features = [];
            Object.keys(pgSelections).forEach(function(fid) {
                var userVids = pgSelections[fid];
                if (!userVids || userVids.length === 0) return;
                // Include parent's variant so PHP generates full cartesian
                var feat = pgFeatures.find(function(f){ return String(f.id||f.feature_id) === fid; });
                var allVids = userVids.slice();
                if (feat && feat._parentVariantId && allVids.indexOf(feat._parentVariantId) < 0) {
                    allVids.push(feat._parentVariantId);
                }
                // S478: per-variant amounts for this feature (only the filled ones).
                var prices = {};
                allVids.forEach(function(vid) {
                    var a = pgPriceAmount(fid, vid);
                    if (a !== null) prices[vid] = a;
                });
                features.push({ feature_id: Number(fid), variant_ids: allVids, prices: prices });
            });
            if (features.length === 0) return;
            // Keep pgFeatures order so 'set' mode's "first feature wins" matches the preview.
            var order = pgFeatures.map(function(f){ return Number(f.id || f.feature_id); });
            features.sort(function(a, b){ return order.indexOf(a.feature_id) - order.indexOf(b.feature_id); });
            setPgCreating(true); setPgError('');
            apiFetch('/products/' + numId + '/group/create-variations', {
                method:'POST', body:{ features: features, price_mode: pgPriceMode, copy_description: pgCopyDesc ? 1 : 0, copy_images: pgCopyImages ? 1 : 0 }
            }).then(function(d) {
                setPgCreating(false);
                if (d && d.error) { setPgError(d.error); }
                else { setPgShowModal(false); pgLoad(); }
            }).catch(function(err) {
                setPgCreating(false);
                var msg = (err && err.message) || (err && err.data && err.data.error) || 'Failed to create variations.';
                setPgError(msg);
                console.error('pgCreateVariations error:', err);
            });
        }
        function pgSearchProducts(q) {
            setPgSearch(q);
            if (!q.trim()) { setPgSearchRes([]); return; }
            setPgSearching(true);
            apiFetch('/products?search=' + encodeURIComponent(q) + '&per_page=10').then(function(d) {
                var rows = d.products || d;
                setPgSearchRes(Array.isArray(rows) ? rows : []);
                setPgSearching(false);
            }).catch(function(){ setPgSearching(false); });
        }
        function pgAddExisting(pid, combo) {
            setPgAdding(true); setPgError('');
            apiFetch('/products/' + numId + '/group/add-existing', {
                method:'POST', body:{ product_id: pid, combination: combo || {} }
            }).then(function(d) {
                setPgAdding(false);
                if (d && d.error) { setPgError(d.error); return; }
                if (d && d.needs_assignment) {
                    // Product has no saved value for some axis — prompt for just those.
                    setPgAddPid(pid);
                    setPgAddNeed((d.feature_ids || []).map(Number));
                    setPgAddCombo({});
                    return;
                }
                setPgAddPid(null); setPgAddCombo({}); setPgAddNeed([]);
                // Keep the search term + results so multiple products can be added in a
                // row without re-typing. The just-added product flips to "Already in
                // group" automatically once pgLoad refreshes the member list.
                pgLoad();
            }).catch(function(err) {
                setPgAdding(false);
                setPgError((err && err.message) || (err && err.data && err.data.error) || 'Failed to add product.');
            });
        }
        function pgRemove(memberPid) {
            if (!confirm('Remove this product from the group?')) return;
            apiFetch('/products/' + numId + '/group/remove/' + memberPid, { method:'DELETE' })
                .then(function(){ pgLoad(); }).catch(function(){});
        }

        // ── Waitlist state ───────────────────────────────────────────────────
        const [wlSubs,      setWlSubs]      = useState([]);
        const [wlStats,     setWlStats]     = useState({ total:0, waiting:0, notified:0, cancelled:0 });
        const [wlLoading,   setWlLoading]   = useState(false);
        const [wlSearch,    setWlSearch]    = useState('');
        const [wlFilter,    setWlFilter]    = useState('all');
        const [wlSelected,  setWlSelected]  = useState([]);
        const [wlShowAdd,   setWlShowAdd]   = useState(false);
        const [wlAddForm,   setWlAddForm]   = useState({ name:'', email:'' });
        const [wlAddSaving, setWlAddSaving] = useState(false);
        const [wlNotifying, setWlNotifying] = useState(false);

        // S204 — Pattern #151 cascade: native confirm() → ConfirmModal for two
        // destructive actions in this editor (waitlist bulk delete + image
        // delete). Same shape as ProductListPage's confirmDelete state from
        // S183. Modals render as siblings inside PE's outer h('main', ...).
        const [cWlBulk,   setCWlBulk]   = useState({ open:false, busy:false });
        const [cDelImage, setCDelImage] = useState({ open:false, busy:false, imgId:null });

        function wlLoad() {
            if (!numId) return;
            setWlLoading(true);
            apiFetch('/products/' + numId + '/waitlist').then(function(d) {
                setWlSubs(d.subscribers || []);
                setWlStats(d.stats || { total:0, waiting:0, notified:0, cancelled:0 });
                setWlLoading(false);
            }).catch(function(){ setWlLoading(false); });
        }
        function wlDelete(subId) {
            if (!confirm('Remove this subscriber?')) return;
            apiFetch('/products/' + numId + '/waitlist/' + subId, { method:'DELETE' }).then(wlLoad);
        }
        function wlBulkDelete() {
            // S204 — Pattern #151 cascade: native confirm() → ConfirmModal.
            // The actual delete fires from cWlBulk modal's onConfirm via
            // wlBulkDeleteConfirm() so the busy state lives on the modal.
            if (!wlSelected.length) return;
            setCWlBulk({ open:true, busy:false });
        }
        function wlBulkDeleteConfirm() {
            setCWlBulk({ open:true, busy:true });
            apiFetch('/products/' + numId + '/waitlist/bulk-delete', { method:'POST', body:{ ids:wlSelected } })
                .then(function() { setCWlBulk({ open:false, busy:false }); setWlSelected([]); wlLoad(); })
                .catch(function() { setCWlBulk({ open:false, busy:false }); });
        }
        function wlNotifyAll() {
            if (!confirm('Send back-in-stock notification to all waiting subscribers?')) return;
            setWlNotifying(true);
            apiFetch('/products/' + numId + '/waitlist/notify', { method:'POST' })
                .then(function(d) { setWlNotifying(false); alert('Notified ' + (d.notified||0) + ' of ' + (d.total||0) + ' subscribers.'); wlLoad(); })
                .catch(function() { setWlNotifying(false); });
        }
        function wlAddSubscriber() {
            if (!wlAddForm.email.trim()) return;
            setWlAddSaving(true);
            apiFetch('/products/' + numId + '/waitlist', { method:'POST', body:wlAddForm })
                .then(function(d) {
                    setWlAddSaving(false);
                    if (d.error) { alert(d.error); }
                    else { setWlShowAdd(false); setWlAddForm({ name:'', email:'' }); wlLoad(); }
                }).catch(function(err) { setWlAddSaving(false); alert((err&&err.message)||'Failed'); });
        }
        function wlToggleSelect(id) {
            id = Number(id);
            setWlSelected(function(prev) { return prev.includes(id) ? prev.filter(function(x){return x!==id;}) : prev.concat([id]); });
        }
        function wlSelectAll(checked) {
            setWlSelected(checked ? wlFiltered().map(function(s){ return Number(s.id); }) : []);
        }
        function wlFiltered() {
            return wlSubs.filter(function(s) {
                if (wlFilter !== 'all' && s.status !== wlFilter) return false;
                if (wlSearch) {
                    var q = wlSearch.toLowerCase();
                    if ((s.email||'').toLowerCase().indexOf(q)<0 && (s.name||'').toLowerCase().indexOf(q)<0) return false;
                }
                return true;
            });
        }

        // ── Tabs state ──────────────────────────────────────────────────────
        const [tbTabs,       setTbTabs]       = useState([]);
        const [tbSizing,     setTbSizing]     = useState(null);
        const [tbLoading,    setTbLoading]    = useState(false);
        const [tbSaving,     setTbSaving]     = useState(false);
        const [tbDirty,      setTbDirty]      = useState(false);
        const [tbNewTitle,   setTbNewTitle]   = useState('');
        const [tbNewContent, setTbNewContent] = useState('');
        const [tbEditId,     setTbEditId]     = useState(null);
        const [tbEditTitle,  setTbEditTitle]  = useState('');
        const [tbEditContent,setTbEditContent]= useState('');

        function tbLoad() {
            if (!numId) return;
            setTbLoading(true);
            apiFetch('/products/' + numId + '/tabs').then(function(d) {
                setTbTabs(d.tabs || []);
                setTbSizing(d.sizing_chart);
                setTbLoading(false);
            }).catch(function(){ setTbLoading(false); });
        }

        function tbSaveOverrides() {
            if (!numId) return;
            setTbSaving(true);
            var overrides = tbTabs.filter(function(t){ return t.scope === 'global'; }).map(function(t) {
                return { tab_id: Number(t.id), status: t.product_status || 'active' };
            });
            apiFetch('/products/' + numId + '/tabs/overrides', { method:'POST', body:{ overrides:overrides } })
                .then(function() { setTbSaving(false); setTbDirty(false); setMsg({ text:'Tab settings saved.', ok:true }); })
                .catch(function() { setTbSaving(false); });
        }

        function tbToggle(tabId) {
            // Find the tab in current state to compute new status BEFORE setState
            var currentTab = tbTabs.find(function(t){ return Number(t.id) === Number(tabId); });
            if (!currentTab) return;
            var newStatus = currentTab.product_status === 'active' ? 'disabled' : 'active';

            // Update local UI state
            setTbTabs(function(prev) {
                return prev.map(function(t) {
                    if (Number(t.id) !== Number(tabId)) return t;
                    return Object.assign({}, t, { product_status: newStatus });
                });
            });

            // When enabling the sizing chart, initialize default data if none exists
            if (currentTab.tab_key === 'sizing_chart' && newStatus === 'active' && !tbSizing) {
                setTbSizing({
                    product_id: numId,
                    chart_data: {
                        unit: 'in',
                        headers: ['Size', 'Chest', 'Waist', 'Hips'],
                        rows: [
                            ['S',  '34-36', '28-30', '34-36'],
                            ['M',  '38-40', '32-34', '38-40'],
                            ['L',  '42-44', '36-38', '42-44'],
                            ['XL', '46-48', '40-42', '46-48']
                        ]
                    },
                    notes: ''
                });
            }

            // Auto-save override to server immediately
            if (numId && currentTab.scope === 'global') {
                console.log('[TitanCart] Saving tab override:', tabId, newStatus);
                apiFetch('/products/' + numId + '/tabs/overrides', {
                    method:'POST',
                    body:{ overrides:[{ tab_id:Number(tabId), status:newStatus }] }
                }).then(function(d) {
                    console.log('[TitanCart] Tab override saved:', d);
                }).catch(function(err) {
                    console.error('[TitanCart] Tab override save FAILED:', err);
                    setMsg({ text:'Failed to save tab toggle: ' + (err.message||'unknown error'), ok:false });
                });
            }
        }

        function tbAddCustom() {
            if (!tbNewTitle.trim() || !numId) return;
            setTbSaving(true);
            apiFetch('/products/' + numId + '/tabs', { method:'POST', body:{ title:tbNewTitle, content:tbNewContent, position:999 } })
                .then(function() { setTbNewTitle(''); setTbNewContent(''); setTbSaving(false); tbLoad(); })
                .catch(function() { setTbSaving(false); });
        }

        function tbUpdateCustom(tabId) {
            setTbSaving(true);
            apiFetch('/products/' + numId + '/tabs/' + tabId, { method:'PUT', body:{ title:tbEditTitle, content:tbEditContent } })
                .then(function() { setTbEditId(null); setTbSaving(false); tbLoad(); })
                .catch(function() { setTbSaving(false); });
        }

        function tbDeleteCustom(tabId) {
            if (!confirm('Delete this custom tab?')) return;
            apiFetch('/products/' + numId + '/tabs/' + tabId, { method:'DELETE' }).then(tbLoad);
        }

        function tbStartEdit(t) {
            setTbEditId(Number(t.id));
            setTbEditTitle(t.title || '');
            setTbEditContent(t.content || '');
        }

        function tbSaveSizing() {
            if (!numId || !tbSizing) return;
            setTbSaving(true);
            apiFetch('/products/' + numId + '/sizing-chart', { method:'POST', body:{ chart_data:tbSizing.chart_data, notes:tbSizing.notes||'' } })
                .then(function() { setTbSaving(false); setMsg({ text:'Sizing chart saved.', ok:true }); })
                .catch(function() { setTbSaving(false); });
        }

        function tbSzSet(key, val) {
            setTbSizing(function(prev) {
                if (!prev) return prev;
                var cd = Object.assign({}, prev.chart_data || {});
                cd[key] = val;
                return Object.assign({}, prev, { chart_data:cd });
            });
        }
        function tbSzCellSet(ri, ci, val) {
            setTbSizing(function(prev) {
                if (!prev) return prev;
                var cd = Object.assign({}, prev.chart_data || {});
                var rows = (cd.rows || []).map(function(r){ return r.slice(); });
                if (rows[ri]) rows[ri][ci] = val;
                cd.rows = rows;
                return Object.assign({}, prev, { chart_data:cd });
            });
        }
        function tbSzHeaderSet(ci, val) {
            setTbSizing(function(prev) {
                if (!prev) return prev;
                var cd = Object.assign({}, prev.chart_data || {});
                var headers = (cd.headers || []).slice();
                headers[ci] = val;
                cd.headers = headers;
                return Object.assign({}, prev, { chart_data:cd });
            });
        }
        function tbSzAddRow() {
            setTbSizing(function(prev) {
                if (!prev) return prev;
                var cd = Object.assign({}, prev.chart_data || {});
                var cols = (cd.headers || []).length || 4;
                var rows = (cd.rows || []).map(function(r){ return r.slice(); });
                rows.push(Array(cols).fill(''));
                cd.rows = rows;
                return Object.assign({}, prev, { chart_data:cd });
            });
        }
        function tbSzRemoveRow(ri) {
            setTbSizing(function(prev) {
                if (!prev) return prev;
                var cd = Object.assign({}, prev.chart_data || {});
                var rows = (cd.rows || []).map(function(r){ return r.slice(); });
                rows.splice(ri, 1);
                cd.rows = rows;
                return Object.assign({}, prev, { chart_data:cd });
            });
        }
        function tbSzAddCol() {
            setTbSizing(function(prev) {
                if (!prev) return prev;
                var cd = Object.assign({}, prev.chart_data || {});
                var headers = (cd.headers || []).slice();
                headers.push('New Column');
                var rows = (cd.rows || []).map(function(r){ var nr = r.slice(); nr.push(''); return nr; });
                cd.headers = headers;
                cd.rows = rows;
                return Object.assign({}, prev, { chart_data:cd });
            });
        }
        function tbSzRemoveCol(ci) {
            setTbSizing(function(prev) {
                if (!prev) return prev;
                var cd = Object.assign({}, prev.chart_data || {});
                var headers = (cd.headers || []).slice();
                headers.splice(ci, 1);
                var rows = (cd.rows || []).map(function(r){ var nr = r.slice(); nr.splice(ci, 1); return nr; });
                cd.headers = headers;
                cd.rows = rows;
                return Object.assign({}, prev, { chart_data:cd });
            });
        }


        // ── Subscription plans state (requires titancart-subscriptions plugin) ──
        var SUBS_API = (Config.apiUrl || '').replace('/titancart/v1', '/titancart-subs/v1');
        function subsApiFetch(path, opts) {
            opts = opts || {};
            opts.headers = Object.assign({ 'Content-Type':'application/json', 'X-WP-Nonce': Config.nonce }, opts.headers || {});
            if (opts.body && typeof opts.body === 'object') opts.body = JSON.stringify(opts.body);
            return fetch(SUBS_API + path, opts).then(function(r) {
                return r.text().then(function(text) {
                    var data = null;
                    if (text) { try { data = JSON.parse(text); } catch(e) { data = null; } }
                    if (!r.ok) {
                        var msg = data && (data.message || data.error || JSON.stringify(data));
                        var err = new Error(msg || 'HTTP ' + r.status);
                        err.status = r.status; err.data = data;
                        return Promise.reject(err);
                    }
                    return data;
                });
            });
        }
        const [spPlans,      setSpPlans]      = useState([]);   // plans linked to this product
        const [spAllPlans,   setSpAllPlans]   = useState([]);   // all plans (for linking)
        const [spCustGroups, setSpCustGroups] = useState([]);   // customer groups for dropdown
        const [spLoading,    setSpLoading]    = useState(false);
        const [spSaving,     setSpSaving]     = useState(false);
        const [spMsg,        setSpMsg]        = useState(null);
        const [spShowCreate, setSpShowCreate] = useState(false);
        const [spForm,       setSpForm]       = useState({ name:'', price:'', billing_period:'month', billing_interval:'1', signup_fee:'0', trial_days:'0', max_renewals:'0', site_limit:'1', usergroup_id:'', status:'active' });

        function spLoad() {
            if (!numId || !Config.subscriptionsActive) return;
            setSpLoading(true);
            Promise.all([
                subsApiFetch('/plans?product_id=' + numId),
                subsApiFetch('/plans?per_page=100'),
                apiFetch('/customer-groups')
            ]).then(function(res) {
                setSpPlans((res[0].data || res[0].items || res[0] || []));
                setSpAllPlans((res[1].data || res[1].items || res[1] || []));
                setSpCustGroups(Array.isArray(res[2]) ? res[2] : res[2].data || []);
                setSpLoading(false);
            }).catch(function() { setSpLoading(false); });
        }
        function spCreatePlan() {
            if (!spForm.name || !spForm.price) { setSpMsg({ type:'error', text:'Name and price are required.' }); return; }
            setSpSaving(true);
            var body = Object.assign({}, spForm, {
                product_id: numId,
                price: parseFloat(spForm.price) || 0,
                billing_interval: parseInt(spForm.billing_interval) || 1,
                signup_fee: parseFloat(spForm.signup_fee) || 0,
                trial_days: parseInt(spForm.trial_days) || 0,
                max_renewals: parseInt(spForm.max_renewals) || 0,
                site_limit: (spForm.site_limit === '' || spForm.site_limit == null) ? 1 : (parseInt(spForm.site_limit) || 0),
                usergroup_id: spForm.usergroup_id ? parseInt(spForm.usergroup_id) : null
            });
            subsApiFetch('/plans', { method:'POST', body:body }).then(function() {
                setSpSaving(false); setSpShowCreate(false);
                setSpForm({ name:'', price:'', billing_period:'month', billing_interval:'1', signup_fee:'0', trial_days:'0', max_renewals:'0', site_limit:'1', usergroup_id:'', status:'active' });
                setSpMsg({ type:'success', text:'Plan created.' });
                spLoad();
                setTimeout(function(){ setSpMsg(null); }, 3000);
            }).catch(function(e) { setSpSaving(false); setSpMsg({ type:'error', text:e.message }); });
        }
        function spLinkPlan(planId) {
            setSpSaving(true);
            subsApiFetch('/plans/' + planId, { method:'PUT', body:{ product_id:numId } }).then(function() {
                setSpSaving(false); setSpMsg({ type:'success', text:'Plan linked.' }); spLoad();
                setTimeout(function(){ setSpMsg(null); }, 3000);
            }).catch(function(e) { setSpSaving(false); setSpMsg({ type:'error', text:e.message }); });
        }
        function spUnlinkPlan(planId) {
            if (!confirm('Unlink this plan from the product? The plan will still exist as a standalone plan.')) return;
            setSpSaving(true);
            subsApiFetch('/plans/' + planId, { method:'PUT', body:{ product_id:null } }).then(function() {
                setSpSaving(false); setSpMsg({ type:'success', text:'Plan unlinked.' }); spLoad();
                setTimeout(function(){ setSpMsg(null); }, 3000);
            }).catch(function(e) { setSpSaving(false); setSpMsg({ type:'error', text:e.message }); });
        }
        function spDeletePlan(plan) {
            if (!confirm('Delete plan "' + (plan.name || 'Untitled') + '"? This cannot be undone.')) return;
            setSpSaving(true);
            subsApiFetch('/plans/' + plan.id, { method:'DELETE' }).then(function() {
                setSpSaving(false); setSpMsg({ type:'success', text:'Plan deleted.' }); spLoad();
                setTimeout(function(){ setSpMsg(null); }, 3000);
            }).catch(function(e) { setSpSaving(false); setSpMsg({ type:'error', text:e.message }); });
        }
        function spSetF(k, v) { setSpForm(function(prev) { var n = Object.assign({}, prev); n[k] = v; return n; }); }


        // Full form state — all product fields
        const [form, setForm] = useState({
            name:'', status: isVendor ? 'hidden' : 'active',
            price:'0.00', list_price:'0.00', cost_price:'0.00',
            description:'', short_description:'',
            // Pricing / inventory
            product_code:'', amount:'1',
            zero_price_action:'do_not_allow', track_inventory:true, is_downloadable:false,
            // S129 Phase 2 — Downloadable Products per-product overrides.
            // Empty string = inherit global setting / unlimited; non-empty int = override.
            download_ttl_hours:'', download_max_count:'',
            min_qty:'0', max_qty:'0', qty_step:'0',
            out_of_stock_actions:'none',
            tax_class_id:'',
            // Price per unit
            unit_name:'', units_in_product:'', show_price_per:'',
            // Availability
            usergroups:['all'], creation_date:'', avail_since:'',
            // Extra
            details_view:'', popularity:'0', search_words:'', promo_text:'',
            // Age Verification
            age_restricted:false, age_limit:0,
            // Shipping
            weight:'', weight_unit:'lb', length:'', width:'', height:'', dimension_unit:'in', free_shipping:false, shipping_freight:'', requires_shipping:true,
            // Subscription display (Session 102 Phase A.1) — suppress One-Time pill on PDP
            subscriptions_only:false,
            // SEO
            meta_title:'', meta_description:'', slug:'',
            // Categories
            category_ids:[],
            // Tags
            tags:[],
            // S124: Reward Points per-product overrides removed — group-keyed
            // earning rates live in tc_settings (Loyalty Rewards admin tab).
        });

        function sf(k, v) { setForm(prev => Object.assign({}, prev, { [k]: v })); setMsg({ text:'', ok:true }); }

        // Live slug-availability check for the SEO tab. Debounced; warns (doesn't block)
        // when another product already owns the slug, and shows the de-duped alternative
        // that save() would assign. exclude=numId so a product never flags its own slug.
        const [slugChk, setSlugChk] = useState({ checking:false, taken:false, suggestion:'' });
        const slugChkTimer = useRef(null);
        useEffect(function() {
            var s = (form.slug || '').trim();
            if (slugChkTimer.current) clearTimeout(slugChkTimer.current);
            if (!s) { setSlugChk({ checking:false, taken:false, suggestion:'' }); return; }
            setSlugChk(function(p){ return Object.assign({}, p, { checking:true }); });
            slugChkTimer.current = setTimeout(function() {
                apiFetch('/products/check-slug?slug=' + encodeURIComponent(s) + '&exclude=' + (numId || 0))
                    .then(function(d) {
                        setSlugChk({ checking:false, taken: !!(d && d.available === false), suggestion: (d && d.suggestion) || '' });
                    })
                    .catch(function(){ setSlugChk({ checking:false, taken:false, suggestion:'' }); });
            }, 400);
        }, [form.slug, numId]);

        // ── Downloadable-products master-toggle awareness ─────────────────────
        // The "Digital product" checkbox is independent of the storefront-wide
        // `catalog_enable_downloads` setting. When that setting is OFF, CartManager
        // rejects digital products at add-to-cart, so a product can look fine in the
        // editor yet be unbuyable by customers (they hit "Downloadable products are
        // temporarily unavailable"). Fetch the flag; if it's off, render a contextual
        // warning + one-click enable next to the checkbox (Pricing / inventory).
        // null = not yet loaded → no warning. Admin context only — vendors don't
        // control store-wide settings.
        var _dlMaster   = useState(null);  var dlMasterOn = _dlMaster[0];   var setDlMasterOn = _dlMaster[1];
        var _dlEnabling = useState(false); var dlEnabling  = _dlEnabling[0]; var setDlEnabling = _dlEnabling[1];
        var _dlErr      = useState(false); var dlErr       = _dlErr[0];      var setDlErr      = _dlErr[1];
        useEffect(function() {
            if (Config.spa_mode === 'vendor') return;
            apiFetch('/settings/general').then(function(d) {
                var vals = (d && d.values && typeof d.values === 'object') ? d.values : (d || {});
                var raw  = vals ? vals.catalog_enable_downloads : null;
                // Schema default is '1' (on). Unknown/absent → treat as on so we never
                // false-warn; only an explicit '0' surfaces the notice.
                setDlMasterOn( (raw === undefined || raw === null) ? true : (String(raw) === '1') );
            }).catch(function() { setDlMasterOn(true); }); // fail-open on fetch error
        }, []);
        function enableDownloads() {
            if (dlEnabling) return;
            setDlErr(false);
            setDlEnabling(true);
            // Partial PUT is safe: update_settings upserts only the keys present in the
            // body (per-key INSERT ... ON DUPLICATE KEY UPDATE), leaving every other
            // general setting untouched.
            apiFetch('/settings/general', { method:'PUT', body:{ catalog_enable_downloads:'1' } })
                .then(function(r) {
                    if (r && r.success) { setDlMasterOn(true); } else { setDlErr(true); }
                    setDlEnabling(false);
                })
                .catch(function() { setDlErr(true); setDlEnabling(false); });
        }

        /** Format a decimal value from DB (e.g. "5.0000") to clean display. Prices→2dp, weight→strip trailing zeros. */
        function fmtDec(val, dp) {
            if (val == null || val === '') return '';
            var n = parseFloat(val);
            if (isNaN(n)) return '';
            if (n === 0 && dp === 2) return '0.00';
            if (n === 0) return '';
            return dp != null ? n.toFixed(dp) : String(parseFloat(n.toFixed(4)));  // strip trailing zeros
        }

        // Load existing product
        useEffect(function() {
            /* S184 — Category + tax-class lookups must route through vendor
             * endpoints when the SPA is in vendor mode. The TC core
             * /categories and /tax/classes endpoints require manage_options
             * which vendors don't have, so a vendor hitting them gets a
             * silent 403 (caught by the empty .catch below) and the
             * pickers stay empty. The marketplace's /me/vendor/categories
             * and /me/vendor/tax-classes endpoints return the same shape
             * but gate on vendor_admin_eligible_check instead.
             */
            if (isVendor) {
                apiFetchVendor('/me/vendor/categories?per_page=200').then(function(d) {
                    setCats(Array.isArray(d) ? d : (d.categories||[]));
                }).catch(function(){});

                apiFetchVendor('/me/vendor/tax-classes').then(function(d) {
                    setTaxClasses(Array.isArray(d) ? d : (d.classes||[]));
                }).catch(function(){});
            } else {
                apiFetch('/categories?format=flat&per_page=200').then(function(d) {
                    setCats(Array.isArray(d) ? d : (d.categories||[]));
                }).catch(function(){});

                apiFetch('/tax/classes').then(function(d) {
                    setTaxClasses(Array.isArray(d) ? d : (d.classes||[]));
                }).catch(function(){});
            }

            if (!isNew && numId) {
                setLoading(true);
                editorFetch(apiBase + '/' + numId).then(function(d) {
                    const p = d.product || d;
                    setForm({
                        name:               p.name || '',
                        status:             p.status || 'active',
                        price:              fmtDec(p.price, 2) || '0.00',
                        list_price:         fmtDec(p.list_price, 2) || '0.00',
                        cost_price:         fmtDec(p.cost_price, 2) || '0.00',
                        description:        p.description || '',
                        short_description:  p.short_desc || '',
                        product_code:       p.sku || '',
                        amount:             p.stock_quantity != null ? String(p.stock_quantity) : '1',
                        zero_price_action:  p.zero_price_action || 'do_not_allow',
                        track_inventory:    p.track_inventory == null ? true : !!Number(p.track_inventory),
                        is_downloadable:    !!Number(p.is_downloadable),
                        // S129 Phase 2 — empty string when null (inherit global / unlimited).
                        download_ttl_hours: (p.download_ttl_hours === null || p.download_ttl_hours === undefined || p.download_ttl_hours === '') ? '' : String(p.download_ttl_hours),
                        download_max_count: (p.download_max_count === null || p.download_max_count === undefined || p.download_max_count === '') ? '' : String(p.download_max_count),
                        min_qty:            p.min_quantity != null ? String(p.min_quantity) : '0',
                        max_qty:            p.max_quantity != null ? String(p.max_quantity) : '0',
                        qty_step:           p.quantity_step != null ? String(p.quantity_step) : '0',
                        out_of_stock_actions: p.out_of_stock_actions || 'none',
                        tax_class_id:       p.tax_class_id != null ? String(p.tax_class_id) : '',
                        unit_name:          p.unit_name || '',
                        units_in_product:   p.units_in_product || '',
                        show_price_per:     p.show_price_per || '',
                        usergroups:         Array.isArray(p.usergroups) ? p.usergroups : ['all'],
                        creation_date:      p.created_at ? p.created_at.substring(0,10) : '',
                        avail_since:        p.avail_since ? p.avail_since.substring(0,10) : '',
                        details_view:       p.details_view || '',
                        popularity:         p.position != null ? String(p.position) : '0',
                        search_words:       p.search_words || '',
                        promo_text:         p.promo_text || '',
                        weight:             fmtDec(p.weight),
                        weight_unit:        p.weight_unit || 'lb',
                        length:             fmtDec(p.length),
                        width:              fmtDec(p.width),
                        height:             fmtDec(p.height),
                        dimension_unit:     p.dimension_unit || 'in',
                        free_shipping:      !!Number(p.free_shipping),
                        shipping_freight:   fmtDec(p.shipping_freight, 2),
                        // S138 Phase 5: requires_shipping defaults to 1 in schema, so
                        // a missing/undefined value still resolves true (existing rows
                        // pre-Phase-5 had no UI to flip it). Load as boolean for the form.
                        requires_shipping:  p.requires_shipping == null ? true : !!Number(p.requires_shipping),
                        subscriptions_only: !!Number(p.subscriptions_only),
                        age_restricted:     !!Number(p.age_restricted),
                        age_limit:          parseInt(p.age_limit) || 0,
                        meta_title:         p.meta_title || '',
                        meta_description:   p.meta_desc || '',
                        slug:               p.slug || '',
                        category_ids:       Array.isArray(p.categories) ? p.categories.map(function(c){
                            // API returns flat integers; guard against legacy object shape {id, is_main, position}
                            return typeof c === 'object' && c !== null ? Number(c.id || c.category_id || 0) : Number(c);
                        }).filter(function(n){ return n > 0; }) : [],
                        tags:               Array.isArray(p.tags) ? p.tags : (typeof p.tags === 'string' && p.tags ? p.tags.split(',').map(t=>t.trim()).filter(Boolean) : []),
                    });
                    // S120: Bulk Buys — populate editor rows from API prices array.
                    // Inverse of save-side: integer usergroup_id → display label.
                    // Empty array (no rows yet) falls back to a single empty placeholder row.
                    if (Array.isArray(p.prices) && p.prices.length > 0) {
                        setQtyDiscounts(p.prices.map(function(row) {
                            return {
                                qty:       row.min_quantity != null ? String(row.min_quantity) : '',
                                value:     String(parseFloat(row.discount_value) || 0),
                                type:      row.discount_type || 'absolute',
                                usergroup: ugIdToLabel(row.usergroup_id),
                                checked:   false,
                            };
                        }));
                    } else {
                        setQtyDiscounts([defaultQtyRow()]);
                    }
                    setImages(Array.isArray(p.images) ? p.images : []);
                    setVideos(Array.isArray(p.videos) ? p.videos : []);
                    setPreviewUrl(p.url || '');
                    setLoading(false);
                }).catch(function(){ setLoading(false); });
            }
        }, [numId]);


        // Lazy-load attributes when tab switches to 'attributes'
        useEffect(function() {
            if (tab === 'attributes' && numId && attrList.length === 0 && !attrLoading) {
                attrLoad();
            }
        }, [tab]);

        // Lazy-load product group when tab switches to 'product-group'
        useEffect(function() {
            if (tab === 'product-group' && numId && !pgGroup && pgMembers.length === 0 && !pgLoading) {
                pgLoad();
            }
        }, [tab]);

        // Lazy-load waitlist when tab switches to 'waitlist'
        useEffect(function() {
            if (tab === 'waitlist' && numId && wlSubs.length === 0 && !wlLoading) {
                wlLoad();
            }
        }, [tab]);

        // Lazy-load tabs when tab switches to 'tabs'
        useEffect(function() {
            if (tab === 'tabs' && numId && tbTabs.length === 0 && !tbLoading) {
                tbLoad();
            }
        }, [tab]);


        // Lazy-load subscription plans when tab switches to 'subscription' or product changes
        useEffect(function() {
            if (tab === 'subscription' && numId && Config.subscriptionsActive) {
                spLoad();
            }
        }, [tab, numId]);

        function save(opts) {
            // S308 — opts.addNew = true → reset to a blank new product after save
            // ("Save & Add New"). The main Save button calls save() with no opts.
            var addNew = !!(opts && opts.addNew);
            if (!form.name.trim()) { setMsg({ text:'Product name is required.', ok:false }); return; }
            if (!form.product_code.trim() && !Config.productCodeAutogenerate) { setMsg({ text:'Product Code is required.', ok:false }); return; }
            setSaving(true);
            const body = {
                // Translation sub-object — all user-facing text
                translation: {
                    name:          form.name,
                    description:   form.description,
                    short_desc:    form.short_description,
                    meta_title:    form.meta_title,
                    meta_desc:     form.meta_description,
                },
                // Core scalar fields (PHP field names)
                sku:              form.product_code,
                slug:             form.slug,
                status:           form.status,
                price:            parseFloat(form.price) || 0,
                list_price:       parseFloat(form.list_price) || 0,
                cost_price:       parseFloat(form.cost_price) || 0,
                weight:           parseFloat(form.weight) || 0,
                weight_unit:      form.weight_unit || 'lb',
                length:           form.length ? parseFloat(form.length) : null,
                width:            form.width ? parseFloat(form.width) : null,
                height:           form.height ? parseFloat(form.height) : null,
                dimension_unit:   form.dimension_unit || 'in',
                shipping_freight: parseFloat(form.shipping_freight) || 0,
                stock_quantity:   parseInt(form.amount) || 0,
                min_quantity:     parseInt(form.min_qty) || 0,
                max_quantity:     parseInt(form.max_qty) || 0,
                quantity_step:    parseInt(form.qty_step) || 0,
                tax_class_id:     form.tax_class_id ? parseInt(form.tax_class_id) : null,
                free_shipping:    form.free_shipping ? 1 : 0,
                // S138 Phase 5: requires_shipping is the runtime gate that drives the
                // checkout shipping pipeline (CheckoutManager::cart_needs_shipping).
                // Carts with no items requiring shipping skip the picker, the address
                // section, and the cost calculation entirely.
                requires_shipping: form.requires_shipping ? 1 : 0,
                subscriptions_only: form.subscriptions_only ? 1 : 0,
                age_restricted:   form.age_restricted ? 1 : 0,
                age_limit:        parseInt(form.age_limit) || 0,
                position:         parseInt(form.popularity) || 0,
                // Extra fields passed through as-is (stored in meta/JSON by repo)
                // Digital products don't need inventory tracking — force it off for data consistency.
                track_inventory:      (form.is_downloadable || !form.track_inventory) ? 0 : 1,
                is_downloadable:      form.is_downloadable ? 1 : 0,
                // S129 Phase 2 — null when blank (inherit global / unlimited).
                download_ttl_hours:   (form.download_ttl_hours === '' || form.download_ttl_hours == null) ? null : parseInt(form.download_ttl_hours, 10),
                download_max_count:   (form.download_max_count === '' || form.download_max_count == null) ? null : parseInt(form.download_max_count, 10),
                zero_price_action:    form.zero_price_action,
                out_of_stock_actions: form.out_of_stock_actions,
                unit_name:            form.unit_name,
                units_in_product:     form.units_in_product,
                show_price_per:       form.show_price_per,
                avail_since:          form.avail_since,
                details_view:         form.details_view,
                search_words:         form.search_words,
                promo_text:           form.promo_text,
                tags:                 form.tags,
                videos:               videos.filter(function(v){ return v.url.trim() !== ''; }),
                // Categories (PHP expects 'categories', not 'category_ids')
                categories:           form.category_ids,
                // Bulk Buys (S120: new shape — see ProductsController L1607-1628)
                // Old shape `qty_discounts` was silently dropped by the sanitizer.
                // New shape: prices: [{ usergroup_id, min_quantity, discount_type, discount_value }]
                prices: qtyDiscounts.filter(r => r.qty !== '').map(r => ({
                    usergroup_id:   ugLabelToId(r.usergroup),
                    min_quantity:   parseInt(r.qty) || 1,
                    discount_type:  r.type,
                    discount_value: parseFloat(r.value) || 0,
                })),
                // S124: rp_* per-product fields removed — earning rates are now
                // group-keyed and managed on the Loyalty Rewards admin page.
            };
            // S308 — a NEW core product's staged images ride along on the create
            // body (sanitize_product_input → repo->save → save_images persists
            // them). Main image first so save_images' pos-0-is-main rule keeps the
            // chosen main; the rest preserve the gallery's drag order (stable sort).
            if (isNew && !isVendor && images.length) {
                var orderedImgs = images.slice().sort(function(a, b){
                    var am = (a.is_main == 1 || a.type === 'main') ? 1 : 0;
                    var bm = (b.is_main == 1 || b.type === 'main') ? 1 : 0;
                    return bm - am;
                });
                body.images = orderedImgs.map(function(img, idx){
                    return {
                        wp_attachment_id: img.wp_attachment_id || 0,
                        position:         idx,
                        is_main:          idx === 0 ? 1 : 0,
                        alt_text:         img.alt_text || '',
                    };
                });
            }
            const req = isNew
                ? editorFetch(apiBase,            { method:'POST', body })
                : editorFetch(apiBase + '/' + numId, { method:'PUT',  body });
            req.then(function(d) {
                /* KI-49 (#448(b)): apiFetch now rejects on a non-JSON 2xx, so `d` can no
                 * longer be null here from that cause. The guard stays because a route
                 * that legitimately answers 204/empty resolves null, and L6481 below
                 * already guarded `d && d.url` — one line under this unguarded read.
                 * The reject is the fix; this is hygiene. */
                const newId = (d && (d.product_id || d.id)) || numId;
                // S308 — refresh the Preview link from the save response so a slug
                // edit keeps it correct without waiting for a reload.
                if (d && d.url) setPreviewUrl(d.url);
                // Persist the Attributes-tab selections as part of the main product
                // Save, so the separate (often below-the-fold) attributes Save button
                // is no longer required and clicking the top Save can't silently drop
                // the chosen attributes. GUARD on attrList.length: only write when the
                // Attributes tab has actually been loaded — otherwise attrValues is {}
                // and the delete-then-insert save_feature_values would WIPE the
                // product's existing attribute values.
                if (newId && attrList.length > 0) {
                    var attrBody = [];
                    Object.keys(attrValues).forEach(function(fid) {
                        var v = attrValues[fid];
                        if (!v) return;
                        attrBody.push(Object.assign({ feature_id: parseInt(fid) }, v));
                    });
                    return apiFetch('/products/' + newId + '/attributes', { method:'PUT', body:{ values: attrBody } })
                        .then(function(){ return newId; });
                }
                return newId;
            }).then(function(newId) {
                setSaving(false);
                setMsg({ text:'Saved successfully.', ok:true });
                // S308 — "Save & Add New" resets the editor to a blank product.
                if (addNew) {
                    if (isNew) {
                        // Already on the new-product route (hash unchanged) → fire a
                        // remount event so ProductsPage bumps the editor key.
                        window.dispatchEvent(new CustomEvent('tc:product-editor-new'));
                    } else {
                        // Coming from an existing product → the route change alone
                        // remounts a blank editor.
                        navigate(listRoute + '/new');
                    }
                } else if (isNew && newId) {
                    navigate(listRoute + '/' + newId);
                }
            }).catch(function(err) {
                setSaving(false);
                const detail = err && err.message ? err.message : 'Unknown error. Check the browser console for details.';
                setMsg({ text:'Save failed: ' + detail, ok:false });
            });
        }

        function handleNameChange(v) {
            sf('name', v);
            // Auto-update slug while on a new product AND the slug still matches
            // the auto-generated form of the previous name (i.e. user hasn't manually edited it).
            if (isNew) {
                const prevSlug = form.name.toLowerCase().replace(/[^a-z0-9]+/g,'-').replace(/^-+|-+$/g,'');
                if (form.slug === '' || form.slug === prevSlug) {
                    sf('slug', v.toLowerCase().replace(/[^a-z0-9]+/g,'-').replace(/^-+|-+$/g,''));
                }
            }
        }

        // Image upload — handles multiple files sequentially
        function uploadImage(e) {
            var files = e.target.files || (e.dataTransfer && e.dataTransfer.files) || [];
            if (!files.length) return;

            // S308 — a NEW core product can attach images BEFORE its first save:
            // stage each file to the media library (no product_id yet) and hold
            // it client-side; save() then carries the staged wp_attachment_ids
            // in the create body. Vendor-new stays out of scope (keeps the
            // original "save first" guard — vendor catalog REST has no staging
            // endpoint).
            var stageMode = isNew && !isVendor;
            if (isNew && isVendor) { setMsg({ text:'Save the product first to upload images.', ok:false }); return; }

            var queue = Array.from(files).filter(function(f) { return f.type && f.type.startsWith('image/'); });
            if (!queue.length) return;

            setImgUploading(true);
            var uploaded = 0;
            var failed = 0;

            // S186 — mode-aware base URL for FormData uploads. Cannot use
            // editorFetch / apiFetchVendor here because both helpers force
            // Content-Type: application/json + JSON.stringify the body, which
            // would break the multipart boundary. Raw fetch with a mode-aware
            // base preserves the FormData contract.
            var coreBase   = (Config.apiUrl || '/wp-json/titancart/v1');
            var uploadBase = (isVendor
                ? (Config.vendor_api_url || '/wp-json/titancart-marketplace/v1')
                : coreBase);
            var endpoint   = stageMode
                ? (coreBase + '/products/stage-image')
                : (uploadBase + apiBase + '/' + numId + '/images');

            function uploadNext(idx) {
                if (idx >= queue.length) {
                    setImgUploading(false);
                    if (failed > 0) setMsg({ text: failed + ' of ' + queue.length + ' uploads failed.', ok:false });
                    // Reset file input so same files can be re-selected
                    if (e.target && e.target.value) e.target.value = '';
                    return;
                }
                var fd = new FormData();
                fd.append('image', queue[idx]);
                fetch(endpoint, {
                    method:'POST', headers:{ 'X-WP-Nonce': Config.nonce||'' }, body: fd,
                }).then(function(r){ return r.json(); }).then(function(d) {
                    if (stageMode) {
                        // Staging response: { wp_attachment_id, url, thumb, ... }.
                        // Build a client-only image record (synthetic id keys the
                        // gallery + the local delete/main/reorder handlers). The
                        // first staged image is the main one.
                        if (d && d.wp_attachment_id) {
                            uploaded++;
                            var aid = d.wp_attachment_id;
                            setImages(function(prev){
                                var first = prev.length === 0;
                                return prev.concat([{
                                    id: 'stg_' + aid, image_id: 'stg_' + aid,
                                    wp_attachment_id: aid,
                                    url: d.url || d.image_url || '',
                                    image_url: d.image_url || d.url || '',
                                    thumb: d.thumb || '',
                                    alt_text: '',
                                    is_main: first ? 1 : 0,
                                    type: first ? 'main' : 'additional',
                                    _staged: true,
                                }]);
                            });
                        } else {
                            failed++;
                        }
                    } else if (d.image || d.id) {
                        uploaded++;
                        setImages(function(prev){ return prev.concat([d.image||d]); });
                    } else {
                        failed++;
                    }
                    uploadNext(idx + 1);
                }).catch(function() {
                    failed++;
                    uploadNext(idx + 1);
                });
            }

            uploadNext(0);
        }

        function deleteImage(imgId) {
            // S204 — Pattern #151 cascade: native confirm() → ConfirmModal.
            // The S186 Pattern #157 routing through editorFetch + apiBase is
            // preserved in deleteImageConfirm() — vendor-mode still hits
            // /me/vendor/catalog/{id}/images/{image_id} via apiFetchVendor.
            // (S185 carry-forward closed in S204 A1-narrow.)
            setCDelImage({ open:true, busy:false, imgId:imgId });
        }
        function deleteImageConfirm() {
            var imgId = cDelImage.imgId;
            if (!imgId) return;
            setCDelImage(function(prev){ return Object.assign({}, prev, { busy:true }); });

            // S308 — a staged (unsaved) image has no product_images row. Remove it
            // locally and clean up its orphaned media-library attachment. If the
            // removed image was the main one, promote the new first (mirrors the
            // server's pos-0-is-main rule).
            var target = images.filter(function(im){ return (im.image_id||im.id) === imgId; })[0];
            if (isNew || (target && target._staged)) {
                var finish = function() {
                    setImages(function(prev){
                        var arr = prev.filter(function(img){ return (img.image_id||img.id) !== imgId; });
                        if (arr.length && !arr.some(function(im){ return im.is_main == 1 || im.type === 'main'; })) {
                            arr = arr.map(function(im, i){ return Object.assign({}, im, { is_main: i===0?1:0, type: i===0?'main':'additional' }); });
                        }
                        return arr;
                    });
                    setCDelImage({ open:false, busy:false, imgId:null });
                };
                var aid = target && target.wp_attachment_id;
                if (aid) {
                    apiFetch('/products/stage-image/' + aid, { method:'DELETE' }).then(finish).catch(finish);
                } else { finish(); }
                return;
            }

            editorFetch(apiBase + '/' + numId + '/images/' + imgId, { method:'DELETE' })
                .then(function(){
                    setImages(function(prev){ return prev.filter(function(img){ return (img.image_id||img.id) !== imgId; }); });
                    setCDelImage({ open:false, busy:false, imgId:null });
                })
                .catch(function(){ setCDelImage(function(prev){ return Object.assign({}, prev, { busy:false }); }); });
        }

        function setMainImage(imgId) {
            // S308 — staged (unsaved) product: set main locally and move it to the
            // front, so the create body's first image becomes is_main (save_images
            // forces position 0 = main).
            if (isNew) {
                setImages(function(prev){
                    var arr = prev.slice();
                    var i = arr.findIndex(function(im){ return (im.image_id||im.id) === imgId; });
                    if (i > 0) { var m = arr.splice(i,1)[0]; arr.unshift(m); }
                    return arr.map(function(im, idx){ return Object.assign({}, im, { is_main: idx===0?1:0, type: idx===0?'main':'additional' }); });
                });
                return;
            }
            editorFetch(apiBase + '/' + numId + '/images/' + imgId + '/set-main', { method:'PUT' })
                .then(function(){ setImages(prev=>prev.map(img=>Object.assign({},img,{ type:(img.image_id||img.id)===imgId?'main':'additional' }))); });
        }

        // Image drag-and-drop reorder
        var imgDragIdx = { current: null };
        function imgDragStart(idx) { imgDragIdx.current = idx; }
        function imgDragOver(e) { e.preventDefault(); }
        function imgDrop(targetIdx) {
            var fromIdx = imgDragIdx.current;
            if (fromIdx === null || fromIdx === targetIdx) return;
            setImages(function(prev) {
                var arr = prev.slice();
                var moved = arr.splice(fromIdx, 1)[0];
                arr.splice(targetIdx, 0, moved);
                // S308 — a staged (unsaved) product reorders locally only; the
                // order persists through the create body. Existing products
                // persist the new order immediately via the reorder endpoint.
                if (!isNew) {
                    var ids = arr.map(function(img) { return img.image_id || img.id; });
                    editorFetch(apiBase + '/' + numId + '/images/reorder', { method:'PUT', body:{ ids: ids } }).catch(function(err){ console.warn('[TitanCart] Image reorder save failed:', err); });
                }
                return arr;
            });
            imgDragIdx.current = null;
        }

        // Alt text handlers — keep local state snappy while typing; persist on blur.
        function setImageAltLocal(imgId, altText) {
            setImages(function(prev) {
                return prev.map(function(img) {
                    return (img.image_id || img.id) === imgId
                        ? Object.assign({}, img, { alt_text: altText })
                        : img;
                });
            });
        }
        function saveImageAlt(imgId, altText) {
            if (isNew) return;
            editorFetch(apiBase + '/' + numId + '/images/' + imgId, {
                method: 'PUT',
                body: { alt_text: altText }
            }).catch(function(err) { console.warn('Alt text save failed:', err); });
        }

        function toggleCat(catId) {
            var id = Number(catId);
            sf('category_ids', form.category_ids.includes(id)
                ? form.category_ids.filter(function(c){ return c!==id; })
                : form.category_ids.concat([id]));
        }

        // ── Style helpers ────────────────────────────────────────────────────
        const fs       = fieldSty(dark, borderColor);
        const inputSm  = Object.assign({}, fs, { width:'auto', minWidth:140 });

        // FR and SectionHead live at module scope (prevents focus-loss on re-render).
        // Pass their needed style values via a single ctx object.
        const edCtx = { mutedColor, borderColor, textColor, open, onToggle: toggleSection };
        // TABS — Post-save tabs only visible after a product is created (requires product_id)
        // ext: if set, tab only shows when that extension is active
        const TABS = [
            { v:'general',   l:'General' },
            { v:'seo',       l:'SEO', ext:'seo' },
            ...(!isNew ? [
                { v:'attributes',    l:'Attributes' },
                { v:'product-group', l:'Product Group', ext:'variations' },
            ] : []),
            { v:'qty',       l:'Bulk Buys' },
            { v:'tags',      l:'Tags' },
            ...(!isNew ? [
                { v:'shipping',      l:'Shipping' },
            ] : []),
            // S124: Reward Points tab removed — per-product overrides retired.
            ...(!isNew ? [
                { v:'tabs',          l:'Tabs' },
                { v:'waitlist',      l:'Waitlist' },
            ] : []),
            // S129 Phase 2 — Downloads tab only when this product is marked digital.
            ...(!isNew && form.is_downloadable ? [
                { v:'downloads',     l:'Downloads' },
            ] : []),
            ...(!isNew && Config.subscriptionsActive ? [
                { v:'subscription',  l:'Subscription' },
            ] : []),
        ].filter(function(t){ return extActive(t.ext); });

        if (loading) return h('main', { style:{ flex:1, overflow:'auto', padding:S.xxl } }, h(Spinner, { mutedColor }));

        // ── Render ───────────────────────────────────────────────────────────
        return h('main', { style:{ flex:1, overflow:'auto', background:dark?'#0A0A0A':TC.lightBg } },

            // ── Header bar ───────────────────────────────────────────────────
            h('div', { style:{ background:cardBg, borderBottom:`1px solid ${borderColor}`, padding:`${S.md}px ${S.xxl}px`,
                                display:'flex', justifyContent:'space-between', alignItems:'center', position:'sticky', top:0, zIndex:10 } },
                h('div', { style:{ display:'flex', alignItems:'center', gap:S.lg } },
                    h('button', { onClick:()=>navigate(listRoute),
                        style:{ background:'transparent', border:'none', cursor:'pointer', color:mutedColor, display:'flex', padding:4 } },
                        ico([h('path',{d:'M15 18l-6-6 6-6'})], 18, '0 0 20 20')
                    ),
                    h('h1', { style:{ fontSize:20, fontWeight:700, letterSpacing:'-.3px' } },
                        isNew ? 'New product' : (form.name || 'Edit product'))
                ),
                h('div', { style:{ display:'flex', gap:S.sm, alignItems:'center' } },
                    msg.text && h('span', { style:{ fontSize:12, color:msg.ok?TC.success:TC.error, marginRight:S.sm } }, msg.text),
                    h('button', { onClick:()=>navigate(listRoute),
                        style:{ padding:`7px ${S.lg}px`, borderRadius:7, border:`1px solid ${borderColor}`, background:'transparent', color:mutedColor, fontSize:13, cursor:'pointer' } }, 'Cancel'),
                    // S308 — Preview the live storefront page (saved products only;
                    // previewUrl comes from the GET/save response permalink).
                    (!isNew && previewUrl) && h('button', {
                        onClick:function(){ window.open(previewUrl, '_blank', 'noopener'); },
                        title:'Open this product on the storefront',
                        style:{ padding:`7px ${S.lg}px`, borderRadius:7, border:`1px solid ${borderColor}`, background:'transparent', color:textColor, fontSize:13, cursor:'pointer' } }, 'Preview \u2197'),
                    // S308 — Save & Add New: persist, then reset to a blank product.
                    h('button', { onClick:function(){ save({ addNew:true }); }, disabled:saving,
                        style:{ padding:`7px ${S.lg}px`, borderRadius:7, border:`1px solid ${borderColor}`, background:'transparent', color:textColor, fontSize:13, cursor:'pointer', opacity:saving?.6:1 } },
                        isNew ? 'Create & Add New' : 'Save & Add New'),
                    h(Btn, { variant:'primary', onClick:function(){ save(); }, disabled:saving, mutedColor, borderColor },
                        saving ? 'Saving…' : (isNew ? 'Create' : 'Save'))
                )
            ),

            // ── Tab bar ──────────────────────────────────────────────────────
            h('div', { style:{ background:cardBg, borderBottom:`1px solid ${borderColor}`, padding:`0 ${S.xxl}px`, display:'flex', gap:0 } },
                ...TABS.map(t => h('button', { key:t.v, onClick:()=>setTab(t.v),
                    style:{ padding:`11px ${S.lg}px`, border:'none', cursor:'pointer', fontSize:13, fontWeight:500,
                            background:'transparent', borderBottom:tab===t.v?`2px solid ${TC.accent}`:'2px solid transparent',
                            color:tab===t.v?TC.accent:mutedColor, marginBottom:-1, whiteSpace:'nowrap' } }, t.l))
            ),

            // ── Page content ─────────────────────────────────────────────────
            h('div', { style:{ padding:`${S.xl}px ${S.xxl}px ${S.xxl}px` } },

                // ════════════ GENERAL TAB ════════════
                tab==='general' && h('div', null,

                    // ── INFORMATION section ───────────────────────────────────
                    h(Card, { cardBg, borderColor, dark, style:{ marginBottom:S.lg } },
                        h(SectionHead, { id:'info', title:'Information', ctx:edCtx }),
                        open.info && h('div', null,

                            h(FR, { label:'Name', required:true, ctx:edCtx },
                                h('input', { value:form.name, onChange:e=>handleNameChange(e.target.value),
                                    placeholder:'', style:fs })
                            ),

                            h(FR, { label:'Categories', required:true, ctx:edCtx,
                                note:'Categories to be displayed in the navigation breadcrumbs.' },
                                h('div', { style:{ position:'relative' } },
                                    // Input row: chips + search + ≡ button
                                    h('div', { style:{ display:'flex', alignItems:'stretch', borderRadius:8, border:`1px solid ${borderColor}`, overflow:'visible', background:dark?'#262626':'#fff' } },
                                        // Chips + inline search
                                        h('div', {
                                            onClick:()=>setShowCatDrop(true),
                                            style:{ flex:1, minHeight:36, padding:`5px ${S.md}px`, cursor:'text',
                                                    display:'flex', flexWrap:'wrap', gap:4, alignItems:'center' }
                                        },
                                            form.category_ids.map(function(cid) {
                                                const cat = cats.find(function(c){ return Number(c.category_id||c.id)===Number(cid); });
                                                if (!cat) return null;
                                                return h('span', { key:cid,
                                                    style:{ display:'inline-flex', alignItems:'center', gap:4, padding:'2px 8px', borderRadius:5,
                                                            background:dark?'rgba(37,99,235,.15)':TC.accentBg, color:TC.accent, fontSize:12 } },
                                                    cat.name,
                                                    h('button', { onClick:e=>{ e.stopPropagation(); toggleCat(cid); },
                                                        style:{ background:'transparent', border:'none', cursor:'pointer', color:TC.accent, fontSize:14, padding:'0 2px', lineHeight:1 } }, '×')
                                                );
                                            }),
                                            h('input', { value:catSearch, onChange:e=>{ setCatSearch(e.target.value); setShowCatDrop(true); },
                                                placeholder: form.category_ids.length===0 ? 'None' : '',
                                                style:{ border:'none', outline:'none', background:'transparent', color:'inherit', fontSize:13, minWidth:80, flex:1 } })
                                        ),
                                        // ≡ button — opens full tree modal
                                        h('button', {
                                            onClick:e=>{ e.stopPropagation(); openCatModal(); },
                                            title:'Browse category tree',
                                            style:{ background:dark?'#262626':'#F5F5F5', border:'none', borderLeft:`1px solid ${borderColor}`,
                                                    borderRadius:'0 8px 8px 0', padding:`0 ${S.md}px`, cursor:'pointer', color:mutedColor,
                                                    display:'flex', alignItems:'center', justifyContent:'center', flexShrink:0 }
                                        },
                                            // Three-line "list" icon
                                            h('svg', { width:16, height:14, viewBox:'0 0 16 14', fill:'none', stroke:'currentColor', strokeWidth:'1.5', strokeLinecap:'round' },
                                                h('line',{x1:0,y1:2,x2:16,y2:2}),
                                                h('line',{x1:0,y1:7,x2:16,y2:7}),
                                                h('line',{x1:0,y1:12,x2:16,y2:12})
                                            )
                                        )
                                    ),
                                    // Inline typeahead dropdown
                                    showCatDrop && h('div', {
                                        style:{ position:'absolute', top:'calc(100% + 4px)', left:0, right:0, zIndex:100,
                                                background:cardBg, border:`1px solid ${borderColor}`, borderRadius:10,
                                                boxShadow:'0 8px 24px rgba(0,0,0,.18)', maxHeight:240, overflowY:'auto' },
                                        onMouseLeave:()=>setShowCatDrop(false)
                                    },
                                        catTreeOptions(cats).filter(function(o) { return !catSearch || (o.name||'').toLowerCase().indexOf(catSearch.toLowerCase()) >= 0; })
                                            .map(function(o) {
                                                var checked = form.category_ids.includes(Number(o.id));
                                                var prefix = o.depth > 0 ? '\u00A0'.repeat(o.depth*2) + '└ ' : '';
                                                return h('div', { key:o.id,
                                                    onClick:function(){ toggleCat(o.id); setCatSearch(''); },
                                                    style:{ display:'flex', alignItems:'center', gap:S.sm, padding:'7px '+S.md+'px',
                                                            paddingLeft: (S.md + o.depth*14) + 'px',
                                                            cursor:'pointer', fontSize:13,
                                                            background:checked?(dark?'rgba(37,99,235,.12)':TC.accentBg):'transparent',
                                                            color:checked?TC.accent:textColor },
                                                    onMouseEnter:function(e){ if(!checked)e.currentTarget.style.background=dark?'rgba(255,255,255,.04)':'#FAFAFA'; },
                                                    onMouseLeave:function(e){ if(!checked)e.currentTarget.style.background='transparent'; }
                                                },
                                                    h('input', { type:'checkbox', checked:checked, readOnly:true, style:{ accentColor:TC.accent, pointerEvents:'none' } }),
                                                    prefix + o.name
                                                );
                                            })
                                    )
                                )
                            ),

                            // ════ CATEGORY TREE MODAL (collapsible, ACM pattern) ════
                            showCatModal && h('div', {
                                style:{ position:'fixed', inset:0, background:'rgba(0,0,0,.55)', display:'flex', alignItems:'center',
                                        justifyContent:'center', zIndex:9999 },
                                onClick:e=>{ if(e.target===e.currentTarget) setShowCatModal(false); }
                            },
                                h('div', { style:{ background:cardBg, borderRadius:14, width:520, maxHeight:'80vh', display:'flex', flexDirection:'column',
                                                   boxShadow:'0 24px 60px rgba(0,0,0,.4)', border:`1px solid ${borderColor}` } },
                                    // Modal header
                                    h('div', { style:{ display:'flex', justifyContent:'space-between', alignItems:'center',
                                                       padding:`${S.lg}px ${S.xl}px`, borderBottom:`1px solid ${borderColor}`, flexShrink:0 } },
                                        h('h3', { style:{ fontSize:15, fontWeight:600 } }, 'Select categories'),
                                        h('button', { onClick:()=>setShowCatModal(false),
                                            style:{ background:'transparent', border:'none', fontSize:22, cursor:'pointer', color:mutedColor, lineHeight:1 } }, '×')
                                    ),
                                    // Column labels
                                    h('div', { style:{ display:'grid', gridTemplateColumns:'1fr 80px', padding:`${S.sm}px ${S.xl}px`,
                                                       borderBottom:`1px solid ${borderColor}`, flexShrink:0,
                                                       background:dark?'rgba(255,255,255,.02)':'#FAFAFA' } },
                                        h('span', { style:{ fontSize:10, fontWeight:500, color:mutedColor, textTransform:'uppercase', letterSpacing:'.5px' } }, '— Categories'),
                                        h('span', { style:{ fontSize:10, fontWeight:500, color:mutedColor, textTransform:'uppercase', letterSpacing:'.5px', textAlign:'right' } }, 'Products')
                                    ),
                                    // Tree body — collapsible using catBuildTree + catFlattenVisible
                                    (function() {
                                        var mTree = catBuildTree(cats);
                                        var mRows = catFlattenVisible(mTree, catModalExp);
                                        function mToggle(cid) { var id=Number(cid); setCatModalExp(function(p){ var n=new Set(p); if(n.has(id)) n.delete(id); else n.add(id); return n; }); }
                                        function mExpandAll() { setCatModalExp(new Set(cats.map(function(c){ return Number(c.id||c.category_id); }))); }
                                        function mCollapseAll() { setCatModalExp(new Set()); }
                                        var chevR = h('svg',{width:12,height:12,viewBox:'0 0 14 14',fill:'none',stroke:'currentColor',strokeWidth:'1.5',strokeLinecap:'round',strokeLinejoin:'round',style:{display:'block'}}, h('path',{d:'M5 3l4 4-4 4'}));
                                        var chevD = h('svg',{width:12,height:12,viewBox:'0 0 14 14',fill:'none',stroke:'currentColor',strokeWidth:'1.5',strokeLinecap:'round',strokeLinejoin:'round',style:{display:'block'}}, h('path',{d:'M3 5l4 4 4-4'}));

                                        return h(Fragment, null,
                                            // Expand/collapse controls
                                            h('div', { style:{ display:'flex', gap:12, padding:`${S.sm}px ${S.xl}px`, borderBottom:`1px solid ${borderColor}`, flexShrink:0 } },
                                                h('button', { onClick:mExpandAll, style:{ background:'transparent', border:'none', color:TC.accent, fontSize:11, fontWeight:500, cursor:'pointer', padding:0 } }, 'Expand all'),
                                                h('button', { onClick:mCollapseAll, style:{ background:'transparent', border:'none', color:mutedColor, fontSize:11, fontWeight:500, cursor:'pointer', padding:0 } }, 'Collapse all')
                                            ),
                                            // Tree rows
                                            h('div', { style:{ overflowY:'auto', flex:1, padding:`${S.sm}px 0` } },
                                                mRows.length === 0 && h('div', { style:{ padding:24, textAlign:'center', color:mutedColor, fontSize:13 } }, 'No categories found.'),
                                                mRows.map(function(cat) {
                                                    var cid = Number(cat.id || cat.category_id);
                                                    var depth = cat._depth || 0;
                                                    var hasKids = cat.children && cat.children.length > 0;
                                                    var isExp = catModalExp.has(cid);
                                                    var checked = modalSel.includes(cid);
                                                    return h('div', { key:cid,
                                                        onClick:function(e){
                                                            if (e.ctrlKey || e.metaKey) {
                                                                sf('category_ids', [cid]);
                                                                setShowCatModal(false);
                                                            } else {
                                                                toggleModalCat(cid);
                                                            }
                                                        },
                                                        style:{ display:'grid', gridTemplateColumns:'1fr 80px', alignItems:'center',
                                                                padding:`7px ${S.xl}px`, paddingLeft:(S.xl + depth*18)+'px',
                                                                cursor:'pointer',
                                                                background:checked?(dark?'rgba(37,99,235,.08)':'rgba(37,99,235,.04)'):'transparent' },
                                                        onMouseEnter:e=>{ if(!checked)e.currentTarget.style.background=dark?'rgba(255,255,255,.03)':'#FAFAFA'; },
                                                        onMouseLeave:e=>{ if(!checked)e.currentTarget.style.background=checked?(dark?'rgba(37,99,235,.08)':'rgba(37,99,235,.04)'):'transparent'; }
                                                    },
                                                        h('div', { style:{ display:'flex', alignItems:'center', gap:S.sm } },
                                                            h('input', { type:'checkbox', checked:checked, readOnly:true,
                                                                style:{ accentColor:TC.accent, pointerEvents:'none', flexShrink:0 } }),
                                                            hasKids
                                                                ? h('button', { onClick:function(e){ e.stopPropagation(); mToggle(cid); },
                                                                    style:{ width:18, height:18, border:'none', background:'transparent', display:'flex', alignItems:'center', justifyContent:'center', cursor:'pointer', color:mutedColor, flexShrink:0, padding:0 } },
                                                                    isExp ? chevD : chevR)
                                                                : h('span', { style:{ width:18, flexShrink:0 } }),
                                                            h('span', { style:{ fontSize:13, color:checked?TC.accent:textColor,
                                                                                fontWeight:checked?500:400 } }, cat.name)
                                                        ),
                                                        h('span', { style:{ fontSize:12, color:mutedColor, textAlign:'right' } },
                                                            cat.product_count != null ? cat.product_count : 0)
                                                    );
                                                })
                                            )
                                        );
                                    })(),
                                    // Tip + Save
                                    h('div', { style:{ borderTop:`1px solid ${borderColor}`, padding:`${S.md}px ${S.xl}px`,
                                                       flexShrink:0, display:'flex', flexDirection:'column', gap:S.md } },
                                        h('div', { style:{ fontSize:11, color:mutedColor } },
                                            'Tip: Hold Ctrl (or ⌘ on Mac) while clicking on a category name. This will add one category and close the window.'
                                        ),
                                        h('div', { style:{ display:'flex', justifyContent:'flex-end' } },
                                            h('button', { onClick:saveCatModal,
                                                style:{ padding:`7px ${S.xl}px`, borderRadius:7, border:'none',
                                                        background:TC.accent, color:'#fff', fontSize:13, fontWeight:500, cursor:'pointer' } }, 'Save')
                                        )
                                    )
                                )
                            ),

                            h(FR, { label:'Price (' + tcMoneySymbol() + ')', required:true, ctx:edCtx },
                                h('div', { style:{ display:'flex', alignItems:'center', gap:10 } },
                                    h('input', { type:'number', min:'0', step:'0.01', value:form.price,
                                        onChange:e=>sf('price',e.target.value), style:inputSm }),
                                    (parseFloat(form.price) > 0 && parseFloat(form.cost_price) > 0) ?
                                        h('span', { style:{ fontSize:13, fontWeight:600, color: (parseFloat(form.price) - parseFloat(form.cost_price)) >= 0 ? '#16A34A' : '#DC2626', whiteSpace:'nowrap' } },
                                            'Profit: $' + (parseFloat(form.price) - parseFloat(form.cost_price)).toFixed(2)
                                        ) : null
                                )
                            ),

                            h(FR, { label:'Full description', ctx:edCtx },
                                h(RichTextEditor, { value:form.description, onChange:function(html){ sf('description',html); },
                                    dark:dark, borderColor:borderColor, minHeight:220, placeholder:'Product description...' })
                            ),

                            h(FR, { label:'Status', required:true, ctx:edCtx },
                                isVendor
                                    /* B3.5c (S182) — Vendors cannot self-set status. Display
                                     * a read-only "Draft" indicator; server forces 'hidden'
                                     * on every save regardless. Admin promotes via B3.5e. */
                                    ? h('div', { style:{ display:'inline-flex', alignItems:'center', gap:8, padding:'6px 12px', borderRadius:999, background:'#FEF3C7', color:'#92400E', fontSize:12, fontWeight:600 } },
                                        h('span', { style:{ width:6, height:6, borderRadius:'50%', background:'#D97706' } }),
                                        'Draft — admin will review before publishing'
                                    )
                                    : h(RadioGroup, { value:form.status, onChange:v=>sf('status',v),
                                        options:[['active','Active'],['disabled','Disabled'],['hidden','Hidden']] })
                            ),

                            // Images
                            h(FR, { label:'Images', ctx:edCtx },
                                h('div', null,
                                    // Drop zone
                                    h('label', {
                                        style:{ display:'block', border:`2px dashed ${borderColor}`, borderRadius:10,
                                                background:dark?'#262626':'#FAFAFA', padding:40, textAlign:'center',
                                                cursor:'pointer', marginBottom:S.sm, transition:'border-color .15s' },
                                        onDragOver:e=>{ e.preventDefault(); e.currentTarget.style.borderColor=TC.accent; },
                                        onDragLeave:e=>{ e.currentTarget.style.borderColor=borderColor; },
                                        onDrop:e=>{ e.preventDefault(); e.currentTarget.style.borderColor=borderColor;
                                            if(e.dataTransfer.files.length){ uploadImage(e); } }
                                    },
                                        imgUploading
                                            ? h('div', { style:{ color:mutedColor, fontSize:13 } }, 'Uploading…')
                                            : h('div', null,
                                                h('div', { style:{ fontSize:32, marginBottom:S.sm, color:mutedColor, display:'flex', justifyContent:'center' } },
                                                    ico([h('rect',{x:3,y:3,width:18,height:18,rx:2}),h('circle',{cx:8.5,cy:8.5,r:1.5}),h('path',{d:'M21 15l-5-5L5 21'})],28,'0 0 24 24')
                                                ),
                                                h('div', { style:{ fontSize:13, color:mutedColor, marginBottom:S.sm } }, 'Drop images here'),
                                                h('div', { style:{ fontSize:12, color:mutedColor } }, 'or'),
                                                h('div', { style:{ display:'inline-flex', alignItems:'center', gap:0, marginTop:S.sm } },
                                                    h('span', { style:{ padding:'6px 20px', borderRadius:'8px 0 0 8px', border:`1px solid ${borderColor}`,
                                                            background:dark?'#262626':'#F5F5F5', color:textColor, fontSize:13, fontWeight:500 } }, 'Select images'),
                                                    h('span', { style:{ padding:'6px 8px', borderRadius:'0 8px 8px 0', border:`1px solid ${borderColor}`, borderLeft:'none',
                                                            background:dark?'#262626':'#F5F5F5', color:mutedColor, fontSize:12 } }, '▾')
                                                )
                                            ),
                                        h('input', { type:'file', accept:'image/*', multiple:true, style:{ display:'none' }, onChange:uploadImage })
                                    ),
                                    h('div', { style:{ fontSize:11, color:mutedColor } }, 'Maximum file size: 16 MB. Maximum dimensions: 6000px on the longest side.'),

                                    // Existing images (drag to reorder)
                                    images.length > 0 && h('div', { style:{ display:'grid', gridTemplateColumns:'repeat(5,1fr)', gap:S.md, marginTop:S.lg } },
                                        ...images.map(function(img, idx) {
                                            const imgId = img.image_id || img.id;
                                            const isMain = img.type === 'main' || img.is_main == 1;
                                            const url = img.url || img.image_url || '';
                                            return h('div', { key:imgId, draggable:true,
                                                onDragStart:function(){ imgDragStart(idx); },
                                                onDragOver:imgDragOver,
                                                onDrop:function(){ imgDrop(idx); },
                                                style:{ borderRadius:8, overflow:'hidden', border:'2px solid '+(isMain?TC.accent:borderColor), position:'relative', cursor:'grab', transition:'opacity .15s' },
                                                onDragEnter:function(e){ e.currentTarget.style.opacity='0.5'; },
                                                onDragLeave:function(e){ e.currentTarget.style.opacity='1'; },
                                                onDragEnd:function(e){ e.currentTarget.style.opacity='1'; }
                                            },
                                                url ? h('img', { src:url, draggable:false, style:{ width:'100%', aspectRatio:Config.productImageShape==='square'?'1':'3/4', objectFit:'cover', display:'block', pointerEvents:'none' } })
                                                     : h('div', { style:{ aspectRatio:Config.productImageShape==='square'?'1':'3/4', background:dark?'#262626':'#F5F5F5', display:'flex', alignItems:'center', justifyContent:'center', fontSize:20, color:mutedColor } }, '🖼️'),
                                                isMain && h('div', { style:{ position:'absolute', top:4, left:4, background:TC.accent, color:'#fff', fontSize:9, fontWeight:600, padding:'1px 5px', borderRadius:3 } }, 'MAIN'),
                                                // Alt text input — accessibility / SEO. Saves on blur.
                                                h('input', {
                                                    type:'text',
                                                    value: img.alt_text || '',
                                                    placeholder:'Alt text',
                                                    title:'Alt text — describes the image for screen readers and search engines',
                                                    onChange: function(e) { setImageAltLocal(imgId, e.target.value); },
                                                    onBlur:   function(e) { saveImageAlt(imgId, e.target.value); },
                                                    style:{ display:'block', width:'100%', boxSizing:'border-box', border:'none', borderTop:'1px solid '+borderColor, padding:'4px 6px', fontSize:10, background:'transparent', color:textColor, outline:'none' }
                                                }),
                                                h('div', { style:{ display:'flex', gap:2, padding:4 } },
                                                    !isMain && h('button', { onClick:()=>setMainImage(imgId),
                                                        style:{ flex:1, padding:'3px', fontSize:9, cursor:'pointer', border:'1px solid '+borderColor, borderRadius:4, background:'transparent', color:TC.accent } }, 'Main'),
                                                    h('button', { onClick:()=>deleteImage(imgId),
                                                        style:{ flex:1, padding:'3px', fontSize:9, cursor:'pointer', border:'1px solid '+TC.error+'30', borderRadius:4, background:'transparent', color:TC.error } }, '✕')
                                                )
                                            );
                                        })
                                    )
                                )
                            ),

                            // ── Videos ──────────────────────────────────────────────
                            h(FR, { label:'Videos', ctx:edCtx },
                                h('div', null,
                                    // Header row — only show when there are rows
                                    videos.length > 0 && h('div', { style:{ display:'grid', gridTemplateColumns:'1fr 160px 60px auto', gap:S.md, marginBottom:S.xs, paddingLeft:S.sm } },
                                        h('span', { style:{ fontSize:11, color:mutedColor, fontWeight:500 } }, 'URL'),
                                        h('span', { style:{ fontSize:11, color:mutedColor, fontWeight:500 } }, 'Hosting'),
                                        h('span', { style:{ fontSize:11, color:mutedColor, fontWeight:500 } }, 'Preview'),
                                        h('span', null)
                                    ),
                                    // One row per video
                                    ...videos.map(function(row, i) {
                                        // Extract YouTube video ID for thumbnail
                                        var ytId = null;
                                        if (row.url) {
                                            var m = row.url.match(/(?:v=|youtu\.be\/)([\w\-]+)/);
                                            if (m) ytId = m[1];
                                        }
                                        return h('div', { key:i, style:{ display:'grid', gridTemplateColumns:'1fr 160px 60px auto', gap:S.md, marginBottom:S.sm, alignItems:'center' } },
                                            // URL
                                            h('input', { value:row.url, onChange:function(e){setVideoField(i,'url',e.target.value)},
                                                placeholder:'https://www.youtube.com/watch?v=...',
                                                style:Object.assign({},fs,{ fontSize:12 }) }),
                                            // Video hosting
                                            h('select', { value:row.hosting, onChange:function(e){setVideoField(i,'hosting',e.target.value)},
                                                style:Object.assign({},fs,{ fontSize:12 }) },
                                                VIDEO_HOSTINGS.map(function(hn){ return h('option',{key:hn,value:hn},hn); })
                                            ),
                                            // Preview — clickable thumbnail/icon
                                            row.url
                                                ? h('a', { href:row.url, target:'_blank', rel:'noopener',
                                                    style:{ display:'flex', alignItems:'center', justifyContent:'center', height:40, width:56,
                                                            borderRadius:6, overflow:'hidden', background:dark?'#262626':'#F5F5F5',
                                                            border:'1px solid '+borderColor, textDecoration:'none' } },
                                                    ytId
                                                        ? h('img', { src:'https://img.youtube.com/vi/'+ytId+'/default.jpg',
                                                              style:{ width:'100%', height:'100%', objectFit:'cover' }, alt:'Preview' })
                                                        : h('span', { style:{ fontSize:18, color:TC.accent } }, '▶')
                                                  )
                                                : h('div', { style:{ height:40, width:56, borderRadius:6, border:'1px solid '+borderColor,
                                                    background:dark?'#262626':'#F5F5F5', display:'flex', alignItems:'center',
                                                    justifyContent:'center', fontSize:10, color:mutedColor } }, '—'),
                                            // Remove button
                                            h('button', { onClick:function(){removeVideo(i)},
                                                style:{ padding:'5px 10px', fontSize:12, borderRadius:5,
                                                        border:'1px solid '+TC.error+'30', background:'transparent',
                                                        color:TC.error, cursor:'pointer' } }, '×')
                                        );
                                    }),
                                    // Add video button
                                    h('button', { onClick:addVideo,
                                        style:{ display:'inline-flex', alignItems:'center', gap:S.xs, padding:'5px '+S.md+'px', borderRadius:6,
                                                border:'1px dashed '+borderColor, background:'transparent', color:mutedColor,
                                                fontSize:12, cursor:'pointer', marginTop: videos.length ? S.sm : 0 } },
                                        h('span', { style:{ fontSize:15, lineHeight:1 } }, '+'), 'Add video'
                                    )
                                )
                            )
                        )
                    ),

                    // ── PRICING / INVENTORY section ───────────────────────────
                    h(Card, { cardBg, borderColor, dark, style:{ marginBottom:S.lg } },
                        h(SectionHead, { id:'pricing', title:'Pricing / inventory', ctx:edCtx }),
                        open.pricing && h('div', null,

                            h(FR, { label:'Product Code', required:true, ctx:edCtx },
                                h('input', { value:form.product_code, onChange:e=>sf('product_code',e.target.value), style:fs })
                            ),

                            h(FR, { label:'List price (' + tcMoneySymbol() + ')', note:'Manufacturer suggested retail price.', ctx:edCtx },
                                h('input', { type:'number', min:'0', step:'0.01', value:form.list_price,
                                    onChange:e=>sf('list_price',e.target.value), style:inputSm })
                            ),

                            h(FR, { label:'Cost price (' + tcMoneySymbol() + ')', note:'Your cost/wholesale price. Used for profit tracking — not shown to customers.', ctx:edCtx },
                                h('input', { type:'number', min:'0', step:'0.01', value:form.cost_price,
                                    onChange:e=>sf('cost_price',e.target.value), style:inputSm })
                            ),

                            h(FR, { label:'In stock', ctx:edCtx },
                                h('input', { type:'number', min:'0', value:form.amount,
                                    onChange:e=>sf('amount',e.target.value), style:Object.assign({},fs,{ width:100 }) })
                            ),

                            h(FR, { label:'Zero price action', ctx:edCtx },
                                h('select', { value:form.zero_price_action, onChange:e=>sf('zero_price_action',e.target.value), style:Object.assign({},fs,{ width:'auto', minWidth:260 }) },
                                    h('option',{value:'do_not_allow'},'Global (Do not allow customers)'),
                                    h('option',{value:'allow'},'Allow'),
                                    h('option',{value:'ask'},'Ask customer to enter a price')
                                )
                            ),

                            h(FR, { label:'Digital product', note:'Tick this for downloadable items, license keys, or anything delivered electronically (e.g. plugins sold via Software Licensing). Digital products have infinite supply — the Track inventory setting below is ignored at cart and checkout when this is on. Auto-unchecks "Requires shipping" — re-check it on the Shipping tab if this is a vinyl + digital bundle (single product that is BOTH downloadable AND physically shipped).', ctx:edCtx },
                                h('label', { style:{ display:'flex', alignItems:'center', gap:S.sm, fontSize:13, cursor:'pointer', paddingTop:6 } },
                                    h('input', { type:'checkbox', checked:form.is_downloadable, onChange:e=>{
                                        var checked = e.target.checked;
                                        sf('is_downloadable', checked);
                                        // S138 Phase 5: auto-uncheck requires_shipping when toggling to
                                        // digital. The admin can manually re-check it on the Shipping tab
                                        // for the bundle case (vinyl + digital download in one product).
                                        // No auto-action on uncheck — preserves admin's prior intent for
                                        // shipping when reverting a product back from digital.
                                        if (checked) {
                                            sf('requires_shipping', false);
                                        }
                                    }, style:{ accentColor:TC.accent } }),
                                    'This product is delivered digitally (no inventory, no shipping required)'
                                )
                            ),

                            // Contextual warning: the storefront-wide downloadable-products
                            // master toggle (catalog_enable_downloads) is OFF, so this digital
                            // product can't be added to the cart. Surface it here with a one-
                            // click enable instead of letting it fail silently for customers.
                            // Admin context only (vendors can't change store-wide settings).
                            Config.spa_mode !== 'vendor' && form.is_downloadable && dlMasterOn === false && h('div', {
                                style:{ margin:'4px 0 12px', padding:'10px 12px', borderRadius:8, border:'1px solid #F3C97D', background:'#FFF8EC', fontSize:13, lineHeight:1.45 }
                            },
                                h('div', { style:{ color:'#7A4E00', fontWeight:600, marginBottom:3 } }, 'Downloadable products are turned off storefront-wide'),
                                h('div', { style:{ color:'#8A6A2E', marginBottom:8 } }, "Customers can't add this digital product to the cart until you enable them. A file in the Downloads tab is optional — products delivered by an external licensing system still work."),
                                h('button', { type:'button', disabled:dlEnabling, onClick:enableDownloads,
                                    style:{ background:'#D97706', color:'#fff', border:'none', borderRadius:6, padding:'6px 12px', fontSize:12.5, fontWeight:600, cursor: dlEnabling ? 'default' : 'pointer', opacity: dlEnabling ? 0.7 : 1 } },
                                    dlEnabling ? 'Enabling…' : 'Enable downloadable products'
                                ),
                                dlErr && h('div', { style:{ color:'#B42318', fontSize:12, marginTop:6 } }, 'Couldn\'t enable automatically — turn on "Enable downloadable products" in Settings → General → Catalog.')
                            ),

                            h(FR, { label:'Track inventory', note: form.is_downloadable ? 'Disabled — digital products never run out, so inventory tracking does not apply.' : 'When enabled, the stock quantity decreases after each purchase. The global "Track inventory" setting in Settings → General → Catalog acts as a master switch — if it is off, stock is never decremented regardless of this field.', ctx:edCtx },
                                h('select', { value:form.track_inventory ? '1' : '0', disabled: form.is_downloadable, onChange:e=>sf('track_inventory', e.target.value === '1'), style:Object.assign({},fs,{ width:'auto', minWidth:180, opacity: form.is_downloadable ? 0.5 : 1, cursor: form.is_downloadable ? 'not-allowed' : 'pointer' }) },
                                    h('option',{value:'1'},'Yes'),
                                    h('option',{value:'0'},'No')
                                )
                            ),

                            h(FR, { label:'Minimum quantity to buy per product', ctx:edCtx },
                                h('select', { value:form.min_qty, onChange:e=>sf('min_qty',e.target.value), style:Object.assign({},fs,{ width:'auto', minWidth:180 }) },
                                    ['0','1','2','3','5','10','25','50','100'].map(v => h('option',{key:v,value:v}, v==='0'?'Global ('+v+')':v))
                                )
                            ),

                            h(FR, { label:'Maximum quantity to buy per product', ctx:edCtx },
                                h('select', { value:form.max_qty, onChange:e=>sf('max_qty',e.target.value), style:Object.assign({},fs,{ width:'auto', minWidth:180 }) },
                                    ['0','1','2','3','5','10','25','50','100'].map(v => h('option',{key:v,value:v}, v==='0'?'Global ('+v+')':v))
                                )
                            ),

                            h(FR, { label:'Quantity step', ctx:edCtx },
                                h('select', { value:form.qty_step, onChange:e=>sf('qty_step',e.target.value), style:Object.assign({},fs,{ width:'auto', minWidth:180 }) },
                                    ['0','1','2','3','5','10'].map(v => h('option',{key:v,value:v}, v==='0'?'Global ('+v+')':v))
                                )
                            ),

                            h(FR, { label:'Tax class', note:'Determines which tax rates apply to this product at checkout. Manage classes in Tax Rules.', ctx:edCtx },
                                taxClasses.length > 0
                                    ? h('select', { value:form.tax_class_id, onChange:e=>sf('tax_class_id', e.target.value), style:Object.assign({},fs,{ width:'auto', minWidth:200 }) },
                                        h('option', { value:'' }, 'Not taxable'),
                                        taxClasses.map(c => h('option', { key:c.id, value:String(c.id) }, c.name || c.handle || ('Class #'+c.id)))
                                    )
                                    : h('div', { style:{ fontSize:12, color:mutedColor } }, 'No tax classes configured. Add one under Tax Rules to make products taxable.')
                            ),

                            h(FR, { label:'Subscription plans', note:'Hides the "One-Time Purchase" option on the storefront product page, forcing customers to pick a subscription plan. Requires the TitanCart Subscriptions extension.', ctx:edCtx },
                                h('label', { style:{ display:'flex', alignItems:'center', gap:S.sm, fontSize:13, cursor:'pointer', paddingTop:6 } },
                                    h('input', { type:'checkbox', checked:form.subscriptions_only, onChange:e=>sf('subscriptions_only',e.target.checked), style:{ accentColor:TC.accent } }),
                                    'Subscriptions only — suppress One-Time Purchase pill on storefront PDP'
                                )
                            )
                        )
                    ),

                    // ── PRICE PER UNIT section ────────────────────────────────
                    h(Card, { cardBg, borderColor, dark, style:{ marginBottom:S.lg } },
                        h(SectionHead, { id:'perunit', title:'Price per unit', ctx:edCtx }),
                        open.perunit && h('div', null,
                            h(FR, { label:'Unit name', note:"For example, 'ml' or 'g' without quotes.", ctx:edCtx },
                                h('input', { value:form.unit_name, onChange:e=>sf('unit_name',e.target.value), style:inputSm })
                            ),
                            h(FR, { label:'Units in product', note:'Required for automatic calculation of price per unit.', ctx:edCtx },
                                h('input', { value:form.units_in_product, onChange:e=>sf('units_in_product',e.target.value), style:inputSm })
                            ),
                            h(FR, { label:'Show price per X units', note:"For cases when you want to show price 'per 250 ml' instead of 'per ml'.", ctx:edCtx },
                                h('input', { value:form.show_price_per, onChange:e=>sf('show_price_per',e.target.value), style:inputSm })
                            )
                        )
                    ),

                    // ── AVAILABILITY section ──────────────────────────────────
                    h(Card, { cardBg, borderColor, dark, style:{ marginBottom:S.lg } },
                        h(SectionHead, { id:'availability', title:'Availability', ctx:edCtx }),
                        open.availability && h('div', null,
                            h(FR, { label:'User groups', ctx:edCtx },
                                h('div', { style:{ display:'flex', gap:S.xl, alignItems:'center', paddingTop:6 } },
                                    ...['all','guest','registered'].map(function(g) {
                                        const labels = { all:'All', guest:'Guest', registered:'Registered user' };
                                        const checked = form.usergroups.includes(g);
                                        return h('label', { key:g, style:{ display:'flex', alignItems:'center', gap:6, fontSize:13, cursor:'pointer' } },
                                            h('input', { type:'checkbox', checked, onChange:function(e){
                                                sf('usergroups', e.target.checked ? [...form.usergroups, g] : form.usergroups.filter(x=>x!==g));
                                            }, style:{ accentColor:TC.accent } }),
                                            labels[g]
                                        );
                                    })
                                )
                            ),
                            h(FR, { label:'Creation date', ctx:edCtx },
                                h('input', { type:'date', value:form.creation_date || new Date().toISOString().substring(0,10),
                                    onChange:e=>sf('creation_date',e.target.value), style:Object.assign({},fs,{ width:'auto' }) })
                            ),
                            h(FR, { label:'Avail since', ctx:edCtx },
                                h('input', { type:'date', value:form.avail_since, onChange:e=>sf('avail_since',e.target.value), style:Object.assign({},fs,{ width:'auto' }) })
                            ),
                            h(FR, { label:'Out of stock actions', ctx:edCtx,
                                note: "\"Buy in advance\" lets customers buy out-of-stock products and products that will be available since a specific date. \"Sign up for notification\" works only when the product has zero or negative quantity." },
                                h('select', { value:form.out_of_stock_actions, onChange:e=>sf('out_of_stock_actions',e.target.value), style:Object.assign({},fs,{ width:'auto', minWidth:200 }) },
                                    h('option',{value:'none'},'None'),
                                    h('option',{value:'buy'},'Buy in advance'),
                                    h('option',{value:'subscribe'},'Sign up for notification')
                                )
                            )
                        )
                    ),

                    // ── EXTRA section ─────────────────────────────────────────
                    h(Card, { cardBg, borderColor, dark },
                        h(SectionHead, { id:'extra', title:'Extra', ctx:edCtx }),
                        open.extra && h('div', null,
                            h(FR, { label:'Product details view', ctx:edCtx,
                                note:'Override the global product page layout for this product. Leave on "Use global setting" to inherit from Settings → Appearance.' },
                                h('select', { value:form.details_view, onChange:e=>sf('details_view',e.target.value), style:Object.assign({},fs,{ width:'auto', minWidth:240 }) },
                                    h('option',{value:''},'Use global setting'),
                                    h('option',{value:'default'},'Default (50/50)'),
                                    h('option',{value:'wide'},'Wide (60/40 gallery)'),
                                    h('option',{value:'gallery'},'Gallery (single column)')
                                )
                            ),
                            h(FR, { label:'Short description', ctx:edCtx },
                                h(RichTextEditor, { value:form.short_description, onChange:function(html){ sf('short_description',html); },
                                    dark:dark, borderColor:borderColor, minHeight:120, placeholder:'Brief product summary...' })
                            ),
                            h(FR, { label:'Popularity', ctx:edCtx,
                                note:'Product popularity rating based on how many times the storefront has been viewed, number of additions to cart and number of purchases.' },
                                h('input', { type:'number', min:'0', value:form.popularity, onChange:e=>sf('popularity',e.target.value), style:Object.assign({},fs,{ width:100 }) })
                            ),
                            h(FR, { label:'Search words', ctx:edCtx,
                                note:'Enter a few words here, and the product will appear in the results of the built-in search for those words, even if the product name or description doesn\'t have them. Separate different search words by commas.' },
                                h('textarea', { value:form.search_words, onChange:e=>sf('search_words',e.target.value),
                                    rows:3, style:Object.assign({},fs,{ resize:'vertical' }) })
                            ),
                            h(FR, { label:'Promo text', ctx:edCtx },
                                h('textarea', { value:form.promo_text, onChange:e=>sf('promo_text',e.target.value),
                                    rows:4, style:Object.assign({},fs,{ resize:'vertical', fontFamily:'inherit', lineHeight:1.6 }) })
                            ),
                            extActive('age-verification') && h(FR, { label:'Age restricted', ctx:edCtx,
                                note:'When checked, customers must verify their age before viewing this product.' },
                                h('div', { style:{ display:'flex', alignItems:'center', gap:S.md } },
                                    h('label', { style:{ display:'flex', alignItems:'center', gap:S.sm, fontSize:13, cursor:'pointer' } },
                                        h('input', { type:'checkbox', checked:form.age_restricted, onChange:e=>sf('age_restricted',e.target.checked), style:{ accentColor:TC.accent } }),
                                        'This product is age-restricted'
                                    ),
                                    form.age_restricted && h('div', { style:{ display:'flex', alignItems:'center', gap:S.sm } },
                                        h('span', { style:{ fontSize:12, color:mutedColor } }, 'Minimum age:'),
                                        h('input', { type:'number', min:0, max:99, value:form.age_limit, onChange:e=>sf('age_limit',e.target.value),
                                            style:Object.assign({},fs,{ width:70 }), placeholder:'18' }),
                                        h('span', { style:{ fontSize:11, color:mutedColor } }, '(0 = use global default)')
                                    )
                                )
                            )
                        )
                    )
                ),

                // ════════════ SEO TAB ════════════
                // SEO section (slug) only shown after product is saved.
                // Meta data + preview always visible.
                tab==='seo' && h('div', null,

                    // ── SEO section — hidden on new products ───────────────────
                    !isNew && h(Card, { cardBg, borderColor, dark, style:{ marginBottom:S.lg } },
                        h(SectionHead, { id:'seo_name', title:'SEO', ctx:edCtx }),
                        open.seo_name && h('div', null,
                            h(FR, { label:'SEO name', ctx:edCtx },
                                h('div', { style:{ display:'flex' } },
                                    // Category path prefix — show primary category slug chain
                                    h('span', { style:{
                                        padding:'8px 12px', fontSize:13,
                                        background: dark ? '#171717' : '#F5F5F5',
                                        border: `1px solid ${borderColor}`, borderRight:'none',
                                        borderRadius:'8px 0 0 8px', color:mutedColor,
                                        whiteSpace:'nowrap', userSelect:'none',
                                    } },
                                        (function() {
                                            // Build prefix from primary category chain
                                            if (!form.category_ids.length) return '/';
                                            const primary = form.category_ids[0];
                                            const cat = cats.find(function(c){ return Number(c.category_id||c.id) === primary; });
                                            if (!cat) return '/';
                                            // Walk up the breadcrumb slugs if available
                                            const parts = [];
                                            if (cat.breadcrumb && cat.breadcrumb.length) {
                                                cat.breadcrumb.forEach(function(b) { if (b.slug) parts.push(b.slug); });
                                            }
                                            if (cat.slug) parts.push(cat.slug);
                                            return '/' + parts.join('/') + '/';
                                        })()
                                    ),
                                    h('input', { value:form.slug, onChange:e=>sf('slug',e.target.value),
                                        placeholder:'product-url-slug',
                                        style:Object.assign({}, fieldSty(dark,borderColor), { borderRadius:'0 8px 8px 0' }) })
                                ),
                                slugChk.checking && h('div', { style:{ fontSize:11, color:mutedColor, marginTop:6 } }, 'Checking availability\u2026'),
                                !slugChk.checking && slugChk.taken && h('div', { style:{ fontSize:12, color:TC.amber, marginTop:6, lineHeight:1.5 } },
                                    'This slug is already used by another product \u2014 it will be saved as ',
                                    h('strong', null, slugChk.suggestion),
                                    ' unless you change it.')
                            )
                        )
                    ),

                    // ── Meta data section ──────────────────────────────────────
                    h(Card, { cardBg, borderColor, dark, style:{ marginBottom:S.lg } },
                        h(SectionHead, { id:'seo_meta', title:'Meta data', ctx:edCtx }),
                        open.seo_meta && h('div', null,
                            h(FR, { label:'Page title', ctx:edCtx },
                                h('input', { value:form.meta_title, onChange:e=>sf('meta_title',e.target.value),
                                    placeholder:'', style:fieldSty(dark,borderColor) })
                            ),
                            h(FR, { label:'Meta description', ctx:edCtx },
                                h('textarea', { value:form.meta_description, onChange:e=>sf('meta_description',e.target.value),
                                    rows:3, style:Object.assign({},fieldSty(dark,borderColor),{ resize:'vertical' }) })
                            )
                        )
                    ),

                    // ── Google rich snippets preview ───────────────────────────
                    h(Card, { cardBg, borderColor, dark },
                        h(SectionHead, { id:'seo_preview', title:'Google rich snippets preview', ctx:edCtx }),
                        open.seo_preview && h('div', { style:{ paddingTop:S.sm } },
                            h('div', { style:{ fontSize:18, fontWeight:400, color:'#1a0dab', marginBottom:2, cursor:'pointer' } },
                                form.meta_title || form.name || 'Product name'
                            ),
                            h('div', { style:{ fontSize:13, color:'#006621', marginBottom:2 } },
                                (typeof window !== 'undefined' ? window.location.origin : 'https://your-site.com') +
                                (form.slug ? '/' + form.slug + '/' : '/')
                            ),
                            h('div', { style:{ fontSize:13, color: dark ? '#8ab4f8' : '#4d5156' } },
                                [
                                    form.price ? '$' + parseFloat(form.price||0).toFixed(2) : null,
                                    form.amount > 0 || form.amount === '' ? 'In stock' : 'Out of stock',
                                ].filter(Boolean).join(' - ')
                            )
                        )
                    )
                ),

                // ════════════ BULK BUYS TAB ════════════
                tab==='qty' && h(Card, { cardBg, borderColor, dark },

                    // Column headers
                    h('div', { style:{ display:'grid', gridTemplateColumns:'36px 1fr 1fr 1fr auto',
                        gap:S.md, padding:`0 0 ${S.md}px 0`, borderBottom:`1px solid ${borderColor}`,
                        marginBottom:S.sm, alignItems:'center' } },
                        h('span', null),
                        h('span', { style:{ fontSize:12, fontWeight:500, color:mutedColor } }, 'Quantity'),
                        h('div', { style:{ display:'flex', alignItems:'center', gap:4 } },
                            h('span', { style:{ fontSize:12, fontWeight:500, color:mutedColor } }, 'Value'),
                            // ? help icon
                            h('span', { title:'The discount value. For Absolute — a fixed price, for Percent — % off the product price.',
                                style:{ width:15, height:15, borderRadius:'50%', border:`1px solid ${mutedColor}`,
                                        display:'inline-flex', alignItems:'center', justifyContent:'center',
                                        fontSize:9, color:mutedColor, cursor:'help', flexShrink:0 } }, '?')
                        ),
                        h('span', { style:{ fontSize:12, fontWeight:500, color:mutedColor } }, 'Type'),
                        h('span', { style:{ fontSize:12, fontWeight:500, color:mutedColor } }, 'User group')
                    ),

                    // Rows
                    h('div', null,
                        ...qtyDiscounts.map(function(row, i) {
                            return h('div', { key:i, style:{ display:'grid', gridTemplateColumns:'36px 1fr 1fr 1fr auto',
                                gap:S.md, alignItems:'center', marginBottom:S.sm } },
                                // Checkbox
                                h('input', { type:'checkbox', checked:row.checked, onChange:()=>qtyToggle(i),
                                    style:{ accentColor:TC.accent, width:16, height:16, cursor:'pointer' } }),
                                // Quantity
                                h('input', { type:'number', min:'0', value:row.qty,
                                    onChange:e=>qtySet(i,'qty',e.target.value),
                                    style:fieldSty(dark,borderColor) }),
                                // Value
                                h('input', { type:'number', min:'0', step:'0.01', value:row.value,
                                    onChange:e=>qtySet(i,'value',e.target.value),
                                    style:fieldSty(dark,borderColor) }),
                                // Type dropdown
                                h('select', { value:row.type, onChange:e=>qtySet(i,'type',e.target.value),
                                    style:fieldSty(dark,borderColor) },
                                    QTY_TYPES.map(t => h('option',{key:t.v,value:t.v},t.l))
                                ),
                                // User group + actions
                                h('div', { style:{ display:'flex', alignItems:'center', gap:S.sm } },
                                    h('select', { value:row.usergroup, onChange:e=>qtySet(i,'usergroup',e.target.value),
                                        style:Object.assign({},fieldSty(dark,borderColor),{ minWidth:120, flex:1 }) },
                                        QTY_GROUPS.map(g => h('option',{key:g,value:g},g))
                                    ),
                                    h('button', { onClick:qtyAdd,
                                        style:{ padding:`6px ${S.md}px`, borderRadius:6, border:`1px solid ${borderColor}`,
                                                background:'transparent', color:textColor, fontSize:12, fontWeight:500, cursor:'pointer',
                                                whiteSpace:'nowrap', flexShrink:0 } }, 'Add'),
                                    h('button', { onClick:()=>qtyClone(i),
                                        style:{ padding:`6px ${S.md}px`, borderRadius:6, border:`1px solid ${borderColor}`,
                                                background:'transparent', color:textColor, fontSize:12, fontWeight:500, cursor:'pointer',
                                                whiteSpace:'nowrap', flexShrink:0 } }, 'Clone'),
                                    h('button', { onClick:()=>qtyDelete(i),
                                        style:{ width:30, height:30, borderRadius:6, border:`1px solid ${TC.error}30`,
                                                background:'transparent', color:TC.error, fontSize:14, cursor:'pointer',
                                                display:'flex', alignItems:'center', justifyContent:'center', flexShrink:0 } },
                                        ico([h('polyline',{points:'3 6 5 6 21 6'}),h('path',{d:'M19 6l-1 14H6L5 6'}),h('path',{d:'M10 11v6'}),h('path',{d:'M14 11v6'}),h('path',{d:'M9 6V4h6v2'})], 14, '0 0 24 24')
                                    )
                                )
                            );
                        })
                    )
                ),

                // ════════════ TAGS TAB ════════════
                tab==='tags' && h(Card, { cardBg, borderColor, dark },
                    h('div', { style:{ padding:`${S.xl}px` } },
                        h('div', { style:{ marginBottom:S.lg } },
                            h('div', { style:{ fontSize:13, fontWeight:600, color:textColor, marginBottom:4 } }, 'Product Labels'),
                            h('div', { style:{ fontSize:12, color:mutedColor } }, 'Labels help customers find products and improve internal organisation. Press Enter or comma to add.')
                        ),

                        // Tag chips + input
                        h('div', {
                            style:{
                                display:'flex', flexWrap:'wrap', gap:S.sm, alignItems:'center',
                                minHeight:44, padding:`${S.sm}px ${S.md}px`,
                                border:`1px solid ${borderColor}`, borderRadius:8,
                                background: dark ? '#262626' : '#F5F5F5',
                                cursor:'text',
                            },
                            onClick:function(e){ const inp = e.currentTarget.querySelector('input'); if(inp) inp.focus(); }
                        },
                            // Existing tag chips
                            form.tags.map(function(tag, ti) {
                                return h('span', { key:tag+ti,
                                    style:{
                                        display:'inline-flex', alignItems:'center', gap:5,
                                        background: dark ? 'rgba(37,99,235,0.15)' : TC.accentBg,
                                        color: TC.accent,
                                        fontSize:12, fontWeight:500,
                                        padding:'3px 8px 3px 10px', borderRadius:20,
                                        border:`1px solid ${dark ? 'rgba(37,99,235,0.3)' : '#BFDBFE'}`,
                                        lineHeight:1.4,
                                    }},
                                    tag,
                                    h('button', {
                                        onClick:function(e){ e.stopPropagation(); sf('tags', form.tags.filter(function(_,i){ return i!==ti; })); },
                                        style:{ background:'none', border:'none', cursor:'pointer', color:TC.accent,
                                            padding:'0 0 0 2px', fontSize:14, lineHeight:1, opacity:.7,
                                            display:'flex', alignItems:'center' }
                                    }, '×')
                                );
                            }),

                            // Text input
                            h('input', {
                                placeholder: form.tags.length === 0 ? 'Add a label and press Enter…' : 'Add another…',
                                style:{
                                    border:'none', outline:'none', background:'transparent',
                                    fontSize:13, color:textColor, flex:'1 1 120px', minWidth:120,
                                    padding:'3px 0',
                                },
                                onKeyDown: function(e) {
                                    const val = e.target.value.trim().replace(/,$/, '').trim();
                                    if ((e.key === 'Enter' || e.key === ',') && val) {
                                        e.preventDefault();
                                        if (!form.tags.includes(val)) sf('tags', form.tags.concat(val));
                                        e.target.value = '';
                                    }
                                    // Backspace on empty input removes last tag
                                    if (e.key === 'Backspace' && e.target.value === '' && form.tags.length) {
                                        e.preventDefault();
                                        sf('tags', form.tags.slice(0, -1));
                                    }
                                },
                                onBlur: function(e) {
                                    const val = e.target.value.trim().replace(/,$/, '').trim();
                                    if (val && !form.tags.includes(val)) { sf('tags', form.tags.concat(val)); e.target.value = ''; }
                                }
                            })
                        ),

                        // Tag count + clear
                        form.tags.length > 0 && h('div', { style:{ display:'flex', justifyContent:'space-between', alignItems:'center', marginTop:S.sm } },
                            h('span', { style:{ fontSize:11, color:mutedColor } }, form.tags.length + ' label' + (form.tags.length !== 1 ? 's' : '')),
                            h('button', {
                                onClick:function(){ sf('tags', []); },
                                style:{ background:'none', border:'none', cursor:'pointer', fontSize:11, color:mutedColor,
                                    textDecoration:'underline', padding:0 }
                            }, 'Clear all')
                        )
                    )
                ),

                // S124: REWARD POINTS TAB removed — per-product overrides retired
                // in favor of group-keyed earning rates managed on the Loyalty
                // Rewards admin page (Earning Rates tab).

                // ════════════ SHIPPING TAB (only visible after save) ════════════
                tab==='shipping' && !isNew && h('div', null,
                    h(Card, { cardBg, borderColor, dark, style:{ marginBottom:S.lg } },
                        h('h3', { style:{ fontSize:14, fontWeight:600, marginBottom:S.xl } }, 'Weight & Dimensions'),
                        h(FR, { label:'Weight', note:'Product weight used to calculate shipping rates.', ctx:edCtx },
                            h('div', { style:{ display:'flex', alignItems:'center', gap:S.sm } },
                                h('input', { type:'number', min:'0', step:'0.001', value:(form.weight && parseFloat(form.weight) !== 0) ? parseFloat(form.weight) : '',
                                    onChange:e=>sf('weight',e.target.value), placeholder:'0.000',
                                    style:Object.assign({},fs,{ width:120 }) }),
                                h('select', { value:form.weight_unit, onChange:e=>sf('weight_unit',e.target.value),
                                    style:Object.assign({},fs,{ width:70 }) },
                                    h('option',{value:'lb'},'lb'), h('option',{value:'oz'},'oz'),
                                    h('option',{value:'kg'},'kg'), h('option',{value:'g'},'g')
                                )
                            )
                        ),
                        h(FR, { label:'Box dimensions', note:'Length × Width × Height — used for carrier rate calculations.', ctx:edCtx },
                            h('div', { style:{ display:'flex', alignItems:'center', gap:S.sm, flexWrap:'wrap' } },
                                h('input', { type:'number', min:'0', step:'0.01', value:form.length,
                                    onChange:e=>sf('length',e.target.value), placeholder:'Length',
                                    style:Object.assign({},fs,{ width:90 }) }),
                                h('span', { style:{ fontSize:12, color:mutedColor } }, '×'),
                                h('input', { type:'number', min:'0', step:'0.01', value:form.width,
                                    onChange:e=>sf('width',e.target.value), placeholder:'Width',
                                    style:Object.assign({},fs,{ width:90 }) }),
                                h('span', { style:{ fontSize:12, color:mutedColor } }, '×'),
                                h('input', { type:'number', min:'0', step:'0.01', value:form.height,
                                    onChange:e=>sf('height',e.target.value), placeholder:'Height',
                                    style:Object.assign({},fs,{ width:90 }) }),
                                h('select', { value:form.dimension_unit, onChange:e=>sf('dimension_unit',e.target.value),
                                    style:Object.assign({},fs,{ width:65 }) },
                                    h('option',{value:'in'},'in'), h('option',{value:'cm'},'cm'), h('option',{value:'mm'},'mm')
                                )
                            )
                        )
                    ),
                    h(Card, { cardBg, borderColor, dark, style:{ marginBottom:S.lg } },
                        h('h3', { style:{ fontSize:14, fontWeight:600, marginBottom:S.xl } }, 'Shipping Options'),
                        h(FR, { label:'Per-item surcharge (' + tcMoneySymbol() + ')', note:'Added to the shipping cost per item, regardless of shipping method.', ctx:edCtx },
                            h('input', { type:'number', min:'0', step:'0.01', value:form.shipping_freight,
                                onChange:e=>sf('shipping_freight',e.target.value), placeholder:'0.00',
                                style:Object.assign({},fs,{ width:120 }) })
                        ),
                        h(FR, { label:'Free shipping', ctx:edCtx },
                            h('label', { style:{ display:'flex', alignItems:'center', gap:S.sm, fontSize:13, cursor:'pointer', paddingTop:6 } },
                                h('input', { type:'checkbox', checked:form.free_shipping, onChange:e=>sf('free_shipping',e.target.checked), style:{ accentColor:TC.accent } }),
                                'This product ships free (overrides all shipping rates)'
                            )
                        ),
                        h(FR, { label:'Requires shipping', note:'When unchecked, the cart skips the shipping section, picker, and cost for this product. Auto-unchecks when "Digital product" is enabled — re-check this box for vinyl + digital download bundles or any product that is BOTH downloadable AND physically shipped.', ctx:edCtx },
                            h('label', { style:{ display:'flex', alignItems:'center', gap:S.sm, fontSize:13, cursor:'pointer', paddingTop:6 } },
                                h('input', { type:'checkbox', checked:form.requires_shipping, onChange:e=>sf('requires_shipping',e.target.checked), style:{ accentColor:TC.accent } }),
                                'This product needs to be physically shipped'
                            )
                        )
                    )
                ),


                // ════════════ ATTRIBUTES TAB (post-save only) ════════════
                tab==='attributes' && !isNew && (function() {
                    // Group attributes by parent group
                    var grouped = {};
                    var ungrouped = [];
                    attrList.forEach(function(attr) {
                        if (attr.group_name) {
                            if (!grouped[attr.group_name]) grouped[attr.group_name] = [];
                            grouped[attr.group_name].push(attr);
                        } else {
                            ungrouped.push(attr);
                        }
                    });

                    function renderAttrInput(attr) {
                        var fid = Number(attr.id);
                        var val = attrValues[fid] || {};
                        var type = attr.feature_type;
                        var hasChoices = (attr.choices||[]).length > 0;

                        // variation_shared → multi-select checkboxes (+ image picker for type I).
                        // Also: a filter-purpose Color-Swatch attribute (type E) → same
                        // multi-select so a product can hold several values (e.g. Color
                        // Family) and each stores its own variant_id row (filterable).
                        if (hasChoices && (attr.purpose === 'variation_shared' || (attr.purpose === 'filter' && type === 'E'))) {
                            var selected = val.variant_ids || (val.variant_id ? [val.variant_id] : []);
                            var varImages = val.variant_images || {};
                            var isImageType = (type === 'I');
                            // S478: per-variant price modifier for shared variations (Large +5.00).
                            var isSharedVar = (attr.purpose === 'variation_shared');
                            var varPrices = val.variant_prices || {};
                            return h('div', { style:{ display:'flex', flexDirection:'column', gap:S.sm, paddingTop:4 } },
                                isSharedVar && h('div', { style:{ fontSize:11, color:mutedColor, lineHeight:1.5 } },
                                    Number(attr.global_prices)
                                        ? 'Tick the choices this product comes in. This attribute has site-wide price changes (set in Attributes → Choices); leave a box blank to use them, or enter a number (even 0) to override for this product.'
                                        : 'Tick the choices this product comes in. A ticked choice can change the price: enter how much it adds to (or, with a minus, takes off) the product price of '+(Config.currency_symbol||'$')+(parseFloat(form.price)||0).toFixed(2)+'. Blank = same price.'),
                                (attr.choices||[]).map(function(c) {
                                    var cid = Number(c.id);
                                    var checked = selected.indexOf(cid) >= 0;
                                    var imgId = varImages[cid] || null;
                                    var imgObj = imgId ? images.find(function(im){ return Number(im.image_id||im.id) === imgId; }) : null;
                                    var imgThumb = imgObj ? (imgObj.thumbnail||imgObj.thumb||imgObj.url) : null;
                                    var priceVal = (varPrices[cid] != null && varPrices[cid] !== '') ? varPrices[cid] : '';

                                    return h('div', { key:c.id, style:{ display:'flex', alignItems:'center', gap:S.sm } },
                                        h('label', { style:{ display:'flex', alignItems:'center', gap:S.xs, fontSize:13, cursor:'pointer',
                                            padding:'4px 10px', borderRadius:6, border:'1px solid '+(checked?TC.accent:borderColor),
                                            background:checked?(dark?'rgba(37,99,235,.12)':'rgba(37,99,235,.06)'):'transparent',
                                            color:checked?TC.accent:textColor, fontWeight:checked?500:400, transition:'all .15s',
                                            minWidth:120 } },
                                            h('input', { type:'checkbox', checked:checked,
                                                onChange:function() {
                                                    var next = checked ? selected.filter(function(x){return x!==cid;}) : selected.concat([cid]);
                                                    attrSetValue(fid, 'variant_ids', next);
                                                    // Clean up image for unchecked variants
                                                    if (checked && varImages[cid]) {
                                                        var vi = Object.assign({}, varImages);
                                                        delete vi[cid];
                                                        attrSetValue(fid, 'variant_images', vi);
                                                    }
                                                    // S478: and its price modifier
                                                    if (checked && varPrices[cid] != null) {
                                                        var vp = Object.assign({}, varPrices);
                                                        delete vp[cid];
                                                        attrSetValue(fid, 'variant_prices', vp);
                                                    }
                                                },
                                                style:{ accentColor:TC.accent, marginRight:2 } }),
                                            c.name||'—'
                                        ),
                                        // S478: price modifier — only for shared variations and when checked.
                                        // Blank = the attribute's site-wide amount (shown as the placeholder) when it has one, else no change.
                                        isSharedVar && checked && h('div', { style:{ display:'flex', alignItems:'center', gap:4 }, title:(c.price_modifier != null ? 'Site-wide: +'+(Config.currency_symbol||'$')+Number(c.price_modifier).toFixed(2)+'. Blank uses it; a number (even 0) overrides it for this product.' : 'Amount added to the product price when this choice is selected (minus takes off)') },
                                            h('span', { style:{ fontSize:12, color:mutedColor } }, '+ ' + (Config.currency_symbol||'$')),
                                            h('input', { type:'number', step:'0.01', value:priceVal, placeholder:(c.price_modifier != null ? Number(c.price_modifier).toFixed(2) : '0.00'),
                                                onChange:function(e) {
                                                    var vp = Object.assign({}, varPrices);
                                                    if (e.target.value === '') { delete vp[cid]; } else { vp[cid] = e.target.value; }
                                                    attrSetValue(fid, 'variant_prices', vp);
                                                },
                                                style:Object.assign({}, fs, { fontSize:12, padding:'4px 8px', width:90 }) }),
                                            (priceVal !== '' && !isNaN(parseFloat(priceVal)))
                                                ? h('span', { style:{ fontSize:11, color:mutedColor } }, '= ' + (Config.currency_symbol||'$') + Math.max(0, (parseFloat(form.price)||0) + parseFloat(priceVal)).toFixed(2))
                                                : (c.price_modifier != null && h('span', { style:{ fontSize:11, color:mutedColor, fontStyle:'italic' } }, 'site-wide \u2192 ' + (Config.currency_symbol||'$') + Math.max(0, (parseFloat(form.price)||0) + Number(c.price_modifier)).toFixed(2)))
                                        ),
                                        // Image picker — only for type I and when checked
                                        isImageType && checked && h('div', { style:{ display:'flex', alignItems:'center', gap:S.xs } },
                                            imgThumb
                                                ? h('img', { src:imgThumb, style:{ width:36, height:48, borderRadius:6, objectFit:'cover', border:'2px solid '+(dark?'#333':borderColor) } })
                                                : h('span', { style:{ width:36, height:48, borderRadius:6, background:dark?'#222':'#f0f0f0', display:'flex', alignItems:'center', justifyContent:'center', fontSize:10, color:mutedColor, border:'1px dashed '+borderColor } }, '?'),
                                            h('select', {
                                                value: imgId || '',
                                                onChange: function(e) {
                                                    var vi = Object.assign({}, varImages);
                                                    if (e.target.value) { vi[cid] = parseInt(e.target.value); }
                                                    else { delete vi[cid]; }
                                                    attrSetValue(fid, 'variant_images', vi);
                                                },
                                                style: Object.assign({}, fs, { fontSize:11, padding:'4px 6px', maxWidth:140 })
                                            },
                                                h('option', { value:'' }, 'Pick image\u2026'),
                                                images.map(function(im, idx) {
                                                    var iid = Number(im.image_id||im.id);
                                                    var label = im.type === 'main' ? 'Main image' : 'Image ' + (idx + 1);
                                                    if (im.alt_text) label = im.alt_text;
                                                    return h('option', { key:iid, value:iid }, label);
                                                })
                                            )
                                        )
                                    );
                                })
                            );
                        }

                        // Any attribute with choices → dropdown (covers S, E, T, N with variants)
                        if (hasChoices && type !== 'M' && type !== 'C') {
                            return h('select', { value:val.variant_id||'',
                                onChange:function(e){ attrSetValue(fid, 'variant_id', e.target.value ? parseInt(e.target.value) : null); },
                                style:Object.assign({}, fs, { fontSize:13 }) },
                                h('option', { value:'' }, '— None —'),
                                (attr.choices||[]).map(function(c) {
                                    return h('option', { key:c.id, value:c.id }, c.name||'—');
                                })
                            );
                        }
                        // Any choice-based type with no choices yet → disabled dropdown
                        // Covers: S (Dropdown), E (Color Swatches), I (Product Images),
                        // and T (Size Pills) when purpose is variation_split/variation_shared
                        var needsChoices = (attr.purpose === 'variation_split' || attr.purpose === 'variation_shared');
                        if (!hasChoices && (type === 'S' || type === 'E' || type === 'I' || (needsChoices && type === 'T'))) {
                            return h('select', { value:'', disabled:true,
                                style:Object.assign({}, fs, { fontSize:13, opacity:0.5 }) },
                                h('option', { value:'' }, 'No choices defined')
                            );
                        }
                        // Multi checkbox → checkboxes
                        if (type === 'M') {
                            var selected = val.value_str ? val.value_str.split(',').map(Number) : [];
                            return h('div', { style:{ display:'flex', flexWrap:'wrap', gap:S.md, paddingTop:4 } },
                                (attr.choices||[]).map(function(c) {
                                    var cid = Number(c.id);
                                    var checked = selected.indexOf(cid) >= 0;
                                    return h('label', { key:c.id, style:{ display:'flex', alignItems:'center', gap:S.xs, fontSize:13, cursor:'pointer' } },
                                        h('input', { type:'checkbox', checked:checked,
                                            onChange:function() {
                                                var next = checked ? selected.filter(function(x){return x!==cid;}) : selected.concat([cid]);
                                                attrSetValue(fid, 'value_str', next.join(','));
                                            },
                                            style:{ accentColor:TC.accent } }),
                                        c.name||'—'
                                    );
                                })
                            );
                        }
                        // Checkbox (Yes/No)
                        if (type === 'C') {
                            return h('select', { value: val.variant_id||val.value_str||'',
                                onChange:function(e){ attrSetValue(fid, 'variant_id', e.target.value ? parseInt(e.target.value) : null); },
                                style:Object.assign({}, fs, { fontSize:13, maxWidth:200 }) },
                                h('option', { value:'' }, '— None —'),
                                (attr.choices||[]).length > 0
                                    ? (attr.choices||[]).map(function(c) { return h('option', { key:c.id, value:c.id }, c.name); })
                                    : [h('option', { key:'y', value:'1' }, 'Yes'), h('option', { key:'n', value:'0' }, 'No')]
                            );
                        }
                        // Number
                        if (type === 'O' || type === 'N') {
                            return h('input', { type:'number', step:'any', value:val.value_float!==undefined && val.value_float!==null ? val.value_float : (val.value_int||''),
                                onChange:function(e){ attrSetValue(fid, 'value_float', e.target.value); },
                                placeholder:'Enter number', style:Object.assign({}, fs, { fontSize:13, maxWidth:200 }) });
                        }
                        // Date
                        if (type === 'D') {
                            return h('input', { type:'date', value:val.value_date||'',
                                onChange:function(e){ attrSetValue(fid, 'value_date', e.target.value); },
                                style:Object.assign({}, fs, { fontSize:13, maxWidth:200 }) });
                        }
                        // Text (default)
                        return h('input', { type:'text', value:val.value_str||val.value_text||'',
                            onChange:function(e){ attrSetValue(fid, 'value_str', e.target.value); },
                            placeholder:'Enter value', style:Object.assign({}, fs, { fontSize:13 }) });
                    }

                    function renderAttrRow(attr) {
                        return h('div', { key:attr.id, style:{ display:'grid', gridTemplateColumns:'180px 1fr', gap:S.lg, alignItems:'start',
                            padding:S.lg+'px 0', borderBottom:'1px solid '+(dark?'rgba(255,255,255,.04)':borderColor) } },
                            h('div', null,
                                h('div', { style:{ fontSize:13, fontWeight:600 } }, attr.name||'—'),
                                attr.purpose && h('span', { style:{ fontSize:10, color:mutedColor } },
                                    ({ filter:'Filter', variation_split:'Variation', variation_shared:'Shared variation', brand:'Brand', info:'Info' })[attr.purpose] || attr.purpose
                                )
                            ),
                            renderAttrInput(attr)
                        );
                    }

                    return h('div', null,
                        h(Card, { cardBg, borderColor, dark },
                            // Header
                            h('div', { style:{ display:'flex', justifyContent:'space-between', alignItems:'center', marginBottom:S.lg } },
                                h('div', null,
                                    h('h3', { style:{ fontSize:14, fontWeight:600, marginBottom:2 } }, 'Product Attributes'),
                                    h('p', { style:{ fontSize:12, color:mutedColor } }, 'Set values for attributes assigned to this product\u2019s categories.')
                                ),
                                h('div', { style:{ display:'flex', gap:S.sm } },
                                    attrMsg.text && h('span', { style:{ fontSize:12, color:attrMsg.ok?TC.success:TC.error, alignSelf:'center', marginRight:S.sm } }, attrMsg.text),
                                    h('button', { onClick:function(){ setAttrQuickShow(!attrQuickShow); },
                                        style:{ padding:'5px 12px', borderRadius:6, border:'1px solid '+borderColor, background:'transparent', fontSize:12, fontWeight:500, cursor:'pointer', color:textColor } },
                                        attrQuickShow ? 'Cancel' : '+ Add Attribute'),
                                    h('button', { onClick:function(){ navigate('attributes'); },
                                        style:{ padding:'5px 12px', borderRadius:6, border:'1px solid '+borderColor, background:'transparent', fontSize:12, fontWeight:500, cursor:'pointer', color:mutedColor } },
                                        'Manage Attributes'),
                                    h('button', { onClick:attrSave, disabled:attrSaving,
                                        style:{ padding:'5px 14px', borderRadius:6, border:'none', background:TC.accent, color:'#fff', fontSize:12, fontWeight:500, cursor:'pointer', opacity:attrSaving?.5:1 } },
                                        attrSaving ? 'Saving\u2026' : 'Save')
                                )
                            ),

                            // Quick-add form
                            attrQuickShow && h('div', { style:{ border:'2px solid '+TC.accent, borderRadius:10, padding:S.lg, marginBottom:S.lg, background:dark?'rgba(37,99,235,.04)':'rgba(37,99,235,.02)' } },
                                h('div', { style:{ fontSize:13, fontWeight:600, marginBottom:S.md } }, 'Quick Add Attribute'),
                                h('div', { style:{ display:'grid', gridTemplateColumns:'1fr 1fr', gap:S.md, marginBottom:S.md } },
                                    h('div', null,
                                        h('label', { style:{ fontSize:12, fontWeight:500, color:mutedColor, display:'block', marginBottom:S.xs } }, 'Attribute Name *'),
                                        h('input', { value:attrQuickForm.name, placeholder:'e.g. Color, Material',
                                            onChange:function(e){ setAttrQuickForm(function(p){ return Object.assign({},p,{name:e.target.value}); }); },
                                            style:Object.assign({}, fs, { fontSize:13 }) })
                                    ),
                                    h('div', null,
                                        h('label', { style:{ fontSize:12, fontWeight:500, color:mutedColor, display:'block', marginBottom:S.xs } }, 'Choices (comma separated)'),
                                        h('input', { value:attrQuickForm.choices, placeholder:'Red, Blue, Green',
                                            onChange:function(e){ setAttrQuickForm(function(p){ return Object.assign({},p,{choices:e.target.value}); }); },
                                            style:Object.assign({}, fs, { fontSize:13 }) }),
                                        h('p', { style:{ fontSize:10, color:mutedColor, marginTop:2 } }, 'Leave empty for text/number input. Use commas to separate choices.')
                                    )
                                ),
                                h('div', { style:{ display:'flex', justifyContent:'space-between', alignItems:'center' } },
                                    h('button', { onClick:function(){ navigate('attributes'); },
                                        style:{ padding:'5px 12px', borderRadius:6, border:'1px solid '+borderColor, background:'transparent', fontSize:12, fontWeight:500, cursor:'pointer', color:TC.accent } },
                                        'Advanced attribute creation \u2192'),
                                    h('button', { onClick:attrQuickCreate, disabled:attrQuickSaving||!attrQuickForm.name.trim(),
                                        style:{ padding:'6px 16px', borderRadius:6, border:'none', background:TC.accent, color:'#fff', fontSize:12, fontWeight:500, cursor:'pointer', opacity:attrQuickSaving||!attrQuickForm.name.trim()?.5:1 } },
                                        attrQuickSaving ? 'Creating\u2026' : 'Create')
                                )
                            ),

                            // Loading
                            attrLoading && h('div', { style:{ textAlign:'center', padding:S.xxl, color:mutedColor, fontSize:13 } }, 'Loading attributes\u2026'),

                            // Empty state
                            !attrLoading && attrList.length === 0 && h('div', { style:{ textAlign:'center', padding:S.xxl+'px 0', color:mutedColor, fontSize:13 } },
                                'No attributes apply to this product yet. Create attributes on the Attributes page and assign them to this product\u2019s categories.'
                            ),

                            // Grouped attributes
                            !attrLoading && Object.keys(grouped).map(function(groupName) {
                                return h('div', { key:groupName, style:{ marginBottom:S.lg } },
                                    h('div', { style:{ fontSize:12, fontWeight:600, color:TC.accent, textTransform:'uppercase', letterSpacing:'.5px', marginBottom:S.sm, paddingBottom:S.xs, borderBottom:'1px solid '+borderColor } }, groupName),
                                    grouped[groupName].map(renderAttrRow)
                                );
                            }),

                            // Ungrouped attributes
                            !attrLoading && ungrouped.map(renderAttrRow)
                        )
                    );
                })(),

                // ════════════ PRODUCT GROUP TAB (post-save only) ════════════
                tab==='product-group' && !isNew && (function() {

                    return h('div', null,
                        h(Card, { cardBg, borderColor, dark },
                            // Header row
                            h('div', { style:{ display:'flex', justifyContent:'space-between', alignItems:'flex-start', marginBottom:S.xl } },
                                h('div', null,
                                    h('div', { style:{ display:'flex', alignItems:'center', gap:S.md, marginBottom:S.sm } },
                                        h('h3', { style:{ fontSize:15, fontWeight:600 } }, 'Product Group'),
                                        pgGroup && h('span', { style:{ fontSize:11, color:mutedColor, background:dark?'#1E1E1E':'#F5F5F5', padding:'2px 8px', borderRadius:10 } },
                                            pgMembers.length + ' product' + (pgMembers.length === 1 ? '' : 's'))
                                    ),
                                    pgGroup
                                        ? h('div', { style:{ display:'flex', alignItems:'center', gap:S.sm } },
                                            h('span', { style:{ fontSize:12, color:mutedColor } }, 'Group ID'),
                                            h('input', { type:'text', value:pgCodeEdit,
                                                onChange:function(e){ setPgCodeEdit(e.target.value); },
                                                onBlur:function(){ if (pgCodeEdit.trim() && pgCodeEdit.trim() !== pgGroup.group_code) pgSaveCode(); },
                                                onKeyDown:function(e){ if (e.key === 'Enter') { e.target.blur(); } },
                                                style:{ fontSize:12, fontFamily:'monospace', fontWeight:500, padding:'4px 10px', borderRadius:6,
                                                        border:'1px solid '+borderColor, background:dark?'#1A1A1A':'#FAFAFA', color:textColor, width:180,
                                                        outline:'none', transition:'border-color .15s' } }),
                                            pgCodeSaving && h('span', { style:{ fontSize:11, color:TC.accent } }, 'Saving\u2026')
                                        )
                                        : h('p', { style:{ fontSize:13, color:mutedColor, lineHeight:1.5 } }, 'Group separate products together so they display on a single product page with attribute selectors.')
                                ),
                                h(Btn, { variant:'primary', onClick:pgOpenModal, mutedColor, borderColor }, '+ Add Variations')
                            ),

                            // roadmap #15: display-as-single-product settings (only when a group exists)
                            pgGroup && pgMembers.length > 0 && h('div', {
                                style:{ border:'1px solid '+borderColor, borderRadius:10, padding:S.xl, marginBottom:S.xl,
                                        background:dark?'#161616':'#FAFAFA' } },
                                h('label', { style:{ display:'flex', alignItems:'flex-start', gap:S.md, cursor:'pointer' } },
                                    h('input', { type:'checkbox', checked:pgDisplaySingle,
                                        onChange:function(e){ var v=e.target.checked; setPgDisplaySingle(v); pgSaveDisplay(v, undefined); },
                                        style:{ marginTop:2, width:16, height:16, cursor:'pointer', accentColor:TC.accent } }),
                                    h('div', null,
                                        h('div', { style:{ fontSize:13, fontWeight:600, color:textColor } }, 'Display as a single product'),
                                        h('div', { style:{ fontSize:12, color:mutedColor, lineHeight:1.5, marginTop:2 } },
                                            'Show this group as ONE product card on the storefront (shop grid, search, sitemap) instead of one card per variation. The product page still shows the full selector, and each variation keeps its own product code.')
                                    )
                                ),
                                pgDisplaySingle && h('div', { style:{ marginTop:S.xl, paddingTop:S.xl, borderTop:'1px solid '+borderColor } },
                                    h('div', { style:{ fontSize:12, fontWeight:600, color:textColor, marginBottom:S.xs } }, 'Storefront name (optional)'),
                                    h('div', { style:{ fontSize:11, color:mutedColor, marginBottom:S.sm, lineHeight:1.5 } },
                                        'The name shown on the single card. Leave blank to use the product name automatically.'),
                                    h('input', { type:'text', value:pgDisplayName,
                                        placeholder:'Leave blank to auto-name',
                                        onChange:function(e){ setPgDisplayName(e.target.value); },
                                        onBlur:function(){ pgSaveDisplay(undefined, pgDisplayName); },
                                        onKeyDown:function(e){ if (e.key === 'Enter') { e.target.blur(); } },
                                        style:{ width:'100%', boxSizing:'border-box', fontSize:13, padding:'8px 12px', borderRadius:8,
                                                border:'1px solid '+borderColor, background:cardBg, color:textColor, outline:'none' } })
                                ),
                                pgDisplaySaving && h('div', { style:{ fontSize:11, color:TC.accent, marginTop:S.sm } }, 'Saving\u2026')
                            ),

                            // Loading
                            pgLoading && h('div', { style:{ textAlign:'center', padding:S.xxl+'px 0', color:mutedColor, fontSize:13 } }, 'Loading\u2026'),

                            // Empty
                            !pgLoading && pgMembers.length === 0 && h('div', { style:{ textAlign:'center', padding:(S.xxl*2)+'px '+S.xxl+'px', color:mutedColor, border:'1px dashed '+borderColor, borderRadius:10 } },
                                h('div', { style:{ fontSize:28, marginBottom:S.md, opacity:.4 } }, '\uD83D\uDCE6'),
                                h('div', { style:{ fontSize:14, fontWeight:500, marginBottom:S.xs } }, 'No variations yet'),
                                h('div', { style:{ fontSize:12, maxWidth:380, margin:'0 auto', lineHeight:1.6 } },
                                    'Click "Add Variations" to create a group of products that differ by attributes like Color or Size. Each variation is a separate product with its own stock and price.')
                            ),

                            // Members table
                            !pgLoading && pgMembers.length > 0 && h('div', { style:{ border:'1px solid '+borderColor, borderRadius:10, overflow:'hidden' } },
                                // Table header
                                h('div', { style:{ display:'grid', gridTemplateColumns:'52px 1fr 150px 120px 100px 70px 80px 44px', gap:0,
                                    padding:'10px '+S.lg+'px', background:dark?'rgba(255,255,255,.02)':'#FAFBFD', borderBottom:'1px solid '+borderColor } },
                                    h('span'),
                                    ['Name','Product Code','Attributes','Price','Qty','Status',''].map(function(col) {
                                        return h('span', { key:col, style:{ fontSize:10, fontWeight:600, color:mutedColor, textTransform:'uppercase', letterSpacing:'.6px' } }, col);
                                    })
                                ),
                                // Table rows
                                pgMembers.map(function(m, i) {
                                    var pid = Number(m.id || m.product_id);
                                    var isSelf = pid === numId;
                                    var imgUrl = m.images && m.images.length > 0 ? m.images[0].url : null;
                                    var isLast = i === pgMembers.length - 1;
                                    return h('div', { key:pid,
                                        style:{ display:'grid', gridTemplateColumns:'52px 1fr 150px 120px 100px 70px 80px 44px', gap:0,
                                            padding:'12px '+S.lg+'px', borderBottom:isLast?'none':'1px solid '+(dark?'rgba(255,255,255,.04)':borderColor),
                                            alignItems:'center', transition:'background .12s',
                                            background:isSelf?(dark?'rgba(37,99,235,.04)':'rgba(37,99,235,.02)'):'transparent' },
                                        onMouseEnter:function(e){ if(!isSelf) e.currentTarget.style.background=dark?'rgba(255,255,255,.02)':'#F9FAFB'; },
                                        onMouseLeave:function(e){ if(!isSelf) e.currentTarget.style.background='transparent'; else e.currentTarget.style.background=dark?'rgba(37,99,235,.04)':'rgba(37,99,235,.02)'; } },

                                        // Image
                                        imgUrl
                                            ? h('img', { src:imgUrl, style:{ width:40, height:Config.productImageShape==='square'?40:53, borderRadius:8, objectFit:'cover', border:'1px solid '+borderColor } })
                                            : h('div', { style:{ width:40, height:Config.productImageShape==='square'?40:53, borderRadius:8, background:dark?'#1E1E1E':'#F0F2F5', display:'flex', alignItems:'center', justifyContent:'center',
                                                border:'1px solid '+(dark?'rgba(255,255,255,.06)':borderColor) } },
                                                h('svg', { width:16, height:16, viewBox:'0 0 16 16', fill:'none', stroke:mutedColor, strokeWidth:'1.2' },
                                                    h('rect', { x:1, y:3, width:14, height:10, rx:1.5 }),
                                                    h('circle', { cx:5, cy:7, r:1.5 }),
                                                    h('path', { d:'M1 11l4-3 3 2 3-2 4 3' })
                                                )
                                            ),

                                        // Name
                                        h('div', { style:{ paddingRight:S.sm, minWidth:0, cursor:'pointer' },
                                            onClick:function(){ if(!isSelf) navigate('products/' + pid); } },
                                            h('div', { style:{ fontSize:13, fontWeight:isSelf?600:500, whiteSpace:'nowrap', overflow:'hidden', textOverflow:'ellipsis',
                                                color:isSelf?textColor:TC.accent, textDecoration:isSelf?'none':'none' },
                                                onMouseEnter:function(e){ if(!isSelf) e.currentTarget.style.textDecoration='underline'; },
                                                onMouseLeave:function(e){ e.currentTarget.style.textDecoration='none'; } }, m.name || '\u2014'),
                                            isSelf && h('span', { style:{ fontSize:10, color:TC.accent, fontWeight:500 } }, 'Current product')
                                        ),

                                        // Code (S478: inline-editable)
                                        h('div', { style:{ minWidth:0, paddingRight:S.sm } },
                                            h('input', { key:pid+'-sku-'+(m.sku||''), type:'text', defaultValue:m.sku||'', title:'Product code \u2014 click to edit',
                                                onBlur:function(e){ var v=e.target.value.trim(); if (v && v !== (m.sku||'')) pgSaveMember(pid, 'sku', v); else if (!v) e.target.value = m.sku||''; },
                                                onKeyDown:function(e){ if (e.key==='Enter') e.target.blur(); if (e.key==='Escape') { e.target.value = m.sku||''; e.target.blur(); } },
                                                style:{ width:'100%', boxSizing:'border-box', fontSize:11, fontFamily:'monospace', color:mutedColor, padding:'4px 6px', borderRadius:6,
                                                        border:'1px solid transparent', background:'transparent', outline:'none', transition:'border-color .15s' },
                                                onFocus:function(e){ e.target.style.borderColor=borderColor; e.target.style.background=cardBg; e.target.style.color=textColor; },
                                                onMouseEnter:function(e){ e.target.style.borderColor=borderColor; },
                                                onMouseLeave:function(e){ if (document.activeElement!==e.target) e.target.style.borderColor='transparent'; } })
                                        ),

                                        // Combination chips
                                        h('div', { style:{ display:'flex', flexWrap:'wrap', gap:3 } },
                                            (m.combination_label || '\u2014').split(' / ').map(function(part, pi) {
                                                return h('span', { key:pi, style:{ fontSize:10, fontWeight:500, padding:'2px 7px', borderRadius:4,
                                                    background:dark?'rgba(255,255,255,.06)':'#F0F2F5', color:dark?'#B0B8C4':TC.muted,
                                                    whiteSpace:'nowrap' } }, part);
                                            })
                                        ),

                                        // Price (S478: inline-editable — the group's answer to "sizes cost more")
                                        h('div', { style:{ display:'flex', alignItems:'center', gap:2, paddingRight:S.sm } },
                                            h('span', { style:{ fontSize:12, color:mutedColor } }, Config.currency_symbol||'$'),
                                            h('input', { key:pid+'-price-'+m.price, type:'number', step:'0.01', min:0, defaultValue:(parseFloat(m.price)||0).toFixed(2), title:'Price \u2014 click to edit',
                                                onBlur:function(e){ var v=parseFloat(e.target.value); if (isNaN(v)) { e.target.value=(parseFloat(m.price)||0).toFixed(2); return; } if (Math.abs(v-(parseFloat(m.price)||0))>0.0001) pgSaveMember(pid, 'price', v); },
                                                onKeyDown:function(e){ if (e.key==='Enter') e.target.blur(); if (e.key==='Escape') { e.target.value=(parseFloat(m.price)||0).toFixed(2); e.target.blur(); } },
                                                style:{ width:'100%', boxSizing:'border-box', fontSize:13, fontWeight:500, padding:'4px 6px', borderRadius:6, color:textColor,
                                                        border:'1px solid transparent', background:'transparent', outline:'none', transition:'border-color .15s' },
                                                onFocus:function(e){ e.target.style.borderColor=borderColor; e.target.style.background=cardBg; },
                                                onMouseEnter:function(e){ e.target.style.borderColor=borderColor; },
                                                onMouseLeave:function(e){ if (document.activeElement!==e.target) e.target.style.borderColor='transparent'; } })
                                        ),

                                        // Qty (S478: inline-editable)
                                        h('input', { key:pid+'-qty-'+m.stock_quantity, type:'number', step:'1', min:0, defaultValue:m.stock_quantity||0, title:'Quantity \u2014 click to edit',
                                            onBlur:function(e){ var v=parseInt(e.target.value,10); if (isNaN(v)) { e.target.value=m.stock_quantity||0; return; } if (v !== (parseInt(m.stock_quantity,10)||0)) pgSaveMember(pid, 'stock_quantity', v); },
                                            onKeyDown:function(e){ if (e.key==='Enter') e.target.blur(); if (e.key==='Escape') { e.target.value=m.stock_quantity||0; e.target.blur(); } },
                                            style:{ width:'100%', boxSizing:'border-box', fontSize:13, color:mutedColor, textAlign:'center', padding:'4px 4px', borderRadius:6,
                                                    border:'1px solid transparent', background:'transparent', outline:'none', transition:'border-color .15s' },
                                            onFocus:function(e){ e.target.style.borderColor=borderColor; e.target.style.background=cardBg; e.target.style.color=textColor; },
                                            onMouseEnter:function(e){ e.target.style.borderColor=borderColor; },
                                            onMouseLeave:function(e){ if (document.activeElement!==e.target) e.target.style.borderColor='transparent'; } }),

                                        // Status (+ saving indicator)
                                        pgMemberSaving === pid
                                            ? h('span', { style:{ fontSize:11, color:TC.accent } }, 'Saving\u2026')
                                            : h(Badge, { status:m.status==='active'?'active':'inactive', dark }, m.status==='active'?'Active':'Off'),

                                        // Remove
                                        !isSelf
                                            ? h('button', { onClick:function(){ pgRemove(pid); }, title:'Remove from group',
                                                style:{ width:28, height:28, borderRadius:6, border:'1px solid '+(dark?'rgba(255,255,255,.08)':borderColor),
                                                    background:'transparent', cursor:'pointer', display:'flex', alignItems:'center', justifyContent:'center',
                                                    color:mutedColor, transition:'all .15s' },
                                                onMouseEnter:function(e){ e.currentTarget.style.borderColor=TC.error; e.currentTarget.style.color=TC.error; },
                                                onMouseLeave:function(e){ e.currentTarget.style.borderColor=dark?'rgba(255,255,255,.08)':borderColor; e.currentTarget.style.color=mutedColor; } },
                                                h('svg', { width:14, height:14, viewBox:'0 0 14 14', fill:'none', stroke:'currentColor', strokeWidth:'1.5', strokeLinecap:'round' },
                                                    h('path', { d:'M3.5 3.5l7 7M10.5 3.5l-7 7' })
                                                )
                                            )
                                            : h('span')
                                    );
                                })
                            ),
                            // S478: inline-edit hint
                            !pgLoading && pgMembers.length > 0 && h('p', { style:{ fontSize:11, color:mutedColor, margin:S.sm+'px 0 0', lineHeight:1.5 } },
                                'Click a price, quantity or product code to change it here \u2014 it saves when you leave the field. Everything else lives in each variation\u2019s own editor.')
                        ),

                        // ═══════════ ADD VARIATIONS MODAL ═══════════
                        pgShowModal && h('div', { style:{ position:'fixed', inset:0, background:'rgba(0,0,0,.5)', display:'flex', alignItems:'center', justifyContent:'center', zIndex:9999 } },
                            h('div', { style:{ background:cardBg, borderRadius:16, width:720, maxHeight:'90vh', display:'flex', flexDirection:'column', boxShadow:'0 20px 60px rgba(0,0,0,.3)' } },
                                // Header
                                h('div', { style:{ padding:S.xl+'px '+S.xxl+'px', borderBottom:'1px solid '+borderColor, display:'flex', justifyContent:'space-between', alignItems:'center', flexShrink:0 } },
                                    h('h3', { style:{ fontSize:16, fontWeight:600 } }, 'Add Variations'),
                                    h('button', { onClick:function(){ setPgShowModal(false); }, style:{ background:'transparent', border:'none', fontSize:22, cursor:'pointer', color:mutedColor } }, '\u00D7')
                                ),
                                // Tabs
                                h('div', { style:{ display:'flex', borderBottom:'1px solid '+borderColor, flexShrink:0 } },
                                    [{ v:'create', l:'Create new products' }, { v:'existing', l:'Use existing products' }].map(function(t) {
                                        var isA = pgModalTab === t.v;
                                        return h('button', { key:t.v, onClick:function(){ setPgModalTab(t.v); },
                                            style:{ flex:1, padding:'12px 0', fontSize:13, fontWeight:isA?600:400, cursor:'pointer',
                                                    background:isA?(dark?'rgba(37,99,235,.08)':'rgba(37,99,235,.05)'):'transparent',
                                                    color:isA?TC.accent:mutedColor, border:'none', borderBottom:isA?'2px solid '+TC.accent:'2px solid transparent' } }, t.l);
                                    })
                                ),
                                // Body
                                h('div', { style:{ flex:1, overflow:'auto', padding:S.xxl } },

                                    // ── CREATE TAB ──
                                    pgModalTab === 'create' && (function() {
                                        var combos = pgGetCombinations();
                                        var totalNew = combos.length;
                                        return h('div', null,
                                            // Error
                                            pgError && h('div', { style:{ background:dark?'rgba(220,38,38,.1)':'#FEF2F2', border:'1px solid '+(dark?'rgba(220,38,38,.3)':TC.error), borderRadius:8, padding:S.md, marginBottom:S.lg, fontSize:12, color:TC.error } }, pgError),
                                            // Loading
                                            pgFeatLoad && h('div', { style:{ color:mutedColor, fontSize:12, padding:S.lg, textAlign:'center' } }, 'Loading variation attributes\u2026'),
                                            // No variation features
                                            !pgFeatLoad && pgFeatures.length === 0 && h('div', { style:{ background:dark?'rgba(255,255,255,.03)':'#FFFBEB', border:'1px solid '+(dark?'rgba(217,167,6,.3)':'#FCD34D'), borderRadius:8, padding:S.lg, fontSize:12, color:dark?'#FBBF24':TC.amber } },
                                                'No variation attributes with saved values found for this product. To create variations:',
                                                h('ol', { style:{ margin:S.sm+'px 0 0 '+S.lg+'px', lineHeight:1.8 } },
                                                    h('li', null, 'Create an attribute with purpose "Variations (separate products)" on the Attributes page \u2014 e.g. Color, Size'),
                                                    h('li', null, 'Go to this product\u2019s Attributes tab and set values (e.g. Color = Blue, Size = Small)'),
                                                    h('li', null, 'Come back here to create the other variations')
                                                )
                                            ),
                                            // S478: attribute pricing mode (shown once a variation attribute exists)
                                            !pgFeatLoad && pgFeatures.length > 0 && h('div', { style:{ marginBottom:S.lg, padding:S.md+'px '+S.lg+'px', border:'1px solid '+borderColor, borderRadius:10,
                                                    display:'flex', alignItems:'center', gap:S.lg, flexWrap:'wrap', background:dark?'rgba(37,99,235,.04)':'rgba(37,99,235,.03)' } },
                                                h('span', { style:{ fontSize:12, fontWeight:600 } }, 'Attribute pricing'),
                                                [{ v:'add', l:'Add to this product\u2019s price' }, { v:'set', l:'Set each variation\u2019s price' }].map(function(o) {
                                                    return h('label', { key:o.v, style:{ display:'flex', alignItems:'center', gap:6, fontSize:12, cursor:'pointer' } },
                                                        h('input', { type:'radio', name:'pg-price-mode', checked:pgPriceMode===o.v, onChange:function(){ setPgPriceMode(o.v); }, style:{ accentColor:TC.accent } }),
                                                        o.l
                                                    );
                                                }),
                                                h('span', { style:{ fontSize:11, color:mutedColor, flexBasis:'100%' } },
                                                    pgPriceMode === 'set'
                                                        ? 'Enter the full price beside each selected choice. Blank = this product\u2019s price ('+(Config.currency_symbol||'$')+(parseFloat(form.price)||0).toFixed(2)+'). With several attributes the first attribute\u2019s amount wins.'
                                                        : 'Enter how much each choice adds to (or, with a minus, takes off) this product\u2019s price of '+(Config.currency_symbol||'$')+(parseFloat(form.price)||0).toFixed(2)+'. Blank = no change. With several attributes the amounts add up.')
                                            ),
                                            // Attribute rows — one per variation feature
                                            !pgFeatLoad && pgFeatures.map(function(feat) {
                                                var fid = Number(feat.id || feat.feature_id);
                                                var choices = feat._choices || [];
                                                var selected = pgSelections[fid] || [];
                                                var priced = choices.filter(function(c){ return selected.includes(Number(c.id)); });
                                                return h('div', { key:fid, style:{ marginBottom:S.lg, padding:S.lg, border:'1px solid '+borderColor, borderRadius:10, background:dark?'rgba(255,255,255,.02)':'#FAFAFA' } },
                                                    h('div', { style:{ display:'flex', justifyContent:'space-between', alignItems:'center', marginBottom:S.md } },
                                                        h('label', { style:{ fontSize:14, fontWeight:600 } }, feat.name || 'Attribute'),
                                                        h('button', { onClick:function(){ pgAddAllChoices(fid); },
                                                            style:{ background:'transparent', border:'none', color:TC.accent, fontSize:12, cursor:'pointer' } }, 'Add all choices')
                                                    ),
                                                    choices.length === 0 && h('span', { style:{ fontSize:12, color:mutedColor } }, 'No choices available.'),
                                                    choices.length > 0 && h('div', { style:{ display:'flex', flexWrap:'wrap', gap:S.sm } },
                                                        choices.map(function(c) {
                                                            var cid = Number(c.id);
                                                            var isParent = cid === feat._parentVariantId;
                                                            var sel = selected.includes(cid);
                                                            if (isParent) {
                                                                return h('button', { key:cid, disabled:true,
                                                                    style:{ padding:'5px 12px', borderRadius:16, fontSize:12, fontWeight:500,
                                                                            border:'2px solid '+TC.success, cursor:'default', opacity:0.7,
                                                                            background:dark?'rgba(22,163,74,.1)':'rgba(22,163,74,.06)', color:TC.success } },
                                                                    '\u2713 ', c.name || '\u2014', ' (this product)'
                                                                );
                                                            }
                                                            return h('button', { key:cid, onClick:function(){ pgToggleChoice(fid, cid); },
                                                                style:{ padding:'5px 12px', borderRadius:16, fontSize:12, fontWeight:500, cursor:'pointer',
                                                                        border: sel ? '2px solid '+TC.accent : '1px solid '+borderColor,
                                                                        background: sel ? (dark?'rgba(37,99,235,.12)':'rgba(37,99,235,.06)') : 'transparent',
                                                                        color: sel ? TC.accent : textColor } },
                                                                sel && h('span', { style:{ marginRight:4 } }, '\u2713 '),
                                                                c.name || '\u2014'
                                                            );
                                                        })
                                                    ),
                                                    // S478: one amount per selected choice
                                                    priced.length > 0 && h('div', { style:{ marginTop:S.md, paddingTop:S.md, borderTop:'1px solid '+borderColor,
                                                            display:'grid', gridTemplateColumns:'repeat(auto-fill, minmax(190px, 1fr))', gap:S.sm } },
                                                        priced.map(function(c) {
                                                            var cid = Number(c.id);
                                                            var cur = (pgPrices[fid] && pgPrices[fid][cid] != null) ? pgPrices[fid][cid] : '';
                                                            return h('label', { key:cid, style:{ display:'flex', alignItems:'center', gap:6, fontSize:12 } },
                                                                h('span', { style:{ flex:1, minWidth:0, whiteSpace:'nowrap', overflow:'hidden', textOverflow:'ellipsis' } }, c.name || ('V'+cid)),
                                                                h('span', { style:{ color:mutedColor, fontSize:11 } }, pgPriceMode === 'set' ? (Config.currency_symbol||'$') : '+'),
                                                                h('input', { type:'number', step:'0.01', value:cur, placeholder: pgPriceMode === 'set' ? (parseFloat(form.price)||0).toFixed(2) : '0.00',
                                                                    onChange:function(e){ pgSetPrice(fid, cid, e.target.value); },
                                                                    style:{ width:84, fontSize:12, padding:'5px 8px', borderRadius:6, border:'1px solid '+borderColor, background:cardBg, color:textColor, outline:'none' } })
                                                            );
                                                        })
                                                    )
                                                );
                                            }),
                                            // Preview — cartesian product
                                            totalNew > 0 && h('div', { style:{ marginTop:S.lg } },
                                                h('div', { style:{ fontSize:12, fontWeight:600, marginBottom:S.sm } }, totalNew + ' new product' + (totalNew===1?'':'s') + ' to be created:'),
                                                h('div', { style:{ display:'grid', gridTemplateColumns:'1.2fr 1.8fr 80px', gap:S.sm, padding:S.sm+'px 0', borderBottom:'1px solid '+borderColor } },
                                                    ['Attributes','Name','Price'].map(function(col) {
                                                        return h('span', { key:col, style:{ fontSize:10, fontWeight:500, color:mutedColor, textTransform:'uppercase' } }, col);
                                                    })
                                                ),
                                                h('div', { style:{ maxHeight:200, overflowY:'auto' } },
                                                    combos.map(function(combo, ci) {
                                                        var parts = Object.keys(combo).map(function(fid) {
                                                            return pgGetVariantName(combo[fid]);
                                                        });
                                                        var label = parts.join(' / ');
                                                        return h('div', { key:ci, style:{ display:'grid', gridTemplateColumns:'1.2fr 1.8fr 80px', gap:S.sm, padding:'6px 0',
                                                            borderBottom:'1px solid '+(dark?'rgba(255,255,255,.04)':borderColor), alignItems:'center' } },
                                                            h('div', { style:{ display:'flex', flexWrap:'wrap', gap:3 } },
                                                                parts.map(function(p, pi) {
                                                                    return h('span', { key:pi, style:{ fontSize:11, fontWeight:500, padding:'2px 8px', borderRadius:4,
                                                                        background:dark?'rgba(255,255,255,.06)':'#F0F2F5', color:dark?'#B0B8C4':TC.text } }, p);
                                                                })
                                                            ),
                                                            h('span', { style:{ fontSize:11, color:mutedColor } }, (form.name||'Product') + ' - ' + label),
                                                            h('span', { style:{ fontSize:12, fontWeight:500 } }, (Config.currency_symbol||'$')+pgPreviewPrice(combo).toFixed(2))
                                                        );
                                                    })
                                                ),
                                                h('p', { style:{ fontSize:11, color:mutedColor, marginTop:S.sm, fontStyle:'italic' } }, 'Product codes will be auto-generated.'),
                                                // S478 (Sean): copy the parent's description / images onto every new variation
                                                h('div', { style:{ display:'flex', flexWrap:'wrap', gap:S.lg, marginTop:S.md, padding:S.md+'px '+S.lg+'px', border:'1px solid '+borderColor, borderRadius:8, background:dark?'rgba(255,255,255,.02)':'#FAFAFA' } },
                                                    h('span', { style:{ fontSize:12, fontWeight:600, flexBasis:'100%' } }, 'Copy from this product to every new variation'),
                                                    h('label', { style:{ display:'flex', alignItems:'center', gap:6, fontSize:12, cursor:'pointer' } },
                                                        h('input', { type:'checkbox', checked:pgCopyDesc, onChange:function(e){ setPgCopyDesc(e.target.checked); }, style:{ accentColor:TC.accent } }),
                                                        'Description & short description'),
                                                    h('label', { style:{ display:'flex', alignItems:'center', gap:6, fontSize:12, cursor:'pointer' } },
                                                        h('input', { type:'checkbox', checked:pgCopyImages, onChange:function(e){ setPgCopyImages(e.target.checked); }, style:{ accentColor:TC.accent } }),
                                                        'Images (main + gallery)'),
                                                    h('span', { style:{ fontSize:11, color:mutedColor, flexBasis:'100%' } }, 'Untick either if the variations will get their own copy or photos. Attributes, price and stock are set above; everything else can be edited per variation afterwards.')
                                                )
                                            )
                                        );
                                    })(),

                                    // ── EXISTING TAB ──
                                    pgModalTab === 'existing' && h('div', null,
                                        h('div', { style:{ marginBottom:S.md } },
                                            h('input', { type:'text', value:pgSearch, onChange:function(e){ pgSearchProducts(e.target.value); },
                                                placeholder:'Search products by name or SKU\u2026', style:Object.assign({}, fs, { fontSize:13 }) })
                                        ),
                                        pgError && h('div', { style:{ fontSize:12, color:TC.error, marginBottom:S.md } }, pgError),
                                        !pgFeatLoad && pgFeatures.length === 0 && h('div', { style:{ fontSize:11, color:TC.amber, marginBottom:S.md, lineHeight:1.5 } },
                                            'Set this product\u2019s variation attribute (e.g. Color) on the Attributes tab first, so each product you add can be given a swatch value. Without it, added products won\u2019t show as selectable options on the storefront.'),
                                        !pgFeatLoad && pgFeatures.length > 0 && h('div', { style:{ fontSize:11, color:mutedColor, marginBottom:S.md } },
                                            'Products are added with their own saved variation value (e.g. Color). You\u2019ll only be asked to pick one if a product has none set.'),
                                        pgSearching && h('div', { style:{ color:mutedColor, fontSize:12, padding:S.lg } }, 'Searching\u2026'),
                                        !pgSearching && pgSearchRes.length === 0 && pgSearch && h('div', { style:{ color:mutedColor, fontSize:12, textAlign:'center', padding:S.lg } }, 'No products found.'),
                                        pgSearchRes.map(function(p) {
                                            var pid = Number(p.id || p.product_id);
                                            var alreadyIn = pgMembers.some(function(m){ return Number(m.id||m.product_id) === pid; }) || pid === numId;
                                            var assigning   = pgAddPid === pid;
                                            var needFeatures = pgFeatures.filter(function(f){ return pgAddNeed.indexOf(Number(f.id||f.feature_id)) >= 0; });
                                            var allChosen   = needFeatures.length > 0 && needFeatures.every(function(f){ return pgAddCombo[Number(f.id||f.feature_id)]; });
                                            return h('div', { key:pid, style:{ padding:S.md+'px 0', borderBottom:'1px solid '+(dark?'rgba(255,255,255,.04)':borderColor) } },
                                                h('div', { style:{ display:'flex', alignItems:'center', justifyContent:'space-between' } },
                                                    h('div', null,
                                                        h('span', { style:{ fontSize:13, fontWeight:500 } }, p.name || '\u2014'),
                                                        h('span', { style:{ fontSize:11, color:mutedColor, marginLeft:S.sm } }, p.sku || '')
                                                    ),
                                                    alreadyIn
                                                        ? h('span', { style:{ fontSize:11, color:mutedColor } }, 'Already in group')
                                                        : ( assigning
                                                            ? h('button', { onClick:function(){ setPgAddPid(null); setPgAddCombo({}); setPgAddNeed([]); },
                                                                style:{ padding:'4px 12px', borderRadius:6, border:'1px solid '+borderColor, background:'transparent', color:mutedColor, fontSize:11, fontWeight:500, cursor:'pointer' } }, 'Cancel')
                                                            : h('button', { onClick:function(){ pgAddExisting(pid); }, disabled:pgAdding,
                                                                style:{ padding:'4px 12px', borderRadius:6, border:'none', background:TC.accent, color:'#fff', fontSize:11, fontWeight:500, cursor:pgAdding?'default':'pointer', opacity:pgAdding?0.6:1 } }, '+ Add') )
                                                ),
                                                // Prompt only for the axis/axes this product has no saved value for.
                                                assigning && needFeatures.length > 0 && h('div', { style:{ marginTop:S.md, padding:S.md, borderRadius:8, background:(dark?'rgba(255,255,255,.03)':'#F9FAFB') } },
                                                    h('div', { style:{ fontSize:11, color:mutedColor, marginBottom:S.sm } }, 'This product has no saved value for the option(s) below \u2014 pick one to add it.'),
                                                    h('div', { style:{ display:'flex', flexWrap:'wrap', gap:S.md, alignItems:'flex-end' } },
                                                        needFeatures.map(function(f) {
                                                            var fid = Number(f.id || f.feature_id);
                                                            var choices = f._choices || [];
                                                            return h('div', { key:fid, style:{ display:'flex', flexDirection:'column', gap:4 } },
                                                                h('label', { style:{ fontSize:11, color:mutedColor, fontWeight:500 } }, f.name || ('Attribute #'+fid)),
                                                                h('select', {
                                                                    value: pgAddCombo[fid] || '',
                                                                    onChange:function(e){
                                                                        var vid = Number(e.target.value);
                                                                        setPgAddCombo(function(prev){ var n = Object.assign({}, prev); if (vid) n[fid] = vid; else delete n[fid]; return n; });
                                                                    },
                                                                    style:Object.assign({}, fs, { fontSize:13, minWidth:140, padding:'6px 8px' })
                                                                },
                                                                    h('option', { value:'' }, 'Select\u2026'),
                                                                    choices.map(function(c){ return h('option', { key:c.id, value:c.id }, c.name || ('V'+c.id)); })
                                                                )
                                                            );
                                                        }),
                                                        h('button', {
                                                            onClick:function(){ if (allChosen && !pgAdding) pgAddExisting(pid, pgAddCombo); },
                                                            disabled: !allChosen || pgAdding,
                                                            style:{ padding:'6px 14px', borderRadius:6, border:'none', background:(allChosen && !pgAdding)?TC.success:'#9CA3AF', color:'#fff', fontSize:12, fontWeight:600, cursor:(allChosen && !pgAdding)?'pointer':'not-allowed' } },
                                                            pgAdding ? 'Adding\u2026' : 'Confirm')
                                                    )
                                                )
                                            );
                                        })
                                    )
                                ),

                                // Footer
                                (function() {
                                    var comboCount = pgModalTab === 'create' ? pgGetCombinations().length : 0;
                                    return h('div', { style:{ padding:S.lg+'px '+S.xxl+'px', borderTop:'1px solid '+borderColor, display:'flex', gap:S.sm, justifyContent:'flex-end', flexShrink:0 } },
                                        h(Btn, { onClick:function(){ setPgShowModal(false); }, mutedColor, borderColor }, 'Cancel'),
                                        pgModalTab === 'create' && comboCount > 0 && h(Btn, {
                                            variant:'primary', onClick:pgCreateVariations, disabled:pgCreating, mutedColor, borderColor },
                                            pgCreating ? 'Creating\u2026' : 'Create ' + comboCount + ' variation' + (comboCount===1?'':'s'))
                                    );
                                })()
                            )
                        )
                    );
                })(),

                // ════════════ WAITLIST TAB (post-save only) ════════════
                tab==='waitlist' && !isNew && (function() {
                    var filtered = wlFiltered();
                    var allSelected = filtered.length > 0 && filtered.every(function(s){ return wlSelected.includes(Number(s.id)); });

                    var statusColors = {
                        waiting:   { bg: dark?'rgba(37,99,235,.08)':'#EFF6FF', color: TC.accent },
                        notified:  { bg: dark?'rgba(22,163,74,.08)':'#F0FDF4', color: TC.success },
                        cancelled: { bg: dark?'rgba(115,115,115,.08)':'#F5F5F5', color: TC.muted }
                    };

                    return h('div', null,
                        // ── Stat cards row ──
                        h('div', { style:{ display:'grid', gridTemplateColumns:'repeat(4,1fr)', gap:S.lg, marginBottom:S.xl } },
                            [
                                { label:'Total Subscribers', value:wlStats.total, icon:'\uD83D\uDCE7', color:TC.accent },
                                { label:'Waiting', value:wlStats.waiting, icon:'\u23F3', color:TC.amber },
                                { label:'Notified', value:wlStats.notified, icon:'\u2705', color:TC.success },
                                { label:'Cancelled', value:wlStats.cancelled, icon:'\u274C', color:TC.error },
                            ].map(function(stat) {
                                return h('div', { key:stat.label, style:{
                                    background:cardBg, borderRadius:12, padding:S.xl, border:'1px solid '+borderColor,
                                    boxShadow:dark?'0 1px 3px rgba(0,0,0,.24)':'0 1px 3px rgba(0,0,0,.04)'
                                } },
                                    h('div', { style:{ display:'flex', justifyContent:'space-between', alignItems:'center', marginBottom:S.sm } },
                                        h('span', { style:{ fontSize:11, color:mutedColor, fontWeight:500 } }, stat.label),
                                        h('span', { style:{ fontSize:18 } }, stat.icon)
                                    ),
                                    h('div', { style:{ fontSize:24, fontWeight:700, letterSpacing:'-.5px', color:stat.color } }, stat.value)
                                );
                            })
                        ),

                        // ── Main table card ──
                        h(Card, { cardBg, borderColor, dark },
                            // Toolbar
                            h('div', { style:{ display:'flex', justifyContent:'space-between', alignItems:'center', marginBottom:S.lg, flexWrap:'wrap', gap:S.md } },
                                h('div', { style:{ display:'flex', alignItems:'center', gap:S.md } },
                                    // Search
                                    h('div', { style:{ position:'relative' } },
                                        h('svg', { width:14, height:14, viewBox:'0 0 14 14', fill:'none', stroke:mutedColor, strokeWidth:'1.5',
                                            style:{ position:'absolute', left:10, top:'50%', transform:'translateY(-50%)' } },
                                            h('circle', { cx:6, cy:6, r:4 }),
                                            h('path', { d:'M9 9l3 3' })
                                        ),
                                        h('input', { type:'text', value:wlSearch, placeholder:'Search by name or email\u2026',
                                            onChange:function(e){ setWlSearch(e.target.value); },
                                            style:Object.assign({}, fs, { paddingLeft:32, width:240, fontSize:12 }) })
                                    ),
                                    // Status filter chips
                                    ['all','waiting','notified','cancelled'].map(function(f) {
                                        var isActive = wlFilter === f;
                                        var label = f === 'all' ? 'All' : f.charAt(0).toUpperCase() + f.slice(1);
                                        return h('button', { key:f, onClick:function(){ setWlFilter(f); },
                                            style:{ padding:'5px 12px', borderRadius:16, fontSize:11, fontWeight:isActive?600:400, cursor:'pointer',
                                                    border: isActive ? '2px solid '+TC.accent : '1px solid '+borderColor,
                                                    background: isActive ? (dark?'rgba(37,99,235,.08)':'rgba(37,99,235,.04)') : 'transparent',
                                                    color: isActive ? TC.accent : mutedColor } }, label);
                                    })
                                ),
                                h('div', { style:{ display:'flex', gap:S.sm } },
                                    wlSelected.length > 0 && h(Btn, { onClick:wlBulkDelete, mutedColor, borderColor },
                                        'Delete Selected (' + wlSelected.length + ')'),
                                    wlStats.waiting > 0 && h(Btn, { variant:'primary', onClick:wlNotifyAll, disabled:wlNotifying, mutedColor, borderColor },
                                        wlNotifying ? 'Sending\u2026' : 'Notify All (' + wlStats.waiting + ')'),
                                    h(Btn, { onClick:function(){ setWlShowAdd(!wlShowAdd); }, mutedColor, borderColor },
                                        wlShowAdd ? 'Cancel' : '+ Add Subscriber')
                                )
                            ),

                            // Add subscriber form
                            wlShowAdd && h('div', { style:{ display:'flex', gap:S.md, marginBottom:S.lg, padding:S.lg, border:'2px solid '+TC.accent,
                                borderRadius:10, background:dark?'rgba(37,99,235,.03)':'rgba(37,99,235,.02)', alignItems:'flex-end' } },
                                h('div', { style:{ flex:1 } },
                                    h('label', { style:{ fontSize:11, fontWeight:500, color:mutedColor, display:'block', marginBottom:3 } }, 'Name'),
                                    h('input', { type:'text', value:wlAddForm.name, placeholder:'Customer name',
                                        onChange:function(e){ setWlAddForm(function(p){ return Object.assign({},p,{name:e.target.value}); }); },
                                        style:Object.assign({}, fs, { fontSize:12 }) })
                                ),
                                h('div', { style:{ flex:1 } },
                                    h('label', { style:{ fontSize:11, fontWeight:500, color:mutedColor, display:'block', marginBottom:3 } }, 'Email *'),
                                    h('input', { type:'email', value:wlAddForm.email, placeholder:'email@example.com',
                                        onChange:function(e){ setWlAddForm(function(p){ return Object.assign({},p,{email:e.target.value}); }); },
                                        style:Object.assign({}, fs, { fontSize:12 }) })
                                ),
                                h(Btn, { variant:'primary', onClick:wlAddSubscriber, disabled:wlAddSaving||!wlAddForm.email.trim(), mutedColor, borderColor },
                                    wlAddSaving ? 'Adding\u2026' : 'Add')
                            ),

                            // Loading
                            wlLoading && h('div', { style:{ textAlign:'center', padding:S.xxl+'px 0', color:mutedColor, fontSize:13 } }, 'Loading waitlist\u2026'),

                            // Empty
                            !wlLoading && filtered.length === 0 && h('div', { style:{ textAlign:'center', padding:(S.xxl*2)+'px '+S.xxl+'px', color:mutedColor,
                                border:'1px dashed '+borderColor, borderRadius:10 } },
                                h('div', { style:{ fontSize:28, marginBottom:S.md, opacity:.4 } }, '\uD83D\uDD14'),
                                h('div', { style:{ fontSize:14, fontWeight:500, marginBottom:S.xs } }, 'No subscribers yet'),
                                h('div', { style:{ fontSize:12, maxWidth:380, margin:'0 auto', lineHeight:1.6 } },
                                    'When this product is out of stock, customers can sign up to be notified when it\u2019s back. Their requests will appear here.')
                            ),

                            // Table
                            !wlLoading && filtered.length > 0 && h('div', { style:{ border:'1px solid '+borderColor, borderRadius:10, overflow:'hidden' } },
                                // Header
                                h('div', { style:{ display:'grid', gridTemplateColumns:'40px 1.2fr 1.5fr 1fr 100px 100px 44px', gap:0,
                                    padding:'10px '+S.lg+'px', background:dark?'rgba(255,255,255,.02)':'#FAFBFD', borderBottom:'1px solid '+borderColor } },
                                    h('div', { style:{ display:'flex', alignItems:'center' } },
                                        h('input', { type:'checkbox', checked:allSelected, onChange:function(e){ wlSelectAll(e.target.checked); },
                                            style:{ accentColor:TC.accent, width:15, height:15 } })
                                    ),
                                    ['Name','Email','Attributes','Status','Date',''].map(function(col) {
                                        return h('span', { key:col, style:{ fontSize:10, fontWeight:600, color:mutedColor, textTransform:'uppercase', letterSpacing:'.6px' } }, col);
                                    })
                                ),
                                // Rows
                                filtered.map(function(sub, i) {
                                    var sid = Number(sub.id);
                                    var isChecked = wlSelected.includes(sid);
                                    var sc = statusColors[sub.status] || statusColors.waiting;
                                    var attrs = sub.attributes || {};
                                    var attrParts = Object.keys(attrs).map(function(k){ return attrs[k]; });
                                    var isLast = i === filtered.length - 1;
                                    var dateStr = tcFmtDate(sub.created_at);

                                    return h('div', { key:sid, style:{ display:'grid', gridTemplateColumns:'40px 1.2fr 1.5fr 1fr 100px 100px 44px', gap:0,
                                        padding:'12px '+S.lg+'px', borderBottom:isLast?'none':'1px solid '+(dark?'rgba(255,255,255,.04)':borderColor),
                                        alignItems:'center', transition:'background .12s' },
                                        onMouseEnter:function(e){ e.currentTarget.style.background=dark?'rgba(255,255,255,.02)':'#F9FAFB'; },
                                        onMouseLeave:function(e){ e.currentTarget.style.background='transparent'; } },

                                        // Checkbox
                                        h('div', { style:{ display:'flex', alignItems:'center' } },
                                            h('input', { type:'checkbox', checked:isChecked, onChange:function(){ wlToggleSelect(sid); },
                                                style:{ accentColor:TC.accent, width:15, height:15 } })
                                        ),

                                        // Name + initials avatar
                                        h('div', { style:{ display:'flex', alignItems:'center', gap:S.sm } },
                                            h('div', { style:{ width:32, height:32, borderRadius:8,
                                                background: dark ? 'rgba(37,99,235,.1)' : 'rgba(37,99,235,.06)',
                                                display:'flex', alignItems:'center', justifyContent:'center',
                                                fontSize:11, fontWeight:600, color:TC.accent, flexShrink:0 } },
                                                (sub.name||sub.email||'?').charAt(0).toUpperCase()
                                            ),
                                            h('div', { style:{ minWidth:0 } },
                                                h('div', { style:{ fontSize:13, fontWeight:500, whiteSpace:'nowrap', overflow:'hidden', textOverflow:'ellipsis' } }, sub.name || '\u2014'),
                                                sub.user_id && h('span', { style:{ fontSize:10, color:mutedColor } }, 'Registered user')
                                            )
                                        ),

                                        // Email
                                        h('span', { style:{ fontSize:12, color:mutedColor, whiteSpace:'nowrap', overflow:'hidden', textOverflow:'ellipsis' } }, sub.email || '\u2014'),

                                        // Attributes chips
                                        h('div', { style:{ display:'flex', flexWrap:'wrap', gap:3 } },
                                            attrParts.length > 0
                                                ? attrParts.map(function(v, vi) {
                                                    return h('span', { key:vi, style:{ fontSize:10, fontWeight:500, padding:'2px 7px', borderRadius:4,
                                                        background:dark?'rgba(255,255,255,.06)':'#F0F2F5', color:dark?'#B0B8C4':TC.muted, whiteSpace:'nowrap' } }, v);
                                                })
                                                : h('span', { style:{ fontSize:11, color:mutedColor } }, '\u2014')
                                        ),

                                        // Status badge
                                        h('span', { style:{ fontSize:10, fontWeight:500, padding:'2px 8px', borderRadius:5, display:'inline-block',
                                            background:sc.bg, color:sc.color, width:'fit-content' } },
                                            sub.status === 'waiting' ? 'Waiting' : sub.status === 'notified' ? 'Notified' : 'Cancelled'
                                        ),

                                        // Date
                                        h('span', { style:{ fontSize:11, color:mutedColor } }, dateStr),

                                        // Delete
                                        h('button', { onClick:function(){ wlDelete(sid); }, title:'Remove subscriber',
                                            style:{ width:28, height:28, borderRadius:6, border:'1px solid '+(dark?'rgba(255,255,255,.08)':borderColor),
                                                background:'transparent', cursor:'pointer', display:'flex', alignItems:'center', justifyContent:'center',
                                                color:mutedColor, transition:'all .15s' },
                                            onMouseEnter:function(e){ e.currentTarget.style.borderColor=TC.error; e.currentTarget.style.color=TC.error; },
                                            onMouseLeave:function(e){ e.currentTarget.style.borderColor=dark?'rgba(255,255,255,.08)':borderColor; e.currentTarget.style.color=mutedColor; } },
                                            h('svg', { width:14, height:14, viewBox:'0 0 14 14', fill:'none', stroke:'currentColor', strokeWidth:'1.5', strokeLinecap:'round' },
                                                h('path', { d:'M3.5 3.5l7 7M10.5 3.5l-7 7' })
                                            )
                                        )
                                    );
                                })
                            ),

                            // Footer count
                            !wlLoading && filtered.length > 0 && h('div', { style:{ marginTop:S.md, fontSize:11, color:mutedColor } },
                                'Showing ' + filtered.length + ' of ' + wlStats.total + ' subscriber' + (wlStats.total===1?'':'s')
                            )
                        )
                    );
                })(),

                // ════════════ TABS TAB (post-save only) ════════════
                tab==='tabs' && !isNew && (function() {
                    var tfs = fieldSty(dark, borderColor);
                    var tInputSm = Object.assign({}, tfs, { width:'auto', minWidth:100, fontSize:12, padding:'6px 10px' });
                    var globalTabs = tbTabs.filter(function(t){ return t.scope === 'global'; });
                    var customTabs = tbTabs.filter(function(t){ return t.scope === 'product'; });
                    var sizingTab = globalTabs.find(function(t){ return t.tab_key === 'sizing_chart'; });
                    var sizingEnabled = sizingTab && sizingTab.product_status === 'active';
                    var szData = tbSizing && tbSizing.chart_data ? tbSizing.chart_data : null;

                    // Type badge
                    function typeBadge(type) {
                        var map = {
                            builtin:      { label:'Built-in',      bg:dark?'rgba(31,106,225,.1)':'#EEF4FE',  color:TC.accent },
                            richtext:     { label:'Rich Text',     bg:dark?'rgba(31,138,112,.1)':'#EBF5F1',  color:TC.success },
                            sizing_chart: { label:'Sizing Chart',  bg:dark?'rgba(217,164,65,.1)':'#FEF6EC',  color:TC.amber },
                            template:     { label:'Template',      bg:dark?'rgba(113,121,126,.1)':'#F0F2F5',  color:TC.gray },
                        };
                        var s = map[type] || map.richtext;
                        return h('span', { style:{ fontSize:10, fontWeight:500, padding:'2px 8px', borderRadius:4, background:s.bg, color:s.color, whiteSpace:'nowrap' } }, s.label);
                    }

                    // Toggle switch
                    function toggle(isOn, onToggle) {
                        return h('div', {
                            onClick:onToggle,
                            style:{
                                width:36, height:20, borderRadius:10, cursor:'pointer', position:'relative',
                                background:isOn ? TC.accent : (dark?'#333':'#D1D5DB'),
                                transition:'background .2s ease', flexShrink:0
                            }
                        },
                            h('div', { style:{
                                width:16, height:16, borderRadius:8, background:'#fff',
                                position:'absolute', top:2, left:isOn?18:2,
                                transition:'left .2s ease',
                                boxShadow:'0 1px 3px rgba(0,0,0,.2)'
                            } })
                        );
                    }

                    if (tbLoading) return h('div', null, h(Spinner, { mutedColor:mutedColor }));

                    return h('div', { style:{ display:'flex', flexDirection:'column', gap:S.lg } },

                        // ── Section 1: Storefront Tabs ──
                        h(Card, { cardBg:cardBg, borderColor:borderColor, dark:dark },
                            h('div', { style:{ marginBottom:S.lg } },
                                h('h3', { style:{ fontSize:14, fontWeight:600, marginBottom:4 } }, 'Storefront Tabs'),
                                h('p', { style:{ fontSize:12, color:mutedColor, margin:0 } }, 'Toggle which tabs appear on this product\u2019s page. Changes save automatically.')
                            ),

                            // Header row
                            h('div', { style:{
                                display:'grid', gridTemplateColumns:'1fr 100px 80px',
                                padding:'8px 12px', borderRadius:8,
                                background:dark?'rgba(255,255,255,.02)':'#FAFBFD',
                                marginBottom:S.sm, gap:S.md
                            } },
                                h('span', { style:{ fontSize:10, fontWeight:500, color:mutedColor, textTransform:'uppercase', letterSpacing:'.5px' } }, 'Tab'),
                                h('span', { style:{ fontSize:10, fontWeight:500, color:mutedColor, textTransform:'uppercase', letterSpacing:'.5px' } }, 'Type'),
                                h('span', { style:{ fontSize:10, fontWeight:500, color:mutedColor, textTransform:'uppercase', letterSpacing:'.5px', textAlign:'right' } }, 'Visible')
                            ),

                            // Tab rows
                            globalTabs.map(function(t) {
                                var isOn = t.product_status === 'active';
                                return h('div', { key:t.id, style:{
                                    display:'grid', gridTemplateColumns:'1fr 100px 80px', alignItems:'center',
                                    padding:'10px 12px', borderRadius:8, gap:S.md,
                                    borderBottom:'1px solid '+borderColor
                                } },
                                    h('div', { style:{ display:'flex', alignItems:'center', gap:S.sm } },
                                        h('span', { style:{ fontSize:13, fontWeight:500 } }, t.title || t.tab_key),
                                        t.is_primary == 1 && h('span', { style:{ fontSize:9, color:mutedColor, fontStyle:'italic' } }, '(primary)')
                                    ),
                                    typeBadge(t.tab_type),
                                    h('div', { style:{ display:'flex', justifyContent:'flex-end' } },
                                        toggle(isOn, function(){ tbToggle(t.id); })
                                    )
                                );
                            })
                        ),

                        // ── Section 2: Sizing Chart Editor ──
                        sizingEnabled && h(Card, { cardBg:cardBg, borderColor:borderColor, dark:dark },
                            h('div', { style:{ display:'flex', justifyContent:'space-between', alignItems:'center', marginBottom:S.lg } },
                                h('div', null,
                                    h('h3', { style:{ fontSize:14, fontWeight:600, marginBottom:4 } }, 'Sizing Chart'),
                                    h('p', { style:{ fontSize:12, color:mutedColor, margin:0 } }, 'Define the sizing table for this product. Customers see this in the Sizing Chart tab.')
                                ),
                                h('div', { style:{ display:'flex', gap:S.sm } },
                                    // Unit selector
                                    h('select', {
                                        value: szData ? szData.unit : 'in',
                                        onChange: function(e){ tbSzSet('unit', e.target.value); },
                                        style: Object.assign({}, tInputSm, { minWidth:80 })
                                    },
                                        h('option', { value:'in' }, 'Inches'),
                                        h('option', { value:'cm' }, 'Centimeters')
                                    ),
                                    h(Btn, { variant:'primary', onClick:tbSaveSizing, disabled:tbSaving, mutedColor:mutedColor, borderColor:borderColor },
                                        tbSaving ? 'Saving\u2026' : 'Save Chart')
                                )
                            ),

                            szData ? h('div', null,
                                // Toolbar
                                h('div', { style:{ display:'flex', gap:S.sm, marginBottom:S.md } },
                                    h(Btn, { onClick:tbSzAddCol, mutedColor:mutedColor, borderColor:borderColor },
                                        h('span', { style:{ marginRight:4 } }, '+'), 'Column'),
                                    h(Btn, { onClick:tbSzAddRow, mutedColor:mutedColor, borderColor:borderColor },
                                        h('span', { style:{ marginRight:4 } }, '+'), 'Row')
                                ),

                                // Table
                                h('div', { style:{ overflowX:'auto', borderRadius:8, border:'1px solid '+borderColor } },
                                    h('table', { style:{ width:'100%', borderCollapse:'collapse', fontSize:12 } },
                                        // Header
                                        h('thead', null,
                                            h('tr', { style:{ background:dark?'rgba(255,255,255,.02)':'#FAFBFD' } },
                                                (szData.headers || []).map(function(hdr, ci) {
                                                    return h('th', { key:ci, style:{ padding:'8px 6px', borderBottom:'1px solid '+borderColor, fontWeight:500 } },
                                                        h('div', { style:{ display:'flex', alignItems:'center', gap:4 } },
                                                            h('input', { type:'text', value:hdr,
                                                                onChange:function(e){ tbSzHeaderSet(ci, e.target.value); },
                                                                style:Object.assign({}, tInputSm, { minWidth:60, width:'100%', fontWeight:500 })
                                                            }),
                                                            (szData.headers || []).length > 2 && h('button', {
                                                                onClick:function(){ tbSzRemoveCol(ci); },
                                                                title:'Remove column',
                                                                style:{ background:'transparent', border:'none', color:TC.error, cursor:'pointer', fontSize:14, padding:0, lineHeight:1 }
                                                            }, '\u00D7')
                                                        )
                                                    );
                                                }),
                                                h('th', { style:{ width:36, padding:'8px 4px', borderBottom:'1px solid '+borderColor } })
                                            )
                                        ),
                                        // Body
                                        h('tbody', null,
                                            (szData.rows || []).map(function(row, ri) {
                                                return h('tr', { key:ri, style:{ borderBottom: ri < (szData.rows||[]).length-1 ? '1px solid '+borderColor : 'none' } },
                                                    (row || []).map(function(cell, ci) {
                                                        return h('td', { key:ci, style:{ padding:'4px 6px' } },
                                                            h('input', { type:'text', value:cell,
                                                                onChange:function(e){ tbSzCellSet(ri, ci, e.target.value); },
                                                                style:Object.assign({}, tInputSm, { minWidth:50, width:'100%' })
                                                            })
                                                        );
                                                    }),
                                                    h('td', { style:{ padding:'4px', textAlign:'center' } },
                                                        (szData.rows || []).length > 1 && h('button', {
                                                            onClick:function(){ tbSzRemoveRow(ri); },
                                                            title:'Remove row',
                                                            style:{ background:'transparent', border:'none', color:TC.error, cursor:'pointer', fontSize:14, padding:'2px 6px', lineHeight:1 }
                                                        }, '\u00D7')
                                                    )
                                                );
                                            })
                                        )
                                    )
                                ),

                                // Notes
                                h('div', { style:{ marginTop:S.lg } },
                                    h('label', { style:{ fontSize:12, fontWeight:500, display:'block', marginBottom:4, color:mutedColor } }, 'How to Measure (optional)'),
                                    h('textarea', {
                                        value: tbSizing ? (tbSizing.notes||'') : '',
                                        onChange:function(e){ setTbSizing(function(prev){ return Object.assign({}, prev, { notes:e.target.value }); }); },
                                        rows:3, placeholder:'E.g. \u201CMeasure across the chest under the arms\u2026\u201D',
                                        style:Object.assign({}, tfs, { width:'100%', resize:'vertical', fontSize:12 })
                                    })
                                )
                            ) : h('p', { style:{ fontSize:12, color:mutedColor, fontStyle:'italic' } }, 'Loading sizing chart data\u2026')
                        ),

                        // ── Section 3: Product-Specific Custom Tabs ──
                        h(Card, { cardBg:cardBg, borderColor:borderColor, dark:dark },
                            h('h3', { style:{ fontSize:14, fontWeight:600, marginBottom:4 } }, 'Custom Tabs'),
                            h('p', { style:{ fontSize:12, color:mutedColor, marginBottom:S.lg } }, 'Add rich-text tabs that only appear on this product (e.g. Care Instructions, Warranty Info).'),

                            // Existing custom tabs
                            customTabs.length > 0 && h('div', { style:{ display:'flex', flexDirection:'column', gap:S.md, marginBottom:S.lg } },
                                customTabs.map(function(t) {
                                    var isEditing = tbEditId === Number(t.id);
                                    return h('div', { key:t.id, style:{
                                        border:'1px solid '+borderColor, borderRadius:8, padding:S.base,
                                        background:dark?'rgba(255,255,255,.01)':'#FAFBFD'
                                    } },
                                        isEditing
                                            ? h('div', { style:{ display:'flex', flexDirection:'column', gap:S.md } },
                                                h('input', { type:'text', value:tbEditTitle,
                                                    onChange:function(e){ setTbEditTitle(e.target.value); },
                                                    placeholder:'Tab title',
                                                    style:Object.assign({}, tfs, { width:'100%', fontSize:13, fontWeight:500 })
                                                }),
                                                h('textarea', { value:tbEditContent,
                                                    onChange:function(e){ setTbEditContent(e.target.value); },
                                                    placeholder:'Tab content (HTML allowed)',
                                                    rows:5,
                                                    style:Object.assign({}, tfs, { width:'100%', resize:'vertical', fontSize:12 })
                                                }),
                                                h('div', { style:{ display:'flex', gap:S.sm } },
                                                    h(Btn, { variant:'primary', onClick:function(){ tbUpdateCustom(t.id); }, disabled:tbSaving, mutedColor:mutedColor, borderColor:borderColor },
                                                        tbSaving ? 'Saving\u2026' : 'Update'),
                                                    h(Btn, { onClick:function(){ setTbEditId(null); }, mutedColor:mutedColor, borderColor:borderColor }, 'Cancel')
                                                )
                                            )
                                            : h('div', { style:{ display:'flex', justifyContent:'space-between', alignItems:'center' } },
                                                h('div', null,
                                                    h('span', { style:{ fontSize:13, fontWeight:500 } }, t.title || 'Untitled'),
                                                    t.content && h('p', { style:{ fontSize:11, color:mutedColor, marginTop:2, maxWidth:500, overflow:'hidden', textOverflow:'ellipsis', whiteSpace:'nowrap' } },
                                                        t.content.replace(/<[^>]+>/g, '').substring(0, 80) + (t.content.length > 80 ? '\u2026' : ''))
                                                ),
                                                h('div', { style:{ display:'flex', gap:S.sm } },
                                                    h(Btn, { onClick:function(){ tbStartEdit(t); }, mutedColor:mutedColor, borderColor:borderColor }, 'Edit'),
                                                    h(Btn, { onClick:function(){ tbDeleteCustom(t.id); }, mutedColor:mutedColor, borderColor:borderColor },
                                                        h('span', { style:{ color:TC.error } }, 'Delete'))
                                                )
                                            )
                                    );
                                })
                            ),

                            // Add new custom tab
                            h('div', { style:{ border:'1px dashed '+borderColor, borderRadius:8, padding:S.base } },
                                h('div', { style:{ fontSize:12, fontWeight:500, marginBottom:S.sm, color:mutedColor } }, 'Add New Tab'),
                                h('div', { style:{ display:'flex', flexDirection:'column', gap:S.md } },
                                    h('input', { type:'text', value:tbNewTitle,
                                        onChange:function(e){ setTbNewTitle(e.target.value); },
                                        placeholder:'Tab title (e.g. Care Instructions)',
                                        style:Object.assign({}, tfs, { width:'100%', fontSize:13 })
                                    }),
                                    h('textarea', { value:tbNewContent,
                                        onChange:function(e){ setTbNewContent(e.target.value); },
                                        placeholder:'Tab content (HTML allowed)',
                                        rows:4,
                                        style:Object.assign({}, tfs, { width:'100%', resize:'vertical', fontSize:12 })
                                    }),
                                    h('div', null,
                                        h(Btn, { variant:'primary', onClick:tbAddCustom, disabled:tbSaving || !tbNewTitle.trim(), mutedColor:mutedColor, borderColor:borderColor },
                                            tbSaving ? 'Adding\u2026' : 'Add Tab')
                                    )
                                )
                            )
                        )
                    );
                })(),


                // ════════════ DOWNLOADS TAB (post-save only, only for digital products) ════════════
                tab==='downloads' && !isNew && form.is_downloadable && h(DownloadsTabContent, {
                    productId: numId, form: form, sf: sf,
                    dark: dark, cardBg: cardBg, mutedColor: mutedColor,
                    borderColor: borderColor, textColor: textColor, tableHeaderBg: tableHeaderBg
                }),

                // ════════════ SUBSCRIPTION TAB (post-save only, requires plugin) ════════════
                tab==='subscription' && !isNew && Config.subscriptionsActive && (function() {
                    var sfs = fieldSty(dark, borderColor);
                    var sSelS = Object.assign({}, sfs, { cursor:'pointer', appearance:'auto' });

                    function billingDesc(price, period, interval) {
                        var amt = tcFmtMoney(price);
                        var iv = parseInt(interval)||1;
                        if (iv === 1) return amt + ' / ' + (period||'month');
                        return amt + ' every ' + iv + ' ' + (period||'month') + 's';
                    }

                    if (spLoading) return h('div', null, h(Spinner, { mutedColor:mutedColor }));

                    // ── Create plan form ──
                    if (spShowCreate) {
                        return h('div', { style:{ display:'flex', flexDirection:'column', gap:S.lg } },
                            h(Card, { cardBg:cardBg, borderColor:borderColor, dark:dark, style:{ padding:S.xl } },
                                h('div', { style:{ display:'flex', alignItems:'center', gap:S.md, marginBottom:S.lg } },
                                    h('button', { onClick:function(){ setSpShowCreate(false); }, style:{ background:'none', border:'none', cursor:'pointer', color:mutedColor, fontSize:14 } }, '\u2190 Back'),
                                    h('h3', { style:{ fontSize:15, fontWeight:600, margin:0 } }, 'Create Subscription Plan')
                                ),
                                spMsg && h('div', { style:{ padding:'10px 16px', borderRadius:8, marginBottom:S.md, fontSize:13, fontWeight:500,
                                    background:spMsg.type==='success'?(dark?'rgba(22,163,74,.12)':'#ECFDF5'):(dark?'rgba(220,38,38,.12)':'#FEF2F2'),
                                    color:spMsg.type==='success'?'#16A34A':'#DC2626' } }, spMsg.text),
                                h('div', { style:{ display:'grid', gridTemplateColumns:'1fr 1fr', gap:S.lg } },
                                    h('div', null,
                                        h('label', { style:{ fontSize:12, fontWeight:500, color:mutedColor, display:'block', marginBottom:S.xs } }, 'Plan Name *'),
                                        h('input', { style:sfs, value:spForm.name, onChange:function(e){ spSetF('name', e.target.value); }, placeholder:'e.g. Monthly, Annual' })
                                    ),
                                    h('div', null,
                                        h('label', { style:{ fontSize:12, fontWeight:500, color:mutedColor, display:'block', marginBottom:S.xs } }, 'Price *'),
                                        h('input', { style:sfs, type:'number', step:'0.01', value:spForm.price, onChange:function(e){ spSetF('price', e.target.value); } })
                                    ),
                                    h('div', null,
                                        h('label', { style:{ fontSize:12, fontWeight:500, color:mutedColor, display:'block', marginBottom:S.xs } }, 'Billing Period'),
                                        h('select', { style:sSelS, value:spForm.billing_period, onChange:function(e){ spSetF('billing_period', e.target.value); } },
                                            h('option',{value:'day'},'Day'),h('option',{value:'week'},'Week'),h('option',{value:'month'},'Month'),h('option',{value:'year'},'Year')
                                        )
                                    ),
                                    h('div', null,
                                        h('label', { style:{ fontSize:12, fontWeight:500, color:mutedColor, display:'block', marginBottom:S.xs } }, 'Billing Interval'),
                                        h('input', { style:sfs, type:'number', min:'1', value:spForm.billing_interval, onChange:function(e){ spSetF('billing_interval', e.target.value); } }),
                                        h('div', { style:{ fontSize:11, color:mutedColor, marginTop:4 } }, 'e.g. 3 + Month = every 3 months')
                                    ),
                                    h('div', null,
                                        h('label', { style:{ fontSize:12, fontWeight:500, color:mutedColor, display:'block', marginBottom:S.xs } }, 'Signup Fee'),
                                        h('input', { style:sfs, type:'number', step:'0.01', value:spForm.signup_fee, onChange:function(e){ spSetF('signup_fee', e.target.value); } })
                                    ),
                                    h('div', null,
                                        h('label', { style:{ fontSize:12, fontWeight:500, color:mutedColor, display:'block', marginBottom:S.xs } }, 'Trial Days'),
                                        h('input', { style:sfs, type:'number', min:'0', value:spForm.trial_days, onChange:function(e){ spSetF('trial_days', e.target.value); } })
                                    ),
                                    h('div', null,
                                        h('label', { style:{ fontSize:12, fontWeight:500, color:mutedColor, display:'block', marginBottom:S.xs } }, 'Max Renewals'),
                                        h('input', { style:sfs, type:'number', min:'0', value:spForm.max_renewals, onChange:function(e){ spSetF('max_renewals', e.target.value); } }),
                                        h('div', { style:{ fontSize:11, color:mutedColor, marginTop:4 } }, '0 = unlimited')
                                    ),
                                    h('div', null,
                                        h('label', { style:{ fontSize:12, fontWeight:500, color:mutedColor, display:'block', marginBottom:S.xs } }, 'Site Limit'),
                                        h('input', { style:sfs, type:'number', min:'0', value:spForm.site_limit, onChange:function(e){ spSetF('site_limit', e.target.value); } }),
                                        h('div', { style:{ fontSize:11, color:mutedColor, marginTop:4 } }, 'Licensed activations allowed. 0 = unlimited.')
                                    ),
                                    h('div', null,
                                        h('label', { style:{ fontSize:12, fontWeight:500, color:mutedColor, display:'block', marginBottom:S.xs } }, 'Status'),
                                        h('select', { style:sSelS, value:spForm.status, onChange:function(e){ spSetF('status', e.target.value); } },
                                            h('option',{value:'active'},'Active'),h('option',{value:'disabled'},'Disabled')
                                        )
                                    ),
                                    h('div', null,
                                        h('label', { style:{ fontSize:12, fontWeight:500, color:mutedColor, display:'block', marginBottom:S.xs } }, 'Customer Group'),
                                        h('select', { style:sSelS, value:spForm.usergroup_id, onChange:function(e){ spSetF('usergroup_id', e.target.value); } },
                                            h('option', { value:'' }, '\u2014 None \u2014'),
                                            spCustGroups.map(function(g) { return h('option', { key:g.id, value:String(g.id) }, g.name || g.handle || ('Group #'+g.id)); })
                                        ),
                                        h('div', { style:{ fontSize:11, color:mutedColor, marginTop:4 } }, 'Auto-assign on activation')
                                    )
                                ),
                                h('div', { style:{ marginTop:S.lg, display:'flex', gap:S.sm } },
                                    h(Btn, { variant:'primary', onClick:spCreatePlan, disabled:spSaving||!spForm.name||!spForm.price, mutedColor:mutedColor, borderColor:borderColor },
                                        spSaving ? 'Creating\u2026' : 'Create Plan'),
                                    h(Btn, { onClick:function(){ setSpShowCreate(false); }, mutedColor:mutedColor, borderColor:borderColor }, 'Cancel')
                                )
                            )
                        );
                    }

                    // ── Default: show linked plans + link existing ──
                    var unlinked = spAllPlans.filter(function(p) {
                        return (!p.product_id || p.product_id === 0) && !spPlans.some(function(lp){ return lp.id === p.id; });
                    });

                    return h('div', { style:{ display:'flex', flexDirection:'column', gap:S.lg } },
                        spMsg && h('div', { style:{ padding:'10px 16px', borderRadius:8, fontSize:13, fontWeight:500,
                            background:spMsg.type==='success'?(dark?'rgba(22,163,74,.12)':'#ECFDF5'):(dark?'rgba(220,38,38,.12)':'#FEF2F2'),
                            color:spMsg.type==='success'?'#16A34A':'#DC2626' } }, spMsg.text),

                        h(Card, { cardBg:cardBg, borderColor:borderColor, dark:dark, style:{ padding:S.xl } },
                            h('div', { style:{ display:'flex', justifyContent:'space-between', alignItems:'center', marginBottom:S.lg } },
                                h('div', null,
                                    h('h3', { style:{ fontSize:14, fontWeight:600, marginBottom:4 } }, 'Subscription Plans'),
                                    h('p', { style:{ fontSize:12, color:mutedColor, margin:0 } }, 'Plans linked to this product. Customers see a plan selector on the product page.')
                                ),
                                h('div', { style:{ display:'flex', gap:S.sm } },
                                    h(Btn, { variant:'primary', onClick:function(){ setSpShowCreate(true); }, mutedColor:mutedColor, borderColor:borderColor }, '+ Create Plan'),
                                    unlinked.length > 0 && h('select', { style:Object.assign({}, sSelS, { fontSize:12, padding:'6px 12px' }),
                                        value:'', onChange:function(e){ if (e.target.value) spLinkPlan(parseInt(e.target.value)); e.target.value=''; } },
                                        h('option', { value:'' }, 'Link existing plan\u2026'),
                                        unlinked.map(function(p) {
                                            return h('option', { key:p.id, value:p.id }, p.name + ' \u2014 ' + billingDesc(p.price, p.billing_period, p.billing_interval));
                                        })
                                    )
                                )
                            ),

                            spPlans.length === 0
                                ? h('div', { style:{ textAlign:'center', padding:S.xxl+'px '+S.xl+'px', border:'1px dashed '+borderColor, borderRadius:8 } },
                                    h('p', { style:{ fontSize:13, color:mutedColor, margin:0 } }, 'No subscription plans assigned to this product.'),
                                    h('p', { style:{ fontSize:12, color:mutedColor, marginTop:4 } }, 'Create a new plan or link an existing standalone plan.')
                                )
                                : h('div', null,
                                    h('div', { style:{ display:'grid', gridTemplateColumns:'0.5fr 2fr 1fr 0.6fr 0.6fr 0.6fr 0.6fr', padding:'10px 16px', borderBottom:'1px solid '+borderColor, background:tableHeaderBg, boxShadow:tableHeaderShadow } },
                                        ['ID','Name','Billing','Trial','Signup','Status',''].map(function(hd) {
                                            return h('span', { key:hd, style:{ fontSize:10, fontWeight:500, color:mutedColor, textTransform:'uppercase', letterSpacing:'.5px' } }, hd);
                                        })
                                    ),
                                    spPlans.map(function(p) {
                                        return h('div', { key:p.id, style:{ display:'grid', gridTemplateColumns:'0.5fr 2fr 1fr 0.6fr 0.6fr 0.6fr 0.6fr', padding:'12px 16px', borderBottom:'1px solid '+borderColor, alignItems:'center' } },
                                            h('span', { style:{ fontSize:12, fontWeight:500, color:TC.accent } }, '#' + p.id),
                                            h('span', { style:{ fontSize:13, fontWeight:500 } }, p.name || '(unnamed)'),
                                            h('span', { style:{ fontSize:12, color:mutedColor } }, billingDesc(p.price, p.billing_period, p.billing_interval)),
                                            h('span', { style:{ fontSize:12, color:mutedColor } }, p.trial_days > 0 ? p.trial_days + ' days' : '\u2014'),
                                            h('span', { style:{ fontSize:12, color:mutedColor } }, parseFloat(p.signup_fee) > 0 ? tcFmtMoney(p.signup_fee) : '\u2014'),
                                            h('span', null, h('span', { style:{ fontSize:10, fontWeight:600, padding:'2px 6px', borderRadius:4,
                                                background:p.status==='active'?'#ecfdf5':'#f5f5f5', color:p.status==='active'?'#16A34A':'#71797E' } },
                                                p.status==='active'?'Active':'Disabled')),
                                            h('div', { style:{ display:'flex', gap:S.xs } },
                                                h('button', { onClick:function(){ spUnlinkPlan(p.id); },
                                                    style:{ background:'none', border:'none', cursor:'pointer', color:mutedColor, fontSize:11, fontWeight:500 } }, 'Unlink'),
                                                h('button', { onClick:function(){ spDeletePlan(p); },
                                                    style:{ background:'none', border:'none', cursor:'pointer', color:TC.error, fontSize:11, fontWeight:500 } }, 'Delete')
                                            )
                                        );
                                    })
                                )
                        ),

                        /* Manage all plans link */
                        h('div', { style:{ textAlign:'right' } },
                            h('button', { onClick:function(){ navigate('subscriptions/plans'); },
                                style:{ background:'none', border:'none', color:TC.accent, fontSize:12, fontWeight:500, cursor:'pointer', textDecoration:'underline' } },
                                'Manage all plans in Subscriptions \u2192')
                        )
                    );
                })(),

                /* S204 — Pattern #151 cascade close-out for ProductEditorPage.
                 * Two destructive actions previously gated by native confirm() —
                 * waitlist bulk subscriber removal + product image delete —
                 * now route through ConfirmModal for consistent Pulse UI.
                 * Sibling actions on the same Waitlist tab (single subscriber
                 * remove, notify-all) remain on native confirm() pending the
                 * Pattern #151 BROAD cascade in B10 post-launch. */
                h(ConfirmModal, {
                    open: cWlBulk.open,
                    title: 'Remove ' + wlSelected.length + ' subscriber' + (wlSelected.length === 1 ? '' : 's') + '?',
                    message: 'The selected ' + (wlSelected.length === 1 ? 'subscriber' : 'subscribers') + ' will be removed from this product\u2019s waitlist. They will not be notified.',
                    confirmLabel: 'Remove ' + wlSelected.length,
                    busy: cWlBulk.busy,
                    busyLabel: 'Removing\u2026',
                    onConfirm: wlBulkDeleteConfirm,
                    onCancel: function(){ setCWlBulk({ open:false, busy:false }); },
                    dark: dark, cardBg: cardBg, borderColor: borderColor, textColor: textColor, mutedColor: mutedColor,
                }),

                h(ConfirmModal, {
                    open: cDelImage.open,
                    title: 'Remove this image?',
                    message: 'The image will be removed from this product. The file remains in your media library unless deleted separately.',
                    confirmLabel: 'Remove image',
                    busy: cDelImage.busy,
                    busyLabel: 'Removing\u2026',
                    onConfirm: deleteImageConfirm,
                    onCancel: function(){ setCDelImage({ open:false, busy:false, imgId:null }); },
                    dark: dark, cardBg: cardBg, borderColor: borderColor, textColor: textColor, mutedColor: mutedColor,
                })
            )
        );
    }



    /* Orphaned duplicate ProductEditorPage body removed — it was calling hooks at module scope,
       causing "Invalid hook call" which crashed the entire React tree and froze navigation. */

    /* ── PRODUCTS PAGE ROUTER ──────────────────────────────────────── */
    function ProductsPage({ subId, dark, cardBg, textColor, mutedColor, borderColor, tableHeaderBg }) {
        const props = { dark, cardBg, textColor, mutedColor, borderColor, tableHeaderBg };

        // Bump listKey each time we return from the editor (subId goes truthy → falsy).
        // Must be in useEffect to avoid setState-during-render React error.
        const [listKey, setListKey] = useState(1);
        const prevSubId = useRef(subId);
        useEffect(function() {
            if (!subId && prevSubId.current) {
                setListKey(function(k){ return k + 1; });
            }
            prevSubId.current = subId;
        }, [subId]);

        // S308 — "Save & Add New" reset. When the editor is already on the
        // new-product route (new → new) the hash doesn't change, so React keeps
        // the same editor instance and its state. Bumping edNonce changes the
        // editor key → a clean remount to a blank product. (existing → new resets
        // via the subId change alone, so the editor only fires this event in the
        // new → new case where subId is stable — no stale-subId flicker.)
        const [edNonce, setEdNonce] = useState(0);
        useEffect(function() {
            function onNew(){ setEdNonce(function(n){ return n + 1; }); }
            window.addEventListener('tc:product-editor-new', onNew);
            return function(){ window.removeEventListener('tc:product-editor-new', onNew); };
        }, []);

        if (subId) return h(ProductEditorPage, Object.assign({ key: 'ed-' + subId + '-' + edNonce, productId: subId }, props));
        return h(ProductListPage, Object.assign({ key: listKey }, props));
    }

    /* ── BULK ADD CATEGORIES PAGE (Pulse UI) ── */
    function CategoriesBulkAddPage({ dark, cardBg, textColor, mutedColor, borderColor }) {
        var fs = fieldSty(dark, borderColor);
        var defaultRow = function(){ return { parent_id:'0', name:'', position:'', status:'active' }; };
        var _rows = useState([defaultRow()]), rows = _rows[0], setRows = _rows[1];
        var _cats = useState([]), cats = _cats[0], setCats = _cats[1];
        var _saving = useState(false), saving = _saving[0], setSaving = _saving[1];
        var _msg = useState(''), msg = _msg[0], setMsg = _msg[1];

        useEffect(function(){
            apiFetch('/categories?format=flat&per_page=500').then(function(d){
                setCats(Array.isArray(d) ? d : (d.categories||[]));
            }).catch(function(){});
        }, []);

        function setField(i, k, v) {
            setRows(function(prev){ return prev.map(function(r, idx){ if(idx!==i) return r; var n=Object.assign({},r); n[k]=v; return n; }); });
        }
        function addRow() { setRows(function(prev){ return prev.concat([defaultRow()]); }); }
        function cloneRow(i) { setRows(function(prev){ var c=Object.assign({},prev[i]); var n=prev.slice(); n.splice(i+1,0,c); return n; }); }
        function removeRow(i) { setRows(function(prev){ return prev.length===1 ? [defaultRow()] : prev.filter(function(_,idx){ return idx!==i; }); }); }

        function create() {
            var valid = rows.filter(function(r){ return r.name.trim(); });
            if (!valid.length) { setMsg('Enter at least one category name.'); return; }
            setSaving(true); setMsg('');
            Promise.all(valid.map(function(r){
                return apiFetch('/categories', { method:'POST', body:{
                    parent_id: parseInt(r.parent_id)||0,
                    status: r.status,
                    position: parseInt(r.position)||0,
                    translation:{ name: r.name.trim() }
                }});
            })).then(function(){
                setSaving(false);
                navigate('categories');
            }).catch(function(){ setSaving(false); setMsg('Some categories failed to save.'); });
        }

        var thSty = { padding:'10px 12px', fontSize:11, fontWeight:500, color:mutedColor, textTransform:'uppercase', letterSpacing:'.5px', textAlign:'left' };
        var tdSty = { padding:'8px 12px', verticalAlign:'middle' };
        var smallSelect = Object.assign({}, fs, { padding:'7px 10px', fontSize:13, minWidth:0 });
        var smallInput  = Object.assign({}, fs, { padding:'7px 10px', fontSize:13 });
        var actionBtn = function(label, onClick, color) {
            return h('button', { onClick:onClick, style:{ padding:'5px 10px', borderRadius:6, border:'1px solid '+borderColor, background:'transparent', color:color||textColor, fontSize:12, fontWeight:500, cursor:'pointer' } }, label);
        };

        return h('main', { style:{ flex:1, overflow:'auto', padding:S.xxl } },
            // Back + title row
            h('div', { style:{ display:'flex', justifyContent:'space-between', alignItems:'center', marginBottom:S.lg } },
                h('div', { style:{ display:'flex', alignItems:'center', gap:12 } },
                    h('button', { onClick:function(){ navigate('categories'); },
                        style:{ width:34, height:34, borderRadius:8, border:'1px solid '+borderColor, background:'transparent', display:'flex', alignItems:'center', justifyContent:'center', cursor:'pointer', color:mutedColor } },
                        h('svg', { width:16, height:16, viewBox:'0 0 24 24', fill:'none', stroke:'currentColor', strokeWidth:'2', strokeLinecap:'round', strokeLinejoin:'round' },
                            h('path', { d:'m15 18-6-6 6-6' }))
                    ),
                    h('h2', { style:{ fontSize:20, fontWeight:600, letterSpacing:'-.3px' } }, 'Add Categories')
                ),
                h('div', { style:{ display:'flex', alignItems:'center', gap:8 } },
                    msg && h('span', { style:{ fontSize:13, color:msg.indexOf('failed')>=0?TC.error:TC.success, marginRight:8 } }, msg),
                    h('button', { onClick:create, disabled:saving,
                        style:{ padding:'8px 20px', borderRadius:8, border:'none', background:TC.primary, color:TC.primaryFg, fontSize:13, fontWeight:500, cursor:'pointer', opacity:saving?.7:1 } }, saving ? 'Creating…' : 'Create')
                )
            ),

            // Table card
            h('div', { style:{ background:cardBg, borderRadius:12, border:'1px solid '+borderColor, boxShadow:cardShadow(dark) } },
                h('div', { style:{ overflowX:'auto' } },
                    h('table', { style:{ width:'100%', borderCollapse:'collapse', minWidth:800 } },
                        h('thead', null,
                            h('tr', { style:{ borderBottom:'1px solid '+borderColor } },
                                h('th', { style:Object.assign({}, thSty, { width:200 }) }, 'Category Location'),
                                h('th', { style:thSty }, 'Category Name'),
                                h('th', { style:Object.assign({}, thSty, { width:180 }) }, 'Customer Group'),
                                h('th', { style:Object.assign({}, thSty, { width:90 }) }, 'Position'),
                                h('th', { style:Object.assign({}, thSty, { width:130 }) }, 'Status'),
                                h('th', { style:Object.assign({}, thSty, { width:140, textAlign:'right' }) }, '')
                            )
                        ),
                        h('tbody', null,
                            rows.map(function(row, i) {
                                return h('tr', { key:i, style:{ borderBottom: i < rows.length-1 ? '1px solid '+borderColor : 'none' } },
                                    // Category location (parent dropdown)
                                    h('td', { style:tdSty },
                                        h('select', { value:row.parent_id, onChange:function(e){ setField(i,'parent_id',e.target.value); }, style:Object.assign({}, smallSelect, { width:'100%' }) },
                                            h('option', { value:'0' }, '- Root level -'),
                                            catTreeOptions(cats).map(function(o){
                                                var prefix = o.depth > 0 ? '\u00A0'.repeat(o.depth*3) + '└ ' : '';
                                                return h('option', { key:o.id, value:String(o.id) }, prefix + (o.name||'—'));
                                            })
                                        )
                                    ),
                                    // Category name
                                    h('td', { style:tdSty },
                                        h('input', { value:row.name, onChange:function(e){ setField(i,'name',e.target.value); },
                                            placeholder:'Category name', style:Object.assign({}, smallInput, { width:'100%' }) })
                                    ),
                                    // Customer group (placeholder — not yet implemented)
                                    h('td', { style:tdSty },
                                        h('div', { style:{ display:'flex', alignItems:'center', gap:6, color:mutedColor, fontSize:13 } },
                                            h('span', { style:{ display:'flex' } }, icons.customers),
                                            h('span', null, 'All groups')
                                        )
                                    ),
                                    // Position
                                    h('td', { style:tdSty },
                                        h('input', { type:'number', value:row.position, onChange:function(e){ setField(i,'position',e.target.value); },
                                            placeholder:'0', style:Object.assign({}, smallInput, { width:'100%' }) })
                                    ),
                                    // Status
                                    h('td', { style:tdSty },
                                        h('select', { value:row.status, onChange:function(e){ setField(i,'status',e.target.value); }, style:Object.assign({}, smallSelect, { width:'100%' }) },
                                            h('option', { value:'active' }, 'Active'),
                                            h('option', { value:'disabled' }, 'Disabled'),
                                            h('option', { value:'hidden' }, 'Hidden')
                                        )
                                    ),
                                    // Row actions: Add / Clone / Delete
                                    h('td', { style:Object.assign({}, tdSty, { textAlign:'right' }) },
                                        h('div', { style:{ display:'flex', gap:4, justifyContent:'flex-end' } },
                                            actionBtn('Add', addRow, TC.accent),
                                            actionBtn('Clone', function(){ cloneRow(i); }, mutedColor),
                                            h('button', { onClick:function(){ removeRow(i); },
                                                style:{ width:30, height:30, borderRadius:6, border:'1px solid '+borderColor, background:'transparent', display:'flex', alignItems:'center', justifyContent:'center', cursor:'pointer', color:TC.error } },
                                                icons.trash)
                                        )
                                    )
                                );
                            })
                        )
                    )
                )
            )
        );
    }

    /* ── CATEGORIES PAGE — Tree helpers (ACM pattern) ────────────── */

    /** Build nested tree from flat array. Each node gets a `children` array. */
    function catBuildTree(flat) {
        var byParent = {};
        flat.forEach(function(c) {
            var pid = parseInt(c.parent_id) || 0;
            if (!byParent[pid]) byParent[pid] = [];
            byParent[pid].push(c);
        });
        function attach(parentId) {
            return (byParent[parentId] || []).map(function(c) {
                var cid = parseInt(c.id || c.category_id) || 0;
                return Object.assign({}, c, { children: attach(cid) });
            });
        }
        return attach(0);
    }

    /** Walk tree depth-first, only descending into expanded nodes. Returns flat display list. */
    function catFlattenVisible(tree, expanded) {
        var out = [];
        function walk(nodes, depth) {
            nodes.forEach(function(node) {
                out.push(Object.assign({}, node, { _depth: depth }));
                var nid = Number(node.id || node.category_id);
                if (expanded.has(nid) && node.children && node.children.length) {
                    walk(node.children, depth + 1);
                }
            });
        }
        walk(tree, 0);
        return out;
    }

    /** When searching, include matching cats + all their ancestors so tree stays navigable. */
    function catFilterTree(flat, search) {
        if (!search) return flat;
        var q = search.toLowerCase();
        var idMap = {};
        flat.forEach(function(c) { idMap[Number(c.id || c.category_id)] = c; });
        var vis = new Set();
        flat.forEach(function(c) {
            if ((c.name || '').toLowerCase().indexOf(q) >= 0) {
                var cid = Number(c.id || c.category_id);
                vis.add(cid);
                var pid = parseInt(c.parent_id) || 0;
                while (pid && idMap[pid]) {
                    if (vis.has(pid)) break;
                    vis.add(pid);
                    pid = parseInt(idMap[pid].parent_id) || 0;
                }
            }
        });
        return flat.filter(function(c) { return vis.has(Number(c.id || c.category_id)); });
    }

    /** Flatten the full tree in depth-first order for dropdown <option> lists. */
    function catTreeOptions(flat) {
        var tree = catBuildTree(flat);
        var out = [];
        function walk(nodes, depth) {
            nodes.forEach(function(node) {
                out.push({ id: Number(node.id || node.category_id), name: node.name, depth: depth });
                if (node.children && node.children.length) walk(node.children, depth + 1);
            });
        }
        walk(tree, 0);
        return out;
    }

    /* ── CATEGORIES PAGE (Pulse UI + ACM collapsible tree) ─────── */
    function CategoriesPage({ subId, dark, cardBg, textColor, mutedColor, borderColor, tableHeaderBg }) {
        if (subId === 'bulk-add') return h(CategoriesBulkAddPage, { dark:dark, cardBg:cardBg, textColor:textColor, mutedColor:mutedColor, borderColor:borderColor });

        var _ct = useState([]),          cats = _ct[0],      setCats = _ct[1];
        var _ld = useState(true),        loading = _ld[0],   setLoading = _ld[1];
        var _sl = useState(null),        selected = _sl[0],  setSelected = _sl[1];
        var _fm = useState({ name:'', parent_id:'0', description:'', status:'active', meta_title:'', meta_description:'', slug:'', position:'0', image_id:null, image_url:'' });
        var form = _fm[0], setForm = _fm[1];
        var _sv = useState(false),       saving = _sv[0],    setSaving = _sv[1];
        var _mg = useState(''),          msg = _mg[0],       setMsg = _mg[1];
        var _nw = useState(false),       isNew = _nw[0],     setIsNew = _nw[1];
        var _sr = useState(''),          search = _sr[0],    setSearch = _sr[1];
        var _ss = useState(new Set()),   selSet = _ss[0],    setSelSet = _ss[1];
        var _ex = useState(new Set()),   expanded = _ex[0],  setExpanded = _ex[1];
        var _exu = useState(''),         exportUrl = _exu[0], setExportUrl = _exu[1];

        // S204 — Pattern #151 cascade: native confirm() → ConfirmModal for the
        // three destructive prompts on this page. del() and delSelected() are
        // refactored to two-step (trigger modal → onConfirm fires the actual
        // DELETE). cDel = initial single-delete; cDelForce = 409-cascade
        // follow-up when category has products; cBulkDel = bulk delete.
        // Note: this is the LIVE CategoriesPage. There is also an orphaned
        // /* */-commented sibling block further down (L9621+) that is dead
        // text inside an /* ORPHANED_OLD_CATEGORIES */ comment range; do NOT
        // edit code there — JS parser never sees it. S204 Pattern #174 lesson:
        // verify runtime binding (DevTools) before assuming which duplicate
        // declaration is live.
        var _cd  = useState({ open:false, busy:false, target:null }),                  cDel      = _cd[0],       setCDel      = _cd[1];
        var _cdf = useState({ open:false, busy:false, target:null, count:0 }),         cDelForce = _cdf[0],      setCDelForce = _cdf[1];
        var _cbd = useState({ open:false, busy:false, ids:[] }),                       cBulkDel  = _cbd[0],      setCBulkDel  = _cbd[1];

        // Drag-and-drop reorder state. Only same-parent reorders allowed in this phase.
        // dragId         = id of row currently being dragged (null when no drag in flight)
        // dropOverId     = id of row currently hovered as a valid drop target
        // dropPos        = 'before' | 'after' (which side of dropOverId the drop will land)
        // reorderSaving  = guard against concurrent reorder POSTs
        var _dg  = useState(null),       dragId = _dg[0],         setDragId = _dg[1];
        var _do  = useState(null),       dropOverId = _do[0],     setDropOverId = _do[1];
        var _dp  = useState(null),       dropPos = _dp[0],        setDropPos = _dp[1];
        var _rs  = useState(false),      reorderSaving = _rs[0],  setReorderSaving = _rs[1];

        var fs = fieldSty(dark, borderColor);
        var labelSty = { fontSize:12, fontWeight:500, display:'block', marginBottom:S.xs, color:mutedColor };

        function load() {
            setLoading(true);
            apiFetch('/categories?per_page=500&status=').then(function(d) {
                setCats(Array.isArray(d) ? d : (d.categories || []));
                setLoading(false);
            }).catch(function(){ setLoading(false); });
        }
        useEffect(load, []);

        var _exm = useState(false), showExportMenu = _exm[0], setShowExportMenu = _exm[1];

        function exportCats(mode) {
            setShowExportMenu(false);
            var ids = Array.from(selSet).join(',');
            if (!ids) return;
            apiFetch('/categories/export?mode=' + mode + '&ids=' + ids)
                .then(function(d){
                    if (d && d.url) window.open(d.url, '_blank');
                    else if (d && d.error) alert(d.error);
                });
        }

        // ── Tree computation ──
        var filtered = search ? catFilterTree(cats, search) : cats;
        var tree = catBuildTree(filtered);
        // When searching, auto-expand everything so matches are visible
        var effectiveExpanded = search
            ? new Set(filtered.map(function(c){ return Number(c.id || c.category_id); }))
            : expanded;
        var rows = catFlattenVisible(tree, effectiveExpanded);

        // ── Stats ──
        var stats = { total:0, products:0, active:0, hidden:0, disabled:0 };
        cats.forEach(function(c) {
            stats.total++;
            stats.products += parseInt(c.product_count) || 0;
            var s = (c.status||'').toLowerCase();
            if (s === 'active') stats.active++;
            else if (s === 'hidden') stats.hidden++;
            else stats.disabled++;
        });

        // ── Expand / collapse ──
        function toggleExpand(cid) {
            var id = Number(cid);
            setExpanded(function(prev) {
                var n = new Set(prev);
                if (n.has(id)) n.delete(id); else n.add(id);
                return n;
            });
        }
        function expandAll() {
            setExpanded(new Set(cats.map(function(c){ return Number(c.id || c.category_id); })));
        }
        function collapseAll() {
            setExpanded(new Set());
        }

        // ── Selection ──
        var allChecked = rows.length > 0 && rows.every(function(c){ return selSet.has(Number(c.id||c.category_id)); });
        function toggleAll(e) {
            if (e.target.checked) { setSelSet(new Set(rows.map(function(c){ return Number(c.id||c.category_id); }))); }
            else { setSelSet(new Set()); }
        }
        function toggleOne(cid) {
            var id = Number(cid);
            setSelSet(function(prev){ var n=new Set(prev); if(n.has(id)) n.delete(id); else n.add(id); return n; });
        }

        // ── CRUD ──
        function selectCat(cat) {
            setIsNew(false); setSelected(cat); setMsg('');
            setForm({
                name: cat.name||'', parent_id: String(cat.parent_id||'0'), description: cat.description||'',
                status: cat.status||'active', meta_title: cat.meta_title||'',
                meta_description: cat.meta_description||cat.meta_desc||'', slug: cat.slug||'',
                position: String(cat.position||'0'),
                age_restricted: !!Number(cat.age_restricted),
                age_limit: parseInt(cat.age_limit) || 0,
                image_id: cat.image_id ? Number(cat.image_id) : null,
                image_url: cat.image_url || '',
            });
        }
        function addNew() {
            setIsNew(true); setSelected(null); setMsg('');
            setForm({ name:'', parent_id:'0', description:'', status:'active', meta_title:'', meta_description:'', slug:'', position:'0', image_id:null, image_url:'', age_restricted:false, age_limit:0 });
        }
        function setField(k, v) { setForm(function(prev){ return Object.assign({}, prev, { [k]: v }); }); }
        function handleNameChange(v) {
            setField('name', v);
            if (!form.slug || isNew) setField('slug', v.toLowerCase().replace(/[^a-z0-9]+/g,'-').replace(/^-+|-+$/g,''));
        }
        function save() {
            if (!form.name.trim()) { setMsg('Category name is required.'); return; }
            setSaving(true); setMsg('');
            var body = {
                parent_id: parseInt(form.parent_id)||0, status: form.status, slug: form.slug,
                position: parseInt(form.position)||0,
                image_id: form.image_id || 0,
                age_restricted: form.age_restricted ? 1 : 0,
                age_limit: parseInt(form.age_limit) || 0,
                translation: { name:form.name, description:form.description, meta_title:form.meta_title, meta_desc:form.meta_description },
            };
            var catId = selected ? Number(selected.category_id||selected.id) : null;
            var req = (isNew||!catId)
                ? apiFetch('/categories', { method:'POST', body:body })
                : apiFetch('/categories/'+catId, { method:'PUT', body:body });
            req.then(function(d) {
                setSaving(false); setMsg('Saved.'); load();
                if (d.category_id||d.id) { setSelected(Object.assign({},form,d)); setIsNew(false); }
            }).catch(function(){ setSaving(false); setMsg('Save failed.'); });
        }
        function del(cat) {
            // S204 — Pattern #151 cascade: native confirm() → ConfirmModal.
            // The actual delete fires from cDel modal's onConfirm via
            // delConfirm(). On 409 (category has products), the cDelForce
            // modal opens with the product count to confirm the force-delete.
            setCDel({ open:true, busy:false, target:cat });
        }
        function delConfirm() {
            var cat = cDel.target;
            if (!cat) return;
            var catId = Number(cat.category_id||cat.id);
            setCDel(function(prev){ return Object.assign({}, prev, { busy:true }); });
            // Optimistically remove from local state for instant feedback.
            setCats(function(prev){ return prev.filter(function(c){ return Number(c.category_id||c.id) !== catId; }); });
            if (selected && Number(selected.category_id||selected.id)===catId) { setSelected(null); setIsNew(false); }
            apiFetch('/categories/'+catId, { method:'DELETE' }).then(function() {
                setCDel({ open:false, busy:false, target:null });
                load();
            }).catch(function(err){
                setCDel({ open:false, busy:false, target:null });
                if (err && err.status === 409 && err.data && err.data.data && err.data.data.product_count) {
                    setCDelForce({ open:true, busy:false, target:cat, count:err.data.data.product_count });
                    return;
                }
                load();
            });
        }
        function delForceConfirm() {
            var cat = cDelForce.target;
            if (!cat) return;
            var catId = Number(cat.category_id||cat.id);
            setCDelForce(function(prev){ return Object.assign({}, prev, { busy:true }); });
            apiFetch('/categories/'+catId+'?force=true', { method:'DELETE' })
                .then(function(){ setCDelForce({ open:false, busy:false, target:null, count:0 }); load(); })
                .catch(function(){ setCDelForce({ open:false, busy:false, target:null, count:0 }); load(); });
        }
        function delSelected() {
            // S204 — Pattern #151 cascade: native confirm() → ConfirmModal.
            // Bulk delete force-cascades (existing pre-S204 behavior — no
            // 409 round-trip), so there is no separate force modal here.
            if (!selSet.size) return;
            setCBulkDel({ open:true, busy:false, ids:Array.from(selSet) });
        }
        function delSelectedConfirm() {
            var ids = cBulkDel.ids;
            if (!ids.length) return;
            setCBulkDel(function(prev){ return Object.assign({}, prev, { busy:true }); });
            // Optimistically remove from local state.
            setCats(function(prev){ return prev.filter(function(c){ return !ids.includes(Number(c.category_id||c.id)); }); });
            setSelSet(new Set());
            Promise.all(ids.map(function(id){ return apiFetch('/categories/'+id+'?force=true', { method:'DELETE' }).catch(function(){}); }))
                .then(function(){ setCBulkDel({ open:false, busy:false, ids:[] }); load(); })
                .catch(function(){ setCBulkDel({ open:false, busy:false, ids:[] }); load(); });
        }

        // ── Drag-and-drop reorder ────────────────────────────────────────────────
        // Only same-parent reorders are supported in this phase. Cross-parent moves
        // continue to flow through the edit form (parent_id field). Disabled when a
        // search filter is active because filtered rows are not the full sibling set
        // — committing a partial reorder would corrupt position values.
        function isReorderDisabled() {
            return !!search || !!reorderSaving;
        }
        function findCatById(cid) {
            for (var i = 0; i < cats.length; i++) {
                if (Number(cats[i].id || cats[i].category_id) === cid) return cats[i];
            }
            return null;
        }
        function getSiblingsOrdered(parentId) {
            // cats is returned by /categories ordered by position ASC, so filter
            // preserves canonical sibling order.
            return cats.filter(function(c){ return (parseInt(c.parent_id)||0) === parentId; });
        }
        function clearDragState() {
            setDragId(null); setDropOverId(null); setDropPos(null);
        }
        function onRowDragStart(cat, e) {
            if (isReorderDisabled()) { if (e.preventDefault) e.preventDefault(); return; }
            var cid = Number(cat.id || cat.category_id);
            setDragId(cid);
            if (e.dataTransfer) {
                e.dataTransfer.effectAllowed = 'move';
                // Firefox requires data on the transfer to start the drag.
                try { e.dataTransfer.setData('text/plain', String(cid)); } catch (_) {}
            }
        }
        function onRowDragOver(cat, e) {
            if (dragId == null) return;
            var cid = Number(cat.id || cat.category_id);
            if (cid === dragId) return;
            var dragged = findCatById(dragId);
            if (!dragged) return;
            // Same-parent guard — silently ignore cross-parent hovers.
            if ((parseInt(dragged.parent_id)||0) !== (parseInt(cat.parent_id)||0)) return;
            if (e.preventDefault) e.preventDefault();
            if (e.dataTransfer) e.dataTransfer.dropEffect = 'move';
            var rect = e.currentTarget.getBoundingClientRect();
            var pos  = (e.clientY < rect.top + rect.height/2) ? 'before' : 'after';
            if (dropOverId !== cid || dropPos !== pos) {
                setDropOverId(cid); setDropPos(pos);
            }
        }
        function onRowDrop(cat, e) {
            if (e.preventDefault) e.preventDefault();
            if (dragId == null) { clearDragState(); return; }
            var dragged = findCatById(dragId);
            var targetId = Number(cat.id || cat.category_id);
            var pos = dropPos || 'after';
            clearDragState();
            if (!dragged || targetId === Number(dragged.id||dragged.category_id)) return;
            if ((parseInt(dragged.parent_id)||0) !== (parseInt(cat.parent_id)||0)) return;
            doReorder(dragged, cat, pos);
        }
        function onRowDragEnd() { clearDragState(); }

        function doReorder(draggedCat, targetCat, pos) {
            var parentId  = parseInt(draggedCat.parent_id) || 0;
            var draggedId = Number(draggedCat.id || draggedCat.category_id);
            var targetId  = Number(targetCat.id  || targetCat.category_id);

            // Build the full new sibling order (POST /categories/reorder requires
            // the complete sibling set, not just the moved id).
            var siblings   = getSiblingsOrdered(parentId);
            var orderedIds = siblings
                .map(function(c){ return Number(c.id || c.category_id); })
                .filter(function(id){ return id !== draggedId; });
            var targetIdx  = orderedIds.indexOf(targetId);
            if (targetIdx < 0) return;
            var insertIdx  = (pos === 'before') ? targetIdx : targetIdx + 1;
            orderedIds.splice(insertIdx, 0, draggedId);

            // Snapshot for rollback if the server rejects the move.
            var snapshot = cats;

            // Optimistic local update — splice siblings into their new order in place
            // (at the slot where the first sibling currently lives) and bump local
            // position values so the visual sort matches what the server will store.
            var siblingSet  = new Set(orderedIds);
            var byId        = {};
            cats.forEach(function(c){
                var id = Number(c.id || c.category_id);
                if (siblingSet.has(id)) byId[id] = c;
            });
            var newSiblings = orderedIds.map(function(id, idx){
                return Object.assign({}, byId[id], { position: idx });
            });
            var newCats = [];
            var inserted = false;
            cats.forEach(function(c){
                var id = Number(c.id || c.category_id);
                if (siblingSet.has(id)) {
                    if (!inserted) {
                        newSiblings.forEach(function(s){ newCats.push(s); });
                        inserted = true;
                    }
                    // skip — sibling already inserted as part of newSiblings block
                } else {
                    newCats.push(c);
                }
            });
            setCats(newCats);

            setReorderSaving(true);
            apiFetch('/categories/reorder', {
                method: 'POST',
                body:   { parent_id: parentId, ordered_ids: orderedIds }
            }).then(function(){
                setReorderSaving(false);
                // Reload to pick up server-side rebuilt nested-set values (lft/rgt/depth).
                load();
            }).catch(function(err){
                setReorderSaving(false);
                setCats(snapshot);
                var msg = (err && err.data && err.data.message)
                    || (err && err.message)
                    || 'Reorder failed.';
                alert(msg);
            });
        }

        // ── Status badge ──
        function statusBadge(st) {
            var s = (st||'').toLowerCase();
            if (s==='active') return { bg:dark?'rgba(22,163,74,.1)':'#F0FDF4', color:TC.success, border:TC.success+'30', label:'Active' };
            if (s==='hidden') return { bg:dark?'rgba(217,119,6,.1)':'#FFFBEB', color:TC.amber, border:TC.amber+'30', label:'Hidden' };
            return { bg:dark?'rgba(220,38,38,.1)':'#FEF2F2', color:TC.error, border:TC.error+'30', label:'Disabled' };
        }

        // ── Chevron icons ──
        var chevRight = h('svg',{width:14,height:14,viewBox:'0 0 14 14',fill:'none',stroke:'currentColor',strokeWidth:'1.5',strokeLinecap:'round',strokeLinejoin:'round',style:{display:'block'}}, h('path',{d:'M5 3l4 4-4 4'}));
        var chevDown  = h('svg',{width:14,height:14,viewBox:'0 0 14 14',fill:'none',stroke:'currentColor',strokeWidth:'1.5',strokeLinecap:'round',strokeLinejoin:'round',style:{display:'block'}}, h('path',{d:'M3 5l4 4 4-4'}));

        var showForm = selected || isNew;

        return h('main', { style:{ flex:1, overflow:'auto', padding:S.xxl } },
            h('div', { style:{ display:'grid', gridTemplateColumns: showForm ? '1fr 380px' : '1fr 260px', gap:S.lg, alignItems:'start' } },

                // ═══ LEFT — Category tree table ═══
                h('div', { style:{ background:cardBg, borderRadius:12, border:'1px solid '+borderColor, boxShadow:cardShadow(dark) } },

                    // Card header
                    h('div', { style:{ display:'flex', justifyContent:'space-between', alignItems:'center', padding:'16px 24px', borderBottom:'1px solid '+borderColor, flexWrap:'wrap', gap:12 } },
                        h('div', null,
                            h('div', { style:{ fontSize:16, fontWeight:600, marginBottom:2 } }, 'Categories'),
                            h('div', { style:{ fontSize:13, color:mutedColor } }, stats.total + ' categories')
                        ),
                        h('div', { style:{ display:'flex', gap:8, alignItems:'center' } },
                            // Search
                            h('div', { style:{ position:'relative', width:200 } },
                                h('span', { style:{ position:'absolute', left:10, top:'50%', transform:'translateY(-50%)', color:mutedColor, display:'flex' } }, icons.search),
                                h('input', { value:search, onChange:function(e){ setSearch(e.target.value); },
                                    placeholder:'Search...', style:{ width:'100%', padding:'7px 10px 7px 32px', borderRadius:8, border:'1px solid '+borderColor, background:dark?'#262626':'#fff', color:textColor, fontSize:13, outline:'none' } })
                            ),
                            // Export gear dropdown
                            h('div', { style:{ position:'relative' } },
                                h('button', { onClick:function(){ setShowExportMenu(!showExportMenu); },
                                    style:{ display:'flex', alignItems:'center', gap:6, padding:'7px 10px', borderRadius:8, border:'1px solid '+borderColor, background:'transparent', color: selSet.size ? textColor : mutedColor, fontSize:13, cursor:'pointer' } },
                                    icons.settings),
                                showExportMenu && h('div', { onClick:function(){ setShowExportMenu(false); }, style:{ position:'fixed', inset:0, zIndex:99 } }),
                                showExportMenu && h('div', { style:{ position:'absolute', top:'100%', right:0, marginTop:4, background:cardBg, border:'1px solid '+borderColor, borderRadius:8, boxShadow:'0 4px 12px rgba(0,0,0,.12)', zIndex:100, minWidth:300, padding:'4px 0' } },
                                    [
                                        { mode:'categories', label:'Export selected categories' },
                                        { mode:'categories_with_subs', label:'Export selected categories & subcategories' },
                                        { mode:'products', label:'Export products from selected categories' },
                                        { mode:'products_with_subs', label:'Export products from selected categories & subcategories' },
                                    ].map(function(opt) {
                                        var disabled = selSet.size === 0;
                                        return h('div', { key:opt.mode, onClick:function(){ if(!disabled) exportCats(opt.mode); },
                                            style:{ padding:'8px 16px', fontSize:12.5, color: disabled ? mutedColor : textColor, cursor: disabled ? 'default' : 'pointer', opacity: disabled ? 0.5 : 1 },
                                            onMouseEnter:function(e){ if(!disabled) e.currentTarget.style.background = dark?'rgba(255,255,255,.04)':'#F5F5F5'; },
                                            onMouseLeave:function(e){ e.currentTarget.style.background = 'transparent'; }
                                        }, opt.label);
                                    })
                                )
                            ),
                            // Bulk add
                            h('button', { onClick:function(){ navigate('categories/bulk-add'); },
                                style:{ display:'flex', alignItems:'center', gap:6, padding:'7px 14px', borderRadius:8, border:'1px solid '+borderColor, background:'transparent', color:textColor, fontSize:13, fontWeight:500, cursor:'pointer' } },
                                icons.plus, ' Bulk Add'),
                            // Add category
                            h('button', { onClick:addNew,
                                style:{ display:'flex', alignItems:'center', gap:6, padding:'7px 14px', borderRadius:8, border:'none', background:TC.primary, color:TC.primaryFg, fontSize:13, fontWeight:500, cursor:'pointer' } },
                                icons.plus, ' Add Category')
                        )
                    ),

                    // Card content
                    h('div', { style:{ padding:0 } },

                        // Expand/Collapse bar + selection
                        h('div', { style:{ display:'flex', justifyContent:'space-between', alignItems:'center', padding:'8px 24px', borderBottom:'1px solid '+borderColor, background:dark?'rgba(255,255,255,.01)':'#FAFBFC' } },
                            !search
                                ? h('div', { style:{ display:'flex', gap:12 } },
                                    h('button', { onClick:expandAll, style:{ background:'transparent', border:'none', color:TC.accent, fontSize:12, fontWeight:500, cursor:'pointer', padding:0 } }, 'Expand all'),
                                    h('button', { onClick:collapseAll, style:{ background:'transparent', border:'none', color:mutedColor, fontSize:12, fontWeight:500, cursor:'pointer', padding:0 } }, 'Collapse all')
                                )
                                : h('span', { style:{ fontSize:12, color:mutedColor } }, rows.length + ' match' + (rows.length !== 1 ? 'es' : '')),
                            selSet.size > 0 && h('div', { style:{ display:'flex', gap:8, alignItems:'center' } },
                                h('span', { style:{ fontSize:12, color:mutedColor } }, selSet.size + ' selected'),
                                h('button', { onClick:delSelected,
                                    style:{ padding:'4px 10px', borderRadius:6, border:'none', background:TC.error, color:'#fff', fontSize:11, fontWeight:500, cursor:'pointer' } }, 'Delete')
                            )
                        ),

                        // Table
                        h('div', { style:{ overflowX:'auto' } },
                            h('table', { style:{ width:'100%', borderCollapse:'collapse', minWidth:550 } },
                                h('thead', null,
                                    h('tr', { style:{ borderBottom:'1px solid '+borderColor } },
                                        h('th', { style:{ width:32, padding:'10px 0', textAlign:'center' } }, ''),
                                        h('th', { style:{ width:40, padding:'10px 0 10px 24px', textAlign:'left' } },
                                            h('input', { type:'checkbox', checked:allChecked, onChange:toggleAll, style:{ cursor:'pointer' } })
                                        ),
                                        h('th', { style:{ padding:'10px 12px', fontSize:11, fontWeight:500, color:mutedColor, textTransform:'uppercase', letterSpacing:'.5px', textAlign:'left', width:70 } }, 'Pos.'),
                                        h('th', { style:{ padding:'10px 12px', fontSize:11, fontWeight:500, color:mutedColor, textTransform:'uppercase', letterSpacing:'.5px', textAlign:'left' } }, 'Name'),
                                        h('th', { style:{ padding:'10px 12px', fontSize:11, fontWeight:500, color:mutedColor, textTransform:'uppercase', letterSpacing:'.5px', textAlign:'left', width:90 } }, 'Products'),
                                        h('th', { style:{ padding:'10px 12px', fontSize:11, fontWeight:500, color:mutedColor, textTransform:'uppercase', letterSpacing:'.5px', textAlign:'left', width:100 } }, 'Status'),
                                        h('th', { style:{ padding:'10px 12px', fontSize:11, fontWeight:500, color:mutedColor, textTransform:'uppercase', letterSpacing:'.5px', textAlign:'right', width:50 } }, '')
                                    )
                                ),
                                h('tbody', null,
                                    loading && h('tr', null, h('td', { colSpan:7, style:{ textAlign:'center', padding:48, color:mutedColor, fontSize:13 } }, 'Loading…')),
                                    !loading && rows.length === 0 && h('tr', null, h('td', { colSpan:7, style:{ textAlign:'center', padding:48, color:mutedColor, fontSize:13 } },
                                        search ? 'No categories match "'+search+'".' : 'No categories yet. Click "+ Add Category" to start.')),
                                    !loading && rows.map(function(cat, i) {
                                        var cid = Number(cat.id || cat.category_id);
                                        var depth = cat._depth || 0;
                                        var hasKids = cat.children && cat.children.length > 0;
                                        var isExp = effectiveExpanded.has(cid);
                                        var isSel = selSet.has(cid);
                                        var isEditing = selected && Number(selected.category_id||selected.id) === cid;
                                        var badge = statusBadge(cat.status);

                                        return h('tr', { key:cid,
                                            draggable: !isReorderDisabled(),
                                            onDragStart:function(e){ onRowDragStart(cat, e); },
                                            onDragOver: function(e){ onRowDragOver(cat, e); },
                                            onDrop:     function(e){ onRowDrop(cat, e); },
                                            onDragEnd:  onRowDragEnd,
                                            style:{ borderBottom: i < rows.length-1 ? '1px solid '+borderColor : 'none',
                                                    background: isEditing ? (dark?'rgba(37,99,235,.06)':'#EFF6FF') : isSel ? (dark?'rgba(37,99,235,.04)':'#F5F9FF') : 'transparent',
                                                    opacity: dragId === cid ? 0.45 : 1,
                                                    boxShadow: (dropOverId === cid && dragId != null && dragId !== cid)
                                                        ? (dropPos === 'before'
                                                            ? 'inset 0 2px 0 0 ' + TC.accent
                                                            : 'inset 0 -2px 0 0 ' + TC.accent)
                                                        : 'none',
                                                    transition:'background .1s, opacity .1s' },
                                            onMouseEnter:function(e){ if(!isSel&&!isEditing&&dragId==null) e.currentTarget.style.background=dark?'rgba(255,255,255,.02)':'#F9FAFB'; },
                                            onMouseLeave:function(e){ if(!isSel&&!isEditing&&dragId==null) e.currentTarget.style.background='transparent'; }
                                        },
                                            // Drag handle
                                            h('td', {
                                                style:{
                                                    width:32, padding:'10px 0', textAlign:'center', verticalAlign:'middle',
                                                    cursor: isReorderDisabled() ? 'not-allowed' : (dragId === cid ? 'grabbing' : 'grab'),
                                                    color: isReorderDisabled() ? (mutedColor) : mutedColor,
                                                    opacity: isReorderDisabled() ? 0.3 : 0.7,
                                                    userSelect:'none'
                                                },
                                                title: search ? 'Clear search to reorder' : (reorderSaving ? 'Saving…' : 'Drag to reorder')
                                            },
                                                h('svg', { width:10, height:16, viewBox:'0 0 10 16', fill:'currentColor', style:{ display:'block', margin:'0 auto', pointerEvents:'none' } },
                                                    h('circle', { cx:2.5, cy:3,  r:1.2 }), h('circle', { cx:7.5, cy:3,  r:1.2 }),
                                                    h('circle', { cx:2.5, cy:8,  r:1.2 }), h('circle', { cx:7.5, cy:8,  r:1.2 }),
                                                    h('circle', { cx:2.5, cy:13, r:1.2 }), h('circle', { cx:7.5, cy:13, r:1.2 })
                                                )
                                            ),
                                            // Checkbox
                                            h('td', { style:{ padding:'10px 0 10px 24px', verticalAlign:'middle' } },
                                                h('input', { type:'checkbox', checked:isSel, onChange:function(){ toggleOne(cid); }, style:{ cursor:'pointer' }, onClick:function(e){ e.stopPropagation(); } })
                                            ),
                                            // Position
                                            h('td', { style:{ padding:'10px 12px', fontSize:13, color:mutedColor, verticalAlign:'middle' } }, String(cat.position||0)),
                                            // Name — indented with chevron toggle
                                            h('td', { style:{ padding:'10px 12px', verticalAlign:'middle' } },
                                                h('div', { style:{ display:'flex', alignItems:'center', paddingLeft:(depth*20)+'px' } },
                                                    // Chevron or placeholder
                                                    hasKids
                                                        ? h('button', { onClick:function(e){ e.stopPropagation(); toggleExpand(cid); },
                                                            style:{ width:22, height:22, borderRadius:4, border:'none', background:'transparent', display:'flex', alignItems:'center', justifyContent:'center', cursor:'pointer', color:mutedColor, flexShrink:0, marginRight:4, transition:'color .15s' },
                                                            onMouseEnter:function(e){ e.currentTarget.style.color=TC.accent; },
                                                            onMouseLeave:function(e){ e.currentTarget.style.color=mutedColor; }
                                                          }, isExp ? chevDown : chevRight)
                                                        : h('span', { style:{ width:26, flexShrink:0 } }),
                                                    // Name link
                                                    h('span', { onClick:function(){ selectCat(cat); },
                                                        style:{ fontSize:13, fontWeight:isEditing?600:hasKids?500:400, cursor:'pointer', color:isEditing?TC.accent:textColor } },
                                                        cat.name||'—')
                                                )
                                            ),
                                            // Products count — clickable to filter products by this category
                                            h('td', { style:{ padding:'10px 12px', verticalAlign:'middle' } },
                                                h('span', { onClick:function(){ if(parseInt(cat.product_count)){ window.__tc_nav_cat = cid; navigate('products'); } },
                                                    style:{ fontSize:12, fontWeight:500, padding:'2px 10px', borderRadius:99, background:dark?'rgba(255,255,255,.05)':'#F5F5F5', color: parseInt(cat.product_count) ? TC.accent : mutedColor, display:'inline-block', cursor: parseInt(cat.product_count) ? 'pointer' : 'default' } }, String(cat.product_count||0))
                                            ),
                                            // Status
                                            h('td', { style:{ padding:'10px 12px', verticalAlign:'middle' } },
                                                h('span', { style:{ fontSize:11, fontWeight:500, padding:'2px 8px', borderRadius:99, background:badge.bg, color:badge.color, border:'1px solid '+badge.border, display:'inline-block', whiteSpace:'nowrap' } }, badge.label)
                                            ),
                                            // Actions
                                            h('td', { style:{ padding:'10px 12px', textAlign:'right', verticalAlign:'middle' } },
                                                h(RowActionMenu, {
                                                    pid:cid,
                                                    onView:function(){ selectCat(cat); },
                                                    onEdit:function(){ selectCat(cat); },
                                                    onDelete:function(){ del(cat); },
                                                    dark:dark, cardBg:cardBg, borderColor:borderColor, textColor:textColor, mutedColor:mutedColor
                                                })
                                            )
                                        );
                                    })
                                )
                            )
                        )
                    )
                ),

                // ═══ RIGHT — Stats or Edit form ═══
                showForm
                    ? h('div', { style:{ background:cardBg, borderRadius:12, border:'1px solid '+borderColor, boxShadow:cardShadow(dark), padding:S.xl, position:'sticky', top:S.xxl } },
                        h('div', { style:{ display:'flex', justifyContent:'space-between', alignItems:'center', marginBottom:S.lg } },
                            h('div', { style:{ fontSize:15, fontWeight:600 } }, isNew ? 'New Category' : 'Edit Category'),
                            msg && h('span', { style:{ fontSize:12, color:msg==='Save failed.'?TC.error:TC.success } }, msg)
                        ),
                        h('div', { style:{ marginBottom:S.md } },
                            h('label', { style:labelSty }, 'Category Name *'),
                            h('input', { value:form.name, onChange:function(e){ handleNameChange(e.target.value); }, placeholder:'e.g. Electronics', style:fs })
                        ),
                        h('div', { style:{ marginBottom:S.md } },
                            h('label', { style:labelSty }, 'Parent Category'),
                            h('select', { value:form.parent_id, onChange:function(e){ setField('parent_id',e.target.value); }, style:fs },
                                h('option', { value:'0' }, '— Root (top level) —'),
                                catTreeOptions(cats).filter(function(o){ return !selected || o.id!==Number(selected.category_id||selected.id); }).map(function(o) {
                                    var prefix = o.depth > 0 ? '\u00A0'.repeat(o.depth*3) + '└ ' : '';
                                    return h('option', { key:o.id, value:String(o.id) }, prefix + (o.name||'—'));
                                })
                            )
                        ),
                        h('div', { style:{ marginBottom:S.md } },
                            h('label', { style:labelSty }, 'Description'),
                            h('textarea', { value:form.description, onChange:function(e){ setField('description',e.target.value); }, rows:3, style:Object.assign({},fs,{resize:'vertical'}) })
                        ),
                        // Category Image
                        h('div', { style:{ marginBottom:S.md } },
                            h('label', { style:labelSty }, 'Category Image'),
                            h('div', { style:{ fontSize:11, color:mutedColor, marginBottom:6 } }, 'Recommended: 400 \u00D7 400 px square image. Shown on homepage and category pages.'),
                            form.image_url
                                ? h('div', { style:{ position:'relative', display:'inline-block', marginBottom:8 } },
                                    h('img', { src:form.image_url, alt:'Category image',
                                        style:{ width:120, height:120, objectFit:'cover', borderRadius:10, border:'1px solid '+borderColor } }),
                                    h('button', {
                                        onClick:function(){ setField('image_id', null); setField('image_url', ''); },
                                        title:'Remove image',
                                        style:{ position:'absolute', top:-6, right:-6, width:22, height:22, borderRadius:'50%', border:'none',
                                            background:'#DC2626', color:'#fff', fontSize:12, cursor:'pointer', display:'flex', alignItems:'center', justifyContent:'center', padding:0 }
                                    }, '\u00D7')
                                  )
                                : null,
                            h('button', {
                                type:'button',
                                onClick:function() {
                                    if (!window.wp || !wp.media) { alert('WordPress media library not available.'); return; }
                                    var frame = wp.media({ title:'Select Category Image', button:{ text:'Use this image' }, multiple:false, library:{ type:'image' } });
                                    frame.on('select', function() {
                                        var att = frame.state().get('selection').first().toJSON();
                                        setField('image_id', att.id);
                                        setField('image_url', (att.sizes && att.sizes.medium) ? att.sizes.medium.url : att.url);
                                    });
                                    frame.open();
                                },
                                style:{ padding:'6px 14px', borderRadius:8, border:'1px solid '+borderColor, background:dark?'#2a2a2a':'#F5F5F5',
                                    color:textColor, fontSize:12, fontWeight:500, cursor:'pointer', display:'inline-flex', alignItems:'center', gap:6 }
                            }, '\uD83D\uDDBC ', form.image_url ? 'Change Image' : 'Upload Image')
                        ),
                        h('div', { style:{ display:'grid', gridTemplateColumns:'1fr 1fr', gap:S.md, marginBottom:S.md } },
                            h('div', null,
                                h('label', { style:labelSty }, 'Status'),
                                h('select', { value:form.status, onChange:function(e){ setField('status',e.target.value); }, style:fs },
                                    h('option',{value:'active'},'Active'), h('option',{value:'disabled'},'Disabled'), h('option',{value:'hidden'},'Hidden')
                                )
                            ),
                            h('div', null,
                                h('label', { style:labelSty }, 'Position'),
                                h('input', { type:'number', value:form.position, onChange:function(e){ setField('position',e.target.value); }, style:fs })
                            )
                        ),
                        h('div', { style:{ marginBottom:S.md, padding:'12px 14px', border:'1px solid '+borderColor, borderRadius:8, background:dark?'#2a2a2a':'#FAFAFA' } },
                            h('label', { style:{ display:'flex', alignItems:'center', gap:8, cursor:'pointer', fontSize:13, fontWeight:500, color:textColor } },
                                h('input', { type:'checkbox', checked:!!form.age_restricted, onChange:function(e){ setField('age_restricted', e.target.checked); }, style:{ accentColor:TC.accent } }),
                                'Products in this category are age-restricted'
                            ),
                            form.age_restricted && h('div', { style:{ display:'flex', alignItems:'center', gap:8, marginTop:10 } },
                                h('label', { style:{ fontSize:12, color:mutedColor } }, 'Minimum age'),
                                h('input', { type:'number', min:0, max:99, value:form.age_limit, onChange:function(e){ setField('age_limit', e.target.value); },
                                    onWheel:function(e){ e.target.blur(); },
                                    style:Object.assign({}, fs, { width:80 }) }),
                                h('span', { style:{ fontSize:12, color:mutedColor } }, '(0 = use global default)')
                            )
                        ),
                        h('div', { style:{ marginBottom:S.md } },
                            h('label', { style:labelSty }, 'URL Slug'),
                            h('input', { value:form.slug, onChange:function(e){ setField('slug',e.target.value); }, placeholder:'url-slug', style:fs })
                        ),
                        h('div', { style:{ marginBottom:S.md } },
                            h('label', { style:labelSty }, 'Meta Title'),
                            h('input', { value:form.meta_title, onChange:function(e){ setField('meta_title',e.target.value); }, style:fs })
                        ),
                        h('div', { style:{ marginBottom:S.lg } },
                            h('label', { style:labelSty }, 'Meta Description'),
                            h('textarea', { value:form.meta_description, onChange:function(e){ setField('meta_description',e.target.value); }, rows:2, style:Object.assign({},fs,{resize:'vertical'}) })
                        ),
                        h('div', { style:{ display:'flex', gap:S.sm, justifyContent:'flex-end' } },
                            h('button', { onClick:function(){ setSelected(null); setIsNew(false); setMsg(''); },
                                style:{ padding:'7px 16px', borderRadius:8, border:'1px solid '+borderColor, background:'transparent', color:textColor, fontSize:13, fontWeight:500, cursor:'pointer' } }, 'Cancel'),
                            h('button', { onClick:save, disabled:saving,
                                style:{ padding:'7px 16px', borderRadius:8, border:'none', background:TC.primary, color:TC.primaryFg, fontSize:13, fontWeight:500, cursor:'pointer', opacity:saving?.7:1 } }, saving?'Saving…':'Save Category')
                        )
                    )
                    : h('div', { style:{ background:cardBg, borderRadius:12, border:'1px solid '+borderColor, boxShadow:cardShadow(dark), padding:S.xl, position:'sticky', top:S.xxl } },
                        h('div', { style:{ fontSize:15, fontWeight:600, marginBottom:S.lg } }, 'Total'),
                        [
                            ['Categories',          stats.total],
                            ['Products',            stats.products],
                            ['Active categories',   stats.active],
                            ['Hidden categories',   stats.hidden],
                            ['Disabled categories',  stats.disabled],
                        ].map(function(row) {
                            return h('div', { key:row[0], style:{ display:'flex', justifyContent:'space-between', alignItems:'center', padding:'8px 0', borderBottom:'1px solid '+borderColor, fontSize:13 } },
                                h('span', { style:{ color:mutedColor } }, row[0]),
                                h('span', { style:{ fontWeight:500 } }, String(row[1]))
                            );
                        })
                    )
            ),

            /* S204 — Pattern #151 cascade close-out for CategoriesPage.
             * Three destructive prompts previously gated by native confirm() —
             * single delete, 409-cascade force follow-up when category has
             * products, and bulk delete — now route through ConfirmModal for
             * consistent Pulse UI. Pattern #174 third instance (this session)
             * caught a structural duplicate but runtime-binding lookup via
             * DevTools was required to identify WHICH declaration was live.
             * The dead one is the function declaration INSIDE the
             * ORPHANED_OLD_CATEGORIES comment block further down in this
             * file — JS parser never sees it. */
            h(ConfirmModal, {
                open: cDel.open,
                title: 'Delete category?',
                detail: cDel.target ? ('"' + (cDel.target.name || '(unnamed)') + '"') : null,
                message: 'This category will be removed. If it has products, you will be asked to confirm before products are unlinked.',
                confirmLabel: 'Delete',
                busy: cDel.busy,
                onConfirm: delConfirm,
                onCancel: function(){ setCDel({ open:false, busy:false, target:null }); },
                dark: dark, cardBg: cardBg, borderColor: borderColor, textColor: textColor, mutedColor: mutedColor,
            }),

            h(ConfirmModal, {
                open: cDelForce.open,
                title: 'Category has ' + cDelForce.count + ' product' + (cDelForce.count === 1 ? '' : 's'),
                detail: cDelForce.target ? ('"' + (cDelForce.target.name || '(unnamed)') + '"') : null,
                message: cDelForce.count === 1
                    ? 'The 1 product in this category will be unlinked but not deleted. Continue?'
                    : ('The ' + cDelForce.count + ' products in this category will be unlinked but not deleted. Continue?'),
                confirmLabel: 'Unlink and delete',
                busy: cDelForce.busy,
                onConfirm: delForceConfirm,
                onCancel: function(){ setCDelForce({ open:false, busy:false, target:null, count:0 }); },
                dark: dark, cardBg: cardBg, borderColor: borderColor, textColor: textColor, mutedColor: mutedColor,
            }),

            h(ConfirmModal, {
                open: cBulkDel.open,
                title: 'Delete ' + cBulkDel.ids.length + ' categor' + (cBulkDel.ids.length === 1 ? 'y' : 'ies') + '?',
                message: 'Any products in the selected categor' + (cBulkDel.ids.length === 1 ? 'y' : 'ies') + ' will be unlinked but not deleted.',
                confirmLabel: 'Delete ' + cBulkDel.ids.length,
                busy: cBulkDel.busy,
                onConfirm: delSelectedConfirm,
                onCancel: function(){ setCBulkDel({ open:false, busy:false, ids:[] }); },
                dark: dark, cardBg: cardBg, borderColor: borderColor, textColor: textColor, mutedColor: mutedColor,
            })
        );
    }
    /* ── END_NEW_CATEGORIES ── */
    /* ── ORPHANED_OLD_CATEGORIES_START
        var byParent = {};
        flat.forEach(function(c) {
            var pid = parseInt(c.parent_id) || 0;
            if (!byParent[pid]) byParent[pid] = [];
            byParent[pid].push(c);
        });
        var result = [];
        function walk(pid, depth) {
            var children = byParent[pid] || [];
            children.forEach(function(c) {
                c._depth = depth;
                result.push(c);
                walk(c.id || c.category_id, depth + 1);
            });
        }
        walk(0, 0);
        // Append any orphans that weren't reached (safety net)
        if (result.length < flat.length) {
            var seen = new Set(result.map(function(c){ return c.id || c.category_id; }));
            flat.forEach(function(c) { if (!seen.has(c.id || c.category_id)) { c._depth = 0; result.push(c); } });
        }
        return result;
    }

    function CategoriesPage({ subId, dark, cardBg, textColor, mutedColor, borderColor, tableHeaderBg }) {
        if (subId === 'bulk-add') return h(CategoriesBulkAddPage, { dark:dark, cardBg:cardBg, textColor:textColor, mutedColor:mutedColor, borderColor:borderColor });
        var _ct = useState([]),       cats = _ct[0],      setCats = _ct[1];
        var _ld = useState(true),     loading = _ld[0],   setLoading = _ld[1];
        var _sl = useState(null),     selected = _sl[0],  setSelected = _sl[1];
        var _fm = useState({ name:'', parent_id:'0', description:'', status:'active', meta_title:'', meta_description:'', slug:'', position:'0', image_id:null, image_url:'' });
        var form = _fm[0], setForm = _fm[1];
        var _sv = useState(false),    saving = _sv[0],    setSaving = _sv[1];
        var _mg = useState(''),       msg = _mg[0],       setMsg = _mg[1];
        var _nw = useState(false),    isNew = _nw[0],     setIsNew = _nw[1];
        var _sr = useState(''),       search = _sr[0],    setSearch = _sr[1];
        var _ss = useState(new Set()),selSet = _ss[0],    setSelSet = _ss[1];
        var _pg = useState(1),        page = _pg[0],      setPage = _pg[1];
        var _ex = useState(''),       exportUrl = _ex[0], setExportUrl = _ex[1];

        var fs = fieldSty(dark, borderColor);
        var labelSty = { fontSize:12, fontWeight:500, display:'block', marginBottom:S.xs, color:mutedColor };
        var PAGE_SIZE = TC_PER_PAGE;

        function load() {
            setLoading(true);
            apiFetch('/categories?per_page=500&status=').then(function(d) {
                var flat = Array.isArray(d) ? d : (d.categories || []);
                setCats(treeSortCats(flat));
                setLoading(false);
            }).catch(function(){ setLoading(false); });
        }
        useEffect(load, []);

        // Stats
        var stats = { total:0, products:0, active:0, hidden:0, disabled:0 };
        cats.forEach(function(c) {
            stats.total++;
            stats.products += (c.product_count || 0);
            var s = (c.status||'').toLowerCase();
            if (s === 'active') stats.active++;
            else if (s === 'hidden') stats.hidden++;
            else stats.disabled++;
        });

        // Search filter
        var filtered = cats;
        if (search) {
            var q = search.toLowerCase();
            filtered = cats.filter(function(c){ return (c.name||'').toLowerCase().indexOf(q) >= 0; });
        }

        var totalPages = Math.max(1, Math.ceil(filtered.length / PAGE_SIZE));
        var paged = filtered.slice((page-1)*PAGE_SIZE, page*PAGE_SIZE);
        var pStart = filtered.length === 0 ? 0 : (page-1)*PAGE_SIZE + 1;
        var pEnd   = Math.min(page*PAGE_SIZE, filtered.length);

        // Selection
        var allChecked = paged.length > 0 && paged.every(function(c){ return selSet.has(c.category_id||c.id); });
        function toggleAll(e) {
            if (e.target.checked) { setSelSet(new Set(paged.map(function(c){ return c.category_id||c.id; }))); }
            else { setSelSet(new Set()); }
        }
        function toggleOne(cid) {
            setSelSet(function(prev){ var n=new Set(prev); if(n.has(cid)) n.delete(cid); else n.add(cid); return n; });
        }

        function selectCat(cat) {
            setIsNew(false);
            setSelected(cat);
            setForm({
                name:             cat.name || '',
                parent_id:        String(cat.parent_id || '0'),
                description:      cat.description || '',
                status:           cat.status || 'active',
                meta_title:       cat.meta_title || '',
                meta_description: cat.meta_description || cat.meta_desc || '',
                slug:             cat.slug || '',
                position:         String(cat.position || '0'),
                image_id:         cat.image_id ? Number(cat.image_id) : null,
                image_url:        cat.image_url || '',
            });
        }
        function addNew() {
            setIsNew(true); setSelected(null);
            setForm({ name:'', parent_id:'0', description:'', status:'active', meta_title:'', meta_description:'', slug:'', position:'0', image_id:null, image_url:'' });
        }
        function setField(k, v) { setForm(function(prev){ return Object.assign({}, prev, { [k]: v }); }); }
        function handleNameChange(v) {
            setField('name', v);
            if (!form.slug || isNew) {
                setField('slug', v.toLowerCase().replace(/[^a-z0-9]+/g,'-').replace(/^-+|-+$/g,''));
            }
        }
        function save() {
            if (!form.name.trim()) { setMsg('Category name is required.'); return; }
            setSaving(true); setMsg('');
            var body = {
                parent_id:   parseInt(form.parent_id) || 0,
                status:      form.status,
                slug:        form.slug,
                position:    parseInt(form.position) || 0,
                image_id:    form.image_id || 0,
                translation: { name: form.name, description: form.description, meta_title: form.meta_title, meta_desc: form.meta_description },
            };
            var catId = selected ? (selected.category_id || selected.id) : null;
            var req = (isNew || !catId)
                ? apiFetch('/categories',         { method:'POST', body: body })
                : apiFetch('/categories/' + catId, { method:'PUT',  body: body });
            req.then(function(d) {
                setSaving(false); setMsg('Saved.');
                load();
                if (d.category_id || d.id) { setSelected(Object.assign({}, form, d)); setIsNew(false); }
            }).catch(function(){ setSaving(false); setMsg('Save failed.'); });
        }
        function del(cat) {
            var catId = cat.category_id || cat.id;
            if (!confirm('Delete "' + (cat.name||'(unnamed)') + '"? This cannot be undone.')) return;
            // Optimistically remove from local state for instant feedback.
            setCats(function(prev){ return prev.filter(function(c){ return (c.category_id||c.id) !== catId; }); });
            if (selected && (selected.category_id || selected.id) === catId) { setSelected(null); setIsNew(false); }
            apiFetch('/categories/' + catId, { method:'DELETE' }).then(function() {
                load();
            }).catch(function(err){
                if (err && err.status === 409 && err.data && err.data.data && err.data.data.product_count) {
                    var cnt = err.data.data.product_count;
                    if (confirm('This category has ' + cnt + ' product(s). Products will be unlinked but not deleted. Continue?')) {
                        apiFetch('/categories/'+catId+'?force=true', { method:'DELETE' }).then(function(){ load(); }).catch(function(){ load(); });
                        return;
                    }
                }
                load();
            });
        }
        function delSelected() {
            if (!selSet.size || !confirm('Delete ' + selSet.size + ' category(ies)? Products will be unlinked but not deleted.')) return;
            var ids = Array.from(selSet);
            // Optimistically remove from local state.
            setCats(function(prev){ return prev.filter(function(c){ return !ids.includes(c.category_id||c.id); }); });
            setSelSet(new Set());
            Promise.all(ids.map(function(id){ return apiFetch('/categories/'+id+'?force=true', { method:'DELETE' }).catch(function(){}); }))
                .then(function(){ load(); }).catch(function(){ load(); });
        }

        function exportCats() {
            setExportUrl('');
            fetch(apiUrl('/categories/export'), { headers: { 'X-WP-Nonce': Config.nonce || '' } })
                .then(function(r){ return r.json(); })
                .then(function(d){ if (d.url) setExportUrl(d.url); });
        }

        // Status badge
        function statusBadge(st) {
            var s = (st||'').toLowerCase();
            if (s === 'active') return { bg:dark?'rgba(22,163,74,.1)':'#F0FDF4', color:TC.success, border:TC.success+'30', label:'Active' };
            if (s === 'hidden') return { bg:dark?'rgba(217,119,6,.1)':'#FFFBEB', color:TC.amber, border:TC.amber+'30', label:'Hidden' };
            return { bg:dark?'rgba(220,38,38,.1)':'#FEF2F2', color:TC.error, border:TC.error+'30', label:'Disabled' };
        }

        var showForm = selected || isNew;

        return h('main', { style:{ flex:1, overflow:'auto', padding:S.xxl } },
            h('div', { style:{ display:'grid', gridTemplateColumns: showForm ? '1fr 380px' : '1fr 260px', gap:S.lg, alignItems:'start' } },

                // ═══ LEFT — Category table card ═══
                h('div', { style:{ background:cardBg, borderRadius:12, border:'1px solid '+borderColor, boxShadow:cardShadow(dark) } },

                    // Card header
                    h('div', { style:{ display:'flex', justifyContent:'space-between', alignItems:'center', padding:'16px 24px', borderBottom:'1px solid '+borderColor, flexWrap:'wrap', gap:12 } },
                        h('div', null,
                            h('div', { style:{ fontSize:16, fontWeight:600, marginBottom:2 } }, 'Categories'),
                            h('div', { style:{ fontSize:13, color:mutedColor } }, stats.total + ' categories')
                        ),
                        h('div', { style:{ display:'flex', gap:8, alignItems:'center' } },
                            // Search
                            h('div', { style:{ position:'relative', width:220 } },
                                h('span', { style:{ position:'absolute', left:10, top:'50%', transform:'translateY(-50%)', color:mutedColor, display:'flex' } }, icons.search),
                                h('input', { value:search, onChange:function(e){ setSearch(e.target.value); setPage(1); },
                                    placeholder:'Search categories...', style:{ width:'100%', padding:'7px 10px 7px 32px', borderRadius:8, border:'1px solid '+borderColor, background:dark?'#262626':'#fff', color:textColor, fontSize:13, outline:'none' } })
                            ),
                            // Export CSV
                            h('button', { onClick:exportCats,
                                style:{ display:'flex', alignItems:'center', gap:6, padding:'7px 14px', borderRadius:8, border:'1px solid '+borderColor, background:'transparent', color:mutedColor, fontSize:13, fontWeight:500, cursor:'pointer' } },
                                exportUrl ? h('a', { href:exportUrl, style:{ color:TC.accent, textDecoration:'none', fontSize:13 } }, '\u2B07 Download CSV') : '\u2B07 Export CSV'),
                            // Bulk add
                            h('button', { onClick:function(){ navigate('categories/bulk-add'); },
                                style:{ display:'flex', alignItems:'center', gap:6, padding:'7px 14px', borderRadius:8, border:'1px solid '+borderColor, background:'transparent', color:textColor, fontSize:13, fontWeight:500, cursor:'pointer' } },
                                icons.plus, ' Bulk Add'),
                            // Add category
                            h('button', { onClick:addNew,
                                style:{ display:'flex', alignItems:'center', gap:6, padding:'7px 14px', borderRadius:8, border:'none', background:TC.primary, color:TC.primaryFg, fontSize:13, fontWeight:500, cursor:'pointer' } },
                                icons.plus, ' Add Category')
                        )
                    ),

                    // Card content
                    h('div', { style:{ padding:'0' } },

                        // Selection bar
                        selSet.size > 0 && h('div', { style:{ display:'flex', justifyContent:'space-between', alignItems:'center', padding:'10px 24px', borderBottom:'1px solid '+borderColor, background:dark?'rgba(255,255,255,.02)':'#FAFAFA' } },
                            h('span', { style:{ fontSize:13, color:mutedColor } }, selSet.size + ' selected'),
                            h('button', { onClick:delSelected,
                                style:{ padding:'5px 12px', borderRadius:7, border:'none', background:TC.error, color:'#fff', fontSize:12, fontWeight:500, cursor:'pointer' } }, 'Delete Selected')
                        ),

                        // Table
                        h('div', { style:{ overflowX:'auto' } },
                            h('table', { style:{ width:'100%', borderCollapse:'collapse', minWidth:600 } },
                                h('thead', null,
                                    h('tr', { style:{ borderBottom:'1px solid '+borderColor } },
                                        h('th', { style:{ width:40, padding:'10px 0 10px 24px', textAlign:'left' } },
                                            h('input', { type:'checkbox', checked:allChecked, onChange:toggleAll, style:{ cursor:'pointer' } })
                                        ),
                                        ['Pos.','Name','Products','Status',''].map(function(col) {
                                            return h('th', { key:col, style:{ padding:'10px 12px', fontSize:11, fontWeight:500, color:mutedColor, textTransform:'uppercase', letterSpacing:'.5px', textAlign:col===''?'right':'left' } }, col);
                                        })
                                    )
                                ),
                                h('tbody', null,
                                    loading && h('tr', null, h('td', { colSpan:6, style:{ textAlign:'center', padding:48, color:mutedColor, fontSize:13 } }, 'Loading…')),
                                    !loading && paged.length === 0 && h('tr', null, h('td', { colSpan:6, style:{ textAlign:'center', padding:48, color:mutedColor, fontSize:13 } }, 'No categories found')),
                                    !loading && paged.map(function(cat, i) {
                                        var cid   = cat.category_id || cat.id;
                                        var isSel = selSet.has(cid);
                                        var badge = statusBadge(cat.status);
                                        var depth = cat._depth || cat.depth || 0;
                                        var isEditing = selected && (selected.category_id||selected.id) === cid;

                                        return h('tr', { key:cid,
                                            style:{ borderBottom: i < paged.length-1 ? '1px solid '+borderColor : 'none',
                                                    background: isEditing ? (dark?'rgba(37,99,235,.06)':'#EFF6FF') : isSel ? (dark?'rgba(37,99,235,.04)':'#F5F9FF') : 'transparent',
                                                    transition:'background .1s' },
                                            onMouseEnter:function(e){ if(!isSel&&!isEditing) e.currentTarget.style.background = dark?'rgba(255,255,255,.02)':'#F9FAFB'; },
                                            onMouseLeave:function(e){ if(!isSel&&!isEditing) e.currentTarget.style.background = 'transparent'; }
                                        },
                                            // Checkbox
                                            h('td', { style:{ padding:'12px 0 12px 24px', verticalAlign:'middle' } },
                                                h('input', { type:'checkbox', checked:isSel, onChange:function(){ toggleOne(cid); }, style:{ cursor:'pointer' }, onClick:function(e){ e.stopPropagation(); } })
                                            ),
                                            // Position
                                            h('td', { style:{ padding:'12px', fontSize:13, color:mutedColor, verticalAlign:'middle', width:60 } }, String(cat.position || 0)),
                                            // Name (tree-indented)
                                            h('td', { style:{ padding:'12px', verticalAlign:'middle' } },
                                                h('div', { style:{ display:'flex', alignItems:'center', gap:6, paddingLeft:(depth*18)+'px' } },
                                                    depth > 0 && h('span', { style:{ color:dark?'#525252':'#D4D4D4', fontSize:14, flexShrink:0 } }, '└'),
                                                    h('span', { onClick:function(){ selectCat(cat); },
                                                        style:{ fontSize:13, fontWeight:isEditing?600:500, cursor:'pointer', color:isEditing?TC.accent:textColor } }, cat.name || '—')
                                                )
                                            ),
                                            // Products count — clickable to filter products by this category
                                            h('td', { style:{ padding:'12px', verticalAlign:'middle' } },
                                                h('span', { onClick:function(){ if(parseInt(cat.product_count)){ window.__tc_nav_cat = cid; navigate('products'); } },
                                                    style:{ fontSize:12, fontWeight:500, padding:'2px 10px', borderRadius:99, background:dark?'rgba(255,255,255,.05)':'#F5F5F5', color: parseInt(cat.product_count) ? TC.accent : mutedColor, display:'inline-block', cursor: parseInt(cat.product_count) ? 'pointer' : 'default' } }, String(cat.product_count || 0))
                                            ),
                                            // Status badge
                                            h('td', { style:{ padding:'12px', verticalAlign:'middle' } },
                                                h('span', { style:{ fontSize:11, fontWeight:500, padding:'2px 8px', borderRadius:99, background:badge.bg, color:badge.color, border:'1px solid '+badge.border, display:'inline-block', whiteSpace:'nowrap' } }, badge.label)
                                            ),
                                            // Actions
                                            h('td', { style:{ padding:'12px', textAlign:'right', verticalAlign:'middle' } },
                                                h(RowActionMenu, {
                                                    pid:cid,
                                                    onView:function(){ selectCat(cat); },
                                                    onEdit:function(){ selectCat(cat); },
                                                    onDelete:function(){ del(cat); },
                                                    dark:dark, cardBg:cardBg, borderColor:borderColor, textColor:textColor, mutedColor:mutedColor
                                                })
                                            )
                                        );
                                    })
                                )
                            )
                        ),

                        // Pagination
                        h('div', { style:{ display:'flex', justifyContent:'space-between', alignItems:'center', padding:'12px 24px', borderTop:'1px solid '+borderColor } },
                            h('span', { style:{ fontSize:13, color:mutedColor } },
                                filtered.length === 0 ? 'Showing 0 of 0' : 'Showing ' + pStart + ' - ' + pEnd + ' of ' + filtered.length
                            ),
                            h('div', { style:{ display:'flex', alignItems:'center', gap:8 } },
                                h('button', { onClick:function(){ setPage(function(p){ return Math.max(1,p-1); }); }, disabled:page===1,
                                    style:{ padding:'5px 12px', borderRadius:7, border:'1px solid '+borderColor, background:'transparent', color:page===1?mutedColor:textColor, fontSize:12, fontWeight:500, cursor:page===1?'default':'pointer', opacity:page===1?.5:1 } }, 'Prev'),
                                h('span', { style:{ fontSize:13, color:mutedColor } }, 'Page ' + page + ' of ' + totalPages),
                                h('button', { onClick:function(){ setPage(function(p){ return Math.min(totalPages,p+1); }); }, disabled:page===totalPages,
                                    style:{ padding:'5px 12px', borderRadius:7, border:'1px solid '+borderColor, background:'transparent', color:page===totalPages?mutedColor:textColor, fontSize:12, fontWeight:500, cursor:page===totalPages?'default':'pointer', opacity:page===totalPages?.5:1 } }, 'Next')
                            )
                        )
                    )
                ),

                // ═══ RIGHT — Stats or Edit form ═══
                showForm
                    // ── Edit form panel ──
                    ? h('div', { style:{ background:cardBg, borderRadius:12, border:'1px solid '+borderColor, boxShadow:cardShadow(dark), padding:S.xl, position:'sticky', top:S.xxl } },
                        h('div', { style:{ display:'flex', justifyContent:'space-between', alignItems:'center', marginBottom:S.lg } },
                            h('div', { style:{ fontSize:15, fontWeight:600 } }, isNew ? 'New Category' : 'Edit Category'),
                            msg && h('span', { style:{ fontSize:12, color:msg==='Save failed.'?TC.error:TC.success } }, msg)
                        ),
                        // Name
                        h('div', { style:{ marginBottom:S.md } },
                            h('label', { style:labelSty }, 'Category Name *'),
                            h('input', { value:form.name, onChange:function(e){ handleNameChange(e.target.value); }, placeholder:'e.g. Electronics', style:fs })
                        ),
                        // Parent
                        h('div', { style:{ marginBottom:S.md } },
                            h('label', { style:labelSty }, 'Parent Category'),
                            h('select', { value:form.parent_id, onChange:function(e){ setField('parent_id', e.target.value); }, style:fs },
                                h('option', { value:'0' }, '— Root (top level) —'),
                                cats.filter(function(c){ return !selected || (c.category_id||c.id) !== (selected.category_id||selected.id); }).map(function(c) {
                                    return h('option', { key:c.category_id||c.id, value:String(c.category_id||c.id) },
                                        '\u00A0'.repeat((c._depth||c.depth||0)*3) + (c.name||'—'));
                                })
                            )
                        ),
                        // Description
                        h('div', { style:{ marginBottom:S.md } },
                            h('label', { style:labelSty }, 'Description'),
                            h('textarea', { value:form.description, onChange:function(e){ setField('description', e.target.value); }, rows:3,
                                style:Object.assign({}, fs, { resize:'vertical' }) })
                        ),
                        // Category Image
                        h('div', { style:{ marginBottom:S.md } },
                            h('label', { style:labelSty }, 'Category Image'),
                            h('div', { style:{ fontSize:11, color:mutedColor, marginBottom:6 } }, 'Recommended: 400 × 400 px square image. Shown on homepage category carousel and category pages.'),
                            form.image_url
                                ? h('div', { style:{ position:'relative', display:'inline-block', marginBottom:8 } },
                                    h('img', { src:form.image_url, alt:'Category image',
                                        style:{ width:120, height:120, objectFit:'cover', borderRadius:10, border:'1px solid '+borderColor } }),
                                    h('button', {
                                        onClick:function(){ setField('image_id', null); setField('image_url', ''); },
                                        title:'Remove image',
                                        style:{ position:'absolute', top:-6, right:-6, width:22, height:22, borderRadius:'50%', border:'none',
                                            background:'#DC2626', color:'#fff', fontSize:12, cursor:'pointer', display:'flex', alignItems:'center', justifyContent:'center', padding:0 }
                                    }, '×')
                                  )
                                : null,
                            h('button', {
                                type:'button',
                                onClick:function() {
                                    if (!window.wp || !wp.media) { alert('WordPress media library not available.'); return; }
                                    var frame = wp.media({
                                        title: 'Select Category Image',
                                        button: { text: 'Use this image' },
                                        multiple: false,
                                        library: { type: 'image' }
                                    });
                                    frame.on('select', function() {
                                        var attachment = frame.state().get('selection').first().toJSON();
                                        setField('image_id', attachment.id);
                                        var url = (attachment.sizes && attachment.sizes.medium) ? attachment.sizes.medium.url : attachment.url;
                                        setField('image_url', url);
                                    });
                                    frame.open();
                                },
                                style:{ padding:'6px 14px', borderRadius:8, border:'1px solid '+borderColor, background:dark?'#2a2a2a':'#F5F5F5',
                                    color:textColor, fontSize:12, fontWeight:500, cursor:'pointer', display:'inline-flex', alignItems:'center', gap:6 }
                            }, '\uD83D\uDDBC ', form.image_url ? 'Change Image' : 'Upload Image')
                        ),
                        // Status + Position row
                        h('div', { style:{ display:'grid', gridTemplateColumns:'1fr 1fr', gap:S.md, marginBottom:S.md } },
                            h('div', null,
                                h('label', { style:labelSty }, 'Status'),
                                h('select', { value:form.status, onChange:function(e){ setField('status', e.target.value); }, style:fs },
                                    h('option',{value:'active'},'Active'), h('option',{value:'disabled'},'Disabled'), h('option',{value:'hidden'},'Hidden')
                                )
                            ),
                            h('div', null,
                                h('label', { style:labelSty }, 'Position'),
                                h('input', { type:'number', value:form.position, onChange:function(e){ setField('position', e.target.value); }, style:fs })
                            )
                        ),
                        // Slug
                        h('div', { style:{ marginBottom:S.md } },
                            h('label', { style:labelSty }, 'URL Slug'),
                            h('input', { value:form.slug, onChange:function(e){ setField('slug', e.target.value); }, placeholder:'url-slug', style:fs })
                        ),
                        // Meta
                        h('div', { style:{ marginBottom:S.md } },
                            h('label', { style:labelSty }, 'Meta Title'),
                            h('input', { value:form.meta_title, onChange:function(e){ setField('meta_title', e.target.value); }, style:fs })
                        ),
                        h('div', { style:{ marginBottom:S.lg } },
                            h('label', { style:labelSty }, 'Meta Description'),
                            h('textarea', { value:form.meta_description, onChange:function(e){ setField('meta_description', e.target.value); }, rows:2, style:Object.assign({}, fs, { resize:'vertical' }) })
                        ),
                        // Actions
                        h('div', { style:{ display:'flex', gap:S.sm, justifyContent:'flex-end' } },
                            h('button', { onClick:function(){ setSelected(null); setIsNew(false); setMsg(''); },
                                style:{ padding:'7px 16px', borderRadius:8, border:'1px solid '+borderColor, background:'transparent', color:textColor, fontSize:13, fontWeight:500, cursor:'pointer' } }, 'Cancel'),
                            h('button', { onClick:save, disabled:saving,
                                style:{ padding:'7px 16px', borderRadius:8, border:'none', background:TC.primary, color:TC.primaryFg, fontSize:13, fontWeight:500, cursor:'pointer', opacity:saving?.7:1 } }, saving ? 'Saving…' : 'Save Category')
                        )
                    )
                    // ── Stats panel ──
                    : h('div', { style:{ background:cardBg, borderRadius:12, border:'1px solid '+borderColor, boxShadow:cardShadow(dark), padding:S.xl, position:'sticky', top:S.xxl } },
                        h('div', { style:{ fontSize:15, fontWeight:600, marginBottom:S.lg } }, 'Total'),
                        [
                            ['Categories',          stats.total],
                            ['Products',            stats.products],
                            ['Active categories',   stats.active],
                            ['Hidden categories',   stats.hidden],
                            ['Disabled categories',  stats.disabled],
                        ].map(function(row) {
                            return h('div', { key:row[0], style:{ display:'flex', justifyContent:'space-between', alignItems:'center', padding:'8px 0', borderBottom:'1px solid '+borderColor, fontSize:13 } },
                                h('span', { style:{ color:mutedColor } }, row[0]),
                                h('span', { style:{ fontWeight:500 } }, String(row[1]))
                            );
                        })
                    )
            )
        );
    ORPHANED_OLD_CATEGORIES_END ── */

    /* ── FEATURES / ATTRIBUTES PAGE ────────────────────────────────── */
    function FeaturesPage({ dark, cardBg, textColor, mutedColor, borderColor, tableHeaderBg }) {
        const [features, setFeatures] = useState([]);
        const [loading,  setLoading]  = useState(true);
        const [search,   setSearch]   = useState('');
        const [fGroup,   setFGroup]   = useState('');
        const [groups,   setGroups]   = useState([]);

        // Modal state
        const [showModal,  setShowModal]  = useState(false);
        const [editing,    setEditing]    = useState(null);  // null = new, obj = editing
        const [modalTab,   setModalTab]   = useState('general');
        const [saving,     setSaving]     = useState(false);
        const [form, setForm] = useState({
            name:'', feature_type:'S', filter_type:'checkbox', purpose:'filter', feature_code:'',
            parent_id:0, filterable:true, compareable:false, show_on_pdp:true, sort_mode:'manual', status:'active', feature_description:''
        });

        // Variants sub-state (for the Variants tab)
        const [variants,     setVariants]     = useState([]);
        const [varLoading,   setVarLoading]   = useState(false);
        const [newVarName,   setNewVarName]   = useState('');
    const [newVarHex,    setNewVarHex]    = useState('');
        // S466 (KI-110): one-line notice after a delete merged assignments into a same-name survivor.
        const [varNotice,    setVarNotice]    = useState('');

        // Categories sub-state (for the Categories tab)
        const [featCats,     setFeatCats]     = useState([]);   // assigned category IDs
        const [allCats,      setAllCats]      = useState([]);   // flat list from API
        const [catLoading,   setCatLoading]   = useState(false);
        const [catSearch,    setCatSearch]    = useState('');
        const [catExpanded,  setCatExpanded]  = useState(new Set());

        const fs = fieldSty(dark, borderColor);
        const labelSty = { fontSize:12, fontWeight:500, display:'block', marginBottom:S.xs, color:mutedColor };

        var TYPE_LABELS = {
            S:'Dropdown', M:'Multiple checkbox', C:'Checkbox',
            T:'Text or number', O:'Number', D:'Date', E:'Images / Brand', I:'Product Images', G:'Group'
        };
        var PURPOSE_OPTIONS = [
            { v:'filter',           l:'Storefront filters',   desc:'Appears in category page filters so customers can search by this attribute.' },
            { v:'variation_split',  l:'Variations (separate products)', desc:'Each combination (e.g. Red / Large) becomes its own product with a unique SKU, price, and stock.' },
            { v:'variation_shared', l:'Variations (shared product)',    desc:'One product with selectable options (e.g. pick Color, pick Size) that share the same SKU.' },
            { v:'brand',            l:'Brand / Manufacturer',  desc:'Displayed as the product brand on the storefront.' },
            { v:'info',             l:'Additional specs',      desc:'Shown in the product specifications table.' },
        ];
        // Display type + list display options depend on the selected purpose
        var STYLE_BY_PURPOSE = {
            filter:           [{ v:'S', l:'Dropdown' }, { v:'E', l:'Color Swatches' }, { v:'M', l:'Multiple checkbox' }, { v:'C', l:'Checkbox' }, { v:'T', l:'Text or number' }],
            variation_split:  [{ v:'E', l:'Color Swatches' }, { v:'I', l:'Product Images' }, { v:'T', l:'Size Pills' }, { v:'S', l:'Dropdown' }],
            variation_shared: [{ v:'E', l:'Color Swatches' }, { v:'I', l:'Product Images' }, { v:'T', l:'Size Pills' }, { v:'S', l:'Dropdown' }],
            brand:            [{ v:'E', l:'Brand' }],
            info:             [{ v:'T', l:'Text or number' }],
        };
        var FILTER_BY_PURPOSE = {
            filter:           [{ v:'checkbox', l:'Checkbox' }, { v:'slider', l:'Number slider' }, { v:'color', l:'Color swatches' }, { v:'size_pills', l:'Size pills' }],
            variation_split:  [{ v:'checkbox', l:'Checkbox' }, { v:'slider', l:'Number slider' }, { v:'color', l:'Color swatches' }, { v:'size_pills', l:'Size pills' }],
            variation_shared: [{ v:'checkbox', l:'Checkbox' }, { v:'slider', l:'Number slider' }, { v:'color', l:'Color swatches' }, { v:'size_pills', l:'Size pills' }],
            brand:            [{ v:'checkbox', l:'Checkbox' }],
            info:             [],  // no filter for additional info
        };
        var STYLE_DEFAULTS  = { filter:'T', variation_split:'E', variation_shared:'E', brand:'E', info:'T' };
        var FILTER_DEFAULTS = { filter:'checkbox', variation_split:'checkbox', variation_shared:'checkbox', brand:'checkbox', info:'none' };

        function getStyleOptions()  { return STYLE_BY_PURPOSE[form.purpose]  || []; }
        function getFilterOptions() { return FILTER_BY_PURPOSE[form.purpose] || []; }
        var showFilterType = form.purpose !== 'info' && form.purpose !== 'brand';

        // ── Data loading ──
        function load() {
            setLoading(true);
            var q = new URLSearchParams();
            if (search) q.set('search', search);
            if (fGroup) q.set('group', fGroup);
            apiFetch('/product-features?' + q.toString()).then(function(d) {
                setFeatures(Array.isArray(d) ? d : []);
                setLoading(false);
            }).catch(function(){ setLoading(false); });
        }
        useEffect(load, [search, fGroup]);

        useEffect(function() {
            apiFetch('/product-features/groups').then(function(d) {
                setGroups(Array.isArray(d) ? d : []);
            }).catch(function(){});
        }, []);

        // ── Modal open/close ──
        function openNew() {
            setEditing(null);
            setForm({ name:'', feature_type:'S', filter_type:'checkbox', purpose:'filter', feature_code:'',
                       parent_id:0, filterable:true, compareable:false, show_on_pdp:true, sort_mode:'manual', status:'active', feature_description:'' });
            setVariants([]); setFeatCats([]); setModalTab('general'); setShowModal(true);
        }
        function openEdit(f) {
            setEditing(f);
            setForm({
                name: f.name||'', feature_type: f.feature_type||f.type||'T',
                filter_type: f.filter_type||'checkbox',
                purpose: f.purpose||'filter', feature_code: f.feature_code||'',
                parent_id: parseInt(f.parent_id)||0, filterable: !!parseInt(f.filterable),
                compareable: !!parseInt(f.compareable),
                show_on_pdp: (f.show_on_pdp === undefined || f.show_on_pdp === null) ? true : !!parseInt(f.show_on_pdp),
                sort_mode: f.sort_mode === 'alpha' ? 'alpha' : 'manual',
                global_prices: parseInt(f.global_prices) ? 1 : 0,
                status: f.status === 'disabled' ? 'disabled' : 'active',
                feature_description: f.feature_description||''
            });
            setModalTab('general'); setShowModal(true); setVarNotice('');
            // Load variants + categories
            var fid = f.feature_id || f.id;
            setVarLoading(true);
            apiFetch('/product-features/' + fid + '/variants').then(function(d) {
                setVariants(Array.isArray(d) ? d : []); setVarLoading(false);
            }).catch(function(){ setVarLoading(false); });
            setCatLoading(true);
            apiFetch('/product-features/' + fid + '/categories').then(function(d) {
                var ids = (Array.isArray(d) ? d : []).map(function(c){ return Number(c.category_id); });
                setFeatCats(ids); setCatLoading(false);
            }).catch(function(){ setCatLoading(false); });
        }
        function closeModal() { setShowModal(false); setEditing(null); }

        // Load category tree for picker (once)
        useEffect(function() {
            apiFetch('/categories?format=flat&per_page=500').then(function(d) {
                setAllCats(Array.isArray(d) ? d : (d.categories||[]));
            }).catch(function(){});
        }, []);

        // ── Save ──
        function save() {
            if (!form.name.trim()) return;
            setSaving(true);
            var fid = editing ? (editing.feature_id || editing.id) : null;
            var req = fid
                ? apiFetch('/product-features/' + fid, { method:'PUT',  body:form })
                : apiFetch('/product-features',        { method:'POST', body:form });
            req.then(function(d) {
                var savedId = d.feature_id || d.id || fid;
                // Save categories if we have a saved ID
                if (savedId) {
                    apiFetch('/product-features/' + savedId + '/categories', {
                        method:'POST', body:{ category_ids: featCats }
                    }).catch(function(){});
                }
                setSaving(false); setShowModal(false); load();
                // Refresh groups if we saved a group type
                if (form.feature_type === 'G') {
                    apiFetch('/product-features/groups').then(function(g) { setGroups(Array.isArray(g)?g:[]); }).catch(function(){});
                }
            }).catch(function(){ setSaving(false); });
        }

        function del(f) {
            if (!confirm('Delete attribute "' + f.name + '"? This removes it from all products.')) return;
            apiFetch('/product-features/' + (f.feature_id||f.id), { method:'DELETE' }).then(load);
        }

        // ── Variant helpers ──
        // Hex normaliser (roadmap #11 side-ask): accept "ff0000", "#ff0000", "f00" —
        // returns canonical "#FF0000" or '' when invalid. Server applies the same rule.
        function normHex(raw) {
            var s = String(raw||'').trim().replace(/^#/, '').toUpperCase();
            if (!s) return '';
            return /^[0-9A-F]{3}$|^[0-9A-F]{6}$/.test(s) ? '#'+s : '';
        }
        function nextVarPosition() {
            return variants.reduce(function(m, v){ var p = parseInt(v.position); return isNaN(p) ? m : Math.max(m, p+1); }, 0);
        }
        function addVariant() {
            if (!newVarName.trim() || !editing) return;
            var fid = editing.feature_id || editing.id;
            var hex = normHex(newVarHex);
            var varData = { name:newVarName.trim(), position:nextVarPosition() };
            if (hex) varData.color_hex = hex;
            apiFetch('/product-features/' + fid + '/variants', {
                method:'POST', body:{ variants:[varData] }
            }).then(function(d) {
                setVariants(Array.isArray(d) ? d : []); setNewVarName(''); setNewVarHex('');
            }).catch(function(){});
        }
        // One-click "Sort A–Z now" — rewrites `position` on the server (the only
        // operation that does). In Alphabetical mode positions are never touched,
        // so switching back to Manual restores the hand-made order.
        function sortVariantsAlpha() {
            if (!editing) return;
            var fid = editing.feature_id || editing.id;
            setVarLoading(true);
            apiFetch('/product-features/' + fid + '/variants/sort-alpha', { method:'POST', body:{} }).then(function(d) {
                setVariants(Array.isArray(d) ? d : []); setVarLoading(false);
            }).catch(function(){ setVarLoading(false); });
        }
        // S466 (KI-110): the server merges a deleted DUPLICATE choice into its same-name
        // survivor (product assignments, variation combos, group combos, hex carry-over).
        // Re-fetch the list afterwards so the survivor row reflects any carried hex.
        function delVariant(vid) {
            if (!editing) return;
            var fid = editing.feature_id || editing.id;
            setVarNotice('');
            apiFetch('/product-features/' + fid + '/variants/' + vid, { method:'DELETE' }).then(function(res) {
                var m = (res && res.moved) || {};
                var moved = (m.values||0) + (m.variation_links||0) + (m.group_products||0);
                if (res && res.merged_into) {
                    setVarNotice('Merged into the remaining \u201C' + (res.merged_name||'') + '\u201D \u2014 ' + moved + ' product assignment' + (moved===1?'':'s') + ' moved.');
                } else if (m.dropped_values) {
                    setVarNotice('Deleted \u2014 ' + m.dropped_values + ' product assignment' + (m.dropped_values===1?'':'s') + ' removed (no same-name choice to merge into).');
                }
                return apiFetch('/product-features/' + fid + '/variants');
            }).then(function(d) {
                if (Array.isArray(d)) setVariants(d);
            }).catch(function(){});
        }
        function updateVariantField(vid, field, value) {
            setVariants(function(prev) {
                return prev.map(function(v) {
                    if (Number(v.id) === Number(vid)) {
                        var copy = Object.assign({}, v);
                        copy[field] = value;
                        copy._dirty = true;
                        return copy;
                    }
                    return v;
                });
            });
        }
        function saveAllVariants() {
            if (!editing) return;
            var fid = editing.feature_id || editing.id;
            // Send each row's OWN position (not its list index): in Alphabetical
            // mode the list is name-ordered, and index would silently overwrite
            // the manual order. Server normalises color_hex (with/without '#').
            var payload = variants.map(function(v, idx) {
                var p = parseInt(v.position);
                var d = { id: Number(v.id), name: v.name || '', position: isNaN(p) ? idx : p };
                d.color_hex = normHex(v.color_hex) || '';
                // S478: site-wide price change (blank clears)
                d.price_modifier = (v.price_modifier === '' || v.price_modifier == null || isNaN(parseFloat(v.price_modifier))) ? null : parseFloat(v.price_modifier);
                return d;
            });
            apiFetch('/product-features/' + fid + '/variants', {
                method:'POST', body:{ variants: payload }
            }).then(function(d) {
                setVariants(Array.isArray(d) ? d : []);
            }).catch(function(){});
        }
        // S478 (Sean): move a choice up/down in Manual mode. Swaps the two rows in
        // the list and gives every row a fresh sequential `position` (index + 1) —
        // the stored positions are often all 0 or gappy from imports, so a pure
        // swap of the two values would not stick. Marks every row dirty so
        // saveAllVariants() persists the whole order. Hidden in Alphabetical mode.
        function moveVariant(vid, dir) {
            setVariants(function(prev) {
                var i = prev.findIndex(function(v){ return Number(v.id) === Number(vid); });
                var j = i + dir;
                if (i < 0 || j < 0 || j >= prev.length) return prev;
                var next = prev.slice();
                var tmp = next[i]; next[i] = next[j]; next[j] = tmp;
                return next.map(function(v, k) {
                    return Object.assign({}, v, { position: k + 1, _dirty: true });
                });
            });
        }

        // ── Category tree helpers ──
        function catBuildTree(items, parentId) {
            parentId = parentId || 0;
            return items.filter(function(c){ return Number(c.parent_id||0) === parentId; })
                .sort(function(a,b){ return (a.position||0) - (b.position||0); })
                .map(function(c){ return { id:Number(c.id), name:c.name||'—', children:catBuildTree(items, Number(c.id)) }; });
        }
        function catToggleExpand(cid) {
            setCatExpanded(function(prev){ var s=new Set(prev); if(s.has(cid)) s.delete(cid); else s.add(cid); return s; });
        }
        function catToggle(cid) {
            cid = Number(cid);
            setFeatCats(function(prev){ return prev.includes(cid) ? prev.filter(function(c){return c!==cid;}) : prev.concat([cid]); });
        }
        function renderCatTree(nodes, depth) {
            depth = depth || 0;
            var result = [];
            nodes.forEach(function(node) {
                var hasKids = node.children && node.children.length > 0;
                var isExp = catExpanded.has(node.id);
                var isChecked = featCats.includes(node.id);
                var matchesSearch = !catSearch || (node.name||'').toLowerCase().indexOf(catSearch.toLowerCase()) >= 0;
                if (!matchesSearch && !hasKids) return;
                result.push(
                    h('div', { key:node.id, style:{ display:'flex', alignItems:'center', gap:S.sm,
                        paddingLeft:(depth*20)+S.sm, paddingTop:5, paddingBottom:5,
                        borderBottom:'1px solid '+(dark?'rgba(255,255,255,.04)':borderColor) } },
                        hasKids ? h('button', { onClick:function(){ catToggleExpand(node.id); },
                            style:{ background:'transparent', border:'none', cursor:'pointer', color:mutedColor, fontSize:12, width:18, padding:0,
                                    transform:isExp?'rotate(90deg)':'rotate(0)', transition:'transform .15s' } }, '▸')
                            : h('span', { style:{ width:18 } }),
                        h('input', { type:'checkbox', checked:isChecked, onChange:function(){ catToggle(node.id); },
                            style:{ accentColor:TC.accent, width:15, height:15 } }),
                        h('span', { style:{ fontSize:13, cursor:'pointer' }, onClick:function(){ catToggle(node.id); } }, node.name)
                    )
                );
                if (hasKids && isExp) {
                    result = result.concat(renderCatTree(node.children, depth+1));
                }
            });
            return result;
        }

        // Derived — Choices tab shows for all purposes except Additional specs
        var hasChoices = form.purpose !== 'info';

        // ── Modal tabs ──
        var MODAL_TABS = [{ v:'general', l:'General' }];
        if (hasChoices) MODAL_TABS.push({ v:'choices', l:'Choices' });
        MODAL_TABS.push({ v:'categories', l:'Categories' });
        // Auto-reset if current tab no longer exists
        if (!MODAL_TABS.some(function(t){ return t.v === modalTab; }) && modalTab !== 'general') {
            // Can't call setModalTab during render, so guard at render level
        }
        var activeModalTab = MODAL_TABS.some(function(t){ return t.v === modalTab; }) ? modalTab : 'general';

        // ── Render ──
        var cols = '28px 1.5fr 1fr 1fr 1.2fr 0.6fr 0.6fr 0.8fr';
        var dragRef = useRef(null);
        var dropRef = useRef(null);

        function handleDragStart(i) { dragRef.current = i; }
        function handleDragOver(e, i) { e.preventDefault(); dropRef.current = i; }
        function handleDrop() {
            var from = dragRef.current;
            var to = dropRef.current;
            if (from === null || to === null || from === to) return;
            var reordered = features.slice();
            var item = reordered.splice(from, 1)[0];
            reordered.splice(to, 0, item);
            setFeatures(reordered);
            // Save new order
            var ids = reordered.map(function(f) { return Number(f.feature_id || f.id); });
            apiFetch('/product-features/reorder', { method:'POST', body:{ ids:ids } }).catch(function(){});
            dragRef.current = null;
            dropRef.current = null;
        }
        return h(PageWrap, { title:'Attributes', sub:'Define filterable product properties — Color, Size, Brand, Material, and more.', mutedColor,
            action: h('div', { style:{ display:'flex', gap:S.sm } },
                h(Btn, { variant:'primary', onClick:openNew, mutedColor, borderColor }, '+ New Attribute')
            ) },

            // Search + filter bar
            h('div', { style:{ display:'flex', gap:S.md, marginBottom:S.lg, alignItems:'center' } },
                h('input', { type:'text', value:search, onChange:function(e){ setSearch(e.target.value); },
                    placeholder:'Search attributes\u2026', style:Object.assign({}, fs, { maxWidth:280, fontSize:13 }) }),
                h('select', { value:fGroup, onChange:function(e){ setFGroup(e.target.value); },
                    style:Object.assign({}, fs, { maxWidth:200, fontSize:13 }) },
                    h('option', { value:'' }, 'All Groups'),
                    h('option', { value:'0' }, 'Ungrouped'),
                    groups.map(function(g) { return h('option', { key:g.id, value:g.id }, g.name||'Group #'+g.id); })
                )
            ),

            // Table
            h(Card, { cardBg, borderColor, dark, pad:0 },
                h('div', { style:{ display:'grid', gridTemplateColumns:cols, padding:S.md+'px '+S.xl+'px', borderBottom:'1px solid '+borderColor, background:tableHeaderBg, boxShadow:tableHeaderShadow } },
                    h('span', { style:{ fontSize:10, fontWeight:500, color:mutedColor } }),
                    ['Attribute Name','Display','List Display','Purpose','Filterable','Status','Actions'].map(function(col) {
                        return h('span', { key:col, style:{ fontSize:10, fontWeight:500, color:mutedColor, textTransform:'uppercase', letterSpacing:'.5px' } }, col);
                    })
                ),
                loading && h(Spinner, { mutedColor }),
                !loading && features.length===0 && h(EmptyState, { mutedColor, icon:'\uD83C\uDFF7\uFE0F', title:'No attributes yet',
                    sub:'Create product attributes like Color, Size, Brand, or Material to organize and filter your catalog.' }),
                !loading && features.map(function(f, i) {
                    var purposeLabel = (PURPOSE_OPTIONS.find(function(p){ return p.v === f.purpose; }) || {}).l || f.purpose || '—';
                    var listDisplayLabel = ({checkbox:'Checkbox',slider:'Number slider',color:'Color swatches',size_pills:'Size pills',range:'Range',none:'—'})[f.filter_type] || f.filter_type || '—';
                    return h('div', { key:f.feature_id||f.id||i,
                        draggable:true,
                        onDragStart:function(){ handleDragStart(i); },
                        onDragOver:function(e){ handleDragOver(e, i); },
                        onDrop:handleDrop,
                        style:{ display:'grid', gridTemplateColumns:cols, padding:'12px '+S.xl+'px',
                                borderBottom:i<features.length-1?'1px solid '+borderColor:'none', alignItems:'center', cursor:'pointer' },
                        onClick:function(){ openEdit(f); },
                        onMouseEnter:function(e){ e.currentTarget.style.background=dark?'rgba(255,255,255,.02)':'#F9FAFB'; },
                        onMouseLeave:function(e){ e.currentTarget.style.background='transparent'; } },
                        h('span', { style:{ cursor:'grab', color:mutedColor, fontSize:16, userSelect:'none', display:'flex', alignItems:'center' },
                            onClick:function(e){ e.stopPropagation(); } }, '\u2630'),
                        h('div', null,
                            h('div', { style:{ fontSize:13, fontWeight:500 } }, f.name||'—'),
                            f.variant_count > 0 && h('span', { style:{ fontSize:10, color:mutedColor } }, f.variant_count + ' choice' + (f.variant_count===1?'':'s'))
                        ),
                        h('span', { style:{ fontSize:12, color:mutedColor } }, TYPE_LABELS[f.feature_type] || f.feature_type || '—'),
                        h('span', { style:{ fontSize:12, color:mutedColor } }, listDisplayLabel),
                        h('span', { style:{ fontSize:12, color:mutedColor } }, purposeLabel),
                        h('span', { style:{ fontSize:12, color:parseInt(f.filterable)?TC.success:mutedColor } }, parseInt(f.filterable)?'Yes':'No'),
                        h(Badge, { status:f.status==='active'?'active':'inactive', dark }, f.status==='active'?'Active':'Disabled'),
                        h('div', { style:{ display:'flex', gap:S.xs } },
                            h('button', { onClick:function(e){ e.stopPropagation(); openEdit(f); },
                                style:{ background:'transparent', border:'1px solid '+borderColor, borderRadius:6, padding:'4px 10px', fontSize:11, cursor:'pointer', color:TC.accent } }, 'Edit'),
                            h('button', { onClick:function(e){ e.stopPropagation(); del(f); },
                                style:{ background:'transparent', border:'1px solid '+borderColor, borderRadius:6, padding:'4px 10px', fontSize:11, cursor:'pointer', color:TC.error } }, 'Del')
                        )
                    );
                })
            ),

            // ═══════════════════════════════════════════════════════════════════
            // MODAL — Create / Edit Attribute
            // ═══════════════════════════════════════════════════════════════════
            showModal && h('div', { style:{ position:'fixed', inset:0, background:'rgba(0,0,0,.5)', display:'flex', alignItems:'center', justifyContent:'center', zIndex:9999 } },
                h('div', { style:{ background:cardBg, borderRadius:16, width:780, maxHeight:'90vh', display:'flex', flexDirection:'column', boxShadow:'0 20px 60px rgba(0,0,0,.3)' } },

                    // Header
                    h('div', { style:{ padding:S.xl+'px '+S.xxl+'px', borderBottom:'1px solid '+borderColor, display:'flex', justifyContent:'space-between', alignItems:'center', flexShrink:0 } },
                        h('h3', { style:{ fontSize:16, fontWeight:600 } }, editing ? 'Edit Attribute' : 'New Attribute'),
                        h('button', { onClick:closeModal, style:{ background:'transparent', border:'none', fontSize:22, cursor:'pointer', color:mutedColor, lineHeight:1 } }, '\u00D7')
                    ),

                    // Tabs
                    h('div', { style:{ display:'flex', borderBottom:'1px solid '+borderColor, flexShrink:0 } },
                        MODAL_TABS.map(function(t) {
                            var isActive = activeModalTab === t.v;
                            return h('button', { key:t.v, onClick:function(){ setModalTab(t.v); },
                                style:{ flex:1, padding:'12px 0', fontSize:13, fontWeight:isActive?600:400, cursor:'pointer',
                                        background:isActive?(dark?'rgba(37,99,235,.08)':'rgba(37,99,235,.05)'):'transparent',
                                        color:isActive?TC.accent:mutedColor, border:'none', borderBottom:isActive?'2px solid '+TC.accent:'2px solid transparent',
                                        transition:'all .15s' } }, t.l);
                        })
                    ),

                    // Body (scrollable)
                    h('div', { style:{ flex:1, overflow:'auto', padding:S.xxl } },

                        // ── GENERAL TAB ──
                        activeModalTab === 'general' && h('div', null,
                            // Name
                            h('div', { style:{ marginBottom:S.lg } },
                                h('label', { style:labelSty }, 'Attribute Name *'),
                                h('input', { value:form.name, onChange:function(e){ setForm(function(p){ return Object.assign({},p,{name:e.target.value}); }); },
                                    placeholder:'e.g. Color, Size, Brand, Material', style:fs })
                            ),
                            // Purpose (radio group)
                            h('div', { style:{ marginBottom:S.lg } },
                                h('label', { style:Object.assign({}, labelSty, { marginBottom:S.sm }) }, 'Purpose *'),
                                PURPOSE_OPTIONS.map(function(p) {
                                    var isActive = form.purpose === p.v;
                                    return h('label', { key:p.v, style:{ display:'flex', alignItems:'flex-start', gap:S.sm, marginBottom:S.md, cursor:'pointer',
                                        padding:S.md, borderRadius:8, border:'1px solid '+(isActive?TC.accent:borderColor),
                                        background:isActive?(dark?'rgba(37,99,235,.06)':'rgba(37,99,235,.03)'):'transparent',
                                        transition:'all .15s' } },
                                        h('input', { type:'radio', name:'purpose', value:p.v, checked:isActive,
                                            onChange:function(){ setForm(function(prev){ return Object.assign({},prev,{
                                                purpose:p.v,
                                                filterable: p.v==='filter' || p.v==='variation_split' || p.v==='variation_shared',
                                                feature_type: STYLE_DEFAULTS[p.v] || 'T',
                                                filter_type: FILTER_DEFAULTS[p.v] || 'none'
                                            }); }); setModalTab('general'); },
                                            style:{ accentColor:TC.accent, marginTop:2 } }),
                                        h('div', null,
                                            h('div', { style:{ fontSize:13, fontWeight:500 } }, p.l),
                                            h('div', { style:{ fontSize:11, color:mutedColor, marginTop:2 } }, p.desc)
                                        )
                                    );
                                })
                            ),
                            // Display type + Filter type row
                            h('div', { style:{ display:'grid', gridTemplateColumns: showFilterType ? '1fr 1fr' : '1fr', gap:S.lg, marginBottom:S.lg } },
                                h('div', null,
                                    h('label', { style:labelSty }, 'Display Type *'),
                                    h('p', { style:{ fontSize:11, color:mutedColor, marginBottom:S.sm } }, 'How this attribute appears on the product page.'),
                                    h('select', { value:form.feature_type,
                                        onChange:function(e){ setForm(function(p){ return Object.assign({},p,{feature_type:e.target.value}); }); },
                                        style:fs },
                                        getStyleOptions().map(function(o) { return h('option', { key:o.v, value:o.v }, o.l); })
                                    )
                                ),
                                showFilterType && h('div', null,
                                    h('label', { style:labelSty }, 'List Display *'),
                                    h('p', { style:{ fontSize:11, color:mutedColor, marginBottom:S.sm } }, 'How this attribute appears on product cards in category and search results.'),
                                    h('select', { value:form.filter_type,
                                        onChange:function(e){ setForm(function(p){ return Object.assign({},p,{filter_type:e.target.value}); }); },
                                        style:fs },
                                        getFilterOptions().map(function(o) { return h('option', { key:o.v, value:o.v }, o.l); })
                                    )
                                )
                            ),
                            // Group row
                            h('div', { style:{ marginBottom:S.lg } },
                                h('label', { style:labelSty }, 'Group'),
                                h('select', { value:form.parent_id,
                                    onChange:function(e){ setForm(function(p){ return Object.assign({},p,{parent_id:parseInt(e.target.value)||0}); }); },
                                    style:Object.assign({}, fs, { maxWidth:300 }) },
                                    h('option', { value:'0' }, '\u2014 None \u2014'),
                                    groups.map(function(g) { return h('option', { key:g.id, value:g.id }, g.name||'Group #'+g.id); })
                                )
                            ),
                            // Attribute code
                            h('div', { style:{ marginBottom:S.lg } },
                                h('label', { style:labelSty }, 'Attribute Code'),
                                h('input', { value:form.feature_code,
                                    onChange:function(e){ setForm(function(p){ return Object.assign({},p,{feature_code:e.target.value}); }); },
                                    placeholder:'e.g. brand, isbn, gtin, mpn', style:Object.assign({}, fs, { maxWidth:260 }) }),
                                h('p', { style:{ fontSize:11, color:mutedColor, marginTop:S.xs } },
                                    'Structured data codes: Brand, ISBN, GTIN, MPN help search engines understand your products.')
                            ),
                            // Sort values (roadmap #11) — hidden for Additional specs (no choices)
                            hasChoices && h('div', { style:{ marginBottom:S.lg } },
                                h('label', { style:labelSty }, 'Sort Choices'),
                                h('p', { style:{ fontSize:11, color:mutedColor, marginBottom:S.sm } },
                                    'Order of this attribute\u2019s choices everywhere they appear \u2014 product editor, variation generator, storefront filters and product-page selectors.'),
                                h('select', { value:form.sort_mode||'manual',
                                    onChange:function(e){ setForm(function(p){ return Object.assign({},p,{sort_mode:e.target.value}); }); },
                                    style:Object.assign({}, fs, { maxWidth:300 }) },
                                    h('option', { value:'manual' }, 'Manual order'),
                                    h('option', { value:'alpha' }, 'Alphabetical A\u2013Z')
                                ),
                                h('p', { style:{ fontSize:11, color:mutedColor, marginTop:S.xs } },
                                    form.sort_mode === 'alpha'
                                        ? 'Choices are listed A\u2013Z automatically. Your manual order is kept and comes back if you switch to Manual.'
                                        : 'Choices keep the order you gave them. Use \u201cSort A\u2013Z now\u201d on the Choices tab to rewrite that order once.')
                            ),
                            // S478 (Sean): site-wide prices per choice for shared variations
                            hasChoices && form.purpose === 'variation_shared' && h('div', { style:{ marginBottom:S.lg, padding:S.md+'px '+S.lg+'px', border:'1px solid '+borderColor, borderRadius:8, background:dark?'rgba(37,99,235,.04)':'rgba(37,99,235,.03)' } },
                                h('label', { style:{ display:'flex', alignItems:'center', gap:8, fontSize:13, fontWeight:500, cursor:'pointer' } },
                                    h('input', { type:'checkbox', checked:!!Number(form.global_prices), onChange:function(e){ var on=e.target.checked; setForm(function(p){ return Object.assign({},p,{global_prices:on?1:0}); }); }, style:{ accentColor:TC.accent } }),
                                    'Site-wide price changes'),
                                h('p', { style:{ fontSize:11, color:mutedColor, marginTop:S.xs, lineHeight:1.5 } },
                                    Number(form.global_prices)
                                        ? 'Every product that uses this attribute adds the amounts entered on the Choices tab (e.g. Large +5.00) to its price. A product can still set its own amount for a choice on its Attributes tab \u2014 including 0 to charge nothing for that product.'
                                        : 'Off: each product sets its own amounts on its Attributes tab. Turn on when the same size / option always costs the same extra across the store.')
                            ),
                            // Status + Filterable row
                            h('div', { style:{ display:'grid', gridTemplateColumns:'1fr 1fr', gap:S.lg } },
                                h('div', null,
                                    h('label', { style:labelSty }, 'Status'),
                                    h('select', { value:form.status,
                                        onChange:function(e){ setForm(function(p){ return Object.assign({},p,{status:e.target.value}); }); },
                                        style:fs },
                                        h('option', { value:'active' }, 'Active'), h('option', { value:'disabled' }, 'Disabled')
                                    )
                                ),
                                h('div', { style:{ display:'flex', flexDirection:'column', justifyContent:'flex-end', gap:S.sm, paddingBottom:4 } },
                                    h('label', { style:{ display:'flex', alignItems:'center', gap:S.sm, fontSize:13, cursor:'pointer' } },
                                        h('input', { type:'checkbox', checked:form.filterable,
                                            onChange:function(e){ setForm(function(p){ return Object.assign({},p,{filterable:e.target.checked}); }); },
                                            style:{ accentColor:TC.accent } }),
                                        'Show in storefront filters'
                                    ),
                                    h('label', { style:{ display:'flex', alignItems:'center', gap:S.sm, fontSize:13, cursor:'pointer' } },
                                        h('input', { type:'checkbox', checked:form.compareable,
                                            onChange:function(e){ setForm(function(p){ return Object.assign({},p,{compareable:e.target.checked}); }); },
                                            style:{ accentColor:TC.accent } }),
                                        'Show in product comparison'
                                    ),
                                    h('label', { style:{ display:'flex', alignItems:'center', gap:S.sm, fontSize:13, cursor:'pointer' } },
                                        h('input', { type:'checkbox', checked:form.show_on_pdp,
                                            onChange:function(e){ setForm(function(p){ return Object.assign({},p,{show_on_pdp:e.target.checked}); }); },
                                            style:{ accentColor:TC.accent } }),
                                        'Show on product page'
                                    )
                                )
                            )
                        ),

                        // ── CHOICES TAB ──
                        activeModalTab === 'choices' && h('div', null,
                            !editing
                                ? h('div', { style:{ textAlign:'center', padding:S.xxl, color:mutedColor } },
                                    h('p', { style:{ fontSize:13, marginBottom:S.sm } }, 'Save the attribute first, then add choices.'),
                                    h('p', { style:{ fontSize:12 } }, 'Click "Create" below, then reopen this attribute to add choices like Red, Blue, Large, Small, etc.')
                                )
                                : h(Fragment, null,
                                    h('p', { style:{ fontSize:12, color:mutedColor, marginBottom:S.md } },
                                        'Define the selectable choices for this attribute (e.g. Red, Blue, Green for Color). Hex colors work with or without the #.'),
                                    // Sort bar (roadmap #11)
                                    h('div', { style:{ display:'flex', alignItems:'center', justifyContent:'space-between', gap:S.md, marginBottom:S.lg,
                                        padding:S.sm+'px '+S.md+'px', borderRadius:8, border:'1px solid '+borderColor, background:dark?'rgba(255,255,255,.03)':'#F9FAFB' } },
                                        h('span', { style:{ fontSize:12, color:mutedColor } },
                                            'Showing: ',
                                            h('strong', { style:{ color:textColor } }, form.sort_mode === 'alpha' ? 'Alphabetical A\u2013Z' : 'Manual order'),
                                            (editing && form.sort_mode !== (editing.sort_mode === 'alpha' ? 'alpha' : 'manual')) ? ' (save the attribute to apply the new sort)' : ''
                                        ),
                                        h('button', { onClick:sortVariantsAlpha, disabled:varLoading || variants.length < 2,
                                            title:'Rewrite the manual order so it matches A\u2013Z',
                                            style:{ background:'transparent', border:'1px solid '+borderColor, borderRadius:6, padding:'5px 10px', fontSize:11,
                                                    cursor:(varLoading || variants.length < 2)?'not-allowed':'pointer', color:TC.accent, whiteSpace:'nowrap' } }, 'Sort A\u2013Z now')
                                    ),
                                    // Duplicate warning (roadmap #11)
                                    variants.some(function(v){ return v.is_duplicate; }) && h('div', { style:{ marginBottom:S.lg, padding:S.sm+'px '+S.md+'px', borderRadius:8,
                                        border:'1px solid '+TC.amber, background:dark?'rgba(217,119,6,.10)':'rgba(217,119,6,.08)', fontSize:12, color:TC.amber } },
                                        '\u26A0 Duplicate choice names: ' +
                                        (function(){
                                            var seen = {}; var names = [];
                                            variants.forEach(function(v){ if (v.is_duplicate) { var k=(v.name||'').trim().toLowerCase(); if(!seen[k]){ seen[k]=1; names.push((v.name||'').trim()); } } });
                                            return names.join(', ');
                                        })() + '. Delete or rename the extras so filters and selectors don\u2019t show the same value twice. Deleting a duplicate moves its products to the copy you keep.'
                                    ),
                                    // S466 (KI-110): merge/drop result of the last delete
                                    varNotice && h('div', { style:{ marginBottom:S.lg, padding:S.sm+'px '+S.md+'px', borderRadius:8,
                                        border:'1px solid '+TC.success, background:dark?'rgba(22,163,74,.10)':'rgba(22,163,74,.08)', fontSize:12, color:TC.success,
                                        display:'flex', justifyContent:'space-between', alignItems:'center', gap:S.sm } },
                                        h('span', null, varNotice),
                                        h('button', { onClick:function(){ setVarNotice(''); }, style:{ background:'none', border:'none', color:TC.success, cursor:'pointer', fontSize:14, lineHeight:1 } }, '\u00D7')
                                    ),
                                    // Add row
                                    h('div', { style:{ display:'flex', gap:S.sm, marginBottom:S.lg, alignItems:'flex-end' } },
                                        h('div', { style:{ flex:1 } },
                                            h('label', { style:{ fontSize:11, color:mutedColor, display:'block', marginBottom:3 } }, 'Choice name'),
                                            h('input', { type:'text', value:newVarName, placeholder:'e.g. Red, Large, Cotton\u2026',
                                                onChange:function(e){ setNewVarName(e.target.value); },
                                                onKeyDown:function(e){ if(e.key==='Enter'){ e.preventDefault(); addVariant(); } },
                                                style:Object.assign({}, fs, { width:'100%', fontSize:13 }) })
                                        ),
                                        h('div', { style:{ width:110 } },
                                            h('label', { style:{ fontSize:11, color:mutedColor, display:'block', marginBottom:3 } }, 'Hex color'),
                                            h('div', { style:{ display:'flex', gap:4, alignItems:'center' } },
                                                h('input', { type:'text', value:newVarHex, placeholder:'FF0000',
                                                    onChange:function(e){ setNewVarHex(e.target.value); },
                                                    onKeyDown:function(e){ if(e.key==='Enter'){ e.preventDefault(); addVariant(); } },
                                                    maxLength:7,
                                                    style:Object.assign({}, fs, { width:'100%', fontSize:12, fontFamily:'monospace' }) }),
                                                normHex(newVarHex) && h('span', { style:{
                                                    width:22, height:22, borderRadius:'50%', border:'2px solid '+borderColor, flexShrink:0,
                                                    background:normHex(newVarHex), display:'inline-block'
                                                } })
                                            )
                                        ),
                                        h(Btn, { variant:'primary', onClick:addVariant, disabled:!newVarName.trim(), mutedColor, borderColor }, 'Add')
                                    ),
                                    // List
                                    varLoading && h('div', { style:{ color:mutedColor, fontSize:12, padding:S.lg } }, 'Loading\u2026'),
                                    !varLoading && variants.length === 0 && h('div', { style:{ color:mutedColor, fontSize:12, textAlign:'center', padding:S.xxl } },
                                        'No choices yet. Type a name above and press Enter or click Add.'),
                                    !varLoading && variants.length > 0 && h('div', null,
                                        h('div', { style:{ display:'grid', gridTemplateColumns:(form.purpose === 'variation_shared' && !!Number(form.global_prices)) ? '64px 1fr 100px 110px 32px' : '64px 1fr 100px 32px', gap:S.sm, marginBottom:S.xs, padding:'0 '+S.sm+'px' } },
                                            h('span', { style:{ fontSize:10, fontWeight:500, color:mutedColor, textTransform:'uppercase' } }, form.sort_mode === 'alpha' ? '#' : 'Order'),
                                            h('span', { style:{ fontSize:10, fontWeight:500, color:mutedColor, textTransform:'uppercase' } }, 'Name'),
                                            h('span', { style:{ fontSize:10, fontWeight:500, color:mutedColor, textTransform:'uppercase' } }, 'Hex Color'),
                                            (form.purpose === 'variation_shared' && !!Number(form.global_prices)) ? h('span', { style:{ fontSize:10, fontWeight:500, color:mutedColor, textTransform:'uppercase' }, title:'Added to every product\u2019s price when this choice is selected' }, 'Price change') : null,
                                            h('span')
                                        ),
                                        variants.map(function(v, idx) {
                                            var vid = v.id;
                                            var manual = form.sort_mode !== 'alpha';
                                            var showPrices = (form.purpose === 'variation_shared' && !!Number(form.global_prices));
                                            var arrowStyle = function(disabled){ return { background:'transparent', border:'1px solid '+borderColor, borderRadius:4, width:18, height:18, padding:0, fontSize:9, lineHeight:'16px', cursor:disabled?'not-allowed':'pointer', color:disabled?borderColor:mutedColor }; };
                                            return h('div', { key:vid, style:{ display:'grid', gridTemplateColumns:showPrices ? '64px 1fr 100px 110px 32px' : '64px 1fr 100px 32px', gap:S.sm, alignItems:'center',
                                                padding:S.xs+'px '+S.sm+'px', borderBottom:'1px solid '+(dark?'rgba(255,255,255,.04)':borderColor),
                                                background: v._dirty ? (dark?'rgba(37,99,235,.06)':'rgba(37,99,235,.03)') : 'transparent' } },
                                                // S478: ▲▼ reorder in Manual mode (the number alone in Alphabetical)
                                                h('div', { style:{ display:'flex', alignItems:'center', gap:3 } },
                                                    h('span', { style:{ fontSize:12, color:mutedColor, minWidth:18 } }, idx + 1),
                                                    manual && h('button', { type:'button', title:'Move up', disabled:idx === 0, onClick:function(){ moveVariant(vid, -1); }, style:arrowStyle(idx === 0) }, '\u25B2'),
                                                    manual && h('button', { type:'button', title:'Move down', disabled:idx === variants.length - 1, onClick:function(){ moveVariant(vid, 1); }, style:arrowStyle(idx === variants.length - 1) }, '\u25BC')
                                                ),
                                                h('div', { style:{ display:'flex', alignItems:'center', gap:6 } },
                                                    h('input', { type:'text', value:v.name||'',
                                                        onChange:function(e){ updateVariantField(vid, 'name', e.target.value); },
                                                        style:Object.assign({}, fs, { fontSize:13, padding:'6px 8px', flex:1,
                                                            borderColor: v.is_duplicate ? TC.amber : undefined }) }),
                                                    v.is_duplicate && h('span', { title:'Duplicate name', style:{ fontSize:10, fontWeight:600, color:TC.amber, whiteSpace:'nowrap' } }, 'DUP')
                                                ),
                                                h('div', { style:{ display:'flex', gap:4, alignItems:'center' } },
                                                    h('input', { type:'text', value:v.color_hex||'', placeholder:'hex', maxLength:7,
                                                        onChange:function(e){ updateVariantField(vid, 'color_hex', e.target.value); },
                                                        style:Object.assign({}, fs, { fontSize:11, padding:'6px 6px', fontFamily:'monospace', width:'100%' }) }),
                                                    normHex(v.color_hex) && h('span', { style:{
                                                        width:18, height:18, borderRadius:'50%', border:'2px solid '+borderColor, flexShrink:0,
                                                        background:normHex(v.color_hex), display:'inline-block' } })
                                                ),
                                                // S478: site-wide price change (only when the attribute has it on)
                                                showPrices ? h('div', { style:{ display:'flex', gap:4, alignItems:'center' } },
                                                    h('span', { style:{ fontSize:11, color:mutedColor } }, '+ ' + (Config.currency_symbol||'$')),
                                                    h('input', { type:'number', step:'0.01', value:(v.price_modifier == null ? '' : v.price_modifier), placeholder:'0.00',
                                                        onChange:function(e){ updateVariantField(vid, 'price_modifier', e.target.value); },
                                                        style:Object.assign({}, fs, { fontSize:12, padding:'6px 6px', width:'100%' }) })
                                                ) : null,
                                                h('button', { onClick:function(){ delVariant(vid); },
                                                    style:{ background:'transparent', border:'1px solid '+borderColor, borderRadius:6,
                                                            padding:'3px 8px', fontSize:11, cursor:'pointer', color:TC.error } }, '\u00D7')
                                            );
                                        }),
                                        // Save button — only shows when something was edited
                                        variants.some(function(v){ return v._dirty; }) && h('div', { style:{ marginTop:S.md, display:'flex', justifyContent:'flex-end' } },
                                            h(Btn, { variant:'primary', onClick:saveAllVariants, mutedColor, borderColor }, 'Save Changes')
                                        )
                                    )
                                )
                        ),

                        // ── CATEGORIES TAB ──
                        activeModalTab === 'categories' && h('div', null,
                            !editing
                                ? h('div', { style:{ textAlign:'center', padding:S.xxl, color:mutedColor } },
                                    h('p', { style:{ fontSize:13, marginBottom:S.sm } }, 'Save the attribute first, then assign categories.'),
                                    h('p', { style:{ fontSize:12 } }, 'Click "Create" below, then reopen this attribute to choose which categories it applies to.')
                                )
                                : h(Fragment, null,
                                    featCats.length === 0 && !catSearch
                                        ? h('div', { style:{ background:dark?'rgba(255,255,255,.03)':'#F9FAFB', borderRadius:8, padding:S.xl, textAlign:'center',
                                            color:mutedColor, fontSize:13, marginBottom:S.lg, border:'1px solid '+borderColor } },
                                            'All categories included. Select specific categories below to restrict where this attribute appears.')
                                        : h('div', { style:{ marginBottom:S.sm } },
                                            h('span', { style:{ fontSize:12, color:mutedColor } }, featCats.length + ' categor' + (featCats.length===1?'y':'ies') + ' selected. '),
                                            h('button', { onClick:function(){ setFeatCats([]); },
                                                style:{ background:'transparent', border:'none', color:TC.accent, fontSize:12, cursor:'pointer', textDecoration:'underline' } }, 'Clear all (apply to all)')
                                        ),
                                    // Search
                                    h('div', { style:{ marginBottom:S.md } },
                                        h('input', { type:'text', value:catSearch, onChange:function(e){ setCatSearch(e.target.value); },
                                            placeholder:'Search categories\u2026', style:Object.assign({}, fs, { fontSize:13 }) })
                                    ),
                                    // Tree
                                    catLoading && h('div', { style:{ color:mutedColor, fontSize:12, padding:S.lg } }, 'Loading\u2026'),
                                    !catLoading && h('div', { style:{ border:'1px solid '+borderColor, borderRadius:8, maxHeight:300, overflowY:'auto' } },
                                        renderCatTree(catBuildTree(allCats, 0), 0)
                                    )
                                )
                        )
                    ),

                    // Footer
                    h('div', { style:{ padding:S.lg+'px '+S.xxl+'px', borderTop:'1px solid '+borderColor, display:'flex', gap:S.sm, justifyContent:'flex-end', flexShrink:0 } },
                        h(Btn, { onClick:closeModal, mutedColor, borderColor }, 'Cancel'),
                        h(Btn, { variant:'primary', onClick:save, disabled:saving||!form.name.trim(), mutedColor, borderColor },
                            saving ? 'Saving\u2026' : (editing ? 'Save Attribute' : 'Create'))
                    )
                )
            )
        );
    }

    /* ════════════════════════════════════════════════════════════════════
     * PRODUCT FILTERS PAGE — Storefront filter management
     * ════════════════════════════════════════════════════════════════════ */
    function FiltersPage({ dark, cardBg, textColor, mutedColor, borderColor, tableHeaderBg }) {
        const [filters,    setFilters]    = useState([]);
        const [loading,    setLoading]    = useState(true);
        const [sources,    setSources]    = useState([]);
        const [showModal,  setShowModal]  = useState(false);
        const [editing,    setEditing]    = useState(null);
        const [saving,     setSaving]     = useState(false);
        const [modalTab,   setModalTab]   = useState('general');
        const [form, setForm] = useState({ name:'', filter_by:'', display_type:'checkbox', round_to:'0', position:'0', start_expanded:false, status:'active' });
        // Categories sub-state
        const [filterCats, setFilterCats] = useState([]);
        const [allCats,    setAllCats]    = useState([]);
        const [catSearch,  setCatSearch]  = useState('');
        const [catExp,     setCatExp]     = useState(new Set());

        const fs = fieldSty(dark, borderColor);
        const labelSty = { fontSize:12, fontWeight:500, display:'block', marginBottom:S.xs, color:mutedColor };

        var DISPLAY_LABELS = { checkbox:'Checkbox', slider:'Slider', color:'Color swatches', range:'Range (min–max)', size_pills:'Size pills' };

        // ── Load ──
        function load() {
            setLoading(true);
            apiFetch('/product-filters').then(function(d) {
                setFilters(Array.isArray(d) ? d : []);
                setLoading(false);
            }).catch(function(){ setLoading(false); });
        }
        useEffect(load, []);

        useEffect(function() {
            apiFetch('/product-filters/sources').then(function(d) {
                setSources(Array.isArray(d) ? d : []);
            }).catch(function(){});
            apiFetch('/categories?format=flat&per_page=500').then(function(d) {
                setAllCats(Array.isArray(d) ? d : (d.categories||[]));
            }).catch(function(){});
        }, []);

        // ── Modal ──
        function openNew() {
            setEditing(null);
            setForm({ name:'', filter_by:'', display_type:'checkbox', round_to:'0', position:'0', start_expanded:false, status:'active' });
            setFilterCats([]); setModalTab('general'); setShowModal(true);
        }
        function openEdit(f) {
            setEditing(f);
            setForm({
                name: f.name||'', filter_by: f.filter_by||'',
                display_type: f.display_type||'checkbox', round_to: String(f.round_to||'0'),
                position: String(f.position||'0'), start_expanded: !!parseInt(f.start_expanded), status: f.status||'active'
            });
            setFilterCats(f.category_ids || []);
            setModalTab('general'); setShowModal(true);
        }
        function closeModal() { setShowModal(false); setEditing(null); }

        function onFilterByChange(val) {
            var src = sources.find(function(s){ return s.value === val; });
            setForm(function(p) {
                return Object.assign({}, p, {
                    filter_by: val,
                    display_type: src ? src.default_display : 'checkbox',
                    name: p.name || (src ? src.label : '')
                });
            });
        }

        function save() {
            if (!form.name.trim() || !form.filter_by) return;
            setSaving(true);
            var body = Object.assign({}, form, { category_ids: filterCats, round_to: parseFloat(form.round_to)||0, position: parseInt(form.position)||0 });
            var fid = editing ? (editing.filter_id || editing.id) : null;
            var req = fid
                ? apiFetch('/product-filters/' + fid, { method:'PUT',  body:body })
                : apiFetch('/product-filters',        { method:'POST', body:body });
            req.then(function() {
                setSaving(false); setShowModal(false); load();
            }).catch(function(){ setSaving(false); });
        }

        function del(f) {
            if (!confirm('Delete filter "' + f.name + '"?')) return;
            apiFetch('/product-filters/' + (f.filter_id||f.id), { method:'DELETE' }).then(load);
        }

        // ── Category tree helpers (reuse pattern) ──
        function catBuildTree(items, parentId) {
            parentId = parentId || 0;
            return items.filter(function(c){ return Number(c.parent_id||0) === parentId; })
                .sort(function(a,b){ return (a.position||0)-(b.position||0); })
                .map(function(c){ return { id:Number(c.id), name:c.name||'—', children:catBuildTree(items, Number(c.id)) }; });
        }
        function catToggleExp(cid) { setCatExp(function(prev){ var s=new Set(prev); if(s.has(cid)) s.delete(cid); else s.add(cid); return s; }); }
        function catToggle(cid) {
            cid = Number(cid);
            setFilterCats(function(prev){ return prev.includes(cid) ? prev.filter(function(c){return c!==cid;}) : prev.concat([cid]); });
        }
        function renderCatTree(nodes, depth) {
            depth = depth || 0;
            var result = [];
            nodes.forEach(function(node) {
                var hasKids = node.children && node.children.length > 0;
                var isExp = catExp.has(node.id);
                var isChecked = filterCats.includes(node.id);
                var matchesSearch = !catSearch || (node.name||'').toLowerCase().indexOf(catSearch.toLowerCase()) >= 0;
                if (!matchesSearch && !hasKids) return;
                result.push(
                    h('div', { key:node.id, style:{ display:'flex', alignItems:'center', gap:S.sm,
                        paddingLeft:(depth*20)+S.sm, paddingTop:5, paddingBottom:5,
                        borderBottom:'1px solid '+(dark?'rgba(255,255,255,.04)':borderColor) } },
                        hasKids ? h('button', { onClick:function(){ catToggleExp(node.id); },
                            style:{ background:'transparent', border:'none', cursor:'pointer', color:mutedColor, fontSize:12, width:18, padding:0,
                                    transform:isExp?'rotate(90deg)':'rotate(0)', transition:'transform .15s' } }, '\u25B8')
                            : h('span', { style:{ width:18 } }),
                        h('input', { type:'checkbox', checked:isChecked, onChange:function(){ catToggle(node.id); },
                            style:{ accentColor:TC.accent, width:15, height:15 } }),
                        h('span', { style:{ fontSize:13, cursor:'pointer' }, onClick:function(){ catToggle(node.id); } }, node.name)
                    )
                );
                if (hasKids && isExp) result = result.concat(renderCatTree(node.children, depth+1));
            });
            return result;
        }

        // Group sources for the dropdown
        var groupedSources = {};
        sources.forEach(function(s) {
            if (!groupedSources[s.group]) groupedSources[s.group] = [];
            groupedSources[s.group].push(s);
        });

        // ── Render ──
        var cols = '2fr 1.2fr 1fr 0.6fr 0.7fr 0.8fr';
        return h(PageWrap, { title:'Product Filters', sub:'Manage storefront sidebar filters for category and search pages.', mutedColor,
            action: h(Btn, { variant:'primary', onClick:openNew, mutedColor, borderColor }, '+ Add Filter') },

            h(Card, { cardBg, borderColor, dark, pad:0 },
                h('div', { style:{ display:'grid', gridTemplateColumns:cols, padding:S.md+'px '+S.xl+'px', borderBottom:'1px solid '+borderColor, background:tableHeaderBg, boxShadow:tableHeaderShadow } },
                    ['Filter Name','Filter By','Display','Pos.','Status','Actions'].map(function(col) {
                        return h('span', { key:col, style:{ fontSize:10, fontWeight:500, color:mutedColor, textTransform:'uppercase', letterSpacing:'.5px' } }, col);
                    })
                ),
                loading && h(Spinner, { mutedColor }),
                !loading && filters.length === 0 && h(EmptyState, { mutedColor, icon:'\uD83D\uDD0D', title:'No filters yet',
                    sub:'Create storefront filters so customers can narrow products by Price, Color, Brand, and more.' }),
                !loading && filters.map(function(f, i) {
                    return h('div', { key:f.filter_id||f.id||i,
                        style:{ display:'grid', gridTemplateColumns:cols, padding:'12px '+S.xl+'px',
                                borderBottom:i<filters.length-1?'1px solid '+borderColor:'none', alignItems:'center', cursor:'pointer' },
                        onClick:function(){ openEdit(f); },
                        onMouseEnter:function(e){ e.currentTarget.style.background=dark?'rgba(255,255,255,.02)':'#F9FAFB'; },
                        onMouseLeave:function(e){ e.currentTarget.style.background='transparent'; } },
                        h('div', null,
                            h('div', { style:{ fontSize:13, fontWeight:500 } }, f.name||'\u2014'),
                            f.category_ids && f.category_ids.length > 0
                                ? h('span', { style:{ fontSize:10, color:mutedColor } }, f.category_ids.length + ' categor' + (f.category_ids.length===1?'y':'ies'))
                                : h('span', { style:{ fontSize:10, color:mutedColor } }, 'All categories')
                        ),
                        h('span', { style:{ fontSize:12, color:mutedColor } }, f.source_label||f.filter_by||'\u2014'),
                        h('span', { style:{ fontSize:12, color:mutedColor } }, DISPLAY_LABELS[f.display_type]||f.display_type||'\u2014'),
                        h('span', { style:{ fontSize:12, color:mutedColor } }, f.position||0),
                        h(Badge, { status:f.status==='active'?'active':'inactive', dark }, f.status==='active'?'Active':'Off'),
                        h('div', { style:{ display:'flex', gap:S.xs } },
                            h('button', { onClick:function(e){ e.stopPropagation(); openEdit(f); },
                                style:{ background:'transparent', border:'1px solid '+borderColor, borderRadius:6, padding:'4px 10px', fontSize:11, cursor:'pointer', color:TC.accent } }, 'Edit'),
                            h('button', { onClick:function(e){ e.stopPropagation(); del(f); },
                                style:{ background:'transparent', border:'1px solid '+borderColor, borderRadius:6, padding:'4px 10px', fontSize:11, cursor:'pointer', color:TC.error } }, 'Del')
                        )
                    );
                })
            ),

            // ═══════════════════ MODAL ═══════════════════
            showModal && h('div', { style:{ position:'fixed', inset:0, background:'rgba(0,0,0,.5)', display:'flex', alignItems:'center', justifyContent:'center', zIndex:9999 } },
                h('div', { style:{ background:cardBg, borderRadius:16, width:680, maxHeight:'90vh', display:'flex', flexDirection:'column', boxShadow:'0 20px 60px rgba(0,0,0,.3)' } },

                    // Header
                    h('div', { style:{ padding:S.xl+'px '+S.xxl+'px', borderBottom:'1px solid '+borderColor, display:'flex', justifyContent:'space-between', alignItems:'center', flexShrink:0 } },
                        h('h3', { style:{ fontSize:16, fontWeight:600 } }, editing ? 'Edit Filter' : 'New Filter'),
                        h('button', { onClick:closeModal, style:{ background:'transparent', border:'none', fontSize:22, cursor:'pointer', color:mutedColor, lineHeight:1 } }, '\u00D7')
                    ),

                    // Tabs
                    h('div', { style:{ display:'flex', borderBottom:'1px solid '+borderColor, flexShrink:0 } },
                        [{ v:'general', l:'General' }, { v:'categories', l:'Categories' }].map(function(t) {
                            var isActive = modalTab === t.v;
                            return h('button', { key:t.v, onClick:function(){ setModalTab(t.v); },
                                style:{ flex:1, padding:'12px 0', fontSize:13, fontWeight:isActive?600:400, cursor:'pointer',
                                        background:isActive?(dark?'rgba(37,99,235,.08)':'rgba(37,99,235,.05)'):'transparent',
                                        color:isActive?TC.accent:mutedColor, border:'none', borderBottom:isActive?'2px solid '+TC.accent:'2px solid transparent',
                                        transition:'all .15s' } }, t.l);
                        })
                    ),

                    // Body
                    h('div', { style:{ flex:1, overflow:'auto', padding:S.xxl } },

                        // ── GENERAL TAB ──
                        modalTab === 'general' && h('div', null,
                            // Name
                            h('div', { style:{ marginBottom:S.lg } },
                                h('label', { style:labelSty }, 'Filter Name *'),
                                h('input', { value:form.name,
                                    onChange:function(e){ setForm(function(p){ return Object.assign({},p,{name:e.target.value}); }); },
                                    placeholder:'e.g. Price, Color, Brand', style:fs })
                            ),
                            // Filter By (grouped select)
                            h('div', { style:{ marginBottom:S.lg } },
                                h('label', { style:labelSty }, 'Filter By *'),
                                h('select', { value:form.filter_by,
                                    onChange:function(e){ onFilterByChange(e.target.value); },
                                    style:fs },
                                    h('option', { value:'' }, '— Select what to filter by —'),
                                    Object.keys(groupedSources).map(function(group) {
                                        return h('optgroup', { key:group, label:group },
                                            groupedSources[group].map(function(s) {
                                                return h('option', { key:s.value, value:s.value }, s.label);
                                            })
                                        );
                                    })
                                )
                            ),
                            // Position
                            h('div', { style:{ marginBottom:S.lg } },
                                h('label', { style:labelSty }, 'Position'),
                                h('input', { type:'number', min:'0', value:form.position,
                                    onChange:function(e){ setForm(function(p){ return Object.assign({},p,{position:e.target.value}); }); },
                                    style:Object.assign({}, fs, { maxWidth:120 }) })
                            ),
                            // Display Type + Round To row
                            h('div', { style:{ display:'grid', gridTemplateColumns:'1fr 1fr', gap:S.lg, marginBottom:S.lg } },
                                h('div', null,
                                    h('label', { style:labelSty }, 'Display Type'),
                                    h('select', { value:form.display_type,
                                        onChange:function(e){ setForm(function(p){ return Object.assign({},p,{display_type:e.target.value}); }); },
                                        style:fs },
                                        h('option', { value:'checkbox' }, 'Checkbox'),
                                        h('option', { value:'slider' }, 'Slider'),
                                        h('option', { value:'color' }, 'Color swatches'),
                                        h('option', { value:'size_pills' }, 'Size pills'),
                                        h('option', { value:'range' }, 'Range (min\u2013max)')
                                    )
                                ),
                                (form.display_type === 'slider' || form.display_type === 'range') && h('div', null,
                                    h('label', { style:labelSty }, 'Round To'),
                                    h('input', { type:'number', step:'0.01', min:'0', value:form.round_to,
                                        onChange:function(e){ setForm(function(p){ return Object.assign({},p,{round_to:e.target.value}); }); },
                                        placeholder:'0', style:Object.assign({}, fs, { maxWidth:120 }) }),
                                    h('p', { style:{ fontSize:11, color:mutedColor, marginTop:S.xs } }, 'Round slider values to this increment (e.g. 10 for $10 steps).')
                                )
                            ),
                            // Status
                            h('div', { style:{ marginBottom:S.lg } },
                                h('label', { style:labelSty }, 'Status'),
                                h('select', { value:form.status,
                                    onChange:function(e){ setForm(function(p){ return Object.assign({},p,{status:e.target.value}); }); },
                                    style:Object.assign({}, fs, { maxWidth:200 }) },
                                    h('option', { value:'active' }, 'Active'),
                                    h('option', { value:'disabled' }, 'Disabled')
                                )
                            ),
                            // Start Expanded
                            h('label', { style:{ display:'flex', alignItems:'center', gap:S.sm, fontSize:13, cursor:'pointer' } },
                                h('input', { type:'checkbox', checked:form.start_expanded,
                                    onChange:function(){ setForm(function(p){ return Object.assign({},p,{start_expanded:!p.start_expanded}); }); },
                                    style:{ accentColor:TC.accent } }),
                                'Start expanded when filter panel opens'
                            ),
                            h('p', { style:{ fontSize:11, color:mutedColor, marginTop:S.xs, marginLeft:24 } },
                                'By default all filters start collapsed. Check this to have this filter open automatically.')
                        ),

                        // ── CATEGORIES TAB ──
                        modalTab === 'categories' && h('div', null,
                            filterCats.length === 0 && !catSearch
                                ? h('div', { style:{ background:dark?'rgba(255,255,255,.03)':'#F9FAFB', borderRadius:8, padding:S.xl, textAlign:'center',
                                    color:mutedColor, fontSize:13, marginBottom:S.lg, border:'1px solid '+borderColor } },
                                    'Showing on all categories. Select specific categories below to restrict where this filter appears.')
                                : h('div', { style:{ marginBottom:S.sm } },
                                    h('span', { style:{ fontSize:12, color:mutedColor } }, filterCats.length + ' categor' + (filterCats.length===1?'y':'ies') + ' selected. '),
                                    h('button', { onClick:function(){ setFilterCats([]); },
                                        style:{ background:'transparent', border:'none', color:TC.accent, fontSize:12, cursor:'pointer', textDecoration:'underline' } }, 'Clear all (show on all)')
                                ),
                            h('div', { style:{ marginBottom:S.md } },
                                h('input', { type:'text', value:catSearch, onChange:function(e){ setCatSearch(e.target.value); },
                                    placeholder:'Search categories\u2026', style:Object.assign({}, fs, { fontSize:13 }) })
                            ),
                            h('div', { style:{ border:'1px solid '+borderColor, borderRadius:8, maxHeight:320, overflowY:'auto' } },
                                renderCatTree(catBuildTree(allCats, 0), 0)
                            )
                        )
                    ),

                    // Footer
                    h('div', { style:{ padding:S.lg+'px '+S.xxl+'px', borderTop:'1px solid '+borderColor, display:'flex', gap:S.sm, justifyContent:'flex-end', flexShrink:0 } },
                        h(Btn, { onClick:closeModal, mutedColor, borderColor }, 'Cancel'),
                        h(Btn, { variant:'primary', onClick:save, disabled:saving||!form.name.trim()||!form.filter_by, mutedColor, borderColor },
                            saving ? 'Saving\u2026' : (editing ? 'Save Filter' : 'Create'))
                    )
                )
            )
        );
    }

    /* ════════════════════════════════════════════════════════════════════
     * PRODUCT REVIEWS — Moderation Page
     * ════════════════════════════════════════════════════════════════════ */
    function ReviewsPage({ dark, cardBg, textColor, mutedColor, borderColor, tableHeaderBg }) {
        const [tab,        setTab]        = useState('all');
        const [page,       setPage]       = useState(1);
        const [reviews,    setReviews]    = useState([]);
        const [total,      setTotal]      = useState(0);
        const [totalPages, setTotalPages] = useState(1);
        const [loading,    setLoading]    = useState(true);
        const [selected,   setSelected]   = useState(new Set());
        const [msg,        setMsg]        = useState(null);

        function load() {
            setLoading(true);
            var q = new URLSearchParams({ page:page, per_page:TC_PER_PAGE });
            if (tab !== 'all') q.set('status', tab);
            apiFetch('/admin/reviews?' + q.toString()).then(function(r) {
                var rows = Array.isArray(r) ? r : (r.reviews || []);
                setReviews(rows);
                setTotal(Number(r.total) || rows.length);
                setTotalPages(Number(r.pages) || 1);
                setLoading(false);
            }).catch(function(){ setLoading(false); });
        }
        useEffect(load, [page, tab]);

        function moderate(id, status) {
            apiFetch('/admin/reviews/' + id + '/status', { method:'PUT', body:{ status:status } })
                .then(function() {
                    setMsg({ type:'success', text:'Review ' + status + '.' });
                    load();
                    setTimeout(function(){ setMsg(null); }, 2500);
                }).catch(function(e) {
                    setMsg({ type:'error', text: e.message || 'Failed to update review.' });
                });
        }

        function bulkModerate(status) {
            if (selected.size === 0) return;
            var promises = [];
            selected.forEach(function(id) {
                promises.push(apiFetch('/admin/reviews/' + id + '/status', { method:'PUT', body:{ status:status } }));
            });
            Promise.all(promises).then(function() {
                setMsg({ type:'success', text:selected.size + ' review(s) ' + status + '.' });
                setSelected(new Set());
                load();
                setTimeout(function(){ setMsg(null); }, 2500);
            }).catch(function(){ load(); });
        }

        function deleteReview(id) {
            if (!confirm('Permanently delete this review?')) return;
            apiFetch('/admin/reviews/' + id, { method:'DELETE' }).then(function() {
                setMsg({ type:'success', text:'Review deleted.' });
                load();
                setTimeout(function(){ setMsg(null); }, 2500);
            }).catch(function(){});
        }

        function toggleSelect(id) {
            var s = new Set(selected);
            if (s.has(id)) s.delete(id); else s.add(id);
            setSelected(s);
        }

        function toggleAll() {
            if (selected.size === reviews.length) { setSelected(new Set()); }
            else { setSelected(new Set(reviews.map(function(r){ return r.id; }))); }
        }

        function stars(n) {
            var out = [];
            for (var i = 1; i <= 5; i++) {
                out.push(h('span', { key:i, style:{ color: i <= n ? '#F59E0B' : (dark ? '#333' : '#E5E7EB'), fontSize:14 } }, '★'));
            }
            return h('span', { style:{ display:'inline-flex', gap:1 } }, out);
        }

        function statusBadge(s) {
            var map = {
                pending:  { bg:dark?'rgba(217,164,65,.12)':'#FEF3C7', color:'#D97706', label:'Pending' },
                approved: { bg:dark?'rgba(22,163,74,.12)':'#ECFDF5',  color:'#16A34A', label:'Approved' },
                rejected: { bg:dark?'rgba(220,38,38,.12)':'#FEF2F2',  color:'#DC2626', label:'Rejected' },
                spam:     { bg:dark?'rgba(113,121,126,.12)':'#F3F4F6', color:'#71797E', label:'Spam' },
            };
            var c = map[s] || map.pending;
            return h('span', { style:{ fontSize:11, fontWeight:500, color:c.color, background:c.bg, padding:'2px 8px', borderRadius:20, width:'fit-content', display:'inline-block' } }, c.label);
        }

        function fmtDate(d) { return tcFmtDate(d); }

        var tabs = [
            { v:'all', l:'All Reviews' },
            { v:'pending', l:'Pending' },
            { v:'approved', l:'Approved' },
            { v:'rejected', l:'Rejected' },
            { v:'spam', l:'Spam' },
        ];

        return h(PageWrap, { title:'Product Reviews', sub:total + ' reviews', mutedColor },

            // Message
            msg && h('div', { style:{ padding:'10px 16px', borderRadius:8, marginBottom:S.lg, fontSize:13, fontWeight:500,
                background: msg.type==='success' ? (dark?'rgba(22,163,74,.12)':'#ECFDF5') : (dark?'rgba(220,38,38,.12)':'#FEF2F2'),
                color: msg.type==='success' ? '#16A34A' : '#DC2626' } }, msg.text),

            // Tabs
            h('div', { style:{ display:'flex', gap:S.xs, marginBottom:S.lg, flexWrap:'wrap' } },
                tabs.map(function(t) {
                    var isActive = tab === t.v;
                    return h('button', { key:t.v, onClick:function(){ setTab(t.v); setPage(1); setSelected(new Set()); },
                        style:{ padding:'6px '+S.md+'px', borderRadius:8, border:'none', cursor:'pointer', fontSize:12, fontWeight:isActive?600:400,
                                background:isActive?TC.accent:'transparent', color:isActive?'#fff':mutedColor, transition:'all .15s' } }, t.l);
                })
            ),

            // Bulk actions bar
            selected.size > 0 && h('div', { style:{ display:'flex', gap:S.sm, alignItems:'center', marginBottom:S.lg, padding:'8px 14px', borderRadius:8, background:dark?'rgba(37,99,235,.08)':'#EFF6FF', border:'1px solid '+(dark?'rgba(37,99,235,.2)':'#BFDBFE') } },
                h('span', { style:{ fontSize:12, fontWeight:500, color:TC.accent } }, selected.size + ' selected'),
                h('button', { onClick:function(){ bulkModerate('approved'); }, style:{ padding:'4px 12px', borderRadius:6, border:'none', background:'#16A34A', color:'#fff', fontSize:11, fontWeight:500, cursor:'pointer' } }, '✓ Approve'),
                h('button', { onClick:function(){ bulkModerate('rejected'); }, style:{ padding:'4px 12px', borderRadius:6, border:'none', background:'#DC2626', color:'#fff', fontSize:11, fontWeight:500, cursor:'pointer' } }, '✕ Reject'),
                h('button', { onClick:function(){ bulkModerate('spam'); }, style:{ padding:'4px 12px', borderRadius:6, border:'1px solid '+borderColor, background:'transparent', color:mutedColor, fontSize:11, fontWeight:500, cursor:'pointer' } }, '🚫 Spam'),
                h('button', { onClick:function(){ setSelected(new Set()); }, style:{ marginLeft:'auto', padding:'4px 10px', borderRadius:6, border:'none', background:'transparent', color:mutedColor, fontSize:11, cursor:'pointer' } }, 'Clear')
            ),

            // Table
            h(Card, { cardBg, borderColor, dark, pad:0 },
                // Header row
                h('div', { style:{ display:'grid', gridTemplateColumns:'0.4fr 2.1fr 1.1fr 0.8fr 0.8fr 0.9fr 0.7fr 1.25fr', gap:S.lg, padding:S.md+'px '+S.xl+'px', borderBottom:'1px solid '+borderColor, background:tableHeaderBg, boxShadow:tableHeaderShadow, alignItems:'center' } },
                    h('input', { type:'checkbox', checked:reviews.length>0 && selected.size===reviews.length, onChange:toggleAll, style:{ accentColor:TC.accent } }),
                    ['Review','Product','Rating','Vendor','Status','Date','Actions'].map(function(col) {
                        return h('span', { key:col, style:{ fontSize:10, fontWeight:500, color:mutedColor, textTransform:'uppercase', letterSpacing:'.5px' } }, col);
                    })
                ),

                loading && h(Spinner, { mutedColor }),
                !loading && reviews.length === 0 && h(EmptyState, { mutedColor, icon:'⭐', title:'No reviews', sub: tab==='pending' ? 'No reviews awaiting moderation.' : 'No reviews found for this filter.' }),

                !loading && reviews.map(function(r, i) {
                    var isSelected = selected.has(r.id);
                    return h('div', { key:r.id,
                        style:{ display:'grid', gridTemplateColumns:'0.4fr 2.1fr 1.1fr 0.8fr 0.8fr 0.9fr 0.7fr 1.25fr', gap:S.lg, padding:'18px '+S.xl+'px', borderBottom: i<reviews.length-1 ? '1px solid '+borderColor : 'none', alignItems:'start',
                                background: isSelected ? (dark?'rgba(37,99,235,.06)':'#F0F7FF') : 'transparent', transition:'background .12s' },
                        onMouseEnter:function(e){ if(!isSelected) e.currentTarget.style.background=dark?'rgba(255,255,255,.02)':'#F9FAFB'; },
                        onMouseLeave:function(e){ if(!isSelected) e.currentTarget.style.background='transparent'; }
                    },
                        // Checkbox
                        h('input', { type:'checkbox', checked:isSelected, onChange:function(){ toggleSelect(r.id); }, style:{ accentColor:TC.accent, marginTop:4 } }),

                        // Review content
                        h('div', { style:{ minWidth:0, paddingRight:S.md } },
                            h('div', { style:{ display:'flex', alignItems:'center', gap:S.sm, flexWrap:'wrap', rowGap:2, marginBottom:5 } },
                                h('span', { style:{ fontSize:13, fontWeight:600 } }, r.author_name || 'Anonymous'),
                                r.verified_purchase && h('span', { style:{ fontSize:10, fontWeight:500, color:'#16A34A', background:dark?'rgba(22,163,74,.12)':'#ECFDF5', padding:'1px 6px', borderRadius:4 } }, '✓ Verified'),
                                r.author_email && h('span', { style:{ fontSize:11, color:mutedColor } }, r.author_email)
                            ),
                            r.title && h('div', { style:{ fontSize:13, fontWeight:600, marginBottom:4 } }, r.title),
                            h('div', { style:{ fontSize:12, color:mutedColor, lineHeight:'1.5', display:'-webkit-box', WebkitLineClamp:2, WebkitBoxOrient:'vertical', overflow:'hidden' } }, r.body || ''),
                            (Number(r.helpful) > 0 || Number(r.not_helpful) > 0) && h('div', { style:{ fontSize:11, color:mutedColor, marginTop:4 } },
                                '👍 ' + (r.helpful || 0) + '  👎 ' + (r.not_helpful || 0)
                            )
                        ),

                        // Product
                        h('div', { style:{ fontSize:12, color:mutedColor } },
                            h('span', { style:{ color:TC.accent, cursor:'pointer', fontSize:12 }, onClick:function(){ navigate('products/'+r.product_id); } }, r.product_name || 'Product #'+r.product_id)
                        ),

                        // Rating
                        stars(Number(r.rating)),

                        // Vendor Rating (S201 B9) — composite-review optional
                        // vendor rating. Empty dash when the customer chose
                        // not to rate the vendor (or the product had no
                        // vendor at review time).
                        Number(r.vendor_rating) > 0
                            ? stars(Number(r.vendor_rating))
                            : h('span', { style:{ fontSize:12, color:mutedColor } }, '—'),

                        // Status
                        statusBadge(r.status),

                        // Date
                        h('span', { style:{ fontSize:11, color:mutedColor } }, fmtDate(r.created_at)),

                        // Actions
                        h('div', { style:{ display:'flex', gap:4, flexWrap:'wrap' } },
                            r.status !== 'approved' && h('button', { onClick:function(){ moderate(r.id, 'approved'); }, title:'Approve',
                                style:{ padding:'3px 8px', borderRadius:5, border:'none', background:'#16A34A', color:'#fff', fontSize:11, fontWeight:500, cursor:'pointer' } }, '✓'),
                            r.status !== 'rejected' && h('button', { onClick:function(){ moderate(r.id, 'rejected'); }, title:'Reject',
                                style:{ padding:'3px 8px', borderRadius:5, border:'none', background:'#DC2626', color:'#fff', fontSize:11, fontWeight:500, cursor:'pointer' } }, '✕'),
                            r.status !== 'spam' && h('button', { onClick:function(){ moderate(r.id, 'spam'); }, title:'Mark as spam',
                                style:{ padding:'3px 8px', borderRadius:5, border:'1px solid '+borderColor, background:'transparent', color:mutedColor, fontSize:11, cursor:'pointer' } }, '🚫'),
                            h('button', { onClick:function(){ deleteReview(r.id); }, title:'Delete',
                                style:{ padding:'3px 8px', borderRadius:5, border:'1px solid #FCA5A5', background:dark?'rgba(220,38,38,.08)':'#FEF2F2', color:'#DC2626', fontSize:11, cursor:'pointer' } }, '🗑')
                        )
                    );
                }),

                // Pagination
                !loading && h('div', { style:{ display:'flex', justifyContent:'space-between', alignItems:'center', padding:S.md+'px '+S.xl+'px', borderTop:'1px solid '+borderColor } },
                    h('span', { style:{ fontSize:11, color:mutedColor } }, 'Showing ' + reviews.length + ' of ' + total),
                    h(Pager, { page, totalPages, onPage:function(p){ setPage(p); setSelected(new Set()); }, mutedColor, borderColor })
                )
            )
        );
    }

    /* ════════════════════════════════════════════════════════════════════
     * ORDERS — List + URL-Routed Detail View
     * ════════════════════════════════════════════════════════════════════ */

    /**
     * Refund modal — single-pane replacement for three chained native prompts.
     * Defined at module scope (not nested inside OrderDetailPage) so the input
     * fields retain focus as the user types. Self-contained: handles its own
     * validation + API call, surfaces inline errors without closing. Parent
     * only supplies order context + onClose/onSuccess callbacks.
     */
    function RefundModal({ order, orderId, onClose, onSuccess, dark, cardBg, textColor, mutedColor, borderColor }) {
        var orderTotal = parseFloat(order && order.total || 0);
        const [amount,     setAmount]     = useState(orderTotal.toFixed(2));
        const [reason,     setReason]     = useState('');
        const [err,        setErr]        = useState('');
        const [submitting, setSubmitting] = useState(false);

        function submit() {
            setErr('');
            var n = parseFloat(amount);
            if (isNaN(n) || n <= 0) { setErr('Please enter a valid positive amount.'); return; }
            if (n > orderTotal + 0.005) { setErr('Refund amount cannot exceed the order total of $' + orderTotal.toFixed(2) + '.'); return; }

            setSubmitting(true);
            apiFetch('/orders/' + orderId + '/refund', {
                method: 'POST',
                body: { amount: n, reason: reason.trim() }
            }).then(function(res) {
                setSubmitting(false);
                onSuccess(res);
            }).catch(function(e) {
                setSubmitting(false);
                setErr((e && e.message) ? e.message : 'Refund failed.');
            });
        }

        // Escape key closes modal (unless mid-submit). Enter on inputs is
        // captured at the input level to avoid stray form submissions.
        useEffect(function() {
            function onKey(e) {
                if (e.key === 'Escape' && !submitting) { onClose(); }
            }
            document.addEventListener('keydown', onKey);
            return function() { document.removeEventListener('keydown', onKey); };
        }, [submitting]);

        var labelSt = { display:'block', fontSize:11, fontWeight:600, color:mutedColor,
            textTransform:'uppercase', letterSpacing:'.5px', marginBottom:S.xs };
        var inputSt = { width:'100%', padding:'10px 12px', borderRadius:8,
            border:'1px solid '+borderColor,
            background: dark ? 'rgba(255,255,255,.03)' : '#FFFFFF',
            color:textColor, fontSize:14, fontFamily:'inherit', boxSizing:'border-box' };

        return h('div', {
            onClick: function(e) { if (e.target === e.currentTarget && !submitting) onClose(); },
            style: { position:'fixed', inset:0, background:'rgba(0,0,0,.55)',
                display:'flex', alignItems:'center', justifyContent:'center', zIndex:9999 }
        },
            h('div', { style:{ background:cardBg, borderRadius:14,
                border:'1px solid '+borderColor, width:480, maxWidth:'calc(100vw - 32px)',
                maxHeight:'90vh', overflow:'auto',
                boxShadow:'0 20px 60px rgba(0,0,0,.25)',
                display:'flex', flexDirection:'column' } },

                // ── Header ──────────────────────────────────────────────
                h('div', { style:{ padding:'18px 24px', borderBottom:'1px solid '+borderColor,
                    display:'flex', alignItems:'center', justifyContent:'space-between' } },
                    h('h3', { style:{ fontSize:16, fontWeight:600, margin:0 } }, 'Issue Refund'),
                    h('button', {
                        type:'button',
                        onClick: function() { if (!submitting) onClose(); },
                        disabled: submitting,
                        'aria-label': 'Close',
                        style: { background:'none', border:'none', fontSize:22,
                            color:mutedColor, cursor: submitting?'not-allowed':'pointer',
                            padding:0, lineHeight:1, opacity: submitting?0.4:1 }
                    }, '×')
                ),

                // ── Body ────────────────────────────────────────────────
                h('div', { style:{ padding:'20px 24px', display:'flex', flexDirection:'column', gap:S.lg } },

                    // Order summary strip
                    h('div', { style:{ padding:'12px 14px',
                        background: dark?'rgba(255,255,255,.03)':'#F9FAFB',
                        borderRadius:8, fontSize:13, border:'1px solid '+borderColor } },
                        h('div', { style:{ display:'flex', justifyContent:'space-between', marginBottom:4 } },
                            h('span', { style:{ color:mutedColor } }, 'Order'),
                            h('span', { style:{ fontWeight:500 } }, (order && order.order_number) || ('#' + orderId))
                        ),
                        h('div', { style:{ display:'flex', justifyContent:'space-between' } },
                            h('span', { style:{ color:mutedColor } }, 'Order total'),
                            h('span', { style:{ fontWeight:600 } }, '$' + orderTotal.toFixed(2))
                        )
                    ),

                    // Amount field
                    h('div', null,
                        h('label', { style:labelSt }, 'Refund amount (USD)'),
                        h('div', { style:{ display:'flex', gap:S.sm } },
                            h('input', {
                                type:'number', step:'0.01', min:'0.01', max: orderTotal.toFixed(2),
                                value: amount,
                                onChange: function(e) { setAmount(e.target.value); setErr(''); },
                                disabled: submitting,
                                autoFocus: true,
                                style: Object.assign({}, inputSt, { flex:1 })
                            }),
                            h('button', {
                                type:'button',
                                onClick: function() { setAmount(orderTotal.toFixed(2)); setErr(''); },
                                disabled: submitting,
                                title: 'Fill full order total',
                                style: { padding:'0 14px', borderRadius:8,
                                    border:'1px solid '+borderColor, background:'transparent',
                                    color:mutedColor, fontSize:12, fontWeight:500,
                                    cursor: submitting?'not-allowed':'pointer', whiteSpace:'nowrap' }
                            }, 'Full')
                        ),
                        err && h('div', { style:{ marginTop:8, padding:'8px 10px',
                            background: dark?'rgba(220,38,38,.1)':'#FEF2F2',
                            border:'1px solid #FCA5A5', borderRadius:6,
                            fontSize:12, color:'#DC2626', lineHeight:1.45 } }, err)
                    ),

                    // Reason field
                    h('div', null,
                        h('label', { style:labelSt }, 'Reason (optional)'),
                        h('textarea', {
                            value: reason,
                            onChange: function(e) { setReason(e.target.value); },
                            disabled: submitting,
                            rows: 3,
                            placeholder: 'e.g. Customer requested refund, defective product, duplicate order…',
                            style: Object.assign({}, inputSt, { resize:'vertical', minHeight:72 })
                        })
                    )
                ),

                // ── Footer ──────────────────────────────────────────────
                h('div', { style:{ padding:'14px 24px', borderTop:'1px solid '+borderColor,
                    display:'flex', gap:S.sm, justifyContent:'flex-end',
                    background: dark?'rgba(255,255,255,.02)':'#FAFBFC',
                    borderRadius:'0 0 14px 14px' } },
                    h('button', {
                        type:'button',
                        onClick: function() { if (!submitting) onClose(); },
                        disabled: submitting,
                        style: { padding:'9px 18px', borderRadius:8,
                            border:'1px solid '+borderColor, background:'transparent',
                            color:mutedColor, fontSize:13, fontWeight:500,
                            cursor: submitting?'not-allowed':'pointer' }
                    }, 'Cancel'),
                    h('button', {
                        type:'button',
                        onClick: submit,
                        disabled: submitting,
                        style: { padding:'9px 20px', borderRadius:8, border:'none',
                            background: submitting ? mutedColor : TC.accent,
                            color:'#FFFFFF', fontSize:13, fontWeight:600,
                            cursor: submitting?'not-allowed':'pointer',
                            opacity: submitting?0.85:1 }
                    }, submitting ? 'Processing…' : 'Issue Refund')
                )
            )
        );
    }

    /* ════════════════════════════════════════════════════════════════════
     * ORDER DOWNLOADS PANEL (S129 Phase 2)
     * ════════════════════════════════════════════════════════════════════
     * Mounted into the admin order detail page between order Items and
     * Status History. Fetches /admin/orders/{id}/downloads which returns
     * { keys, acceptances } joined to file + product info.
     *
     * Renders two sub-sections:
     *
     *   1. Issued keys — every download token created for this order's
     *      downloadable line items. Shows file name, product, status pill
     *      (active/expired/revoked), expiry, downloads/max, last activity,
     *      and a Revoke action for active keys.
     *
     *   2. Acceptances — license-agreement chargeback evidence. Shows
     *      agreement title, version, customer/email, IP/UA, accepted_at,
     *      and the truncated SHA-256 content hash. (Phase 5 starts writing
     *      these — until then, expect this section to be empty even on
     *      orders with downloadables.)
     *
     * Renders nothing when both lists are empty AFTER fetch completes —
     * orders without downloadable items don't get a noisy empty panel.
     * ════════════════════════════════════════════════════════════════════ */
    function OrderDownloadsPanel(props) {
        var orderId      = props.orderId;
        var dark         = props.dark;
        var cardBg       = props.cardBg;
        var textColor    = props.textColor;
        var mutedColor   = props.mutedColor;
        var borderColor  = props.borderColor;
        var tableHeaderBg = props.tableHeaderBg;

        var [keys, setKeys]               = useState([]);
        var [acceptances, setAcceptances] = useState([]);
        var [loaded, setLoaded]           = useState(false);
        var [msg, setMsg]                 = useState(null);
        var [busyKeyId, setBusyKeyId]     = useState(null);
        var [resending, setResending]     = useState(false);

        function flash(text, ok) { setMsg({ text:text, ok: !!ok }); setTimeout(function(){ setMsg(null); }, 4000); }

        function load() {
            apiFetch('/admin/orders/' + orderId + '/downloads')
                .then(function(d) {
                    setKeys((d && d.keys) || []);
                    setAcceptances((d && d.acceptances) || []);
                    setLoaded(true);
                })
                .catch(function(e) {
                    setLoaded(true);
                    flash(e.message || 'Could not load order downloads.', false);
                });
        }

        useEffect(function() { if (orderId) load(); /* eslint-disable-next-line */ }, [orderId]);

        function fmtBytes(b) {
            b = Number(b) || 0;
            if (b < 1024) return b + ' B';
            if (b < 1024*1024) return (b/1024).toFixed(1) + ' KB';
            if (b < 1024*1024*1024) return (b/1024/1024).toFixed(1) + ' MB';
            return (b/1024/1024/1024).toFixed(2) + ' GB';
        }

        function revokeKey(k) {
            if (!confirm('Revoke this download link? The customer will no longer be able to use it. Existing chargeback acceptances are unaffected.')) return;
            setBusyKeyId(k.id);
            apiFetch('/admin/keys/' + k.id + '/revoke', { method: 'POST', body: { reason: 'admin_manual_revoke' } })
                .then(function() { setBusyKeyId(null); flash('Download link revoked.', true); load(); })
                .catch(function(e) { setBusyKeyId(null); flash(e.message || 'Revoke failed.', false); });
        }

        // ── Admin-triggered resend (Phase 4 Step 3, S135) ──────────────────
        // Re-issues download keys for this order (revoking the current set)
        // and sends a fresh "Downloads Ready" email to the customer with the
        // is_resend=true copy variant. Old links 410 on click.
        // Endpoint: POST /admin/orders/{id}/resend-downloads-email
        // Returns 200 { success, order_id, order_number, customer_email,
        // keys_issued } on success; 422 no_downloadable_items if the order
        // has no downloadable line items (surfaces the misclick rather than
        // silently no-op'ing per Phase 4 design call).
        function resendDownloadsEmail() {
            if (!confirm('This will revoke the previous download links and email new ones. Continue?')) return;
            setResending(true);
            apiFetch('/admin/orders/' + orderId + '/resend-downloads-email', { method: 'POST' })
                .then(function(d) {
                    setResending(false);
                    var email = (d && d.customer_email) || 'customer';
                    var n     = (d && d.keys_issued) || 0;
                    flash('Email sent to ' + email + '. ' + n + ' new download links issued.', true);
                    load();
                })
                .catch(function(e) {
                    setResending(false);
                    flash(e.message || 'Resend failed.', false);
                });
        }

        function copyToClipboard(text) {
            try {
                if (navigator.clipboard && navigator.clipboard.writeText) {
                    navigator.clipboard.writeText(text).then(function(){ flash('Copied to clipboard.', true); });
                    return;
                }
            } catch(_) {}
            // Fallback for older browsers / non-secure contexts.
            var ta = document.createElement('textarea');
            ta.value = text;
            document.body.appendChild(ta);
            ta.select();
            try { document.execCommand('copy'); flash('Copied.', true); } catch(_) {}
            document.body.removeChild(ta);
        }

        function statusPill(status) {
            var bg, fg;
            if (status === 'active')   { bg = dark?'rgba(22,163,74,.12)':'#ECFDF5'; fg = '#16A34A'; }
            else if (status === 'expired') { bg = dark?'rgba(115,115,115,.18)':'#F4F4F5'; fg = mutedColor; }
            else if (status === 'revoked') { bg = dark?'rgba(220,38,38,.12)':'#FEF2F2'; fg = '#DC2626'; }
            else                       { bg = dark?'rgba(115,115,115,.18)':'#F4F4F5'; fg = mutedColor; }
            return h('span', { style: { fontSize: 10, fontWeight: 600, color: fg, background: bg, padding: '3px 8px', borderRadius: 4, textTransform: 'capitalize', display: 'inline-block' } }, status);
        }

        // ── Don't render the panel for orders with no downloadable activity ──
        if (loaded && keys.length === 0 && acceptances.length === 0) {
            return null;
        }
        if (!loaded) {
            return h(Card, { cardBg: cardBg, borderColor: borderColor, dark: dark, pad: 0, style: { marginBottom: S.lg } },
                h('div', { style: { padding: S.lg + 'px ' + S.xl + 'px' } },
                    h('h3', { style: { fontSize: 14, fontWeight: 600, marginBottom: 6 } }, 'Downloads'),
                    h(Spinner, { mutedColor: mutedColor })
                )
            );
        }

        // ── Keys table ────────────────────────────────────────────────────
        function renderKeysTable() {
            if (keys.length === 0) return null;
            var headerSty = {
                display: 'grid',
                gridTemplateColumns: '2fr 1.4fr 0.8fr 1fr 0.8fr 1.2fr 100px',
                padding: '10px 16px', borderBottom: '1px solid ' + borderColor,
                background: tableHeaderBg, gap: S.sm, alignItems: 'center',
            };
            var rowSty = {
                display: 'grid',
                gridTemplateColumns: '2fr 1.4fr 0.8fr 1fr 0.8fr 1.2fr 100px',
                padding: '12px 16px', borderBottom: '1px solid ' + borderColor,
                gap: S.sm, alignItems: 'center',
            };
            var thSty = { fontSize: 10, fontWeight: 600, color: mutedColor, textTransform: 'uppercase', letterSpacing: '0.5px' };

            return h('div', null,
                h('div', { style: { padding: S.lg + 'px ' + S.xl + 'px ' + S.md + 'px', borderBottom: '1px solid ' + borderColor, display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', gap: S.md } },
                    h('div', { style: { flex: 1, minWidth: 0 } },
                        h('h3', { style: { fontSize: 14, fontWeight: 600, margin: 0 } }, 'Issued download keys'),
                        h('p', { style: { fontSize: 12, color: mutedColor, margin: '4px 0 0' } },
                            'One key per (order item × file). Customers see these as signed download links in their order email and Account → Downloads.')
                    ),
                    h('button', {
                        onClick: resendDownloadsEmail,
                        disabled: resending,
                        title: 'Revoke the current download links and email a fresh set to the customer',
                        style: {
                            padding: '7px 14px', borderRadius: 6, border: 'none',
                            background: resending ? mutedColor : TC.accent,
                            color: '#FFFFFF', fontSize: 12, fontWeight: 600,
                            cursor: resending ? 'wait' : 'pointer',
                            opacity: resending ? 0.85 : 1,
                            whiteSpace: 'nowrap', flexShrink: 0,
                        }
                    }, resending ? 'Sending…' : 'Resend email')
                ),
                h('div', { style: headerSty },
                    h('span', { style: thSty }, 'File'),
                    h('span', { style: thSty }, 'Product'),
                    h('span', { style: thSty }, 'Status'),
                    h('span', { style: thSty }, 'Expires'),
                    h('span', { style: thSty }, 'Downloads'),
                    h('span', { style: thSty }, 'Last activity'),
                    h('span', { style: Object.assign({}, thSty, { textAlign: 'right' }) }, '')
                ),
                keys.map(function(k) {
                    var max  = (k.max_downloads === null || k.max_downloads === undefined) ? '∞' : k.max_downloads;
                    var dlText = (k.download_count || 0) + ' / ' + max;
                    var lastActivity = k.last_downloaded_at
                        ? (tcFmtDateTime(k.last_downloaded_at) + (k.last_ip ? (' · ' + k.last_ip) : ''))
                        : '—';
                    return h('div', { key: k.id, style: rowSty },
                        h('div', null,
                            h('div', { style: { fontSize: 13, fontWeight: 500, color: textColor, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } },
                                k.file_original_name || ('File #' + k.file_id)),
                            h('div', { style: { fontSize: 11, color: mutedColor, marginTop: 2 } },
                                k.file_size ? fmtBytes(k.file_size) : '—',
                                k.file_status === 'disabled' ? ' · file disabled' : '',
                                k.file_mime ? (' · ' + k.file_mime) : '')
                        ),
                        h('span', { style: { fontSize: 12, color: textColor } }, k.product_name || ('Product #' + k.product_id)),
                        statusPill(k.status),
                        h('span', { style: { fontSize: 12, color: mutedColor } },
                            k.expires_at ? tcFmtDate(k.expires_at) : 'never'),
                        h('span', { style: { fontSize: 12, color: mutedColor } }, dlText),
                        h('span', { style: { fontSize: 11, color: mutedColor, lineHeight: 1.4 } }, lastActivity),
                        h('div', { style: { display: 'flex', gap: S.xs, justifyContent: 'flex-end' } },
                            k.status === 'active' && h('button', {
                                onClick: function(){ revokeKey(k); },
                                disabled: busyKeyId === k.id,
                                style: { background:'none', border:'1px solid '+borderColor, padding:'5px 10px', borderRadius:6, fontSize:11, color: TC.error, cursor: busyKeyId===k.id?'wait':'pointer' }
                            }, busyKeyId === k.id ? '…' : 'Revoke')
                        )
                    );
                })
            );
        }

        // ── Acceptances table (chargeback evidence) ─────────────────────────
        function renderAcceptancesTable() {
            if (acceptances.length === 0) return null;
            var headerSty = {
                display: 'grid',
                gridTemplateColumns: '1.6fr 0.5fr 1.4fr 1fr 1.2fr 1.2fr',
                padding: '10px 16px', borderBottom: '1px solid ' + borderColor,
                background: tableHeaderBg, gap: S.sm, alignItems: 'center',
            };
            var rowSty = {
                display: 'grid',
                gridTemplateColumns: '1.6fr 0.5fr 1.4fr 1fr 1.2fr 1.2fr',
                padding: '12px 16px', borderBottom: '1px solid ' + borderColor,
                gap: S.sm, alignItems: 'center',
            };
            var thSty = { fontSize: 10, fontWeight: 600, color: mutedColor, textTransform: 'uppercase', letterSpacing: '0.5px' };

            return h('div', null,
                h('div', { style: { padding: S.lg + 'px ' + S.xl + 'px ' + S.md + 'px', borderTop: keys.length > 0 ? '1px solid ' + borderColor : 'none', borderBottom: '1px solid ' + borderColor } },
                    h('h3', { style: { fontSize: 14, fontWeight: 600, margin: 0 } }, 'License agreement acceptances'),
                    h('p', { style: { fontSize: 12, color: mutedColor, margin: '4px 0 0' } },
                        'Chargeback-evidence ledger. Each row records the exact agreement text the customer saw at the moment of acceptance, fingerprinted as SHA-256.')
                ),
                h('div', { style: headerSty },
                    h('span', { style: thSty }, 'Agreement'),
                    h('span', { style: thSty }, 'Ver.'),
                    h('span', { style: thSty }, 'Customer'),
                    h('span', { style: thSty }, 'IP'),
                    h('span', { style: thSty }, 'Accepted at'),
                    h('span', { style: thSty }, 'Content hash')
                ),
                acceptances.map(function(a) {
                    var hashShort = (a.accepted_content_hash || '').slice(0, 12);
                    return h('div', { key: a.id, style: rowSty },
                        h('div', null,
                            h('div', { style: { fontSize: 13, fontWeight: 500, color: textColor, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } },
                                a.agreement_title || ('Agreement #' + a.agreement_id)),
                            h('div', { style: { fontSize: 11, color: mutedColor, marginTop: 2 } },
                                a.is_global ? 'Global' : (a.agreement_product_id ? 'Product #' + a.agreement_product_id : '—'))
                        ),
                        h('span', { style: { fontSize: 12, color: mutedColor } }, 'v' + a.agreement_version),
                        h('div', null,
                            h('div', { style: { fontSize: 12, color: textColor, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } },
                                a.email || '—'),
                            a.customer_id && h('div', { style: { fontSize: 10, color: mutedColor, marginTop: 2 } }, 'Customer #' + a.customer_id)
                        ),
                        h('span', { style: { fontSize: 12, color: mutedColor, fontFamily: 'monospace' } }, a.ip_address || '—'),
                        h('span', { style: { fontSize: 12, color: mutedColor } },
                            a.accepted_at ? tcFmtDateTime(a.accepted_at) : '—'),
                        h('button', {
                            onClick: function(){ copyToClipboard(a.accepted_content_hash || ''); },
                            title: 'Copy full hash to clipboard',
                            style: { background: 'none', border: '1px solid ' + borderColor, padding: '4px 8px', borderRadius: 5, fontSize: 11, color: mutedColor, cursor: 'pointer', fontFamily: 'monospace', textAlign: 'left' }
                        }, hashShort + '…')
                    );
                })
            );
        }

        return h(Card, { cardBg: cardBg, borderColor: borderColor, dark: dark, pad: 0, style: { marginBottom: S.lg } },
            msg && h('div', {
                style: {
                    margin: '12px ' + S.xl + 'px 0',
                    padding: '8px 12px', borderRadius: 6, fontSize: 12,
                    background: msg.ok ? (dark?'rgba(22,163,74,.12)':'#ECFDF5') : (dark?'rgba(220,38,38,.12)':'#FEF2F2'),
                    color: msg.ok ? '#16A34A' : '#DC2626',
                }
            }, msg.text),
            renderKeysTable(),
            renderAcceptancesTable()
        );
    }

    /* ════════════════════════════════════════════════════════════════════
     * PrintfulOrderPanel (Phase 4) — Printful fulfillment status + manual push
     * on the admin order detail. Module-scope sibling of OrderDetailPage (never
     * nested — avoids the focus-loss trap) and fully self-contained: it talks to
     * the extension's own titancart-printful/v1 REST, owns its state, and renders
     * NOTHING unless the Printful extension is installed AND this order is relevant
     * (has Printful items, or was already pushed). Inert on non-Printful stores.
     * ════════════════════════════════════════════════════════════════════ */
    function PrintfulOrderPanel({ orderId, dark, cardBg, textColor, mutedColor, borderColor }) {
        if (!Config.printfulInstalled) return null;

        var PF_API = Config.printfulApiUrl || (Config.apiUrl || '').replace('/titancart/v1', '/titancart-printful/v1');

        var _d = useState(null),  data    = _d[0], setData    = _d[1];
        var _l = useState(true),  loading = _l[0], setLoading = _l[1];
        var _p = useState(false), pushing = _p[0], setPushing = _p[1];
        var _m = useState(null),  msg     = _m[0], setMsg     = _m[1];

        function pfFetch(path, opts) {
            opts = opts || {};
            opts.headers = Object.assign({ 'Content-Type':'application/json', 'X-WP-Nonce': Config.nonce }, opts.headers || {});
            if (opts.body && typeof opts.body === 'object') opts.body = JSON.stringify(opts.body);
            return fetch(PF_API + path, opts).then(function(r) {
                return r.text().then(function(text) {
                    var d = null; if (text) { try { d = JSON.parse(text); } catch(e) { d = null; } }
                    if (!r.ok || (d && d.success === false)) {
                        var mm = (d && d.error && d.error.message) || (d && d.message) || ('HTTP ' + r.status);
                        var err = new Error(mm); err.status = r.status; return Promise.reject(err);
                    }
                    return d;
                });
            });
        }

        function load() {
            setLoading(true);
            pfFetch('/orders/' + orderId).then(function(d) {
                setData((d && d.data) || null);
                setLoading(false);
            }).catch(function(){ setData(null); setLoading(false); });
        }
        useEffect(function(){ load(); }, [orderId]);

        function doPush() {
            setPushing(true); setMsg(null);
            pfFetch('/orders/' + orderId + '/push', { method:'POST' }).then(function(d) {
                setPushing(false);
                var r = (d && d.data) || {};
                setMsg({ ok: r.status !== 'error', text: r.message || 'Done.' });
                load();
            }).catch(function(e){
                setPushing(false);
                setMsg({ ok:false, text: e.message || 'Push failed.' });
            });
        }

        if (loading || !data) return null;

        var connected = !!data.connected;
        var hasItems  = !!data.has_printful_items;
        var hasMap    = !!data.has_map;
        // Only relevant when this order has Printful items or was already pushed.
        if (!hasItems && !hasMap) return null;

        var row       = data.row || null;
        var status    = row ? String(row.push_status || '') : '';
        var orderMode = String(data.order_mode || 'draft');

        var META = {
            pushed:    { label:'Draft in Printful', color:'#D97706' },
            confirmed: { label:'Confirmed',         color:'#16A34A' },
            error:     { label:'Push failed',       color:'#DC2626' },
            pending:   { label:'Queued',            color:mutedColor }
        };
        var meta = META[status] || { label:(status || 'Not pushed yet'), color:mutedColor };

        var pfId    = row ? Number(row.printful_order_id || 0) : 0;
        var dashUrl = row ? (row.dashboard_url || '') : '';

        // No re-push once an order has a Printful order id (avoids a duplicate
        // fulfillment order); allow a first push or a retry after failure.
        var canPush   = connected && (!row || status === 'error' || pfId <= 0);
        var pushLabel = (status === 'error') ? 'Retry push' : 'Push to Printful';

        return h(Card, { cardBg, borderColor, dark, style:{ marginBottom:S.lg } },
            h('h4', { style:{ fontSize:13, fontWeight:600, marginBottom:S.md, color:mutedColor, textTransform:'uppercase', letterSpacing:'.5px' } }, 'Printful Fulfillment'),

            !connected && h('div', { style:{ fontSize:12, color:mutedColor, marginBottom:S.sm } },
                'Printful is installed but not connected. Connect it under Growth → Printful to push orders.'),

            connected && h('div', { style:{ display:'flex', alignItems:'center', gap:S.sm, marginBottom: row ? S.sm : S.md } },
                h('span', { style:{ display:'inline-block', width:8, height:8, borderRadius:'50%', background:meta.color } }),
                h('span', { style:{ fontSize:13, fontWeight:600, color:meta.color } }, meta.label)
            ),

            row && pfId > 0 && h('div', { style:{ fontSize:12, color:mutedColor, marginBottom:S.sm } },
                'Printful order ',
                dashUrl
                    ? h('a', { href:dashUrl, target:'_blank', rel:'noopener noreferrer', style:{ color:TC.accent, fontWeight:600 } }, '#' + pfId)
                    : h('span', { style:{ fontWeight:600, color:textColor } }, '#' + pfId),
                row.item_count ? ' · ' + row.item_count + ' item' + (Number(row.item_count) === 1 ? '' : 's') : '',
                row.pushed_at ? ' · ' + tcFmtDateTime(row.pushed_at) : ''
            ),

            row && status === 'error' && row.error_message && h('div', { style:{ fontSize:11, color:'#DC2626', background:dark?'rgba(220,38,38,.08)':'#FEF2F2', padding:'6px 10px', borderRadius:6, marginBottom:S.sm } }, String(row.error_message)),

            row && status === 'pushed' && h('div', { style:{ fontSize:11, color:mutedColor, marginBottom:S.sm } },
                orderMode === 'auto'
                    ? 'Auto-confirm is on, but this order is still a draft — confirm it in the Printful dashboard.'
                    : 'Draft-first: confirm this order in the Printful dashboard to start fulfillment.'),

            msg && h('div', { style:{ fontSize:12, padding:'6px 10px', borderRadius:6, marginBottom:S.sm,
                    background: msg.ok ? (dark?'rgba(22,163,74,.12)':'#ECFDF5') : (dark?'rgba(220,38,38,.12)':'#FEF2F2'),
                    color: msg.ok ? '#16A34A' : '#DC2626' } }, msg.text),

            canPush && h('div', { style:{ display:'flex', justifyContent:'flex-end' } },
                h(Btn, { variant:'primary', onClick:doPush, disabled:pushing, mutedColor, borderColor }, pushing ? 'Pushing…' : pushLabel)
            )
        );
    }

    function OrderDetailPage({ orderId, dark, cardBg, textColor, mutedColor, borderColor, tableHeaderBg }) {
        const [order,      setOrder]      = useState(null);
        const [loading,    setLoading]    = useState(true);
        const [statuses,   setStatuses]   = useState([]);
        const [newStatus,  setNewStatus]  = useState('');
        const [updStatus,  setUpdStatus]  = useState(false);
        const [notifyCust, setNotifyCust] = useState(true);
        const [statusComment, setStatusComment] = useState('');
        const [noteText,   setNoteText]   = useState('');
        const [noteType,   setNoteType]   = useState('internal');
        const [savingNote, setSavingNote] = useState(false);
        const [msg,        setMsg]        = useState(null);
        const [showRefundModal, setShowRefundModal] = useState(false);

        // S204 — Pattern #151 cascade: native confirm() → ConfirmModal for the
        // "Cancel Order" destructive action. Modal rendered next to RefundModal
        // at the end of this component's return. Uses existing updStatus for
        // the busy state.
        const [cCancel, setCCancel] = useState(false);

        function loadOrder() {
            setLoading(true);
            apiFetch('/orders/' + orderId).then(function(d) {
                var o = d.order || d;
                setOrder(o);
                setNewStatus(o.status_handle || o.status || '');
                setLoading(false);
            }).catch(function(){ setLoading(false); });
        }

        function loadStatuses() {
            apiFetch('/orders/statuses').then(function(d) {
                setStatuses(Array.isArray(d) ? d : []);
            }).catch(function(){});
        }

        useEffect(function(){ loadOrder(); loadStatuses(); }, [orderId]);

        function changeStatus() {
            if (!newStatus || !order) return;
            setUpdStatus(true);
            apiFetch('/orders/' + orderId + '/status', {
                method:'PATCH',
                body:{ status:newStatus, notify:notifyCust, comment:statusComment }
            }).then(function() {
                setUpdStatus(false);
                setStatusComment('');
                setMsg({ type:'success', text:'Status updated successfully.' });
                loadOrder();
                setTimeout(function(){ setMsg(null); }, 3000);
            }).catch(function(e){
                setUpdStatus(false);
                setMsg({ type:'error', text: e.message || 'Status update failed.' });
            });
        }

        function saveNote() {
            if (!noteText.trim() || !order) return;
            setSavingNote(true);
            apiFetch('/orders/' + orderId + '/notes', {
                method:'POST',
                body:{ note: noteText, type: noteType }
            }).then(function() {
                setSavingNote(false);
                setNoteText('');
                loadOrder();
            }).catch(function(){ setSavingNote(false); });
        }

        function refundOrder() {
            if (!order) return;
            setShowRefundModal(true);
        }

        function handleRefundSuccess(res) {
            setShowRefundModal(false);
            loadOrder();
            setMsg({
                type: 'success',
                text: (res && res.message) ? res.message : 'Refund issued successfully.'
            });
            setTimeout(function() { setMsg(null); }, 6000);
        }

        function parseAddr(raw) {
            if (!raw) return {};
            if (typeof raw === 'object') return raw;
            try { return JSON.parse(raw); } catch(e) { return {}; }
        }

        function fmtAddr(raw) {
            var a = parseAddr(raw);
            var name = [a.first_name, a.last_name].filter(Boolean).join(' ');
            return [name, a.company, a.address_1, a.address_2, [a.city, a.state, a.postcode].filter(Boolean).join(', '), a.country, a.phone].filter(Boolean);
        }

        function fmtAddrObj(a) {
            if (!a || typeof a !== 'object') return [];
            var name = [a.first_name, a.last_name].filter(Boolean).join(' ');
            return [name, a.company, a.address_1, a.address_2, [a.city, a.state, a.postcode].filter(Boolean).join(', '), a.country, a.phone].filter(Boolean);
        }

        // S120: pull this order's stored currency code (order.currency) so historical
        // orders always render in the currency they were placed in. Falls back to
        // store primary if missing.
        function fmtMoney(v) { return tcFmtMoney(v, order && order.currency); }
        function fmtDate(d) { return tcFmtDate(d); }
        function fmtDateTime(d) { return tcFmtDateTime(d); }

        var fs = fieldSty(dark, borderColor);

        if (loading) return h('main', { style:{ flex:1, overflow:'auto', padding:S.xxl } }, h(Spinner, { mutedColor }));

        if (!order) return h('main', { style:{ flex:1, overflow:'auto', padding:S.xxl } },
            h(EmptyState, { mutedColor, icon:'🧾', title:'Order not found', sub:'The order may have been deleted.' })
        );

        var o = order;
        var oid = o.order_number || o.id;
        var ba = parseAddr(o.billing_address);
        if (!ba.first_name && o.billing_first_name) {
            ba = { first_name:o.billing_first_name, last_name:o.billing_last_name, company:o.billing_company, address_1:o.billing_address_1, address_2:o.billing_address_2, city:o.billing_city, state:o.billing_state, postcode:o.billing_postcode, country:o.billing_country, phone:o.billing_phone, email:o.billing_email };
        }
        var sa = parseAddr(o.shipping_address);
        if (!sa.first_name && o.shipping_first_name) {
            sa = { first_name:o.shipping_first_name, last_name:o.shipping_last_name, company:o.shipping_company, address_1:o.shipping_address_1, address_2:o.shipping_address_2, city:o.shipping_city, state:o.shipping_state, postcode:o.shipping_postcode, country:o.shipping_country };
        }
        var items = Array.isArray(o.items) ? o.items : [];
        var history = Array.isArray(o.status_history) ? o.status_history : (Array.isArray(o.history) ? o.history : []);
        var notes = Array.isArray(o.notes) ? o.notes : [];
        var statusColor = o.status_color || '#71797E';
        var statusLabel = o.status_label || o.status_handle || '—';

        return h('main', { style:{ flex:1, overflow:'auto', padding:S.xxl } },

            // ── Top bar
            h('div', { style:{ display:'flex', justifyContent:'space-between', alignItems:'center', marginBottom:S.xl, flexWrap:'wrap', gap:S.md } },
                h('div', { style:{ display:'flex', alignItems:'center', gap:S.md } },
                    h('button', { onClick:function(){ navigate('orders'); },
                        style:{ background:'transparent', border:'1px solid '+borderColor, borderRadius:8, padding:'6px 10px', cursor:'pointer', color:mutedColor, display:'flex', alignItems:'center', gap:4, fontSize:13 } },
                        h('span', { style:{ fontSize:16 } }, '←'), ' Orders'
                    ),
                    h('div', null,
                        h('h2', { style:{ fontSize:20, fontWeight:700, letterSpacing:'-.3px', display:'flex', alignItems:'center', gap:S.sm } },
                            'Order ' + oid,
                            h('span', { style:{ fontSize:12, fontWeight:500, color:statusColor, background:statusColor+'18', padding:'2px 8px', borderRadius:20, marginLeft:4 } }, statusLabel)
                        ),
                        h('span', { style:{ fontSize:12, color:mutedColor } }, 'Placed ' + fmtDate(o.created_at))
                    )
                ),
                h('div', { style:{ display:'flex', gap:S.sm, alignItems:'center' } },
                    // S267 (#7/KI-15): the order detail "Print" button now opens the
                    // real branded invoice document at /order-invoice/{number}/?print=1
                    // (auto-fires the print dialog), instead of window.print()-ing the
                    // whole admin SPA chrome. Admin is authorized server-side
                    // (manage_options) so no ?key= is needed. Falls back to a raw
                    // window.print() only if the order has no number (defensive).
                    // This supersedes the S260 gate that removed the dead "📄 Invoice"
                    // button — the Print button IS the invoice now.
                    h(Btn, { variant:'outline', mutedColor, borderColor, onClick:function(){
                        var num = o.order_number || '';
                        if (!num) { window.print(); return; }
                        var base = (Config.siteUrl || '').replace(/\/+$/, '');
                        window.open(base + '/order-invoice/' + encodeURIComponent(num) + '/?print=1', '_blank');
                    } }, '🖨 Print Invoice'),
                    // S315 — Packing Slip: warehouse pick list (core route, manage_options
                    // only). Opens /order-packing-slip/{number}/?print=1 in a new tab.
                    h(Btn, { variant:'outline', mutedColor, borderColor, onClick:function(){
                        var num = o.order_number || '';
                        if (!num) { return; }
                        var base = (Config.siteUrl || '').replace(/\/+$/, '');
                        window.open(base + '/order-packing-slip/' + encodeURIComponent(num) + '/?print=1', '_blank');
                    } }, '📦 Packing Slip')
                )
            ),

            // ── Success/error message
            msg && h('div', { style:{ padding:'10px 16px', borderRadius:8, marginBottom:S.lg, fontSize:13, fontWeight:500,
                background: msg.type==='success' ? (dark?'rgba(22,163,74,.12)':'#ECFDF5') : (dark?'rgba(220,38,38,.12)':'#FEF2F2'),
                color: msg.type==='success' ? '#16A34A' : '#DC2626',
                border: '1px solid ' + (msg.type==='success' ? '#BBF7D0' : '#FECACA') } }, msg.text),

            // ── Two-column layout
            h('div', { style:{ display:'grid', gridTemplateColumns:'1fr 360px', gap:S.lg, alignItems:'start' } },

                // ═══ LEFT COLUMN ═══
                h('div', null,

                    // ── Items card
                    h(Card, { cardBg, borderColor, dark, pad:0, style:{ marginBottom:S.lg } },
                        h('div', { style:{ padding:S.lg+'px '+S.xl+'px', borderBottom:'1px solid '+borderColor, display:'flex', justifyContent:'space-between', alignItems:'center' } },
                            h('h3', { style:{ fontSize:14, fontWeight:600 } }, 'Order Items'),
                            h('span', { style:{ fontSize:12, color:mutedColor } }, items.length + ' item' + (items.length!==1?'s':''))
                        ),
                        items.length === 0 && h('div', { style:{ padding:S.xl, color:mutedColor, fontSize:13 } }, 'No items found.'),
                        items.length > 0 && h('div', null,
                            // Table header
                            h('div', { style:{ display:'grid', gridTemplateColumns:'1fr 80px 100px 100px', padding:S.sm+'px '+S.xl+'px', background:tableHeaderBg, boxShadow:tableHeaderShadow, borderBottom:'1px solid '+borderColor } },
                                ['Product','Qty','Price','Total'].map(function(col){ return h('span',{key:col,style:{fontSize:10,fontWeight:500,color:mutedColor,textTransform:'uppercase',letterSpacing:'.5px',textAlign:col==='Total'||col==='Price'?'right':'left'}},col); })
                            ),
                            // Rows
                            items.map(function(item, i) {
                                var qty   = Number(item.quantity || item.amount || 1);
                                var price = parseFloat(item.unit_price || item.price || 0);
                                var line  = parseFloat(item.total || item.subtotal || (price * qty));
                                var opts  = Array.isArray(item.options) ? item.options : [];
                                var optsText = opts.map(function(op){ return (op.option_name||'') + ': ' + (op.variant_name||''); }).filter(Boolean).join(', ');

                                return h('div', { key:i, style:{ display:'grid', gridTemplateColumns:'1fr 80px 100px 100px', padding:'14px '+S.xl+'px', borderBottom: i<items.length-1 ? '1px solid '+borderColor : 'none', alignItems:'center' } },
                                    h('div', { style:{ display:'flex', alignItems:'center', gap:S.md } },
                                        // Thumbnail
                                        item.thumbnail
                                            ? h('img', { src:item.thumbnail, style:{ width:44, height:Config.productImageShape==='square'?44:59, borderRadius:8, objectFit:'cover', border:'1px solid '+borderColor, flexShrink:0 } })
                                            : h('div', { style:{ width:44, height:Config.productImageShape==='square'?44:59, borderRadius:8, background:dark?'#1E2130':'#F3F4F6', display:'flex', alignItems:'center', justifyContent:'center', fontSize:18, flexShrink:0 } }, '📦'),
                                        h('div', { style:{ minWidth:0 } },
                                            h('div', { style:{ fontSize:13, fontWeight:500, whiteSpace:'nowrap', overflow:'hidden', textOverflow:'ellipsis' } }, item.name || '—'),
                                            item.sku && h('div', { style:{ fontSize:11, color:mutedColor, marginTop:1 } }, 'Product Code: ' + item.sku),
                                            optsText && h('div', { style:{ fontSize:11, color:mutedColor, marginTop:1 } }, optsText)
                                        )
                                    ),
                                    h('span', { style:{ fontSize:13, textAlign:'center' } }, qty),
                                    h('span', { style:{ fontSize:13, fontWeight:500, textAlign:'right' } }, fmtMoney(price)),
                                    h('span', { style:{ fontSize:13, fontWeight:600, textAlign:'right' } }, fmtMoney(line))
                                );
                            }),
                            // Totals
                            h('div', { style:{ padding:S.lg+'px '+S.xl+'px', borderTop:'1px solid '+borderColor } },
                                [
                                    ['Subtotal', o.subtotal],
                                    o.discount_total && parseFloat(o.discount_total) > 0 ? ['Discount', '-'+fmtMoney(o.discount_total)] : null,
                                    ['Shipping', o.shipping_total || o.shipping_cost],
                                    ['Tax', o.tax_total || o.tax_amount],
                                    o.tip_total && parseFloat(o.tip_total) > 0 ? ['Tip', o.tip_total] : null,
                                ].filter(Boolean).map(function(pair) {
                                    return h('div', { key:pair[0], style:{ display:'flex', justifyContent:'space-between', fontSize:13, marginBottom:6 } },
                                        h('span', { style:{ color:mutedColor } }, pair[0]),
                                        h('span', { style:{ fontWeight:500 } }, typeof pair[1] === 'string' && pair[1].startsWith('-') ? pair[1] : fmtMoney(pair[1]))
                                    );
                                }),
                                h('div', { style:{ display:'flex', justifyContent:'space-between', fontSize:16, fontWeight:700, borderTop:'1px solid '+borderColor, paddingTop:S.md, marginTop:S.xs } },
                                    h('span', null, 'Total'),
                                    h('span', null, fmtMoney(o.total))
                                )
                            )
                        )
                    ),

                    // ── Downloads (S129 Phase 2 — keys + acceptances for this order)
                    h(OrderDownloadsPanel, { orderId: o.id || orderId, dark, cardBg, textColor, mutedColor, borderColor, tableHeaderBg }),

                    // ── Status History (timeline)
                    history.length > 0 && h(Card, { cardBg, borderColor, dark, pad:0, style:{ marginBottom:S.lg } },
                        h('div', { style:{ padding:S.lg+'px '+S.xl+'px', borderBottom:'1px solid '+borderColor } },
                            h('h3', { style:{ fontSize:14, fontWeight:600 } }, 'Order Activity')
                        ),
                        h('div', { style:{ padding:S.lg+'px '+S.xl+'px' } },
                            history.map(function(ev, i) {
                                var toColor = '#71797E';
                                statuses.forEach(function(s){ if (s.handle === ev.to_status || s.handle === ev.new_status) toColor = s.color || '#71797E'; });
                                var toLabel = ev.to_status || ev.new_status || '—';
                                statuses.forEach(function(s){ if (s.handle === toLabel) toLabel = s.label || s.handle; });
                                var fromLabel = ev.from_status || ev.old_status || '';
                                statuses.forEach(function(s){ if (s.handle === fromLabel) fromLabel = s.label || s.handle; });

                                return h('div', { key:i, style:{ display:'flex', gap:S.md, marginBottom: i<history.length-1 ? S.lg : 0, position:'relative', paddingLeft:20 } },
                                    // Timeline dot + line
                                    h('div', { style:{ position:'absolute', left:0, top:3, width:10, height:10, borderRadius:'50%', background:toColor, border:'2px solid '+(dark?'#181B25':'#fff'), zIndex:1 } }),
                                    i < history.length-1 && h('div', { style:{ position:'absolute', left:4, top:14, width:2, height:'calc(100% + '+S.lg+'px - 14px)', background:borderColor } }),
                                    h('div', { style:{ flex:1 } },
                                        h('div', { style:{ fontSize:13, fontWeight:500 } },
                                            'Changed to ',
                                            h('span', { style:{ color:toColor, fontWeight:600 } }, toLabel),
                                            fromLabel ? ' from ' + fromLabel : ''
                                        ),
                                        ev.comment && h('div', { style:{ fontSize:12, color:mutedColor, marginTop:3, fontStyle:'italic' } }, ev.comment),
                                        h('div', { style:{ fontSize:11, color:mutedColor, marginTop:3 } },
                                            fmtDateTime(ev.created_at),
                                            ev.changed_by_name ? ' · ' + ev.changed_by_name : ''
                                        )
                                    )
                                );
                            })
                        )
                    ),

                    // ── Order Notes
                    h(Card, { cardBg, borderColor, dark, style:{ marginBottom:S.lg } },
                        h('h3', { style:{ fontSize:14, fontWeight:600, marginBottom:S.lg } }, 'Order Notes'),
                        notes.length > 0 && h('div', { style:{ marginBottom:S.lg } },
                            notes.map(function(n, i) {
                                var isInternal = n.type === 'internal' || n.type === 'admin';
                                var isSystem   = n.type === 'system';
                                var bgColor = isInternal ? (dark?'rgba(37,99,235,.08)':'#EFF6FF')
                                            : isSystem   ? (dark?'rgba(255,255,255,.03)':'#F9FAFB')
                                            :              (dark?'rgba(22,163,74,.08)':'#F0FDF4');
                                var borderLeft = isInternal ? '3px solid '+TC.accent
                                               : isSystem   ? '3px solid '+mutedColor
                                               :              '3px solid #16A34A';
                                var typeLabel = isInternal ? 'Staff Note' : isSystem ? 'System' : 'Customer Note';
                                return h('div', { key:i, style:{ padding:'10px 14px', borderRadius:8, background:bgColor, borderLeft:borderLeft, marginBottom:S.sm } },
                                    h('div', { style:{ fontSize:12, lineHeight:'1.5' } }, n.note || n.content || ''),
                                    h('div', { style:{ fontSize:10, color:mutedColor, marginTop:4, display:'flex', gap:S.sm } },
                                        h('span', { style:{ fontWeight:600 } }, typeLabel),
                                        h('span', null, '·'),
                                        h('span', null, fmtDateTime(n.created_at)),
                                        n.author_name && h('span', null, '· ' + n.author_name)
                                    )
                                );
                            })
                        ),
                        // Add note form
                        h('div', { style:{ display:'flex', gap:S.sm, marginBottom:S.sm } },
                            h('label', { style:{ display:'flex', alignItems:'center', gap:4, fontSize:12, cursor:'pointer', color: noteType==='internal'?TC.accent:mutedColor, fontWeight: noteType==='internal'?600:400 } },
                                h('input', { type:'radio', name:'ntype', value:'internal', checked:noteType==='internal', onChange:function(){ setNoteType('internal'); }, style:{ accentColor:TC.accent } }),
                                'Staff Note'
                            ),
                            h('label', { style:{ display:'flex', alignItems:'center', gap:4, fontSize:12, cursor:'pointer', color: noteType==='customer'?'#16A34A':mutedColor, fontWeight: noteType==='customer'?600:400 } },
                                h('input', { type:'radio', name:'ntype', value:'customer', checked:noteType==='customer', onChange:function(){ setNoteType('customer'); }, style:{ accentColor:'#16A34A' } }),
                                'Customer Note'
                            )
                        ),
                        noteType==='customer' && h('div', { style:{ fontSize:11, color:'#D97706', background:dark?'rgba(217,118,6,.08)':'#FFFBEB', padding:'6px 10px', borderRadius:6, marginBottom:S.sm } }, '⚠ Customer notes are visible to the customer on their order page.'),
                        h('textarea', { value:noteText, onChange:function(e){ setNoteText(e.target.value); }, placeholder:'Write a note…', rows:3,
                            style:Object.assign({},fs,{ resize:'vertical', marginBottom:S.sm }) }),
                        h('div', { style:{ display:'flex', justifyContent:'flex-end' } },
                            h(Btn, { variant:'primary', onClick:saveNote, disabled:savingNote||!noteText.trim(), mutedColor, borderColor }, savingNote?'Saving…':'Add Note')
                        )
                    )
                ),

                // ═══ RIGHT COLUMN ═══
                h('div', null,

                    // ── Printful Fulfillment (renders only when the Printful ext is installed + this order is relevant)
                    h(PrintfulOrderPanel, { orderId:orderId, dark:dark, cardBg:cardBg, textColor:textColor, mutedColor:mutedColor, borderColor:borderColor }),

                    // ── Update Status card
                    h(Card, { cardBg, borderColor, dark, style:{ marginBottom:S.lg } },
                        h('h4', { style:{ fontSize:13, fontWeight:600, marginBottom:S.lg, color:mutedColor, textTransform:'uppercase', letterSpacing:'.5px' } }, 'Update Status'),
                        h('select', { value:newStatus, onChange:function(e){ setNewStatus(e.target.value); },
                            style:Object.assign({},fs,{ width:'100%', padding:'8px 10px', marginBottom:S.sm }) },
                            statuses.map(function(s) { return h('option',{key:s.handle||s.id,value:s.handle}, s.label || s.handle); })
                        ),
                        h('textarea', { value:statusComment, onChange:function(e){ setStatusComment(e.target.value); }, placeholder:'Optional status change note…', rows:2,
                            style:Object.assign({},fs,{ resize:'vertical', marginBottom:S.sm }) }),
                        h('label', { style:{ display:'flex', alignItems:'center', gap:6, fontSize:12, marginBottom:S.lg, cursor:'pointer' } },
                            h('input', { type:'checkbox', checked:notifyCust, onChange:function(){ setNotifyCust(!notifyCust); }, style:{ accentColor:TC.accent } }),
                            'Notify customer via email'
                        ),
                        h(Btn, { variant:'primary', onClick:changeStatus, disabled:updStatus || newStatus===(o.status_handle||o.status), mutedColor, borderColor, style:{ width:'100%' } }, updStatus?'Updating…':'Update Status')
                    ),

                    // ── Customer card
                    h(Card, { cardBg, borderColor, dark, style:{ marginBottom:S.lg } },
                        h('h4', { style:{ fontSize:13, fontWeight:600, marginBottom:S.lg, color:mutedColor, textTransform:'uppercase', letterSpacing:'.5px' } }, 'Customer'),
                        h('div', { style:{ display:'flex', alignItems:'center', gap:S.md, marginBottom:S.md } },
                            h('div', { style:{ width:40, height:40, borderRadius:'50%', background:'linear-gradient(135deg,'+TC.primary+','+TC.accent+')', display:'flex', alignItems:'center', justifyContent:'center', color:'#fff', fontSize:14, fontWeight:600, flexShrink:0 } },
                                ((ba.first_name||'?')[0] + (ba.last_name||'')[0]).toUpperCase()
                            ),
                            h('div', null,
                                h('div', { style:{ fontSize:13, fontWeight:500 } }, [ba.first_name, ba.last_name].filter(Boolean).join(' ') || '(Guest)'),
                                h('div', { style:{ fontSize:12, color:mutedColor } }, o.billing_email || o.email || '—')
                            )
                        ),
                        ba.phone && h('div', { style:{ fontSize:12, color:mutedColor, marginTop:2 } }, '📞 ' + ba.phone),
                        o.customer_id && h('div', { style:{ marginTop:S.sm } },
                            h('a', { href:'#', onClick:function(e){ e.preventDefault(); navigate('customers/'+o.customer_id); }, style:{ fontSize:12, color:TC.accent, textDecoration:'none' } }, 'View customer profile →')
                        )
                    ),

                    // ── Addresses card
                    h(Card, { cardBg, borderColor, dark, style:{ marginBottom:S.lg } },
                        h('h4', { style:{ fontSize:13, fontWeight:600, marginBottom:S.lg, color:mutedColor, textTransform:'uppercase', letterSpacing:'.5px' } }, 'Addresses'),
                        // Billing
                        h('div', { style:{ marginBottom:S.lg } },
                            h('div', { style:{ fontSize:11, fontWeight:600, color:TC.accent, marginBottom:S.xs, textTransform:'uppercase', letterSpacing:'.5px' } }, 'Billing'),
                            fmtAddrObj(ba).map(function(line, i) { return h('div', { key:i, style:{ fontSize:12, lineHeight:'1.6' } }, line); })
                        ),
                        // Shipping
                        h('div', null,
                            h('div', { style:{ fontSize:11, fontWeight:600, color:TC.accent, marginBottom:S.xs, textTransform:'uppercase', letterSpacing:'.5px' } }, 'Shipping'),
                            fmtAddrObj(sa).map(function(line, i) { return h('div', { key:i, style:{ fontSize:12, lineHeight:'1.6' } }, line); })
                        )
                    ),

                    // ── Payment & Shipping info
                    h(Card, { cardBg, borderColor, dark, style:{ marginBottom:S.lg } },
                        h('h4', { style:{ fontSize:13, fontWeight:600, marginBottom:S.lg, color:mutedColor, textTransform:'uppercase', letterSpacing:'.5px' } }, 'Payment & Shipping'),
                        [
                            ['Payment', o.payment_method || '—'],
                            ['Shipping Method', o.shipping_method || '—'],
                            ['Currency', o.currency || 'USD'],
                            ['IP Address', o.ip_address || '—'],
                        ].map(function(pair) {
                            return h('div', { key:pair[0], style:{ display:'flex', justifyContent:'space-between', fontSize:12, marginBottom:S.sm } },
                                h('span', { style:{ color:mutedColor } }, pair[0]),
                                h('span', { style:{ fontWeight:500 } }, pair[1])
                            );
                        })
                    ),

                    // ── Customer note (if any)
                    o.customer_note && h(Card, { cardBg, borderColor, dark, style:{ marginBottom:S.lg } },
                        h('h4', { style:{ fontSize:13, fontWeight:600, marginBottom:S.sm, color:mutedColor, textTransform:'uppercase', letterSpacing:'.5px' } }, 'Customer Note'),
                        h('p', { style:{ fontSize:12, fontStyle:'italic', lineHeight:'1.6' } }, o.customer_note)
                    ),

                    // ── Referral source (if any) — S323 Essentials #8
                    o.referral_source && h(Card, { cardBg, borderColor, dark, style:{ marginBottom:S.lg } },
                        h('h4', { style:{ fontSize:13, fontWeight:600, marginBottom:S.sm, color:mutedColor, textTransform:'uppercase', letterSpacing:'.5px' } }, 'How They Heard About Us'),
                        h('p', { style:{ fontSize:13, fontWeight:600, lineHeight:'1.6' } }, o.referral_source)
                    ),

                    // ── Actions card
                    h(Card, { cardBg, borderColor, dark },
                        h('h4', { style:{ fontSize:13, fontWeight:600, marginBottom:S.lg, color:mutedColor, textTransform:'uppercase', letterSpacing:'.5px' } }, 'Actions'),
                        (o.status_handle !== 'cancelled' && o.status_handle !== 'refunded' && o.status_handle !== 'pending' && o.status_handle !== 'failed') && h(Btn, {
                            variant:'outline',
                            mutedColor,
                            borderColor,
                            onClick:refundOrder,
                            disabled:updStatus,
                            style:{ width:'100%', marginBottom:S.sm }
                        }, '↩ Refund Order'),
                        (o.status_handle !== 'cancelled' && o.status_handle !== 'refunded') && h('button', {
                            // S204 — Pattern #151 cascade: native confirm() → ConfirmModal.
                            // The actual cancel fires from the cCancel modal's onConfirm.
                            onClick: function(){ setCCancel(true); },
                            style:{ width:'100%', padding:'8px 14px', borderRadius:8, border:'1px solid #FCA5A5', background:dark?'rgba(220,38,38,.08)':'#FEF2F2', color:'#DC2626', fontSize:13, fontWeight:500, cursor:'pointer' }
                        }, '✕ Cancel Order')
                    )
                )
            ),
            showRefundModal && order && h(RefundModal, {
                order: order,
                orderId: orderId,
                onClose: function() { setShowRefundModal(false); },
                onSuccess: handleRefundSuccess,
                dark: dark,
                cardBg: cardBg,
                textColor: textColor,
                mutedColor: mutedColor,
                borderColor: borderColor
            }),

            /* S204 — Pattern #151 cascade close-out for OrderDetailPage. */
            h(ConfirmModal, {
                open: cCancel,
                title: 'Cancel this order?',
                detail: order ? ('Order #' + (order.id || orderId)) : null,
                message: 'The order will be moved to "Cancelled" status and the customer will be notified by email.',
                confirmLabel: 'Cancel order',
                cancelLabel: 'Keep order',
                busy: updStatus,
                busyLabel: 'Cancelling\u2026',
                onConfirm: function() {
                    setUpdStatus(true);
                    apiFetch('/orders/' + orderId + '/status', { method:'PATCH', body:{ status:'cancelled', notify:true, comment:'Order cancelled by admin.' } })
                        .then(function(){ setUpdStatus(false); setCCancel(false); loadOrder(); setMsg({ type:'success', text:'Order cancelled.' }); setTimeout(function(){ setMsg(null); },3000); })
                        .catch(function(){ setUpdStatus(false); setCCancel(false); });
                },
                onCancel: function(){ if (!updStatus) setCCancel(false); },
                dark: dark, cardBg: cardBg, borderColor: borderColor, textColor: textColor, mutedColor: mutedColor,
            })
        );
    }

    function OrdersPage({ subId, dark, cardBg, textColor, mutedColor, borderColor, tableHeaderBg }) {
        // ── Detail view when subId is present
        if (subId) {
            return h(OrderDetailPage, { orderId:Number(subId), dark, cardBg, textColor, mutedColor, borderColor, tableHeaderBg });
        }

        // ── List view
        const [page,       setPage]       = useState(1);
        const [status,     setStatus]     = useState('all');
        const [search,     setSearch]     = useState('');
        const [orders,     setOrders]     = useState([]);
        const [total,      setTotal]      = useState(0);
        const [totalPages, setTotalPages] = useState(1);
        const [loading,    setLoading]    = useState(true);
        const [statuses,   setStatuses]   = useState([]);

        function load() {
            setLoading(true);
            const q = new URLSearchParams({ page, per_page:TC_PER_PAGE, search, status: status==='all'?'':status });
            apiFetch('/orders?' + q).then(function(d) {
                const rows = Array.isArray(d) ? d : (d.orders || d.items || []);
                setOrders(rows);
                setTotal(d.total || rows.length);
                setTotalPages(d.total_pages || d.pages || 1);
                setLoading(false);
            }).catch(function(){ setLoading(false); });
        }

        function loadStatuses() {
            apiFetch('/orders/statuses').then(function(d) {
                setStatuses(Array.isArray(d) ? d : []);
            }).catch(function(){});
        }

        useEffect(function(){ load(); loadStatuses(); }, [page, status, search]);

        function parseAddr(raw) {
            if (!raw) return {};
            if (typeof raw === 'object') return raw;
            try { return JSON.parse(raw); } catch(e) { return {}; }
        }

        // S120: each row's currency comes from its own o.currency column.
        // Pass the row's order object as the 2nd arg so historical orders
        // render in the currency they were placed in.
        function fmtMoney(v, ord) { return tcFmtMoney(v, ord && ord.currency); }

        // Build status tabs from DB statuses (with "All" prepended)
        var statusTabs = [{ v:'all', l:'All', color:null }];
        statuses.forEach(function(s) {
            statusTabs.push({ v: s.handle, l: s.label || s.handle, color: s.color });
        });

        var colGrid = '1.2fr 2fr 1fr 1fr 1fr 0.3fr';

        return h(PageWrap, { title:'Sales', sub:total + ' orders', mutedColor },
            // Status filter tabs
            h('div', { style:{ display:'flex', gap:S.xs, marginBottom:S.lg, flexWrap:'wrap' } },
                statusTabs.map(function(t) {
                    var isActive = status === t.v;
                    return h('button', { key:t.v, onClick:function(){ setStatus(t.v); setPage(1); },
                        style:{ padding:'6px '+S.md+'px', borderRadius:8, border:'none', cursor:'pointer', fontSize:12, fontWeight:isActive?600:400,
                                background:isActive?TC.accent:'transparent', color:isActive?'#fff':mutedColor, transition:'all .15s' } }, t.l);
                })
            ),
            // Search
            h('div', { style:{ marginBottom:S.lg } },
                h(SearchBox, { value:search, onChange:function(v){ setSearch(v); setPage(1); }, placeholder:'Search by order # or customer…', borderColor, dark, mutedColor })
            ),
            // Table
            h(Card, { cardBg, borderColor, dark, pad:0 },
                h('div', { style:{ display:'grid', gridTemplateColumns:colGrid, padding:S.md+'px '+S.xl+'px', borderBottom:'1px solid '+borderColor, background:tableHeaderBg, boxShadow:tableHeaderShadow } },
                    ['Order','Customer','Date','Total','Status',''].map(function(col) {
                        return h('span', { key:col, style:{ fontSize:10, fontWeight:500, color:mutedColor, textTransform:'uppercase', letterSpacing:'.5px' } }, col);
                    })
                ),
                loading && h(Spinner, { mutedColor }),
                !loading && orders.length===0 && h(EmptyState, { mutedColor, icon:'🧾', title:'No orders yet', sub:'Orders placed by customers will appear here.' }),
                !loading && orders.map(function(o, i) {
                    var oid = o.order_number || o.id;
                    var statusColor = o.status_color || '#71797E';
                    var statusLabel = o.status_label || o.status_handle || o.status || '—';
                    var addr = parseAddr(o.billing_address);
                    var actualName = [addr.first_name, addr.last_name].filter(Boolean).join(' ');
                    var name = actualName || o.billing_email || o.email || '—';
                    var emailSub = actualName ? (o.billing_email || o.email || '') : '';
                    return h('div', { key:oid,
                        style:{ display:'grid', gridTemplateColumns:colGrid, padding:'12px '+S.xl+'px', borderBottom: i<orders.length-1 ? '1px solid '+borderColor : 'none', alignItems:'center', cursor:'pointer', transition:'background .12s' },
                        onMouseEnter:function(e){ e.currentTarget.style.background=dark?'rgba(255,255,255,.02)':'#F9FAFB'; },
                        onMouseLeave:function(e){ e.currentTarget.style.background='transparent'; },
                        onClick:function(){ navigate('orders/' + (o.id)); }
                    },
                        h('span', { style:{ fontSize:12, fontWeight:500, color:TC.accent } }, oid),
                        h('div', null,
                            h('div', { style:{ fontSize:12, fontWeight:500 } }, name),
                            emailSub && h('div', { style:{ fontSize:11, color:mutedColor } }, emailSub)
                        ),
                        h('span', { style:{ fontSize:11, color:mutedColor } }, tcFmtDate(o.created_at)),
                        h('span', { style:{ fontSize:12, fontWeight:600 } }, o.total!=null ? fmtMoney(o.total, o) : '—'),
                        h('span', { style:{ fontSize:11, fontWeight:500, color:statusColor, background:statusColor+'18', padding:'2px 8px', borderRadius:20, display:'inline-block', width:'fit-content' } }, statusLabel),
                        h('span', { style:{ fontSize:11, color:TC.accent, textAlign:'right' } }, '→')
                    );
                }),
                h('div', { style:{ display:'flex', justifyContent:'space-between', alignItems:'center', padding:S.md+'px '+S.xl+'px', borderTop:'1px solid '+borderColor } },
                    h('span', { style:{ fontSize:11, color:mutedColor } }, 'Showing ' + orders.length + ' of ' + total),
                    h(Pager, { page, totalPages, onPage:setPage, mutedColor, borderColor })
                )
            )
        );
    }

    /* ════════════════════════════════
     * CUSTOMERS PAGE (list + editor)
     * ════════════════════════════════ */
    function CustomersPage({ subId, dark, cardBg, textColor, mutedColor, borderColor, tableHeaderBg }) {
        if (subId === 'add' || (subId && subId !== 'add')) return h(CustomerEditor, { id: subId === 'add' ? 0 : Number(subId), dark, cardBg, textColor, mutedColor, borderColor });
        // ── List ──
        var _pg = useState(1),           page = _pg[0],      setPage = _pg[1];
        var _sr = useState(''),          search = _sr[0],    setSearch = _sr[1];
        var _ph = useState(''),          phone = _ph[0],     setPhone = _ph[1];
        var _em = useState(''),          email = _em[0],     setEmail = _em[1];
        var _st = useState(''),          status = _st[0],    setStatus = _st[1];
        var _cs = useState([]),          customers = _cs[0], setCust = _cs[1];
        var _tt = useState(0),           total = _tt[0],     setTotal = _tt[1];
        var _tp = useState(1),           totalPages = _tp[0],setTotalPages = _tp[1];
        var _ld = useState(true),        loading = _ld[0],   setLoading = _ld[1];
        var fs = fieldSty(dark, borderColor);
        var labelSty = { fontSize:12, fontWeight:500, display:'block', marginBottom:S.xs, color:mutedColor };

        function load() {
            setLoading(true);
            var q = new URLSearchParams({ page:page, per_page:TC_PER_PAGE });
            var s = [search, phone, email].filter(Boolean).join(' ');
            if (s) q.set('search', s);
            if (status) q.set('status', status);
            apiFetch('/customers?' + q.toString()).then(function(d) {
                var rows = d.customers || [];
                setCust(rows); setTotal(d.total || 0); setTotalPages(d.total_pages || 1); setLoading(false);
            }).catch(function(){ setLoading(false); });
        }
        useEffect(load, [page, search, phone, email, status]);

        function doSearch(e) { if (e) e.preventDefault(); setPage(1); load(); }

        function deleteCustomer(c) {
            if (!confirm('Delete "' + (c.first_name||'') + ' ' + (c.last_name||'') + '"?')) return;
            apiFetch('/customers/' + c.id, { method:'DELETE' }).then(load);
        }

        function statusBadge(st) {
            var s = (st||'active').toLowerCase();
            var map = { active:{ bg:'#ECFDF5', color:'#059669', label:'Active' }, disabled:{ bg:'#FEF3C7', color:'#D97706', label:'Disabled' }, banned:{ bg:'#FEF2F2', color:'#DC2626', label:'Banned' } };
            var c = map[s] || map.active;
            return h('span', { style:{ fontSize:11, fontWeight:500, color:c.color, background:dark?'transparent':c.bg, border: dark? '1px solid '+c.color : 'none', padding:'2px 8px', borderRadius:20, width:'fit-content', display:'inline-block' } }, c.label);
        }

        var colGrid = '1.2fr 1.5fr 0.8fr 0.7fr 0.7fr 0.8fr 0.6fr';
        return h(PageWrap, { title:'Customers', sub: total + ' customers', mutedColor, action:
            h(Btn, { variant:'primary', onClick:function(){ navigate('customers/add'); } }, '+ Add Customer')
        },
            h('div', { style:{ display:'grid', gridTemplateColumns:'1fr 280px', gap:S.lg } },
                /* ── Main table ── */
                h(Card, { cardBg, borderColor, dark, pad:0 },
                    h('div', { style:{ display:'grid', gridTemplateColumns:colGrid, padding:S.md + 'px ' + S.xl + 'px', borderBottom:'1px solid ' + borderColor, background:tableHeaderBg, boxShadow:tableHeaderShadow } },
                        ['Name','Email','Phone','Orders','Spent','Registered','Status'].map(function(col) {
                            return h('span', { key:col, style:{ fontSize:10, fontWeight:500, color:mutedColor, textTransform:'uppercase', letterSpacing:'.5px' } }, col);
                        })
                    ),
                    loading && h(Spinner, { mutedColor:mutedColor }),
                    !loading && customers.length === 0 && h(EmptyState, { mutedColor:mutedColor, icon:'👥', title:'No customers yet', sub:'Customers will appear here once they register or place orders.' }),
                    !loading && customers.map(function(c, i) {
                        var fullName = ((c.first_name||'') + ' ' + (c.last_name||'')).trim() || c.email || '(Guest)';
                        return h('div', { key:c.id||i,
                            style:{ display:'grid', gridTemplateColumns:colGrid, padding:'12px ' + S.xl + 'px', borderBottom: i < customers.length-1 ? '1px solid ' + borderColor : 'none', alignItems:'center', cursor:'pointer' },
                            onClick:function(){ navigate('customers/' + c.id); },
                            onMouseEnter:function(e){ e.currentTarget.style.background = dark ? 'rgba(255,255,255,.02)' : '#F9FAFB'; },
                            onMouseLeave:function(e){ e.currentTarget.style.background = 'transparent'; } },
                            h('div', { style:{ display:'flex', alignItems:'center', gap:10 } },
                                h('div', { style:{ width:32, height:32, borderRadius:'50%', background:dark?'#333':'#E5E7EB', display:'flex', alignItems:'center', justifyContent:'center', fontSize:12, fontWeight:600, color:dark?'#aaa':'#6B7280', flexShrink:0 } },
                                    ((c.first_name||'?')[0] + (c.last_name||'')[0]).toUpperCase()
                                ),
                                h('span', { style:{ fontSize:13, fontWeight:500 } }, fullName)
                            ),
                            h('span', { style:{ fontSize:12, color:mutedColor } }, c.email || '—'),
                            h('span', { style:{ fontSize:12, color:mutedColor } }, c.phone || '—'),
                            h('span', { style:{ fontSize:12, fontWeight:500 } }, c.total_orders || 0),
                            h('span', { style:{ fontSize:12, fontWeight:500 } }, c.total_spent != null ? '$' + parseFloat(c.total_spent).toFixed(2) : '$0.00'),
                            h('span', { style:{ fontSize:11, color:mutedColor } }, tcFmtDate(c.created_at)),
                            statusBadge(c.status)
                        );
                    }),
                    h('div', { style:{ display:'flex', justifyContent:'space-between', alignItems:'center', padding:S.md + 'px ' + S.xl + 'px', borderTop:'1px solid ' + borderColor } },
                        h('span', { style:{ fontSize:11, color:mutedColor } }, 'Showing ' + customers.length + ' of ' + total),
                        h(Pager, { page:page, totalPages:totalPages, onPage:setPage, mutedColor:mutedColor, borderColor:borderColor })
                    )
                ),
                /* ── Search sidebar ── */
                h(Card, { cardBg, borderColor, dark, pad:S.lg, style:{ alignSelf:'flex-start' } },
                    h('h4', { style:{ fontSize:14, fontWeight:600, marginBottom:S.md } }, 'Search'),
                    h('div', { style:{ marginBottom:S.md } },
                        h('label', { style:labelSty }, 'Name'),
                        h('input', { value:search, onChange:function(e){ setSearch(e.target.value); }, style:fs, placeholder:'First or last name' })
                    ),
                    h('div', { style:{ marginBottom:S.md } },
                        h('label', { style:labelSty }, 'Phone'),
                        h('input', { value:phone, onChange:function(e){ setPhone(e.target.value); }, style:fs })
                    ),
                    h('div', { style:{ marginBottom:S.md } },
                        h('label', { style:labelSty }, 'E-mail'),
                        h('input', { value:email, onChange:function(e){ setEmail(e.target.value); }, style:fs })
                    ),
                    h('div', { style:{ marginBottom:S.md } },
                        h('label', { style:labelSty }, 'Status'),
                        h('select', { value:status, onChange:function(e){ setStatus(e.target.value); setPage(1); }, style:fs },
                            h('option', { value:'' }, 'All'),
                            h('option', { value:'active' }, 'Active'),
                            h('option', { value:'disabled' }, 'Disabled'),
                            h('option', { value:'banned' }, 'Banned')
                        )
                    ),
                    h(Btn, { variant:'primary', onClick:doSearch, style:{ width:'100%', justifyContent:'center' } }, 'Search')
                )
            )
        );
    }

    /* ── Customer Editor (add / edit) ── */
    function CustomerEditor({ id, dark, cardBg, textColor, mutedColor, borderColor }) {
        var isNew = !id || id === 0;
        var _fm = useState({ email:'', password:'', confirm_password:'', first_name:'', last_name:'', phone:'', date_of_birth:'', status:'active', tax_exempt:false, language:'en', notes:'',
            address:{ type:'shipping', address_1:'', address_2:'', city:'', state:'', country:'US', postcode:'', company:'', phone:'' } });
        var form = _fm[0], setForm = _fm[1];
        var _sv = useState(false), saving = _sv[0], setSaving = _sv[1];
        var _ld = useState(!isNew),  loading = _ld[0], setLoading = _ld[1];
        var _mg = useState(''),      msg = _mg[0], setMsg = _mg[1];
        var _er = useState(''),      err = _er[0], setErr = _er[1];
        var _orders = useState([]),  orders = _orders[0], setOrders = _orders[1];
        var _dl = useState(false),   deleting = _dl[0], setDeleting = _dl[1];
        var _cf = useState([]),      customFields = _cf[0], setCustomFields = _cf[1];
        var fs = fieldSty(dark, borderColor);
        var labelSty = { fontSize:12, fontWeight:500, display:'block', marginBottom:S.xs, color:mutedColor };
        var sectionHead = function(title) { return h('h3', { style:{ fontSize:15, fontWeight:600, marginBottom:S.md, paddingBottom:S.sm, borderBottom:'1px solid '+borderColor } }, title); };

        useEffect(function() {
            if (isNew) return;
            apiFetch('/customers/' + id).then(function(c) {
                if (c.error) { setErr('Customer not found'); setLoading(false); return; }
                var addr = (c.addresses && c.addresses[0]) || {};
                setForm({
                    email: c.email||'', password:'', confirm_password:'', first_name: c.first_name||'', last_name: c.last_name||'',
                    phone: c.phone||'', status: c.status||'active', tax_exempt: !!c.tax_exempt, language: c.language||'en', notes: c.notes||'',
                    date_of_birth: c.date_of_birth||'',
                    address:{ id: addr.id||0, type: addr.type||'shipping', address_1: addr.address_1||'', address_2: addr.address_2||'',
                        city: addr.city||'', state: addr.state||'', country: addr.country||'US', postcode: addr.postcode||'', company: addr.company||'', phone: addr.phone||'' }
                });
                setOrders(c.recent_orders || []);
                setCustomFields(Array.isArray(c.custom_fields) ? c.custom_fields : []);
                setLoading(false);
            }).catch(function(){ setErr('Failed to load customer'); setLoading(false); });
        }, [id]);

        function setField(k, v) { setForm(function(p){ return Object.assign({}, p, (function(){ var o={}; o[k]=v; return o; })()); }); }
        function setAddr(k, v) {
            setForm(function(p) {
                var a = Object.assign({}, p.address); a[k] = v;
                return Object.assign({}, p, { address: a });
            });
        }
        function setCustomFieldValue(fieldId, v) {
            setCustomFields(function(prev) {
                return prev.map(function(p) {
                    return ((p.field && parseInt(p.field.id, 10)) === parseInt(fieldId, 10))
                        ? Object.assign({}, p, { value: v })
                        : p;
                });
            });
        }

        function save() {
            setMsg(''); setErr('');
            if (!form.email) { setErr('Email is required.'); return; }
            if (isNew && !form.password) { setErr('Password is required for new customers.'); return; }
            if (form.password && form.password !== form.confirm_password) { setErr('Passwords do not match.'); return; }
            setSaving(true);
            var body = { email:form.email, first_name:form.first_name, last_name:form.last_name, phone:form.phone,
                status:form.status, tax_exempt:form.tax_exempt, language:form.language, notes:form.notes, date_of_birth:form.date_of_birth };
            if (form.password) body.password = form.password;
            if (form.address.address_1) body.address = form.address;
            // Pack custom field values as { <field_id>: value, ... } for contact section.
            // Empty strings still UPSERT so admin can clear a previously-set Tax ID, etc.
            if (customFields.length > 0) {
                var cfMap = {};
                customFields.forEach(function(p) {
                    if (p && p.field && p.field.id) {
                        cfMap[parseInt(p.field.id, 10)] = (p.value == null) ? '' : String(p.value);
                    }
                });
                body.custom_fields = cfMap;
            }
            var req = isNew
                ? apiFetch('/customers', { method:'POST', body:body })
                : apiFetch('/customers/' + id, { method:'PUT', body:body });
            req.then(function(d) {
                setSaving(false);
                if (d.error) { setErr(d.error); return; }
                setMsg('Saved successfully.');
                if (isNew && d.id) navigate('customers/' + d.id);
            }).catch(function(e){ setSaving(false); setErr(e.message || 'Save failed.'); });
        }

        function doDelete() {
            if (!id || isNew || deleting) return;
            var label = (form.first_name || form.last_name)
                ? (form.first_name + ' ' + form.last_name).trim()
                : (form.email || 'this customer');
            if (!confirm('Permanently delete ' + label + '?\n\nThis removes the customer record, addresses, custom field values, wishlist, cart, AND the associated WordPress user account. This cannot be undone.')) return;
            setDeleting(true); setErr(''); setMsg('');
            apiFetch('/customers/' + id, { method:'DELETE' }).then(function(d) {
                setDeleting(false);
                if (d && d.error) { setErr(d.error); return; }
                navigate('customers');
            }).catch(function(e) {
                setDeleting(false);
                setErr((e && e.message) || 'Delete failed.');
            });
        }

        if (loading) return h(PageWrap, { title:'Loading…', mutedColor:mutedColor }, h(Spinner, { mutedColor:mutedColor }));
        if (err && !form.email && !isNew) return h(PageWrap, { title:'Error', mutedColor:mutedColor, backBtn:function(){ navigate('customers'); } }, h('p', { style:{ color:TC.error } }, err));

        var title = isNew ? 'New Customer' : (form.first_name + ' ' + form.last_name).trim() || form.email;
        return h(PageWrap, { title:title, mutedColor:mutedColor, backBtn:function(){ navigate('customers'); }, action:
            h(Btn, { variant:'primary', onClick:save, disabled:saving }, saving ? 'Saving…' : 'Save')
        },
            msg && h('div', { style:{ padding:'10px 16px', background:'#ECFDF5', color:'#059669', borderRadius:8, marginBottom:S.md, fontSize:13 } }, msg),
            err && h('div', { style:{ padding:'10px 16px', background:'#FEF2F2', color:'#DC2626', borderRadius:8, marginBottom:S.md, fontSize:13 } }, err),

            h('div', { style:{ display:'grid', gridTemplateColumns:'1fr 1fr', gap:S.lg } },
                /* ── Left column ── */
                h('div', null,
                    /* Account info */
                    h(Card, { cardBg:cardBg, borderColor:borderColor, dark:dark, style:{ marginBottom:S.lg } },
                        sectionHead('Account Information'),
                        h('div', { style:{ marginBottom:S.md } },
                            h('label', { style:labelSty }, 'E-mail *'),
                            h('input', { type:'email', value:form.email, onChange:function(e){ setField('email', e.target.value); }, style:fs })
                        ),
                        isNew && h('div', { style:{ display:'grid', gridTemplateColumns:'1fr 1fr', gap:S.md, marginBottom:S.md } },
                            h('div', null,
                                h('label', { style:labelSty }, 'Password *'),
                                h('input', { type:'password', value:form.password, onChange:function(e){ setField('password', e.target.value); }, style:fs })
                            ),
                            h('div', null,
                                h('label', { style:labelSty }, 'Confirm Password *'),
                                h('input', { type:'password', value:form.confirm_password, onChange:function(e){ setField('confirm_password', e.target.value); }, style:fs })
                            )
                        ),
                        !isNew && h('div', { style:{ display:'grid', gridTemplateColumns:'1fr 1fr', gap:S.md, marginBottom:S.md } },
                            h('div', null,
                                h('label', { style:labelSty }, 'New Password'),
                                h('input', { type:'password', value:form.password, onChange:function(e){ setField('password', e.target.value); }, style:fs, placeholder:'Leave blank to keep current' })
                            ),
                            h('div', null,
                                h('label', { style:labelSty }, 'Confirm Password'),
                                h('input', { type:'password', value:form.confirm_password, onChange:function(e){ setField('confirm_password', e.target.value); }, style:fs })
                            )
                        ),
                        h('div', { style:{ display:'grid', gridTemplateColumns:'1fr 1fr', gap:S.md, marginBottom:S.md } },
                            h('div', null,
                                h('label', { style:labelSty }, 'Status'),
                                h('div', { style:{ display:'flex', gap:S.lg, marginTop:4 } },
                                    ['active','disabled','banned'].map(function(s) {
                                        return h('label', { key:s, style:{ display:'flex', alignItems:'center', gap:6, fontSize:13, cursor:'pointer' } },
                                            h('input', { type:'radio', name:'status', value:s, checked:form.status===s, onChange:function(){ setField('status',s); } }),
                                            s.charAt(0).toUpperCase() + s.slice(1)
                                        );
                                    })
                                )
                            ),
                            h('div', null,
                                h('label', { style:labelSty }, 'Language'),
                                h('select', { value:form.language, onChange:function(e){ setField('language', e.target.value); }, style:fs },
                                    h('option', { value:'en' }, 'English')
                                )
                            )
                        ),
                        h('label', { style:{ display:'flex', alignItems:'center', gap:8, fontSize:13, cursor:'pointer' } },
                            h('input', { type:'checkbox', checked:form.tax_exempt, onChange:function(e){ setField('tax_exempt', e.target.checked); } }),
                            'Tax exempt'
                        )
                    ),
                    /* Contact info */
                    h(Card, { cardBg:cardBg, borderColor:borderColor, dark:dark },
                        sectionHead('Contact Information'),
                        h('div', { style:{ display:'grid', gridTemplateColumns:'1fr 1fr', gap:S.md, marginBottom:S.md } },
                            h('div', null,
                                h('label', { style:labelSty }, 'First Name'),
                                h('input', { value:form.first_name, onChange:function(e){ setField('first_name', e.target.value); }, style:fs })
                            ),
                            h('div', null,
                                h('label', { style:labelSty }, 'Last Name'),
                                h('input', { value:form.last_name, onChange:function(e){ setField('last_name', e.target.value); }, style:fs })
                            )
                        ),
                        h('div', { style:{ display:'grid', gridTemplateColumns:'1fr 1fr', gap:S.md, marginBottom:S.md } },
                            h('div', null,
                                h('label', { style:labelSty }, 'Phone'),
                                h('input', { type:'tel', value:form.phone, onChange:function(e){ setField('phone', e.target.value); }, style:fs })
                            ),
                            h('div', null,
                                h('label', { style:labelSty }, 'Date of Birth'),
                                h('input', { type:'date', value:form.date_of_birth, max:(new Date()).toISOString().slice(0,10), onChange:function(e){ setField('date_of_birth', e.target.value); }, style:fs })
                            )
                        )
                    ),
                    /* Custom contact fields — admin counterpart to storefront /my-account/profile rendering.
                       Renders only when (a) editing an existing customer (isNew has no field values yet)
                       AND (b) at least one custom field is configured + visible (NATIVE_FIELD_NAMES filter
                       applied server-side in CustomerFieldsManager::get_visible_custom_fields). */
                    !isNew && customFields.length > 0 && h(Card, { cardBg:cardBg, borderColor:borderColor, dark:dark, style:{ marginTop:S.lg } },
                        sectionHead('Custom Fields'),
                        customFields.map(function(pair, idx) {
                            var f = pair.field || {};
                            var fid = parseInt(f.id, 10);
                            var lbl = String(f.description || f.field_name || ('Field ' + fid));
                            var ftype = String(f.field_type || 'input');
                            var required = parseInt(f.is_required, 10) === 1;
                            var val = pair.value == null ? '' : String(pair.value);
                            var inputEl;
                            if (ftype === 'textarea') {
                                inputEl = h('textarea', { value:val, onChange:function(e){ setCustomFieldValue(fid, e.target.value); }, rows:3, style:Object.assign({}, fs, { resize:'vertical' }) });
                            } else if (ftype === 'checkbox') {
                                inputEl = h('label', { style:{ display:'flex', alignItems:'center', gap:8, fontSize:13, cursor:'pointer' } },
                                    h('input', { type:'checkbox', checked:val === '1' || val === 1 || val === true, onChange:function(e){ setCustomFieldValue(fid, e.target.checked ? '1' : '0'); } }),
                                    'Yes'
                                );
                            } else {
                                var inputType = (ftype === 'email') ? 'email' : (ftype === 'phone' ? 'tel' : 'text');
                                inputEl = h('input', { type:inputType, value:val, onChange:function(e){ setCustomFieldValue(fid, e.target.value); }, style:fs });
                            }
                            return h('div', { key:fid, style:{ marginBottom: idx < customFields.length - 1 ? S.md : 0 } },
                                h('label', { style:labelSty }, lbl + (required ? ' *' : '')),
                                inputEl
                            );
                        })
                    )
                ),
                /* ── Right column ── */
                h('div', null,
                    /* Shipping address */
                    h(Card, { cardBg:cardBg, borderColor:borderColor, dark:dark, style:{ marginBottom:S.lg } },
                        sectionHead('Shipping Address'),
                        h('div', { style:{ marginBottom:S.md } },
                            h('label', { style:labelSty }, 'Address'),
                            h('input', { value:form.address.address_1, onChange:function(e){ setAddr('address_1', e.target.value); }, style:fs })
                        ),
                        h('div', { style:{ marginBottom:S.md } },
                            h('label', { style:labelSty }, 'Address 2'),
                            h('input', { value:form.address.address_2, onChange:function(e){ setAddr('address_2', e.target.value); }, style:fs })
                        ),
                        h('div', { style:{ display:'grid', gridTemplateColumns:'1fr 1fr', gap:S.md, marginBottom:S.md } },
                            h('div', null,
                                h('label', { style:labelSty }, 'City'),
                                h('input', { value:form.address.city, onChange:function(e){ setAddr('city', e.target.value); }, style:fs })
                            ),
                            h('div', null,
                                h('label', { style:labelSty }, 'State / Province'),
                                h('input', { value:form.address.state, onChange:function(e){ setAddr('state', e.target.value); }, style:fs })
                            )
                        ),
                        h('div', { style:{ display:'grid', gridTemplateColumns:'1fr 1fr', gap:S.md, marginBottom:S.md } },
                            h('div', null,
                                h('label', { style:labelSty }, 'Country'),
                                h('select', { value:form.address.country, onChange:function(e){ setAddr('country', e.target.value); }, style:fs },
                                    h('option', { value:'US' }, 'United States'),
                                    h('option', { value:'CA' }, 'Canada'),
                                    h('option', { value:'GB' }, 'United Kingdom'),
                                    h('option', { value:'AU' }, 'Australia'),
                                    h('option', { value:'DE' }, 'Germany'),
                                    h('option', { value:'FR' }, 'France'),
                                    h('option', { value:'IN' }, 'India'),
                                    h('option', { value:'JP' }, 'Japan')
                                )
                            ),
                            h('div', null,
                                h('label', { style:labelSty }, 'Zip / Postal Code'),
                                h('input', { value:form.address.postcode, onChange:function(e){ setAddr('postcode', e.target.value); }, style:fs })
                            )
                        ),
                        h('div', { style:{ marginBottom:S.md } },
                            h('label', { style:labelSty }, 'Company'),
                            h('input', { value:form.address.company, onChange:function(e){ setAddr('company', e.target.value); }, style:fs })
                        )
                    ),
                    /* Admin notes */
                    h(Card, { cardBg:cardBg, borderColor:borderColor, dark:dark, style:{ marginBottom:S.lg } },
                        sectionHead('Admin Notes'),
                        h('textarea', { value:form.notes, onChange:function(e){ setField('notes', e.target.value); }, rows:3, style:Object.assign({}, fs, { resize:'vertical' }), placeholder:'Internal notes about this customer…' })
                    ),
                    /* Recent orders (edit only) */
                    !isNew && orders.length > 0 && h(Card, { cardBg:cardBg, borderColor:borderColor, dark:dark },
                        sectionHead('Recent Orders'),
                        orders.map(function(o, i) {
                            return h('div', { key:o.id, style:{ display:'flex', justifyContent:'space-between', alignItems:'center', padding:'8px 0', borderBottom: i < orders.length-1 ? '1px solid '+borderColor : 'none', fontSize:12 } },
                                h('span', { style:{ fontWeight:500, color:TC.accent, cursor:'pointer' }, onClick:function(){ navigate('orders/' + o.id); } }, '#' + (o.order_number||o.id)),
                                h('span', { style:{ color:mutedColor } }, tcFmtDate(o.created_at)),
                                h('span', { style:{ fontWeight:500 } }, '$' + parseFloat(o.total||0).toFixed(2)),
                                h('span', { style:{ fontSize:11, textTransform:'capitalize', color:mutedColor } }, o.status)
                            );
                        })
                    )
                )
            ),
            /* Danger zone — full-width card below the two-column form, edit only.
               Uses the same delete-customer endpoint that cascades to addresses,
               field_values, wishlist, cart, and removes the WP user. */
            !isNew && h(Card, { cardBg:cardBg, borderColor:'#FCA5A5', dark:dark, style:{ marginTop:S.lg, background:dark ? 'rgba(220,38,38,0.06)' : '#FEF2F2' } },
                h('h3', { style:{ fontSize:14, fontWeight:600, color:'#DC2626', marginBottom:S.sm } }, 'Delete this customer'),
                h('p', { style:{ fontSize:12, color:mutedColor, marginBottom:S.md, lineHeight:1.5 } },
                    'Permanently removes the customer record, addresses, custom field values, wishlist, cart, and the associated WordPress user account. Order history is preserved with anonymized references. This cannot be undone.'
                ),
                h(Btn, { variant:'danger', onClick:doDelete, disabled:deleting }, deleting ? 'Deleting…' : 'Delete Customer')
            )
        );
    }

    /* ════════════════════════════════
     * CUSTOMER GROUPS PAGE
     * ════════════════════════════════ */
    function CustomerGroupsPage({ dark, cardBg, textColor, mutedColor, borderColor, tableHeaderBg }) {
        var _gp = useState([]),      groups = _gp[0],    setGroups = _gp[1];
        var _ld = useState(true),    loading = _ld[0],   setLoading = _ld[1];
        var _sh = useState(false),   showForm = _sh[0],  setShowForm = _sh[1];
        var _ef = useState(null),    editing = _ef[0],   setEditing = _ef[1];
        var _nm = useState(''),      name = _nm[0],      setName = _nm[1];
        var _st = useState('active'),gStatus = _st[0],   setGStatus = _st[1];
        var _sv = useState(false),   saving = _sv[0],    setSaving = _sv[1];
        var fs = fieldSty(dark, borderColor);
        var labelSty = { fontSize:12, fontWeight:500, display:'block', marginBottom:S.xs, color:mutedColor };

        function load() {
            setLoading(true);
            apiFetch('/customer-groups').then(function(d) {
                setGroups(Array.isArray(d) ? d : []); setLoading(false);
            }).catch(function(){ setLoading(false); });
        }
        useEffect(load, []);

        function openAdd() { setEditing(null); setName(''); setGStatus('active'); setShowForm(true); }
        function openEdit(g) { setEditing(g); setName(g.name||''); setGStatus(g.status||'active'); setShowForm(true); }
        function closeForm() { setShowForm(false); setEditing(null); }

        function saveGroup() {
            if (!name.trim()) return;
            setSaving(true);
            var req = editing
                ? apiFetch('/customer-groups/' + editing.id, { method:'PUT', body:{ name:name, status:gStatus } })
                : apiFetch('/customer-groups', { method:'POST', body:{ name:name, status:gStatus } });
            req.then(function(){ setSaving(false); closeForm(); load(); }).catch(function(){ setSaving(false); });
        }

        function deleteGroup(g) {
            if (!confirm('Delete group "' + g.name + '"?')) return;
            apiFetch('/customer-groups/' + g.id, { method:'DELETE' }).then(load);
        }

        function toggleStatus(g) {
            var newSt = g.status === 'active' ? 'disabled' : 'active';
            apiFetch('/customer-groups/' + g.id, { method:'PUT', body:{ status:newSt } }).then(load);
        }

        function statusBadge(st) {
            var isActive = (st||'active') === 'active';
            return h('span', { style:{ fontSize:11, fontWeight:500, color: isActive ? '#059669' : '#9CA3AF', cursor:'pointer', display:'inline-flex', alignItems:'center', gap:4 } },
                h('span', { style:{ width:8, height:8, borderRadius:'50%', background: isActive ? '#059669' : '#9CA3AF' } }),
                isActive ? 'Active' : 'Disabled'
            );
        }

        var colGrid = '2fr 1fr 1.2fr 0.7fr';
        return h(PageWrap, { title:'Customer Groups', mutedColor:mutedColor, action:
            h(Btn, { variant:'primary', onClick:openAdd }, '+ New Customer Group')
        },
            /* Inline modal for add/edit */
            showForm && h('div', { style:{ position:'fixed', inset:0, zIndex:9999, background:'rgba(0,0,0,0.4)', display:'flex', alignItems:'center', justifyContent:'center' } },
                h('div', { style:{ background:cardBg, borderRadius:12, padding:S.xl, width:420, maxWidth:'90%', border:'1px solid '+borderColor } },
                    h('h3', { style:{ fontSize:16, fontWeight:600, marginBottom:S.lg } }, editing ? 'Edit Customer Group' : 'New Customer Group'),
                    h('div', { style:{ marginBottom:S.md } },
                        h('label', { style:labelSty }, 'Group Name *'),
                        h('input', { value:name, onChange:function(e){ setName(e.target.value); }, style:fs, autoFocus:true })
                    ),
                    h('div', { style:{ marginBottom:S.lg } },
                        h('label', { style:labelSty }, 'Status'),
                        h('div', { style:{ display:'flex', gap:S.lg, marginTop:4 } },
                            ['active','disabled'].map(function(s) {
                                return h('label', { key:s, style:{ display:'flex', alignItems:'center', gap:6, fontSize:13, cursor:'pointer' } },
                                    h('input', { type:'radio', name:'group_status', value:s, checked:gStatus===s, onChange:function(){ setGStatus(s); } }),
                                    s.charAt(0).toUpperCase() + s.slice(1)
                                );
                            })
                        )
                    ),
                    h('div', { style:{ display:'flex', gap:S.sm, justifyContent:'flex-end' } },
                        h(Btn, { variant:'ghost', onClick:closeForm }, 'Cancel'),
                        h(Btn, { variant:'primary', onClick:saveGroup, disabled:saving }, saving ? 'Saving…' : 'Save')
                    )
                )
            ),
            h(Card, { cardBg:cardBg, borderColor:borderColor, dark:dark, pad:0 },
                h('div', { style:{ display:'grid', gridTemplateColumns:colGrid, padding:S.md + 'px ' + S.xl + 'px', borderBottom:'1px solid ' + borderColor, background:tableHeaderBg, boxShadow:tableHeaderShadow } },
                    ['Customer Group','Type','Status',''].map(function(col) {
                        return h('span', { key:col||'actions', style:{ fontSize:10, fontWeight:500, color:mutedColor, textTransform:'uppercase', letterSpacing:'.5px' } }, col);
                    })
                ),
                loading && h(Spinner, { mutedColor:mutedColor }),
                !loading && groups.length === 0 && h(EmptyState, { mutedColor:mutedColor, icon:'👥', title:'No customer groups', sub:'Create groups to offer special pricing or access.' }),
                !loading && groups.map(function(g, i) {
                    return h('div', { key:g.id,
                        style:{ display:'grid', gridTemplateColumns:colGrid, padding:'12px ' + S.xl + 'px', borderBottom: i < groups.length-1 ? '1px solid ' + borderColor : 'none', alignItems:'center' },
                        onMouseEnter:function(e){ e.currentTarget.style.background = dark ? 'rgba(255,255,255,.02)' : '#F9FAFB'; },
                        onMouseLeave:function(e){ e.currentTarget.style.background = 'transparent'; } },
                        h('span', { style:{ fontSize:13, fontWeight:500, cursor:'pointer' }, onClick:function(){ openEdit(g); } }, g.name),
                        h('span', { style:{ fontSize:12, color:mutedColor } }, g.type || 'Customer'),
                        h('span', { onClick:function(){ toggleStatus(g); }, style:{ cursor:'pointer' } }, statusBadge(g.status)),
                        h('div', { style:{ textAlign:'right' } },
                            h('button', { onClick:function(){ deleteGroup(g); }, title:'Delete', style:{ background:'none', border:'none', color:TC.error, cursor:'pointer', fontSize:14 } }, '🗑')
                        )
                    );
                })
            )
        );
    }

    /* ════════════════════════════════
     * ADMINISTRATORS PAGE
     * ════════════════════════════════ */
    function AdministratorsPage({ dark, cardBg, textColor, mutedColor, borderColor, tableHeaderBg }) {
        var _ad = useState([]),      admins = _ad[0],    setAdmins = _ad[1];
        var _ld = useState(true),    loading = _ld[0],   setLoading = _ld[1];
        var _sh = useState(false),   showForm = _sh[0],  setShowForm = _sh[1];
        var _fm = useState({ email:'', password:'', confirm_password:'', first_name:'', last_name:'', phone:'', role:'administrator', status:'active' });
        var form = _fm[0], setForm = _fm[1];
        var _sv = useState(false),   saving = _sv[0],    setSaving = _sv[1];
        var _er = useState(''),      err = _er[0],       setErr = _er[1];
        var fs = fieldSty(dark, borderColor);
        var labelSty = { fontSize:12, fontWeight:500, display:'block', marginBottom:S.xs, color:mutedColor };

        function load() {
            setLoading(true);
            apiFetch('/administrators').then(function(d) {
                setAdmins(d.administrators || []); setLoading(false);
            }).catch(function(){ setLoading(false); });
        }
        useEffect(load, []);

        function openAdd() { setForm({ email:'', password:'', confirm_password:'', first_name:'', last_name:'', phone:'', role:'administrator', status:'active' }); setErr(''); setShowForm(true); }
        function closeForm() { setShowForm(false); setErr(''); }

        function setField(k, v) { setForm(function(p){ var o = Object.assign({}, p); o[k] = v; return o; }); }

        function saveAdmin() {
            setErr('');
            if (!form.email) { setErr('Email is required.'); return; }
            if (!form.password) { setErr('Password is required.'); return; }
            if (form.password !== form.confirm_password) { setErr('Passwords do not match.'); return; }
            setSaving(true);
            apiFetch('/administrators', { method:'POST', body:{ email:form.email, password:form.password, first_name:form.first_name, last_name:form.last_name, phone:form.phone, role:form.role } })
                .then(function(d) {
                    setSaving(false);
                    if (d.error) { setErr(d.error); return; }
                    closeForm(); load();
                }).catch(function(e){ setSaving(false); setErr(e.message || 'Failed to create admin.'); });
        }

        function deleteAdmin(a) {
            if (a.id === parseInt(window.TitanCartConfig && TitanCartConfig.userId)) { alert('You cannot delete your own account.'); return; }
            if (!confirm('Remove admin access for "' + a.display_name + '"? They will be demoted to subscriber.')) return;
            apiFetch('/administrators/' + a.id, { method:'DELETE' }).then(load);
        }

        var colGrid = '1.5fr 1.5fr 1fr 1fr 0.7fr';
        return h(PageWrap, { title:'Administrators', sub: admins.length + ' admin users', mutedColor:mutedColor, action:
            h(Btn, { variant:'primary', onClick:openAdd }, '+ Add Administrator')
        },
            /* Add modal */
            showForm && h('div', { style:{ position:'fixed', inset:0, zIndex:9999, background:'rgba(0,0,0,0.4)', display:'flex', alignItems:'center', justifyContent:'center' } },
                h('div', { style:{ background:cardBg, borderRadius:12, padding:S.xl, width:520, maxWidth:'90%', border:'1px solid '+borderColor, maxHeight:'85vh', overflowY:'auto' } },
                    h('h3', { style:{ fontSize:16, fontWeight:600, marginBottom:S.lg } }, 'New Administrator'),
                    err && h('div', { style:{ padding:'8px 12px', background:'#FEF2F2', color:'#DC2626', borderRadius:8, marginBottom:S.md, fontSize:13 } }, err),

                    h('div', { style:{ marginBottom:S.md } },
                        h('label', { style:labelSty }, 'E-mail *'),
                        h('input', { type:'email', value:form.email, onChange:function(e){ setField('email', e.target.value); }, style:fs, autoFocus:true })
                    ),
                    h('div', { style:{ display:'grid', gridTemplateColumns:'1fr 1fr', gap:S.md, marginBottom:S.md } },
                        h('div', null,
                            h('label', { style:labelSty }, 'Password *'),
                            h('input', { type:'password', value:form.password, onChange:function(e){ setField('password', e.target.value); }, style:fs })
                        ),
                        h('div', null,
                            h('label', { style:labelSty }, 'Confirm Password *'),
                            h('input', { type:'password', value:form.confirm_password, onChange:function(e){ setField('confirm_password', e.target.value); }, style:fs })
                        )
                    ),
                    h('div', { style:{ marginBottom:S.md } },
                        h('label', { style:labelSty }, 'Status'),
                        h('div', { style:{ display:'flex', gap:S.lg, marginTop:4 } },
                            ['active','disabled'].map(function(s) {
                                return h('label', { key:s, style:{ display:'flex', alignItems:'center', gap:6, fontSize:13, cursor:'pointer' } },
                                    h('input', { type:'radio', name:'admin_status', value:s, checked:form.status===s, onChange:function(){ setField('status',s); } }),
                                    s.charAt(0).toUpperCase() + s.slice(1)
                                );
                            })
                        )
                    ),
                    h('div', { style:{ marginBottom:S.md } },
                        h('label', { style:labelSty }, 'Role'),
                        h('select', { value:form.role, onChange:function(e){ setField('role', e.target.value); }, style:fs },
                            h('option', { value:'administrator' }, 'Administrator'),
                            h('option', { value:'editor' }, 'Editor / Content Manager')
                        )
                    ),
                    h('div', { style:{ display:'grid', gridTemplateColumns:'1fr 1fr', gap:S.md, marginBottom:S.md } },
                        h('div', null,
                            h('label', { style:labelSty }, 'First Name'),
                            h('input', { value:form.first_name, onChange:function(e){ setField('first_name', e.target.value); }, style:fs })
                        ),
                        h('div', null,
                            h('label', { style:labelSty }, 'Last Name'),
                            h('input', { value:form.last_name, onChange:function(e){ setField('last_name', e.target.value); }, style:fs })
                        )
                    ),
                    h('div', { style:{ marginBottom:S.lg } },
                        h('label', { style:labelSty }, 'Phone'),
                        h('input', { type:'tel', value:form.phone, onChange:function(e){ setField('phone', e.target.value); }, style:Object.assign({}, fs, { maxWidth:250 }) })
                    ),
                    h('div', { style:{ display:'flex', gap:S.sm, justifyContent:'flex-end' } },
                        h(Btn, { variant:'ghost', onClick:closeForm }, 'Cancel'),
                        h(Btn, { variant:'primary', onClick:saveAdmin, disabled:saving }, saving ? 'Creating…' : 'Create')
                    )
                )
            ),

            h(Card, { cardBg:cardBg, borderColor:borderColor, dark:dark, pad:0 },
                h('div', { style:{ display:'grid', gridTemplateColumns:colGrid, padding:S.md + 'px ' + S.xl + 'px', borderBottom:'1px solid ' + borderColor, background:tableHeaderBg, boxShadow:tableHeaderShadow } },
                    ['Name','Email','Role','Registered',''].map(function(col) {
                        return h('span', { key:col||'actions', style:{ fontSize:10, fontWeight:500, color:mutedColor, textTransform:'uppercase', letterSpacing:'.5px' } }, col);
                    })
                ),
                loading && h(Spinner, { mutedColor:mutedColor }),
                !loading && admins.length === 0 && h(EmptyState, { mutedColor:mutedColor, icon:'🛡', title:'No administrators', sub:'Add admin users to manage your store.' }),
                !loading && admins.map(function(a, i) {
                    var name = ((a.first_name||'') + ' ' + (a.last_name||'')).trim() || a.display_name || a.email;
                    return h('div', { key:a.id,
                        style:{ display:'grid', gridTemplateColumns:colGrid, padding:'12px ' + S.xl + 'px', borderBottom: i < admins.length-1 ? '1px solid ' + borderColor : 'none', alignItems:'center' },
                        onMouseEnter:function(e){ e.currentTarget.style.background = dark ? 'rgba(255,255,255,.02)' : '#F9FAFB'; },
                        onMouseLeave:function(e){ e.currentTarget.style.background = 'transparent'; } },
                        h('div', { style:{ display:'flex', alignItems:'center', gap:10 } },
                            h('div', { style:{ width:32, height:32, borderRadius:'50%', background:dark?'#333':'#DBEAFE', display:'flex', alignItems:'center', justifyContent:'center', fontSize:12, fontWeight:600, color:dark?'#93C5FD':'#2563EB', flexShrink:0 } },
                                ((a.first_name||a.display_name||'?')[0] + (a.last_name||'')[0]).toUpperCase()
                            ),
                            h('span', { style:{ fontSize:13, fontWeight:500 } }, name)
                        ),
                        h('span', { style:{ fontSize:12, color:mutedColor } }, a.email),
                        h('span', { style:{ fontSize:12, textTransform:'capitalize' } }, a.role),
                        h('span', { style:{ fontSize:11, color:mutedColor } }, tcFmtDate(a.registered)),
                        h('div', { style:{ textAlign:'right' } },
                            h('button', { onClick:function(){ deleteAdmin(a); }, title:'Remove admin access', style:{ background:'none', border:'none', color:TC.error, cursor:'pointer', fontSize:14 } }, '🗑')
                        )
                    );
                })
            )
        );
    }

    /* ════════════════════════════════
     * PROMOTIONS PAGE
     * ════════════════════════════════ */
    /* ════════════════════════════════════════════════════════════════════
     * PROMOTION EDITOR — Full 4-tab editor (General, Conditions, Bonuses, Coupons)
     * ════════════════════════════════════════════════════════════════════ */

    var CONDITION_TYPES = [
        { v:'subtotal',       l:'Cart Subtotal',   hint:'Cart subtotal amount', hasValue:'amount' },
        { v:'quantity',       l:'Items in Cart',   hint:'Total items quantity', hasValue:'count' },
        { v:'products',       l:'Specific Products', hint:'Cart contains product IDs', hasValue:'ids' },
        { v:'categories',     l:'Product Categories', hint:'Cart has items from category IDs', hasValue:'ids' },
        { v:'customer_email', l:'Customer Email',  hint:'Customer email is in list', hasValue:'emails' },
    ];
    var OPERATORS = [
        { v:'gte', l:'>=' }, { v:'lte', l:'<=' }, { v:'gt', l:'>' },
        { v:'lt',  l:'<'  }, { v:'eq',  l:'='  }, { v:'in', l:'in' },
    ];
    var BONUS_TYPES = [
        { v:'cart_discount_percent',    l:'Cart Discount (%)',         hasValue:'percent' },
        { v:'cart_discount_fixed',      l:'Cart Discount (Fixed $)',   hasValue:'amount' },
        { v:'product_discount_percent', l:'Product Discount (%)',      hasValue:'percent' },
        { v:'free_shipping',            l:'Free Shipping',             hasValue:null },
    ];


    /* ════════════════════════════════
     * ADMIN VENDOR DETAIL PAGE (B8.5 S200)
     * ════════════════════════════════
     * Single-vendor admin view rendered at hash #/vendors/{id}. Surfaces
     * every column on the vendor row plus the joined customer identity
     * shipped by VendorsController::enrich_with_customer and the
     * logo_url resolved via wp_get_attachment_image_url('medium').
     *
     * Sections:
     *   - Header        title + status pill + state-machine actions
     *   - Profile       logo, display_name, slug w/ storefront link, description
     *   - Customer      first/last name + email + link to /customers/{id}
     *   - Contact       business_email, business_phone, external_url
     *   - Address       address_1/2, city, state, postcode, country
     *   - Lifecycle     status timestamps (created, activated, suspended, deleted)
     *   - Stripe Connect derived status + capabilities + onboarding timestamps
     *   - Admin notes   read-only transition history
     *
     * The five status-transition ConfirmModals mirror AdminVendorsPage exactly
     * so admin muscle memory carries over between list and detail. After any
     * transition, the page reloads itself (re-fetching the row) so the new
     * status + admin_notes append are reflected without leaving the page.
     * ════════════════════════════════ */
    function AdminVendorDetailPage({ vendorId, dark, cardBg, textColor, mutedColor, borderColor, tableHeaderBg }) {
        var _v  = useState(null),  vendor = _v[0],  setVendor = _v[1];
        var _ld = useState(true),  loading = _ld[0], setLoading = _ld[1];
        var _er = useState(null),  error = _er[0],   setError = _er[1];
        var _bz = useState(false), busy = _bz[0],    setBusy = _bz[1];
        var _ts = useState(null),  toast = _ts[0],   setToast = _ts[1];
        var _ca = useState(false), cApprove = _ca[0], setCApprove = _ca[1];
        var _cr = useState(false), cReject = _cr[0],  setCReject = _cr[1];
        var _cs = useState(false), cSuspend = _cs[0], setCSuspend = _cs[1];
        var _cd = useState(false), cDisable = _cd[0], setCDisable = _cd[1];
        var _cv = useState(false), cReact = _cv[0],   setCReact = _cv[1];
        var _cw = useState(false), cRecover = _cw[0], setCRecover = _cw[1];
        var _re = useState(''),    reason = _re[0],   setReason = _re[1];
        var _ci = useState(''),    commissionInput = _ci[0], setCommissionInput = _ci[1];
        var _cb = useState(false), savingComm = _cb[0],      setSavingComm = _cb[1];

        function load() {
            setLoading(true);
            setError(null);
            apiFetchMarketplace('/vendors/' + vendorId)
                .then(function(data) {
                    var v = data && data.vendor ? data.vendor : data;
                    setVendor(v || null);
                    // Hydrate the editable commission field. DECIMAL columns come
                    // back zero-padded ("15.00"); strip via parseFloat so the
                    // number input shows "15" not "15.00". NULL/'' = no override.
                    setCommissionInput(
                        v && v.commission_rate != null && v.commission_rate !== ''
                            ? String(parseFloat(v.commission_rate))
                            : ''
                    );
                    setLoading(false);
                })
                .catch(function(err) {
                    console.error('[TitanCart] AdminVendorDetailPage load failed:', err);
                    setError(String(err && err.message ? err.message : err));
                    setLoading(false);
                });
        }
        useEffect(load, [vendorId]);

        function transitionStatus(newStatus) {
            setBusy(true);
            apiFetchMarketplace('/vendors/' + vendorId + '/status', {
                method: 'POST',
                body: { status: newStatus, reason: reason }
            })
                .then(function() {
                    setBusy(false);
                    setCApprove(false); setCReject(false); setCSuspend(false); setCDisable(false); setCReact(false); setCRecover(false);
                    setReason('');
                    setToast({ kind:'ok', text: (vendor && vendor.display_name ? vendor.display_name : 'Vendor') + ' → ' + newStatus });
                    setTimeout(function(){ setToast(null); }, 3000);
                    load();
                })
                .catch(function(err) {
                    setBusy(false);
                    setToast({ kind:'err', text: 'Failed: ' + (err && err.message ? err.message : 'unknown error') });
                    setTimeout(function(){ setToast(null); }, 5000);
                });
        }

        // S203 — Admin-bypass recovery from terminal `disabled` state.
        // Calls the dedicated /vendors/{id}/recover endpoint (NOT /status —
        // /status would 400 with illegal_transition since the state machine
        // keeps `disabled` terminal). Reason is required by the modal copy
        // (audit trail demands non-empty justification).
        function recoverVendor() {
            setBusy(true);
            apiFetchMarketplace('/vendors/' + vendorId + '/recover', {
                method: 'POST',
                body: { reason: reason }
            })
                .then(function() {
                    setBusy(false);
                    setCRecover(false);
                    setReason('');
                    setToast({ kind:'ok', text: (vendor && vendor.display_name ? vendor.display_name : 'Vendor') + ' recovered → active' });
                    setTimeout(function(){ setToast(null); }, 3000);
                    load();
                })
                .catch(function(err) {
                    setBusy(false);
                    setToast({ kind:'err', text: 'Recovery failed: ' + (err && err.message ? err.message : 'unknown error') });
                    setTimeout(function(){ setToast(null); }, 5000);
                });
        }

        // B10 (S245) — Save the per-vendor commission override. Blank clears
        // the override (commission_rate → NULL) so the vendor falls back to the
        // site default. PUT /vendors/{id} accepts + clamps + normalizes the
        // value server-side (VendorRepository::normalize_column); load() then
        // re-hydrates from the saved row.
        function saveCommission(explicitVal) {
            // When called with an explicit argument (the Clear override button
            // passes '') use it directly so React state-batching can't make us
            // read a stale commissionInput; otherwise use the live input value.
            var commVal = (arguments.length > 0) ? explicitVal : commissionInput;
            var commIsClear = (commVal === '' || commVal === null);
            setSavingComm(true);
            apiFetchMarketplace('/vendors/' + vendorId, {
                method: 'PUT',
                body: { commission_rate: (commIsClear ? null : commVal) }
            })
                .then(function() {
                    setSavingComm(false);
                    setToast({ kind:'ok', text: (commIsClear ? 'Commission override cleared — using site default' : 'Commission rate updated') });
                    setTimeout(function(){ setToast(null); }, 3000);
                    load();
                })
                .catch(function(err) {
                    setSavingComm(false);
                    setToast({ kind:'err', text: 'Save failed: ' + (err && err.message ? err.message : 'unknown error') });
                    setTimeout(function(){ setToast(null); }, 5000);
                });
        }

        // S203 — Centralized cancel handler. Resets reason + closes all 6
        // modals so reason state never leaks across distinct admin actions
        // (e.g. open Disable modal, type a reason, cancel, open Suspend modal
        // — the previous reason must NOT carry over).
        function cancelAllModals() {
            setCApprove(false); setCReject(false); setCSuspend(false); setCDisable(false); setCReact(false); setCRecover(false);
            setReason('');
        }

        function fmtDate(s) {
            if (!s) return null;
            try {
                var d = new Date(String(s).replace(' ', 'T') + 'Z');
                return d.toLocaleString(undefined, { year:'numeric', month:'short', day:'numeric', hour:'2-digit', minute:'2-digit' });
            } catch (e) { return String(s); }
        }

        function statusPill(st) {
            var meta = ({
                'new':       { label:'New',       color:'#64748B', bg:'rgba(100,116,139,.12)' },
                'pending':   { label:'Pending',   color:'#D97706', bg:'rgba(217,119,6,.12)' },
                'active':    { label:'Active',    color:'#16A34A', bg:'rgba(22,163,74,.12)' },
                'suspended': { label:'Suspended', color:'#DC2626', bg:'rgba(220,38,38,.12)' },
                'disabled':  { label:'Disabled',  color:'#737373', bg:'rgba(115,115,115,.14)' },
            })[st] || { label: st || '—', color: mutedColor, bg: 'transparent' };
            return h('span', { style:{ fontSize:11, fontWeight:500, color:meta.color, background:meta.bg, padding:'3px 12px', borderRadius:20, display:'inline-block' } }, meta.label);
        }

        function row(label, value, valStyle) {
            return h('div', {
                key: label,
                style:{ display:'flex', padding:'10px 0', borderBottom:'1px solid ' + borderColor, gap:16, alignItems:'baseline' }
            },
                h('span', { style:{ fontSize:11, color:mutedColor, minWidth:160, maxWidth:160, fontWeight:500, textTransform:'uppercase', letterSpacing:'.3px' } }, label),
                h('span', { style: Object.assign({ fontSize:13, flex:1, wordBreak:'break-word' }, valStyle || {}) },
                    (value === null || value === undefined || value === '')
                        ? h('span', { style:{ color:mutedColor } }, '—')
                        : value
                )
            );
        }

        function section(title) {
            // Accept variadic children via arguments-slicing — calls like
            // section('Stripe Connect', row(...), row(...), row(...)) only
            // bound `title` and dropped the rest when this used the
            // (title, children) two-arg form. h.apply rebuilds the variadic
            // h() call so each child arrives as a separate arg (no key
            // warnings, mirrors the natural h('div', props, c1, c2, c3) shape).
            var children = Array.prototype.slice.call(arguments, 1);
            return h(Card, { cardBg:cardBg, borderColor:borderColor, dark:dark, pad:0 },
                h('div', { style:{ padding: S.md + 'px ' + S.xl + 'px', borderBottom:'1px solid ' + borderColor, fontSize:13, fontWeight:600, letterSpacing:'.3px', background: tableHeaderBg } }, title),
                h.apply(null, [ 'div', { style:{ padding: '4px ' + S.xl + 'px ' + S.md + 'px ' + S.xl + 'px' } } ].concat(children))
            );
        }

        function renderActionButtons() {
            var st = vendor && vendor.status || '';
            var btnStyle = { fontSize:13, padding:'8px 16px', borderRadius:6, border:'none', cursor:'pointer', fontWeight:500, marginLeft:8 };
            if (st === 'pending' || st === 'new') {
                return h('div', { style:{ display:'flex', justifyContent:'flex-end', gap:0 } },
                    h('button', { onClick:function(){ setCReject(true); },  style: Object.assign({}, btnStyle, { background:'transparent', color:TC.error, border:'1px solid '+borderColor }) }, 'Reject'),
                    h('button', { onClick:function(){ setCApprove(true); }, style: Object.assign({}, btnStyle, { background:'#16A34A', color:'#fff' }) }, 'Approve')
                );
            }
            if (st === 'active') {
                return h('div', { style:{ display:'flex', justifyContent:'flex-end', gap:0 } },
                    h('button', { onClick:function(){ setCSuspend(true); }, style: Object.assign({}, btnStyle, { background:'transparent', color:'#D97706', border:'1px solid '+borderColor }) }, 'Suspend'),
                    h('button', { onClick:function(){ setCDisable(true); }, style: Object.assign({}, btnStyle, { background:'transparent', color:TC.error, border:'1px solid '+borderColor }) }, 'Disable')
                );
            }
            if (st === 'suspended') {
                return h('div', { style:{ display:'flex', justifyContent:'flex-end', gap:0 } },
                    h('button', { onClick:function(){ setCDisable(true); }, style: Object.assign({}, btnStyle, { background:'transparent', color:TC.error, border:'1px solid '+borderColor }) }, 'Disable'),
                    h('button', { onClick:function(){ setCReact(true); },   style: Object.assign({}, btnStyle, { background:'#16A34A', color:'#fff' }) }, 'Re-activate')
                );
            }
            if (st === 'disabled') {
                // S203 — Replace pre-S203 "Terminal state — no actions" with the
                // admin-bypass Recover surface. The /recover REST endpoint hits a
                // dedicated service method that explicitly bypasses TRANSITIONS
                // (state machine stays terminal-pure for non-admin code paths).
                return h('div', { style:{ display:'flex', justifyContent:'flex-end', gap:0 } },
                    h('button', { onClick:function(){ setCRecover(true); }, style: Object.assign({}, btnStyle, { background:'#16A34A', color:'#fff' }) }, 'Recover')
                );
            }
            return h('span', { style:{ fontSize:12, color:mutedColor } }, 'Terminal state — no actions');
        }

        var backLink = h('a', {
            href: '#/vendors',
            style: { fontSize:12, color:mutedColor, textDecoration:'none', display:'inline-flex', alignItems:'center', gap:6, marginBottom:S.md }
        }, '← Vendors');

        if (loading) {
            return h(PageWrap, { title:'Vendor', mutedColor:mutedColor }, backLink, h(Spinner, { mutedColor:mutedColor }));
        }
        if (error || !vendor) {
            return h(PageWrap, { title:'Vendor', mutedColor:mutedColor },
                backLink,
                h('div', { style:{ marginBottom:S.md, padding:'10px 14px', borderRadius:8, background:'rgba(220,38,38,.08)', color:'#DC2626', fontSize:13, border:'1px solid rgba(220,38,38,.25)' } },
                    error ? ('Error loading vendor: ' + error) : 'Vendor not found.'
                )
            );
        }

        /* Stripe Connect status summary derived from the row columns. */
        var sc_started   = !!vendor.stripe_onboarding_started_at;
        var sc_completed = !!vendor.stripe_onboarding_completed_at;
        var sc_charges   = !!Number(vendor.stripe_charges_enabled || 0);
        var sc_payouts   = !!Number(vendor.stripe_payouts_enabled || 0);
        var sc_label =
              sc_completed && sc_charges && sc_payouts ? 'Fully onboarded'
            : sc_completed                              ? 'Onboarded (capabilities pending)'
            : sc_started                                ? 'Onboarding in progress'
                                                        : 'Not started';
        var sc_color =
              sc_completed && sc_charges && sc_payouts ? '#16A34A'
            : sc_started                                ? '#D97706'
                                                        : mutedColor;

        /* Customer name fallback ladder: full name → email → em-dash. */
        var cFn = (vendor.customer_first_name || '').trim();
        var cLn = (vendor.customer_last_name  || '').trim();
        var cEm = (vendor.customer_email      || '').trim();
        var customerName = (cFn + ' ' + cLn).trim() || cEm || '—';

        /* Storefront URL — public 404 when vendor isn't active, but the
         * link is still useful for admin context. */
        var storefrontUrl = vendor.slug ? ('/vendors/' + vendor.slug + '/') : null;

        return h(PageWrap, { title:'', mutedColor:mutedColor },
            backLink,

            /* Toast */
            toast && h('div', {
                style:{
                    marginBottom:S.md, padding:'10px 14px', borderRadius:8,
                    background: toast.kind === 'ok' ? 'rgba(22,163,74,.12)' : 'rgba(220,38,38,.12)',
                    color: toast.kind === 'ok' ? '#16A34A' : '#DC2626',
                    fontSize:13,
                    border:'1px solid ' + (toast.kind === 'ok' ? 'rgba(22,163,74,.3)' : 'rgba(220,38,38,.3)'),
                }
            }, toast.text),

            /* Header: title + status + actions */
            h('div', { style:{ display:'flex', alignItems:'flex-start', justifyContent:'space-between', gap:24, marginBottom:S.lg } },
                h('div', null,
                    h('h1', { style:{ fontSize:22, fontWeight:600, margin:'0 0 6px 0' } }, vendor.display_name || '(no display name)'),
                    h('div', { style:{ display:'flex', alignItems:'center', gap:10 } },
                        statusPill(vendor.status),
                        h('span', { style:{ fontSize:12, color:mutedColor } }, 'Vendor #' + vendor.id)
                    )
                ),
                renderActionButtons()
            ),

            /* Profile / Storefront */
            section('Storefront profile',
                vendor.logo_url && h('div', { style:{ paddingTop:S.md, paddingBottom:S.md } },
                    h('img', { src:vendor.logo_url, alt:'Logo', style:{ maxWidth:120, maxHeight:120, objectFit:'contain', borderRadius:8, border:'1px solid '+borderColor } })
                ),
                row('Display name', vendor.display_name),
                row('Slug',
                    vendor.slug
                        ? h('span', null,
                            h('span', { style:{ fontFamily:'ui-monospace, monospace', fontSize:12, color:mutedColor } }, vendor.slug),
                            storefrontUrl && h('a', { href:storefrontUrl, target:'_blank', rel:'noopener noreferrer', style:{ marginLeft:12, fontSize:12, color:TC.accent, textDecoration:'none' } }, 'View storefront ↗')
                          )
                        : null
                ),
                row('Description', vendor.description, { whiteSpace:'pre-wrap' })
            ),

            /* Customer */
            h('div', { style:{ marginTop:S.lg } },
                section('Linked customer',
                    row('Customer name', customerName),
                    row('Customer email', cEm),
                    row('Customer ID',
                        vendor.customer_id
                            ? h('a', { href:'#/customers/' + vendor.customer_id, style:{ color:TC.accent, textDecoration:'none' } }, '#' + vendor.customer_id + ' — view customer profile')
                            : null
                    )
                )
            ),

            /* Contact */
            h('div', { style:{ marginTop:S.lg } },
                section('Public contact',
                    row('Business email', vendor.business_email && h('a', { href:'mailto:'+vendor.business_email, style:{ color:TC.accent, textDecoration:'none' } }, vendor.business_email)),
                    row('Business phone', vendor.business_phone),
                    row('Website', vendor.external_url && h('a', { href:vendor.external_url, target:'_blank', rel:'noopener noreferrer', style:{ color:TC.accent, textDecoration:'none' } }, vendor.external_url))
                )
            ),

            /* Address */
            h('div', { style:{ marginTop:S.lg } },
                section('Business address',
                    row('Line 1', vendor.address_1),
                    row('Line 2', vendor.address_2),
                    row('City', vendor.city),
                    row('State / region', vendor.state),
                    row('Postcode', vendor.postcode),
                    row('Country', vendor.country)
                )
            ),

            /* Lifecycle */
            h('div', { style:{ marginTop:S.lg } },
                section('Lifecycle',
                    row('Status', statusPill(vendor.status)),
                    row('Created', fmtDate(vendor.created_at)),
                    row('First activated', fmtDate(vendor.activated_at)),
                    row('Last suspended', fmtDate(vendor.suspended_at)),
                    vendor.deleted_at && row('Soft-deleted', fmtDate(vendor.deleted_at))
                )
            ),

            /* Stripe Connect */
            h('div', { style:{ marginTop:S.lg } },
                section('Stripe Connect',
                    row('Status', h('span', { style:{ fontWeight:500, color: sc_color } }, sc_label)),
                    row('Account ID', vendor.stripe_account_id && h('span', { style:{ fontFamily:'ui-monospace, monospace', fontSize:12 } }, vendor.stripe_account_id)),
                    row('Charges enabled', sc_charges ? 'Yes' : 'No'),
                    row('Payouts enabled', sc_payouts ? 'Yes' : 'No'),
                    row('Onboarding started', fmtDate(vendor.stripe_onboarding_started_at)),
                    row('Onboarding completed', fmtDate(vendor.stripe_onboarding_completed_at))
                )
            ),

            /* Commission — B10 (S245). Editable per-vendor override; blank = site default. */
            h('div', { style:{ marginTop:S.lg } },
                section('Commission',
                    h('div', { style:{ padding:'12px 0' } },
                        h('div', { style:{ fontSize:12, color:mutedColor, marginBottom:10, lineHeight:1.55 } },
                            'The platform\u2019s percentage cut of this vendor\u2019s sales. Leave blank to use the marketplace default'
                            + (vendor.site_default_commission_rate != null ? (' (' + String(parseFloat(vendor.site_default_commission_rate)) + '%)') : '')
                            + ', which is set under Settings → Marketplace.'),
                        h('div', { style:{ display:'flex', alignItems:'center', gap:10, flexWrap:'wrap' } },
                            h('input', {
                                type:'number', step:'0.01', min:0, max:100,
                                value: commissionInput,
                                placeholder: (vendor.site_default_commission_rate != null ? String(parseFloat(vendor.site_default_commission_rate)) : '10'),
                                onChange: function(e){ setCommissionInput(e.target.value); },
                                style: Object.assign({}, fieldSty(dark, borderColor), { maxWidth:140 })
                            }),
                            h('span', { style:{ fontSize:13, color:mutedColor } }, '%'),
                            h('button', {
                                onClick: function(){ saveCommission(); },
                                disabled: savingComm,
                                style: { fontSize:13, padding:'8px 16px', borderRadius:6, border:'none', cursor: savingComm ? 'default' : 'pointer', fontWeight:500, background:TC.accent, color:'#fff', opacity: savingComm ? 0.6 : 1, marginLeft:4 }
                            }, savingComm ? 'Saving\u2026' : 'Save rate'),
                            commissionInput !== '' && h('button', {
                                onClick: function(){ setCommissionInput(''); saveCommission(''); },
                                disabled: savingComm,
                                style: { fontSize:12, padding:'8px 12px', borderRadius:6, background:'transparent', color:mutedColor, border:'1px solid '+borderColor, cursor: savingComm ? 'default' : 'pointer' }
                            }, 'Clear override')
                        ),
                        (function(){
                            var hasOverride = commissionInput !== '' && !isNaN(parseFloat(commissionInput));
                            var eff = hasOverride
                                ? parseFloat(commissionInput)
                                : (vendor.site_default_commission_rate != null ? parseFloat(vendor.site_default_commission_rate) : null);
                            if (eff === null || isNaN(eff)) { return null; }
                            var keep = Math.round((100 - eff) * 100) / 100;
                            return h('div', { style:{ fontSize:12, color:mutedColor, marginTop:10 } },
                                'Effective: platform keeps ' + eff + '%, vendor keeps ' + keep + '% of each sale'
                                + (hasOverride ? '' : ' (site default)') + '.');
                        })()
                    )
                )
            ),

            /* Admin notes */
            h('div', { style:{ marginTop:S.lg, marginBottom:S.xl } },
                section('Admin notes & transition history',
                    h('div', { style:{ padding:'12px 0', whiteSpace:'pre-wrap', fontSize:12, color: vendor.admin_notes ? textColor : mutedColor, fontFamily:'ui-monospace, monospace', lineHeight:1.5 } },
                        vendor.admin_notes || '— No notes recorded —'
                    )
                )
            ),

            /* Confirmation modals — S203: all 5 status-transition modals now
             * capture an optional reason (audit trail flows into admin_notes +
             * tcmp_vendor_status_changed hook 4th arg). The 6th modal (Recover)
             * requires a non-empty reason — admin overrides of terminal state
             * must be explicitly justified. */
            h(ConfirmModal, {
                open: cApprove,
                title: 'Approve vendor application?',
                detail: vendor.display_name,
                message: 'The applicant becomes an active vendor and will be notified by email. They can immediately begin Stripe Connect onboarding and list products.',
                confirmLabel: 'Approve',
                tone: 'positive',
                busy: busy,
                showReason: true,
                reasonValue: reason,
                onReasonChange: setReason,
                reasonLabel: 'Approval note (optional)',
                reasonPlaceholder: 'e.g. Manually reviewed business documents — verified',
                onConfirm: function(){ transitionStatus('active'); },
                onCancel: cancelAllModals,
                dark: dark, cardBg: cardBg, borderColor: borderColor, textColor: textColor, mutedColor: mutedColor,
            }),
            h(ConfirmModal, {
                open: cReject,
                title: 'Reject vendor application?',
                detail: vendor.display_name,
                message: 'The applicant becomes disabled (terminal). They will be notified by email and will need to contact you to re-apply. This cannot be undone via the SPA.',
                confirmLabel: 'Reject application',
                busy: busy,
                showReason: true,
                reasonValue: reason,
                onReasonChange: setReason,
                reasonLabel: 'Rejection reason (optional)',
                reasonPlaceholder: 'e.g. Business documents could not be verified',
                onConfirm: function(){ transitionStatus('disabled'); },
                onCancel: cancelAllModals,
                dark: dark, cardBg: cardBg, borderColor: borderColor, textColor: textColor, mutedColor: mutedColor,
            }),
            h(ConfirmModal, {
                open: cSuspend,
                title: 'Suspend vendor?',
                detail: vendor.display_name,
                message: 'The vendor will be unable to list new products or modify their catalog. Existing orders continue to settle normally. Re-activate later from this page.',
                confirmLabel: 'Suspend',
                busy: busy,
                showReason: true,
                reasonValue: reason,
                onReasonChange: setReason,
                reasonLabel: 'Suspension reason (optional)',
                reasonPlaceholder: 'e.g. Policy review hold — pending response from vendor',
                onConfirm: function(){ transitionStatus('suspended'); },
                onCancel: cancelAllModals,
                dark: dark, cardBg: cardBg, borderColor: borderColor, textColor: textColor, mutedColor: mutedColor,
            }),
            h(ConfirmModal, {
                open: cDisable,
                title: 'Disable vendor?',
                detail: vendor.display_name,
                message: 'The vendor becomes disabled (terminal). All product listings are removed from the storefront. Pending Stripe transfers are blocked. This cannot be undone via the SPA — use Recover from the disabled state if disable-in-error.',
                confirmLabel: 'Disable vendor',
                busy: busy,
                showReason: true,
                reasonValue: reason,
                onReasonChange: setReason,
                reasonLabel: 'Disable reason (optional)',
                reasonPlaceholder: 'e.g. Vendor offboarded — terms violation confirmed',
                onConfirm: function(){ transitionStatus('disabled'); },
                onCancel: cancelAllModals,
                dark: dark, cardBg: cardBg, borderColor: borderColor, textColor: textColor, mutedColor: mutedColor,
            }),
            h(ConfirmModal, {
                open: cReact,
                title: 'Re-activate vendor?',
                detail: vendor.display_name,
                message: 'The vendor returns to active status and can resume listing products and modifying their catalog.',
                confirmLabel: 'Re-activate',
                tone: 'positive',
                busy: busy,
                showReason: true,
                reasonValue: reason,
                onReasonChange: setReason,
                reasonLabel: 'Re-activation note (optional)',
                reasonPlaceholder: 'e.g. Policy review complete — vendor cleared',
                onConfirm: function(){ transitionStatus('active'); },
                onCancel: cancelAllModals,
                dark: dark, cardBg: cardBg, borderColor: borderColor, textColor: textColor, mutedColor: mutedColor,
            }),
            /* S203 — Recover from terminal disabled state. Reason REQUIRED. */
            h(ConfirmModal, {
                open: cRecover,
                title: 'Recover disabled vendor?',
                detail: vendor.display_name,
                message: 'This is an admin override of a terminal state. The vendor returns to active and their role + customer-group membership are restored. ⚠ Cascade-soft-deleted products and vendor↔product junctions are NOT auto-restored — that side of the cascade is one-way without manual SQL recovery (see S202 archive for the time-scoped UPDATE pattern). A justification is required for the audit trail.',
                confirmLabel: 'Recover vendor',
                tone: 'positive',
                busy: busy,
                busyLabel: 'Recovering…',
                showReason: true,
                reasonValue: reason,
                onReasonChange: setReason,
                reasonRequired: true,
                reasonLabel: 'Justification (required)',
                reasonPlaceholder: 'e.g. Vendor disabled in error — confirmed via support ticket #1234',
                onConfirm: function(){ recoverVendor(); },
                onCancel: cancelAllModals,
                dark: dark, cardBg: cardBg, borderColor: borderColor, textColor: textColor, mutedColor: mutedColor,
            })
        );
    }

    /* ════════════════════════════════
     * ADMIN VENDORS PAGE (B7 S196)
     * ════════════════════════════════
     * Admin-side vendor management surface. Default view: pending vendors
     * awaiting approval. Status chips switch between pending/active/
     * suspended/disabled/all. Approve flips pending→active, Reject flips
     * pending→disabled (terminal). Active vendors can be Suspended or
     * Disabled; suspended can be Re-activated or Disabled; disabled is
     * read-only (terminal).
     *
     * Reaches the marketplace REST namespace via apiFetchMarketplace,
     * which routes to Config.marketplaceApiUrl (titancart-marketplace/v1).
     * The page is gated in NAV_SECTIONS by plugin:'marketplace' — the
     * sidebar filter hides it when the plugin is not installed.
     *
     * S200: subId presence early-returns to AdminVendorDetailPage above.
     * ════════════════════════════════ */
    function AdminVendorsPage({ subId, dark, cardBg, textColor, mutedColor, borderColor, tableHeaderBg }) {
        // S200 — detail page dispatch. Hash #/vendors/{id} renders the
        // single-vendor admin surface; list view otherwise.
        if (subId) {
            return h(AdminVendorDetailPage, { vendorId: Number(subId), dark:dark, cardBg:cardBg, textColor:textColor, mutedColor:mutedColor, borderColor:borderColor, tableHeaderBg:tableHeaderBg });
        }
        var _vs = useState([]),         vendors = _vs[0],     setVendors = _vs[1];
        var _ld = useState(true),       loading = _ld[0],     setLoading = _ld[1];
        var _er = useState(null),       error = _er[0],       setError = _er[1];
        var _st = useState('pending'),  statusFilter = _st[0], setStatusFilter = _st[1];
        var _ca = useState(null),       confirmApprove = _ca[0], setConfirmApprove = _ca[1];
        var _cr = useState(null),       confirmReject = _cr[0],  setConfirmReject = _cr[1];
        var _cs = useState(null),       confirmSuspend = _cs[0], setConfirmSuspend = _cs[1];
        var _cd = useState(null),       confirmDisable = _cd[0], setConfirmDisable = _cd[1];
        var _cv = useState(null),       confirmReactivate = _cv[0], setConfirmReactivate = _cv[1];
        var _cw = useState(null),       confirmRecover = _cw[0], setConfirmRecover = _cw[1];
        var _re = useState(''),         reason = _re[0],     setReason = _re[1];
        var _bz = useState(false),      busy = _bz[0],        setBusy = _bz[1];
        var _ts = useState(null),       toast = _ts[0],       setToast = _ts[1];

        function load() {
            setLoading(true);
            setError(null);
            var qs = statusFilter === 'all' ? '' : ('?status=' + encodeURIComponent(statusFilter));
            apiFetchMarketplace('/vendors' + qs)
                .then(function(data) {
                    var list = Array.isArray(data) ? data : (data && data.items ? data.items : []);
                    setVendors(list);
                    setLoading(false);
                })
                .catch(function(err) {
                    console.error('[TitanCart] AdminVendorsPage load failed:', err);
                    setError(String(err && err.message ? err.message : err));
                    setLoading(false);
                });
        }
        useEffect(load, [statusFilter]);

        function transitionStatus(vendor, newStatus) {
            setBusy(true);
            apiFetchMarketplace('/vendors/' + vendor.id + '/status', {
                method: 'POST',
                body: { status: newStatus, reason: reason }
            })
                .then(function() {
                    setBusy(false);
                    cancelAllModals();
                    setToast({ kind: 'ok', text: vendor.display_name + ' → ' + newStatus });
                    setTimeout(function(){ setToast(null); }, 3000);
                    load();
                })
                .catch(function(err) {
                    setBusy(false);
                    setToast({ kind: 'err', text: 'Failed: ' + (err && err.message ? err.message : 'unknown error') });
                    setTimeout(function(){ setToast(null); }, 5000);
                });
        }

        // S203 — Admin-bypass recovery from terminal `disabled` state.
        // Dedicated /vendors/{id}/recover endpoint; reason required per modal copy.
        function recoverVendor(vendor) {
            setBusy(true);
            apiFetchMarketplace('/vendors/' + vendor.id + '/recover', {
                method: 'POST',
                body: { reason: reason }
            })
                .then(function() {
                    setBusy(false);
                    cancelAllModals();
                    setToast({ kind: 'ok', text: vendor.display_name + ' recovered → active' });
                    setTimeout(function(){ setToast(null); }, 3000);
                    load();
                })
                .catch(function(err) {
                    setBusy(false);
                    setToast({ kind: 'err', text: 'Recovery failed: ' + (err && err.message ? err.message : 'unknown error') });
                    setTimeout(function(){ setToast(null); }, 5000);
                });
        }

        // S203 — Centralized cancel handler. Resets reason + closes all 6
        // modals so reason state never leaks across distinct admin actions.
        function cancelAllModals() {
            setConfirmApprove(null);
            setConfirmReject(null);
            setConfirmSuspend(null);
            setConfirmDisable(null);
            setConfirmReactivate(null);
            setConfirmRecover(null);
            setReason('');
        }

        function fmtDate(s) {
            if (!s) return '—';
            try {
                var d = new Date(String(s).replace(' ', 'T') + 'Z');
                return d.toLocaleDateString(undefined, { year:'numeric', month:'short', day:'numeric' });
            } catch(e) { return String(s); }
        }

        function statusPill(st) {
            var meta = ({
                'new':       { label:'New',       color:'#64748B', bg:'rgba(100,116,139,.12)' },
                'pending':   { label:'Pending',   color:'#D97706', bg:'rgba(217,119,6,.12)' },
                'active':    { label:'Active',    color:'#16A34A', bg:'rgba(22,163,74,.12)' },
                'suspended': { label:'Suspended', color:'#DC2626', bg:'rgba(220,38,38,.12)' },
                'disabled':  { label:'Disabled',  color:'#737373', bg:'rgba(115,115,115,.14)' },
            })[st] || { label: st || '—', color: mutedColor, bg: 'transparent' };
            return h('span', {
                style: { fontSize:11, fontWeight:500, color:meta.color, background:meta.bg, padding:'2px 10px', borderRadius:20, display:'inline-block' }
            }, meta.label);
        }

        function renderActions(v) {
            var st = v.status || '';
            var btnStyle = { fontSize:12, padding:'4px 12px', borderRadius:6, border:'none', cursor:'pointer', fontWeight:500, marginLeft:6 };
            if (st === 'pending' || st === 'new') {
                return h('div', { style:{ display:'flex', justifyContent:'flex-end' } },
                    h('button', {
                        onClick: function(e){ e.stopPropagation(); setConfirmApprove(v); },
                        style: Object.assign({}, btnStyle, { background:'#16A34A', color:'#fff' })
                    }, 'Approve'),
                    h('button', {
                        onClick: function(e){ e.stopPropagation(); setConfirmReject(v); },
                        style: Object.assign({}, btnStyle, { background:'transparent', color:TC.error, border:'1px solid '+borderColor })
                    }, 'Reject')
                );
            }
            if (st === 'active') {
                return h('div', { style:{ display:'flex', justifyContent:'flex-end' } },
                    h('button', {
                        onClick: function(e){ e.stopPropagation(); setConfirmSuspend(v); },
                        style: Object.assign({}, btnStyle, { background:'transparent', color:'#D97706', border:'1px solid '+borderColor })
                    }, 'Suspend'),
                    h('button', {
                        onClick: function(e){ e.stopPropagation(); setConfirmDisable(v); },
                        style: Object.assign({}, btnStyle, { background:'transparent', color:TC.error, border:'1px solid '+borderColor })
                    }, 'Disable')
                );
            }
            if (st === 'suspended') {
                return h('div', { style:{ display:'flex', justifyContent:'flex-end' } },
                    h('button', {
                        onClick: function(e){ e.stopPropagation(); setConfirmReactivate(v); },
                        style: Object.assign({}, btnStyle, { background:'#16A34A', color:'#fff' })
                    }, 'Re-activate'),
                    h('button', {
                        onClick: function(e){ e.stopPropagation(); setConfirmDisable(v); },
                        style: Object.assign({}, btnStyle, { background:'transparent', color:TC.error, border:'1px solid '+borderColor })
                    }, 'Disable')
                );
            }
            if (st === 'disabled') {
                // S203 — Admin-bypass Recover surface (replaces pre-S203
                // "Terminal" no-op span). Hits the dedicated /recover REST
                // endpoint which bypasses TRANSITIONS state-machine purity.
                return h('div', { style:{ display:'flex', justifyContent:'flex-end' } },
                    h('button', {
                        onClick: function(e){ e.stopPropagation(); setConfirmRecover(v); },
                        style: Object.assign({}, btnStyle, { background:'#16A34A', color:'#fff' })
                    }, 'Recover')
                );
            }
            // Defensive fallback — should be unreachable now that all 4
            // statuses (pending/new/active/suspended/disabled) have explicit
            // branches above. Kept as safety net for any future status added
            // to the schema before the SPA is updated.
            return h('span', { style:{ fontSize:11, color:mutedColor } }, 'Terminal');
        }

        var chips = [
            { slug:'pending',   label:'Pending Approval' },
            { slug:'active',    label:'Active' },
            { slug:'suspended', label:'Suspended' },
            { slug:'disabled',  label:'Disabled' },
            { slug:'all',       label:'All' },
        ];

        var colGrid = '1.5fr 1.2fr 1.1fr 1.3fr 0.9fr 0.8fr 1.3fr';

        return h(PageWrap, { title:'Vendors', mutedColor:mutedColor },

            /* Filter chips row */
            h('div', { style:{ display:'flex', gap:8, flexWrap:'wrap', marginBottom:S.lg } },
                chips.map(function(c) {
                    var active = c.slug === statusFilter;
                    return h('button', {
                        key: c.slug,
                        onClick: function(){ setStatusFilter(c.slug); },
                        style: {
                            fontSize:13,
                            padding:'6px 14px',
                            borderRadius:20,
                            border:'1px solid ' + (active ? TC.accent : borderColor),
                            background: active ? (dark ? 'rgba(37,99,235,.16)' : 'rgba(37,99,235,.08)') : 'transparent',
                            color: active ? TC.accent : textColor,
                            fontWeight: active ? 500 : 400,
                            cursor:'pointer',
                            transition:'all .15s ease',
                        }
                    }, c.label);
                })
            ),

            /* Toast */
            toast && h('div', {
                style:{
                    marginBottom:S.md,
                    padding:'10px 14px',
                    borderRadius:8,
                    background: toast.kind === 'ok' ? 'rgba(22,163,74,.12)' : 'rgba(220,38,38,.12)',
                    color: toast.kind === 'ok' ? '#16A34A' : '#DC2626',
                    fontSize:13,
                    border:'1px solid ' + (toast.kind === 'ok' ? 'rgba(22,163,74,.3)' : 'rgba(220,38,38,.3)'),
                }
            }, toast.text),

            /* Error banner */
            error && h('div', {
                style:{
                    marginBottom:S.md,
                    padding:'10px 14px',
                    borderRadius:8,
                    background:'rgba(220,38,38,.08)',
                    color:'#DC2626',
                    fontSize:13,
                    border:'1px solid rgba(220,38,38,.25)',
                }
            }, 'Error loading vendors: ' + error),

            /* List card */
            h(Card, { cardBg:cardBg, borderColor:borderColor, dark:dark, pad:0 },
                h('div', {
                    style:{
                        display:'grid',
                        gridTemplateColumns: colGrid,
                        padding: S.md + 'px ' + S.xl + 'px',
                        borderBottom:'1px solid ' + borderColor,
                        background: tableHeaderBg,
                        boxShadow: tableHeaderShadow,
                    }
                },
                    ['Store','Customer','Slug','Business Email','Created','Status',''].map(function(col) {
                        return h('span', { key:col||'actions', style:{ fontSize:10, fontWeight:500, color:mutedColor, textTransform:'uppercase', letterSpacing:'.5px' } }, col);
                    })
                ),
                loading && h(Spinner, { mutedColor:mutedColor }),
                !loading && vendors.length === 0 && h(EmptyState, {
                    mutedColor:mutedColor,
                    icon:'🏪',
                    title: statusFilter === 'pending' ? 'No pending applications' : ('No ' + statusFilter + ' vendors'),
                    sub: statusFilter === 'pending'
                        ? 'Vendor applications appear here for approval.'
                        : 'Switch filters above to find vendors in other states.'
                }),
                !loading && vendors.map(function(v, i) {
                    return h('div', {
                        key: v.id,
                        style:{
                            display:'grid',
                            gridTemplateColumns: colGrid,
                            padding:'14px ' + S.xl + 'px',
                            borderBottom: i < vendors.length-1 ? '1px solid ' + borderColor : 'none',
                            alignItems:'center',
                        }
                    },
                        h('div', {
                            onClick: function(){ window.location.hash = '#/vendors/' + v.id; },
                            style:{ cursor:'pointer' },
                            title: 'View vendor details'
                        },
                            h('div', { style:{ fontSize:13, fontWeight:500, color:TC.accent } }, v.display_name || '(no name)'),
                            v.description && h('div', { style:{ fontSize:11, color:mutedColor, marginTop:2, overflow:'hidden', textOverflow:'ellipsis', whiteSpace:'nowrap', maxWidth:220 } }, v.description)
                        ),
                        /* S200 — Customer column. first_name + last_name with
                         * email as fallback (covers customers who haven't
                         * filled the profile name). '—' when nothing known. */
                        h('div', null, (function(){
                            var fn = (v.customer_first_name || '').trim();
                            var ln = (v.customer_last_name  || '').trim();
                            var em = (v.customer_email      || '').trim();
                            var full = (fn + ' ' + ln).trim();
                            if (full) {
                                return [
                                    h('div', { key:'n', style:{ fontSize:13 } }, full),
                                    em && h('div', { key:'e', style:{ fontSize:11, color:mutedColor, marginTop:2, overflow:'hidden', textOverflow:'ellipsis', whiteSpace:'nowrap', maxWidth:200 } }, em)
                                ];
                            }
                            return em
                                ? h('span', { style:{ fontSize:12, color:mutedColor } }, em)
                                : h('span', { style:{ fontSize:12, color:mutedColor } }, '—');
                        })()),
                        h('span', { style:{ fontSize:12, color:mutedColor, fontFamily:'ui-monospace, monospace' } }, v.slug || '—'),
                        h('span', { style:{ fontSize:12 } }, v.business_email || '—'),
                        h('span', { style:{ fontSize:11, color:mutedColor } }, fmtDate(v.created_at)),
                        h('span', null, statusPill(v.status)),
                        renderActions(v)
                    );
                })
            ),

            /* Confirmation modals — S203: all 5 status-transition modals
             * capture an optional reason (audit trail flows into admin_notes
             * + tcmp_vendor_status_changed hook 4th arg). The 6th modal
             * (Recover) requires non-empty reason — admin overrides of
             * terminal state must be explicitly justified. */
            h(ConfirmModal, {
                open: !!confirmApprove,
                title: 'Approve vendor application?',
                detail: confirmApprove ? confirmApprove.display_name : null,
                message: 'The applicant becomes an active vendor and will be notified by email. They can immediately begin Stripe Connect onboarding and list products.',
                confirmLabel: 'Approve',
                tone: 'positive',
                busy: busy,
                showReason: true,
                reasonValue: reason,
                onReasonChange: setReason,
                reasonLabel: 'Approval note (optional)',
                reasonPlaceholder: 'e.g. Manually reviewed business documents — verified',
                onConfirm: function(){ transitionStatus(confirmApprove, 'active'); },
                onCancel: cancelAllModals,
                dark: dark, cardBg: cardBg, borderColor: borderColor, textColor: textColor, mutedColor: mutedColor,
            }),

            h(ConfirmModal, {
                open: !!confirmReject,
                title: 'Reject vendor application?',
                detail: confirmReject ? confirmReject.display_name : null,
                message: 'The applicant becomes disabled (terminal). They will be notified by email and will need to contact you to re-apply. This cannot be undone via the SPA.',
                confirmLabel: 'Reject application',
                busy: busy,
                showReason: true,
                reasonValue: reason,
                onReasonChange: setReason,
                reasonLabel: 'Rejection reason (optional)',
                reasonPlaceholder: 'e.g. Business documents could not be verified',
                onConfirm: function(){ transitionStatus(confirmReject, 'disabled'); },
                onCancel: cancelAllModals,
                dark: dark, cardBg: cardBg, borderColor: borderColor, textColor: textColor, mutedColor: mutedColor,
            }),

            h(ConfirmModal, {
                open: !!confirmSuspend,
                title: 'Suspend vendor?',
                detail: confirmSuspend ? confirmSuspend.display_name : null,
                message: 'The vendor will be unable to list new products or modify their catalog. Existing orders continue to settle normally. Re-activate later from this page.',
                confirmLabel: 'Suspend',
                busy: busy,
                showReason: true,
                reasonValue: reason,
                onReasonChange: setReason,
                reasonLabel: 'Suspension reason (optional)',
                reasonPlaceholder: 'e.g. Policy review hold — pending response from vendor',
                onConfirm: function(){ transitionStatus(confirmSuspend, 'suspended'); },
                onCancel: cancelAllModals,
                dark: dark, cardBg: cardBg, borderColor: borderColor, textColor: textColor, mutedColor: mutedColor,
            }),

            h(ConfirmModal, {
                open: !!confirmDisable,
                title: 'Disable vendor?',
                detail: confirmDisable ? confirmDisable.display_name : null,
                message: 'The vendor becomes disabled (terminal). All product listings are removed from the storefront. Pending Stripe transfers are blocked. This cannot be undone via the SPA — use Recover from the disabled state if disable-in-error.',
                confirmLabel: 'Disable vendor',
                busy: busy,
                showReason: true,
                reasonValue: reason,
                onReasonChange: setReason,
                reasonLabel: 'Disable reason (optional)',
                reasonPlaceholder: 'e.g. Vendor offboarded — terms violation confirmed',
                onConfirm: function(){ transitionStatus(confirmDisable, 'disabled'); },
                onCancel: cancelAllModals,
                dark: dark, cardBg: cardBg, borderColor: borderColor, textColor: textColor, mutedColor: mutedColor,
            }),

            h(ConfirmModal, {
                open: !!confirmReactivate,
                title: 'Re-activate vendor?',
                detail: confirmReactivate ? confirmReactivate.display_name : null,
                message: 'The vendor returns to active status and can resume listing products and modifying their catalog.',
                confirmLabel: 'Re-activate',
                tone: 'positive',
                busy: busy,
                showReason: true,
                reasonValue: reason,
                onReasonChange: setReason,
                reasonLabel: 'Re-activation note (optional)',
                reasonPlaceholder: 'e.g. Policy review complete — vendor cleared',
                onConfirm: function(){ transitionStatus(confirmReactivate, 'active'); },
                onCancel: cancelAllModals,
                dark: dark, cardBg: cardBg, borderColor: borderColor, textColor: textColor, mutedColor: mutedColor,
            }),

            /* S203 — Recover from terminal disabled state. Reason REQUIRED. */
            h(ConfirmModal, {
                open: !!confirmRecover,
                title: 'Recover disabled vendor?',
                detail: confirmRecover ? confirmRecover.display_name : null,
                message: 'This is an admin override of a terminal state. The vendor returns to active and their role + customer-group membership are restored. ⚠ Cascade-soft-deleted products and vendor↔product junctions are NOT auto-restored — that side of the cascade is one-way without manual SQL recovery (see S202 archive for the time-scoped UPDATE pattern). A justification is required for the audit trail.',
                confirmLabel: 'Recover vendor',
                tone: 'positive',
                busy: busy,
                busyLabel: 'Recovering…',
                showReason: true,
                reasonValue: reason,
                onReasonChange: setReason,
                reasonRequired: true,
                reasonLabel: 'Justification (required)',
                reasonPlaceholder: 'e.g. Vendor disabled in error — confirmed via support ticket #1234',
                onConfirm: function(){ recoverVendor(confirmRecover); },
                onCancel: cancelAllModals,
                dark: dark, cardBg: cardBg, borderColor: borderColor, textColor: textColor, mutedColor: mutedColor,
            })
        );
    }


    /* ════════════════════════════════════════════════════════════════════
     * FULFILLMENT PAGE — Shipment management
     * ════════════════════════════════════════════════════════════════════ */
    function FulfillmentPage({ subId, dark, cardBg, textColor, mutedColor, borderColor, tableHeaderBg }) {

        // ── Create/Edit view
        if (subId === 'new' || (subId && subId !== 'new')) {
            return h(ShipmentEditor, { shipmentId: subId === 'new' ? null : Number(subId), dark, cardBg, textColor, mutedColor, borderColor, tableHeaderBg });
        }

        // ── List view
        // S286 (#11): the Fulfillment landing now has two views — the derived
        // "Needs Fulfillment" queue (default) and the existing Shipments list.
        // The queue lists shippable orders that still have unshipped items
        // (no placeholder shipment rows); acting on one via the picker creates
        // a real shipment and the order drops out of the queue on its own.
        var [view,       setView]       = useState('queue'); // 'queue' | 'shipments'

        var [shipments,  setShipments]  = useState([]);
        var [loading,    setLoading]    = useState(true);
        var [total,      setTotal]      = useState(0);
        var [totalPages, setTotalPages] = useState(1);
        var [page,       setPage]       = useState(1);
        var [search,     setSearch]     = useState('');
        var [statusFilter, setStatusFilter] = useState('');
        var [statuses,   setStatuses]   = useState([]);
        var [msg,        setMsg]        = useState(null);

        // Needs-Fulfillment queue state.
        var [needs,        setNeeds]        = useState([]);
        var [needsLoading, setNeedsLoading] = useState(true);
        var [needsTotal,   setNeedsTotal]   = useState(0);
        var [needsPages,   setNeedsPages]   = useState(1);
        var [needsPage,    setNeedsPage]    = useState(1);

        function load() {
            setLoading(true);
            var q = new URLSearchParams({ page:page, per_page:TC_PER_PAGE });
            if (search) q.set('search', search);
            if (statusFilter) q.set('status_id', statusFilter);
            apiFetch('/shipments?' + q.toString()).then(function(d) {
                setShipments(d.shipments || []);
                setTotal(d.total || 0);
                setTotalPages(d.pages || 1);
                setLoading(false);
            }).catch(function(){ setLoading(false); });
        }

        function loadStatuses() {
            apiFetch('/shipments/statuses').then(function(d) {
                setStatuses(Array.isArray(d) ? d : []);
            }).catch(function(){});
        }

        function loadNeeds() {
            setNeedsLoading(true);
            var q = new URLSearchParams({ page:needsPage, per_page:TC_PER_PAGE });
            apiFetch('/shipments/needs-fulfillment?' + q.toString()).then(function(d) {
                setNeeds(d.orders || []);
                setNeedsTotal(d.total || 0);
                setNeedsPages(d.pages || 1);
                setNeedsLoading(false);
            }).catch(function(){ setNeedsLoading(false); });
        }

        // Queue loads on mount (so its tab badge has a count in either view)
        // and whenever its page changes. Returning from the picker remounts the
        // list view, so a freshly-shipped order disappears on the next view.
        useEffect(function(){ loadNeeds(); }, [needsPage]);

        // Shipments list loads only when its view is active.
        useEffect(function(){
            if (view === 'shipments') { load(); loadStatuses(); }
        }, [view, page, search, statusFilter]);

        function deleteShipment(s) {
            if (!confirm('Delete shipment #' + s.id + '?')) return;
            apiFetch('/shipments/' + s.id, { method:'DELETE' }).then(function() {
                setMsg({ type:'success', text:'Shipment deleted.' });
                load();
                setTimeout(function(){ setMsg(null); }, 2500);
            }).catch(function(){});
        }

        function fmtDate(d) { return tcFmtDate(d); }

        // Build status tabs
        var statusTabs = [{ v:'', l:'All' }];
        statuses.forEach(function(s) { statusTabs.push({ v:String(s.id), l:s.label||s.handle, color:s.color }); });

        var colGrid      = '0.6fr 0.8fr 2fr 1fr 1fr 1fr 0.8fr 0.6fr';
        var colGridQueue = '0.9fr 2fr 1fr 1fr 1fr 1fr 1.1fr';

        // ── View toggle (Needs Fulfillment | Shipments) ──
        function viewToggle() {
            var tabs = [
                { v:'queue',     l:'Needs Fulfillment', n:needsTotal },
                { v:'shipments', l:'Shipments',         n:null }
            ];
            return h('div', { style:{ display:'flex', gap:S.xs, marginBottom:S.lg, borderBottom:'1px solid '+borderColor } },
                tabs.map(function(t) {
                    var isActive = view === t.v;
                    return h('button', { key:t.v, onClick:function(){ setView(t.v); },
                        style:{ padding:'8px '+S.md+'px', border:'none', borderBottom:'2px solid '+(isActive?TC.accent:'transparent'),
                                background:'transparent', cursor:'pointer', fontSize:13, fontWeight:isActive?600:500,
                                color:isActive?(dark?'#fff':TC.text):mutedColor, marginBottom:-1, transition:'all .15s' } },
                        t.l,
                        (t.n != null && t.n > 0) && h('span', { style:{ marginLeft:6, fontSize:11, fontWeight:600,
                            background:isActive?TC.accent:(dark?'rgba(255,255,255,.1)':'#EEF1F5'), color:isActive?'#fff':mutedColor,
                            borderRadius:20, padding:'1px 7px' } }, String(t.n))
                    );
                })
            );
        }

        // ── Needs-Fulfillment queue table ──
        function queueView() {
            return h(Card, { cardBg, borderColor, dark, pad:0 },
                h('div', { style:{ display:'grid', gridTemplateColumns:colGridQueue, padding:S.md+'px '+S.xl+'px', borderBottom:'1px solid '+borderColor, background:tableHeaderBg, boxShadow:tableHeaderShadow } },
                    ['Order','Customer','To Ship','Total','Status','Placed',''].map(function(col, ci) {
                        return h('span', { key:col+ci, style:{ fontSize:10, fontWeight:500, color:mutedColor, textTransform:'uppercase', letterSpacing:'.5px' } }, col);
                    })
                ),

                needsLoading && h(Spinner, { mutedColor }),
                !needsLoading && needs.length === 0 && h(EmptyState, { mutedColor, icon:'✅', title:'Nothing awaiting fulfillment', sub:'Shippable orders with unshipped items show up here automatically.' }),

                !needsLoading && needs.map(function(o, i) {
                    var custName = [o.billing_first_name, o.billing_last_name].filter(Boolean).join(' ') || '—';
                    var units = Number(o.remaining_units) || 0;
                    return h('div', { key:o.id,
                        style:{ display:'grid', gridTemplateColumns:colGridQueue, padding:'12px '+S.xl+'px', borderBottom: i<needs.length-1 ? '1px solid '+borderColor : 'none', alignItems:'center', cursor:'pointer', transition:'background .12s' },
                        onMouseEnter:function(e){ e.currentTarget.style.background=dark?'rgba(255,255,255,.02)':'#F9FAFB'; },
                        onMouseLeave:function(e){ e.currentTarget.style.background='transparent'; },
                        onClick:function(){ navigate('fulfillment/new/' + o.id); }
                    },
                        h('span', { style:{ fontSize:12, fontWeight:500, color:TC.accent } }, o.order_number || '#' + o.id),
                        h('div', null,
                            h('div', { style:{ fontSize:12, fontWeight:500 } }, custName),
                            o.billing_email && h('div', { style:{ fontSize:11, color:mutedColor } }, o.billing_email)
                        ),
                        h('span', { style:{ fontSize:12, fontWeight:600 } }, units + (units === 1 ? ' item' : ' items')),
                        h('span', { style:{ fontSize:12 } }, tcFmtMoney(o.total, o.currency)),
                        h('span', { style:{ fontSize:11, fontWeight:500, color:o.status_color||'#71797E', background:(o.status_color||'#71797E')+'18', padding:'2px 8px', borderRadius:20, display:'inline-block', width:'fit-content' } }, o.status_label || o.status_handle || '—'),
                        h('span', { style:{ fontSize:11, color:mutedColor } }, fmtDate(o.created_at)),
                        h('span', { style:{ fontSize:12, fontWeight:500, color:TC.accent, textAlign:'right' } }, 'Create Shipment →')
                    );
                }),

                !needsLoading && needs.length > 0 && h('div', { style:{ display:'flex', justifyContent:'space-between', alignItems:'center', padding:S.md+'px '+S.xl+'px', borderTop:'1px solid '+borderColor } },
                    h('span', { style:{ fontSize:11, color:mutedColor } }, 'Showing ' + needs.length + ' of ' + needsTotal),
                    h(Pager, { page:needsPage, totalPages:needsPages, onPage:setNeedsPage, mutedColor, borderColor })
                )
            );
        }

        // ── Shipments list (status tabs + search + table) ──
        function shipmentsView() {
            return h('div', null,
                // Status tabs
                h('div', { style:{ display:'flex', gap:S.xs, marginBottom:S.lg, flexWrap:'wrap' } },
                    statusTabs.map(function(t) {
                        var isActive = statusFilter === t.v;
                        return h('button', { key:t.v, onClick:function(){ setStatusFilter(t.v); setPage(1); },
                            style:{ padding:'6px '+S.md+'px', borderRadius:8, border:'none', cursor:'pointer', fontSize:12, fontWeight:isActive?600:400,
                                    background:isActive?TC.accent:'transparent', color:isActive?'#fff':mutedColor, transition:'all .15s' } }, t.l);
                    })
                ),

                // Search
                h('div', { style:{ marginBottom:S.lg } },
                    h(SearchBox, { value:search, onChange:function(v){ setSearch(v); setPage(1); }, placeholder:'Search by tracking # or order #…', borderColor, dark, mutedColor })
                ),

                // Table
                h(Card, { cardBg, borderColor, dark, pad:0 },
                    h('div', { style:{ display:'grid', gridTemplateColumns:colGrid, padding:S.md+'px '+S.xl+'px', borderBottom:'1px solid '+borderColor, background:tableHeaderBg, boxShadow:tableHeaderShadow } },
                        ['ID','Order','Customer','Carrier','Tracking','Status','Date',''].map(function(col) {
                            return h('span', { key:col, style:{ fontSize:10, fontWeight:500, color:mutedColor, textTransform:'uppercase', letterSpacing:'.5px' } }, col);
                        })
                    ),

                    loading && h(Spinner, { mutedColor }),
                    !loading && shipments.length === 0 && h(EmptyState, { mutedColor, icon:'🚚', title:'No shipments yet', sub:'Create a shipment from an order to start fulfilling.' }),

                    !loading && shipments.map(function(s, i) {
                        var custName = [s.billing_first_name, s.billing_last_name].filter(Boolean).join(' ') || '—';
                        return h('div', { key:s.id,
                            style:{ display:'grid', gridTemplateColumns:colGrid, padding:'12px '+S.xl+'px', borderBottom: i<shipments.length-1 ? '1px solid '+borderColor : 'none', alignItems:'center', cursor:'pointer', transition:'background .12s' },
                            onMouseEnter:function(e){ e.currentTarget.style.background=dark?'rgba(255,255,255,.02)':'#F9FAFB'; },
                            onMouseLeave:function(e){ e.currentTarget.style.background='transparent'; },
                            onClick:function(){ navigate('fulfillment/' + s.id); }
                        },
                            h('span', { style:{ fontSize:12, fontWeight:500, color:TC.accent } }, '#' + s.id),
                            h('span', { style:{ fontSize:12, color:TC.accent, cursor:'pointer' } }, s.order_number || '#' + s.order_id),
                            h('div', null,
                                h('div', { style:{ fontSize:12, fontWeight:500 } }, custName),
                                s.billing_email && h('div', { style:{ fontSize:11, color:mutedColor } }, s.billing_email)
                            ),
                            h('span', { style:{ fontSize:12, color:mutedColor } }, s.carrier || '—'),
                            h('span', { style:{ fontSize:12, fontFamily:'monospace' } }, s.tracking_number || '—'),
                            h('span', { style:{ fontSize:11, fontWeight:500, color:s.status_color||'#71797E', background:(s.status_color||'#71797E')+'18', padding:'2px 8px', borderRadius:20, display:'inline-block', width:'fit-content' } }, s.status_label || 'Pending'),
                            h('span', { style:{ fontSize:11, color:mutedColor } }, s.shipped_at ? fmtDate(s.shipped_at) : (s.created_at ? 'Created '+fmtDate(s.created_at) : '—')),
                            h('button', { onClick:function(e){ e.stopPropagation(); deleteShipment(s); },
                                style:{ background:'transparent', border:'1px solid rgba(220,38,38,.2)', borderRadius:6, padding:'3px 8px', fontSize:11, cursor:'pointer', color:TC.error } }, '🗑')
                        );
                    }),

                    !loading && h('div', { style:{ display:'flex', justifyContent:'space-between', alignItems:'center', padding:S.md+'px '+S.xl+'px', borderTop:'1px solid '+borderColor } },
                        h('span', { style:{ fontSize:11, color:mutedColor } }, 'Showing ' + shipments.length + ' of ' + total),
                        h(Pager, { page, totalPages, onPage:setPage, mutedColor, borderColor })
                    )
                )
            );
        }

        var pageSub = view === 'queue'
            ? (needsTotal + (needsTotal === 1 ? ' order awaiting fulfillment' : ' orders awaiting fulfillment'))
            : (total + ' shipments');

        return h(PageWrap, { title:'Fulfillment', sub:pageSub, mutedColor,
            action: h(Btn, { variant:'primary', onClick:function(){ navigate('fulfillment/new'); }, mutedColor, borderColor }, '+ Create Shipment')
        },
            msg && h('div', { style:{ padding:'10px 16px', borderRadius:8, marginBottom:S.lg, fontSize:13, fontWeight:500,
                background: msg.type==='success' ? (dark?'rgba(22,163,74,.12)':'#ECFDF5') : (dark?'rgba(220,38,38,.12)':'#FEF2F2'),
                color: msg.type==='success' ? '#16A34A' : '#DC2626' } }, msg.text),

            viewToggle(),

            view === 'queue' ? queueView() : shipmentsView()
        );
    }

    /* ── Shipment Editor (create / view+edit) ── */
    function ShipmentEditor({ shipmentId, dark, cardBg, textColor, mutedColor, borderColor, tableHeaderBg }) {
        var isNew = !shipmentId;
        var fs = fieldSty(dark, borderColor);
        var labelSty = { fontSize:12, fontWeight:500, display:'block', marginBottom:S.xs, color:mutedColor };

        var [loading,   setLoading]   = useState(!isNew);
        var [saving,    setSaving]    = useState(false);
        var [msg,       setMsg]       = useState(null);
        var [statuses,  setStatuses]  = useState([]);
        var [carriers,  setCarriers]  = useState([]);

        // Shipment form
        var [form, setForm] = useState({
            order_id:'', tracking_number:'', carrier:'', status_id:'', comments:'', shipped_at:''
        });

        // For new: order items to pick from
        var [orderId,    setOrderId]    = useState('');
        var [unshipped,  setUnshipped]  = useState([]);
        var [itemQtys,   setItemQtys]   = useState({});
        var [loadingItems, setLoadingItems] = useState(false);

        // S282: order picker — a debounced live search replaces the raw numeric
        // "Order ID" input. orderId stays the INTERNAL numeric id (= order.id),
        // which /orders/{id}/unshipped + POST /shipments require; the customer-facing
        // order_number (TC-…) is what the user types/sees. Search hits the orders
        // list endpoint (search matches order_number/email/name; status filters by
        // handle) so a fulfiller can find the order without knowing the DB id.
        var [orderQ,      setOrderQ]      = useState('');   // current search text
        var [orderRes,    setOrderRes]    = useState([]);   // matched orders
        var [orderBusy,   setOrderBusy]   = useState(false);
        var [pickedOrder, setPickedOrder] = useState(null); // selected order summary (chip)
        var orderTimer = useRef(null);

        // For existing: loaded shipment data
        var [shipment,   setShipment]   = useState(null);

        function flash(type, text) { setMsg({ type:type, text:text }); setTimeout(function(){ setMsg(null); }, 3000); }
        function upd(k, v) { setForm(function(prev) { var n = Object.assign({}, prev); n[k] = v; return n; }); }

        useEffect(function() {
            apiFetch('/shipments/statuses').then(function(d) { setStatuses(Array.isArray(d)?d:[]); }).catch(function(){});
            apiFetch('/shipments/carriers').then(function(d) { setCarriers(Array.isArray(d)?d:[]); }).catch(function(){});

            if (shipmentId) {
                setLoading(true);
                apiFetch('/shipments/' + shipmentId).then(function(d) {
                    setShipment(d);
                    setForm({
                        order_id: String(d.order_id||''),
                        tracking_number: d.tracking_number || '',
                        carrier: d.carrier || '',
                        status_id: String(d.status_id || ''),
                        comments: d.comments || '',
                        shipped_at: d.shipped_at_input || '',
                    });
                    setLoading(false);
                }).catch(function(){ setLoading(false); });
            } else {
                // S286 (#11): pre-select an order when arriving from the
                // Needs-Fulfillment queue. The hash router only exposes
                // route/subId, so the order id rides as a third segment
                // (#/fulfillment/new/<orderId>); read it directly, fetch the
                // order summary for the chip, and load its unshipped items —
                // exactly as if the fulfiller had searched + picked it.
                var _hp = (window.location.hash || '').replace(/^#\/?/, '').split('/');
                var presetOrderId = Number(_hp[2] || 0);
                if (presetOrderId) {
                    apiFetch('/orders/' + presetOrderId).then(function(o) {
                        if (o && o.id) pickOrder(o);
                    }).catch(function(){});
                }
            }
        }, [shipmentId]);

        // Load unshipped items when order ID changes (new mode)
        // S282: accepts an optional explicit id so pickOrder() can load items in the
        // same tick without waiting on the async setOrderId() state update.
        function loadUnshipped(explicitId) {
            var oid = Number(explicitId != null ? explicitId : orderId);
            if (!oid) { setUnshipped([]); return; }
            setLoadingItems(true);
            apiFetch('/orders/' + oid + '/unshipped').then(function(d) {
                setUnshipped(Array.isArray(d) ? d : []);
                var qtys = {};
                (Array.isArray(d) ? d : []).forEach(function(item) { qtys[item.order_item_id] = item.remaining; });
                setItemQtys(qtys);
                setLoadingItems(false);
            }).catch(function(){ setLoadingItems(false); setUnshipped([]); });
        }

        // S282: debounced order search (250ms, mirrors the promotion name-picker).
        function searchOrders(q) {
            setOrderQ(q);
            if (orderTimer.current) clearTimeout(orderTimer.current);
            if (!q.trim()) { setOrderBusy(false); setOrderRes([]); return; }
            setOrderBusy(true);
            orderTimer.current = setTimeout(function(){
                var qs = new URLSearchParams({ search:q, status:'processing', per_page:10 });
                apiFetch('/orders?' + qs).then(function(d){
                    var rows = Array.isArray(d) ? d : (d.orders || d.items || []);
                    setOrderRes(rows);
                    setOrderBusy(false);
                }).catch(function(){ setOrderBusy(false); setOrderRes([]); });
            }, 250);
        }
        // Select an order from the results → set the internal id + load its items now.
        function pickOrder(o) {
            if (orderTimer.current) clearTimeout(orderTimer.current);
            setOrderId(String(o.id));
            setPickedOrder(o);
            setOrderQ(''); setOrderRes([]); setOrderBusy(false);
            loadUnshipped(o.id); // explicit id — orderId state hasn't flushed yet
        }
        // Clear the selection to search for a different order.
        function clearPickedOrder() {
            if (orderTimer.current) clearTimeout(orderTimer.current);
            setPickedOrder(null); setOrderId('');
            setUnshipped([]); setItemQtys({});
            setOrderQ(''); setOrderRes([]); setOrderBusy(false);
        }
        // Display name for an order row (billing name → email fallback), mirrors OrdersPage.
        function orderPickName(o) {
            var nm = [o.billing_first_name, o.billing_last_name].filter(Boolean).join(' ');
            return nm || o.billing_email || o.email || '\u2014';
        }

        function saveShipment() {
            setSaving(true);
            if (isNew) {
                // Collect items with qty > 0
                var items = [];
                Object.keys(itemQtys).forEach(function(oiId) {
                    var qty = Number(itemQtys[oiId]) || 0;
                    if (qty > 0) items.push({ order_item_id: Number(oiId), quantity: qty });
                });
                if (!items.length) { flash('error','Select at least one item to ship.'); setSaving(false); return; }

                apiFetch('/shipments', { method:'POST', body:{
                    order_id: Number(orderId),
                    items: items,
                    tracking_number: form.tracking_number,
                    carrier: form.carrier,
                    status_id: form.status_id ? Number(form.status_id) : undefined,
                    comments: form.comments,
                    shipped_at: form.shipped_at || undefined,
                }}).then(function(d) {
                    setSaving(false);
                    flash('success','Shipment created.');
                    if (d && d.id) navigate('fulfillment/' + d.id);
                }).catch(function(e) { setSaving(false); flash('error', e.message || 'Failed.'); });
            } else {
                apiFetch('/shipments/' + shipmentId, { method:'PUT', body:{
                    tracking_number: form.tracking_number,
                    carrier: form.carrier,
                    status_id: form.status_id ? Number(form.status_id) : undefined,
                    comments: form.comments,
                    shipped_at: form.shipped_at || undefined,
                }}).then(function() { setSaving(false); flash('success','Shipment updated.'); }).catch(function(e) { setSaving(false); flash('error', e.message || 'Failed.'); });
            }
        }

        if (loading) return h('main', { style:{ flex:1, overflow:'auto', padding:S.xxl } }, h(Spinner, { mutedColor }));

        return h('main', { style:{ flex:1, overflow:'auto', padding:S.xxl } },
            // Header
            h('div', { style:{ display:'flex', justifyContent:'space-between', alignItems:'center', marginBottom:S.xl } },
                h('div', { style:{ display:'flex', alignItems:'center', gap:S.md } },
                    h('button', { onClick:function(){ navigate('fulfillment'); },
                        style:{ background:'transparent', border:'1px solid '+borderColor, borderRadius:8, padding:'6px 10px', cursor:'pointer', color:mutedColor, display:'flex', alignItems:'center', gap:4, fontSize:13 } },
                        h('span', { style:{ fontSize:16 } }, '←'), ' Fulfillment'
                    ),
                    h('h2', { style:{ fontSize:20, fontWeight:700, letterSpacing:'-.3px' } },
                        isNew ? 'Create Shipment' : 'Shipment #' + shipmentId
                    ),
                    shipment && h('span', { style:{ fontSize:12, fontWeight:500, color:shipment.status_color||'#71797E', background:(shipment.status_color||'#71797E')+'18', padding:'2px 8px', borderRadius:20 } }, shipment.status_label||'Pending')
                ),
                h(Btn, { variant:'primary', onClick:saveShipment, disabled:saving, mutedColor, borderColor },
                    saving ? 'Saving…' : (isNew ? 'Create Shipment' : 'Save Changes')
                )
            ),

            msg && h('div', { style:{ padding:'10px 16px', borderRadius:8, marginBottom:S.lg, fontSize:13, fontWeight:500,
                background: msg.type==='success' ? (dark?'rgba(22,163,74,.12)':'#ECFDF5') : (dark?'rgba(220,38,38,.12)':'#FEF2F2'),
                color: msg.type==='success' ? '#16A34A' : '#DC2626' } }, msg.text),

            h('div', { style:{ display:'grid', gridTemplateColumns:'1fr 340px', gap:S.lg, alignItems:'start' } },

                // ═══ LEFT COLUMN ═══
                h('div', null,

                    // ── Order + Items card
                    // S282: overflow:visible so the order-search dropdown can escape the
                    // Card's default overflow:hidden (otherwise clipped at the card edge — #272).
                    h(Card, { cardBg, borderColor, dark, style:{ marginBottom:S.lg, overflow: isNew ? 'visible' : 'hidden' } },
                        h('h3', { style:{ fontSize:14, fontWeight:600, marginBottom:S.lg } }, isNew ? 'Select Order & Items' : 'Shipment Items'),

                        // New mode: order picker — debounced live search over processing orders
                        isNew && h('div', { style:{ marginBottom:S.lg } },
                            h('label', { style:labelSty }, 'Order'),

                            // Selected-order chip
                            pickedOrder && h('div', { style:{ display:'flex', alignItems:'center', justifyContent:'space-between', gap:S.md, padding:'10px 14px', borderRadius:8, border:'1px solid '+borderColor, background:dark?'rgba(255,255,255,.03)':'#F9FAFB' } },
                                h('div', null,
                                    h('div', { style:{ fontSize:13, fontWeight:600, color:TC.accent } }, pickedOrder.order_number || ('#'+pickedOrder.id)),
                                    h('div', { style:{ fontSize:11, color:mutedColor } },
                                        orderPickName(pickedOrder) + ' \u00B7 ' + (pickedOrder.total!=null ? tcFmtMoney(pickedOrder.total, pickedOrder.currency) : '\u2014') + ' \u00B7 ' + tcFmtDate(pickedOrder.created_at))
                                ),
                                h(Btn, { variant:'outline', onClick:clearPickedOrder, mutedColor, borderColor }, 'Change')
                            ),

                            // Search input + results dropdown
                            !pickedOrder && h('div', { style:{ position:'relative' } },
                                h('input', { type:'text', value:orderQ,
                                    onChange:function(e){ searchOrders(e.target.value); },
                                    placeholder:'Search by order #, customer, or email\u2026',
                                    style:Object.assign({},fs,{ width:'100%' }) }),
                                (orderBusy || orderRes.length > 0 || (orderQ.trim() && !orderBusy)) && h('div', {
                                    style:{ position:'absolute', zIndex:30, top:'100%', left:0, right:0, marginTop:2, background:cardBg||'#fff', border:'1px solid '+borderColor, borderRadius:6, maxHeight:260, overflowY:'auto', boxShadow:'0 6px 18px rgba(0,0,0,.14)' } },
                                    orderBusy && h('div', { style:{ padding:'8px 10px', fontSize:12, color:mutedColor } }, 'Searching\u2026'),
                                    !orderBusy && orderRes.length === 0 && orderQ.trim() && h('div', { style:{ padding:'8px 10px', fontSize:12, color:mutedColor } }, 'No processing orders match.'),
                                    !orderBusy && orderRes.map(function(o){
                                        return h('div', { key:o.id,
                                            onClick:function(){ pickOrder(o); },
                                            style:{ display:'flex', alignItems:'center', justifyContent:'space-between', gap:8, padding:'8px 10px', cursor:'pointer', borderBottom:'1px solid '+(dark?'rgba(255,255,255,.05)':'#F1F1F1') },
                                            onMouseEnter:function(e){ e.currentTarget.style.background = dark?'rgba(255,255,255,.05)':'#F7F9FC'; },
                                            onMouseLeave:function(e){ e.currentTarget.style.background = 'transparent'; } },
                                            h('div', null,
                                                h('span', { style:{ fontSize:13, fontWeight:600, color:TC.accent } }, o.order_number || ('#'+o.id)),
                                                h('span', { style:{ fontSize:12, color:mutedColor, marginLeft:8 } }, orderPickName(o))
                                            ),
                                            h('div', { style:{ textAlign:'right' } },
                                                h('div', { style:{ fontSize:12, fontWeight:600 } }, o.total!=null ? tcFmtMoney(o.total, o.currency) : '\u2014'),
                                                h('div', { style:{ fontSize:11, color:mutedColor } }, tcFmtDate(o.created_at))
                                            )
                                        );
                                    })
                                )
                            ),

                            h('div', { style:{ fontSize:11, color:mutedColor, marginTop:6 } }, 'Showing orders ready to fulfill (Processing). Select one to load its unshipped items.')
                        ),

                        // New mode: item selection
                        isNew && unshipped.length > 0 && h('div', null,
                            h('div', { style:{ display:'grid', gridTemplateColumns:'1fr 80px 80px 100px', padding:S.sm+'px 0', borderBottom:'1px solid '+borderColor, marginBottom:S.sm } },
                                ['Product','Ordered','Remaining','Ship Qty'].map(function(col) { return h('span',{key:col,style:{fontSize:10,fontWeight:500,color:mutedColor,textTransform:'uppercase',letterSpacing:'.5px'}},col); })
                            ),
                            unshipped.map(function(item) {
                                if (item.remaining <= 0) return null;
                                return h('div', { key:item.order_item_id, style:{ display:'grid', gridTemplateColumns:'1fr 80px 80px 100px', padding:'8px 0', alignItems:'center', borderBottom:'1px solid '+borderColor } },
                                    h('div', { style:{ display:'flex', alignItems:'center', gap:S.sm } },
                                        item.thumbnail ? h('img', { src:item.thumbnail, style:{ width:36, height:Config.productImageShape==='square'?36:48, borderRadius:6, objectFit:'cover' } })
                                            : h('div', { style:{ width:36, height:Config.productImageShape==='square'?36:48, borderRadius:6, background:dark?'#1E2130':'#F3F4F6', display:'flex', alignItems:'center', justifyContent:'center', fontSize:14 } }, '📦'),
                                        h('div', null,
                                            h('div', { style:{ fontSize:12, fontWeight:500 } }, item.name || '—'),
                                            item.sku && h('div', { style:{ fontSize:11, color:mutedColor } }, item.sku)
                                        )
                                    ),
                                    h('span', { style:{ fontSize:12, textAlign:'center' } }, item.ordered),
                                    h('span', { style:{ fontSize:12, textAlign:'center', color:item.remaining>0?TC.accent:TC.error, fontWeight:500 } }, item.remaining),
                                    h('input', { type:'number', min:0, max:item.remaining, value:itemQtys[item.order_item_id]||0,
                                        onChange:function(e){ var v = Math.min(Number(e.target.value)||0, item.remaining); setItemQtys(function(prev){ var n=Object.assign({},prev); n[item.order_item_id]=v; return n; }); },
                                        style:Object.assign({},fs,{ width:80, textAlign:'center' }) })
                                );
                            })
                        ),

                        isNew && loadingItems && h(Spinner, { mutedColor }),
                        isNew && orderId && !loadingItems && unshipped.length === 0 && h('div', { style:{ padding:S.lg, textAlign:'center', color:mutedColor, fontSize:13 } }, 'No unshipped items found for this order.'),

                        // Edit mode: show shipped items (read-only)
                        !isNew && shipment && Array.isArray(shipment.items) && h('div', null,
                            h('div', { style:{ display:'grid', gridTemplateColumns:'1fr 80px 80px', padding:S.sm+'px 0', borderBottom:'1px solid '+borderColor, marginBottom:S.sm } },
                                ['Product','Ordered','Shipped'].map(function(col) { return h('span',{key:col,style:{fontSize:10,fontWeight:500,color:mutedColor,textTransform:'uppercase',letterSpacing:'.5px'}},col); })
                            ),
                            shipment.items.map(function(item, i) {
                                return h('div', { key:i, style:{ display:'grid', gridTemplateColumns:'1fr 80px 80px', padding:'8px 0', alignItems:'center', borderBottom: i<shipment.items.length-1 ? '1px solid '+borderColor : 'none' } },
                                    h('div', null,
                                        h('div', { style:{ fontSize:12, fontWeight:500 } }, item.product_name || '—'),
                                        item.product_code && h('div', { style:{ fontSize:11, color:mutedColor } }, item.product_code)
                                    ),
                                    h('span', { style:{ fontSize:12, textAlign:'center' } }, item.ordered_qty || '—'),
                                    h('span', { style:{ fontSize:12, textAlign:'center', fontWeight:600, color:TC.accent } }, item.quantity)
                                );
                            })
                        ),
                        !isNew && shipment && shipment.order_number && h('div', { style:{ marginTop:S.lg } },
                            h('a', { href:'#', onClick:function(e){ e.preventDefault(); navigate('orders/'+shipment.order_id); }, style:{ fontSize:12, color:TC.accent, textDecoration:'none' } }, '← View order ' + shipment.order_number)
                        )
                    ),

                    // ── Comments card
                    h(Card, { cardBg, borderColor, dark },
                        h('h3', { style:{ fontSize:14, fontWeight:600, marginBottom:S.md } }, 'Shipment Comments'),
                        h('textarea', { value:form.comments, onChange:function(e){ upd('comments', e.target.value); }, rows:4, placeholder:'Internal notes about this shipment…',
                            style:Object.assign({},fs,{ width:'100%', resize:'vertical' }) })
                    )
                ),

                // ═══ RIGHT COLUMN ═══
                h('div', null,

                    // ── Tracking card
                    h(Card, { cardBg, borderColor, dark, style:{ marginBottom:S.lg } },
                        h('h4', { style:{ fontSize:13, fontWeight:600, marginBottom:S.lg, color:mutedColor, textTransform:'uppercase', letterSpacing:'.5px' } }, 'Tracking'),
                        h('div', { style:{ marginBottom:S.lg } },
                            h('label', { style:labelSty }, 'Carrier'),
                            h('select', { value:form.carrier, onChange:function(e){ upd('carrier', e.target.value); }, style:Object.assign({},fs,{ width:'100%' }) },
                                h('option', { value:'' }, '— Select carrier —'),
                                carriers.map(function(c) { return h('option', { key:c.code, value:c.code }, c.name); })
                            )
                        ),
                        h('div', { style:{ marginBottom:S.lg } },
                            h('label', { style:labelSty }, 'Tracking Number'),
                            h('input', { value:form.tracking_number, onChange:function(e){ upd('tracking_number', e.target.value); }, placeholder:'e.g. 1Z999AA10123456784',
                                style:Object.assign({},fs,{ width:'100%', fontFamily:'monospace' }) })
                        ),
                        h('div', null,
                            h('label', { style:labelSty }, 'Shipped Date'),
                            h('input', { type:'datetime-local', value:form.shipped_at, onChange:function(e){ upd('shipped_at', e.target.value); },
                                style:Object.assign({},fs,{ width:'100%' }) })
                        )
                    ),

                    // ── Status card
                    h(Card, { cardBg, borderColor, dark, style:{ marginBottom:S.lg } },
                        h('h4', { style:{ fontSize:13, fontWeight:600, marginBottom:S.lg, color:mutedColor, textTransform:'uppercase', letterSpacing:'.5px' } }, 'Fulfillment Status'),
                        h('select', { value:form.status_id, onChange:function(e){ upd('status_id', e.target.value); }, style:Object.assign({},fs,{ width:'100%' }) },
                            h('option', { value:'' }, '— Default —'),
                            statuses.map(function(s) { return h('option', { key:s.id, value:String(s.id) }, s.label||s.handle); })
                        )
                    ),

                    // ── Order info (edit mode)
                    !isNew && shipment && h(Card, { cardBg, borderColor, dark },
                        h('h4', { style:{ fontSize:13, fontWeight:600, marginBottom:S.lg, color:mutedColor, textTransform:'uppercase', letterSpacing:'.5px' } }, 'Order Info'),
                        [
                            ['Order', shipment.order_number || '#' + shipment.order_id],
                            ['Customer', [shipment.billing_first_name, shipment.billing_last_name].filter(Boolean).join(' ') || '—'],
                            ['Email', shipment.billing_email || '—'],
                            ['Shipping Method', shipment.shipping_method_name || '—'],
                            ['Created', tcFmtDate(shipment.created_at)],
                        ].map(function(pair) {
                            return h('div', { key:pair[0], style:{ display:'flex', justifyContent:'space-between', fontSize:12, marginBottom:S.sm } },
                                h('span', { style:{ color:mutedColor } }, pair[0]),
                                h('span', { style:{ fontWeight:500 } }, pair[1])
                            );
                        })
                    )
                )
            )
        );
    }

    /* ════════════════════════════════════════════════════════════════════
     * LOYALTY REWARDS PAGE — 4-tab admin page
     * ════════════════════════════════════════════════════════════════════ */

    /* ════════════════════════════════════════════════════════════════════
     * TAGS PAGE — Product tag management
     * ════════════════════════════════════════════════════════════════════ */
    function TagsPage({ dark, cardBg, textColor, mutedColor, borderColor, tableHeaderBg }) {
        var fs = fieldSty(dark, borderColor);
        var labelSty = { fontSize:12, fontWeight:500, display:'block', marginBottom:S.xs, color:mutedColor };

        var [tags, setTags] = useState([]);
        var [loading, setLoading] = useState(true);
        var [total, setTotal] = useState(0);
        var [totalPages, setTotalPages] = useState(1);
        var [page, setPage] = useState(1);
        var [search, setSearch] = useState('');
        var [sort, setSort] = useState('name');
        var [order, setOrder] = useState('ASC');
        var [msg, setMsg] = useState(null);

        // Modal state
        var [modal, setModal] = useState(null); // null | 'create' | 'edit' | 'merge'
        var [editTag, setEditTag] = useState(null);
        var [tagName, setTagName] = useState('');
        var [saving, setSaving] = useState(false);
        var [selected, setSelected] = useState({});
        var [mergeTarget, setMergeTarget] = useState('');

        function flash(type, text) { setMsg({ type:type, text:text }); setTimeout(function(){ setMsg(null); }, 3000); }

        function load() {
            setLoading(true);
            var q = new URLSearchParams({ page:page, per_page:TC_PER_PAGE, sort:sort, order:order });
            if (search) q.set('search', search);
            apiFetch('/tags?' + q.toString()).then(function(d) {
                setTags(d.tags||[]); setTotal(d.total||0); setTotalPages(d.pages||1); setLoading(false);
            }).catch(function(){ setLoading(false); });
        }

        useEffect(function(){ load(); }, [page, search, sort, order]);

        function toggleSort(col) {
            if (sort === col) { setOrder(order === 'ASC' ? 'DESC' : 'ASC'); }
            else { setSort(col); setOrder('ASC'); }
        }

        // Edit
        function openEdit(tag) { setEditTag(tag); setTagName(tag.name); setModal('edit'); }
        function doEdit() {
            if (!editTag || !tagName.trim()) return;
            setSaving(true);
            apiFetch('/tags/' + editTag.id, { method:'PUT', body:{ name:tagName.trim() } }).then(function() {
                setSaving(false); setModal(null); flash('success','Tag renamed.'); load();
            }).catch(function(e) { setSaving(false); flash('error', e.message||'Failed.'); });
        }

        // Delete
        function doDelete(tag) {
            if (!confirm('Delete tag "' + tag.name + '"? It will be unlinked from all products.')) return;
            apiFetch('/tags/' + tag.id, { method:'DELETE' }).then(function() { flash('success','Tag deleted.'); load(); }).catch(function(){});
        }

        // Bulk delete
        function bulkDelete() {
            var ids = Object.keys(selected).filter(function(k){ return selected[k]; });
            if (!ids.length || !confirm('Delete ' + ids.length + ' tags?')) return;
            Promise.all(ids.map(function(id){ return apiFetch('/tags/' + id, { method:'DELETE' }); })).then(function() {
                flash('success', ids.length + ' tags deleted.'); setSelected({}); load();
            });
        }

        // Merge
        function openMerge() {
            var ids = Object.keys(selected).filter(function(k){ return selected[k]; });
            if (ids.length < 2) { flash('error','Select at least 2 tags to merge.'); return; }
            setMergeTarget(ids[0]);
            setModal('merge');
        }
        function doMerge() {
            var ids = Object.keys(selected).filter(function(k){ return selected[k]; }).map(Number);
            var target = Number(mergeTarget);
            var sources = ids.filter(function(id){ return id !== target; });
            if (!sources.length) return;
            setSaving(true);
            apiFetch('/tags/merge', { method:'POST', body:{ source_ids:sources, target_id:target } }).then(function() {
                setSaving(false); setModal(null); setSelected({}); flash('success','Tags merged.'); load();
            }).catch(function(e) { setSaving(false); flash('error', e.message||'Failed.'); });
        }

        // Selection
        function toggleSelect(id) {
            setSelected(function(prev) { var n = Object.assign({}, prev); if (n[id]) delete n[id]; else n[id] = true; return n; });
        }
        function toggleAll() {
            var allOn = tags.every(function(t){ return selected[t.id]; });
            if (allOn) { setSelected({}); }
            else { var n = {}; tags.forEach(function(t){ n[t.id] = true; }); setSelected(n); }
        }
        var selectedCount = Object.keys(selected).filter(function(k){ return selected[k]; }).length;
        var selectedTags = tags.filter(function(t){ return selected[t.id]; });

        var colGrid = '0.3fr 2fr 1.2fr 0.8fr 0.8fr 0.6fr';
        var sortIcon = function(col) { return sort === col ? (order === 'ASC' ? ' ↑' : ' ↓') : ''; };

        return h(PageWrap, { title:'Tags', sub:total + ' tags', mutedColor },
            msg && h('div', { style:{ padding:'10px 16px', borderRadius:8, marginBottom:S.lg, fontSize:13, fontWeight:500,
                background: msg.type==='success' ? (dark?'rgba(22,163,74,.12)':'#ECFDF5') : (dark?'rgba(220,38,38,.12)':'#FEF2F2'),
                color: msg.type==='success' ? '#16A34A' : '#DC2626' } }, msg.text),

            // Bulk actions bar
            selectedCount > 0 && h('div', { style:{ display:'flex', alignItems:'center', gap:S.md, padding:'10px 16px', borderRadius:8, marginBottom:S.lg, background:TC.accent+'12', border:'1px solid '+TC.accent+'30' } },
                h('span', { style:{ fontSize:13, fontWeight:500, color:TC.accent } }, selectedCount + ' selected'),
                h(Btn, { variant:'outline', onClick:openMerge, mutedColor, borderColor, style:{ fontSize:12, padding:'4px 12px' } }, 'Merge'),
                h(Btn, { variant:'outline', onClick:bulkDelete, mutedColor, borderColor, style:{ fontSize:12, padding:'4px 12px', color:TC.error, borderColor:'rgba(220,38,38,.3)' } }, 'Delete Selected'),
                h('button', { onClick:function(){ setSelected({}); }, style:{ background:'none', border:'none', cursor:'pointer', fontSize:12, color:mutedColor } }, 'Clear')
            ),

            // Search
            h('div', { style:{ marginBottom:S.lg } },
                h(SearchBox, { value:search, onChange:function(v){ setSearch(v); setPage(1); }, placeholder:'Search tags…', borderColor, dark, mutedColor })
            ),

            // Table
            h(Card, { cardBg, borderColor, dark, pad:0 },
                h('div', { style:{ display:'grid', gridTemplateColumns:colGrid, padding:S.md+'px '+S.xl+'px', borderBottom:'1px solid '+borderColor, background:tableHeaderBg, boxShadow:tableHeaderShadow } },
                    h('input', { type:'checkbox', checked: tags.length > 0 && tags.every(function(t){ return selected[t.id]; }), onChange:toggleAll,
                        style:{ width:16, height:16, cursor:'pointer' } }),
                    h('span', { onClick:function(){ toggleSort('name'); }, style:{ fontSize:10, fontWeight:500, color:mutedColor, textTransform:'uppercase', letterSpacing:'.5px', cursor:'pointer' } }, 'Tag Name' + sortIcon('name')),
                    h('span', { style:{ fontSize:10, fontWeight:500, color:mutedColor, textTransform:'uppercase', letterSpacing:'.5px' } }, 'Slug'),
                    h('span', { onClick:function(){ toggleSort('product_count'); }, style:{ fontSize:10, fontWeight:500, color:mutedColor, textTransform:'uppercase', letterSpacing:'.5px', cursor:'pointer', textAlign:'center' } }, 'Products' + sortIcon('product_count')),
                    h('span', { onClick:function(){ toggleSort('created_at'); }, style:{ fontSize:10, fontWeight:500, color:mutedColor, textTransform:'uppercase', letterSpacing:'.5px', cursor:'pointer' } }, 'Created' + sortIcon('created_at')),
                    h('span', { style:{ fontSize:10, fontWeight:500, color:mutedColor, textTransform:'uppercase', letterSpacing:'.5px' } }, '')
                ),

                loading && h(Spinner, { mutedColor }),
                !loading && tags.length === 0 && h(EmptyState, { mutedColor, icon:'🏷️', title:'No tags yet', sub:'Tags will appear here once you add them to products via the product editor.' }),

                !loading && tags.map(function(t, i) {
                    return h('div', { key:t.id,
                        style:{ display:'grid', gridTemplateColumns:colGrid, padding:'10px '+S.xl+'px', borderBottom: i<tags.length-1 ? '1px solid '+borderColor : 'none', alignItems:'center', transition:'background .12s' },
                        onMouseEnter:function(e){ e.currentTarget.style.background=dark?'rgba(255,255,255,.02)':'#F9FAFB'; },
                        onMouseLeave:function(e){ e.currentTarget.style.background='transparent'; }
                    },
                        h('input', { type:'checkbox', checked:!!selected[t.id], onChange:function(){ toggleSelect(t.id); },
                            style:{ width:16, height:16, cursor:'pointer' } }),
                        h('div', { style:{ display:'flex', alignItems:'center', gap:S.sm } },
                            h('span', { style:{ display:'inline-flex', alignItems:'center', padding:'2px 8px', borderRadius:20, fontSize:12, fontWeight:500,
                                background: dark ? 'rgba(37,99,235,0.12)' : '#EEF4FE', color:TC.accent, border:'1px solid '+(dark?'rgba(37,99,235,0.25)':'#BFDBFE') } }, t.name)
                        ),
                        h('span', { style:{ fontSize:11, color:mutedColor, fontFamily:'monospace' } }, t.slug),
                        h('span', { style:{ fontSize:13, fontWeight:600, textAlign:'center', color: Number(t.product_count) > 0 ? TC.accent : mutedColor } }, t.product_count || '0'),
                        h('span', { style:{ fontSize:11, color:mutedColor } }, tcFmtDate(t.created_at)),
                        h('div', { style:{ display:'flex', gap:4, justifyContent:'flex-end' } },
                            h('button', { onClick:function(){ openEdit(t); }, style:{ background:'transparent', border:'1px solid '+borderColor, borderRadius:6, padding:'3px 8px', fontSize:11, cursor:'pointer', color:mutedColor } }, '✏️'),
                            h('button', { onClick:function(){ doDelete(t); }, style:{ background:'transparent', border:'1px solid rgba(220,38,38,.2)', borderRadius:6, padding:'3px 8px', fontSize:11, cursor:'pointer', color:TC.error } }, '🗑')
                        )
                    );
                }),

                !loading && h('div', { style:{ display:'flex', justifyContent:'space-between', alignItems:'center', padding:S.md+'px '+S.xl+'px', borderTop:'1px solid '+borderColor } },
                    h('span', { style:{ fontSize:11, color:mutedColor } }, 'Showing ' + tags.length + ' of ' + total),
                    h(Pager, { page, totalPages, onPage:setPage, mutedColor, borderColor })
                )
            ),

            // ── Rename Modal ──
            modal === 'edit' && h('div', { style:{ position:'fixed', inset:0, background:'rgba(0,0,0,.4)', zIndex:9999, display:'flex', alignItems:'center', justifyContent:'center' } },
                h(Card, { cardBg, borderColor, dark, style:{ width:420, padding:S.xl } },
                    h('h3', { style:{ fontSize:15, fontWeight:600, marginBottom:S.lg } }, 'Rename Tag'),
                    h('div', { style:{ marginBottom:S.lg } },
                        h('label', { style:labelSty }, 'Tag Name'),
                        h('input', { value:tagName, onChange:function(e){ setTagName(e.target.value); },
                            onKeyDown:function(e){ if (e.key==='Enter') doEdit(); },
                            placeholder:'e.g. Summer Sale', autoFocus:true,
                            style:Object.assign({},fs,{ width:'100%' }) })
                    ),
                    h('div', { style:{ display:'flex', gap:S.sm, justifyContent:'flex-end' } },
                        h(Btn, { variant:'outline', onClick:function(){ setModal(null); }, mutedColor, borderColor }, 'Cancel'),
                        h(Btn, { variant:'primary', onClick:doEdit, disabled:saving||!tagName.trim(), mutedColor, borderColor },
                            saving ? 'Saving…' : 'Save')
                    )
                )
            ),

            // ── Merge Modal ──
            modal === 'merge' && h('div', { style:{ position:'fixed', inset:0, background:'rgba(0,0,0,.4)', zIndex:9999, display:'flex', alignItems:'center', justifyContent:'center' } },
                h(Card, { cardBg, borderColor, dark, style:{ width:460, padding:S.xl } },
                    h('h3', { style:{ fontSize:15, fontWeight:600, marginBottom:S.sm } }, 'Merge Tags'),
                    h('p', { style:{ fontSize:13, color:mutedColor, margin:'0 0 '+S.lg+'px' } }, 'All products from the selected tags will be reassigned to the target tag. Source tags will be deleted.'),
                    h('div', { style:{ marginBottom:S.md } },
                        h('div', { style:{ fontSize:12, fontWeight:500, color:mutedColor, marginBottom:S.sm } }, 'Selected tags:'),
                        h('div', { style:{ display:'flex', flexWrap:'wrap', gap:S.xs } },
                            selectedTags.map(function(t) {
                                return h('span', { key:t.id, style:{ padding:'2px 8px', borderRadius:20, fontSize:12, fontWeight:500,
                                    background: dark ? 'rgba(37,99,235,0.12)' : '#EEF4FE', color:TC.accent, border:'1px solid '+(dark?'rgba(37,99,235,0.25)':'#BFDBFE') } }, t.name);
                            })
                        )
                    ),
                    h('div', { style:{ marginBottom:S.lg } },
                        h('label', { style:labelSty }, 'Merge into (target tag):'),
                        h('select', { value:mergeTarget, onChange:function(e){ setMergeTarget(e.target.value); }, style:Object.assign({},fs,{width:'100%'}) },
                            selectedTags.map(function(t) { return h('option', { key:t.id, value:String(t.id) }, t.name + ' (' + t.product_count + ' products)'); })
                        )
                    ),
                    h('div', { style:{ display:'flex', gap:S.sm, justifyContent:'flex-end' } },
                        h(Btn, { variant:'outline', onClick:function(){ setModal(null); }, mutedColor, borderColor }, 'Cancel'),
                        h(Btn, { variant:'primary', onClick:doMerge, disabled:saving, mutedColor, borderColor }, saving ? 'Merging…' : 'Merge Tags')
                    )
                )
            )
        );
    }

    /* ════════════════════════════════════════════════════════════════════
     * SEO PAGE — 3 tabs: General Settings, URL Redirects, Robots.txt
     * ════════════════════════════════════════════════════════════════════ */
    function SeoPage({ dark, cardBg, textColor, mutedColor, borderColor, tableHeaderBg }) {
        var fs = fieldSty(dark, borderColor);
        var labelSty = { fontSize:12, fontWeight:500, display:'block', marginBottom:S.xs, color:mutedColor };

        var [tab, setTab] = useState('settings');
        var [msg, setMsg] = useState(null);

        // ── Settings state
        var [settings, setSettings] = useState({});
        var [settingsLoading, setSettingsLoading] = useState(false);
        var [settingsSaving, setSettingsSaving] = useState(false);

        // ── Redirects state
        var [redirects, setRedirects] = useState([]);
        var [redLoading, setRedLoading] = useState(false);
        var [redTotal, setRedTotal] = useState(0);
        var [redPages, setRedPages] = useState(1);
        var [redPage, setRedPage] = useState(1);
        var [redSearch, setRedSearch] = useState('');
        var [redModal, setRedModal] = useState(null);
        var [redForm, setRedForm] = useState({ from_url:'', to_url:'', type:'301' });
        var [redSaving, setRedSaving] = useState(false);

        // ── Robots state
        var [robots, setRobots] = useState('');
        var [robotsLoading, setRobotsLoading] = useState(false);
        var [robotsSaving, setRobotsSaving] = useState(false);

        function flash(type, text) { setMsg({ type:type, text:text }); setTimeout(function(){ setMsg(null); }, 3000); }

        // ── Settings
        function loadSettings() {
            setSettingsLoading(true);
            apiFetch('/seo/settings').then(function(d){ setSettings(d||{}); setSettingsLoading(false); }).catch(function(){ setSettingsLoading(false); });
        }
        function saveSettings() {
            setSettingsSaving(true);
            apiFetch('/seo/settings', { method:'PUT', body:settings }).then(function(d){ setSettings(d||{}); setSettingsSaving(false); flash('success','SEO settings saved.'); })
                .catch(function(e){ setSettingsSaving(false); flash('error', e.message||'Failed.'); });
        }
        function updS(k,v) { setSettings(function(p){ var n=Object.assign({},p); n[k]=v; return n; }); }

        // ── Redirects
        function loadRedirects() {
            setRedLoading(true);
            var q = new URLSearchParams({ page:redPage, per_page:TC_PER_PAGE });
            if (redSearch) q.set('search', redSearch);
            apiFetch('/seo/redirects?' + q.toString()).then(function(d) {
                setRedirects(d.redirects||[]); setRedTotal(d.total||0); setRedPages(d.pages||1); setRedLoading(false);
            }).catch(function(){ setRedLoading(false); });
        }
        function openRedirectModal(r) {
            if (r) {
                setRedModal(r);
                setRedForm({ from_url:r.from_url||'', to_url:r.to_url||'', type:String(r.type||301) });
            } else {
                setRedModal('new');
                setRedForm({ from_url:'', to_url:'', type:'301' });
            }
        }
        function saveRedirect() {
            setRedSaving(true);
            var body = { from_url:redForm.from_url, to_url:redForm.to_url, type:Number(redForm.type) };
            var p = redModal === 'new'
                ? apiFetch('/seo/redirects', { method:'POST', body:body })
                : apiFetch('/seo/redirects/' + redModal.id, { method:'PUT', body:body });
            p.then(function(){ setRedSaving(false); setRedModal(null); flash('success', redModal==='new'?'Redirect created.':'Redirect updated.'); loadRedirects(); })
             .catch(function(e){ setRedSaving(false); flash('error', e.message||'Failed.'); });
        }
        function deleteRedirect(r) {
            if (!confirm('Delete redirect from "' + r.from_url + '"?')) return;
            apiFetch('/seo/redirects/' + r.id, { method:'DELETE' }).then(function(){ flash('success','Redirect deleted.'); loadRedirects(); }).catch(function(){});
        }
        function toggleRedirectStatus(r) {
            var ns = r.status === 'active' ? 'disabled' : 'active';
            apiFetch('/seo/redirects/' + r.id, { method:'PUT', body:{ status:ns } }).then(function(){ loadRedirects(); }).catch(function(){});
        }

        // ── Robots
        function loadRobots() {
            setRobotsLoading(true);
            apiFetch('/seo/robots').then(function(d){ setRobots(d.content||''); setRobotsLoading(false); }).catch(function(){ setRobotsLoading(false); });
        }
        function saveRobots() {
            setRobotsSaving(true);
            apiFetch('/seo/robots', { method:'PUT', body:{ content:robots } }).then(function(){ setRobotsSaving(false); flash('success','robots.txt saved.'); })
                .catch(function(e){ setRobotsSaving(false); flash('error', e.message||'Failed.'); });
        }

        useEffect(function(){ if (tab==='settings') loadSettings(); }, [tab]);
        useEffect(function(){ if (tab==='redirects') loadRedirects(); }, [tab, redPage, redSearch]);
        useEffect(function(){ if (tab==='robots') loadRobots(); }, [tab]);

        var tabs = [
            { v:'settings', l:'General' },
            { v:'redirects', l:'URL Redirects' },
            { v:'robots', l:'Robots.txt' },
        ];

        return h(PageWrap, { title:'SEO', sub:'Search engine optimization', mutedColor },

            msg && h('div', { style:{ padding:'10px 16px', borderRadius:8, marginBottom:S.lg, fontSize:13, fontWeight:500,
                background: msg.type==='success' ? (dark?'rgba(22,163,74,.12)':'#ECFDF5') : (dark?'rgba(220,38,38,.12)':'#FEF2F2'),
                color: msg.type==='success' ? '#16A34A' : '#DC2626' } }, msg.text),

            // Tabs
            h('div', { style:{ display:'flex', gap:S.xs, marginBottom:S.lg, borderBottom:'1px solid '+borderColor, paddingBottom:S.sm } },
                tabs.map(function(t) {
                    var isActive = tab === t.v;
                    return h('button', { key:t.v, onClick:function(){ setTab(t.v); },
                        style:{ padding:'8px '+S.lg+'px', border:'none', cursor:'pointer', fontSize:13, fontWeight:isActive?600:400,
                                background:'transparent', color:isActive?TC.accent:mutedColor, borderBottom:isActive?'2px solid '+TC.accent:'2px solid transparent', transition:'all .15s', marginBottom:-S.sm-1 } }, t.l);
                })
            ),

            // ═══ SETTINGS TAB ═══
            tab === 'settings' && (settingsLoading ? h(Spinner, { mutedColor }) : h('div', null,
                h('div', { style:{ display:'grid', gridTemplateColumns:'1fr 1fr', gap:S.lg } },

                    // URL Patterns
                    h(Card, { cardBg, borderColor, dark },
                        h('h3', { style:{ fontSize:14, fontWeight:600, marginBottom:S.lg } }, 'URL Patterns'),
                        h('div', { style:{ marginBottom:S.lg } },
                            h('label', { style:labelSty }, 'Product URLs'),
                            h('select', { value:settings.product_url_pattern||'/product/{slug}/', onChange:function(e){ updS('product_url_pattern',e.target.value); },
                                style:Object.assign({},fs,{ width:'100%', fontSize:12 }) },
                                h('option', { value:'/product/{slug}/' }, '/product/product-name/  —  Standard'),
                                h('option', { value:'/p/{slug}/' },       '/p/product-name/  —  Short'),
                                h('option', { value:'/item/{slug}/' },    '/item/product-name/  —  Alternative'),
                                h('option', { value:'/goods/{slug}/' },   '/goods/product-name/'),
                                h('option', { disabled:true }, '── Category-based (coming soon) ──'),
                                h('option', { value:'/{category}/{slug}/', disabled:true },              '/{category}/product-name/'),
                                h('option', { value:'/{category}/{subcategory}/{slug}/', disabled:true }, '/{category}/{subcategory}/product-name/')
                            ),
                            h('p', { style:{ fontSize:11, color:mutedColor, marginTop:S.xs, fontFamily:'monospace' } },
                                'Example: ' + (settings.product_url_pattern||'/product/{slug}/').replace('{slug}', 'laptop-stand').replace('{category}', 'electronics').replace('{subcategory}', 'accessories'))
                        ),
                        h('div', null,
                            h('label', { style:labelSty }, 'Category URLs'),
                            h('select', { value:settings.category_url_pattern||'/shop/{slug}/', onChange:function(e){ updS('category_url_pattern',e.target.value); },
                                style:Object.assign({},fs,{ width:'100%', fontSize:12 }) },
                                h('option', { value:'/shop/{slug}/' },     '/shop/category-name/  —  Standard'),
                                h('option', { value:'/category/{slug}/' }, '/category/category-name/'),
                                h('option', { value:'/catalog/{slug}/' },  '/catalog/category-name/'),
                                h('option', { value:'/c/{slug}/' },        '/c/category-name/  —  Short'),
                                h('option', { value:'/browse/{slug}/' },   '/browse/category-name/')
                            ),
                            h('p', { style:{ fontSize:11, color:mutedColor, marginTop:S.xs, fontFamily:'monospace' } },
                                'Example: ' + (settings.category_url_pattern||'/shop/{slug}/').replace('{slug}', 'electronics'))
                        ),
                        (settings.product_url_pattern && settings.category_url_pattern &&
                         settings.product_url_pattern.split('/')[1] === settings.category_url_pattern.split('/')[1]) &&
                            h('div', { style:{ padding:'8px 12px', borderRadius:6, marginTop:S.md, fontSize:12, fontWeight:500,
                                background: dark?'rgba(220,38,38,.08)':'#FEF2F2', color:'#DC2626', border:'1px solid '+(dark?'rgba(220,38,38,.2)':'#FECACA') } },
                                '⚠️ Product and category URL prefixes cannot be the same. Please choose different patterns.')
                    ),

                    // General SEO
                    h(Card, { cardBg, borderColor, dark },
                        h('h3', { style:{ fontSize:14, fontWeight:600, marginBottom:S.lg } }, 'General'),
                        [
                            { key:'canonical_urls', label:'Canonical URLs', desc:'Add canonical URL tags to prevent duplicate content' },
                            { key:'trailing_slash', label:'Trailing slash', desc:'Add trailing slash to all URLs' },
                            { key:'noindex_filter_pages', label:'Noindex filter pages', desc:'Prevent search engines from indexing filtered/sorted pages' },
                        ].map(function(opt) {
                            return h('div', { key:opt.key, style:{ display:'flex', justifyContent:'space-between', alignItems:'center', padding:S.sm+'px 0', borderBottom:'1px solid '+borderColor } },
                                h('div', null,
                                    h('div', { style:{ fontSize:13, fontWeight:500 } }, opt.label),
                                    h('div', { style:{ fontSize:11, color:mutedColor, marginTop:2 } }, opt.desc)
                                ),
                                h('button', { onClick:function(){ updS(opt.key, settings[opt.key]==='yes'?'no':'yes'); },
                                    style:{ width:44, height:24, borderRadius:12, border:'none', cursor:'pointer', position:'relative',
                                            background: settings[opt.key]==='yes' ? TC.accent : (dark?'#333':'#D1D5DB'), transition:'background .2s' } },
                                    h('div', { style:{ width:18, height:18, borderRadius:9, background:'#fff', position:'absolute', top:3,
                                                left: settings[opt.key]==='yes' ? 23 : 3, transition:'left .2s', boxShadow:'0 1px 3px rgba(0,0,0,.2)' } })
                                )
                            );
                        })
                    )
                ),

                // Meta defaults
                h(Card, { cardBg, borderColor, dark, style:{ marginTop:S.lg } },
                    h('h3', { style:{ fontSize:14, fontWeight:600, marginBottom:S.lg } }, 'Default Meta Templates'),
                    h('div', { style:{ display:'grid', gridTemplateColumns:'1fr 1fr', gap:S.lg } },
                        h('div', null,
                            h('label', { style:labelSty }, 'Default meta title'),
                            h('input', { value:settings.default_meta_title||'', onChange:function(e){ updS('default_meta_title',e.target.value); },
                                style:Object.assign({},fs,{ width:'100%' }) }),
                            h('p', { style:{ fontSize:11, color:mutedColor, marginTop:S.xs } }, 'Variables: {product_name}, {category_name}, {site_name}')
                        ),
                        h('div', null,
                            h('label', { style:labelSty }, 'Default meta description'),
                            h('input', { value:settings.default_meta_desc||'', onChange:function(e){ updS('default_meta_desc',e.target.value); },
                                style:Object.assign({},fs,{ width:'100%' }) }),
                            h('p', { style:{ fontSize:11, color:mutedColor, marginTop:S.xs } }, 'Variables: {short_description}, {product_name}, {price}')
                        )
                    )
                ),

                h('div', { style:{ marginTop:S.lg, display:'flex', justifyContent:'flex-end' } },
                    h(Btn, { variant:'primary', onClick:saveSettings, disabled:settingsSaving, mutedColor, borderColor }, settingsSaving ? 'Saving…' : 'Save Settings')
                )
            )),

            // ═══ REDIRECTS TAB ═══
            tab === 'redirects' && h('div', null,
                h('div', { style:{ display:'flex', justifyContent:'space-between', alignItems:'center', marginBottom:S.lg } },
                    h(SearchBox, { value:redSearch, onChange:function(v){ setRedSearch(v); setRedPage(1); }, placeholder:'Search URLs…', borderColor, dark, mutedColor }),
                    h(Btn, { variant:'primary', onClick:function(){ openRedirectModal(null); }, mutedColor, borderColor }, '+ Add Redirect')
                ),

                // Modal
                redModal && h('div', { style:{ position:'fixed', inset:0, background:'rgba(0,0,0,.4)', zIndex:9999, display:'flex', alignItems:'center', justifyContent:'center' } },
                    h(Card, { cardBg, borderColor, dark, style:{ width:520, padding:S.xl } },
                        h('h3', { style:{ fontSize:15, fontWeight:600, marginBottom:S.lg } }, redModal==='new' ? 'Add Redirect' : 'Edit Redirect'),
                        h('div', { style:{ marginBottom:S.md } },
                            h('label', { style:labelSty }, 'From URL'),
                            h('input', { value:redForm.from_url, onChange:function(e){ setRedForm(function(p){ return Object.assign({},p,{from_url:e.target.value}); }); },
                                placeholder:'/old-page/', style:Object.assign({},fs,{ width:'100%', fontFamily:'monospace', fontSize:12 }) })
                        ),
                        h('div', { style:{ marginBottom:S.md } },
                            h('label', { style:labelSty }, 'To URL'),
                            h('input', { value:redForm.to_url, onChange:function(e){ setRedForm(function(p){ return Object.assign({},p,{to_url:e.target.value}); }); },
                                placeholder:'/new-page/', style:Object.assign({},fs,{ width:'100%', fontFamily:'monospace', fontSize:12 }) })
                        ),
                        h('div', { style:{ marginBottom:S.lg } },
                            h('label', { style:labelSty }, 'Redirect Type'),
                            h('select', { value:redForm.type, onChange:function(e){ setRedForm(function(p){ return Object.assign({},p,{type:e.target.value}); }); },
                                style:Object.assign({},fs,{ width:'100%' }) },
                                h('option', { value:'301' }, '301 — Permanent'),
                                h('option', { value:'302' }, '302 — Temporary')
                            )
                        ),
                        h('div', { style:{ display:'flex', gap:S.sm, justifyContent:'flex-end' } },
                            h(Btn, { variant:'outline', onClick:function(){ setRedModal(null); }, mutedColor, borderColor }, 'Cancel'),
                            h(Btn, { variant:'primary', onClick:saveRedirect, disabled:redSaving||!redForm.from_url||!redForm.to_url, mutedColor, borderColor },
                                redSaving ? 'Saving…' : 'Save')
                        )
                    )
                ),

                h(Card, { cardBg, borderColor, dark, pad:0 },
                    h('div', { style:{ display:'grid', gridTemplateColumns:'1.5fr 1.5fr 0.5fr 0.4fr 0.6fr 0.6fr', padding:S.md+'px '+S.xl+'px', borderBottom:'1px solid '+borderColor, background:tableHeaderBg, boxShadow:tableHeaderShadow } },
                        ['From','To','Type','Hits','Status',''].map(function(col) {
                            return h('span', { key:col, style:{ fontSize:10, fontWeight:500, color:mutedColor, textTransform:'uppercase', letterSpacing:'.5px' } }, col);
                        })
                    ),
                    redLoading && h(Spinner, { mutedColor }),
                    !redLoading && redirects.length === 0 && h(EmptyState, { mutedColor, icon:'🔀', title:'No redirects', sub:'Add URL redirects for moved or deleted pages.' }),
                    !redLoading && redirects.map(function(r, i) {
                        return h('div', { key:r.id, style:{ display:'grid', gridTemplateColumns:'1.5fr 1.5fr 0.5fr 0.4fr 0.6fr 0.6fr', padding:'10px '+S.xl+'px', borderBottom: i<redirects.length-1 ? '1px solid '+borderColor : 'none', alignItems:'center' } },
                            h('span', { style:{ fontSize:12, fontFamily:'monospace', color:TC.error, overflow:'hidden', textOverflow:'ellipsis', whiteSpace:'nowrap' } }, r.from_url),
                            h('span', { style:{ fontSize:12, fontFamily:'monospace', color:'#16A34A', overflow:'hidden', textOverflow:'ellipsis', whiteSpace:'nowrap' } }, r.to_url),
                            h('span', { style:{ fontSize:11, fontWeight:500, color: r.type==301 ? TC.accent : '#D97706' } }, r.type),
                            h('span', { style:{ fontSize:11, color:mutedColor, textAlign:'center' } }, r.hits||0),
                            h('button', { onClick:function(){ toggleRedirectStatus(r); }, style:{ background:'transparent', border:'none', cursor:'pointer', fontSize:11, fontWeight:500, color:r.status==='active'?'#16A34A':'#DC2626' } }, r.status==='active'?'Active':'Off'),
                            h('div', { style:{ display:'flex', gap:4, justifyContent:'flex-end' } },
                                h('button', { onClick:function(){ openRedirectModal(r); }, style:{ background:'transparent', border:'1px solid '+borderColor, borderRadius:6, padding:'3px 8px', fontSize:11, cursor:'pointer', color:mutedColor } }, '✏️'),
                                h('button', { onClick:function(){ deleteRedirect(r); }, style:{ background:'transparent', border:'1px solid rgba(220,38,38,.2)', borderRadius:6, padding:'3px 8px', fontSize:11, cursor:'pointer', color:TC.error } }, '🗑')
                            )
                        );
                    }),
                    !redLoading && h('div', { style:{ display:'flex', justifyContent:'space-between', alignItems:'center', padding:S.md+'px '+S.xl+'px', borderTop:'1px solid '+borderColor } },
                        h('span', { style:{ fontSize:11, color:mutedColor } }, 'Showing ' + redirects.length + ' of ' + redTotal),
                        h(Pager, { page:redPage, totalPages:redPages, onPage:setRedPage, mutedColor, borderColor })
                    )
                )
            ),

            // ═══ ROBOTS TAB ═══
            tab === 'robots' && (robotsLoading ? h(Spinner, { mutedColor }) : h('div', null,
                h(Card, { cardBg, borderColor, dark },
                    h('div', { style:{ display:'flex', justifyContent:'space-between', alignItems:'center', marginBottom:S.lg } },
                        h('h3', { style:{ fontSize:14, fontWeight:600 } }, 'robots.txt'),
                        h('p', { style:{ fontSize:12, color:mutedColor, margin:0 } }, 'Controls which pages search engines can crawl')
                    ),
                    h('textarea', { value:robots, onChange:function(e){ setRobots(e.target.value); }, rows:16,
                        style:Object.assign({},fs,{ width:'100%', fontFamily:'monospace', fontSize:12, lineHeight:1.6, resize:'vertical' }) }),
                    h('p', { style:{ fontSize:11, color:mutedColor, marginTop:S.sm } }, 'This content will be served at /robots.txt when the SEO extension is active.')
                ),
                h('div', { style:{ marginTop:S.lg, display:'flex', justifyContent:'flex-end' } },
                    h(Btn, { variant:'primary', onClick:saveRobots, disabled:robotsSaving, mutedColor, borderColor }, robotsSaving ? 'Saving…' : 'Save Robots.txt')
                )
            ))
        );
    }

    /* ════════════════════════════════════════════════════════════════════
     * PAGES PAGE — Storefront → Pages (roadmap #2, Page Manager S2)
     * ────────────────────────────────────────────────────────────────────
     * One unified surface over the core PageRegistry: every TitanCart-aware
     * storefront page with its live hosting status, a bulk "create missing"
     * action, per-row create-missing, and a hosting-page <select> that
     * re-points a shortcode feature page at an existing published WP page.
     * Structural (Homepage/Blog) and rewrite (Shop/Cart/Checkout/…) rows are
     * shown read-only. Module-scope component (focus-safe); helper render
     * functions (badges/hostCell/actionCell) are plain element factories, not
     * nested components, so they never remount inputs.
     * ════════════════════════════════════════════════════════════════════ */
    function PagesPage({ dark, cardBg, textColor, mutedColor, borderColor, tableHeaderBg }) {
        var fs = fieldSty(dark, borderColor);

        var [rows, setRows] = useState([]);
        var [available, setAvailable] = useState([]);
        var [loading, setLoading] = useState(true);
        var [ensuring, setEnsuring] = useState(false);
        var [busyKey, setBusyKey] = useState('');
        var [msg, setMsg] = useState(null);

        function flash(type, text) { setMsg({ type: type, text: text }); setTimeout(function(){ setMsg(null); }, 3500); }

        function applyPayload(d) {
            if (d && Array.isArray(d.pages)) setRows(d.pages);
            if (d && Array.isArray(d.available)) setAvailable(d.available);
        }

        function load() {
            setLoading(true);
            apiFetch('/pages').then(function(d) {
                applyPayload(d); setLoading(false);
            }).catch(function(e){ flash('error', (e && e.message) || 'Failed to load pages.'); setLoading(false); });
        }

        useEffect(function(){ load(); }, []);

        function ensureAll() {
            setEnsuring(true);
            apiFetch('/pages/ensure', { method:'POST', body:{} }).then(function(d) {
                applyPayload(d);
                var s = (d && d.summary) || { created:0, adopted:0 };
                var parts = [];
                if (s.created) parts.push(s.created + ' created');
                if (s.adopted) parts.push(s.adopted + ' already in place');
                flash('success', parts.length ? ('Pages checked — ' + parts.join(', ') + '.') : 'All pages already in place.');
                setEnsuring(false);
            }).catch(function(e){ flash('error', (e && e.message) || 'Failed to create pages.'); setEnsuring(false); });
        }

        function ensureOne(key) {
            setBusyKey(key);
            apiFetch('/pages/ensure', { method:'POST', body:{ key: key } }).then(function(d) {
                applyPayload(d);
                var s = (d && d.summary) || { created:0, adopted:0 };
                flash('success', s.created ? 'Page created.' : 'Page already in place.');
                setBusyKey('');
            }).catch(function(e){ flash('error', (e && e.message) || 'Failed to create page.'); setBusyKey(''); });
        }

        function assign(key, pageId) {
            var id = Number(pageId);
            if (!id) return;
            setBusyKey(key);
            apiFetch('/pages/' + encodeURIComponent(key) + '/assign', { method:'PUT', body:{ page_id: id } }).then(function(d) {
                applyPayload(d);
                flash('success', 'Hosting page updated.');
                setBusyKey('');
            }).catch(function(e){ flash('error', (e && e.message) || 'Failed to assign page.'); setBusyKey(''); });
        }

        var TYPE_BADGE = {
            structural: { label:'Structural', bg:dark?'rgba(37,99,235,.12)':'#EFF6FF', color:TC.accent },
            shortcode:  { label:'Shortcode',  bg:dark?'rgba(124,58,237,.16)':'#F5F3FF', color:dark?'#A78BFA':'#7C3AED' },
            rewrite:    { label:'Rewrite',    bg:dark?'rgba(255,255,255,.06)':'#F3F4F6', color:mutedColor },
        };
        function typeBadge(t) {
            var c = TYPE_BADGE[t] || TYPE_BADGE.shortcode;
            return h('span', { style:{ fontSize:11, fontWeight:500, padding:'2px 8px', borderRadius:5, background:c.bg, color:c.color, whiteSpace:'nowrap' } }, c.label);
        }

        var STATUS_BADGE = {
            publish:   { label:'Published', bg:dark?'rgba(22,163,74,.12)':'#ECFDF5', color:'#16A34A' },
            draft:     { label:'Draft',     bg:dark?'rgba(217,119,6,.12)':'#FEF6EC', color:'#D97706' },
            pending:   { label:'Pending',   bg:dark?'rgba(217,119,6,.12)':'#FEF6EC', color:'#D97706' },
            'private': { label:'Private',   bg:dark?'rgba(255,255,255,.06)':'#F3F4F6', color:mutedColor },
            trash:     { label:'Trash',     bg:dark?'rgba(220,38,38,.12)':'#FEF2F2', color:'#DC2626' },
            missing:   { label:'Missing',   bg:dark?'rgba(220,38,38,.12)':'#FEF2F2', color:'#DC2626' },
            rewrite:   { label:'Virtual',   bg:dark?'rgba(255,255,255,.06)':'#F3F4F6', color:mutedColor },
        };
        function statusBadge(s) {
            var c = STATUS_BADGE[s] || STATUS_BADGE.missing;
            return h('span', { style:{ fontSize:11, fontWeight:500, padding:'2px 8px', borderRadius:5, background:c.bg, color:c.color, whiteSpace:'nowrap' } }, c.label);
        }

        var linkSty = { fontSize:11, color:TC.accent, textDecoration:'none', whiteSpace:'nowrap' };

        /* Ensure the currently-bound page is always selectable — a draft/private
         * host won't appear in `available` (published pages only). */
        function optionsFor(row) {
            var opts = available.slice();
            if (row.page_id > 0) {
                var present = opts.some(function(o){ return Number(o.id) === Number(row.page_id); });
                if (!present) opts.unshift({ id: row.page_id, title: (row.page_title || ('Page #' + row.page_id)) + ' (' + row.status + ')' });
            }
            return opts;
        }

        function hostLinks(row) {
            if (row.page_id <= 0) return null;
            return h('div', { style:{ display:'flex', gap:S.md, marginTop:4 } },
                row.view_url ? h('a', { href:row.view_url, target:'_blank', rel:'noopener noreferrer', style:linkSty }, 'View \u2197') : null,
                row.edit_url ? h('a', { href:row.edit_url, target:'_blank', rel:'noopener noreferrer', style:linkSty }, 'Edit \u2197') : null
            );
        }

        function hostCell(row) {
            if (row.assignable) {
                return h('div', { style:{ display:'flex', flexDirection:'column', gap:6 } },
                    h('select', {
                        style: Object.assign({}, fs, { cursor:'pointer' }),
                        value: row.page_id > 0 ? String(row.page_id) : '',
                        disabled: busyKey === row.key,
                        onChange: function(e){ assign(row.key, e.target.value); }
                    },
                        h('option', { value:'' }, '\u2014 Select a page \u2014'),
                        optionsFor(row).map(function(o) {
                            return h('option', { key:o.id, value:String(o.id) }, o.title);
                        })
                    ),
                    hostLinks(row)
                );
            }
            if (row.type === 'structural') {
                return h('div', null,
                    h('div', { style:{ fontSize:12, color:textColor } }, row.page_id > 0 ? (row.page_title || ('Page #' + row.page_id)) : 'Not set'),
                    h('div', { style:{ fontSize:11, color:mutedColor, marginTop:2 } }, 'Bound via WordPress Reading settings'),
                    hostLinks(row)
                );
            }
            return h('div', { style:{ fontSize:12, color:mutedColor } }, 'Handled by rewrite rules \u2014 no page needed');
        }

        function actionCell(row) {
            if (row.assignable && row.page_id === 0) {
                var busy = busyKey === row.key;
                return h(Btn, { variant:'secondary', onClick:function(){ ensureOne(row.key); }, disabled:busy, mutedColor:mutedColor, borderColor:borderColor }, busy ? '\u2026' : 'Create');
            }
            return h('span', { style:{ fontSize:12, color:mutedColor } }, '\u2014');
        }

        var COLS = 'minmax(200px,1.6fr) 0.75fr 1fr 0.8fr minmax(230px,1.9fr) 0.85fr';

        if (loading) return h(PageWrap, { title:'Pages', mutedColor:mutedColor }, h(Spinner, { mutedColor:mutedColor }));

        var createBtn = h(Btn, { variant:'primary', onClick:ensureAll, disabled:ensuring, mutedColor:mutedColor, borderColor:borderColor }, ensuring ? 'Working\u2026' : '+ Create missing pages');

        return h(PageWrap, { title:'Pages', sub:'Manage the WordPress pages that host your TitanCart storefront features.', mutedColor:mutedColor, action:createBtn },

            msg && h('div', { style:{ padding:'10px 16px', borderRadius:8, marginBottom:S.lg, fontSize:13, fontWeight:500,
                background: msg.type==='success' ? (dark?'rgba(22,163,74,.12)':'#ECFDF5') : (dark?'rgba(220,38,38,.12)':'#FEF2F2'),
                color: msg.type==='success' ? '#16A34A' : '#DC2626' } }, msg.text),

            h(Card, { cardBg:cardBg, borderColor:borderColor, dark:dark },

                h('div', { style:{ fontSize:12, color:mutedColor, marginBottom:S.lg, lineHeight:1.5 } },
                    'TitanCart creates these pages automatically. Use \u201cCreate missing pages\u201d to rebuild any feature page that was deleted, or re-point a feature at a different published page below. Rewrite-backed pages (Shop, Cart, Checkout, and so on) are served directly by TitanCart and need no WordPress page.'),

                /* Header */
                h('div', { style:{ display:'grid', gridTemplateColumns:COLS, padding:'12px '+S.lg+'px', borderBottom:'1px solid '+borderColor, background:tableHeaderBg, borderRadius:'8px 8px 0 0', gap:S.sm } },
                    ['Page','Type','Shortcode','Status','Hosting page','Action'].map(function(col) {
                        return h('span', { key:col, style:{ fontSize:10, fontWeight:500, color:mutedColor, textTransform:'uppercase', letterSpacing:'.5px' } }, col);
                    })
                ),

                /* Rows */
                rows.map(function(row) {
                    var busy = busyKey === row.key;
                    return h('div', { key:row.key, style:{
                        display:'grid', gridTemplateColumns:COLS, padding:'14px '+S.lg+'px',
                        borderBottom:'1px solid '+borderColor, alignItems:'center', gap:S.sm,
                        opacity: busy ? 0.55 : 1
                    } },
                        h('div', null,
                            h('div', { style:{ fontSize:13, fontWeight:600 } }, row.label),
                            h('div', { style:{ fontSize:11, color:mutedColor, marginTop:2, lineHeight:1.4 } }, row.description)
                        ),
                        typeBadge(row.type),
                        row.shortcode
                            ? h('code', { style:{ fontSize:11, fontFamily:'monospace', color:mutedColor, wordBreak:'break-all' } }, row.shortcode)
                            : h('span', { style:{ fontSize:12, color:mutedColor } }, '\u2014'),
                        statusBadge(row.status),
                        hostCell(row),
                        actionCell(row)
                    );
                })
            )
        );
    }

    /* ════════════════════════════════════════════════════════════════════
     * SITEMAP PAGE — Quick-access sitemap dashboard
     * ════════════════════════════════════════════════════════════════════ */
    function SitemapPage({ dark, cardBg, textColor, mutedColor, borderColor, tableHeaderBg }) {
        var [settings, setSettings] = useState(null);
        var [loading, setLoading] = useState(true);
        var [extActive, setExtActive] = useState(false);

        useEffect(function() {
            // Check if sitemap extension is active
            apiFetch('/extensions').then(function(d) {
                var exts = d.extensions || d || [];
                var sm = (Array.isArray(exts) ? exts : []).find(function(e){ return e.id === 'sitemap'; });
                setExtActive(sm && sm.status === 'active');
            }).catch(function(){});

            // Load sitemap settings
            apiFetch('/extensions/sitemap/settings').then(function(d) {
                var flat = {};
                ((d.schema && d.schema.groups) || []).forEach(function(g) {
                    (g.fields || []).forEach(function(f) {
                        flat[f.key] = f.value !== undefined ? f.value : f.default;
                    });
                });
                setSettings(flat); setLoading(false);
            }).catch(function(){ setSettings({}); setLoading(false); });
        }, []);

        var sitemapUrl = window.location.origin + '/sitemap-titancart.xml';

        if (loading) return h(PageWrap, { title:'Sitemap', mutedColor }, h(Spinner, { mutedColor }));

        return h(PageWrap, { title:'Sitemap', sub:'XML sitemap for search engines', mutedColor },

            // Status banner
            h('div', { style:{ padding:'12px 16px', borderRadius:8, marginBottom:S.xl, display:'flex', justifyContent:'space-between', alignItems:'center',
                background: extActive ? (dark?'rgba(22,163,74,.08)':'#F0FDF4') : (dark?'rgba(220,38,38,.08)':'#FEF2F2'),
                border:'1px solid '+(extActive ? (dark?'rgba(22,163,74,.2)':'#BBF7D0') : (dark?'rgba(220,38,38,.2)':'#FECACA')) } },
                h('div', { style:{ display:'flex', alignItems:'center', gap:S.sm } },
                    h('span', { style:{ fontSize:18 } }, extActive ? '✅' : '⚠️'),
                    h('div', null,
                        h('div', { style:{ fontSize:13, fontWeight:600, color: extActive ? '#16A34A' : '#DC2626' } },
                            extActive ? 'Sitemap is active' : 'Sitemap extension is disabled'),
                        !extActive && h('div', { style:{ fontSize:12, color:mutedColor, marginTop:2 } }, 'Enable it in Extensions → Google Sitemap')
                    )
                ),
                extActive && h('a', { href:sitemapUrl, target:'_blank', rel:'noopener',
                    style:{ fontSize:12, color:TC.accent, textDecoration:'none', fontFamily:'monospace', background:dark?'rgba(37,99,235,.08)':'#EEF4FE', padding:'4px 10px', borderRadius:6, border:'1px solid '+(dark?'rgba(37,99,235,.2)':'#BFDBFE') } },
                    '↗ View Sitemap')
            ),

            h('div', { style:{ display:'grid', gridTemplateColumns:'1fr 1fr', gap:S.lg } },

                // Sitemap URL card
                h(Card, { cardBg, borderColor, dark },
                    h('h3', { style:{ fontSize:14, fontWeight:600, marginBottom:S.lg } }, 'Sitemap URL'),
                    h('div', { style:{ padding:S.md, background:dark?'#1E2130':'#F5F5F5', borderRadius:8, fontFamily:'monospace', fontSize:12, color:TC.accent, wordBreak:'break-all' } }, sitemapUrl),
                    h('p', { style:{ fontSize:11, color:mutedColor, marginTop:S.sm } }, 'Submit this URL to Google Search Console and other search engines for faster indexing.')
                ),

                // What's included card
                h(Card, { cardBg, borderColor, dark },
                    h('h3', { style:{ fontSize:14, fontWeight:600, marginBottom:S.lg } }, "What's Included"),
                    [
                        { label:'Products', key:'include_products', change:'products_change', priority:'products_priority' },
                        { label:'Categories', key:'include_categories', change:'categories_change', priority:'categories_priority' },
                        { label:'Pages', key:'include_pages', change:'pages_change', priority:'pages_priority' },
                    ].map(function(item) {
                        var val = settings && settings[item.key];
                        var included = val === '1' || val === 'yes' || val === true || val === undefined;
                        var changeFreq = (settings && settings[item.change]) || 'weekly';
                        var pri = (settings && settings[item.priority]) || '0.5';
                        return h('div', { key:item.label, style:{ display:'flex', justifyContent:'space-between', alignItems:'center', padding:S.sm+'px 0', borderBottom:'1px solid '+borderColor } },
                            h('div', { style:{ display:'flex', alignItems:'center', gap:S.sm } },
                                h('span', { style:{ fontSize:14 } }, included ? '✅' : '❌'),
                                h('span', { style:{ fontSize:13, fontWeight:500 } }, item.label)
                            ),
                            included && h('div', { style:{ display:'flex', gap:S.md, alignItems:'center' } },
                                h('span', { style:{ fontSize:11, color:mutedColor } }, 'Change: ' + changeFreq),
                                h('span', { style:{ fontSize:11, color:mutedColor } }, 'Priority: ' + pri)
                            )
                        );
                    })
                )
            ),

            // Manage button
            h('div', { style:{ marginTop:S.xl, display:'flex', gap:S.md } },
                h(Btn, { variant:'primary', onClick:function(){ navigate('ext-settings/sitemap'); }, mutedColor, borderColor }, '⚙️ Configure Sitemap Settings'),
                h(Btn, { variant:'outline', onClick:function(){ window.open(sitemapUrl, '_blank'); }, mutedColor, borderColor }, '↗ Preview Sitemap XML')
            )
        );
    }

    /* ════════════════════════════════════════════════════════════════════
     * TESTIMONIALS PAGE — Customer testimonials managed by store owner
     * ════════════════════════════════════════════════════════════════════ */
    function TestimonialsPage({ dark, cardBg, textColor, mutedColor, borderColor, tableHeaderBg }) {
        var fs = fieldSty(dark, borderColor);

        var [rows, setRows] = useState([]);
        var [total, setTotal] = useState(0);
        var [pages, setPages] = useState(1);
        var [page, setPage] = useState(1);
        var [loading, setLoading] = useState(true);
        var [search, setSearch] = useState('');
        var [statusFilter, setStatusFilter] = useState('');
        var [ratingFilter, setRatingFilter] = useState('');
        var [selected, setSelected] = useState(new Set());
        var [msg, setMsg] = useState(null);
        var [modal, setModal] = useState(null);
        var [form, setForm] = useState({});
        var [saving, setSaving] = useState(false);
        var [stats, setStats] = useState({ total:0, pending:0, approved:0, rejected:0 });

        function flash(type, text) { setMsg({type,text}); setTimeout(function(){ setMsg(null); }, 3000); }
        var labelSty = { fontSize:12, fontWeight:500, display:'block', marginBottom:S.xs, color:mutedColor };

        /* ═══ DATA ═══ */

        function loadStats() {
            apiFetch('/admin/comments/stats').then(function(d) {
                setStats(d && d.testimonial ? d.testimonial : { total:0, pending:0, approved:0, rejected:0 });
            }).catch(function(){});
        }

        function loadList() {
            setLoading(true);
            var q = new URLSearchParams({ page:page, per_page:TC_PER_PAGE, object_type:'testimonial' });
            if (statusFilter) q.set('status', statusFilter);
            if (ratingFilter) q.set('rating', ratingFilter);
            if (search) q.set('search', search);
            apiFetch('/admin/comments?' + q.toString()).then(function(d) {
                setRows(d.comments||[]);
                setTotal(d.total||0);
                setPages(d.pages||1);
                setLoading(false);
            }).catch(function(){ setLoading(false); });
        }

        useEffect(function(){ loadStats(); }, []);
        useEffect(function(){ setPage(1); setSelected(new Set()); }, [statusFilter, ratingFilter, search]);
        useEffect(function(){ loadList(); }, [page, statusFilter, ratingFilter, search]);

        /* ═══ ACTIONS ═══ */

        function moderate(id, status) {
            apiFetch('/admin/comments/' + id + '/status', { method:'PUT', body:{ status:status } })
                .then(function(){ flash('success', 'Testimonial ' + status + '.'); loadList(); loadStats(); })
                .catch(function(e){ flash('error', e.message||'Failed.'); });
        }

        function bulkModerate(status) {
            if (selected.size === 0) return;
            var promises = [];
            selected.forEach(function(id) {
                promises.push(apiFetch('/admin/comments/' + id + '/status', { method:'PUT', body:{ status:status } }));
            });
            Promise.all(promises).then(function() {
                flash('success', selected.size + ' testimonial(s) ' + status + '.'); setSelected(new Set()); loadList(); loadStats();
            }).catch(function(){ loadList(); });
        }

        function deleteItem(id) {
            if (!confirm('Permanently delete this testimonial?')) return;
            apiFetch('/admin/comments/' + id, { method:'DELETE' }).then(function() {
                flash('success', 'Testimonial deleted.'); loadList(); loadStats();
            }).catch(function(){});
        }

        function toggleSelect(id) { var s = new Set(selected); if (s.has(id)) s.delete(id); else s.add(id); setSelected(s); }
        function toggleAll() { if (selected.size === rows.length) setSelected(new Set()); else setSelected(new Set(rows.map(function(r){ return Number(r.id); }))); }

        /* ═══ MODAL ═══ */

        function openAdd() {
            setForm({ object_type:'testimonial', author_name:'', author_email:'', rating:'5', title:'', body:'', status:'approved' });
            setModal({ mode:'new' });
        }

        function openEdit(row) {
            setForm({
                object_type:'testimonial', author_name:row.author_name||'', author_email:row.author_email||'',
                rating:String(row.rating||'5'), title:row.title||'', body:row.body||'', status:row.status||'pending'
            });
            setModal({ mode:'edit', id:row.id });
        }

        function saveItem() {
            setSaving(true);
            var body = Object.assign({}, form);
            var prom = modal.mode === 'edit'
                ? apiFetch('/admin/comments/' + modal.id, { method:'PUT', body:body })
                : apiFetch('/admin/comments', { method:'POST', body:body });
            prom.then(function() {
                setSaving(false); setModal(null);
                flash('success', modal.mode==='edit' ? 'Testimonial updated.' : 'Testimonial created.');
                loadList(); loadStats();
            }).catch(function(e) { setSaving(false); flash('error', e.message||'Failed.'); });
        }

        /* ═══ HELPERS ═══ */

        var STATUS_COLORS = {
            pending:  { bg:dark?'rgba(217,164,65,.12)':'#FEF6EC', color:'#D97706' },
            approved: { bg:dark?'rgba(22,163,74,.12)':'#ECFDF5',  color:'#16A34A' },
            rejected: { bg:dark?'rgba(220,38,38,.12)':'#FEF2F2',  color:'#DC2626' },
        };
        function statusBadge(s) {
            var c = STATUS_COLORS[s] || STATUS_COLORS.pending;
            return h('span', { style:{ fontSize:11, fontWeight:500, padding:'2px 8px', borderRadius:5, background:c.bg, color:c.color, textTransform:'capitalize', width:'fit-content', display:'inline-block' } }, s);
        }
        function stars(n) {
            if (!n) return '—';
            var out = '';
            for (var i = 0; i < 5; i++) out += i < n ? '★' : '☆';
            return h('span', { style:{ color:'#D97706', fontSize:13, letterSpacing:1 } }, out);
        }
        function ago(dateStr) {
            if (!dateStr) return '';
            var d = tcParseDate(dateStr), now = new Date(), diff = Math.floor((now - d) / 60000);
            if (diff < 1) return 'just now';
            if (diff < 60) return diff + 'm ago';
            if (diff < 1440) return Math.floor(diff/60) + 'h ago';
            return Math.floor(diff/1440) + 'd ago';
        }

        /* ═══ RENDER ═══ */

        return h(PageWrap, { title:'Testimonials', sub:'Manage customer testimonials displayed on your storefront', mutedColor },

            msg && h('div', { style:{ padding:'10px 16px', borderRadius:8, marginBottom:S.lg, fontSize:13, fontWeight:500,
                background:msg.type==='success'?(dark?'rgba(22,163,74,.12)':'#ECFDF5'):(dark?'rgba(220,38,38,.12)':'#FEF2F2'),
                color:msg.type==='success'?'#16A34A':'#DC2626' } }, msg.text),

            /* ── Stats Row ── */
            h('div', { style:{ display:'grid', gridTemplateColumns:'repeat(4,1fr)', gap:S.lg, marginBottom:S.xl } },
                [
                    { label:'Total', value:stats.total||0, color:TC.accent },
                    { label:'Pending', value:stats.pending||0, color:'#D97706' },
                    { label:'Approved', value:stats.approved||0, color:'#16A34A' },
                    { label:'Rejected', value:stats.rejected||0, color:'#DC2626' },
                ].map(function(s) {
                    return h(Card, { key:s.label, cardBg:cardBg, borderColor:borderColor, dark:dark },
                        h('div', { style:{ fontSize:12, color:mutedColor, fontWeight:500, marginBottom:S.xs } }, s.label),
                        h('div', { style:{ fontSize:24, fontWeight:700, color:s.color } }, s.value)
                    );
                })
            ),

            h(Card, { cardBg, borderColor, dark },

                /* ── Toolbar ── */
                h('div', { style:{ display:'flex', alignItems:'center', justifyContent:'space-between', marginBottom:S.xl, flexWrap:'wrap', gap:S.md } },
                    h('div', { style:{ display:'flex', gap:S.sm } },
                        selected.size > 0 && h(Btn, { variant:'outline', onClick:function(){ bulkModerate('approved'); }, mutedColor, borderColor }, '✓ Approve'),
                        selected.size > 0 && h(Btn, { variant:'outline', onClick:function(){ bulkModerate('rejected'); }, mutedColor, borderColor }, '✕ Reject')
                    ),
                    h(Btn, { variant:'primary', onClick:openAdd, mutedColor, borderColor }, '+ Add Testimonial')
                ),

                /* ── Filters ── */
                h('div', { style:{ display:'flex', gap:S.md, marginBottom:S.lg, flexWrap:'wrap' } },
                    h('input', { style:Object.assign({},fs,{flex:1,minWidth:180}), placeholder:'Search author, message…', value:search, onChange:function(e){ setSearch(e.target.value); } }),
                    h('select', { style:Object.assign({},fs,{width:140,cursor:'pointer'}), value:statusFilter, onChange:function(e){ setStatusFilter(e.target.value); } },
                        h('option', { value:'' }, 'All Statuses'),
                        h('option', { value:'pending' }, 'Pending'),
                        h('option', { value:'approved' }, 'Approved'),
                        h('option', { value:'rejected' }, 'Rejected')
                    ),
                    h('select', { style:Object.assign({},fs,{width:140,cursor:'pointer'}), value:ratingFilter, onChange:function(e){ setRatingFilter(e.target.value); } },
                        h('option', { value:'' }, 'All Ratings'),
                        h('option', { value:'5' }, '★★★★★'),
                        h('option', { value:'4' }, '★★★★☆'),
                        h('option', { value:'3' }, '★★★☆☆'),
                        h('option', { value:'2' }, '★★☆☆☆'),
                        h('option', { value:'1' }, '★☆☆☆☆')
                    )
                ),

                /* ── Table ── */
                loading
                    ? h('div', { style:{ textAlign:'center', padding:S.xxl+' 0', color:mutedColor } }, 'Loading…')
                    : rows.length === 0
                        ? h(EmptyState, { mutedColor, icon:'💬', title:'No testimonials yet', sub:'Click "+ Add Testimonial" to create your first one.' })
                        : h('div', null,
                            /* Header */
                            h('div', { style:{ display:'grid', gridTemplateColumns:'0.3fr 1.2fr 2fr 0.7fr 0.7fr 0.5fr', padding:S.md+' '+S.lg+'px', borderBottom:'1px solid '+borderColor, background:tableHeaderBg, boxShadow:tableHeaderShadow, borderRadius:'8px 8px 0 0', gap:S.sm } },
                                h('input', { type:'checkbox', checked:selected.size===rows.length&&rows.length>0, onChange:toggleAll, style:{ cursor:'pointer' } }),
                                ['Author','Message','Rating','Status','Actions'].map(function(col) {
                                    return h('span', { key:col, style:{ fontSize:10, fontWeight:500, color:mutedColor, textTransform:'uppercase', letterSpacing:'.5px' } }, col);
                                })
                            ),

                            /* Rows */
                            rows.map(function(row) {
                                var rid = Number(row.id);
                                var isSelected = selected.has(rid);
                                return h('div', { key:rid, style:{
                                    display:'grid', gridTemplateColumns:'0.3fr 1.2fr 2fr 0.7fr 0.7fr 0.5fr',
                                    padding:'12px '+S.lg+'px', borderBottom:'1px solid '+borderColor,
                                    alignItems:'center', gap:S.sm, transition:'background .1s',
                                    background:isSelected?(dark?'rgba(37,99,235,.08)':'rgba(37,99,235,.04)'):'transparent',
                                },
                                onMouseEnter:function(e){ if(!isSelected) e.currentTarget.style.background=dark?'rgba(255,255,255,.02)':'#F9FAFB'; },
                                onMouseLeave:function(e){ if(!isSelected) e.currentTarget.style.background='transparent'; }
                                },
                                    h('input', { type:'checkbox', checked:isSelected, onChange:function(){ toggleSelect(rid); }, style:{ cursor:'pointer' } }),

                                    /* Author */
                                    h('div', null,
                                        h('div', { style:{ fontSize:13, fontWeight:500 } }, row.author_name || 'Anonymous'),
                                        h('div', { style:{ fontSize:11, color:mutedColor } }, row.author_email || ''),
                                        h('div', { style:{ fontSize:10, color:mutedColor } }, ago(row.created_at))
                                    ),

                                    /* Message */
                                    h('div', null,
                                        row.title && h('div', { style:{ fontSize:12, fontWeight:600, marginBottom:2 } }, row.title),
                                        h('div', { style:{ fontSize:12, color:mutedColor, overflow:'hidden', textOverflow:'ellipsis', display:'-webkit-box', WebkitLineClamp:2, WebkitBoxOrient:'vertical' } }, row.body || '(no message)')
                                    ),

                                    /* Rating */
                                    stars(Number(row.rating)),

                                    /* Status */
                                    statusBadge(row.status),

                                    /* Actions */
                                    h('div', { style:{ display:'flex', gap:4 } },
                                        row.status !== 'approved' && h('button', { onClick:function(){ moderate(rid,'approved'); }, title:'Approve',
                                            style:{ background:'transparent', border:'1px solid #16A34A30', borderRadius:5, padding:'3px 6px', fontSize:12, cursor:'pointer', color:'#16A34A' } }, '✓'),
                                        row.status !== 'rejected' && h('button', { onClick:function(){ moderate(rid,'rejected'); }, title:'Reject',
                                            style:{ background:'transparent', border:'1px solid #DC262630', borderRadius:5, padding:'3px 6px', fontSize:12, cursor:'pointer', color:'#DC2626' } }, '✕'),
                                        h('button', { onClick:function(){ openEdit(row); }, title:'Edit',
                                            style:{ background:'transparent', border:'1px solid '+borderColor, borderRadius:5, padding:'3px 6px', fontSize:12, cursor:'pointer', color:mutedColor } }, '✎'),
                                        h('button', { onClick:function(){ deleteItem(rid); }, title:'Delete',
                                            style:{ background:'transparent', border:'1px solid #DC262620', borderRadius:5, padding:'3px 6px', fontSize:12, cursor:'pointer', color:'#DC2626' } }, '🗑')
                                    )
                                );
                            }),

                            /* Pagination */
                            pages > 1 && h('div', { style:{ display:'flex', justifyContent:'space-between', alignItems:'center', padding:S.md+' '+S.lg+'px' } },
                                h('span', { style:{ fontSize:12, color:mutedColor } }, 'Showing ' + rows.length + ' of ' + total),
                                h('div', { style:{ display:'flex', gap:S.xs } },
                                    h(Btn, { variant:'outline', onClick:function(){ setPage(Math.max(1,page-1)); }, disabled:page<=1, mutedColor, borderColor }, '‹ Prev'),
                                    h('span', { style:{ fontSize:12, color:mutedColor, padding:'6px 12px' } }, page + ' / ' + pages),
                                    h(Btn, { variant:'outline', onClick:function(){ setPage(Math.min(pages,page+1)); }, disabled:page>=pages, mutedColor, borderColor }, 'Next ›')
                                )
                            )
                        )
            ),

            /* ════ MODAL — Add / Edit ════ */
            modal && h('div', { style:{ position:'fixed', inset:0, zIndex:9999, display:'flex', alignItems:'center', justifyContent:'center' } },
                h('div', { style:{ position:'absolute', inset:0, background:'rgba(0,0,0,.45)', backdropFilter:'blur(2px)' }, onClick:function(){ setModal(null); } }),
                h('div', { style:{ position:'relative', background:cardBg, borderRadius:14, padding:S.xxl, width:540, maxWidth:'92vw', maxHeight:'85vh', overflow:'auto', border:'1px solid '+borderColor, boxShadow:'0 24px 64px rgba(0,0,0,.2)' } },

                    h('div', { style:{ display:'flex', justifyContent:'space-between', alignItems:'center', marginBottom:S.xl } },
                        h('h3', { style:{ fontSize:17, fontWeight:600 } }, modal.mode === 'edit' ? 'Edit Testimonial' : 'Add Testimonial'),
                        h('button', { onClick:function(){ setModal(null); }, style:{ background:'transparent', border:'none', fontSize:20, cursor:'pointer', color:mutedColor, lineHeight:1 } }, '✕')
                    ),

                    h('div', { style:{ display:'grid', gridTemplateColumns:'1fr 1fr', gap:S.md } },
                        h('div', null,
                            h('label', { style:labelSty }, 'Customer Name *'),
                            h('input', { style:fs, value:form.author_name||'', onChange:function(e){ setForm(Object.assign({},form,{author_name:e.target.value})); } })
                        ),
                        h('div', null,
                            h('label', { style:labelSty }, 'Customer Email'),
                            h('input', { style:fs, type:'email', value:form.author_email||'', onChange:function(e){ setForm(Object.assign({},form,{author_email:e.target.value})); } })
                        )
                    ),

                    h('div', { style:{ marginTop:S.md } },
                        h('label', { style:labelSty }, 'Rating'),
                        h('div', { style:{ display:'flex', gap:S.sm } },
                            [1,2,3,4,5].map(function(n) {
                                var active = Number(form.rating) >= n;
                                return h('button', { key:n, onClick:function(){ setForm(Object.assign({},form,{rating:String(n)})); },
                                    style:{ background:'transparent', border:'none', cursor:'pointer', fontSize:24, color:active?'#D97706':'#d4d4d4', padding:0, transition:'color .1s' } }, '★');
                            })
                        )
                    ),

                    h('label', { style:Object.assign({},labelSty,{marginTop:S.md}) }, 'Title'),
                    h('input', { style:fs, value:form.title||'', placeholder:'Short headline (optional)', onChange:function(e){ setForm(Object.assign({},form,{title:e.target.value})); } }),

                    h('label', { style:Object.assign({},labelSty,{marginTop:S.md}) }, 'Testimonial *'),
                    h('textarea', { style:Object.assign({},fs,{minHeight:100}), value:form.body||'', placeholder:'What the customer said…', onChange:function(e){ setForm(Object.assign({},form,{body:e.target.value})); } }),

                    h('label', { style:Object.assign({},labelSty,{marginTop:S.md}) }, 'Status'),
                    h('select', { style:Object.assign({},fs,{width:180,cursor:'pointer'}), value:form.status||'approved', onChange:function(e){ setForm(Object.assign({},form,{status:e.target.value})); } },
                        h('option', { value:'approved' }, 'Approved'),
                        h('option', { value:'pending' }, 'Pending'),
                        h('option', { value:'rejected' }, 'Rejected')
                    ),

                    h('div', { style:{ display:'flex', justifyContent:'flex-end', gap:S.sm, marginTop:S.xxl } },
                        h(Btn, { variant:'outline', onClick:function(){ setModal(null); }, mutedColor, borderColor }, 'Cancel'),
                        h(Btn, { variant:'primary', onClick:saveItem, disabled:saving||!form.author_name||!form.body, mutedColor, borderColor }, saving ? 'Saving…' : modal.mode==='edit' ? 'Update' : 'Create')
                    )
                )
            )
        );
    }

    /* ════════════════════════════════════════════════════════════════════
     * FaqPage — Storefront → FAQ (S478, Sean). Unlimited question/answer rows,
     * grouped by a free-text category, ▲▼ ordered. Rendered on the storefront
     * by the [titancart_faq] shortcode as a native <details> accordion.
     * REST: /faqs (list/create), /faqs/{id} (update/delete), /faqs/reorder.
     * ════════════════════════════════════════════════════════════════════ */
    function FaqPage({ dark, cardBg, textColor, mutedColor, borderColor, tableHeaderBg }) {
        var fs = fieldSty(dark, borderColor);
        var [rows, setRows] = useState([]);
        var [cats, setCats] = useState([]);
        var [loading, setLoading] = useState(true);
        var [search, setSearch] = useState('');
        var [statusFilter, setStatusFilter] = useState('');
        var [msg, setMsg] = useState(null);
        var [modal, setModal] = useState(null);
        var [form, setForm] = useState({});
        var [saving, setSaving] = useState(false);
        var [copied, setCopied] = useState(false);

        function flash(type, text) { setMsg({type,text}); setTimeout(function(){ setMsg(null); }, 3000); }
        var labelSty = { fontSize:12, fontWeight:500, display:'block', marginBottom:S.xs, color:mutedColor };

        function load() {
            setLoading(true);
            var q = new URLSearchParams();
            if (search) q.set('search', search);
            if (statusFilter) q.set('status', statusFilter);
            apiFetch('/faqs' + (q.toString() ? '?' + q.toString() : '')).then(function(d) {
                setRows(d.faqs || []); setCats(d.categories || []); setLoading(false);
            }).catch(function(){ setLoading(false); });
        }
        useEffect(function(){ load(); }, [search, statusFilter]);

        function openAdd() { setForm({ question:'', answer:'', category:'', status:'active' }); setModal({ mode:'new' }); }
        function openEdit(r) { setForm({ question:r.question||'', answer:r.answer||'', category:r.category||'', status:r.status||'active' }); setModal({ mode:'edit', id:r.id }); }
        function saveItem() {
            setSaving(true);
            var prom = modal.mode === 'edit'
                ? apiFetch('/faqs/' + modal.id, { method:'PUT', body:form })
                : apiFetch('/faqs', { method:'POST', body:form });
            prom.then(function(){ setSaving(false); setModal(null); flash('success', modal.mode==='edit' ? 'Question updated.' : 'Question added.'); load(); })
                .catch(function(e){ setSaving(false); flash('error', (e && e.message) || 'Failed.'); });
        }
        function deleteItem(r) {
            if (!confirm('Delete this question?')) return;
            apiFetch('/faqs/' + r.id, { method:'DELETE' }).then(function(){ flash('success', 'Question deleted.'); load(); }).catch(function(){});
        }
        function toggleStatus(r) {
            apiFetch('/faqs/' + r.id, { method:'PUT', body:{ status: r.status === 'active' ? 'disabled' : 'active' } }).then(load).catch(function(){});
        }
        // Move within its category group; persist the whole visible order (positions 1..n per group).
        function move(r, dir) {
            var group = rows.filter(function(x){ return (x.category||'') === (r.category||''); });
            var i = group.findIndex(function(x){ return Number(x.id) === Number(r.id); });
            var j = i + dir;
            if (i < 0 || j < 0 || j >= group.length) return;
            var tmp = group[i]; group[i] = group[j]; group[j] = tmp;
            apiFetch('/faqs/reorder', { method:'POST', body:{ order: group.map(function(x){ return Number(x.id); }) } }).then(load).catch(function(){});
        }
        function copyShortcode() {
            var t = '[titancart_faq]';
            try { navigator.clipboard.writeText(t); setCopied(true); setTimeout(function(){ setCopied(false); }, 1500); } catch(e) { prompt('Copy the shortcode:', t); }
        }

        // Group for display: ungrouped first, then categories A–Z (server order).
        var groups = [];
        rows.forEach(function(r) {
            var c = r.category || '';
            var g = groups.find(function(x){ return x.cat === c; });
            if (!g) { g = { cat:c, items:[] }; groups.push(g); }
            g.items.push(r);
        });
        var arrow = function(disabled){ return { background:'transparent', border:'1px solid '+borderColor, borderRadius:4, width:20, height:20, padding:0, fontSize:9, cursor:disabled?'not-allowed':'pointer', color:disabled?borderColor:mutedColor }; };

        return h(PageWrap, { title:'FAQ', sub:'Questions and answers for your storefront FAQ page', mutedColor },
            msg && h('div', { style:{ padding:'10px 16px', borderRadius:8, marginBottom:S.lg, fontSize:13, fontWeight:500,
                background:msg.type==='success'?(dark?'rgba(22,163,74,.12)':'#ECFDF5'):(dark?'rgba(220,38,38,.12)':'#FEF2F2'),
                color:msg.type==='success'?'#16A34A':'#DC2626' } }, msg.text),

            h(Card, { cardBg, borderColor, dark },
                h('div', { style:{ display:'flex', alignItems:'center', justifyContent:'space-between', marginBottom:S.lg, flexWrap:'wrap', gap:S.md } },
                    h('div', { style:{ fontSize:12, color:mutedColor, lineHeight:1.6 } },
                        'Put the shortcode ', h('code', { style:{ padding:'2px 6px', borderRadius:4, background:dark?'rgba(255,255,255,.06)':'#F3F4F6', fontSize:12 } }, '[titancart_faq]'),
                        ' on any page (e.g. "FAQ"). It renders every active question as an accordion, grouped by category. ',
                        h('button', { onClick:copyShortcode, style:{ background:'transparent', border:'none', color:TC.accent, cursor:'pointer', fontSize:12, padding:0 } }, copied ? 'Copied ✓' : 'Copy shortcode'),
                        h('span', { style:{ display:'block', marginTop:2 } }, 'One category only: ', h('code', { style:{ fontSize:11 } }, '[titancart_faq category="Shipping"]'), ' · first answer open: ', h('code', { style:{ fontSize:11 } }, 'open_first="1"'))
                    ),
                    h(Btn, { variant:'primary', onClick:openAdd, mutedColor, borderColor }, '+ Add Question')
                ),
                h('div', { style:{ display:'flex', gap:S.md, marginBottom:S.lg, flexWrap:'wrap' } },
                    h('input', { style:Object.assign({},fs,{flex:1,minWidth:180}), placeholder:'Search questions, answers, categories…', value:search, onChange:function(e){ setSearch(e.target.value); } }),
                    h('select', { style:Object.assign({},fs,{width:150,cursor:'pointer'}), value:statusFilter, onChange:function(e){ setStatusFilter(e.target.value); } },
                        h('option', { value:'' }, 'All'), h('option', { value:'active' }, 'Shown'), h('option', { value:'disabled' }, 'Hidden'))
                ),
                loading
                    ? h('div', { style:{ textAlign:'center', padding:S.xxl+' 0', color:mutedColor } }, 'Loading…')
                    : rows.length === 0
                        ? h(EmptyState, { mutedColor, icon:'❓', title:'No questions yet', sub:'Click "+ Add Question" — as many as you need. Give them a category to group them on the page.' })
                        : groups.map(function(g) {
                            return h('div', { key:g.cat || '__none', style:{ marginBottom:S.xl } },
                                h('div', { style:{ fontSize:11, fontWeight:600, color:mutedColor, textTransform:'uppercase', letterSpacing:'.5px', padding:'6px '+S.lg+'px', background:tableHeaderBg, borderRadius:'8px 8px 0 0', borderBottom:'1px solid '+borderColor } }, g.cat || 'Ungrouped'),
                                g.items.map(function(r, idx) {
                                    return h('div', { key:r.id, style:{ display:'grid', gridTemplateColumns:'64px 1fr 90px 120px', gap:S.sm, alignItems:'center', padding:'10px '+S.lg+'px', borderBottom:'1px solid '+borderColor, opacity:r.status==='active'?1:.55 } },
                                        h('div', { style:{ display:'flex', gap:3, alignItems:'center' } },
                                            h('span', { style:{ fontSize:12, color:mutedColor, minWidth:16 } }, idx+1),
                                            h('button', { title:'Move up', disabled:idx===0, onClick:function(){ move(r,-1); }, style:arrow(idx===0) }, '\u25B2'),
                                            h('button', { title:'Move down', disabled:idx===g.items.length-1, onClick:function(){ move(r,1); }, style:arrow(idx===g.items.length-1) }, '\u25BC')),
                                        h('div', null,
                                            h('div', { style:{ fontSize:13, fontWeight:500 } }, r.question),
                                            h('div', { style:{ fontSize:12, color:mutedColor, overflow:'hidden', textOverflow:'ellipsis', display:'-webkit-box', WebkitLineClamp:2, WebkitBoxOrient:'vertical' } }, (r.answer||'').replace(/<[^>]+>/g,' ').trim() || '(no answer yet)')),
                                        h('button', { onClick:function(){ toggleStatus(r); }, title:'Click to '+(r.status==='active'?'hide':'show'),
                                            style:{ fontSize:11, fontWeight:500, padding:'2px 8px', borderRadius:5, border:'none', cursor:'pointer',
                                                    background:r.status==='active'?(dark?'rgba(22,163,74,.12)':'#ECFDF5'):(dark?'rgba(255,255,255,.06)':'#F3F4F6'),
                                                    color:r.status==='active'?'#16A34A':mutedColor } }, r.status==='active'?'Shown':'Hidden'),
                                        h('div', { style:{ display:'flex', gap:4, justifyContent:'flex-end' } },
                                            h('button', { onClick:function(){ openEdit(r); }, title:'Edit', style:{ background:'transparent', border:'1px solid '+borderColor, borderRadius:5, padding:'3px 8px', fontSize:12, cursor:'pointer', color:mutedColor } }, '\u270E'),
                                            h('button', { onClick:function(){ deleteItem(r); }, title:'Delete', style:{ background:'transparent', border:'1px solid #DC262620', borderRadius:5, padding:'3px 8px', fontSize:12, cursor:'pointer', color:'#DC2626' } }, '\uD83D\uDDD1'))
                                    );
                                })
                            );
                        })
            ),

            modal && h('div', { style:{ position:'fixed', inset:0, zIndex:9999, display:'flex', alignItems:'center', justifyContent:'center' } },
                h('div', { style:{ position:'absolute', inset:0, background:'rgba(0,0,0,.45)', backdropFilter:'blur(2px)' }, onClick:function(){ setModal(null); } }),
                h('div', { style:{ position:'relative', background:cardBg, borderRadius:14, padding:S.xxl, width:620, maxWidth:'92vw', maxHeight:'85vh', overflow:'auto', border:'1px solid '+borderColor, boxShadow:'0 24px 64px rgba(0,0,0,.2)' } },
                    h('div', { style:{ display:'flex', justifyContent:'space-between', alignItems:'center', marginBottom:S.xl } },
                        h('h3', { style:{ fontSize:17, fontWeight:600 } }, modal.mode === 'edit' ? 'Edit Question' : 'Add Question'),
                        h('button', { onClick:function(){ setModal(null); }, style:{ background:'transparent', border:'none', fontSize:20, cursor:'pointer', color:mutedColor, lineHeight:1 } }, '\u2715')),
                    h('label', { style:labelSty }, 'Question *'),
                    h('input', { style:fs, value:form.question||'', placeholder:'e.g. Do you deliver on weekends?', onChange:function(e){ setForm(Object.assign({},form,{question:e.target.value})); } }),
                    h('label', { style:Object.assign({},labelSty,{marginTop:S.md}) }, 'Answer'),
                    h('textarea', { style:Object.assign({},fs,{minHeight:140,resize:'vertical',fontFamily:'inherit'}), value:form.answer||'', placeholder:'Plain text or simple HTML (links, lists, bold). Blank lines make paragraphs.', onChange:function(e){ setForm(Object.assign({},form,{answer:e.target.value})); } }),
                    h('div', { style:{ display:'grid', gridTemplateColumns:'1fr 160px', gap:S.md, marginTop:S.md } },
                        h('div', null,
                            h('label', { style:labelSty }, 'Category (group heading on the page)'),
                            h('input', { style:fs, list:'tc-faq-cats', value:form.category||'', placeholder:'e.g. Ordering, Delivery, Returns — blank = ungrouped', onChange:function(e){ setForm(Object.assign({},form,{category:e.target.value})); } }),
                            h('datalist', { id:'tc-faq-cats' }, cats.map(function(c){ return h('option', { key:c, value:c }); }))),
                        h('div', null,
                            h('label', { style:labelSty }, 'Visibility'),
                            h('select', { style:Object.assign({},fs,{cursor:'pointer'}), value:form.status||'active', onChange:function(e){ setForm(Object.assign({},form,{status:e.target.value})); } },
                                h('option', { value:'active' }, 'Shown'), h('option', { value:'disabled' }, 'Hidden')))),
                    h('div', { style:{ display:'flex', justifyContent:'flex-end', gap:S.sm, marginTop:S.xl } },
                        h(Btn, { variant:'outline', onClick:function(){ setModal(null); }, mutedColor, borderColor }, 'Cancel'),
                        h(Btn, { variant:'primary', onClick:saveItem, disabled:saving||!(form.question||'').trim(), mutedColor, borderColor }, saving ? 'Saving…' : modal.mode==='edit' ? 'Update' : 'Add'))
                )
            )
        );
    }

    /* ════════════════════════════════════════════════════════════════════
     * PrintfulPage — Printful print-on-demand extension (S334, roadmap #14).
     * Phase 1: connect a store-level Private Token (validated server-side against
     * Printful GET /stores), then configure enable / order-handling / stock /
     * auto-sync. Talks to the extension's own REST namespace
     * (titancart-printful/v1), NOT core. Gated into the Growth menu by
     * Config.printfulInstalled; connect + the master enable toggle are gated by
     * Config.printfulLicenseActive. The token is write-only over REST (never
     * echoed back). Product import lands in a later phase.
     * ════════════════════════════════════════════════════════════════════ */
    function PrintfulPage({ subId, dark, cardBg, textColor, mutedColor, borderColor, tableHeaderBg }) {
        var fs = fieldSty(dark, borderColor);
        var PF_API = Config.printfulApiUrl || (Config.apiUrl || '').replace('/titancart/v1', '/titancart-printful/v1');
        var licenseActive = !!Config.printfulLicenseActive;

        function pfApiFetch(path, opts) {
            opts = opts || {};
            opts.headers = Object.assign({ 'Content-Type':'application/json', 'X-WP-Nonce': Config.nonce }, opts.headers || {});
            if (opts.body && typeof opts.body === 'object') opts.body = JSON.stringify(opts.body);
            return fetch(PF_API + path, opts).then(function(r) {
                return r.text().then(function(text) {
                    var data = null;
                    if (text) { try { data = JSON.parse(text); } catch(e) { data = null; } }
                    if (!r.ok || (data && data.success === false)) {
                        var m = (data && data.error && data.error.message) || (data && data.message) || ('HTTP ' + r.status);
                        var err = new Error(m); err.status = r.status; err.data = data;
                        err.code = data && data.error && data.error.code;
                        return Promise.reject(err);
                    }
                    return data;
                });
            });
        }

        function asBool(v) { return v === '1' || v === 1 || v === true; }

        function normalize(s) {
            s = s || {};
            return {
                connected:           !!s.connected,
                enabled:             asBool(s.enabled),
                store_name:          s.store_name != null ? String(s.store_name) : '',
                store_id:            s.store_id != null ? String(s.store_id) : '',
                import_scope:        s.import_scope != null ? String(s.import_scope) : 'all',
                stock_mode:          s.stock_mode != null ? String(s.stock_mode) : 'in_stock',
                order_mode:          s.order_mode != null ? String(s.order_mode) : 'draft',
                auto_sync:           asBool(s.auto_sync),
                live_shipping_rates: asBool(s.live_shipping_rates),
                default_category_id: s.default_category_id != null ? String(s.default_category_id) : '0',
                webhooks:            (s.webhooks && typeof s.webhooks === 'object') ? s.webhooks : null
            };
        }

        var _f = useState(null), form = _f[0], setForm = _f[1];
        var _l = useState(true), loading = _l[0], setLoading = _l[1];
        var _s = useState(false), saving = _s[0], setSaving = _s[1];
        var _m = useState(null), msg = _m[0], setMsg = _m[1];
        var _t = useState(''), token = _t[0], setToken = _t[1];
        var _c = useState(false), connecting = _c[0], setConnecting = _c[1];
        var _o = useState({ connection:true, settings:true, webhooks:true, import:true }), open = _o[0], setOpen = _o[1];
        var _wr = useState(false), regWH = _wr[0], setRegWH = _wr[1];
        var _im = useState(false), importing = _im[0], setImporting = _im[1];
        var _ip = useState(null), importProg = _ip[0], setImportProg = _ip[1];
        var importPoll = useRef(null);

        function toggleSection(id) { setOpen(function(p){ var n = Object.assign({}, p); n[id] = !n[id]; return n; }); }
        var edCtx = { mutedColor:mutedColor, borderColor:borderColor, textColor:textColor, open:open, onToggle:toggleSection };

        function load() {
            setLoading(true);
            pfApiFetch('/settings').then(function(d) {
                setForm(normalize((d && d.data) || d));
                setLoading(false);
            }).catch(function(e) {
                setMsg({ type:'error', text:e.message || 'Failed to load settings.' });
                setLoading(false);
            });
        }
        useEffect(function(){ load(); }, []);

        function set(k, v) { setForm(function(f){ var n = Object.assign({}, f); n[k] = v; return n; }); }

        function doConnect() {
            var t = (token || '').trim();
            if (t.length < 20) { setMsg({ type:'error', text:'Paste your Printful private token first.' }); return; }
            setConnecting(true); setMsg(null);
            pfApiFetch('/connect', { method:'POST', body:{ token:t } }).then(function(d) {
                setForm(normalize((d && d.data) || d));
                setToken('');
                var name = (d && d.meta && d.meta.store_name) || '';
                setMsg({ type:'success', text:'Connected' + (name ? ' to ' + name : '') + '.' });
                setConnecting(false);
            }).catch(function(e) {
                setMsg({ type:'error', text:e.message || 'Could not connect to Printful.' });
                setConnecting(false);
            });
        }

        function doDisconnect() {
            setConnecting(true); setMsg(null);
            pfApiFetch('/disconnect', { method:'POST' }).then(function(d) {
                setForm(normalize((d && d.data) || d));
                setMsg({ type:'success', text:'Disconnected.' });
                setConnecting(false);
            }).catch(function(e) {
                setMsg({ type:'error', text:e.message || 'Could not disconnect.' });
                setConnecting(false);
            });
        }

        function save() {
            if (!form) return;
            setSaving(true); setMsg(null);
            var payload = {
                enabled:             form.enabled ? '1' : '0',
                import_scope:        form.import_scope || 'all',
                stock_mode:          form.stock_mode || 'in_stock',
                order_mode:          form.order_mode || 'draft',
                auto_sync:           form.auto_sync ? '1' : '0',
                live_shipping_rates: form.live_shipping_rates ? '1' : '0',
                default_category_id: String(form.default_category_id != null ? form.default_category_id : '0')
            };
            pfApiFetch('/settings', { method:'PUT', body:payload }).then(function(d) {
                setForm(normalize((d && d.data) || d));
                setMsg({ type:'success', text:'Settings saved.' });
                setSaving(false);
            }).catch(function(e) {
                var t = e.message || 'Save failed.';
                if (e.code === 'tcpful_license_required') t = 'An active license is required to enable Printful.';
                else if (e.code === 'tcpful_not_connected') t = 'Connect a Printful account before enabling sync.';
                setMsg({ type:'error', text:t });
                setSaving(false);
            });
        }

        function doRegisterWebhooks() {
            if (regWH) return;
            setRegWH(true); setMsg(null);
            pfApiFetch('/webhooks/register', { method:'POST' }).then(function(d){
                if (d && d.data) set('webhooks', d.data);
                setMsg({ type:'success', text:'Fulfillment webhooks registered with Printful.' });
                setRegWH(false);
            }).catch(function(e){
                var t = e.message || 'Could not register webhooks.';
                if (e.code === 'tcpful_license_required') t = 'An active license is required to register webhooks.';
                else if (e.code === 'tcpful_not_connected') t = 'Connect a Printful account before registering webhooks.';
                setMsg({ type:'error', text:t });
                setRegWH(false);
            });
        }

        function stopImportPoll() { if (importPoll.current) { clearInterval(importPoll.current); importPoll.current = null; } }
        useEffect(function(){ return stopImportPoll; }, []);

        function doImport() {
            if (importing) return;
            setImporting(true); setMsg(null);
            setImportProg({ status:'running', total:0, done:0, created:0, adopted:0, updated:0, removed:0, groups:0, current:'' });
            stopImportPoll();
            importPoll.current = setInterval(function(){
                pfApiFetch('/import/status').then(function(d){
                    var p = (d && d.data) || null;
                    if (p) setImportProg(p);
                }).catch(function(){});
            }, 2000);
            pfApiFetch('/import', { method:'POST' }).then(function(d){
                stopImportPoll();
                var p = (d && d.data) || null;
                if (p) setImportProg(p);
                var created = (p && p.created) || 0;
                var updated = (p && p.updated) || 0;
                var removed = (p && p.removed) || 0;
                var groups  = (p && p.groups)  || 0;
                var bits = [ created + ' added' ];
                if (updated) bits.push(updated + ' updated');
                if (removed) bits.push(removed + ' removed');
                setMsg({ type:'success', text:'Import & sync complete — ' + bits.join(', ') + (groups ? ' across ' + groups + ' product group(s)' : '') + '.' });
                setImporting(false);
            }).catch(function(e){
                stopImportPoll();
                var t = e.message || 'Import failed.';
                if (e.code === 'tcpful_import_running') t = 'An import is already in progress.';
                else if (e.code === 'tcpful_not_connected') t = 'Connect a Printful account before importing.';
                else if (e.code === 'tcpful_license_required') t = 'An active license is required to import.';
                setMsg({ type:'error', text:t });
                setImporting(false);
            });
        }

        function toggleRow(key, label, note, disabled) {
            return h(FR, { label:label, note:note, ctx:edCtx, key:key },
                h('label', { style:{ display:'inline-flex', alignItems:'center', gap:S.sm, fontSize:14, cursor:disabled?'not-allowed':'pointer', paddingTop:6, opacity:disabled?0.55:1 } },
                    h('input', { type:'checkbox', checked:!!(form && form[key]), disabled:disabled,
                        onChange:function(e){ set(key, e.target.checked); } }),
                    h('span', { style:{ color:textColor } }, note || '')
                )
            );
        }

        function selectRow(key, label, note, options) {
            return h(FR, { label:label, note:note, ctx:edCtx, key:key },
                h('select', { value:(form && form[key] != null) ? form[key] : '', onChange:function(e){ set(key, e.target.value); },
                    style:Object.assign({}, fs, { width:280, cursor:'pointer' }) },
                    options.map(function(o){ return h('option', { key:o.value, value:o.value }, o.label); })
                )
            );
        }

        /* ── Render ── */
        if (loading) return h('main', { style:{ flex:1, overflow:'auto', padding:S.xl } }, h(Spinner, { mutedColor:mutedColor }));

        var connected = !!(form && form.connected);
        var canEnable = licenseActive && connected;

        return h('main', { style:{ flex:1, overflow:'auto', padding:S.xl } },
            h('div', { style:{ marginBottom:S.xl } },
                h('h1', { style:{ fontSize:22, fontWeight:700, margin:0, color:textColor } }, 'Printful'),
                h('p', { style:{ fontSize:13, color:mutedColor, margin:'6px 0 0' } },
                    'Connect your Printful account, then import your print-on-demand products into the TitanCart catalog and keep them in sync.')
            ),

            !licenseActive && h('div', { style:{ display:'flex', gap:10, alignItems:'flex-start', background:TC.amberBg, border:'1px solid '+TC.amber, borderRadius:8, padding:'12px 16px', marginBottom:S.lg } },
                h('span', { style:{ color:TC.amber, fontWeight:600, fontSize:13 } }, '⚠'),
                h('div', { style:{ fontSize:13, color:textColor } },
                    h('strong', null, 'License required. '),
                    'Printful is installed but not licensed. Enter your license under Extensions → Installed Extensions to connect an account and import products.')
            ),

            msg && h('div', { style:{ padding:'10px 16px', borderRadius:8, marginBottom:S.lg, fontSize:13, fontWeight:500,
                background: msg.type==='error' ? TC.errorBg : TC.successBg,
                color: msg.type==='error' ? TC.error : TC.success,
                border: '1px solid ' + (msg.type==='error' ? TC.error : TC.success) } }, msg.text),

            h('div', { style:{ background:cardBg, border:'1px solid '+borderColor, borderRadius:12, padding:'4px 20px 8px', marginBottom:S.lg } },
                h(SectionHead, { id:'connection', title:'Connection', ctx:edCtx }),
                open.connection && (connected
                    ? h('div', { style:{ paddingBottom:S.md } },
                        h('div', { style:{ display:'flex', alignItems:'center', gap:10, marginBottom:S.md } },
                            h('span', { style:{ display:'inline-block', width:9, height:9, borderRadius:999, background:TC.success } }),
                            h('span', { style:{ fontSize:14, color:textColor } },
                                'Connected to ', h('strong', null, (form && form.store_name) || 'your Printful store'),
                                (form && form.store_id) ? h('span', { style:{ color:mutedColor } }, '  (store #' + form.store_id + ')') : null)
                        ),
                        h(Btn, { variant:'outline', onClick:doDisconnect, disabled:connecting, mutedColor:mutedColor, borderColor:borderColor }, connecting ? 'Working…' : 'Disconnect')
                    )
                    : h('div', { style:{ paddingBottom:S.md } },
                        h('p', { style:{ fontSize:13, color:mutedColor, margin:'0 0 10px' } },
                            'Paste a store-level Private Token from the Printful Developer Portal (Settings → Developers → your token). Scopes needed: sync products, orders, stores, file library, webhooks.'),
                        h('div', { style:{ display:'flex', gap:S.sm, flexWrap:'wrap', alignItems:'center' } },
                            h('input', { type:'password', value:token, placeholder:'Printful private token', disabled:!licenseActive || connecting,
                                onChange:function(e){ setToken(e.target.value); },
                                style:Object.assign({}, fs, { width:360, maxWidth:'100%' }) }),
                            h(Btn, { variant:'primary', onClick:doConnect, disabled:!licenseActive || connecting, mutedColor:mutedColor, borderColor:borderColor },
                                connecting ? 'Connecting…' : 'Connect')
                        )
                    )
                )
            ),

            connected && h('div', { style:{ background:cardBg, border:'1px solid '+borderColor, borderRadius:12, padding:'4px 20px 12px', marginBottom:S.lg } },
                h(SectionHead, { id:'settings', title:'Import & fulfillment', ctx:edCtx }),
                open.settings && h('div', null,
                    toggleRow('enabled', 'Enable Printful', canEnable ? 'Turn on product sync and order fulfillment.' : 'Connect an account and activate your license to enable.', !canEnable),
                    selectRow('order_mode', 'Order handling', 'How paid orders are sent to Printful.', [
                        { value:'draft', label:'Create as draft (confirm in Printful)' },
                        { value:'auto',  label:'Auto-confirm on payment' }
                    ]),
                    selectRow('stock_mode', 'Stock', 'Print-on-demand is made to order.', [
                        { value:'in_stock',     label:'Always in stock' },
                        { value:'availability', label:'Follow Printful availability' }
                    ]),
                    toggleRow('auto_sync', 'Daily auto-sync', 'Automatically re-sync prices and availability once a day.', false),
                    toggleRow('live_shipping_rates', 'Live Printful shipping rates', 'Charge the live Printful shipping cost at checkout so print-on-demand shipping never eats your margin.', false),
                    h('div', { style:{ display:'flex', justifyContent:'flex-end', marginTop:S.md } },
                        h(Btn, { variant:'primary', onClick:save, disabled:saving, mutedColor:mutedColor, borderColor:borderColor }, saving ? 'Saving…' : 'Save settings')
                    ),
                    h('p', { style:{ fontSize:12, color:mutedColor, margin:'10px 0 0' } },
                        'Use Import & sync below to bring your Printful catalog into TitanCart.')
                )
            ),

            connected && h('div', { style:{ background:cardBg, border:'1px solid '+borderColor, borderRadius:12, padding:'4px 20px 16px', marginBottom:S.lg } },
                h(SectionHead, { id:'webhooks', title:'Fulfillment webhooks', ctx:edCtx }),
                open.webhooks && (function(){
                    var wh = (form && form.webhooks) || null;
                    var registered = !!(wh && wh.registered);
                    return h('div', null,
                        h('p', { style:{ fontSize:13, color:mutedColor, margin:'0 0 12px' } },
                            'When Printful ships an order it notifies TitanCart, so tracking is attached to the order automatically and the order clears the fulfillment queue. This registers once and re-checks itself each time you open the admin.'),
                        h('div', { style:{ display:'flex', alignItems:'center', gap:10, marginBottom:S.md } },
                            h('span', { style:{ display:'inline-block', width:9, height:9, borderRadius:999, background: registered ? TC.success : TC.amber } }),
                            h('span', { style:{ fontSize:14, color:textColor } },
                                registered ? 'Active — Printful will notify this store when packages ship.' : 'Not registered yet.')
                        ),
                        (registered && wh && wh.url) ? h('p', { style:{ fontSize:12, color:mutedColor, margin:'0 0 12px', wordBreak:'break-all' } },
                            'Endpoint: ', h('code', { style:{ color:textColor } }, wh.url)) : null,
                        h('div', { style:{ display:'flex', gap:S.sm, alignItems:'center', flexWrap:'wrap' } },
                            h(Btn, { variant: registered ? 'outline' : 'primary', onClick:doRegisterWebhooks, disabled:regWH, mutedColor:mutedColor, borderColor:borderColor },
                                regWH ? 'Working…' : (registered ? 'Re-register' : 'Register webhooks'))
                        )
                    );
                })()
            ),

            connected && h('div', { style:{ background:cardBg, border:'1px solid '+borderColor, borderRadius:12, padding:'4px 20px 16px', marginBottom:S.lg } },
                h(SectionHead, { id:'import', title:'Import & sync', ctx:edCtx }),
                open.import && h('div', null,
                    h('p', { style:{ fontSize:13, color:mutedColor, margin:'0 0 12px' } },
                        'Import your entire Printful catalog into TitanCart, or run it again any time to re-sync. Each Printful product becomes one storefront listing with its size/color options, and every variant keeps its own SKU for fulfillment. Re-running is safe and idempotent: new products are added, existing ones have their price, name, availability and images reconciled in place, and variants Printful no longer lists are marked out of stock.'),
                    h('div', { style:{ display:'flex', gap:S.sm, alignItems:'center', flexWrap:'wrap' } },
                        h(Btn, { variant:'primary', onClick:doImport, disabled:importing, mutedColor:mutedColor, borderColor:borderColor },
                            importing ? 'Importing…' : 'Import & sync'),
                        importing ? h('span', { style:{ fontSize:12, color:mutedColor } }, 'This can take a few minutes for large catalogs — keep this tab open.') : null
                    ),
                    importProg ? (function(){
                        var total = importProg.total || 0, done = importProg.done || 0;
                        var pct = total > 0 ? Math.min(100, Math.round(done / total * 100)) : (importing ? 5 : 0);
                        return h('div', { style:{ marginTop:S.md } },
                            h('div', { style:{ height:8, background:dark?'#333':'#eee', borderRadius:999, overflow:'hidden' } },
                                h('div', { style:{ height:'100%', width:pct+'%', background:TC.success, transition:'width .3s' } })),
                            h('div', { style:{ display:'flex', justifyContent:'space-between', fontSize:12, color:mutedColor, marginTop:6 } },
                                h('span', null, total ? (done + ' / ' + total + ' products') : (importing ? 'Fetching catalog…' : '')),
                                h('span', null, (function(){
                                    var parts = [];
                                    if (importProg.created != null) parts.push(importProg.created + ' created');
                                    if (importProg.updated) parts.push(importProg.updated + ' updated');
                                    if (importProg.removed) parts.push(importProg.removed + ' removed');
                                    if (importProg.groups)  parts.push(importProg.groups + ' groups');
                                    return parts.join(' · ');
                                })())),
                            importProg.current ? h('div', { style:{ fontSize:12, color:mutedColor, marginTop:4 } }, 'Current: ' + importProg.current) : null,
                            (importProg.errors && importProg.errors.length) ? h('div', { style:{ marginTop:8, fontSize:12, color:TC.error } },
                                importProg.errors.length + ' issue(s): ',
                                h('span', { style:{ color:mutedColor } }, importProg.errors.slice(0,3).join('; ') + (importProg.errors.length > 3 ? '…' : ''))) : null
                        );
                    })() : null
                )
            )
        );
    }

    /* ════════════════════════════════════════════════════════════════════
     * WISHLIST+ PAGE — Smart Wishlists admin (S221)
     *
     * Settings surface for the Wishlist+ paid extension. Talks to the
     * extension's own REST namespace (titancart-wishlist/v1), NOT core.
     * Gated into the Growth menu by Config.wishlistPlusInstalled; the
     * master enable toggle is license-gated (Config.wishlistPlusLicenseActive)
     * to match the SettingsController, which 403s an enable-ON without a key.
     * ════════════════════════════════════════════════════════════════════ */
    /* ════════════════════════════════════════════════════════════════════
     * TipDonationPage — Tips & Donations paid extension.
     * Settings + a tips report. Talks to the extension's own REST namespace
     * (titancart-tip/v1), NOT core. Gated into the Growth menu by
     * Config.tipDonationInstalled; the master enable toggle is license-gated
     * (Config.tipDonationLicenseActive) to match the SettingsController, which
     * 403s an enable-ON without a key. The report reads core's tc_orders.tip_total.
     * ════════════════════════════════════════════════════════════════════ */
    function TipDonationPage({ subId, dark, cardBg, textColor, mutedColor, borderColor, tableHeaderBg }) {
        var fs = fieldSty(dark, borderColor);
        var TIP_API = Config.tipDonationApiUrl || (Config.apiUrl || '').replace('/titancart/v1', '/titancart-tip/v1');
        var licenseActive = !!Config.tipDonationLicenseActive;
        var csym = Config.currency_symbol || '$';

        function tipApiFetch(path, opts) {
            opts = opts || {};
            opts.headers = Object.assign({ 'Content-Type':'application/json', 'X-WP-Nonce': Config.nonce }, opts.headers || {});
            if (opts.body && typeof opts.body === 'object') opts.body = JSON.stringify(opts.body);
            return fetch(TIP_API + path, opts).then(function(r) {
                return r.text().then(function(text) {
                    var data = null;
                    if (text) { try { data = JSON.parse(text); } catch(e) { data = null; } }
                    if (!r.ok || (data && data.success === false)) {
                        var m = (data && data.error && data.error.message) || (data && data.message) || ('HTTP ' + r.status);
                        var err = new Error(m); err.status = r.status; err.data = data;
                        err.code = data && data.error && data.error.code;
                        return Promise.reject(err);
                    }
                    return data;
                });
            });
        }

        function asBool(v) { return v === '1' || v === 1 || v === true; }
        function fmtMoney(v, sym) { return (sym || csym) + parseFloat(v || 0).toLocaleString('en-US', { minimumFractionDigits:2, maximumFractionDigits:2 }); }

        function normalize(s) {
            s = s || {};
            return {
                enabled:             asBool(s.enabled),
                label:               s.label != null ? String(s.label) : 'Tip',
                heading:             s.heading != null ? String(s.heading) : '',
                blurb:               s.blurb != null ? String(s.blurb) : '',
                round_up:            asBool(s.round_up),
                round_up_default_on: asBool(s.round_up_default_on),
                presets_enabled:     asBool(s.presets_enabled),
                presets:             s.presets != null ? String(s.presets) : '',
                allow_custom:        asBool(s.allow_custom),
                max_amount:          s.max_amount != null ? String(s.max_amount) : '0',
                charity_cause:       s.charity_cause != null ? String(s.charity_cause) : '',
                thank_you:           s.thank_you != null ? String(s.thank_you) : '',
                goal_enabled:        asBool(s.goal_enabled),
                goal_title:          s.goal_title != null ? String(s.goal_title) : '',
                goal_target:         s.goal_target != null ? String(s.goal_target) : '0',
                goal_start:          s.goal_start != null ? String(s.goal_start) : '',
                goal_show_widget:    s.goal_show_widget != null ? asBool(s.goal_show_widget) : true,
            };
        }

        var _f = useState(null),    form = _f[0],    setForm = _f[1];
        var _l = useState(true),    loading = _l[0], setLoading = _l[1];
        var _s = useState(false),   saving = _s[0],  setSaving = _s[1];
        var _m = useState(null),    msg = _m[0],     setMsg = _m[1];
        var _o = useState({ general:true, widget:true, amounts:true, goal:true, report:true });
        var open = _o[0], setOpen = _o[1];
        var _r = useState(null),    report = _r[0], setReport = _r[1];
        var _rl = useState(true),   reportLoading = _rl[0], setReportLoading = _rl[1];

        function toggleSection(id) { setOpen(function(p){ var n = Object.assign({}, p); n[id] = !n[id]; return n; }); }
        var edCtx = { mutedColor:mutedColor, borderColor:borderColor, textColor:textColor, open:open, onToggle:toggleSection };

        function load() {
            setLoading(true);
            tipApiFetch('/settings').then(function(d) {
                setForm(normalize((d && d.data) || d));
                setLoading(false);
            }).catch(function(e) {
                setMsg({ type:'error', text:e.message || 'Failed to load settings.' });
                setLoading(false);
            });
        }
        useEffect(function(){ load(); }, []);

        function loadReport() {
            setReportLoading(true);
            tipApiFetch('/report').then(function(d) {
                setReport((d && d.data) || d);
                setReportLoading(false);
            }).catch(function() {
                setReport(null);
                setReportLoading(false);
            });
        }
        useEffect(function(){ loadReport(); }, []);

        function set(k, v) { setForm(function(f){ var n = Object.assign({}, f); n[k] = v; return n; }); }

        function save() {
            if (!form) return;
            setSaving(true); setMsg(null);
            var payload = {
                enabled:             form.enabled ? '1' : '0',
                label:               form.label || '',
                heading:             form.heading || '',
                blurb:               form.blurb || '',
                round_up:            form.round_up ? '1' : '0',
                round_up_default_on: form.round_up_default_on ? '1' : '0',
                presets_enabled:     form.presets_enabled ? '1' : '0',
                presets:             form.presets || '',
                allow_custom:        form.allow_custom ? '1' : '0',
                max_amount:          String(form.max_amount != null ? form.max_amount : '0'),
                charity_cause:       form.charity_cause || '',
                thank_you:           form.thank_you || '',
                goal_enabled:        form.goal_enabled ? '1' : '0',
                goal_title:          form.goal_title || '',
                goal_target:         String(form.goal_target != null ? form.goal_target : '0'),
                goal_start:          form.goal_start || '',
                goal_show_widget:    form.goal_show_widget ? '1' : '0',
            };
            tipApiFetch('/settings', { method:'PUT', body:payload }).then(function(d) {
                setForm(normalize((d && d.data) || d));
                setMsg({ type:'success', text:'Settings saved.' });
                setSaving(false);
                loadReport();
            }).catch(function(e) {
                var t = e.message || 'Save failed.';
                if (e.code === 'tctip_license_required') t = 'An active license is required to enable Tips & Donations.';
                setMsg({ type:'error', text:t });
                setSaving(false);
            });
        }

        /* ── Render helpers (element factories, not components) ── */
        function toggleRow(key, label, note, disabled, lockNote) {
            return h(FR, { label:label, note:lockNote || note, ctx:edCtx, key:key },
                h('label', { style:{ display:'inline-flex', alignItems:'center', gap:S.sm, fontSize:14, cursor:disabled?'not-allowed':'pointer', paddingTop:6, opacity:disabled?0.55:1 } },
                    h('input', { type:'checkbox', checked:!!(form && form[key]), disabled:disabled,
                        onChange:function(e){ set(key, e.target.checked); },
                        style:{ accentColor:TC.accent, cursor:disabled?'not-allowed':'pointer', width:16, height:16 } }),
                    h('span', null, (form && form[key]) ? 'Enabled' : 'Disabled')
                )
            );
        }
        function textRow(key, label, note, placeholder) {
            return h(FR, { label:label, note:note, ctx:edCtx, key:key },
                h('input', { type:'text', value:(form && form[key] != null) ? form[key] : '', placeholder:placeholder || '',
                    onChange:function(e){ set(key, e.target.value); },
                    style:Object.assign({}, fs, { width:'100%', maxWidth:360 }) })
            );
        }
        function moneyRow(key, label, note) {
            return h(FR, { label:label, note:note, ctx:edCtx, key:key },
                h('div', { style:{ display:'flex', alignItems:'center', gap:S.sm } },
                    h('span', { style:{ fontSize:13, color:mutedColor } }, csym),
                    h('input', { type:'number', min:0, step:'0.01', value:(form && form[key] != null) ? form[key] : '0',
                        onChange:function(e){ set(key, e.target.value); },
                        style:Object.assign({}, fs, { width:140 }) })
                )
            );
        }
        function dateRow(key, label, note) {
            return h(FR, { label:label, note:note, ctx:edCtx, key:key },
                h('input', { type:'date', value:(form && form[key] != null) ? form[key] : '',
                    onChange:function(e){ set(key, e.target.value); },
                    style:Object.assign({}, fs, { width:180 }) })
            );
        }
        function statTile(label, value) {
            return h('div', { style:{ flex:'1 1 140px', minWidth:140, padding:'14px 16px', border:'1px solid '+borderColor, borderRadius:10, background:cardBg } },
                h('div', { style:{ fontSize:12, color:mutedColor, marginBottom:4 } }, label),
                h('div', { style:{ fontSize:20, fontWeight:700, color:textColor } }, value)
            );
        }

        /* ── Render ── */
        if (loading) return h('main', { style:{ flex:1, overflow:'auto', padding:S.xl } }, h(Spinner, { mutedColor:mutedColor }));

        var reportBody;
        if (reportLoading) {
            reportBody = h(Spinner, { mutedColor:mutedColor });
        } else if (!report) {
            reportBody = h('p', { style:{ fontSize:13, color:mutedColor, margin:'4px 0 0' } }, 'No report data available yet.');
        } else {
            var tipLabel = report.label || (form && form.label) || 'Tip';
            var recent = Array.isArray(report.recent) ? report.recent : [];
            var donors = Array.isArray(report.donors) ? report.donors : [];
            var goal = (report.goal && report.goal.enabled) ? report.goal : null;

            var goalPct = goal ? Math.max(0, Math.min(100, parseInt(goal.pct, 10) || 0)) : 0;
            var goalPanel = goal ? h('div', { style:{ marginBottom:S.lg, padding:'16px 18px', border:'1px solid '+borderColor, borderRadius:12, background:cardBg } },
                (goal.title ? h('div', { style:{ fontSize:14, fontWeight:700, color:textColor, marginBottom:10 } }, goal.title) : null),
                h('div', { style:{ position:'relative', height:14, borderRadius:999, background:dark?'rgba(255,255,255,.10)':'rgba(0,0,0,.08)', overflow:'hidden' } },
                    h('div', { style:{ position:'absolute', left:0, top:0, bottom:0, borderRadius:999, background:TC.accent, width:goalPct+'%', transition:'width .4s ease' } })
                ),
                h('div', { style:{ display:'flex', justifyContent:'space-between', gap:12, marginTop:8, fontSize:13, color:mutedColor } },
                    h('span', null, fmtMoney(goal.raised, goal.currency) + ' of ' + fmtMoney(goal.target, goal.currency) + ' raised'),
                    h('span', { style:{ fontWeight:700, color:textColor } }, goalPct + '%')
                ),
                h('div', { style:{ fontSize:12, color:mutedColor, marginTop:4 } },
                    ((parseFloat(goal.remaining) || 0) > 0 ? fmtMoney(goal.remaining, goal.currency) + ' to go' : 'Goal reached — thank you!') +
                    (goal.start ? ('  ·  counting since ' + goal.start) : ''))
            ) : null;

            var donorsTable = donors.length === 0 ? null : h('div', { style:{ marginTop:S.lg } },
                h('div', { style:{ fontSize:13, fontWeight:600, color:textColor, margin:'0 0 8px' } }, 'Top donors'),
                h('div', { style:{ overflowX:'auto' } },
                    h('table', { style:{ width:'100%', borderCollapse:'collapse', fontSize:13 } },
                        h('thead', null,
                            h('tr', { style:{ textAlign:'left', background:tableHeaderBg } },
                                h('th', { style:{ padding:'8px 10px', color:mutedColor, fontWeight:600 } }, 'Donor'),
                                h('th', { style:{ padding:'8px 10px', color:mutedColor, fontWeight:600, textAlign:'right' } }, 'Orders'),
                                h('th', { style:{ padding:'8px 10px', color:mutedColor, fontWeight:600, textAlign:'right' } }, 'Total ' + tipLabel.toLowerCase()),
                                h('th', { style:{ padding:'8px 10px', color:mutedColor, fontWeight:600 } }, 'Last')
                            )
                        ),
                        h('tbody', null,
                            donors.map(function(d, idx) {
                                return h('tr', { key:idx, style:{ borderTop:'1px solid '+borderColor } },
                                    h('td', { style:{ padding:'8px 10px', color:textColor, fontWeight:600 } }, d.donor || '—'),
                                    h('td', { style:{ padding:'8px 10px', color:textColor, textAlign:'right' } }, String(d.orders != null ? d.orders : 0)),
                                    h('td', { style:{ padding:'8px 10px', color:TC.success, fontWeight:600, textAlign:'right' } }, fmtMoney(d.total_tip)),
                                    h('td', { style:{ padding:'8px 10px', color:mutedColor } }, (d.last_at || '').replace('T', ' ').substring(0, 10))
                                );
                            })
                        )
                    )
                )
            );

            reportBody = h('div', null,
                goalPanel,
                h('div', { style:{ display:'flex', flexWrap:'wrap', gap:S.md, marginBottom:S.lg } },
                    statTile('Total ' + tipLabel + 's', fmtMoney(report.total_tips)),
                    statTile('This month', fmtMoney(report.month_tips)),
                    statTile('Tipped orders', String(report.tipped_orders != null ? report.tipped_orders : 0)),
                    statTile('Average ' + tipLabel.toLowerCase(), fmtMoney(report.avg_tip))
                ),
                h('div', { style:{ fontSize:13, fontWeight:600, color:textColor, margin:'0 0 8px' } }, 'Recent ' + tipLabel.toLowerCase() + 's'),
                recent.length === 0
                    ? h('p', { style:{ fontSize:13, color:mutedColor, margin:0 } }, 'No orders with a ' + tipLabel.toLowerCase() + ' yet.')
                    : h('div', { style:{ overflowX:'auto' } },
                        h('table', { style:{ width:'100%', borderCollapse:'collapse', fontSize:13 } },
                            h('thead', null,
                                h('tr', { style:{ textAlign:'left', background:tableHeaderBg } },
                                    h('th', { style:{ padding:'8px 10px', color:mutedColor, fontWeight:600 } }, 'Order'),
                                    h('th', { style:{ padding:'8px 10px', color:mutedColor, fontWeight:600 } }, 'Customer'),
                                    h('th', { style:{ padding:'8px 10px', color:mutedColor, fontWeight:600, textAlign:'right' } }, tipLabel),
                                    h('th', { style:{ padding:'8px 10px', color:mutedColor, fontWeight:600, textAlign:'right' } }, 'Order total'),
                                    h('th', { style:{ padding:'8px 10px', color:mutedColor, fontWeight:600 } }, 'Date')
                                )
                            ),
                            h('tbody', null,
                                recent.map(function(row, idx) {
                                    var sym = tcMoneySymbol(row.currency);
                                    return h('tr', { key:idx, style:{ borderTop:'1px solid '+borderColor } },
                                        h('td', { style:{ padding:'8px 10px', color:textColor, fontWeight:600 } }, row.order_number || '—'),
                                        h('td', { style:{ padding:'8px 10px', color:textColor } }, row.customer || '—'),
                                        h('td', { style:{ padding:'8px 10px', color:TC.success, fontWeight:600, textAlign:'right' } }, fmtMoney(row.tip, sym)),
                                        h('td', { style:{ padding:'8px 10px', color:textColor, textAlign:'right' } }, fmtMoney(row.total, sym)),
                                        h('td', { style:{ padding:'8px 10px', color:mutedColor } }, (row.created_at || '').replace('T', ' ').substring(0, 16))
                                    );
                                })
                            )
                        )
                    ),
                donorsTable
            );
        }

        return h('main', { style:{ flex:1, overflow:'auto', padding:S.xl } },

            h('div', { style:{ marginBottom:S.xl } },
                h('h1', { style:{ fontSize:22, fontWeight:700, margin:0, color:textColor } }, 'Tips & Donations'),
                h('p', { style:{ fontSize:13, color:mutedColor, margin:'6px 0 0' } },
                    'Let customers add a tip or a charitable donation at checkout — round-up, preset amounts, or a custom figure. Persisted on the order and reported below.')
            ),

            !licenseActive && h('div', { style:{ display:'flex', gap:S.sm, alignItems:'flex-start', padding:'12px 16px', borderRadius:8,
                    background:dark?'rgba(217,119,6,.12)':TC.amberBg, border:'1px solid '+TC.amber+'40', marginBottom:S.lg } },
                h('span', { style:{ color:TC.amber, fontWeight:600, fontSize:13 } }, '⚠'),
                h('div', { style:{ fontSize:13, color:textColor } },
                    h('strong', null, 'License required. '),
                    'Tips & Donations is installed but not licensed. You can adjust preferences below, but the master switch can only be enabled once a valid license is active.')
            ),

            msg && h('div', { style:{ padding:'10px 16px', borderRadius:8, marginBottom:S.lg, fontSize:13, fontWeight:500,
                    background: msg.type === 'success' ? (dark?'rgba(22,163,74,.12)':TC.successBg) : (dark?'rgba(220,38,38,.12)':TC.errorBg),
                    color: msg.type === 'success' ? TC.success : TC.error,
                    border:'1px solid ' + (msg.type === 'success' ? TC.success : TC.error) + '30' } },
                msg.text),

            h('div', { style:{ background:cardBg, border:'1px solid '+borderColor, borderRadius:12, padding:'8px '+S.xl+'px '+S.xl+'px' } },

                h(SectionHead, { id:'general', title:'General', ctx:edCtx }),
                open.general && h('div', null,
                    toggleRow('enabled', 'Enable Tips & Donations',
                        'Master switch. When off, the checkout widget is hidden and no tip is collected (existing orders are untouched).',
                        !licenseActive,
                        !licenseActive ? 'Requires an active license to enable.' : null),
                    textRow('label', 'Line label',
                        'How the line is labelled on the checkout summary, invoice, and cart (e.g. "Tip" or "Donation").', 'Tip'),
                    textRow('heading', 'Widget heading',
                        'The heading shown above the tip options at checkout.', 'Add a tip'),
                    textRow('blurb', 'Widget blurb',
                        'Optional supporting line under the heading (e.g. "100% goes to our staff").', '')
                ),

                h(SectionHead, { id:'widget', title:'Widget Options', ctx:edCtx }),
                open.widget && h('div', null,
                    toggleRow('round_up', 'Offer round-up',
                        'Show a "round up to the next dollar" option. This is the default model — small, friction-free amounts.'),
                    toggleRow('round_up_default_on', 'Pre-select round-up',
                        'Start the widget with round-up already selected (opt-out). Off by default so a tip is always the shopper\'s explicit choice.'),
                    toggleRow('allow_custom', 'Allow a custom amount',
                        'Let customers type their own amount, clamped to the maximum below.')
                ),

                h(SectionHead, { id:'amounts', title:'Amounts & Cause', ctx:edCtx }),
                open.amounts && h('div', null,
                    toggleRow('presets_enabled', 'Offer preset amounts',
                        'Show fixed one-tap amounts (e.g. $2 / $5 / $10).'),
                    textRow('presets', 'Preset amounts',
                        'Comma-separated amounts, up to six. Invalid or duplicate values are ignored.', '2,5,10'),
                    moneyRow('max_amount', 'Maximum amount',
                        'Ceiling for a custom amount. Leave 0 to use the safe default (the greater of ' + csym + '1000 or 5× the order).'),
                    textRow('charity_cause', 'Charity / cause',
                        'Optional cause name shown in the widget (e.g. a charity you donate to). Leave blank for a plain tip.', ''),
                    textRow('thank_you', 'Thank-you note',
                        'Optional short note. (Reserved for the storefront confirmation.)', '')
                ),

                h(SectionHead, { id:'goal', title:'Campaign Goal', ctx:edCtx }),
                open.goal && h('div', null,
                    toggleRow('goal_enabled', 'Enable a donation goal',
                        'Show a fundraising target and a live progress bar. Progress is the sum of tips/donations counted from the start date below.'),
                    textRow('goal_title', 'Campaign title',
                        'Shown above the progress bar (e.g. "Help us raise ' + csym + '5,000 for the shelter").', ''),
                    moneyRow('goal_target', 'Goal target',
                        'The fundraising target. Progress is measured against this amount.'),
                    dateRow('goal_start', 'Campaign start date',
                        'Only tips on or after this date count toward the goal. Leave blank to count all tips. To reset the campaign, set a new start date and save.'),
                    toggleRow('goal_show_widget', 'Show progress at checkout',
                        'Also render the progress bar inside the checkout tip widget. The [titancart_donation_progress] shortcode works on any page regardless of this setting.')
                ),

                h(SectionHead, { id:'report', title:'Tips Report', ctx:edCtx }),
                open.report && h('div', { style:{ paddingTop:6 } }, reportBody)
            ),

            h('div', { style:{ marginTop:S.lg, display:'flex', justifyContent:'flex-end' } },
                h('button', { onClick:save, disabled:saving,
                    style:{ padding:'10px 20px', borderRadius:8, border:'none', background:TC.accent, color:'#fff',
                        fontSize:14, fontWeight:600, cursor:saving?'not-allowed':'pointer', opacity:saving?0.6:1 } },
                    saving ? 'Saving…' : 'Save Changes')
            )
        );
    }

    function WishlistPlusPage({ subId, dark, cardBg, textColor, mutedColor, borderColor, tableHeaderBg }) {
        var fs = fieldSty(dark, borderColor);
        var WL_API = Config.wishlistPlusApiUrl || (Config.apiUrl || '').replace('/titancart/v1', '/titancart-wishlist/v1');
        var licenseActive = !!Config.wishlistPlusLicenseActive;

        function wlApiFetch(path, opts) {
            opts = opts || {};
            opts.headers = Object.assign({ 'Content-Type':'application/json', 'X-WP-Nonce': Config.nonce }, opts.headers || {});
            if (opts.body && typeof opts.body === 'object') opts.body = JSON.stringify(opts.body);
            return fetch(WL_API + path, opts).then(function(r) {
                return r.text().then(function(text) {
                    var data = null;
                    if (text) { try { data = JSON.parse(text); } catch(e) { data = null; } }
                    if (!r.ok || (data && data.success === false)) {
                        var m = (data && data.error && data.error.message) || (data && data.message) || ('HTTP ' + r.status);
                        var err = new Error(m); err.status = r.status; err.data = data;
                        err.code = data && data.error && data.error.code;
                        return Promise.reject(err);
                    }
                    return data;
                });
            });
        }

        function asBool(v) { return v === '1' || v === 1 || v === true; }

        function normalize(s) {
            s = s || {};
            return {
                enabled:              asBool(s.enabled),
                allow_multiple_lists: asBool(s.allow_multiple_lists),
                allow_guest_lists:    asBool(s.allow_guest_lists),
                allow_registry:       asBool(s.allow_registry),
                alert_price_drop:     asBool(s.alert_price_drop),
                alert_back_in_stock:  asBool(s.alert_back_in_stock),
                alert_low_stock:      asBool(s.alert_low_stock),
                alert_deal:           asBool(s.alert_deal),
                low_stock_threshold:  parseInt(s.low_stock_threshold, 10) || 0,
                notify_throttle_hrs:  parseInt(s.notify_throttle_hrs, 10) || 0,
                social_networks:      Array.isArray(s.social_networks) ? s.social_networks.slice() : [],
            };
        }

        var _f = useState(null),    form = _f[0],    setForm = _f[1];
        var _l = useState(true),    loading = _l[0], setLoading = _l[1];
        var _s = useState(false),   saving = _s[0],  setSaving = _s[1];
        var _m = useState(null),    msg = _m[0],     setMsg = _m[1];   // { type:'success'|'error', text }
        var _o = useState({ general:true, lists:true, alerts:false, share:false });
        var open = _o[0], setOpen = _o[1];

        function toggleSection(id) { setOpen(function(p){ var n = Object.assign({}, p); n[id] = !n[id]; return n; }); }
        var edCtx = { mutedColor:mutedColor, borderColor:borderColor, textColor:textColor, open:open, onToggle:toggleSection };

        function load() {
            setLoading(true);
            wlApiFetch('/settings').then(function(d) {
                setForm(normalize((d && d.data) || d));
                setLoading(false);
            }).catch(function(e) {
                setMsg({ type:'error', text:e.message || 'Failed to load settings.' });
                setLoading(false);
            });
        }
        useEffect(function(){ load(); }, []);

        function set(k, v) { setForm(function(f){ var n = Object.assign({}, f); n[k] = v; return n; }); }

        function toggleSocial(slug) {
            setForm(function(f) {
                var cur = Array.isArray(f.social_networks) ? f.social_networks.slice() : [];
                var i = cur.indexOf(slug);
                if (i === -1) cur.push(slug); else cur.splice(i, 1);
                var n = Object.assign({}, f); n.social_networks = cur; return n;
            });
        }

        function save() {
            if (!form) return;
            setSaving(true); setMsg(null);
            var payload = {
                enabled:              form.enabled ? '1' : '0',
                allow_multiple_lists: form.allow_multiple_lists ? '1' : '0',
                allow_guest_lists:    form.allow_guest_lists ? '1' : '0',
                allow_registry:       form.allow_registry ? '1' : '0',
                alert_price_drop:     form.alert_price_drop ? '1' : '0',
                alert_back_in_stock:  form.alert_back_in_stock ? '1' : '0',
                alert_low_stock:      form.alert_low_stock ? '1' : '0',
                alert_deal:           form.alert_deal ? '1' : '0',
                low_stock_threshold:  parseInt(form.low_stock_threshold, 10) || 0,
                notify_throttle_hrs:  parseInt(form.notify_throttle_hrs, 10) || 0,
                social_networks:      form.social_networks || [],
            };
            wlApiFetch('/settings', { method:'PUT', body:payload }).then(function(d) {
                setForm(normalize((d && d.data) || d));
                setMsg({ type:'success', text:'Settings saved.' });
                setSaving(false);
            }).catch(function(e) {
                var t = e.message || 'Save failed.';
                if (e.code === 'tcwp_license_required') t = 'An active license is required to enable Wishlist+.';
                setMsg({ type:'error', text:t });
                setSaving(false);
            });
        }

        /* ── Toggle row: an FR-wrapped checkbox + inline caption ── */
        function toggleRow(key, label, note, disabled, lockNote) {
            return h(FR, { label:label, note:lockNote || note, ctx:edCtx, key:key },
                h('label', { style:{ display:'inline-flex', alignItems:'center', gap:S.sm, fontSize:14, cursor:disabled?'not-allowed':'pointer', paddingTop:6, opacity:disabled?0.55:1 } },
                    h('input', { type:'checkbox', checked:!!(form && form[key]), disabled:disabled,
                        onChange:function(e){ set(key, e.target.checked); },
                        style:{ accentColor:TC.accent, cursor:disabled?'not-allowed':'pointer', width:16, height:16 } }),
                    h('span', null, (form && form[key]) ? 'Enabled' : 'Disabled')
                )
            );
        }

        /* ── Number row: small numeric input ── */
        function numberRow(key, label, note, suffix) {
            return h(FR, { label:label, note:note, ctx:edCtx, key:key },
                h('div', { style:{ display:'flex', alignItems:'center', gap:S.sm } },
                    h('input', { type:'number', min:0, value:(form && form[key]) != null ? form[key] : 0,
                        onChange:function(e){ set(key, parseInt(e.target.value, 10) || 0); },
                        style:Object.assign({}, fs, { width:120 }) }),
                    suffix && h('span', { style:{ fontSize:13, color:mutedColor } }, suffix)
                )
            );
        }

        var SOCIALS = [
            ['facebook',  'Facebook'],
            ['twitter',   'X / Twitter'],
            ['pinterest', 'Pinterest'],
            ['whatsapp',  'WhatsApp'],
            ['email',     'Email'],
            ['link',      'Copy link'],
        ];

        /* ── Render ── */
        if (loading) return h('main', { style:{ flex:1, overflow:'auto', padding:S.xl } }, h(Spinner, { mutedColor:mutedColor }));

        return h('main', { style:{ flex:1, overflow:'auto', padding:S.xl } },

            /* Header */
            h('div', { style:{ marginBottom:S.xl } },
                h('h1', { style:{ fontSize:22, fontWeight:700, margin:0, color:textColor } }, 'Wishlists'),
                h('p', { style:{ fontSize:13, color:mutedColor, margin:'6px 0 0' } },
                    'Smart wishlists, gift registries, and price & stock alerts powered by the Wishlist+ extension.')
            ),

            /* License banner when installed-but-unlicensed */
            !licenseActive && h('div', { style:{ display:'flex', gap:S.sm, alignItems:'flex-start', padding:'12px 16px', borderRadius:8,
                    background:dark?'rgba(217,119,6,.12)':TC.amberBg, border:'1px solid '+TC.amber+'40', marginBottom:S.lg } },
                h('span', { style:{ color:TC.amber, fontWeight:600, fontSize:13 } }, '⚠'),
                h('div', { style:{ fontSize:13, color:textColor } },
                    h('strong', null, 'License required. '),
                    'Wishlist+ is installed but not licensed. You can adjust preferences below, but the master switch can only be enabled once a valid license is active.')
            ),

            /* Status message */
            msg && h('div', { style:{ padding:'10px 16px', borderRadius:8, marginBottom:S.lg, fontSize:13, fontWeight:500,
                    background: msg.type === 'success' ? (dark?'rgba(22,163,74,.12)':TC.successBg) : (dark?'rgba(220,38,38,.12)':TC.errorBg),
                    color: msg.type === 'success' ? TC.success : TC.error,
                    border:'1px solid ' + (msg.type === 'success' ? TC.success : TC.error) + '30' } },
                msg.text),

            /* Card */
            h('div', { style:{ background:cardBg, border:'1px solid '+borderColor, borderRadius:12, padding:'8px '+S.xl+'px '+S.xl+'px' } },

                /* SECTION: General */
                h(SectionHead, { id:'general', title:'General', ctx:edCtx }),
                open.general && h('div', null,
                    toggleRow('enabled', 'Enable Wishlist+',
                        'Master switch. When off, all Wishlist+ storefront surfaces and alert scans pause (saved wishlists are never deleted).',
                        !licenseActive,
                        !licenseActive ? 'Requires an active license to enable.' : null)
                ),

                /* SECTION: Smart Wishlists */
                h(SectionHead, { id:'lists', title:'Smart Wishlists', ctx:edCtx }),
                open.lists && h('div', null,
                    toggleRow('allow_multiple_lists', 'Multiple lists',
                        'Let customers create more than one named wishlist (e.g. "Birthday", "Home office"). When off, each customer keeps a single default list.'),
                    toggleRow('allow_guest_lists', 'Guest wishlists',
                        'Allow visitors who are not logged in to build a wishlist. Guest items are held against a browser cookie and merged into the account on login.'),
                    toggleRow('allow_registry', 'Gift registries',
                        'Allow event registries (wedding, baby shower, and the like) with a public, shareable list and purchased-item tracking.')
                ),

                /* SECTION: Price & Stock Alerts */
                h(SectionHead, { id:'alerts', title:'Price & Stock Alerts', ctx:edCtx }),
                open.alerts && h('div', null,
                    toggleRow('alert_price_drop', 'Price-drop alerts',
                        'Email a customer when an item on their wishlist drops below the price it was at when they saved it.'),
                    toggleRow('alert_back_in_stock', 'Back-in-stock alerts',
                        'Email when a previously out-of-stock wishlisted item becomes available again.'),
                    toggleRow('alert_low_stock', 'Low-stock alerts',
                        'Email when a wishlisted item is running low, nudging the customer to buy before it sells out.'),
                    toggleRow('alert_deal', 'Deal alerts',
                        'Email when a wishlisted item is added to an active promotion.'),
                    numberRow('low_stock_threshold', 'Low-stock threshold',
                        'Trigger low-stock alerts when available quantity is at or below this number.', 'units or fewer'),
                    numberRow('notify_throttle_hrs', 'Alert throttle',
                        'Minimum hours between repeat alerts for the same item, so customers are not spammed.', 'hours')
                ),

                /* SECTION: Share Anywhere */
                h(SectionHead, { id:'share', title:'Share Anywhere', ctx:edCtx }),
                open.share && h('div', null,
                    h(FR, { label:'Share buttons', note:'Choose which share options appear on public wishlists and registries.', ctx:edCtx },
                        h('div', { style:{ display:'flex', flexWrap:'wrap', gap:S.md, paddingTop:6 } },
                            ...SOCIALS.map(function(pair) {
                                var slug = pair[0], lbl = pair[1];
                                var on = !!(form && Array.isArray(form.social_networks) && form.social_networks.indexOf(slug) !== -1);
                                return h('label', { key:slug, style:{ display:'inline-flex', alignItems:'center', gap:6, fontSize:13, cursor:'pointer',
                                        padding:'6px 12px', borderRadius:8, border:'1px solid '+borderColor,
                                        background:on ? (dark?'rgba(37,99,235,.12)':TC.accentBg) : 'transparent',
                                        color:on ? TC.accent : mutedColor } },
                                    h('input', { type:'checkbox', checked:on, onChange:function(){ toggleSocial(slug); },
                                        style:{ accentColor:TC.accent, cursor:'pointer' } }),
                                    lbl
                                );
                            })
                        )
                    )
                ),

                /* Save bar */
                h('div', { style:{ display:'flex', justifyContent:'flex-end', gap:S.sm, marginTop:S.xl, paddingTop:S.lg, borderTop:'1px solid '+borderColor } },
                    h(Btn, { variant:'primary', onClick:save, disabled:saving || !form, mutedColor:mutedColor, borderColor:borderColor },
                        saving ? 'Saving…' : 'Save settings')
                )
            )
        );
    }

    /* ════════════════════════════════════════════════════════════════════
     * DATA FEEDS PAGE — Product Data Feeds admin (S225)
     *
     * Gated into the Growth menu by Config.productFeedsInstalled; the master
     * enable toggle is gated by Config.productFeedsLicenseActive (amber banner
     * when unlicensed, mirroring Wishlist+).
     *
     * Three views, dispatched off the hash subId (parse() → { route, subId }):
     *   #/datafeeds              → FeedsList   (table + Add Feed)
     *   #/datafeeds/new          → FeedEditor  (create)
     *   #/datafeeds/{id}         → FeedEditor  (edit + run history + regenerate)
     *   #/datafeeds/df-settings  → FeedSettings (master toggle + global defaults)
     *
     * Each view is its own inner component that owns its own state, so the
     * parent only re-renders on a subId change — the SubscriptionsPage model,
     * which keeps typing focus stable (no inner-component remount on keystroke).
     *
     * REST: titancart-feeds/v1, envelope { success, data, meta?, error? }.
     * GET responses key on DB column names; DECIMAL is not used here (all
     * numeric feed fields are ints) so no padded-string coercion is needed.
     * ════════════════════════════════════════════════════════════════════ */
    function DataFeedsPage({ subId, dark, cardBg, textColor, mutedColor, borderColor, tableHeaderBg }) {
        var fs   = fieldSty(dark, borderColor);
        var selS = Object.assign({}, fs, { cursor:'pointer', appearance:'auto' });
        var FEEDS_API = Config.productFeedsApiUrl || (Config.apiUrl || '').replace('/titancart/v1', '/titancart-feeds/v1');
        var CORE_API  = Config.apiUrl || '/wp-json/titancart/v1';
        var licenseActive = !!Config.productFeedsLicenseActive;

        /* Envelope-aware fetch against the feeds REST namespace. */
        function pfApiFetch(path, opts) {
            opts = opts || {};
            opts.headers = Object.assign({ 'Content-Type':'application/json', 'X-WP-Nonce': Config.nonce }, opts.headers || {});
            if (opts.body && typeof opts.body === 'object') opts.body = JSON.stringify(opts.body);
            return fetch(FEEDS_API + path, opts).then(function(r) {
                return r.text().then(function(text) {
                    var data = null;
                    if (text) { try { data = JSON.parse(text); } catch(e) { data = null; } }
                    if (!r.ok || (data && data.success === false)) {
                        var m = (data && data.error && data.error.message) || (data && data.message) || ('HTTP ' + r.status);
                        var err = new Error(m); err.status = r.status; err.data = data;
                        err.code = data && data.error && data.error.code;
                        return Promise.reject(err);
                    }
                    return data;
                });
            });
        }

        /* Plain core-API fetch (categories for the product filter). */
        function coreFetch(path) {
            return fetch(CORE_API + path, { headers:{ 'X-WP-Nonce': Config.nonce || '' } }).then(function(r){ return r.json(); });
        }

        function asBool(v) { return v === '1' || v === 1 || v === true; }
        function unwrap(d) { return (d && d.data !== undefined) ? d.data : d; }

        /* ── Shared lookups ── */
        var CHANNELS = [
            ['google',   'Google Shopping'],
            ['facebook', 'Facebook / Meta Catalog'],
            ['bing',     'Bing Shopping'],
        ];
        var SCHEDULES = [
            ['manual', 'Manual only'],
            ['hourly', 'Hourly'],
            ['daily',  'Daily'],
            ['weekly', 'Weekly'],
        ];
        var CONDITIONS = [
            ['new',         'New'],
            ['refurbished', 'Refurbished'],
            ['used',        'Used'],
        ];
        var STATUSES = [
            ['active',   'Active'],
            ['disabled', 'Disabled'],
            ['hidden',   'Hidden'],
        ];
        function channelLabel(c) { var p = CHANNELS.find(function(x){ return x[0] === c; }); return p ? p[1] : (c || '—'); }
        function scheduleLabel(s) { var p = SCHEDULES.find(function(x){ return x[0] === s; }); return p ? p[1] : (s || 'Manual only'); }
        function formatForChannel(c) { return c === 'facebook' ? 'CSV' : 'XML'; }
        function fmtDuration(ms) { ms = parseInt(ms, 10) || 0; return ms >= 1000 ? (ms / 1000).toFixed(1) + 's' : ms + ' ms'; }

        /* Status pill shared by the list, editor, and run-history rows. */
        function statusPill(st) {
            var map = {
                never:   { c:mutedColor, bg: dark?'rgba(255,255,255,.06)':'#F5F5F5',     l:'Never run' },
                running: { c:TC.accent,  bg: dark?'rgba(37,99,235,.12)':TC.accentBg,     l:'Running' },
                success: { c:TC.success, bg: dark?'rgba(22,163,74,.12)':TC.successBg,    l:'Success' },
                error:   { c:TC.error,   bg: dark?'rgba(220,38,38,.12)':TC.errorBg,      l:'Error' },
            };
            var m = map[st] || map.never;
            return h('span', { style:{ fontSize:11, fontWeight:600, padding:'2px 8px', borderRadius:99, color:m.c, background:m.bg, display:'inline-block', whiteSpace:'nowrap' } }, m.l);
        }

        /* Copy-to-clipboard pill for the public feed URL (own state → no remount). */
        function CopyUrl({ url }) {
            var _c = useState(false), copied = _c[0], setCopied = _c[1];
            return h('div', { style:{ display:'flex', alignItems:'center', gap:S.sm, minWidth:0 } },
                h('code', { title:url, style:{ flex:1, minWidth:0, fontSize:12, color:mutedColor, background:dark?'rgba(255,255,255,.04)':'#F5F5F5',
                        padding:'6px 10px', borderRadius:6, overflow:'hidden', textOverflow:'ellipsis', whiteSpace:'nowrap', border:'1px solid '+borderColor } }, url || '—'),
                url && h('button', {
                    onClick: function(){ try { navigator.clipboard.writeText(url); } catch(e){} setCopied(true); setTimeout(function(){ setCopied(false); }, 1500); },
                    style:{ flexShrink:0, padding:'6px 12px', borderRadius:6, border:'1px solid '+borderColor, background:'transparent',
                        color: copied ? TC.success : mutedColor, fontSize:12, fontWeight:500, cursor:'pointer' }
                }, copied ? 'Copied!' : 'Copy')
            );
        }

        /* ── Which view? ── */
        var tab = 'list';
        if (subId === 'df-settings') tab = 'settings';
        else if (subId && subId !== '') tab = 'editor'; // 'new' or a numeric id

        /* ══════════════════════════════════════════════════════════════════
         * VIEW: FEEDS LIST
         * ══════════════════════════════════════════════════════════════════ */
        function FeedsList() {
            var _f = useState(null), feeds = _f[0], setFeeds = _f[1];
            var _l = useState(true), loading = _l[0], setLoading = _l[1];
            var _m = useState(null), msg = _m[0], setMsg = _m[1];
            var _b = useState(0),    busyId = _b[0], setBusyId = _b[1]; // feed id currently regenerating
            var _d = useState(null), confirmDel = _d[0], setConfirmDel = _d[1];
            var _dl = useState(false), deleting = _dl[0], setDeleting = _dl[1];

            function load() {
                setLoading(true);
                pfApiFetch('/feeds').then(function(d) {
                    setFeeds(unwrap(d) || []);
                    setLoading(false);
                }).catch(function(e) {
                    setMsg({ type:'error', text:e.message || 'Failed to load feeds.' });
                    setLoading(false);
                });
            }
            useEffect(function(){ load(); }, []);

            function regenerate(id) {
                setBusyId(id); setMsg(null);
                pfApiFetch('/feeds/' + id + '/regenerate', { method:'POST', body:{} }).then(function(d) {
                    var r = unwrap(d) || {};
                    setMsg({ type:'success', text:'Feed regenerated — ' + (r.item_count != null ? r.item_count : 0) + ' items in ' + fmtDuration(r.duration_ms) + '.' });
                    setBusyId(0);
                    load();
                }).catch(function(e) {
                    var t = e.message || 'Regeneration failed.';
                    if (e.code === 'inactive') t = 'Enable Data Feeds in Settings (license + master toggle) before regenerating.';
                    setMsg({ type:'error', text:t });
                    setBusyId(0);
                });
            }

            function doDelete() {
                if (!confirmDel) return;
                setDeleting(true);
                pfApiFetch('/feeds/' + confirmDel.id, { method:'DELETE' }).then(function() {
                    setDeleting(false); setConfirmDel(null);
                    setMsg({ type:'success', text:'Feed deleted.' });
                    load();
                }).catch(function(e) {
                    setDeleting(false); setConfirmDel(null);
                    setMsg({ type:'error', text:e.message || 'Delete failed.' });
                });
            }

            if (loading) return h(Spinner, { mutedColor:mutedColor });

            return h('div', null,
                msg && h('div', { style:{ padding:'10px 16px', borderRadius:8, marginBottom:S.lg, fontSize:13, fontWeight:500,
                        background: msg.type === 'success' ? (dark?'rgba(22,163,74,.12)':TC.successBg) : (dark?'rgba(220,38,38,.12)':TC.errorBg),
                        color: msg.type === 'success' ? TC.success : TC.error,
                        border:'1px solid ' + (msg.type === 'success' ? TC.success : TC.error) + '30' } }, msg.text),

                h('div', { style:{ display:'flex', justifyContent:'flex-end', marginBottom:S.lg } },
                    h(Btn, { variant:'primary', onClick:function(){ navigate('datafeeds/new'); }, mutedColor:mutedColor, borderColor:borderColor }, '+ New feed')
                ),

                (!feeds || feeds.length === 0)
                    ? h(Card, { cardBg:cardBg, borderColor:borderColor, dark:dark },
                        h(EmptyState, { mutedColor:mutedColor, icon:'📡', title:'No feeds yet',
                            sub:'Create a feed to publish your catalog to Google Shopping, Facebook/Meta, or Bing.' }))
                    : h(Card, { cardBg:cardBg, borderColor:borderColor, dark:dark, pad:0 },
                        /* Header row */
                        h('div', { style:{ display:'grid', gridTemplateColumns:'1.5fr 1fr 0.9fr 1fr 0.5fr 2.6fr', gap:S.md, padding:'12px 16px',
                                borderBottom:'1px solid '+borderColor, fontSize:11, fontWeight:600, textTransform:'uppercase', letterSpacing:'.3px', color:mutedColor } },
                            h('span', null, 'Feed'),
                            h('span', null, 'Channel'),
                            h('span', null, 'Schedule'),
                            h('span', null, 'Last run'),
                            h('span', { style:{ textAlign:'right' } }, 'Items'),
                            h('span', { style:{ textAlign:'right' } }, 'Public URL')
                        ),
                        feeds.map(function(f) {
                            var busy = busyId === Number(f.id);
                            return h('div', { key:f.id, style:{ display:'grid', gridTemplateColumns:'1.5fr 1fr 0.9fr 1fr 0.5fr 2.6fr', gap:S.md,
                                    padding:'14px 16px', borderBottom:'1px solid '+borderColor, alignItems:'center' } },
                                /* Feed name + format + enabled dot */
                                h('div', { style:{ minWidth:0 } },
                                    h('div', { style:{ display:'flex', alignItems:'center', gap:6 } },
                                        h('span', { title: asBool(f.enabled) ? 'Enabled' : 'Disabled',
                                            style:{ width:8, height:8, borderRadius:'50%', flexShrink:0, background: asBool(f.enabled) ? TC.success : mutedColor } }),
                                        h('button', { onClick:function(){ navigate('datafeeds/' + f.id); },
                                            style:{ background:'none', border:'none', padding:0, cursor:'pointer', color:TC.accent, fontSize:13, fontWeight:600,
                                                textAlign:'left', overflow:'hidden', textOverflow:'ellipsis', whiteSpace:'nowrap' } }, f.name || 'Untitled feed')
                                    ),
                                    h('div', { style:{ fontSize:11, color:mutedColor, marginTop:2 } }, String(f.format || formatForChannel(f.channel)).toUpperCase())
                                ),
                                h('span', { style:{ fontSize:13, color:textColor } }, channelLabel(f.channel)),
                                h('span', { style:{ fontSize:13, color:mutedColor } }, scheduleLabel(f.schedule)),
                                h('div', null,
                                    statusPill(f.last_status || 'never'),
                                    f.last_run_at && h('div', { style:{ fontSize:11, color:mutedColor, marginTop:3 } }, tcFmtDateTime(f.last_run_at))
                                ),
                                h('span', { style:{ fontSize:13, color:mutedColor, textAlign:'right' } }, f.last_item_count != null ? f.last_item_count : 0),
                                /* Public URL + actions — single row beside the URL box */
                                h('div', { style:{ display:'flex', alignItems:'center', gap:6, minWidth:0 } },
                                    h('div', { style:{ flex:1, minWidth:0 } }, h(CopyUrl, { url:f.public_url })),
                                    h('button', { title: busy ? 'Regenerating…' : 'Regenerate feed', onClick:function(){ regenerate(Number(f.id)); }, disabled:busy,
                                        style:{ flexShrink:0, width:32, height:32, display:'flex', alignItems:'center', justifyContent:'center', borderRadius:6, border:'1px solid '+borderColor, background:'transparent',
                                            color: busy ? mutedColor : TC.accent, fontSize:15, lineHeight:1, cursor: busy ? 'default' : 'pointer' } },
                                        busy ? '…' : '↻'),
                                    h('button', { title:'Edit feed', onClick:function(){ navigate('datafeeds/' + f.id); },
                                        style:{ flexShrink:0, width:32, height:32, display:'flex', alignItems:'center', justifyContent:'center', borderRadius:6, border:'1px solid '+borderColor, background:'transparent', color:mutedColor, fontSize:14, lineHeight:1, cursor:'pointer' } }, '✎'),
                                    h('button', { title:'Delete feed', onClick:function(){ setConfirmDel({ id:Number(f.id), name:f.name }); },
                                        style:{ flexShrink:0, width:32, height:32, display:'flex', alignItems:'center', justifyContent:'center', borderRadius:6, border:'1px solid rgba(220,38,38,.25)', background:'transparent', color:TC.error, fontSize:13, lineHeight:1, cursor:'pointer' } }, '🗑')
                                )
                            );
                        })
                    ),

                h(ConfirmModal, {
                    open: !!confirmDel,
                    title: 'Delete feed?',
                    detail: confirmDel ? confirmDel.name : '',
                    message: 'This permanently removes the feed definition, its run history, and its cached file. The public URL will stop working immediately. This cannot be undone.',
                    confirmLabel: 'Delete feed',
                    busy: deleting,
                    onConfirm: doDelete,
                    onCancel: function(){ if (!deleting) setConfirmDel(null); },
                    dark:dark, cardBg:cardBg, borderColor:borderColor, textColor:textColor, mutedColor:mutedColor
                })
            );
        }

        /* ══════════════════════════════════════════════════════════════════
         * VIEW: FEED EDITOR (create / edit)
         * ══════════════════════════════════════════════════════════════════ */
        function FeedEditor({ id }) {
            var isNew = !id;
            var _f = useState(null),  form = _f[0], setForm = _f[1];
            var _r = useState([]),    runs = _r[0], setRuns = _r[1];
            var _e = useState(null),  eligible = _e[0], setEligible = _e[1];
            var _u = useState(''),    publicUrl = _u[0], setPublicUrl = _u[1];
            var _l = useState(!isNew), loading = _l[0], setLoading = _l[1];
            var _s = useState(false), saving = _s[0], setSaving = _s[1];
            var _g = useState(false), regening = _g[0], setRegening = _g[1];
            var _m = useState(null),  msg = _m[0], setMsg = _m[1];
            var _c = useState([]),    cats = _c[0], setCats = _c[1];
            var _o = useState({ general:true, filters:true, mapping:false, output:true });
            var open = _o[0], setOpen = _o[1];

            function toggleSection(sid) { setOpen(function(p){ var n = Object.assign({}, p); n[sid] = !n[sid]; return n; }); }
            var edCtx = { mutedColor:mutedColor, borderColor:borderColor, textColor:textColor, open:open, onToggle:toggleSection };

            /* Normalize a feed row (or build a blank one) into editor form shape. */
            function normalize(feed) {
                feed = feed || {};
                var filters = feed.filters || {};
                var mapping = feed.mapping || {};
                return {
                    id:            feed.id != null ? Number(feed.id) : null,
                    name:          feed.name || '',
                    channel:       feed.channel || 'google',
                    schedule:      feed.schedule || 'daily',
                    enabled:       feed.enabled != null ? asBool(feed.enabled) : true,
                    language_code: feed.language_code || 'en',
                    f_status:        filters.status || 'active',
                    f_in_stock_only: !!filters.in_stock_only,
                    f_category_ids:  Array.isArray(filters.category_ids) ? filters.category_ids.map(Number) : [],
                    m_default_brand:           mapping.default_brand || '',
                    m_default_condition:       mapping.default_condition || 'new',
                    m_default_google_category: mapping.default_google_category || '',
                    m_use_sku_as_mpn:          mapping.use_sku_as_mpn != null ? asBool(mapping.use_sku_as_mpn) : true,
                };
            }

            function load() {
                setLoading(true);
                pfApiFetch('/feeds/' + id).then(function(d) {
                    var feed = unwrap(d) || {};
                    setForm(normalize(feed));
                    setRuns(Array.isArray(feed.runs) ? feed.runs : []);
                    setEligible(feed.eligible_count != null ? feed.eligible_count : null);
                    setPublicUrl(feed.public_url || '');
                    setLoading(false);
                }).catch(function(e) {
                    setMsg({ type:'error', text:e.message || 'Failed to load feed.' });
                    setLoading(false);
                });
            }

            useEffect(function(){
                if (isNew) { setForm(normalize(null)); } else { load(); }
            }, []);

            /* Categories (flat, for the product filter checklist). */
            useEffect(function(){
                coreFetch('/categories?format=flat&per_page=500').then(function(d){
                    setCats(Array.isArray(d) ? d : ((d && (d.data || d.categories)) || []));
                }).catch(function(){});
            }, []);

            function set(k, v) { setForm(function(f){ var n = Object.assign({}, f); n[k] = v; return n; }); }

            function toggleCategory(cid) {
                cid = Number(cid);
                setForm(function(f) {
                    var cur = Array.isArray(f.f_category_ids) ? f.f_category_ids.slice() : [];
                    var i = cur.indexOf(cid);
                    if (i === -1) cur.push(cid); else cur.splice(i, 1);
                    var n = Object.assign({}, f); n.f_category_ids = cur; return n;
                });
            }

            /* Map editor form → REST payload shape. */
            function payload() {
                return {
                    name:          form.name,
                    channel:       form.channel,
                    schedule:      form.schedule,
                    enabled:       form.enabled ? 1 : 0,
                    language_code: form.language_code || 'en',
                    filters: {
                        status:        form.f_status,
                        in_stock_only: !!form.f_in_stock_only,
                        category_ids:  form.f_category_ids || [],
                    },
                    mapping: {
                        default_brand:           form.m_default_brand,
                        default_condition:       form.m_default_condition,
                        default_google_category: form.m_default_google_category,
                        use_sku_as_mpn:          form.m_use_sku_as_mpn ? '1' : '0',
                    },
                };
            }

            function save() {
                if (!form) return;
                setSaving(true); setMsg(null);
                var req = isNew
                    ? pfApiFetch('/feeds', { method:'POST', body:payload() })
                    : pfApiFetch('/feeds/' + id, { method:'PUT', body:payload() });
                req.then(function(d) {
                    var feed = unwrap(d) || {};
                    setSaving(false);
                    if (isNew && feed.id != null) {
                        navigate('datafeeds/' + feed.id); // land on the saved feed's edit view
                    } else {
                        setForm(normalize(feed));
                        setRuns(Array.isArray(feed.runs) ? feed.runs : []);
                        setEligible(feed.eligible_count != null ? feed.eligible_count : null);
                        setPublicUrl(feed.public_url || '');
                        setMsg({ type:'success', text:'Feed saved.' });
                    }
                }).catch(function(e) {
                    setSaving(false);
                    setMsg({ type:'error', text:e.message || 'Save failed.' });
                });
            }

            function regenerate() {
                if (isNew) return;
                setRegening(true); setMsg(null);
                pfApiFetch('/feeds/' + id + '/regenerate', { method:'POST', body:{} }).then(function(d) {
                    var r = unwrap(d) || {};
                    setRegening(false);
                    setMsg({ type:'success', text:'Regenerated — ' + (r.item_count != null ? r.item_count : 0) + ' items in ' + fmtDuration(r.duration_ms) + '.' });
                    load();
                }).catch(function(e) {
                    setRegening(false);
                    var t = e.message || 'Regeneration failed.';
                    if (e.code === 'inactive') t = 'Enable Data Feeds in Settings (license + master toggle) before regenerating.';
                    setMsg({ type:'error', text:t });
                });
            }

            /* ── Pure render helpers (called directly, not via h() → no remount) ── */
            function textRow(key, label, note, placeholder) {
                return h(FR, { label:label, note:note, ctx:edCtx, key:key },
                    h('input', { type:'text', value:(form && form[key] != null) ? form[key] : '', placeholder:placeholder || '',
                        onChange:function(e){ set(key, e.target.value); }, style:fs })
                );
            }
            function selectRow(key, label, note, options) {
                return h(FR, { label:label, note:note, ctx:edCtx, key:key },
                    h('select', { value:(form && form[key]) || options[0][0], onChange:function(e){ set(key, e.target.value); }, style:selS },
                        options.map(function(o){ return h('option', { key:o[0], value:o[0] }, o[1]); }))
                );
            }
            function checkRow(key, label, note, captionOn, captionOff) {
                return h(FR, { label:label, note:note, ctx:edCtx, key:key },
                    h('label', { style:{ display:'inline-flex', alignItems:'center', gap:S.sm, fontSize:14, cursor:'pointer', paddingTop:6 } },
                        h('input', { type:'checkbox', checked:!!(form && form[key]), onChange:function(e){ set(key, e.target.checked); },
                            style:{ accentColor:TC.accent, cursor:'pointer', width:16, height:16 } }),
                        h('span', null, (form && form[key]) ? (captionOn || 'Enabled') : (captionOff || 'Disabled'))
                    )
                );
            }

            if (loading || !form) return h(Spinner, { mutedColor:mutedColor });

            return h('div', null,
                /* Back link */
                h('button', { onClick:function(){ navigate('datafeeds'); },
                    style:{ background:'none', border:'none', color:mutedColor, fontSize:13, cursor:'pointer', padding:0, marginBottom:S.lg, display:'inline-flex', alignItems:'center', gap:6 } },
                    h('span', { style:{ fontSize:15 } }, '←'), 'All feeds'),

                msg && h('div', { style:{ padding:'10px 16px', borderRadius:8, marginBottom:S.lg, fontSize:13, fontWeight:500,
                        background: msg.type === 'success' ? (dark?'rgba(22,163,74,.12)':TC.successBg) : (dark?'rgba(220,38,38,.12)':TC.errorBg),
                        color: msg.type === 'success' ? TC.success : TC.error,
                        border:'1px solid ' + (msg.type === 'success' ? TC.success : TC.error) + '30' } }, msg.text),

                h('div', { style:{ background:cardBg, border:'1px solid '+borderColor, borderRadius:12, padding:'8px '+S.xl+'px '+S.xl+'px' } },

                    /* SECTION: General */
                    h(SectionHead, { id:'general', title:'General', ctx:edCtx }),
                    open.general && h('div', null,
                        textRow('name', 'Feed name', 'A label for your reference (e.g. "Google — main catalog").', 'My product feed'),
                        selectRow('channel', 'Channel', 'Where this feed will be submitted. The output format is set automatically.', CHANNELS),
                        h(FR, { label:'Format', note:'Determined by the channel — Google & Bing use XML, Facebook/Meta uses CSV.', ctx:edCtx },
                            h('div', { style:{ paddingTop:8, fontSize:14, fontWeight:600, color:textColor } }, formatForChannel(form.channel))
                        ),
                        selectRow('schedule', 'Refresh schedule', 'How often the feed is rebuilt automatically. "Manual only" rebuilds when you click Regenerate.', SCHEDULES),
                        checkRow('enabled', 'Feed enabled', 'When off, the public URL stops serving and the schedule is skipped (the feed is not deleted).', 'Enabled', 'Disabled')
                    ),

                    /* SECTION: Product filters */
                    h(SectionHead, { id:'filters', title:'Product filters', ctx:edCtx }),
                    open.filters && h('div', null,
                        selectRow('f_status', 'Product status', 'Only products with this status are included.', STATUSES),
                        checkRow('f_in_stock_only', 'In stock only', 'Exclude out-of-stock products from the feed.', 'In-stock products only', 'Include all products'),
                        h(FR, { label:'Categories', note: cats.length ? 'Limit the feed to selected categories. Leave all unchecked to include every category.' : 'Loading categories…', ctx:edCtx },
                            h('div', { style:{ maxHeight:220, overflow:'auto', border:'1px solid '+borderColor, borderRadius:8, padding:S.sm } },
                                cats.length === 0
                                    ? h('div', { style:{ fontSize:13, color:mutedColor, padding:'8px 4px' } }, 'No categories found.')
                                    : cats.map(function(c) {
                                        var cid = Number(c.id || c.category_id);
                                        var depth = c._depth || c.depth || 0;
                                        var checked = (form.f_category_ids || []).indexOf(cid) !== -1;
                                        return h('label', { key:cid, style:{ display:'flex', alignItems:'center', gap:8, fontSize:13, cursor:'pointer', padding:'4px 4px' } },
                                            h('input', { type:'checkbox', checked:checked, onChange:function(){ toggleCategory(cid); },
                                                style:{ accentColor:TC.accent, cursor:'pointer' } }),
                                            h('span', null, ('\u00A0'.repeat(depth * 3)) + (depth > 0 ? '└ ' : '') + (c.name || '—'))
                                        );
                                    })
                            )
                        ),
                        eligible != null && h('div', { style:{ fontSize:12, color:mutedColor, marginTop:-8, marginBottom:S.lg, paddingLeft:194 } },
                            h('strong', { style:{ color:textColor } }, String(eligible)), ' product' + (eligible === 1 ? '' : 's') + ' currently match these filters.')
                    ),

                    /* SECTION: Field mapping */
                    h(SectionHead, { id:'mapping', title:'Field mapping', ctx:edCtx }),
                    open.mapping && h('div', null,
                        h('div', { style:{ fontSize:12, color:mutedColor, marginBottom:S.lg, paddingLeft:194 } },
                            'Per-feed overrides for the global defaults set under Settings. Leave a field blank to inherit the global default.'),
                        textRow('m_default_brand', 'Default brand', 'Used as the brand for products that have no brand set. Required by Google for most categories.', 'Your brand'),
                        selectRow('m_default_condition', 'Condition', 'Item condition reported to the channel.', CONDITIONS),
                        textRow('m_default_google_category', 'Google product category', 'Optional Google product category ID or full path applied to every item.', 'e.g. 166 or Apparel & Accessories > …'),
                        checkRow('m_use_sku_as_mpn', 'Use product code as MPN', 'Map each product code (SKU) to the channel MPN field.', 'Product code → MPN', 'Do not send MPN')
                    ),

                    /* SECTION: Output & history (edit only) */
                    !isNew && h(SectionHead, { id:'output', title:'Output & history', ctx:edCtx }),
                    !isNew && open.output && h('div', null,
                        h(FR, { label:'Public feed URL', note:'Submit this URL to the channel’s merchant centre. It always serves the latest cached build.', ctx:edCtx },
                            h('div', { style:{ paddingTop:4 } }, h(CopyUrl, { url:publicUrl }))
                        ),
                        h(FR, { label:'Regenerate', note:'Rebuild the cached feed now from current catalog data.', ctx:edCtx },
                            h('div', { style:{ paddingTop:4 } },
                                h(Btn, { variant:'secondary', onClick:regenerate, disabled:regening, mutedColor:mutedColor, borderColor:borderColor },
                                    regening ? 'Regenerating…' : 'Regenerate now')
                            )
                        ),
                        h('div', { style:{ marginTop:S.sm } },
                            h('div', { style:{ fontSize:13, fontWeight:600, color:textColor, marginBottom:S.sm } }, 'Recent runs'),
                            (!runs || runs.length === 0)
                                ? h('div', { style:{ fontSize:13, color:mutedColor, padding:'8px 0' } }, 'No runs yet — regenerate to create the first build.')
                                : h('div', { style:{ border:'1px solid '+borderColor, borderRadius:8, overflow:'hidden' } },
                                    h('div', { style:{ display:'grid', gridTemplateColumns:'1.6fr 0.8fr 0.9fr 0.7fr 0.8fr 1.4fr', gap:S.sm, padding:'8px 12px',
                                            borderBottom:'1px solid '+borderColor, fontSize:11, fontWeight:600, textTransform:'uppercase', letterSpacing:'.3px', color:mutedColor } },
                                        h('span', null, 'When'), h('span', null, 'Trigger'), h('span', null, 'Status'),
                                        h('span', { style:{ textAlign:'right' } }, 'Items'), h('span', { style:{ textAlign:'right' } }, 'Time'), h('span', null, 'Error')),
                                    runs.map(function(run) {
                                        return h('div', { key:run.id, style:{ display:'grid', gridTemplateColumns:'1.6fr 0.8fr 0.9fr 0.7fr 0.8fr 1.4fr', gap:S.sm,
                                                padding:'8px 12px', borderBottom:'1px solid '+borderColor, alignItems:'center', fontSize:12 } },
                                            h('span', { style:{ color:mutedColor } }, tcFmtDateTime(run.started_at)),
                                            h('span', { style:{ color:mutedColor, textTransform:'capitalize' } }, run.trigger_type || '—'),
                                            statusPill(run.status || 'never'),
                                            h('span', { style:{ textAlign:'right', color:mutedColor } }, run.item_count != null ? run.item_count : 0),
                                            h('span', { style:{ textAlign:'right', color:mutedColor } }, run.duration_ms != null ? fmtDuration(run.duration_ms) : '—'),
                                            h('span', { title:run.error || '', style:{ color: run.error ? TC.error : mutedColor, overflow:'hidden', textOverflow:'ellipsis', whiteSpace:'nowrap' } }, run.error || '—')
                                        );
                                    })
                                )
                        )
                    ),

                    /* Save bar */
                    h('div', { style:{ display:'flex', justifyContent:'flex-end', gap:S.sm, marginTop:S.xl, paddingTop:S.lg, borderTop:'1px solid '+borderColor } },
                        h(Btn, { variant:'secondary', onClick:function(){ navigate('datafeeds'); }, mutedColor:mutedColor, borderColor:borderColor }, 'Cancel'),
                        h(Btn, { variant:'primary', onClick:save, disabled:saving, mutedColor:mutedColor, borderColor:borderColor },
                            saving ? 'Saving…' : (isNew ? 'Create feed' : 'Save changes'))
                    )
                )
            );
        }

        /* ══════════════════════════════════════════════════════════════════
         * VIEW: SETTINGS (master toggle + global mapping defaults)
         * ══════════════════════════════════════════════════════════════════ */
        function FeedSettings() {
            var _f = useState(null),  form = _f[0], setForm = _f[1];
            var _l = useState(true),  loading = _l[0], setLoading = _l[1];
            var _s = useState(false), saving = _s[0], setSaving = _s[1];
            var _m = useState(null),  msg = _m[0], setMsg = _m[1];
            var _o = useState({ general:true, defaults:true });
            var open = _o[0], setOpen = _o[1];

            function toggleSection(sid) { setOpen(function(p){ var n = Object.assign({}, p); n[sid] = !n[sid]; return n; }); }
            var edCtx = { mutedColor:mutedColor, borderColor:borderColor, textColor:textColor, open:open, onToggle:toggleSection };

            function normalize(s) {
                s = s || {};
                return {
                    enabled:                 asBool(s.enabled),
                    default_brand:           s.default_brand || '',
                    default_condition:       s.default_condition || 'new',
                    default_google_category: s.default_google_category || '',
                    use_sku_as_mpn:          s.use_sku_as_mpn != null ? asBool(s.use_sku_as_mpn) : true,
                };
            }

            function load() {
                setLoading(true);
                pfApiFetch('/settings').then(function(d) {
                    var data = unwrap(d) || {};
                    setForm(normalize(data.settings || {}));
                    setLoading(false);
                }).catch(function(e) {
                    setMsg({ type:'error', text:e.message || 'Failed to load settings.' });
                    setLoading(false);
                });
            }
            useEffect(function(){ load(); }, []);

            function set(k, v) { setForm(function(f){ var n = Object.assign({}, f); n[k] = v; return n; }); }

            function save() {
                if (!form) return;
                setSaving(true); setMsg(null);
                var body = {
                    enabled:                 form.enabled ? '1' : '0',
                    default_brand:           form.default_brand,
                    default_condition:       form.default_condition,
                    default_google_category: form.default_google_category,
                    use_sku_as_mpn:          form.use_sku_as_mpn ? '1' : '0',
                };
                pfApiFetch('/settings', { method:'PUT', body:body }).then(function(d) {
                    var data = unwrap(d) || {};
                    setForm(normalize(data.settings || {}));
                    setMsg({ type:'success', text:'Settings saved.' });
                    setSaving(false);
                }).catch(function(e) {
                    var t = e.message || 'Save failed.';
                    if (e.code === 'tcpf_license_required') t = 'An active license is required to enable Product Data Feeds.';
                    setMsg({ type:'error', text:t });
                    setSaving(false);
                });
            }

            function textRow(key, label, note, placeholder) {
                return h(FR, { label:label, note:note, ctx:edCtx, key:key },
                    h('input', { type:'text', value:(form && form[key] != null) ? form[key] : '', placeholder:placeholder || '',
                        onChange:function(e){ set(key, e.target.value); }, style:fs })
                );
            }

            if (loading || !form) return h(Spinner, { mutedColor:mutedColor });

            return h('div', null,
                /* License banner (mirrors Wishlist+) */
                !licenseActive && h('div', { style:{ display:'flex', gap:S.sm, alignItems:'flex-start', padding:'12px 16px', borderRadius:8,
                        background:dark?'rgba(217,119,6,.12)':TC.amberBg, border:'1px solid '+TC.amber+'40', marginBottom:S.lg } },
                    h('span', { style:{ color:TC.amber, fontWeight:600, fontSize:13 } }, '⚠'),
                    h('div', { style:{ fontSize:13, color:textColor } },
                        h('strong', null, 'License required. '),
                        'Product Data Feeds is installed but not licensed. You can configure defaults below, but the master switch can only be enabled once a valid license is active.')
                ),

                msg && h('div', { style:{ padding:'10px 16px', borderRadius:8, marginBottom:S.lg, fontSize:13, fontWeight:500,
                        background: msg.type === 'success' ? (dark?'rgba(22,163,74,.12)':TC.successBg) : (dark?'rgba(220,38,38,.12)':TC.errorBg),
                        color: msg.type === 'success' ? TC.success : TC.error,
                        border:'1px solid ' + (msg.type === 'success' ? TC.success : TC.error) + '30' } }, msg.text),

                h('div', { style:{ background:cardBg, border:'1px solid '+borderColor, borderRadius:12, padding:'8px '+S.xl+'px '+S.xl+'px' } },

                    /* SECTION: General */
                    h(SectionHead, { id:'general', title:'General', ctx:edCtx }),
                    open.general && h('div', null,
                        h(FR, { label:'Enable Product Data Feeds', note: !licenseActive ? 'Requires an active license to enable.' : 'Master switch. When off, no feed is served publicly and scheduled refresh is paused (feeds are never deleted).', ctx:edCtx },
                            h('label', { style:{ display:'inline-flex', alignItems:'center', gap:S.sm, fontSize:14, cursor: licenseActive ? 'pointer' : 'not-allowed', paddingTop:6, opacity: licenseActive ? 1 : 0.55 } },
                                h('input', { type:'checkbox', checked:!!(form && form.enabled), disabled:!licenseActive,
                                    onChange:function(e){ set('enabled', e.target.checked); },
                                    style:{ accentColor:TC.accent, cursor: licenseActive ? 'pointer' : 'not-allowed', width:16, height:16 } }),
                                h('span', null, (form && form.enabled) ? 'Enabled' : 'Disabled')
                            )
                        )
                    ),

                    /* SECTION: Default field mapping */
                    h(SectionHead, { id:'defaults', title:'Default field mapping', ctx:edCtx }),
                    open.defaults && h('div', null,
                        h('div', { style:{ fontSize:12, color:mutedColor, marginBottom:S.lg, paddingLeft:194 } },
                            'Defaults a newly-created feed inherits. Each feed can override these in its own Field mapping.'),
                        textRow('default_brand', 'Default brand', 'Fallback brand for products with no brand set.', 'Your brand'),
                        h(FR, { label:'Condition', note:'Default item condition.', ctx:edCtx },
                            h('select', { value:(form && form.default_condition) || 'new', onChange:function(e){ set('default_condition', e.target.value); }, style:selS },
                                CONDITIONS.map(function(o){ return h('option', { key:o[0], value:o[0] }, o[1]); }))
                        ),
                        textRow('default_google_category', 'Google product category', 'Optional default Google product category ID or path.', 'e.g. 166'),
                        h(FR, { label:'Use product code as MPN', note:'Map each product code (SKU) to the channel MPN field by default.', ctx:edCtx },
                            h('label', { style:{ display:'inline-flex', alignItems:'center', gap:S.sm, fontSize:14, cursor:'pointer', paddingTop:6 } },
                                h('input', { type:'checkbox', checked:!!(form && form.use_sku_as_mpn), onChange:function(e){ set('use_sku_as_mpn', e.target.checked); },
                                    style:{ accentColor:TC.accent, cursor:'pointer', width:16, height:16 } }),
                                h('span', null, (form && form.use_sku_as_mpn) ? 'Product code → MPN' : 'Do not send MPN')
                            )
                        )
                    ),

                    /* Save bar */
                    h('div', { style:{ display:'flex', justifyContent:'flex-end', gap:S.sm, marginTop:S.xl, paddingTop:S.lg, borderTop:'1px solid '+borderColor } },
                        h(Btn, { variant:'primary', onClick:save, disabled:saving || !form, mutedColor:mutedColor, borderColor:borderColor },
                            saving ? 'Saving…' : 'Save settings')
                    )
                )
            );
        }

        /* ── Tab bar (Feeds | Settings); hidden in the editor view ── */
        var tabs = [
            { v:'list',     l:'Feeds',    slug:'datafeeds' },
            { v:'settings', l:'Settings', slug:'datafeeds/df-settings' },
        ];

        return h(PageWrap, { title:'Data Feeds', sub:'Publish your catalog to Google Shopping, Facebook/Meta, and Bing.', mutedColor:mutedColor },

            tab !== 'editor' && h('div', { style:{ display:'flex', gap:S.xs, marginBottom:S.xl, borderBottom:'1px solid '+borderColor, paddingBottom:1 } },
                tabs.map(function(t) {
                    var a = tab === t.v;
                    return h('button', { key:t.v, onClick:function(){ navigate(t.slug); },
                        style:{ padding:'10px 18px', border:'none', cursor:'pointer', fontSize:13, fontWeight:a?600:400,
                            background:'transparent', color:a?TC.accent:mutedColor, borderBottom:a?'2px solid '+TC.accent:'2px solid transparent',
                            marginBottom:-1, transition:'all .15s' } },
                        t.l);
                })
            ),

            tab === 'list'     ? h(FeedsList)
            : tab === 'editor' ? h(FeedEditor, { id: subId === 'new' ? null : subId })
            : tab === 'settings' ? h(FeedSettings)
            : null
        );
    }

    /* ════════════════════════════════════════════════════════════════════
     * CART RECOVERY PAGE — Abandoned Cart Recovery admin (S228, Part 3)
     *
     * Growth → Cart Recovery. Gated into the menu by Config.abandonedCartInstalled;
     * the master enable toggle is gated by Config.abandonedCartLicenseActive.
     *
     * Four views, dispatched off the hash subId (parse() → { route, subId }):
     *   #/cartrecovery              → RecoveriesList  (tracked carts + status counts)
     *   #/cartrecovery/{id}         → RecoveryDetail  (snapshot + event timeline)
     *   #/cartrecovery/sequence     → SequenceConfig  (threshold + steps + block email builder)
     *   #/cartrecovery/cr-settings  → Settings        (master toggle + sender + guards)
     *
     * Like DataFeeds/Subscriptions, each view is its own inner component that owns
     * its own state, so keystrokes never re-render this parent (focus-safe). The
     * block builder (EmailBuilder) keeps its own draft and only PUTs on Save.
     * No JSX — h()/wp.element.createElement throughout.
     * ════════════════════════════════════════════════════════════════════ */
    function AbandonedCartPage({ subId, dark, cardBg, textColor, mutedColor, borderColor, tableHeaderBg }) {
        var fs   = fieldSty(dark, borderColor);
        var selS = Object.assign({}, fs, { cursor:'pointer', appearance:'auto' });
        var AC_API = Config.abandonedCartApiUrl || (Config.apiUrl || '').replace('/titancart/v1', '/titancart-cart-recovery/v1');
        var licenseActive = !!Config.abandonedCartLicenseActive;

        /* Envelope-aware fetch against the cart-recovery REST namespace. */
        function acApiFetch(path, opts) {
            opts = opts || {};
            opts.headers = Object.assign({ 'Content-Type':'application/json', 'X-WP-Nonce': Config.nonce }, opts.headers || {});
            if (opts.body && typeof opts.body === 'object') opts.body = JSON.stringify(opts.body);
            return fetch(AC_API + path, opts).then(function(r) {
                return r.text().then(function(text) {
                    var data = null;
                    if (text) { try { data = JSON.parse(text); } catch(e) { data = null; } }
                    if (!r.ok || (data && data.success === false)) {
                        var m = (data && data.error && data.error.message) || (data && data.message) || ('HTTP ' + r.status);
                        var err = new Error(m); err.status = r.status; err.data = data;
                        err.code = data && data.error && data.error.code;
                        return Promise.reject(err);
                    }
                    return data;
                });
            });
        }

        function asBool(v) { return v === '1' || v === 1 || v === true; }
        function unwrap(d) { return (d && d.data !== undefined) ? d.data : d; }

        /* ── Shared lookups / formatters ── */
        var RSTATUS = {
            pending:             { l:'Pending',      c:mutedColor, bg: dark?'rgba(255,255,255,.06)':'#F5F5F5' },
            active:              { l:'Active',        c:TC.accent,  bg: dark?'rgba(37,99,235,.12)':TC.accentBg },
            recovered:           { l:'Recovered',     c:TC.success, bg: dark?'rgba(22,163,74,.12)':TC.successBg },
            converted_elsewhere: { l:'Converted',     c:TC.amber,   bg: dark?'rgba(217,119,6,.12)':TC.amberBg },
            expired:             { l:'Expired',       c:mutedColor, bg: dark?'rgba(255,255,255,.06)':'#F5F5F5' },
            unsubscribed:        { l:'Unsubscribed',  c:mutedColor, bg: dark?'rgba(255,255,255,.06)':'#F5F5F5' },
            failed:              { l:'Failed',        c:TC.error,   bg: dark?'rgba(220,38,38,.12)':TC.errorBg },
        };
        function statusPill(st) {
            var m = RSTATUS[st] || { l: st || '—', c:mutedColor, bg: dark?'rgba(255,255,255,.06)':'#F5F5F5' };
            return h('span', { style:{ fontSize:11, fontWeight:600, padding:'2px 8px', borderRadius:99, color:m.c, background:m.bg, display:'inline-block', whiteSpace:'nowrap' } }, m.l);
        }
        function fmtDelay(min) {
            min = parseInt(min, 10) || 0;
            if (min < 60)   return min + (min === 1 ? ' minute' : ' minutes');
            if (min < 1440) { var hr = Math.round(min / 60 * 10) / 10; return hr + (hr === 1 ? ' hour' : ' hours'); }
            var d = Math.round(min / 1440 * 10) / 10; return d + (d === 1 ? ' day' : ' days');
        }
        function fmtDate(s) {
            if (!s) return '—';
            var t = (s.indexOf('T') < 0) ? s.replace(' ', 'T') + 'Z' : s;
            var d = new Date(t);
            if (isNaN(d.getTime())) return s;
            return d.toLocaleString();
        }
        function num(v) { var n = parseFloat(v); return isNaN(n) ? 0 : n; }

        var COUPON_TYPES = [
            ['none',    'No discount'],
            ['percent', 'Percent off'],
            ['fixed',   'Fixed amount off'],
        ];
        var TOKENS = ['{{restore_url}}', '{{coupon_code}}', '{{first_name}}', '{{cart_total}}', '{{site_name}}', '{{unsubscribe}}'];
        var BLOCK_PALETTE = [
            { type:'header',       label:'Heading',     icon:'H' },
            { type:'text',         label:'Text',        icon:'¶' },
            { type:'product_grid', label:'Cart items',  icon:'▦' },
            { type:'coupon',       label:'Discount',    icon:'%' },
            { type:'button',       label:'Button',      icon:'▭' },
            { type:'divider',      label:'Divider',     icon:'—' },
            { type:'spacer',       label:'Spacer',      icon:'↕' },
            { type:'footer',       label:'Footer',      icon:'⌄' },
        ];
        function blockLabel(t) { var p = BLOCK_PALETTE.find(function(x){ return x.type === t; }); return p ? p.label : t; }
        function newBlock(type) {
            switch (type) {
                case 'header':       return { type:'header', text:'Heading text' };
                case 'text':         return { type:'text', text:'Your message here.' };
                case 'product_grid': return { type:'product_grid', show_price:true };
                case 'coupon':       return { type:'coupon', label:'Your discount code' };
                case 'button':       return { type:'button', text:'Return to your cart', href:'{{restore_url}}' };
                case 'divider':      return { type:'divider' };
                case 'spacer':       return { type:'spacer', height:16 };
                case 'footer':       return { type:'footer', text:'You\u2019re receiving this because you left items in your cart. {{unsubscribe}}' };
                default:             return { type:'text', text:'' };
            }
        }

        /* Banner shown above every view while unlicensed. */
        function licenseBanner() {
            if (licenseActive) return null;
            return h('div', { style:{ display:'flex', gap:S.sm, alignItems:'flex-start', padding:'12px 16px', borderRadius:8,
                    background: dark?'rgba(217,119,6,.12)':TC.amberBg, border:'1px solid '+TC.amber+'40', marginBottom:S.lg } },
                h('span', { style:{ color:TC.amber, fontWeight:600, fontSize:13 } }, '\u26A0'),
                h('div', { style:{ fontSize:13, color:textColor } },
                    h('strong', null, 'License required. '),
                    'Abandoned Cart Recovery is installed but not licensed. You can configure the sequence and emails below, but recovery emails only send once a valid license is active and the master switch is on.')
            );
        }
        function flash(msg) {
            if (!msg) return null;
            return h('div', { style:{ padding:'10px 16px', borderRadius:8, marginBottom:S.lg, fontSize:13, fontWeight:500,
                    background: msg.type === 'success' ? (dark?'rgba(22,163,74,.12)':TC.successBg) : (dark?'rgba(220,38,38,.12)':TC.errorBg),
                    color: msg.type === 'success' ? TC.success : TC.error,
                    border:'1px solid ' + (msg.type === 'success' ? TC.success : TC.error) + '30' } }, msg.text);
        }

        /* ══════════════════════════════════════════════════════════════════
         * VIEW: RECOVERIES LIST
         * ══════════════════════════════════════════════════════════════════ */
        function RecoveriesList() {
            var _l = useState(true),  loading = _l[0], setLoading = _l[1];
            var _r = useState([]),    rows = _r[0], setRows = _r[1];
            var _m = useState(null),  meta = _m[0], setMeta = _m[1];
            var _e = useState(null),  err = _e[0], setErr = _e[1];
            var _f = useState('all'), filter = _f[0], setFilter = _f[1];
            var _p = useState(1),     page = _p[0], setPage = _p[1];

            function load() {
                setLoading(true); setErr(null);
                var qs = '?page=' + page + '&per_page=25' + (filter !== 'all' ? '&status=' + encodeURIComponent(filter) : '');
                acApiFetch('/recoveries' + qs).then(function(d) {
                    setRows(unwrap(d) || []);
                    setMeta((d && d.meta) || null);
                    setLoading(false);
                }).catch(function(e) { setErr(e.message || 'Failed to load recoveries.'); setLoading(false); });
            }
            useEffect(function(){ load(); }, [filter, page]);

            var counts = (meta && meta.status_counts) || {};
            var total  = (meta && meta.total) || 0;
            var totalPages = Math.max(1, Math.ceil(total / 25));

            var FILTERS = [['all', 'All']].concat(
                ['active', 'recovered', 'pending', 'converted_elsewhere', 'expired', 'unsubscribed', 'failed'].map(function(s) {
                    return [s, (RSTATUS[s] ? RSTATUS[s].l : s) + (counts[s] ? ' (' + counts[s] + ')' : '')];
                })
            );

            return h('div', null,
                licenseBanner(),
                err && flash({ type:'error', text:err }),

                /* Filter chips */
                h('div', { style:{ display:'flex', gap:S.xs, flexWrap:'wrap', marginBottom:S.lg } },
                    FILTERS.map(function(o) {
                        var a = filter === o[0];
                        return h('button', { key:o[0], onClick:function(){ setPage(1); setFilter(o[0]); },
                            style:{ padding:'6px 14px', borderRadius:99, fontSize:12, fontWeight:500, cursor:'pointer',
                                border:'1px solid ' + (a ? TC.accent : borderColor),
                                background: a ? (dark?'rgba(37,99,235,.12)':TC.accentBg) : 'transparent',
                                color: a ? TC.accent : mutedColor } }, o[1]);
                    })
                ),

                loading ? h(Spinner, { mutedColor:mutedColor })
                : rows.length === 0 ? h(EmptyState, { mutedColor:mutedColor, icon:'\uD83D\uDED2', title:'No tracked carts yet',
                        sub:'Abandoned carts appear here once the detector picks them up.' })
                : h('div', { style:{ background:cardBg, border:'1px solid '+borderColor, borderRadius:12, overflow:'hidden' } },
                    h('table', { style:{ width:'100%', borderCollapse:'collapse', fontSize:13 } },
                        h('thead', null,
                            h('tr', { style:{ background:tableHeaderBg, textAlign:'left', color:mutedColor } },
                                ['Email', 'Status', 'Cart total', 'Items', 'Detected'].map(function(c, i) {
                                    return h('th', { key:i, style:{ padding:'10px 16px', fontWeight:600, fontSize:11, textTransform:'uppercase', letterSpacing:'.3px',
                                        textAlign: (c === 'Cart total' || c === 'Items') ? 'right' : 'left' } }, c);
                                })
                            )
                        ),
                        h('tbody', null,
                            rows.map(function(r) {
                                return h('tr', { key:r.id, onClick:function(){ navigate('cartrecovery/' + r.id); },
                                        style:{ borderTop:'1px solid '+borderColor, cursor:'pointer' } },
                                    h('td', { style:{ padding:'11px 16px', color:TC.accent, fontWeight:500, maxWidth:280, overflow:'hidden', textOverflow:'ellipsis', whiteSpace:'nowrap' } }, r.email || h('span', { style:{ color:mutedColor, fontStyle:'italic' } }, 'No email')),
                                    h('td', { style:{ padding:'11px 16px' } }, statusPill(r.status)),
                                    h('td', { style:{ padding:'11px 16px', textAlign:'right', fontVariantNumeric:'tabular-nums' } }, tcFmtMoney(num(r.cart_total), r.currency)),
                                    h('td', { style:{ padding:'11px 16px', textAlign:'right', color:mutedColor } }, parseInt(r.item_count, 10) || 0),
                                    h('td', { style:{ padding:'11px 16px', color:mutedColor, whiteSpace:'nowrap' } }, fmtDate(r.detected_at))
                                );
                            })
                        )
                    )
                ),

                totalPages > 1 && h('div', { style:{ display:'flex', justifyContent:'flex-end', marginTop:S.lg } },
                    h(Pager, { page:page, totalPages:totalPages, onPage:setPage, mutedColor:mutedColor, borderColor:borderColor })
                )
            );
        }

        /* ══════════════════════════════════════════════════════════════════
         * VIEW: RECOVERY DETAIL
         * ══════════════════════════════════════════════════════════════════ */
        function RecoveryDetail({ id }) {
            var _l = useState(true), loading = _l[0], setLoading = _l[1];
            var _r = useState(null), rec = _r[0], setRec = _r[1];
            var _e = useState(null), err = _e[0], setErr = _e[1];

            useEffect(function() {
                setLoading(true); setErr(null);
                acApiFetch('/recoveries/' + id).then(function(d) { setRec(unwrap(d)); setLoading(false); })
                    .catch(function(e) { setErr(e.message || 'Failed to load recovery.'); setLoading(false); });
            }, [id]);

            var backBtn = h('button', { onClick:function(){ navigate('cartrecovery'); },
                style:{ background:'none', border:'none', color:mutedColor, fontSize:13, cursor:'pointer', padding:0, marginBottom:S.lg, display:'inline-flex', alignItems:'center', gap:6 } },
                h('span', { style:{ fontSize:15 } }, '\u2190'), 'All recoveries');

            if (loading) return h('div', null, backBtn, h(Spinner, { mutedColor:mutedColor }));
            if (err)     return h('div', null, backBtn, flash({ type:'error', text:err }));
            if (!rec)    return h('div', null, backBtn, h(EmptyState, { mutedColor:mutedColor, title:'Recovery not found' }));

            var items  = Array.isArray(rec.cart_snapshot) ? rec.cart_snapshot : (rec.cart_snapshot && rec.cart_snapshot.items) || [];
            var events = Array.isArray(rec.events) ? rec.events : [];

            function field(label, value) {
                return h('div', null,
                    h('div', { style:{ fontSize:11, color:mutedColor, textTransform:'uppercase', letterSpacing:'.3px', marginBottom:3 } }, label),
                    h('div', { style:{ fontSize:14, color:textColor } }, value)
                );
            }

            return h('div', null,
                backBtn,
                h('div', { style:{ display:'flex', alignItems:'center', gap:S.md, marginBottom:S.lg } },
                    h('h3', { style:{ fontSize:18, fontWeight:600, margin:0 } }, rec.email || 'Guest cart'),
                    statusPill(rec.status)
                ),

                /* Summary card */
                h('div', { style:{ background:cardBg, border:'1px solid '+borderColor, borderRadius:12, padding:S.xl, marginBottom:S.lg } },
                    h('div', { style:{ display:'grid', gridTemplateColumns:'repeat(auto-fit, minmax(150px, 1fr))', gap:S.lg } },
                        field('Cart total', tcFmtMoney(num(rec.cart_total), rec.currency)),
                        field('Items', parseInt(rec.item_count, 10) || 0),
                        field('Email source', rec.email_source || '—'),
                        field('Detected', fmtDate(rec.detected_at)),
                        field('Last activity', fmtDate(rec.last_activity_at)),
                        rec.recovered_at ? field('Recovered', fmtDate(rec.recovered_at)) : null,
                        (rec.recovered_total != null && num(rec.recovered_total) > 0) ? field('Recovered total', tcFmtMoney(num(rec.recovered_total), rec.currency)) : null
                    )
                ),

                /* Cart snapshot */
                h('div', { style:{ background:cardBg, border:'1px solid '+borderColor, borderRadius:12, padding:S.xl, marginBottom:S.lg } },
                    h('div', { style:{ fontSize:13, fontWeight:600, marginBottom:S.md } }, 'Cart contents at abandonment'),
                    items.length === 0 ? h('div', { style:{ fontSize:13, color:mutedColor } }, 'No snapshot captured.')
                    : h('table', { style:{ width:'100%', borderCollapse:'collapse', fontSize:13 } },
                        h('tbody', null,
                            items.map(function(it, i) {
                                return h('tr', { key:i, style:{ borderTop: i ? '1px solid '+borderColor : 'none' } },
                                    h('td', { style:{ padding:'8px 0', color:textColor } }, it.name || ('Product #' + (it.product_id || '?'))),
                                    h('td', { style:{ padding:'8px 0', textAlign:'right', color:mutedColor } }, 'Qty ' + (parseInt(it.quantity, 10) || 1)),
                                    h('td', { style:{ padding:'8px 0', textAlign:'right', fontVariantNumeric:'tabular-nums' } }, tcFmtMoney(num(it.unit_price) * (parseInt(it.quantity, 10) || 1), rec.currency))
                                );
                            })
                        )
                    )
                ),

                /* Event timeline */
                h('div', { style:{ background:cardBg, border:'1px solid '+borderColor, borderRadius:12, padding:S.xl } },
                    h('div', { style:{ fontSize:13, fontWeight:600, marginBottom:S.md } }, 'Activity timeline'),
                    events.length === 0 ? h('div', { style:{ fontSize:13, color:mutedColor } }, 'No events yet.')
                    : h('div', { style:{ display:'flex', flexDirection:'column', gap:S.sm } },
                        events.map(function(ev, i) {
                            return h('div', { key:ev.id || i, style:{ display:'flex', gap:S.md, alignItems:'flex-start' } },
                                h('div', { style:{ width:8, height:8, borderRadius:'50%', background:TC.accent, marginTop:5, flexShrink:0 } }),
                                h('div', { style:{ flex:1 } },
                                    h('div', { style:{ fontSize:13, color:textColor, fontWeight:500 } }, (ev.event_type || '').replace(/_/g, ' ')),
                                    h('div', { style:{ fontSize:11, color:mutedColor } }, fmtDate(ev.created_at))
                                )
                            );
                        })
                    )
                )
            );
        }

        /* ══════════════════════════════════════════════════════════════════
         * VIEW: SEQUENCE CONFIG + EMAIL BUILDER
         * ══════════════════════════════════════════════════════════════════ */
        function SequenceConfig() {
            var _l = useState(true),  loading = _l[0], setLoading = _l[1];
            var _t = useState(null),  tree = _t[0], setTree = _t[1];
            var _m = useState(null),  msg = _m[0], setMsg = _m[1];
            var _ed = useState(null), editEmail = _ed[0], setEditEmail = _ed[1]; // email row currently in builder
            var _cd = useState(null), confirmDel = _cd[0], setConfirmDel = _cd[1]; // { kind:'step'|'email', id, name }
            var _busy = useState(false), busy = _busy[0], setBusy = _busy[1];

            function reload() {
                setLoading(true);
                acApiFetch('/sequence').then(function(d) { setTree(unwrap(d)); setLoading(false); })
                    .catch(function(e) { setMsg({ type:'error', text:e.message || 'Failed to load sequence.' }); setLoading(false); });
            }
            useEffect(function(){ reload(); }, []);

            // Apply a fresh tree from any write response (writes return the re-read tree).
            function applyTree(d) { setTree(unwrap(d)); }
            function notify(t, text) { setMsg({ type:t, text:text }); }

            if (loading) return h(Spinner, { mutedColor:mutedColor });
            if (!tree) return h('div', null, licenseBanner(),
                h(EmptyState, { mutedColor:mutedColor, title:'No sequence found',
                    sub:'Re-activate the extension to seed the default recovery sequence.' }));

            var steps = Array.isArray(tree.steps) ? tree.steps : [];

            /* ── Sequence header: abandon threshold (own draft) ── */
            function SequenceHeader() {
                var _v = useState(String(tree.abandon_after_minutes || 60)), val = _v[0], setVal = _v[1];
                var _s = useState(false), saving = _s[0], setSaving = _s[1];
                function save() {
                    setSaving(true); setMsg(null);
                    acApiFetch('/sequence/' + tree.id, { method:'PUT', body:{ abandon_after_minutes: parseInt(val, 10) || 60 } })
                        .then(function(d){ applyTree(d); notify('success', 'Threshold saved.'); setSaving(false); })
                        .catch(function(e){ notify('error', e.message || 'Save failed.'); setSaving(false); });
                }
                return h('div', { style:{ background:cardBg, border:'1px solid '+borderColor, borderRadius:12, padding:S.xl, marginBottom:S.lg } },
                    h('div', { style:{ fontSize:13, fontWeight:600, marginBottom:S.xs } }, 'Abandonment threshold'),
                    h('div', { style:{ fontSize:12, color:mutedColor, marginBottom:S.md } }, 'How long a cart must sit idle before it counts as abandoned and enters the sequence.'),
                    h('div', { style:{ display:'flex', alignItems:'center', gap:S.sm } },
                        h('input', { type:'number', min:1, value:val, onWheel:function(e){ e.target.blur(); },
                            onChange:function(e){ setVal(e.target.value); },
                            style:Object.assign({}, fs, { width:120 }) }),
                        h('span', { style:{ fontSize:13, color:mutedColor } }, 'minutes (' + fmtDelay(val) + ')'),
                        h('div', { style:{ flex:1 } }),
                        h(Btn, { variant:'primary', onClick:save, disabled:saving, mutedColor:mutedColor, borderColor:borderColor }, saving ? 'Saving\u2026' : 'Save')
                    )
                );
            }

            /* ── One step card: delay + enabled + discount (own draft) + variant tabs ── */
            function StepCard({ step, index, count }) {
                var _en = useState(asBool(step.enabled)), enabled = _en[0], setEnabled = _en[1];
                var _dl = useState(String(step.delay_minutes != null ? step.delay_minutes : 60)), delay = _dl[0], setDelay = _dl[1];
                var _ct = useState(step.coupon_type || 'none'), ctype = _ct[0], setCtype = _ct[1];
                var _cv = useState(String(step.coupon_value != null ? parseFloat(step.coupon_value) : 0)), cval = _cv[0], setCval = _cv[1];
                var _ce = useState(String(step.coupon_expiry_hours != null ? step.coupon_expiry_hours : 72)), cexp = _ce[0], setCexp = _ce[1];
                var _s = useState(false), saving = _s[0], setSaving = _s[1];

                function save() {
                    setSaving(true); setMsg(null);
                    acApiFetch('/steps/' + step.id, { method:'PUT', body:{
                        enabled: enabled ? '1' : '0',
                        delay_minutes: parseInt(delay, 10) || 0,
                        coupon_type: ctype,
                        coupon_value: ctype === 'none' ? 0 : (parseFloat(cval) || 0),
                        coupon_expiry_hours: parseInt(cexp, 10) || 72,
                    } }).then(function(d){ applyTree(d); notify('success', 'Step ' + (index + 1) + ' saved.'); setSaving(false); })
                        .catch(function(e){ notify('error', e.message || 'Save failed.'); setSaving(false); });
                }
                function move(dir) {
                    var ids = steps.map(function(s){ return s.id; });
                    var j = index + dir;
                    if (j < 0 || j >= ids.length) return;
                    var tmp = ids[index]; ids[index] = ids[j]; ids[j] = tmp;
                    setMsg(null);
                    acApiFetch('/steps/reorder', { method:'POST', body:{ sequence_id: tree.id, ordered_ids: ids } })
                        .then(applyTree).catch(function(e){ notify('error', e.message || 'Reorder failed.'); });
                }

                var emails = Array.isArray(step.emails) ? step.emails : [];

                return h('div', { style:{ background:cardBg, border:'1px solid '+borderColor, borderRadius:12, padding:S.xl, marginBottom:S.md, opacity: enabled ? 1 : 0.7 } },
                    /* Header row */
                    h('div', { style:{ display:'flex', alignItems:'center', gap:S.md, marginBottom:S.lg } },
                        h('div', { style:{ width:28, height:28, borderRadius:8, background:dark?'rgba(37,99,235,.12)':TC.accentBg, color:TC.accent,
                            display:'flex', alignItems:'center', justifyContent:'center', fontWeight:700, fontSize:13, flexShrink:0 } }, index + 1),
                        h('div', { style:{ flex:1, fontSize:14, fontWeight:600 } }, 'Step ' + (index + 1),
                            h('span', { style:{ fontSize:12, fontWeight:400, color:mutedColor, marginLeft:8 } }, 'sends ' + fmtDelay(delay) + ' after abandonment')),
                        /* reorder */
                        h('button', { onClick:function(){ move(-1); }, disabled:index === 0, title:'Move up',
                            style:{ background:'none', border:'1px solid '+borderColor, borderRadius:6, width:26, height:26, cursor:index===0?'default':'pointer', color:mutedColor, opacity:index===0?0.4:1 } }, '\u2191'),
                        h('button', { onClick:function(){ move(1); }, disabled:index === count - 1, title:'Move down',
                            style:{ background:'none', border:'1px solid '+borderColor, borderRadius:6, width:26, height:26, cursor:index===count-1?'default':'pointer', color:mutedColor, opacity:index===count-1?0.4:1 } }, '\u2193'),
                        h('button', { onClick:function(){ setConfirmDel({ kind:'step', id:step.id, name:'Step ' + (index + 1) }); }, title:'Delete step',
                            style:{ background:'none', border:'1px solid '+borderColor, borderRadius:6, width:26, height:26, cursor:'pointer', color:TC.error } }, '\u00D7')
                    ),

                    /* Editable fields */
                    h('div', { style:{ display:'grid', gridTemplateColumns:'repeat(auto-fit, minmax(180px, 1fr))', gap:S.lg, marginBottom:S.lg } },
                        h('label', { style:{ display:'flex', flexDirection:'column', gap:5, fontSize:12, color:mutedColor } }, 'Send delay (minutes after abandonment)',
                            h('input', { type:'number', min:0, value:delay, onWheel:function(e){ e.target.blur(); }, onChange:function(e){ setDelay(e.target.value); }, style:fs })),
                        h('label', { style:{ display:'flex', flexDirection:'column', gap:5, fontSize:12, color:mutedColor } }, 'Discount',
                            h('select', { value:ctype, onChange:function(e){ setCtype(e.target.value); }, style:selS },
                                COUPON_TYPES.map(function(o){ return h('option', { key:o[0], value:o[0] }, o[1]); }))),
                        ctype !== 'none' && h('label', { style:{ display:'flex', flexDirection:'column', gap:5, fontSize:12, color:mutedColor } }, ctype === 'percent' ? 'Percent (%)' : 'Amount',
                            h('input', { type:'number', min:0, step:'0.01', value:cval, onWheel:function(e){ e.target.blur(); }, onChange:function(e){ setCval(e.target.value); }, style:fs })),
                        ctype !== 'none' && h('label', { style:{ display:'flex', flexDirection:'column', gap:5, fontSize:12, color:mutedColor } }, 'Code expires after (hours)',
                            h('input', { type:'number', min:1, value:cexp, onWheel:function(e){ e.target.blur(); }, onChange:function(e){ setCexp(e.target.value); }, style:fs }))
                    ),

                    /* enabled + save */
                    h('div', { style:{ display:'flex', alignItems:'center', gap:S.md, paddingTop:S.md, borderTop:'1px solid '+borderColor } },
                        h('label', { style:{ display:'inline-flex', alignItems:'center', gap:S.sm, fontSize:13, cursor:'pointer' } },
                            h('input', { type:'checkbox', checked:enabled, onChange:function(e){ setEnabled(e.target.checked); }, style:{ accentColor:TC.accent, width:16, height:16, cursor:'pointer' } }),
                            h('span', null, enabled ? 'Enabled' : 'Disabled')),
                        h('div', { style:{ flex:1 } }),
                        h(Btn, { variant:'primary', onClick:save, disabled:saving, mutedColor:mutedColor, borderColor:borderColor }, saving ? 'Saving\u2026' : 'Save step')
                    ),

                    /* Email variants */
                    h('div', { style:{ marginTop:S.lg, paddingTop:S.lg, borderTop:'1px solid '+borderColor } },
                        h('div', { style:{ display:'flex', alignItems:'center', gap:S.sm, marginBottom:S.md } },
                            h('span', { style:{ fontSize:12, fontWeight:600, color:mutedColor, textTransform:'uppercase', letterSpacing:'.3px' } }, 'Email variants'),
                            emails.length > 1 && h('span', { style:{ fontSize:11, color:mutedColor } }, '(A/B split by weight)')
                        ),
                        h('div', { style:{ display:'flex', flexWrap:'wrap', gap:S.sm } },
                            emails.map(function(em) {
                                return h('div', { key:em.id, style:{ display:'flex', alignItems:'center', gap:S.sm, padding:'8px 12px', border:'1px solid '+borderColor, borderRadius:8,
                                        background: asBool(em.enabled) ? 'transparent' : (dark?'rgba(255,255,255,.03)':'#FAFAFA') } },
                                    h('span', { style:{ width:22, height:22, borderRadius:6, background:dark?'rgba(37,99,235,.12)':TC.accentBg, color:TC.accent,
                                        display:'flex', alignItems:'center', justifyContent:'center', fontWeight:700, fontSize:11 } }, em.variant || 'A'),
                                    h('div', { style:{ minWidth:0 } },
                                        h('div', { style:{ fontSize:13, fontWeight:500, maxWidth:200, overflow:'hidden', textOverflow:'ellipsis', whiteSpace:'nowrap' } }, em.subject || '(no subject)'),
                                        h('div', { style:{ fontSize:11, color:mutedColor } }, (Array.isArray(em.blocks) ? em.blocks.length : 0) + ' blocks \u00B7 weight ' + (em.weight || 1) + (asBool(em.enabled) ? '' : ' \u00B7 off'))),
                                    h('button', { onClick:function(){ setEditEmail(em); },
                                        style:{ background:'none', border:'none', color:TC.accent, fontSize:12, fontWeight:600, cursor:'pointer' } }, 'Edit'),
                                    emails.length > 1 && h('button', { onClick:function(){ setConfirmDel({ kind:'email', id:em.id, name:'Variant ' + (em.variant || '') }); },
                                        style:{ background:'none', border:'none', color:TC.error, fontSize:14, cursor:'pointer' } }, '\u00D7')
                                );
                            }),
                            h('button', { onClick:function(){
                                    setMsg(null);
                                    acApiFetch('/steps/' + step.id + '/emails', { method:'POST', body:{ subject:'New variant', preheader:'', blocks:[ newBlock('header'), newBlock('product_grid'), newBlock('button') ] } })
                                        .then(function(d){ applyTree(d); notify('success', 'Variant added.'); }).catch(function(e){ notify('error', e.message || 'Add failed.'); });
                                },
                                style:{ padding:'8px 12px', border:'1px dashed '+borderColor, borderRadius:8, background:'transparent', color:mutedColor, fontSize:12, fontWeight:500, cursor:'pointer' } }, '+ Add A/B variant')
                        )
                    )
                );
            }

            /* ── Block-based email builder (own draft; PUT on save) ── */
            function EmailBuilder({ email }) {
                var _subj = useState(email.subject || ''), subj = _subj[0], setSubj = _subj[1];
                var _pre  = useState(email.preheader || ''), pre = _pre[0], setPre = _pre[1];
                var _wt   = useState(String(email.weight || 1)), wt = _wt[0], setWt = _wt[1];
                var _en   = useState(asBool(email.enabled)), en = _en[0], setEn = _en[1];
                // Give each block a stable transient key so React preserves focus across reorder.
                var _bl = useState(function() {
                    var arr = Array.isArray(email.blocks) ? email.blocks : [];
                    return arr.map(function(b, i){ return Object.assign({ _k: 'b' + i + '_' + Math.random().toString(36).slice(2, 7) }, b); });
                });
                var blocks = _bl[0], setBlocks = _bl[1];
                var _s = useState(false), saving = _s[0], setSaving = _s[1];
                var dragIdx = useRef(null);
                var _do = useState(null), dropIdx = _do[0], setDropIdx = _do[1];

                function addBlock(type) {
                    setBlocks(function(p){ return p.concat([ Object.assign({ _k:'b_' + Math.random().toString(36).slice(2, 8) }, newBlock(type)) ]); });
                }
                function removeBlock(i) { setBlocks(function(p){ return p.filter(function(_, j){ return j !== i; }); }); }
                function patchBlock(i, patch) { setBlocks(function(p){ return p.map(function(b, j){ return j === i ? Object.assign({}, b, patch) : b; }); }); }
                function moveBlock(from, to) {
                    if (to < 0 || from < 0) return;
                    setBlocks(function(p) {
                        if (to >= p.length) return p;
                        var arr = p.slice(); var it = arr.splice(from, 1)[0]; arr.splice(to, 0, it); return arr;
                    });
                }

                function save() {
                    setSaving(true); setMsg(null);
                    var clean = blocks.map(function(b){ var c = Object.assign({}, b); delete c._k; return c; });
                    acApiFetch('/emails/' + email.id, { method:'PUT', body:{
                        subject: subj, preheader: pre, weight: parseInt(wt, 10) || 1, enabled: en ? '1' : '0', blocks: clean,
                    } }).then(function(d){ applyTree(d); notify('success', 'Email saved.'); setSaving(false); setEditEmail(null); })
                        .catch(function(e){ notify('error', e.message || 'Save failed.'); setSaving(false); });
                }

                function blockEditor(b, i) {
                    var rows = [];
                    if (b.type === 'header' || b.type === 'text' || b.type === 'footer') {
                        rows.push(h('textarea', { key:'t', value:b.text || '', rows: b.type === 'text' || b.type === 'footer' ? 3 : 2,
                            onChange:function(e){ patchBlock(i, { text:e.target.value }); },
                            placeholder:'Text\u2026 you can use ' + (b.type === 'footer' ? '{{unsubscribe}}' : '{{first_name}}'),
                            style:Object.assign({}, fs, { fontFamily:'inherit', resize:'vertical' }) }));
                    } else if (b.type === 'button') {
                        rows.push(h('input', { key:'bt', type:'text', value:b.text || '', placeholder:'Button label',
                            onChange:function(e){ patchBlock(i, { text:e.target.value }); }, style:Object.assign({}, fs, { marginBottom:6 }) }));
                        rows.push(h('input', { key:'bh', type:'text', value:b.href || '', placeholder:'Link (default {{restore_url}})',
                            onChange:function(e){ patchBlock(i, { href:e.target.value }); }, style:fs }));
                    } else if (b.type === 'coupon') {
                        rows.push(h('input', { key:'cl', type:'text', value:b.label || '', placeholder:'Label above the code',
                            onChange:function(e){ patchBlock(i, { label:e.target.value }); }, style:fs }));
                        rows.push(h('div', { key:'cn', style:{ fontSize:11, color:mutedColor, marginTop:5 } }, 'The code is generated per-recovery from the step discount. This block renders nothing if the step has no discount.'));
                    } else if (b.type === 'product_grid') {
                        rows.push(h('label', { key:'pg', style:{ display:'inline-flex', alignItems:'center', gap:S.sm, fontSize:13, cursor:'pointer' } },
                            h('input', { type:'checkbox', checked:b.show_price !== false, onChange:function(e){ patchBlock(i, { show_price:e.target.checked }); }, style:{ accentColor:TC.accent } }),
                            h('span', null, 'Show line prices')));
                        rows.push(h('div', { key:'pn', style:{ fontSize:11, color:mutedColor, marginTop:5 } }, 'Renders the shopper\u2019s actual abandoned items with thumbnails.'));
                    } else if (b.type === 'spacer') {
                        rows.push(h('label', { key:'sp', style:{ display:'flex', flexDirection:'column', gap:5, fontSize:12, color:mutedColor } }, 'Height (px)',
                            h('input', { type:'number', min:4, max:80, value:b.height != null ? b.height : 16, onWheel:function(e){ e.target.blur(); },
                                onChange:function(e){ patchBlock(i, { height: parseInt(e.target.value, 10) || 16 }); }, style:Object.assign({}, fs, { width:120 }) })));
                    } else if (b.type === 'divider') {
                        rows.push(h('div', { key:'dv', style:{ fontSize:11, color:mutedColor } }, 'A horizontal rule.'));
                    }
                    return rows;
                }

                return h('div', { style:{ background:cardBg, border:'1px solid '+borderColor, borderRadius:12, padding:S.xl } },
                    /* Builder header */
                    h('div', { style:{ display:'flex', alignItems:'center', gap:S.md, marginBottom:S.lg } },
                        h('button', { onClick:function(){ setEditEmail(null); },
                            style:{ background:'none', border:'none', color:mutedColor, fontSize:13, cursor:'pointer', padding:0, display:'inline-flex', alignItems:'center', gap:6 } },
                            h('span', { style:{ fontSize:15 } }, '\u2190'), 'Back to sequence'),
                        h('div', { style:{ flex:1 } }),
                        h('span', { style:{ fontSize:12, color:mutedColor } }, 'Variant ' + (email.variant || 'A'))
                    ),

                    /* Subject / preheader / weight / enabled */
                    h(FR, { label:'Subject', ctx:{ mutedColor:mutedColor }, },
                        h('input', { type:'text', value:subj, onChange:function(e){ setSubj(e.target.value); }, placeholder:'You left something behind\u2026', style:fs })),
                    h(FR, { label:'Preheader', note:'Preview text shown in the inbox after the subject.', ctx:{ mutedColor:mutedColor } },
                        h('input', { type:'text', value:pre, onChange:function(e){ setPre(e.target.value); }, placeholder:'Complete your order in one click', style:fs })),
                    h(FR, { label:'Split weight', note:'Relative A/B send weight when the step has multiple enabled variants.', ctx:{ mutedColor:mutedColor } },
                        h('input', { type:'number', min:1, value:wt, onWheel:function(e){ e.target.blur(); }, onChange:function(e){ setWt(e.target.value); }, style:Object.assign({}, fs, { width:120 }) })),
                    h(FR, { label:'Variant enabled', ctx:{ mutedColor:mutedColor } },
                        h('label', { style:{ display:'inline-flex', alignItems:'center', gap:S.sm, fontSize:14, cursor:'pointer', paddingTop:6 } },
                            h('input', { type:'checkbox', checked:en, onChange:function(e){ setEn(e.target.checked); }, style:{ accentColor:TC.accent, width:16, height:16 } }),
                            h('span', null, en ? 'Enabled' : 'Disabled'))),

                    /* Token hint */
                    h('div', { style:{ fontSize:11, color:mutedColor, marginBottom:S.lg, lineHeight:1.6 } },
                        'Tokens you can use in text/button/footer blocks: ',
                        TOKENS.map(function(t, i){ return h('code', { key:t, style:{ background:dark?'rgba(255,255,255,.06)':'#F5F5F5', padding:'1px 5px', borderRadius:4, marginRight:5, fontSize:11 } }, t); })
                    ),

                    /* Block list */
                    h('div', { style:{ marginBottom:S.lg } },
                        blocks.length === 0 && h('div', { style:{ textAlign:'center', padding:32, border:'1px dashed '+borderColor, borderRadius:10, color:mutedColor, fontSize:13, marginBottom:S.md } }, 'No blocks yet. Add one below.'),
                        blocks.map(function(b, i) {
                            return h('div', { key:b._k, draggable:true,
                                    onDragStart:function(){ dragIdx.current = i; },
                                    onDragOver:function(e){ e.preventDefault(); if (dropIdx !== i) setDropIdx(i); },
                                    onDragEnd:function(){ dragIdx.current = null; setDropIdx(null); },
                                    onDrop:function(e){ e.preventDefault(); var from = dragIdx.current; if (from != null && from !== i) moveBlock(from, i); dragIdx.current = null; setDropIdx(null); },
                                    style:{ border:'1px solid ' + (dropIdx === i ? TC.accent : borderColor), borderRadius:10, padding:S.md, marginBottom:S.sm,
                                        background: dark?'rgba(255,255,255,.02)':'#FCFCFD', transition:'border-color .12s' } },
                                /* Block head */
                                h('div', { style:{ display:'flex', alignItems:'center', gap:S.sm, marginBottom: (b.type==='divider') ? 0 : S.sm } },
                                    h('span', { title:'Drag to reorder', style:{ cursor:'grab', color:mutedColor, fontSize:14, userSelect:'none' } }, '\u22EE\u22EE'),
                                    h('span', { style:{ fontSize:12, fontWeight:600, color:textColor, flex:1 } }, blockLabel(b.type)),
                                    h('button', { onClick:function(){ moveBlock(i, i - 1); }, disabled:i === 0, title:'Move up',
                                        style:{ background:'none', border:'1px solid '+borderColor, borderRadius:6, width:24, height:24, cursor:i===0?'default':'pointer', color:mutedColor, opacity:i===0?0.4:1 } }, '\u2191'),
                                    h('button', { onClick:function(){ moveBlock(i, i + 1); }, disabled:i === blocks.length - 1, title:'Move down',
                                        style:{ background:'none', border:'1px solid '+borderColor, borderRadius:6, width:24, height:24, cursor:i===blocks.length-1?'default':'pointer', color:mutedColor, opacity:i===blocks.length-1?0.4:1 } }, '\u2193'),
                                    h('button', { onClick:function(){ removeBlock(i); }, title:'Remove block',
                                        style:{ background:'none', border:'1px solid '+borderColor, borderRadius:6, width:24, height:24, cursor:'pointer', color:TC.error } }, '\u00D7')
                                ),
                                /* Block body */
                                blockEditor(b, i)
                            );
                        })
                    ),

                    /* Block palette */
                    h('div', { style:{ marginBottom:S.lg } },
                        h('div', { style:{ fontSize:11, color:mutedColor, textTransform:'uppercase', letterSpacing:'.3px', marginBottom:S.sm } }, 'Add a block'),
                        h('div', { style:{ display:'flex', flexWrap:'wrap', gap:S.sm } },
                            BLOCK_PALETTE.map(function(p) {
                                return h('button', { key:p.type, onClick:function(){ addBlock(p.type); },
                                    style:{ display:'inline-flex', alignItems:'center', gap:6, padding:'6px 12px', border:'1px solid '+borderColor, borderRadius:8,
                                        background:'transparent', color:textColor, fontSize:12, fontWeight:500, cursor:'pointer' } },
                                    h('span', { style:{ color:TC.accent, fontWeight:700 } }, p.icon), p.label);
                            })
                        )
                    ),

                    /* Save bar */
                    h('div', { style:{ display:'flex', justifyContent:'flex-end', gap:S.sm, paddingTop:S.lg, borderTop:'1px solid '+borderColor } },
                        h(Btn, { variant:'secondary', onClick:function(){ setEditEmail(null); }, mutedColor:mutedColor, borderColor:borderColor }, 'Cancel'),
                        h(Btn, { variant:'primary', onClick:save, disabled:saving, mutedColor:mutedColor, borderColor:borderColor }, saving ? 'Saving\u2026' : 'Save email')
                    )
                );
            }

            /* Builder takes over the whole view when an email is open. */
            if (editEmail) {
                return h('div', null, licenseBanner(), flash(msg), h(EmailBuilder, { email:editEmail, key:editEmail.id }));
            }

            function doDelete() {
                if (!confirmDel) return;
                setBusy(true);
                var path = confirmDel.kind === 'step' ? '/steps/' + confirmDel.id : '/emails/' + confirmDel.id;
                acApiFetch(path, { method:'DELETE' }).then(function(d){ applyTree(d); notify('success', confirmDel.name + ' deleted.'); setBusy(false); setConfirmDel(null); })
                    .catch(function(e){ notify('error', e.message || 'Delete failed.'); setBusy(false); setConfirmDel(null); });
            }

            return h('div', null,
                licenseBanner(),
                flash(msg),
                h(SequenceHeader),
                h('div', { style:{ display:'flex', alignItems:'center', gap:S.sm, margin:S.lg + 'px 0 ' + S.md + 'px' } },
                    h('span', { style:{ fontSize:13, fontWeight:600, color:mutedColor, textTransform:'uppercase', letterSpacing:'.3px' } }, 'Drip steps'),
                    h('span', { style:{ fontSize:12, color:mutedColor } }, '(' + steps.length + ')')
                ),
                steps.map(function(s, i) { return h(StepCard, { key:s.id, step:s, index:i, count:steps.length }); }),
                h('button', { onClick:function() {
                        setMsg(null);
                        acApiFetch('/steps', { method:'POST', body:{ sequence_id: tree.id, delay_minutes: 1440, enabled:'1', coupon_type:'none' } })
                            .then(function(d){ applyTree(d); notify('success', 'Step added.'); }).catch(function(e){ notify('error', e.message || 'Add failed.'); });
                    },
                    style:{ width:'100%', padding:'12px', border:'1px dashed '+borderColor, borderRadius:10, background:'transparent', color:mutedColor, fontSize:13, fontWeight:500, cursor:'pointer' } }, '+ Add step'),

                h(ConfirmModal, {
                    open: !!confirmDel,
                    title: confirmDel && confirmDel.kind === 'step' ? 'Delete this step?' : 'Delete this variant?',
                    message: confirmDel && confirmDel.kind === 'step'
                        ? 'This removes the step and all of its email variants. Already-scheduled sends for this step will be skipped. This cannot be undone.'
                        : 'This removes the email variant. This cannot be undone.',
                    detail: confirmDel ? confirmDel.name : '',
                    confirmLabel: 'Delete', busy: busy, dark: dark, cardBg: cardBg, borderColor: borderColor, textColor: textColor, mutedColor: mutedColor,
                    onConfirm: doDelete, onCancel: function(){ if (!busy) setConfirmDel(null); }
                })
            );
        }

        /* ══════════════════════════════════════════════════════════════════
         * VIEW: SETTINGS
         * ══════════════════════════════════════════════════════════════════ */
        function Settings() {
            var _l = useState(true),  loading = _l[0], setLoading = _l[1];
            var _f = useState(null),  form = _f[0], setForm = _f[1];
            var _sv = useState(false), saving = _sv[0], setSaving = _sv[1];
            var _m = useState(null),  msg = _m[0], setMsg = _m[1];
            var _o = useState({ general:true, sender:true, guards:true, capture:true });
            var open = _o[0], setOpen = _o[1];
            function toggleSection(sid) { setOpen(function(p){ var n = Object.assign({}, p); n[sid] = !n[sid]; return n; }); }
            var edCtx = { mutedColor:mutedColor, borderColor:borderColor, textColor:textColor, open:open, onToggle:toggleSection };

            function normalize(s) {
                s = s || {};
                return {
                    enabled:             asBool(s.enabled),
                    from_name:           s.from_name || '',
                    from_email:          s.from_email || '',
                    min_cart_total:      s.min_cart_total != null ? String(s.min_cart_total) : '0',
                    send_to_guests:      s.send_to_guests != null ? asBool(s.send_to_guests) : true,
                    dedupe_window_hours: s.dedupe_window_hours != null ? String(s.dedupe_window_hours) : '24',
                    capture_popup_enabled: asBool(s.capture_popup_enabled),
                    exit_intent_enabled:   asBool(s.exit_intent_enabled),
                    capture_popup_delay:   s.capture_popup_delay != null ? String(s.capture_popup_delay) : '15',
                };
            }
            function load() {
                setLoading(true);
                acApiFetch('/settings').then(function(d){ var data = unwrap(d) || {}; setForm(normalize(data.settings || {})); setLoading(false); })
                    .catch(function(e){ setMsg({ type:'error', text:e.message || 'Failed to load settings.' }); setLoading(false); });
            }
            useEffect(function(){ load(); }, []);
            function set(k, v) { setForm(function(f){ var n = Object.assign({}, f); n[k] = v; return n; }); }

            function save() {
                if (!form) return;
                setSaving(true); setMsg(null);
                acApiFetch('/settings', { method:'PUT', body:{
                    enabled:             form.enabled ? '1' : '0',
                    from_name:           form.from_name,
                    from_email:          form.from_email,
                    min_cart_total:      String(num(form.min_cart_total)),
                    send_to_guests:      form.send_to_guests ? '1' : '0',
                    dedupe_window_hours: String(parseInt(form.dedupe_window_hours, 10) || 0),
                    capture_popup_enabled: form.capture_popup_enabled ? '1' : '0',
                    exit_intent_enabled:   form.exit_intent_enabled ? '1' : '0',
                    capture_popup_delay:   String(parseInt(form.capture_popup_delay, 10) || 0),
                } }).then(function(d){ var data = unwrap(d) || {}; setForm(normalize(data.settings || {})); setMsg({ type:'success', text:'Settings saved.' }); setSaving(false); })
                    .catch(function(e){ var t = e.message || 'Save failed.'; if (e.code === 'tcac_license_required') t = 'An active license is required to enable Abandoned Cart Recovery.'; setMsg({ type:'error', text:t }); setSaving(false); });
            }

            if (loading || !form) return h(Spinner, { mutedColor:mutedColor });

            return h('div', null,
                licenseBanner(),
                flash(msg),
                h('div', { style:{ background:cardBg, border:'1px solid '+borderColor, borderRadius:12, padding:'8px '+S.xl+'px '+S.xl+'px' } },

                    /* General */
                    h(SectionHead, { id:'general', title:'General', ctx:edCtx }),
                    open.general && h('div', null,
                        h(FR, { label:'Enable Cart Recovery', note: !licenseActive ? 'Requires an active license to enable.' : 'Master switch. When off, detection and recovery sends pause (tracked data is kept).', ctx:edCtx },
                            h('label', { style:{ display:'inline-flex', alignItems:'center', gap:S.sm, fontSize:14, cursor: licenseActive ? 'pointer' : 'not-allowed', paddingTop:6, opacity: licenseActive ? 1 : 0.55 } },
                                h('input', { type:'checkbox', checked:!!form.enabled, disabled:!licenseActive, onChange:function(e){ set('enabled', e.target.checked); },
                                    style:{ accentColor:TC.accent, cursor: licenseActive ? 'pointer' : 'not-allowed', width:16, height:16 } }),
                                h('span', null, form.enabled ? 'Enabled' : 'Disabled')))
                    ),

                    /* Sender */
                    h(SectionHead, { id:'sender', title:'Sender', ctx:edCtx }),
                    open.sender && h('div', null,
                        h(FR, { label:'From name', note:'Blank uses your site name.', ctx:edCtx },
                            h('input', { type:'text', value:form.from_name, placeholder:'Your store', onChange:function(e){ set('from_name', e.target.value); }, style:fs })),
                        h(FR, { label:'From email', note:'Blank uses your site admin email.', ctx:edCtx },
                            h('input', { type:'email', value:form.from_email, placeholder:'hello@yourstore.com', onChange:function(e){ set('from_email', e.target.value); }, style:fs }))
                    ),

                    /* Guards */
                    h(SectionHead, { id:'guards', title:'Sending guards', ctx:edCtx }),
                    open.guards && h('div', null,
                        h(FR, { label:'Minimum cart total', note:'Skip carts below this subtotal. 0 = no minimum.', ctx:edCtx },
                            h('input', { type:'number', min:0, step:'0.01', value:form.min_cart_total, onWheel:function(e){ e.target.blur(); }, onChange:function(e){ set('min_cart_total', e.target.value); }, style:Object.assign({}, fs, { width:160 }) })),
                        h(FR, { label:'Email guests', note:'Send to guest carts once an email address is captured.', ctx:edCtx },
                            h('label', { style:{ display:'inline-flex', alignItems:'center', gap:S.sm, fontSize:14, cursor:'pointer', paddingTop:6 } },
                                h('input', { type:'checkbox', checked:!!form.send_to_guests, onChange:function(e){ set('send_to_guests', e.target.checked); }, style:{ accentColor:TC.accent, width:16, height:16 } }),
                                h('span', null, form.send_to_guests ? 'On' : 'Off'))),
                        h(FR, { label:'Dedupe window (hours)', note:'Don\u2019t re-track the same email\u2019s cart within this window.', ctx:edCtx },
                            h('input', { type:'number', min:0, value:form.dedupe_window_hours, onWheel:function(e){ e.target.blur(); }, onChange:function(e){ set('dedupe_window_hours', e.target.value); }, style:Object.assign({}, fs, { width:160 }) }))
                    ),

                    /* Capture popup */
                    h(SectionHead, { id:'capture', title:'Capture popup', ctx:edCtx }),
                    open.capture && h('div', null,
                        h(FR, { label:'Email-capture popup', note:'Show a guest email-capture popup on the storefront so guest carts can be recovered. Only shows to guests who have items in their cart.', ctx:edCtx },
                            h('label', { style:{ display:'inline-flex', alignItems:'center', gap:S.sm, fontSize:14, cursor:'pointer', paddingTop:6 } },
                                h('input', { type:'checkbox', checked:!!form.capture_popup_enabled, onChange:function(e){ set('capture_popup_enabled', e.target.checked); }, style:{ accentColor:TC.accent, width:16, height:16 } }),
                                h('span', null, form.capture_popup_enabled ? 'On' : 'Off'))),
                        h(FR, { label:'Popup delay (seconds)', note:'How long to wait before showing the popup.', ctx:edCtx },
                            h('input', { type:'number', min:0, value:form.capture_popup_delay, onWheel:function(e){ e.target.blur(); }, onChange:function(e){ set('capture_popup_delay', e.target.value); }, style:Object.assign({}, fs, { width:160 }) })),
                        h(FR, { label:'Exit-intent', note:'Also show the popup when a guest moves to leave the page (desktop only).', ctx:edCtx },
                            h('label', { style:{ display:'inline-flex', alignItems:'center', gap:S.sm, fontSize:14, cursor:'pointer', paddingTop:6 } },
                                h('input', { type:'checkbox', checked:!!form.exit_intent_enabled, onChange:function(e){ set('exit_intent_enabled', e.target.checked); }, style:{ accentColor:TC.accent, width:16, height:16 } }),
                                h('span', null, form.exit_intent_enabled ? 'On' : 'Off')))
                    ),

                    /* Save */
                    h('div', { style:{ display:'flex', justifyContent:'flex-end', marginTop:S.xl, paddingTop:S.lg, borderTop:'1px solid '+borderColor } },
                        h(Btn, { variant:'primary', onClick:save, disabled:saving || !form, mutedColor:mutedColor, borderColor:borderColor }, saving ? 'Saving\u2026' : 'Save settings')
                    )
                )
            );
        }

        /* ══════════════════════════════════════════════════════════════════
         * VIEW: ANALYTICS  [Part 4]
         * Recovery-rate dashboard, revenue recovered, funnel drop-off, and A/B
         * variant results. Read-only and NOT license-gated (viewing insights is
         * always allowed; only sending is gated). Built on tracked data only —
         * sent → clicked → recovered. There is no open-tracking pixel, so an
         * open rate is intentionally not shown.
         * ══════════════════════════════════════════════════════════════════ */
        function Analytics() {
            var [days, setDays] = useState(30);
            var [data, setData] = useState(null);
            var [loading, setLoading] = useState(true);
            var [err, setErr] = useState('');

            useEffect(function() {
                var alive = true;
                setLoading(true); setErr('');
                acApiFetch('/analytics?days=' + days)
                    .then(function(d) { if (alive) { setData(unwrap(d)); setLoading(false); } })
                    .catch(function(e) { if (alive) { setErr(e.message || 'Failed to load analytics.'); setLoading(false); } });
                return function() { alive = false; };
            }, [days]);

            var RANGES = [ { l:'30 days', v:30 }, { l:'90 days', v:90 }, { l:'1 year', v:365 }, { l:'All time', v:0 } ];
            function rangeBar() {
                return h('div', { style:{ display:'flex', gap:S.xs } },
                    RANGES.map(function(r) {
                        var a = days === r.v;
                        return h('button', { key:r.v, onClick:function(){ setDays(r.v); },
                            style:{ padding:'6px 12px', fontSize:12, fontWeight:a?600:500, cursor:'pointer', borderRadius:8,
                                border:'1px solid '+(a?TC.accent:borderColor), background:a?(dark?'rgba(37,99,235,.12)':TC.accentBg):'transparent',
                                color:a?TC.accent:mutedColor } }, r.l);
                    })
                );
            }

            var header = h('div', { style:{ display:'flex', justifyContent:'space-between', alignItems:'center', marginBottom:S.lg, flexWrap:'wrap', gap:S.md } },
                h('div', { style:{ fontSize:13, color:mutedColor } }, 'Performance over the selected window.'),
                rangeBar()
            );

            if (loading) return h('div', null, licenseBanner(), header, h('div', { style:{ padding:40, textAlign:'center', color:mutedColor } }, 'Loading analytics\u2026'));
            if (err)     return h('div', null, licenseBanner(), header, flash({ type:'error', text:err }));
            if (!data)   return h('div', null, licenseBanner(), header, h(EmptyState, { mutedColor:mutedColor, icon:'\uD83D\uDCCA', title:'No data', sub:'Analytics will appear once carts are tracked.' }));

            var k = data.kpis || {};
            var cur = data.currency || 'USD';
            var ts = data.timeseries || [];
            var funnel = data.funnel || [];
            var variants = data.variants || [];
            var sb = data.status_breakdown || {};

            var noActivity = (k.tracked || 0) === 0 && (k.emails_sent || 0) === 0;
            if (noActivity) {
                return h('div', null, licenseBanner(), header,
                    h(EmptyState, { mutedColor:mutedColor, icon:'\uD83D\uDCCA', title:'No recovery data yet',
                        sub:'Once carts are detected and recovery emails go out, your recovery rate, revenue recovered, funnel and A/B results show up here.' }));
            }

            /* KPI cards */
            var kpis = [
                { l:'Revenue recovered', v:tcFmtMoney(k.revenue_recovered, cur), c:TC.success, sub:(k.avg_recovered_order ? tcFmtMoney(k.avg_recovered_order, cur) + ' avg order' : null) },
                { l:'Recovery rate',     v:(num(k.recovery_rate)).toFixed(1) + '%', c:TC.accent, sub:(k.recovered||0) + ' of ' + (k.tracked||0) + ' carts' },
                { l:'Carts recovered',   v:String(k.recovered || 0), c:textColor, sub:(k.tracked||0) + ' tracked' },
                { l:'Emails sent',       v:String(k.emails_sent || 0), c:textColor, sub:null },
            ];
            var kpiGrid = h('div', { style:{ display:'grid', gridTemplateColumns:'repeat(4,1fr)', gap:S.md, marginBottom:S.xl } },
                kpis.map(function(s) {
                    return h(Card, { key:s.l, cardBg:cardBg, borderColor:borderColor, dark:dark, style:{ padding:S.lg } },
                        h('div', { style:{ fontSize:11, color:mutedColor, fontWeight:500, marginBottom:S.xs, textTransform:'uppercase', letterSpacing:'.04em' } }, s.l),
                        h('div', { style:{ fontSize:24, fontWeight:700, color:s.c } }, s.v),
                        s.sub ? h('div', { style:{ fontSize:11, color:mutedColor, marginTop:2 } }, s.sub) : null
                    );
                })
            );

            /* Revenue-recovered chart (daily SVG columns) */
            var maxRev = ts.reduce(function(m, d){ return Math.max(m, num(d.revenue)); }, 0);
            function chart() {
                var n = ts.length;
                if (!n) return h('div', { style:{ color:mutedColor, fontSize:13 } }, 'No data in range.');
                var W = 760, H = 170, padT = 10, padB = 22, padX = 6;
                var innerH = H - padT - padB;
                var step = (W - padX*2) / n;
                var barW = Math.max(2, step * 0.6);
                var rects = ts.map(function(d, i) {
                    var v = num(d.revenue);
                    var bh = maxRev > 0 ? (v / maxRev) * innerH : 0;
                    var x = padX + i*step + (step - barW)/2;
                    var y = padT + innerH - bh;
                    return h('rect', { key:i, x:x, y:y, width:barW, height:Math.max(0, bh), rx:1.5,
                        fill: v > 0 ? TC.success : (dark?'rgba(255,255,255,.06)':'#F0F0F0') },
                        h('title', null, d.date + '  \u00B7  ' + tcFmtMoney(v, cur) + '  \u00B7  ' + (d.recovered||0) + ' recovered, ' + (d.detected||0) + ' detected'));
                });
                var ticks = Math.min(6, n);
                var labels = [];
                for (var t = 0; t < ticks; t++) {
                    var idx = Math.round(t * (n - 1) / (ticks - 1 || 1));
                    var lx = padX + idx*step + step/2;
                    labels.push(h('text', { key:'t'+t, x:lx, y:H-6, fontSize:9, fill:mutedColor, textAnchor:'middle' }, (ts[idx].date || '').slice(5)));
                }
                var baseline = h('line', { x1:padX, y1:padT+innerH+0.5, x2:W-padX, y2:padT+innerH+0.5, stroke:borderColor, strokeWidth:1 });
                return h('svg', { viewBox:'0 0 '+W+' '+H, width:'100%', style:{ display:'block', height:'auto' } }, rects, baseline, labels);
            }
            var chartCard = h(Card, { cardBg:cardBg, borderColor:borderColor, dark:dark, style:{ padding:S.xl, marginBottom:S.xl } },
                h('div', { style:{ display:'flex', justifyContent:'space-between', alignItems:'baseline', marginBottom:S.md } },
                    h('h3', { style:{ fontSize:15, fontWeight:600, margin:0 } }, 'Revenue recovered'),
                    h('span', { style:{ fontSize:11, color:mutedColor } }, 'Daily, by recovery date')),
                chart()
            );

            /* Funnel */
            var FUNNEL_C = { detected:mutedColor, emailed:TC.accent, clicked:TC.amber, recovered:TC.success };
            var top = (funnel[0] && funnel[0].count) || 0;
            function funnelCard() {
                return h(Card, { cardBg:cardBg, borderColor:borderColor, dark:dark, style:{ padding:S.xl } },
                    h('h3', { style:{ fontSize:15, fontWeight:600, marginBottom:S.lg } }, 'Recovery funnel'),
                    funnel.map(function(stg, i) {
                        var pct = top > 0 ? Math.round(stg.count / top * 100) : 0;
                        var prev = i > 0 ? funnel[i-1].count : null;
                        var conv = (prev && prev > 0) ? Math.round(stg.count / prev * 100) : null;
                        var c = FUNNEL_C[stg.key] || TC.accent;
                        return h('div', { key:stg.key, style:{ marginBottom:S.md } },
                            h('div', { style:{ display:'flex', justifyContent:'space-between', fontSize:12, marginBottom:4 } },
                                h('span', { style:{ fontWeight:500 } }, stg.stage),
                                h('span', { style:{ color:mutedColor } },
                                    String(stg.count),
                                    conv !== null ? h('span', { style:{ marginLeft:6 } }, '(' + conv + '% of prev)') : null)),
                            h('div', { style:{ height:10, background:dark?'#262A36':'#f0f0f0', borderRadius:5, overflow:'hidden' } },
                                h('div', { style:{ width:Math.max(pct, stg.count>0?2:0)+'%', height:'100%', background:c, borderRadius:5, transition:'width .3s ease' } }))
                        );
                    })
                );
            }

            /* Status breakdown */
            function statusCard() {
                var keys = Object.keys(sb);
                var total = keys.reduce(function(a, kk){ return a + sb[kk]; }, 0) || 1;
                return h(Card, { cardBg:cardBg, borderColor:borderColor, dark:dark, style:{ padding:S.xl } },
                    h('h3', { style:{ fontSize:15, fontWeight:600, marginBottom:S.lg } }, 'Status breakdown'),
                    keys.length ? keys.map(function(st) {
                        var count = sb[st];
                        var pct = Math.round(count / total * 100);
                        var m = RSTATUS[st] || { l:st, c:mutedColor };
                        return h('div', { key:st, style:{ display:'flex', alignItems:'center', gap:S.sm, marginBottom:S.md } },
                            h('div', { style:{ width:96, fontSize:12, fontWeight:500 } }, m.l),
                            h('div', { style:{ flex:1, height:8, background:dark?'#262A36':'#f0f0f0', borderRadius:4, overflow:'hidden' } },
                                h('div', { style:{ width:Math.max(pct, count>0?2:0)+'%', height:'100%', background:m.c, borderRadius:4 } })),
                            h('span', { style:{ fontSize:12, fontWeight:600, color:m.c, width:40, textAlign:'right' } }, count));
                    }) : h('div', { style:{ color:mutedColor, fontSize:13 } }, 'No data')
                );
            }

            /* A/B variants */
            function variantsCard() {
                var multi = variants.length > 1;
                return h(Card, { cardBg:cardBg, borderColor:borderColor, dark:dark, style:{ padding:S.xl } },
                    h('div', { style:{ display:'flex', justifyContent:'space-between', alignItems:'baseline', marginBottom:S.lg } },
                        h('h3', { style:{ fontSize:15, fontWeight:600, margin:0 } }, 'A/B variant results'),
                        h('span', { style:{ fontSize:11, color:mutedColor } }, 'Sent emails by variant')),
                    variants.length ? h('table', { style:{ width:'100%', borderCollapse:'collapse', fontSize:13 } },
                        h('thead', null, h('tr', { style:{ textAlign:'left', color:mutedColor, fontSize:11, textTransform:'uppercase', letterSpacing:'.04em' } },
                            h('th', { style:{ padding:'0 0 8px' } }, 'Variant'),
                            h('th', { style:{ padding:'0 0 8px', textAlign:'right' } }, 'Sent'),
                            h('th', { style:{ padding:'0 0 8px', textAlign:'right' } }, 'Clicked'),
                            h('th', { style:{ padding:'0 0 8px', textAlign:'right' } }, 'Click rate'),
                            h('th', { style:{ padding:'0 0 8px', textAlign:'right' } }, 'Recovered'))),
                        h('tbody', null, variants.map(function(v) {
                            return h('tr', { key:v.variant, style:{ borderTop:'1px solid '+borderColor } },
                                h('td', { style:{ padding:'8px 0', fontWeight:600 } }, 'Variant ' + v.variant),
                                h('td', { style:{ padding:'8px 0', textAlign:'right' } }, String(v.sent)),
                                h('td', { style:{ padding:'8px 0', textAlign:'right' } }, String(v.clicked)),
                                h('td', { style:{ padding:'8px 0', textAlign:'right' } }, num(v.click_rate).toFixed(1) + '%'),
                                h('td', { style:{ padding:'8px 0', textAlign:'right', fontWeight:600, color:TC.success } }, String(v.recovered)));
                        }))
                    ) : h('div', { style:{ color:mutedColor, fontSize:13 } }, 'No emails sent in this range yet.'),
                    !multi ? h('div', { style:{ fontSize:11, color:mutedColor, marginTop:S.md } },
                        'Add a second email variant on any step in the Sequence editor to start A/B testing.') : null
                );
            }

            return h('div', null,
                licenseBanner(),
                header,
                kpiGrid,
                chartCard,
                h('div', { style:{ display:'grid', gridTemplateColumns:'1fr 1fr', gap:S.lg, marginBottom:S.xl } },
                    funnelCard(),
                    statusCard()
                ),
                variantsCard()
            );
        }

        /* ── Which view? ── */
        var tab = 'recoveries';
        if (subId === 'sequence')         tab = 'sequence';
        else if (subId === 'analytics')   tab = 'analytics';
        else if (subId === 'cr-settings') tab = 'settings';
        else if (subId && subId !== '')   tab = 'detail';

        var tabs = [
            { v:'recoveries', l:'Recoveries', slug:'cartrecovery' },
            { v:'analytics',  l:'Analytics',  slug:'cartrecovery/analytics' },
            { v:'sequence',   l:'Sequence',   slug:'cartrecovery/sequence' },
            { v:'settings',   l:'Settings',   slug:'cartrecovery/cr-settings' },
        ];

        return h(PageWrap, { title:'Cart Recovery', sub:'Win back abandoned carts with an automated email drip.', mutedColor:mutedColor },

            tab !== 'detail' && h('div', { style:{ display:'flex', gap:S.xs, marginBottom:S.xl, borderBottom:'1px solid '+borderColor, paddingBottom:1 } },
                tabs.map(function(t) {
                    var a = tab === t.v;
                    return h('button', { key:t.v, onClick:function(){ navigate(t.slug); },
                        style:{ padding:'10px 18px', border:'none', cursor:'pointer', fontSize:13, fontWeight:a?600:400,
                            background:'transparent', color:a?TC.accent:mutedColor, borderBottom:a?'2px solid '+TC.accent:'2px solid transparent',
                            marginBottom:-1, transition:'all .15s' } }, t.l);
                })
            ),

            tab === 'recoveries' ? h(RecoveriesList)
            : tab === 'detail'    ? h(RecoveryDetail, { id: subId })
            : tab === 'analytics' ? h(Analytics)
            : tab === 'sequence'  ? h(SequenceConfig)
            : tab === 'settings'  ? h(Settings)
            : null
        );
    }

    /* ════════════════════════════════════════════════════════════════════
     * SMS NOTIFICATIONS PAGE — SMS Notifications admin (S231, Part 2)
     *
     * Growth → SMS Notifications. Gated into the menu by Config.smsNotificationsInstalled;
     * the master enable toggle is gated by Config.smsNotificationsLicenseActive.
     *
     * Four views, dispatched off the hash subId (parse() → { route, subId }):
     *   #/smsnotifications               → Overview     (stats KPIs + per-event breakdown)
     *   #/smsnotifications/log           → MessageLog   (paginated send log + filters)
     *   #/smsnotifications/subscribers   → Subscribers  (paginated opt-in registry + filter)
     *   #/smsnotifications/sms-settings  → Settings     (provider + creds + events + opt-in + test send)
     *
     * Like DataFeeds/Cart Recovery/Subscriptions, each view is its own inner component
     * that owns its own state, so keystrokes never re-render this parent (focus-safe).
     * The parent holds NO state — `tab` is derived purely from subId.
     *
     * REST namespace: titancart-sms/v1. Envelope { success, data, meta?, error? }.
     * No JSX — h()/wp.element.createElement throughout; module-scope helpers w/ ctx.
     * ════════════════════════════════════════════════════════════════════ */
    function SmsNotificationsPage({ subId, dark, cardBg, textColor, mutedColor, borderColor, tableHeaderBg }) {
        var fs   = fieldSty(dark, borderColor);
        var selS = Object.assign({}, fs, { cursor:'pointer', appearance:'auto' });
        var SMS_API = Config.smsNotificationsApiUrl || (Config.apiUrl || '').replace('/titancart/v1', '/titancart-sms/v1');
        var licenseActive = !!Config.smsNotificationsLicenseActive;

        /* Envelope-aware fetch against the SMS REST namespace. */
        function smsApiFetch(path, opts) {
            opts = opts || {};
            opts.headers = Object.assign({ 'Content-Type':'application/json', 'X-WP-Nonce': Config.nonce }, opts.headers || {});
            if (opts.body && typeof opts.body === 'object') opts.body = JSON.stringify(opts.body);
            return fetch(SMS_API + path, opts).then(function(r) {
                return r.text().then(function(text) {
                    var data = null;
                    if (text) { try { data = JSON.parse(text); } catch(e) { data = null; } }
                    if (!r.ok || (data && data.success === false)) {
                        var m = (data && data.error && data.error.message) || (data && data.message) || ('HTTP ' + r.status);
                        var err = new Error(m); err.status = r.status; err.data = data;
                        err.code = data && data.error && data.error.code;
                        return Promise.reject(err);
                    }
                    return data;
                });
            });
        }

        function asBool(v) { return v === '1' || v === 1 || v === true; }
        function unwrap(d) { return (d && d.data !== undefined) ? d.data : d; }
        function num(v) { var n = parseFloat(v); return isNaN(n) ? 0 : n; }
        function fmtDate(s) {
            if (!s) return '\u2014';
            var t = (String(s).indexOf('T') < 0) ? String(s).replace(' ', 'T') + 'Z' : s;
            var d = new Date(t);
            if (isNaN(d.getTime())) return s;
            return d.toLocaleString();
        }

        /* ── Shared lookups ── */
        var MSTATUS = {
            queued:  { l:'Queued',  c:mutedColor, bg: dark?'rgba(255,255,255,.06)':'#F5F5F5' },
            sent:    { l:'Sent',    c:TC.success, bg: dark?'rgba(22,163,74,.12)':TC.successBg },
            failed:  { l:'Failed',  c:TC.error,   bg: dark?'rgba(220,38,38,.12)':TC.errorBg },
            skipped: { l:'Skipped', c:TC.amber,   bg: dark?'rgba(217,119,6,.12)':TC.amberBg },
        };
        var SUBSTATUS = {
            subscribed:   { l:'Subscribed',   c:TC.success, bg: dark?'rgba(22,163,74,.12)':TC.successBg },
            unsubscribed: { l:'Unsubscribed', c:mutedColor, bg: dark?'rgba(255,255,255,.06)':'#F5F5F5' },
        };
        function pill(map, st) {
            var m = map[st] || { l: st || '\u2014', c:mutedColor, bg: dark?'rgba(255,255,255,.06)':'#F5F5F5' };
            return h('span', { style:{ fontSize:11, fontWeight:600, padding:'2px 8px', borderRadius:99, color:m.c, background:m.bg, display:'inline-block', whiteSpace:'nowrap' } }, m.l);
        }
        var EVENT_LABEL = {
            order_placed:'Order placed', order_shipped:'Order shipped', order_delivered:'Order delivered',
            low_stock:'Low stock', failed_payment:'Failed payment', test:'Test', manual:'Manual',
        };
        function eventLabel(e) { return EVENT_LABEL[e] || e || '\u2014'; }

        /* Settings event maps (toggle/template/status keys differ by event). */
        var CUSTOMER_EVENTS = [
            { key:'order_placed',    label:'Order placed',    toggle:'ev_order_placed_enabled',    tpl:'ev_order_placed_template' },
            { key:'order_shipped',   label:'Order shipped',   toggle:'ev_order_shipped_enabled',   tpl:'ev_order_shipped_template',   statuses:'shipped_statuses',   statusLabel:'Shipped status handles',   statusNote:'Comma-separated order-status handles that count as \u201cshipped\u201d. Add a Shipped status under Sales \u2192 Order Statuses, then list its handle here.' },
            { key:'order_delivered', label:'Order delivered', toggle:'ev_order_delivered_enabled', tpl:'ev_order_delivered_template', statuses:'delivered_statuses', statusLabel:'Delivered status handles', statusNote:'Comma-separated order-status handles that count as \u201cdelivered\u201d.' },
        ];
        var ADMIN_EVENTS = [
            { key:'low_stock',      label:'Low-stock alert',      toggle:'ev_low_stock_enabled',      tpl:'tpl_low_stock',      note:'Hourly sweep texts your admin number once when a tracked product drops to/below the catalog low-stock threshold (re-arms on restock).' },
            { key:'failed_payment', label:'Failed-payment alert', toggle:'ev_failed_payment_enabled', tpl:'tpl_failed_payment', note:'Texts your admin number when a payment fails for an order.' },
        ];

        /* Banner shown above every view while unlicensed. */
        function licenseBanner() {
            if (licenseActive) return null;
            return h('div', { style:{ display:'flex', gap:S.sm, alignItems:'flex-start', padding:'12px 16px', borderRadius:8,
                    background: dark?'rgba(217,119,6,.12)':TC.amberBg, border:'1px solid '+TC.amber+'40', marginBottom:S.lg } },
                h('span', { style:{ color:TC.amber, fontWeight:600, fontSize:13 } }, '\u26A0'),
                h('div', { style:{ fontSize:13, color:textColor } },
                    h('strong', null, 'License required. '),
                    'SMS Notifications is installed but not licensed. You can configure a provider, templates, and the opt-in copy below, but SMS only sends once a valid license is active and the master switch is on.')
            );
        }
        function flash(msg) {
            if (!msg) return null;
            return h('div', { style:{ padding:'10px 16px', borderRadius:8, marginBottom:S.lg, fontSize:13, fontWeight:500,
                    background: msg.type === 'success' ? (dark?'rgba(22,163,74,.12)':TC.successBg) : (dark?'rgba(220,38,38,.12)':TC.errorBg),
                    color: msg.type === 'success' ? TC.success : TC.error,
                    border:'1px solid ' + (msg.type === 'success' ? TC.success : TC.error) + '30' } }, msg.text);
        }

        /* ══════════════════════════════════════════════════════════════════
         * VIEW: OVERVIEW (stats dashboard)
         * ══════════════════════════════════════════════════════════════════ */
        function Overview() {
            var [loading, setLoading] = useState(true);
            var [stats, setStats] = useState(null);
            var [err, setErr] = useState('');

            useEffect(function() {
                var alive = true;
                setLoading(true); setErr('');
                smsApiFetch('/stats')
                    .then(function(d) { if (alive) { setStats(unwrap(d) || {}); setLoading(false); } })
                    .catch(function(e) { if (alive) { setErr(e.message || 'Failed to load stats.'); setLoading(false); } });
                return function() { alive = false; };
            }, []);

            function tile(label, value, color) {
                return h('div', { style:{ flex:'1 1 150px', minWidth:140, background:cardBg, border:'1px solid '+borderColor, borderRadius:12, padding:'16px 18px' } },
                    h('div', { style:{ fontSize:26, fontWeight:700, color: color || textColor, fontVariantNumeric:'tabular-nums', lineHeight:1.1 } }, String(value)),
                    h('div', { style:{ fontSize:12, color:mutedColor, marginTop:4, textTransform:'uppercase', letterSpacing:'.3px' } }, label)
                );
            }

            if (loading) return h('div', null, licenseBanner(), h(Spinner, { mutedColor:mutedColor }));
            if (err) return h('div', null, licenseBanner(), flash({ type:'error', text:err }));

            var s = stats || {};
            var byEvent = s.by_event || {};
            var byKeys = Object.keys(byEvent);

            return h('div', null,
                licenseBanner(),

                h('div', { style:{ display:'flex', gap:S.md, flexWrap:'wrap', marginBottom:S.xl } },
                    tile('Sent',        num(s.sent),    TC.success),
                    tile('Failed',      num(s.failed),  num(s.failed) > 0 ? TC.error : textColor),
                    tile('Skipped',     num(s.skipped), num(s.skipped) > 0 ? TC.amber : textColor),
                    tile('Subscribers', num(s.subscribers), TC.accent),
                    tile('Total log',   num(s.total))
                ),

                h('div', { style:{ background:cardBg, border:'1px solid '+borderColor, borderRadius:12, padding:'8px '+S.xl+'px '+S.xl+'px' } },
                    h(SectionHead, { id:'byevent', title:'Messages by event', ctx:{ mutedColor:mutedColor, borderColor:borderColor, textColor:textColor, open:{ byevent:true }, onToggle:function(){} } }),
                    byKeys.length === 0
                        ? h('div', { style:{ fontSize:13, color:mutedColor, padding:'8px 0 4px' } }, 'No messages logged yet. Once SMS is enabled and events fire, a per-event breakdown shows here.')
                        : h('table', { style:{ width:'100%', borderCollapse:'collapse', fontSize:13, marginTop:S.xs } },
                            h('tbody', null,
                                byKeys.map(function(k) {
                                    return h('tr', { key:k, style:{ borderTop:'1px solid '+borderColor } },
                                        h('td', { style:{ padding:'10px 4px', color:textColor } }, eventLabel(k)),
                                        h('td', { style:{ padding:'10px 4px', textAlign:'right', color:mutedColor, fontVariantNumeric:'tabular-nums' } }, String(num(byEvent[k])))
                                    );
                                })
                            )
                        )
                )
            );
        }

        /* ══════════════════════════════════════════════════════════════════
         * VIEW: MESSAGE LOG
         * ══════════════════════════════════════════════════════════════════ */
        function MessageLog() {
            var [loading, setLoading] = useState(true);
            var [rows, setRows] = useState([]);
            var [meta, setMeta] = useState(null);
            var [err, setErr] = useState('');
            var [evFilter, setEvFilter] = useState('all');
            var [stFilter, setStFilter] = useState('all');
            var [page, setPage] = useState(1);

            function load() {
                setLoading(true); setErr('');
                var qs = '?page=' + page + '&per_page=25'
                    + (evFilter !== 'all' ? '&event_type=' + encodeURIComponent(evFilter) : '')
                    + (stFilter !== 'all' ? '&status=' + encodeURIComponent(stFilter) : '');
                smsApiFetch('/messages' + qs).then(function(d) {
                    setRows(unwrap(d) || []);
                    setMeta((d && d.meta) || null);
                    setLoading(false);
                }).catch(function(e) { setErr(e.message || 'Failed to load messages.'); setLoading(false); });
            }
            useEffect(function(){ load(); }, [evFilter, stFilter, page]);

            var total = (meta && meta.total) || 0;
            var totalPages = Math.max(1, Math.ceil(total / 25));

            var EV_OPTS = [['all','All events']].concat(
                ['order_placed','order_shipped','order_delivered','low_stock','failed_payment','test'].map(function(k){ return [k, eventLabel(k)]; })
            );
            var ST_OPTS = [['all','All statuses'],['sent','Sent'],['failed','Failed'],['skipped','Skipped'],['queued','Queued']];

            return h('div', null,
                licenseBanner(),
                err && flash({ type:'error', text:err }),

                /* Filters */
                h('div', { style:{ display:'flex', gap:S.sm, flexWrap:'wrap', marginBottom:S.lg, alignItems:'center' } },
                    h('select', { value:evFilter, onChange:function(e){ setPage(1); setEvFilter(e.target.value); }, style:Object.assign({}, selS, { width:'auto', minWidth:160 }) },
                        EV_OPTS.map(function(o){ return h('option', { key:o[0], value:o[0] }, o[1]); })),
                    h('select', { value:stFilter, onChange:function(e){ setPage(1); setStFilter(e.target.value); }, style:Object.assign({}, selS, { width:'auto', minWidth:140 }) },
                        ST_OPTS.map(function(o){ return h('option', { key:o[0], value:o[0] }, o[1]); })),
                    total > 0 && h('span', { style:{ fontSize:12, color:mutedColor, marginLeft:'auto' } }, total + (total === 1 ? ' message' : ' messages'))
                ),

                loading ? h(Spinner, { mutedColor:mutedColor })
                : rows.length === 0 ? h(EmptyState, { mutedColor:mutedColor, icon:'\uD83D\uDCAC', title:'No messages logged',
                        sub:'Sent, skipped, and failed SMS attempts appear here once events fire.' })
                : h('div', { style:{ background:cardBg, border:'1px solid '+borderColor, borderRadius:12, overflow:'hidden' } },
                    h('table', { style:{ width:'100%', borderCollapse:'collapse', fontSize:13 } },
                        h('thead', null,
                            h('tr', { style:{ background:tableHeaderBg, textAlign:'left', color:mutedColor } },
                                ['When','Event','Recipient','Provider','Status','Note'].map(function(c, i) {
                                    return h('th', { key:i, style:{ padding:'10px 16px', fontWeight:600, fontSize:11, textTransform:'uppercase', letterSpacing:'.3px' } }, c);
                                })
                            )
                        ),
                        h('tbody', null,
                            rows.map(function(r) {
                                var note = r.error || (r.body ? (String(r.body).length > 60 ? String(r.body).slice(0, 60) + '\u2026' : r.body) : '');
                                var noteColor = r.error ? (r.status === 'failed' ? TC.error : mutedColor) : mutedColor;
                                return h('tr', { key:r.id, style:{ borderTop:'1px solid '+borderColor } },
                                    h('td', { style:{ padding:'11px 16px', color:mutedColor, whiteSpace:'nowrap' } }, fmtDate(r.created_at)),
                                    h('td', { style:{ padding:'11px 16px', color:textColor } },
                                        eventLabel(r.event_type),
                                        r.recipient_role === 'admin' && h('span', { style:{ fontSize:10, color:mutedColor, marginLeft:6, textTransform:'uppercase', letterSpacing:'.3px' } }, 'admin')),
                                    h('td', { style:{ padding:'11px 16px', color:textColor, fontVariantNumeric:'tabular-nums', whiteSpace:'nowrap' } }, r.to_number || h('span', { style:{ color:mutedColor, fontStyle:'italic' } }, 'none')),
                                    h('td', { style:{ padding:'11px 16px', color:mutedColor } }, r.provider || '\u2014'),
                                    h('td', { style:{ padding:'11px 16px' } }, pill(MSTATUS, r.status)),
                                    h('td', { style:{ padding:'11px 16px', color:noteColor, maxWidth:280, overflow:'hidden', textOverflow:'ellipsis', whiteSpace:'nowrap' } }, note || '\u2014')
                                );
                            })
                        )
                    )
                ),

                totalPages > 1 && h('div', { style:{ display:'flex', justifyContent:'flex-end', marginTop:S.lg } },
                    h(Pager, { page:page, totalPages:totalPages, onPage:setPage, mutedColor:mutedColor, borderColor:borderColor })
                )
            );
        }

        /* ══════════════════════════════════════════════════════════════════
         * VIEW: SUBSCRIBERS (opt-in registry)
         * ══════════════════════════════════════════════════════════════════ */
        function Subscribers() {
            var [loading, setLoading] = useState(true);
            var [rows, setRows] = useState([]);
            var [meta, setMeta] = useState(null);
            var [err, setErr] = useState('');
            var [filter, setFilter] = useState('all');
            var [page, setPage] = useState(1);

            function load() {
                setLoading(true); setErr('');
                var qs = '?page=' + page + '&per_page=25' + (filter !== 'all' ? '&status=' + encodeURIComponent(filter) : '');
                smsApiFetch('/subscribers' + qs).then(function(d) {
                    setRows(unwrap(d) || []);
                    setMeta((d && d.meta) || null);
                    setLoading(false);
                }).catch(function(e) { setErr(e.message || 'Failed to load subscribers.'); setLoading(false); });
            }
            useEffect(function(){ load(); }, [filter, page]);

            var total = (meta && meta.total) || 0;
            var totalPages = Math.max(1, Math.ceil(total / 25));
            var FILTERS = [['all','All'],['subscribed','Subscribed'],['unsubscribed','Unsubscribed']];
            var SOURCE_LABEL = { checkout:'Checkout', manual:'Manual', import:'Import', api:'API' };

            return h('div', null,
                licenseBanner(),
                err && flash({ type:'error', text:err }),

                h('div', { style:{ display:'flex', gap:S.xs, flexWrap:'wrap', marginBottom:S.lg, alignItems:'center' } },
                    FILTERS.map(function(o) {
                        var a = filter === o[0];
                        return h('button', { key:o[0], onClick:function(){ setPage(1); setFilter(o[0]); },
                            style:{ padding:'6px 14px', borderRadius:99, fontSize:12, fontWeight:500, cursor:'pointer',
                                border:'1px solid ' + (a ? TC.accent : borderColor),
                                background: a ? (dark?'rgba(37,99,235,.12)':TC.accentBg) : 'transparent',
                                color: a ? TC.accent : mutedColor } }, o[1]);
                    }),
                    total > 0 && h('span', { style:{ fontSize:12, color:mutedColor, marginLeft:'auto' } }, total + (total === 1 ? ' subscriber' : ' subscribers'))
                ),

                loading ? h(Spinner, { mutedColor:mutedColor })
                : rows.length === 0 ? h(EmptyState, { mutedColor:mutedColor, icon:'\uD83D\uDCF1', title:'No subscribers yet',
                        sub:'Customers who opt in at checkout (or are imported) appear here. Only opted-in numbers receive customer SMS.' })
                : h('div', { style:{ background:cardBg, border:'1px solid '+borderColor, borderRadius:12, overflow:'hidden' } },
                    h('table', { style:{ width:'100%', borderCollapse:'collapse', fontSize:13 } },
                        h('thead', null,
                            h('tr', { style:{ background:tableHeaderBg, textAlign:'left', color:mutedColor } },
                                ['Phone','Status','Source','Customer','Opted in'].map(function(c, i) {
                                    return h('th', { key:i, style:{ padding:'10px 16px', fontWeight:600, fontSize:11, textTransform:'uppercase', letterSpacing:'.3px' } }, c);
                                })
                            )
                        ),
                        h('tbody', null,
                            rows.map(function(r) {
                                return h('tr', { key:r.id, style:{ borderTop:'1px solid '+borderColor } },
                                    h('td', { style:{ padding:'11px 16px', color:textColor, fontVariantNumeric:'tabular-nums', fontWeight:500, whiteSpace:'nowrap' } }, r.phone || '\u2014'),
                                    h('td', { style:{ padding:'11px 16px' } }, pill(SUBSTATUS, r.status)),
                                    h('td', { style:{ padding:'11px 16px', color:mutedColor } }, SOURCE_LABEL[r.source] || r.source || '\u2014'),
                                    h('td', { style:{ padding:'11px 16px', color:mutedColor } }, (r.customer_id && parseInt(r.customer_id, 10) > 0) ? ('#' + r.customer_id) : h('span', { style:{ fontStyle:'italic' } }, 'Guest')),
                                    h('td', { style:{ padding:'11px 16px', color:mutedColor, whiteSpace:'nowrap' } }, fmtDate(r.consent_at))
                                );
                            })
                        )
                    )
                ),

                totalPages > 1 && h('div', { style:{ display:'flex', justifyContent:'flex-end', marginTop:S.lg } },
                    h(Pager, { page:page, totalPages:totalPages, onPage:setPage, mutedColor:mutedColor, borderColor:borderColor })
                )
            );
        }

        /* ══════════════════════════════════════════════════════════════════
         * VIEW: SETTINGS (provider + creds + events + opt-in + test send)
         * ══════════════════════════════════════════════════════════════════ */
        function Settings() {
            var [loading, setLoading] = useState(true);
            var [form, setForm] = useState(null);
            var [tpls, setTpls] = useState({});
            var [providers, setProviders] = useState([]);
            var [saving, setSaving] = useState(false);
            var [msg, setMsg] = useState(null);
            var [open, setOpen] = useState({ general:true, creds:true, events:true, admin:false, optin:false, test:false });
            var [testPhone, setTestPhone] = useState('');
            var [testing, setTesting] = useState(false);
            var [testResult, setTestResult] = useState(null);

            function toggleSection(sid) { setOpen(function(p){ var n = Object.assign({}, p); n[sid] = !n[sid]; return n; }); }
            var edCtx = { mutedColor:mutedColor, borderColor:borderColor, textColor:textColor, open:open, onToggle:toggleSection };

            function normalize(s) {
                s = s || {};
                return {
                    enabled:                     asBool(s.enabled),
                    provider:                    s.provider || 'twilio',
                    twilio_sid:                  s.twilio_sid || '',
                    twilio_token:                s.twilio_token || '',
                    twilio_from:                 s.twilio_from || '',
                    vonage_key:                  s.vonage_key || '',
                    vonage_secret:               s.vonage_secret || '',
                    vonage_from:                 s.vonage_from || '',
                    ev_order_placed_enabled:     asBool(s.ev_order_placed_enabled),
                    ev_order_placed_template:    s.ev_order_placed_template || '',
                    ev_order_shipped_enabled:    asBool(s.ev_order_shipped_enabled),
                    ev_order_shipped_template:   s.ev_order_shipped_template || '',
                    shipped_statuses:            s.shipped_statuses != null ? s.shipped_statuses : 'shipped',
                    ev_order_delivered_enabled:  asBool(s.ev_order_delivered_enabled),
                    ev_order_delivered_template: s.ev_order_delivered_template || '',
                    delivered_statuses:          s.delivered_statuses != null ? s.delivered_statuses : 'delivered,completed',
                    admin_phone:                 s.admin_phone || '',
                    ev_low_stock_enabled:        asBool(s.ev_low_stock_enabled),
                    tpl_low_stock:               s.tpl_low_stock || '',
                    ev_failed_payment_enabled:   asBool(s.ev_failed_payment_enabled),
                    tpl_failed_payment:          s.tpl_failed_payment || '',
                    checkout_optin_enabled:      asBool(s.checkout_optin_enabled),
                    checkout_optin_default:      asBool(s.checkout_optin_default),
                    checkout_optin_text:         s.checkout_optin_text != null ? s.checkout_optin_text : '',
                };
            }

            function load() {
                setLoading(true);
                Promise.all([
                    smsApiFetch('/settings'),
                    smsApiFetch('/templates').catch(function(){ return null; }),
                    smsApiFetch('/providers').catch(function(){ return null; })
                ]).then(function(res) {
                    var sData = unwrap(res[0]) || {};
                    setForm(normalize(sData.settings || {}));
                    var tData = res[1] ? (unwrap(res[1]) || {}) : {};
                    setTpls(tData.templates || {});
                    var pData = res[2] ? (unwrap(res[2]) || {}) : {};
                    setProviders((pData && pData.providers) || []);
                    setLoading(false);
                }).catch(function(e) { setMsg({ type:'error', text:e.message || 'Failed to load settings.' }); setLoading(false); });
            }
            useEffect(function(){ load(); }, []);
            function set(k, v) { setForm(function(f){ var n = Object.assign({}, f); n[k] = v; return n; }); }

            function save() {
                if (!form) return;
                setSaving(true); setMsg(null);
                var body = {
                    enabled:                     form.enabled ? '1' : '0',
                    provider:                    form.provider,
                    twilio_sid:                  form.twilio_sid,
                    twilio_token:                form.twilio_token,
                    twilio_from:                 form.twilio_from,
                    vonage_key:                  form.vonage_key,
                    vonage_secret:               form.vonage_secret,
                    vonage_from:                 form.vonage_from,
                    ev_order_placed_enabled:     form.ev_order_placed_enabled ? '1' : '0',
                    ev_order_placed_template:    form.ev_order_placed_template,
                    ev_order_shipped_enabled:    form.ev_order_shipped_enabled ? '1' : '0',
                    ev_order_shipped_template:   form.ev_order_shipped_template,
                    shipped_statuses:            form.shipped_statuses,
                    ev_order_delivered_enabled:  form.ev_order_delivered_enabled ? '1' : '0',
                    ev_order_delivered_template: form.ev_order_delivered_template,
                    delivered_statuses:          form.delivered_statuses,
                    admin_phone:                 form.admin_phone,
                    ev_low_stock_enabled:        form.ev_low_stock_enabled ? '1' : '0',
                    tpl_low_stock:               form.tpl_low_stock,
                    ev_failed_payment_enabled:   form.ev_failed_payment_enabled ? '1' : '0',
                    tpl_failed_payment:          form.tpl_failed_payment,
                    checkout_optin_enabled:      form.checkout_optin_enabled ? '1' : '0',
                    checkout_optin_default:      form.checkout_optin_default ? '1' : '0',
                    checkout_optin_text:         form.checkout_optin_text,
                };
                smsApiFetch('/settings', { method:'PUT', body:body }).then(function(d) {
                    var data = unwrap(d) || {};
                    setForm(normalize(data.settings || {}));
                    setMsg({ type:'success', text:'Settings saved.' });
                    setSaving(false);
                }).catch(function(e) {
                    var t = e.message || 'Save failed.';
                    if (e.code === 'tcsms_license_required') t = 'An active license is required to enable SMS sending.';
                    setMsg({ type:'error', text:t });
                    setSaving(false);
                });
            }

            function sendTest() {
                if (!testPhone.trim()) { setTestResult({ ok:false, text:'Enter a phone number to test.' }); return; }
                setTesting(true); setTestResult(null);
                smsApiFetch('/test-send', { method:'POST', body:{ to:testPhone } }).then(function(d) {
                    var r = unwrap(d) || {};
                    if (r.success) {
                        setTestResult({ ok:true, text:'Test SMS sent via ' + (r.provider || 'provider') + '. Check the phone (and the Message log).' });
                    } else {
                        setTestResult({ ok:false, text:'Test ' + (r.status || 'failed') + (r.error ? (': ' + r.error) : '') + (r.provider ? (' (' + r.provider + ')') : '') + '.' });
                    }
                    setTesting(false);
                }).catch(function(e) {
                    setTestResult({ ok:false, text:e.message || 'Test send failed.' });
                    setTesting(false);
                });
            }

            if (loading || !form) return h(Spinner, { mutedColor:mutedColor });

            /* token hint chips for an event key */
            function tokenHints(eventKey) {
                var t = (tpls[eventKey] && tpls[eventKey].tokens) || [];
                if (!t.length) return null;
                return h('div', { style:{ display:'flex', gap:6, flexWrap:'wrap', marginTop:6 } },
                    t.map(function(tok) {
                        return h('code', { key:tok, style:{ fontSize:11, padding:'2px 7px', borderRadius:6, background: dark?'rgba(255,255,255,.06)':'#F5F5F5', color:mutedColor, fontFamily:'ui-monospace, Menlo, Consolas, monospace' } }, '{' + tok + '}');
                    })
                );
            }
            function tplPlaceholder(eventKey) {
                return (tpls[eventKey] && tpls[eventKey].default) ? tpls[eventKey].default : 'Leave blank to use the built-in default.';
            }

            /* a customer/admin event block (toggle + template + tokens [+ status handles]) */
            function eventBlock(ev) {
                return h('div', { key:ev.key, style:{ borderTop:'1px solid '+borderColor, paddingTop:S.md, marginTop:S.md } },
                    h(FR, { label:ev.label, note: ev.note || 'Toggle this SMS on, then customize the message (or leave it blank for the default).', ctx:edCtx },
                        h('label', { style:{ display:'inline-flex', alignItems:'center', gap:S.sm, fontSize:14, cursor:'pointer', paddingTop:6 } },
                            h('input', { type:'checkbox', checked:!!form[ev.toggle], onChange:function(e){ set(ev.toggle, e.target.checked); }, style:{ accentColor:TC.accent, width:16, height:16 } }),
                            h('span', null, form[ev.toggle] ? 'On' : 'Off'))),
                    form[ev.toggle] && h('div', { style:{ paddingLeft:2 } },
                        ev.statuses && h(FR, { label:ev.statusLabel, note:ev.statusNote, ctx:edCtx },
                            h('input', { type:'text', value:form[ev.statuses], placeholder:'shipped', onChange:function(e){ set(ev.statuses, e.target.value); }, style:Object.assign({}, fs, { fontFamily:'ui-monospace, Menlo, Consolas, monospace' }) })),
                        h(FR, { label:'Message', note:'Plain-text SMS. Blank uses the default.', ctx:edCtx },
                            h('textarea', { value:form[ev.tpl], placeholder:tplPlaceholder(ev.key), onChange:function(e){ set(ev.tpl, e.target.value); },
                                rows:3, style:Object.assign({}, fs, { resize:'vertical', lineHeight:1.5, fontFamily:'inherit' }) }),
                            tokenHints(ev.key))
                    )
                );
            }

            var provider = form.provider;
            var currentProviderMeta = providers.find ? providers.find(function(p){ return p.slug === provider; }) : null;

            return h('div', null,
                licenseBanner(),
                flash(msg),
                h('div', { style:{ background:cardBg, border:'1px solid '+borderColor, borderRadius:12, padding:'8px '+S.xl+'px '+S.xl+'px' } },

                    /* General */
                    h(SectionHead, { id:'general', title:'General', ctx:edCtx }),
                    open.general && h('div', null,
                        h(FR, { label:'Enable SMS Notifications', note: !licenseActive ? 'Requires an active license to enable.' : 'Master switch. When off, no SMS is sent (order listeners, low-stock sweep, and opt-in capture all pause).', ctx:edCtx },
                            h('label', { style:{ display:'inline-flex', alignItems:'center', gap:S.sm, fontSize:14, cursor: licenseActive ? 'pointer' : 'not-allowed', paddingTop:6, opacity: licenseActive ? 1 : 0.55 } },
                                h('input', { type:'checkbox', checked:!!form.enabled, disabled:!licenseActive, onChange:function(e){ set('enabled', e.target.checked); },
                                    style:{ accentColor:TC.accent, cursor: licenseActive ? 'pointer' : 'not-allowed', width:16, height:16 } }),
                                h('span', null, form.enabled ? 'Enabled' : 'Disabled'))),
                        h(FR, { label:'SMS provider', note:'Which gateway sends your texts. Twilio is the verified path; Vonage is beta; Mock logs without sending (testing only).', ctx:edCtx },
                            h('select', { value:provider, onChange:function(e){ set('provider', e.target.value); }, style:Object.assign({}, selS, { maxWidth:280 }) },
                                h('option', { value:'twilio' }, 'Twilio'),
                                h('option', { value:'vonage' }, 'Vonage (beta)'),
                                h('option', { value:'mock' }, 'Mock (no send \u2014 testing)')),
                            currentProviderMeta && h('div', { style:{ fontSize:12, marginTop:6, color: currentProviderMeta.configured ? TC.success : TC.amber } },
                                currentProviderMeta.configured ? ('\u2713 ' + currentProviderMeta.label + ' is configured.') : ('\u26A0 ' + currentProviderMeta.label + ' is not fully configured \u2014 fill in the credentials below.')))
                    ),

                    /* Provider credentials */
                    h(SectionHead, { id:'creds', title:'Provider credentials', ctx:edCtx }),
                    open.creds && h('div', null,
                        provider === 'twilio' && h('div', null,
                            h(FR, { label:'Account SID', note:'Your Twilio Account SID (starts with AC\u2026).', ctx:edCtx },
                                h('input', { type:'text', value:form.twilio_sid, placeholder:'AC\u2026', onChange:function(e){ set('twilio_sid', e.target.value); }, style:Object.assign({}, fs, { fontFamily:'ui-monospace, Menlo, Consolas, monospace' }) })),
                            h(FR, { label:'Auth Token', note:'Your Twilio Auth Token. Stored in your site settings.', ctx:edCtx },
                                h('input', { type:'password', value:form.twilio_token, placeholder:'\u2022\u2022\u2022\u2022\u2022\u2022', autoComplete:'new-password', onChange:function(e){ set('twilio_token', e.target.value); }, style:fs })),
                            h(FR, { label:'From', note:'A purchased +E.164 number (e.g. +15551234567) OR a Messaging Service SID (starts with MG\u2026).', ctx:edCtx },
                                h('input', { type:'text', value:form.twilio_from, placeholder:'+15551234567 or MG\u2026', onChange:function(e){ set('twilio_from', e.target.value); }, style:Object.assign({}, fs, { fontFamily:'ui-monospace, Menlo, Consolas, monospace' }) }))
                        ),
                        provider === 'vonage' && h('div', null,
                            h(FR, { label:'API Key', note:'Your Vonage API key.', ctx:edCtx },
                                h('input', { type:'text', value:form.vonage_key, onChange:function(e){ set('vonage_key', e.target.value); }, style:Object.assign({}, fs, { fontFamily:'ui-monospace, Menlo, Consolas, monospace' }) })),
                            h(FR, { label:'API Secret', note:'Your Vonage API secret.', ctx:edCtx },
                                h('input', { type:'password', value:form.vonage_secret, placeholder:'\u2022\u2022\u2022\u2022\u2022\u2022', autoComplete:'new-password', onChange:function(e){ set('vonage_secret', e.target.value); }, style:fs })),
                            h(FR, { label:'From', note:'An alphanumeric sender ID or a number (country support varies).', ctx:edCtx },
                                h('input', { type:'text', value:form.vonage_from, placeholder:'YourStore', onChange:function(e){ set('vonage_from', e.target.value); }, style:fs }))
                        ),
                        provider === 'mock' && h('div', { style:{ fontSize:13, color:mutedColor, padding:'6px 0 8px' } },
                            'The Mock provider logs each message as \u201csent\u201d with a synthetic SID and makes no real network call \u2014 useful for exercising the pipeline without credentials. Switch to Twilio or Vonage to send real texts.')
                    ),

                    /* Order events */
                    h(SectionHead, { id:'events', title:'Order update messages', ctx:edCtx }),
                    open.events && h('div', null,
                        h('div', { style:{ fontSize:13, color:mutedColor, marginBottom:S.xs } },
                            'Customer texts. These only send to numbers that have opted in (subscribed) \u2014 anything else is logged as \u201cskipped\u201d for your records.'),
                        CUSTOMER_EVENTS.map(function(ev){ return eventBlock(ev); })
                    ),

                    /* Admin alerts */
                    h(SectionHead, { id:'admin', title:'Admin alerts', ctx:edCtx }),
                    open.admin && h('div', null,
                        h(FR, { label:'Admin phone', note:'Operational alerts (low stock, failed payment) text this number. +E.164 format recommended.', ctx:edCtx },
                            h('input', { type:'text', value:form.admin_phone, placeholder:'+15551234567', onChange:function(e){ set('admin_phone', e.target.value); }, style:Object.assign({}, fs, { fontFamily:'ui-monospace, Menlo, Consolas, monospace' }) })),
                        ADMIN_EVENTS.map(function(ev){ return eventBlock(ev); })
                    ),

                    /* Checkout opt-in */
                    h(SectionHead, { id:'optin', title:'Checkout opt-in', ctx:edCtx }),
                    open.optin && h('div', null,
                        h(FR, { label:'Show opt-in at checkout', note:'Adds an SMS opt-in checkbox + phone field to the checkout so customers can consent to order-update texts.', ctx:edCtx },
                            h('label', { style:{ display:'inline-flex', alignItems:'center', gap:S.sm, fontSize:14, cursor:'pointer', paddingTop:6 } },
                                h('input', { type:'checkbox', checked:!!form.checkout_optin_enabled, onChange:function(e){ set('checkout_optin_enabled', e.target.checked); }, style:{ accentColor:TC.accent, width:16, height:16 } }),
                                h('span', null, form.checkout_optin_enabled ? 'Shown' : 'Hidden'))),
                        form.checkout_optin_enabled && h('div', null,
                            h(FR, { label:'Pre-checked', note:'Whether the box starts ticked. Off (explicit consent) is the compliant default in most regions.', ctx:edCtx },
                                h('label', { style:{ display:'inline-flex', alignItems:'center', gap:S.sm, fontSize:14, cursor:'pointer', paddingTop:6 } },
                                    h('input', { type:'checkbox', checked:!!form.checkout_optin_default, onChange:function(e){ set('checkout_optin_default', e.target.checked); }, style:{ accentColor:TC.accent, width:16, height:16 } }),
                                    h('span', null, form.checkout_optin_default ? 'Pre-checked' : 'Unchecked'))),
                            h(FR, { label:'Consent text', note:'The label shown next to the checkbox.', ctx:edCtx },
                                h('textarea', { value:form.checkout_optin_text, placeholder:'Text me order updates by SMS. Message & data rates may apply.', onChange:function(e){ set('checkout_optin_text', e.target.value); },
                                    rows:2, style:Object.assign({}, fs, { resize:'vertical', lineHeight:1.5, fontFamily:'inherit' }) })))
                    ),

                    /* Test send */
                    h(SectionHead, { id:'test', title:'Send a test', ctx:edCtx }),
                    open.test && h('div', null,
                        h(FR, { label:'Test phone number', note:'Sends a one-off test SMS through the configured provider (no opt-in required). Save your provider credentials first.', ctx:edCtx },
                            h('div', { style:{ display:'flex', gap:S.sm, alignItems:'center', flexWrap:'wrap' } },
                                h('input', { type:'text', value:testPhone, placeholder:'+15551234567', onChange:function(e){ setTestPhone(e.target.value); }, style:Object.assign({}, fs, { width:220, fontFamily:'ui-monospace, Menlo, Consolas, monospace' }) }),
                                h(Btn, { variant:'secondary', onClick:sendTest, disabled:testing, mutedColor:mutedColor, borderColor:borderColor }, testing ? 'Sending\u2026' : 'Send test'))),
                        testResult && h('div', { style:{ marginTop:S.sm, fontSize:13, fontWeight:500, color: testResult.ok ? TC.success : TC.error } },
                            (testResult.ok ? '\u2713 ' : '\u26A0 ') + testResult.text)
                    ),

                    /* Save */
                    h('div', { style:{ display:'flex', justifyContent:'flex-end', marginTop:S.xl, paddingTop:S.lg, borderTop:'1px solid '+borderColor } },
                        h(Btn, { variant:'primary', onClick:save, disabled:saving || !form, mutedColor:mutedColor, borderColor:borderColor }, saving ? 'Saving\u2026' : 'Save settings')
                    )
                )
            );
        }

        /* ── Which view? ── */
        var tab = 'overview';
        if (subId === 'log')              tab = 'log';
        else if (subId === 'subscribers') tab = 'subscribers';
        else if (subId === 'sms-settings') tab = 'settings';

        var tabs = [
            { v:'overview',    l:'Overview',    slug:'smsnotifications' },
            { v:'log',         l:'Message log', slug:'smsnotifications/log' },
            { v:'subscribers', l:'Subscribers', slug:'smsnotifications/subscribers' },
            { v:'settings',    l:'Settings',    slug:'smsnotifications/sms-settings' },
        ];

        return h(PageWrap, { title:'SMS Notifications', sub:'Text customers order updates and alert yourself to low stock \u2014 via Twilio or Vonage.', mutedColor:mutedColor },

            h('div', { style:{ display:'flex', gap:S.xs, marginBottom:S.xl, borderBottom:'1px solid '+borderColor, paddingBottom:1 } },
                tabs.map(function(t) {
                    var a = tab === t.v;
                    return h('button', { key:t.v, onClick:function(){ navigate(t.slug); },
                        style:{ padding:'10px 18px', border:'none', cursor:'pointer', fontSize:13, fontWeight:a?600:400,
                            background:'transparent', color:a?TC.accent:mutedColor, borderBottom:a?'2px solid '+TC.accent:'2px solid transparent',
                            marginBottom:-1, transition:'all .15s' } }, t.l);
                })
            ),

            tab === 'overview'      ? h(Overview)
            : tab === 'log'         ? h(MessageLog)
            : tab === 'subscribers' ? h(Subscribers)
            : tab === 'settings'    ? h(Settings)
            : null
        );
    }

    /* ════════════════════════════════════════════════════════════════════
     * AFFILIATES PAGE — Affiliate Marketing admin (S233, Part 2)
     *
     * Growth → Affiliates. Gated into the menu by Config.affiliateMarketingInstalled;
     * the master enable toggle is gated by Config.affiliateMarketingLicenseActive.
     *
     * Six views, dispatched off the hash subId (parse() → { route, subId }):
     *   #/affiliates                 → AffiliatesList (list + status chips + actions)
     *   #/affiliates/{id}            → AffiliateDetail (profile + overrides + tax)
     *   #/affiliates/conversions     → Conversions (list + approve/reverse)
     *   #/affiliates/payouts         → Payouts (list + create + mark-paid + CSV)
     *   #/affiliates/af-settings     → Settings (toggle + program defaults + assets)
     *   #/affiliates/analytics       → Analytics (KPIs + leaderboard + timeseries)
     *
     * Focus-safe model (like SmsNotificationsPage): parent stateless, each inner
     * view owns its state; render-helpers return elements (called directly, never
     * via h()). No JSX — h()/module-scope helpers w/ ctx:edCtx.
     * ════════════════════════════════════════════════════════════════════ */
    function AffiliatesPage({ subId, dark, cardBg, textColor, mutedColor, borderColor, tableHeaderBg }) {
        var fs   = fieldSty(dark, borderColor);
        var selS = Object.assign({}, fs, { cursor:'pointer', appearance:'auto' });
        var AF_API = Config.affiliateMarketingApiUrl || (Config.apiUrl || '').replace('/titancart/v1', '/titancart-affiliate/v1');
        var licenseActive = !!Config.affiliateMarketingLicenseActive;

        /* Envelope-aware fetch against the affiliate REST namespace. */
        function afApiFetch(path, opts) {
            opts = opts || {};
            opts.headers = Object.assign({ 'Content-Type':'application/json', 'X-WP-Nonce': Config.nonce }, opts.headers || {});
            if (opts.body && typeof opts.body === 'object') opts.body = JSON.stringify(opts.body);
            return fetch(AF_API + path, opts).then(function(r) {
                return r.text().then(function(text) {
                    var data = null;
                    if (text) { try { data = JSON.parse(text); } catch(e) { data = null; } }
                    if (!r.ok || (data && data.success === false)) {
                        var m = (data && data.error && data.error.message) || (data && data.message) || ('HTTP ' + r.status);
                        var err = new Error(m); err.status = r.status; err.data = data;
                        err.code = data && data.error && data.error.code;
                        return Promise.reject(err);
                    }
                    return data;
                });
            });
        }

        function asBool(v) { return v === '1' || v === 1 || v === true; }
        function unwrap(d) { return (d && d.data !== undefined) ? d.data : d; }
        function num(v) { var n = parseFloat(v); return isNaN(n) ? 0 : n; }
        function money(v, code) { return tcFmtMoney(num(v), code || 'USD'); }
        function fmtDate(s) {
            if (!s) return '\u2014';
            var t = (String(s).indexOf('T') < 0) ? String(s).replace(' ', 'T') + 'Z' : s;
            var d = new Date(t);
            if (isNaN(d.getTime())) return s;
            return d.toLocaleString();
        }
        function fmtDay(s) {
            if (!s) return '\u2014';
            var t = (String(s).indexOf('T') < 0) ? String(s).replace(' ', 'T') + 'Z' : s;
            var d = new Date(t);
            if (isNaN(d.getTime())) return s;
            return d.toLocaleDateString();
        }

        /* ── status lookups ── */
        var AFF_STATUS = {
            pending:   { l:'Pending',   c:TC.amber,   bg: dark?'rgba(217,119,6,.12)':TC.amberBg },
            approved:  { l:'Approved',  c:TC.success, bg: dark?'rgba(22,163,74,.12)':TC.successBg },
            rejected:  { l:'Rejected',  c:TC.error,   bg: dark?'rgba(220,38,38,.12)':TC.errorBg },
            suspended: { l:'Suspended', c:mutedColor, bg: dark?'rgba(255,255,255,.06)':'#F5F5F5' },
        };
        var CONV_STATUS = {
            pending:  { l:'Pending',  c:TC.amber,   bg: dark?'rgba(217,119,6,.12)':TC.amberBg },
            approved: { l:'Approved', c:TC.accent,  bg: dark?'rgba(37,99,235,.12)':TC.accentBg },
            paid:     { l:'Paid',     c:TC.success, bg: dark?'rgba(22,163,74,.12)':TC.successBg },
            reversed: { l:'Reversed', c:mutedColor, bg: dark?'rgba(255,255,255,.06)':'#F5F5F5' },
        };
        var PAYOUT_STATUS = {
            pending:    { l:'Pending',    c:TC.amber,   bg: dark?'rgba(217,119,6,.12)':TC.amberBg },
            processing: { l:'Processing', c:TC.accent,  bg: dark?'rgba(37,99,235,.12)':TC.accentBg },
            paid:       { l:'Paid',       c:TC.success, bg: dark?'rgba(22,163,74,.12)':TC.successBg },
            failed:     { l:'Failed',     c:TC.error,   bg: dark?'rgba(220,38,38,.12)':TC.errorBg },
        };
        function pill(map, st) {
            var m = map[st] || { l: st || '\u2014', c:mutedColor, bg: dark?'rgba(255,255,255,.06)':'#F5F5F5' };
            return h('span', { style:{ fontSize:11, fontWeight:600, padding:'2px 8px', borderRadius:99, color:m.c, background:m.bg, display:'inline-block', whiteSpace:'nowrap' } }, m.l);
        }

        function licenseBanner() {
            if (licenseActive) return null;
            return h('div', { style:{ display:'flex', gap:S.sm, alignItems:'flex-start', padding:'12px 16px', borderRadius:8,
                    background: dark?'rgba(217,119,6,.12)':TC.amberBg, border:'1px solid '+TC.amber+'40', marginBottom:S.lg } },
                h('span', { style:{ color:TC.amber, fontWeight:600, fontSize:13 } }, '\u26A0'),
                h('div', { style:{ fontSize:13, color:textColor } },
                    h('strong', null, 'License required. '),
                    'Affiliate Marketing is installed but not licensed. You can review applications, configure commissions, and set up the program below, but clicks are only tracked and commissions only accrue once a valid license is active and the master switch is on.')
            );
        }
        function flash(msg) {
            if (!msg) return null;
            return h('div', { style:{ padding:'10px 16px', borderRadius:8, marginBottom:S.lg, fontSize:13, fontWeight:500,
                    background: msg.type === 'success' ? (dark?'rgba(22,163,74,.12)':TC.successBg) : (dark?'rgba(220,38,38,.12)':TC.errorBg),
                    color: msg.type === 'success' ? TC.success : TC.error,
                    border:'1px solid ' + (msg.type === 'success' ? TC.success : TC.error) + '30' } }, msg.text);
        }

        /* Contextual lifecycle actions for an affiliate row/detail. */
        function affiliateActions(status, onAction, busy) {
            var btns = [];
            function mk(action, label, variant) {
                btns.push(h(Btn, { key:action, variant:variant||'secondary', disabled:busy, mutedColor:mutedColor, borderColor:borderColor,
                    onClick:function(){ onAction(action); } }, label));
            }
            if (status === 'pending')   { mk('approve','Approve','primary'); mk('reject','Reject','danger'); }
            else if (status === 'approved')  { mk('suspend','Suspend','danger'); }
            else if (status === 'suspended') { mk('reinstate','Reinstate','primary'); }
            else if (status === 'rejected')  { mk('approve','Approve','primary'); }
            return btns;
        }

        /* CSV download with nonce (plain <a> can't carry X-WP-Nonce). */
        function downloadCsv(type, qs) {
            fetch(AF_API + '/export/' + type + (qs || ''), {
                headers: { 'X-WP-Nonce': Config.nonce }
            }).then(function(r){ return r.text(); }).then(function(csv){
                var blob = new Blob([csv], { type:'text/csv;charset=utf-8' });
                var url = URL.createObjectURL(blob);
                var a = document.createElement('a');
                a.href = url; a.download = 'affiliate-' + type + '-' + Date.now() + '.csv';
                document.body.appendChild(a); a.click();
                document.body.removeChild(a);
                setTimeout(function(){ URL.revokeObjectURL(url); }, 1000);
            });
        }

        /* ══════════════════════════════════════════════════════════════════
         * VIEW: AFFILIATES LIST
         * ══════════════════════════════════════════════════════════════════ */
        function AffiliatesList() {
            var [loading, setLoading] = useState(true);
            var [rows, setRows] = useState([]);
            var [meta, setMeta] = useState(null);
            var [err, setErr] = useState('');
            var [filter, setFilter] = useState('all');
            var [search, setSearch] = useState('');
            var [page, setPage] = useState(1);
            var [busyId, setBusyId] = useState(0);

            function load() {
                setLoading(true); setErr('');
                var qs = '?page=' + page + '&per_page=20'
                    + (filter !== 'all' ? '&status=' + encodeURIComponent(filter) : '')
                    + (search.trim() ? '&search=' + encodeURIComponent(search.trim()) : '');
                afApiFetch('/affiliates' + qs).then(function(d) {
                    setRows(unwrap(d) || []);
                    setMeta((d && d.meta) || null);
                    setLoading(false);
                }).catch(function(e) { setErr(e.message || 'Failed to load affiliates.'); setLoading(false); });
            }
            useEffect(function(){ load(); }, [filter, page]);
            useEffect(function(){
                var id = setTimeout(function(){ setPage(1); load(); }, 350);
                return function(){ clearTimeout(id); };
            }, [search]);

            function act(id, action) {
                setBusyId(id);
                afApiFetch('/affiliates/' + id + '/' + action, { method:'POST' }).then(function(){
                    setBusyId(0); load();
                }).catch(function(e){ setBusyId(0); setErr(e.message || 'Action failed.'); });
            }

            var counts = (meta && meta.status_counts) || {};
            var total = (meta && meta.total) || 0;
            var totalPages = Math.max(1, Math.ceil(total / 20));
            var CHIPS = [['all','All'],['pending','Pending'],['approved','Approved'],['suspended','Suspended'],['rejected','Rejected']];

            return h('div', null,
                licenseBanner(),
                err && flash({ type:'error', text:err }),

                h('div', { style:{ display:'flex', gap:S.sm, flexWrap:'wrap', marginBottom:S.lg, alignItems:'center' } },
                    CHIPS.map(function(o) {
                        var a = filter === o[0];
                        var c = counts[o[0]];
                        return h('button', { key:o[0], onClick:function(){ setPage(1); setFilter(o[0]); },
                            style:{ padding:'6px 14px', borderRadius:99, fontSize:12, fontWeight:500, cursor:'pointer',
                                border:'1px solid ' + (a ? TC.accent : borderColor),
                                background: a ? (dark?'rgba(37,99,235,.12)':TC.accentBg) : 'transparent',
                                color: a ? TC.accent : mutedColor } },
                            o[1] + (c != null ? ' (' + c + ')' : ''));
                    }),
                    h('div', { style:{ marginLeft:'auto' } },
                        h(SearchBox, { value:search, onChange:setSearch, placeholder:'Search code, email, name\u2026', borderColor:borderColor, dark:dark, mutedColor:mutedColor }))
                ),

                loading ? h(Spinner, { mutedColor:mutedColor })
                : rows.length === 0 ? h(EmptyState, { mutedColor:mutedColor, icon:'\uD83E\uDD1D', title:'No affiliates yet',
                        sub:'Approved partners and pending applications appear here. Share your program signup to get started.' })
                : h('div', { style:{ background:cardBg, border:'1px solid '+borderColor, borderRadius:12, overflow:'hidden' } },
                    h('table', { style:{ width:'100%', borderCollapse:'collapse', fontSize:13 } },
                        h('thead', null,
                            h('tr', { style:{ background:tableHeaderBg, textAlign:'left', color:mutedColor } },
                                ['Affiliate','Code','Status','Clicks','Conv.','Earned','Actions'].map(function(c, i) {
                                    return h('th', { key:i, style:{ padding:'10px 16px', fontWeight:600, fontSize:11, textTransform:'uppercase', letterSpacing:'.3px' } }, c);
                                })
                            )
                        ),
                        h('tbody', null,
                            rows.map(function(r) {
                                return h('tr', { key:r.id, style:{ borderTop:'1px solid '+borderColor, cursor:'pointer' },
                                        onClick:function(e){ if (e.target.tagName !== 'BUTTON') navigate('affiliates/' + r.id); } },
                                    h('td', { style:{ padding:'11px 16px', color:textColor } },
                                        h('div', { style:{ fontWeight:600 } }, r.display_name || '\u2014'),
                                        h('div', { style:{ fontSize:12, color:mutedColor } }, r.email || '')),
                                    h('td', { style:{ padding:'11px 16px', color:mutedColor, fontFamily:'ui-monospace, Menlo, Consolas, monospace', fontSize:12 } }, r.code),
                                    h('td', { style:{ padding:'11px 16px' } }, pill(AFF_STATUS, r.status)),
                                    h('td', { style:{ padding:'11px 16px', color:mutedColor, fontVariantNumeric:'tabular-nums' } }, String(num(r.clicks_count))),
                                    h('td', { style:{ padding:'11px 16px', color:mutedColor, fontVariantNumeric:'tabular-nums' } }, String(num(r.conversions_count))),
                                    h('td', { style:{ padding:'11px 16px', color:textColor, fontVariantNumeric:'tabular-nums', fontWeight:500 } }, money(r.earned_total)),
                                    h('td', { style:{ padding:'11px 16px' } },
                                        h('div', { style:{ display:'flex', gap:S.xs, flexWrap:'wrap' } },
                                            affiliateActions(r.status, function(action){ act(r.id, action); }, busyId === r.id)))
                                );
                            })
                        )
                    )
                ),

                totalPages > 1 && h('div', { style:{ display:'flex', justifyContent:'flex-end', marginTop:S.lg } },
                    h(Pager, { page:page, totalPages:totalPages, onPage:setPage, mutedColor:mutedColor, borderColor:borderColor })
                )
            );
        }

        /* ══════════════════════════════════════════════════════════════════
         * VIEW: AFFILIATE DETAIL
         * ══════════════════════════════════════════════════════════════════ */
        function AffiliateDetail() {
            var affId = parseInt(subId, 10) || 0;
            var [loading, setLoading] = useState(true);
            var [aff, setAff] = useState(null);
            var [convCounts, setConvCounts] = useState({});
            var [form, setForm] = useState(null);
            var [saving, setSaving] = useState(false);
            var [busy, setBusy] = useState(false);
            var [msg, setMsg] = useState(null);
            var [open, setOpen] = useState({ profile:true, commission:true, products:false, tax:false });

            function toggleSection(sid) { setOpen(function(p){ var n = Object.assign({}, p); n[sid] = !n[sid]; return n; }); }
            var edCtx = { mutedColor:mutedColor, borderColor:borderColor, textColor:textColor, open:open, onToggle:toggleSection };

            function normalize(a) {
                a = a || {};
                var tiers = [];
                if (a.tiers) { try { tiers = JSON.parse(a.tiers) || []; } catch(e) { tiers = []; } }
                var prods = [];
                if (a.product_commissions) {
                    try {
                        var pc = JSON.parse(a.product_commissions) || {};
                        Object.keys(pc).forEach(function(pid){ prods.push({ product_id:pid, type:(pc[pid].type||'percent'), rate:(pc[pid].rate!=null?String(pc[pid].rate):'') }); });
                    } catch(e) { prods = []; }
                }
                return {
                    display_name:    a.display_name || '',
                    website:         a.website || '',
                    payout_method:   a.payout_method || 'paypal',
                    payout_email:    a.payout_email || '',
                    note:            a.note || '',
                    commission_type: a.commission_type || '',
                    commission_rate: a.commission_rate != null ? String(a.commission_rate) : '',
                    tiers:           tiers,
                    products:        prods,
                    tax_name:        a.tax_name || '',
                    tax_id:          a.tax_id || '',
                    tax_country:     a.tax_country || '',
                };
            }

            function load() {
                setLoading(true);
                afApiFetch('/affiliates/' + affId).then(function(d) {
                    var data = unwrap(d) || {};
                    setAff(data.affiliate || null);
                    setConvCounts(data.conv_counts || {});
                    setForm(normalize(data.affiliate || {}));
                    setLoading(false);
                }).catch(function(e) { setMsg({ type:'error', text:e.message || 'Failed to load affiliate.' }); setLoading(false); });
            }
            useEffect(function(){ load(); }, []);
            function set(k, v) { setForm(function(f){ var n = Object.assign({}, f); n[k] = v; return n; }); }

            function save() {
                if (!form) return;
                setSaving(true); setMsg(null);
                var pc = {};
                form.products.forEach(function(row){
                    var pid = parseInt(row.product_id, 10);
                    if (pid > 0) pc[pid] = { type: row.type || 'percent', rate: num(row.rate) };
                });
                var body = {
                    display_name:    form.display_name,
                    website:         form.website,
                    payout_method:   form.payout_method,
                    payout_email:    form.payout_email,
                    note:            form.note,
                    commission_type: form.commission_type || null,
                    commission_rate: form.commission_rate === '' ? null : form.commission_rate,
                    tiers:           form.tiers && form.tiers.length ? form.tiers : null,
                    product_commissions: Object.keys(pc).length ? pc : null,
                    tax_name:        form.tax_name,
                    tax_id:          form.tax_id,
                    tax_country:     form.tax_country,
                };
                afApiFetch('/affiliates/' + affId, { method:'PUT', body:body }).then(function(d){
                    var data = unwrap(d) || {};
                    setAff(data.affiliate || null);
                    setForm(normalize(data.affiliate || {}));
                    setMsg({ type:'success', text:'Affiliate saved.' });
                    setSaving(false);
                }).catch(function(e){ setMsg({ type:'error', text:e.message || 'Save failed.' }); setSaving(false); });
            }

            function act(action) {
                setBusy(true); setMsg(null);
                afApiFetch('/affiliates/' + affId + '/' + action, { method:'POST' }).then(function(){
                    setBusy(false); load();
                }).catch(function(e){ setBusy(false); setMsg({ type:'error', text:e.message || 'Action failed.' }); });
            }

            function createPayout() {
                setBusy(true); setMsg(null);
                afApiFetch('/payouts', { method:'POST', body:{ affiliate_id:affId } }).then(function(d){
                    var r = unwrap(d) || {};
                    setBusy(false);
                    setMsg({ type:'success', text:'Payout created: ' + money(r.amount) + ' (' + (r.count||0) + ' conversions).' });
                    load();
                }).catch(function(e){
                    setBusy(false);
                    var t = e.message || 'Could not create payout.';
                    if (e.code === 'below_minimum') t = 'Approved balance is below the payout minimum.';
                    if (e.code === 'nothing_to_pay') t = 'No approved conversions to pay out.';
                    setMsg({ type:'error', text:t });
                });
            }

            var backBtn = h('button', { onClick:function(){ navigate('affiliates'); },
                style:{ display:'flex', alignItems:'center', gap:6, padding:'6px 12px', borderRadius:8, border:'1px solid '+borderColor,
                    background:'transparent', color:mutedColor, fontSize:13, cursor:'pointer' } }, '\u2190 Back');

            if (loading || !form) return h('div', null, backBtn, h(Spinner, { mutedColor:mutedColor }));
            if (!aff) return h('div', null, backBtn, flash({ type:'error', text:'Affiliate not found.' }));

            /* commission overrides editor (tiers + the rate field) */
            function tierRows() {
                return h('div', null,
                    (form.tiers || []).map(function(row, i) {
                        return h('div', { key:i, style:{ display:'flex', gap:S.sm, alignItems:'center', marginBottom:S.xs } },
                            h('input', { type:'number', step:'0.01', value:row.min_sales != null ? row.min_sales : '', placeholder:'Min sales $',
                                onChange:function(e){ setForm(function(f){ var n=Object.assign({},f); n.tiers=n.tiers.slice(); n.tiers[i]=Object.assign({},n.tiers[i],{min_sales:e.target.value}); return n; }); },
                                style:Object.assign({}, fs, { maxWidth:160 }) }),
                            h('input', { type:'number', step:'0.01', value:row.rate != null ? row.rate : '', placeholder:'Rate %',
                                onChange:function(e){ setForm(function(f){ var n=Object.assign({},f); n.tiers=n.tiers.slice(); n.tiers[i]=Object.assign({},n.tiers[i],{rate:e.target.value}); return n; }); },
                                style:Object.assign({}, fs, { maxWidth:120 }) }),
                            h('button', { onClick:function(){ setForm(function(f){ var n=Object.assign({},f); n.tiers=n.tiers.filter(function(_,j){return j!==i;}); return n; }); },
                                style:{ border:'none', background:'transparent', color:TC.error, cursor:'pointer', fontSize:18 } }, '\u00D7')
                        );
                    }),
                    h(Btn, { variant:'secondary', mutedColor:mutedColor, borderColor:borderColor,
                        onClick:function(){ setForm(function(f){ var n=Object.assign({},f); n.tiers=(n.tiers||[]).concat([{min_sales:'',rate:''}]); return n; }); } }, '+ Add tier')
                );
            }

            function productRows() {
                return h('div', null,
                    (form.products || []).map(function(row, i) {
                        return h('div', { key:i, style:{ display:'flex', gap:S.sm, alignItems:'center', marginBottom:S.xs } },
                            h('input', { type:'number', value:row.product_id || '', placeholder:'Product ID',
                                onChange:function(e){ setForm(function(f){ var n=Object.assign({},f); n.products=n.products.slice(); n.products[i]=Object.assign({},n.products[i],{product_id:e.target.value}); return n; }); },
                                style:Object.assign({}, fs, { maxWidth:130 }) }),
                            h('select', { value:row.type || 'percent',
                                onChange:function(e){ setForm(function(f){ var n=Object.assign({},f); n.products=n.products.slice(); n.products[i]=Object.assign({},n.products[i],{type:e.target.value}); return n; }); },
                                style:Object.assign({}, selS, { maxWidth:120 }) },
                                h('option', { value:'percent' }, 'Percent'),
                                h('option', { value:'flat' }, 'Flat')),
                            h('input', { type:'number', step:'0.01', value:row.rate || '', placeholder:'Rate',
                                onChange:function(e){ setForm(function(f){ var n=Object.assign({},f); n.products=n.products.slice(); n.products[i]=Object.assign({},n.products[i],{rate:e.target.value}); return n; }); },
                                style:Object.assign({}, fs, { maxWidth:120 }) }),
                            h('button', { onClick:function(){ setForm(function(f){ var n=Object.assign({},f); n.products=n.products.filter(function(_,j){return j!==i;}); return n; }); },
                                style:{ border:'none', background:'transparent', color:TC.error, cursor:'pointer', fontSize:18 } }, '\u00D7')
                        );
                    }),
                    h(Btn, { variant:'secondary', mutedColor:mutedColor, borderColor:borderColor,
                        onClick:function(){ setForm(function(f){ var n=Object.assign({},f); n.products=(n.products||[]).concat([{product_id:'',type:'percent',rate:''}]); return n; }); } }, '+ Add product rule')
                );
            }

            function statTile(label, value, color) {
                return h('div', { style:{ flex:'1 1 120px', minWidth:110, background:cardBg, border:'1px solid '+borderColor, borderRadius:12, padding:'14px 16px' } },
                    h('div', { style:{ fontSize:20, fontWeight:700, color: color || textColor, fontVariantNumeric:'tabular-nums' } }, String(value)),
                    h('div', { style:{ fontSize:11, color:mutedColor, marginTop:4, textTransform:'uppercase', letterSpacing:'.3px' } }, label)
                );
            }

            return h('div', null,
                h('div', { style:{ display:'flex', alignItems:'center', gap:S.md, marginBottom:S.lg } },
                    backBtn,
                    h('div', { style:{ flex:1 } },
                        h('div', { style:{ fontSize:18, fontWeight:700, color:textColor } }, aff.display_name || aff.code),
                        h('div', { style:{ fontSize:12, color:mutedColor } }, aff.email)),
                    pill(AFF_STATUS, aff.status),
                    h('div', { style:{ display:'flex', gap:S.xs } }, affiliateActions(aff.status, act, busy))
                ),
                flash(msg),

                /* stats */
                h('div', { style:{ display:'flex', gap:S.md, flexWrap:'wrap', marginBottom:S.lg } },
                    statTile('Clicks', num(aff.clicks_count)),
                    statTile('Conversions', num(aff.conversions_count)),
                    statTile('Earned', money(aff.earned_total), TC.success),
                    statTile('Payable', money(aff.payable), TC.accent),
                    statTile('Paid', money(aff.paid_total))
                ),

                /* referral link */
                h('div', { style:{ background:cardBg, border:'1px solid '+borderColor, borderRadius:12, padding:'12px 16px', marginBottom:S.lg, display:'flex', gap:S.sm, alignItems:'center' } },
                    h('span', { style:{ fontSize:12, color:mutedColor, whiteSpace:'nowrap' } }, 'Referral link:'),
                    h('input', { type:'text', readOnly:true, value:aff.referral_link || '', style:Object.assign({}, fs, { fontFamily:'ui-monospace, Menlo, Consolas, monospace', fontSize:12 }) })
                ),

                h('div', { style:{ background:cardBg, border:'1px solid '+borderColor, borderRadius:12, padding:'8px '+S.xl+'px '+S.xl+'px' } },
                    /* Profile */
                    h(SectionHead, { id:'profile', title:'Profile & payout', ctx:edCtx }),
                    open.profile && h('div', null,
                        h(FR, { label:'Display name', ctx:edCtx },
                            h('input', { type:'text', value:form.display_name, onChange:function(e){ set('display_name', e.target.value); }, style:fs })),
                        h(FR, { label:'Website', ctx:edCtx },
                            h('input', { type:'text', value:form.website, placeholder:'https://', onChange:function(e){ set('website', e.target.value); }, style:fs })),
                        h(FR, { label:'Payout method', ctx:edCtx },
                            h('select', { value:form.payout_method, onChange:function(e){ set('payout_method', e.target.value); }, style:Object.assign({}, selS, { maxWidth:220 }) },
                                h('option', { value:'paypal' }, 'PayPal'),
                                h('option', { value:'bank' }, 'Bank transfer'),
                                h('option', { value:'manual' }, 'Manual'))),
                        h(FR, { label:'Payout email', ctx:edCtx },
                            h('input', { type:'email', value:form.payout_email, onChange:function(e){ set('payout_email', e.target.value); }, style:fs })),
                        h(FR, { label:'Internal note', note:'Admin-only; not shown to the affiliate.', ctx:edCtx },
                            h('textarea', { value:form.note, rows:2, onChange:function(e){ set('note', e.target.value); }, style:Object.assign({}, fs, { resize:'vertical', fontFamily:'inherit' }) }))
                    ),

                    /* Commission overrides */
                    h(SectionHead, { id:'commission', title:'Commission override', ctx:edCtx }),
                    open.commission && h('div', null,
                        h('div', { style:{ fontSize:13, color:mutedColor, marginBottom:S.sm } }, 'Leave the type blank to inherit the program default. Set a type here to override it for this affiliate.'),
                        h(FR, { label:'Commission type', ctx:edCtx },
                            h('select', { value:form.commission_type, onChange:function(e){ set('commission_type', e.target.value); }, style:Object.assign({}, selS, { maxWidth:220 }) },
                                h('option', { value:'' }, 'Inherit program default'),
                                h('option', { value:'percent' }, 'Percent'),
                                h('option', { value:'flat' }, 'Flat'),
                                h('option', { value:'tiered' }, 'Tiered'))),
                        (form.commission_type === 'percent' || form.commission_type === 'flat') && h(FR, { label: form.commission_type === 'flat' ? 'Flat amount ($)' : 'Rate (%)', ctx:edCtx },
                            h('input', { type:'number', step:'0.01', value:form.commission_rate, onChange:function(e){ set('commission_rate', e.target.value); }, style:Object.assign({}, fs, { maxWidth:160 }) })),
                        form.commission_type === 'tiered' && h(FR, { label:'Tiers', note:'Each tier: minimum cumulative sales \u2192 rate %. Highest matching tier applies.', ctx:edCtx }, tierRows())
                    ),

                    /* Product-specific */
                    h(SectionHead, { id:'products', title:'Product-specific commissions', ctx:edCtx }),
                    open.products && h('div', null,
                        h('div', { style:{ fontSize:13, color:mutedColor, marginBottom:S.sm } }, 'Override the commission for specific products by ID. These take precedence over the type above.'),
                        productRows()
                    ),

                    /* Tax */
                    h(SectionHead, { id:'tax', title:'Tax details', ctx:edCtx }),
                    open.tax && h('div', null,
                        h('div', { style:{ fontSize:13, color:mutedColor, marginBottom:S.sm } },
                            'Collect-only (v1). Flagged for 1099 reporting once yearly commission crosses the program threshold.',
                            asBool(aff.tax_threshold_flagged) && h('strong', { style:{ color:TC.amber, marginLeft:6 } }, 'Threshold reached this year.')),
                        h(FR, { label:'Legal name', ctx:edCtx },
                            h('input', { type:'text', value:form.tax_name, onChange:function(e){ set('tax_name', e.target.value); }, style:fs })),
                        h(FR, { label:'Tax ID', ctx:edCtx },
                            h('input', { type:'text', value:form.tax_id, onChange:function(e){ set('tax_id', e.target.value); }, style:fs })),
                        h(FR, { label:'Country', note:'2-letter ISO code.', ctx:edCtx },
                            h('input', { type:'text', maxLength:2, value:form.tax_country, onChange:function(e){ set('tax_country', e.target.value.toUpperCase()); }, style:Object.assign({}, fs, { maxWidth:100, textTransform:'uppercase' }) }))
                    ),

                    aff.payout_requested_at && h('div', { style:{ marginTop:S.lg, padding:'10px 14px', borderRadius:10, border:'1px solid '+TC.amber, background:(dark?'rgba(217,119,6,.14)':TC.amberBg), color:TC.amber, fontSize:13, fontWeight:600, display:'flex', alignItems:'center', gap:8, flexWrap:'wrap' } },
                        h('span', null, '\uD83D\uDCB0 Payout requested'),
                        h('span', { style:{ fontWeight:500 } }, (aff.payout_request_amount ? ('\u2014 ' + money(aff.payout_request_amount)) : '') + (aff.payout_requested_at ? (' on ' + fmtDay(aff.payout_requested_at)) : ''))),

                    h('div', { style:{ display:'flex', justifyContent:'space-between', alignItems:'center', marginTop:S.xl, paddingTop:S.lg, borderTop:'1px solid '+borderColor } },
                        h(Btn, { variant:'secondary', disabled:busy || num(aff.payable) <= 0, mutedColor:mutedColor, borderColor:borderColor, onClick:createPayout },
                            num(aff.payable) > 0 ? ('Create payout (' + money(aff.payable) + ')') : 'No payable balance'),
                        h(Btn, { variant:'primary', disabled:saving, mutedColor:mutedColor, borderColor:borderColor, onClick:save }, saving ? 'Saving\u2026' : 'Save changes')
                    )
                )
            );
        }

        /* ══════════════════════════════════════════════════════════════════
         * VIEW: CONVERSIONS
         * ══════════════════════════════════════════════════════════════════ */
        function Conversions() {
            var [loading, setLoading] = useState(true);
            var [rows, setRows] = useState([]);
            var [meta, setMeta] = useState(null);
            var [err, setErr] = useState('');
            var [filter, setFilter] = useState('all');
            var [page, setPage] = useState(1);
            var [busyId, setBusyId] = useState(0);

            function load() {
                setLoading(true); setErr('');
                var qs = '?page=' + page + '&per_page=25' + (filter !== 'all' ? '&status=' + encodeURIComponent(filter) : '');
                afApiFetch('/conversions' + qs).then(function(d) {
                    setRows(unwrap(d) || []);
                    setMeta((d && d.meta) || null);
                    setLoading(false);
                }).catch(function(e) { setErr(e.message || 'Failed to load conversions.'); setLoading(false); });
            }
            useEffect(function(){ load(); }, [filter, page]);

            function act(id, action) {
                setBusyId(id);
                afApiFetch('/conversions/' + id + '/' + action, { method:'POST' }).then(function(){
                    setBusyId(0); load();
                }).catch(function(e){ setBusyId(0); setErr(e.message || 'Action failed.'); });
            }

            var counts = (meta && meta.status_counts) || {};
            var total = (meta && meta.total) || 0;
            var totalPages = Math.max(1, Math.ceil(total / 25));
            var CHIPS = [['all','All'],['pending','Pending'],['approved','Approved'],['paid','Paid'],['reversed','Reversed']];

            return h('div', null,
                licenseBanner(),
                err && flash({ type:'error', text:err }),

                h('div', { style:{ display:'flex', gap:S.sm, flexWrap:'wrap', marginBottom:S.lg, alignItems:'center' } },
                    CHIPS.map(function(o) {
                        var a = filter === o[0];
                        var c = counts[o[0]];
                        return h('button', { key:o[0], onClick:function(){ setPage(1); setFilter(o[0]); },
                            style:{ padding:'6px 14px', borderRadius:99, fontSize:12, fontWeight:500, cursor:'pointer',
                                border:'1px solid ' + (a ? TC.accent : borderColor),
                                background: a ? (dark?'rgba(37,99,235,.12)':TC.accentBg) : 'transparent',
                                color: a ? TC.accent : mutedColor } },
                            o[1] + (c != null ? ' (' + c + ')' : ''));
                    })
                ),

                loading ? h(Spinner, { mutedColor:mutedColor })
                : rows.length === 0 ? h(EmptyState, { mutedColor:mutedColor, icon:'\uD83D\uDCB0', title:'No conversions',
                        sub:'Attributed orders appear here. Approve a pending conversion to make it payable.' })
                : h('div', { style:{ background:cardBg, border:'1px solid '+borderColor, borderRadius:12, overflow:'hidden' } },
                    h('table', { style:{ width:'100%', borderCollapse:'collapse', fontSize:13 } },
                        h('thead', null,
                            h('tr', { style:{ background:tableHeaderBg, textAlign:'left', color:mutedColor } },
                                ['Order','Affiliate','When','Order value','Commission','Status','Actions'].map(function(c, i) {
                                    return h('th', { key:i, style:{ padding:'10px 16px', fontWeight:600, fontSize:11, textTransform:'uppercase', letterSpacing:'.3px' } }, c);
                                })
                            )
                        ),
                        h('tbody', null,
                            rows.map(function(r) {
                                var canApprove = r.status === 'pending';
                                var canReverse = r.status !== 'paid' && r.status !== 'reversed';
                                return h('tr', { key:r.id, style:{ borderTop:'1px solid '+borderColor } },
                                    h('td', { style:{ padding:'11px 16px', color:textColor, fontFamily:'ui-monospace, Menlo, Consolas, monospace', fontSize:12 } }, r.order_number || ('#' + r.order_id)),
                                    h('td', { style:{ padding:'11px 16px', color:textColor } }, r.affiliate_name || r.affiliate_code || ('#' + r.affiliate_id)),
                                    h('td', { style:{ padding:'11px 16px', color:mutedColor, whiteSpace:'nowrap' } }, fmtDay(r.created_at)),
                                    h('td', { style:{ padding:'11px 16px', color:mutedColor, fontVariantNumeric:'tabular-nums' } }, money(r.commission_base, r.currency)),
                                    h('td', { style:{ padding:'11px 16px', color:textColor, fontVariantNumeric:'tabular-nums', fontWeight:600 } }, money(r.commission, r.currency)),
                                    h('td', { style:{ padding:'11px 16px' } }, pill(CONV_STATUS, r.status)),
                                    h('td', { style:{ padding:'11px 16px' } },
                                        h('div', { style:{ display:'flex', gap:S.xs } },
                                            canApprove && h(Btn, { variant:'primary', disabled:busyId===r.id, mutedColor:mutedColor, borderColor:borderColor, onClick:function(){ act(r.id,'approve'); } }, 'Approve'),
                                            canReverse && h(Btn, { variant:'danger', disabled:busyId===r.id, mutedColor:mutedColor, borderColor:borderColor, onClick:function(){ act(r.id,'reverse'); } }, 'Reverse')))
                                );
                            })
                        )
                    )
                ),

                totalPages > 1 && h('div', { style:{ display:'flex', justifyContent:'flex-end', marginTop:S.lg } },
                    h(Pager, { page:page, totalPages:totalPages, onPage:setPage, mutedColor:mutedColor, borderColor:borderColor })
                )
            );
        }

        /* ══════════════════════════════════════════════════════════════════
         * VIEW: PAYOUTS
         * ══════════════════════════════════════════════════════════════════ */
        function Payouts() {
            var [loading, setLoading] = useState(true);
            var [rows, setRows] = useState([]);
            var [meta, setMeta] = useState(null);
            var [err, setErr] = useState('');
            var [filter, setFilter] = useState('all');
            var [page, setPage] = useState(1);
            var [busyId, setBusyId] = useState(0);
            var [refInput, setRefInput] = useState({});
            var [msg, setMsg] = useState(null);

            function load() {
                setLoading(true); setErr('');
                var qs = '?page=' + page + '&per_page=25' + (filter !== 'all' ? '&status=' + encodeURIComponent(filter) : '');
                afApiFetch('/payouts' + qs).then(function(d) {
                    setRows(unwrap(d) || []);
                    setMeta((d && d.meta) || null);
                    setLoading(false);
                }).catch(function(e) { setErr(e.message || 'Failed to load payouts.'); setLoading(false); });
            }
            useEffect(function(){ load(); }, [filter, page]);

            function act(id, action, body) {
                setBusyId(id); setMsg(null);
                afApiFetch('/payouts/' + id + '/' + action, { method:'POST', body:body || {} }).then(function(){
                    setBusyId(0); load();
                }).catch(function(e){ setBusyId(0); setMsg({ type:'error', text:e.message || 'Action failed.' }); });
            }

            var total = (meta && meta.total) || 0;
            var totalPages = Math.max(1, Math.ceil(total / 25));
            var CHIPS = [['all','All'],['pending','Pending'],['processing','Processing'],['paid','Paid'],['failed','Failed']];

            return h('div', null,
                licenseBanner(),
                err && flash({ type:'error', text:err }),
                flash(msg),

                h('div', { style:{ display:'flex', gap:S.sm, flexWrap:'wrap', marginBottom:S.lg, alignItems:'center' } },
                    CHIPS.map(function(o) {
                        var a = filter === o[0];
                        return h('button', { key:o[0], onClick:function(){ setPage(1); setFilter(o[0]); },
                            style:{ padding:'6px 14px', borderRadius:99, fontSize:12, fontWeight:500, cursor:'pointer',
                                border:'1px solid ' + (a ? TC.accent : borderColor),
                                background: a ? (dark?'rgba(37,99,235,.12)':TC.accentBg) : 'transparent',
                                color: a ? TC.accent : mutedColor } }, o[1]);
                    }),
                    h('div', { style:{ marginLeft:'auto' } },
                        h(Btn, { variant:'secondary', mutedColor:mutedColor, borderColor:borderColor, onClick:function(){ downloadCsv('payouts', filter !== 'all' ? ('?status=' + filter) : ''); } }, 'Export CSV'))
                ),

                loading ? h(Spinner, { mutedColor:mutedColor })
                : rows.length === 0 ? h(EmptyState, { mutedColor:mutedColor, icon:'\uD83D\uDCB8', title:'No payouts yet',
                        sub:'Create a payout from an affiliate\u2019s detail page once they have an approved balance.' })
                : h('div', { style:{ background:cardBg, border:'1px solid '+borderColor, borderRadius:12, overflow:'hidden' } },
                    h('table', { style:{ width:'100%', borderCollapse:'collapse', fontSize:13 } },
                        h('thead', null,
                            h('tr', { style:{ background:tableHeaderBg, textAlign:'left', color:mutedColor } },
                                ['Affiliate','Amount','Method','Conv.','Status','Reference','Actions'].map(function(c, i) {
                                    return h('th', { key:i, style:{ padding:'10px 16px', fontWeight:600, fontSize:11, textTransform:'uppercase', letterSpacing:'.3px' } }, c);
                                })
                            )
                        ),
                        h('tbody', null,
                            rows.map(function(r) {
                                var pending = r.status === 'pending' || r.status === 'processing';
                                var paid = r.status === 'paid';
                                var refVal = refInput[r.id] != null ? refInput[r.id] : (r.reference || '');
                                var refField = h('input', { type:'text', placeholder:'Reference', value: refVal,
                                    onChange:function(e){ var v=e.target.value; setRefInput(function(p){ var n=Object.assign({},p); n[r.id]=v; return n; }); },
                                    style:Object.assign({}, fs, { maxWidth:130, padding:'5px 9px' }) });
                                return h('tr', { key:r.id, style:{ borderTop:'1px solid '+borderColor } },
                                    h('td', { style:{ padding:'11px 16px', color:textColor } }, r.affiliate_name || r.affiliate_code || ('#' + r.affiliate_id)),
                                    h('td', { style:{ padding:'11px 16px', color:textColor, fontVariantNumeric:'tabular-nums', fontWeight:600 } }, money(r.amount, r.currency)),
                                    h('td', { style:{ padding:'11px 16px', color:mutedColor } }, r.method || '\u2014'),
                                    h('td', { style:{ padding:'11px 16px', color:mutedColor, fontVariantNumeric:'tabular-nums' } }, String(num(r.conversion_count))),
                                    h('td', { style:{ padding:'11px 16px' } }, pill(PAYOUT_STATUS, r.status)),
                                    h('td', { style:{ padding:'11px 16px', color:mutedColor, fontSize:12 } }, r.reference || '\u2014'),
                                    h('td', { style:{ padding:'11px 16px' } },
                                        pending && h('div', { style:{ display:'flex', gap:S.xs, alignItems:'center', flexWrap:'wrap' } },
                                            refField,
                                            h(Btn, { variant:'primary', disabled: busyId===r.id || !refVal.trim(), mutedColor:mutedColor, borderColor:borderColor, onClick:function(){ act(r.id, 'mark-paid', { reference: refVal }); } }, 'Mark paid'),
                                            h(Btn, { variant:'danger', disabled:busyId===r.id, mutedColor:mutedColor, borderColor:borderColor, onClick:function(){ act(r.id, 'mark-failed', {}); } }, 'Fail')),
                                        paid && h('div', { style:{ display:'flex', gap:S.xs, alignItems:'center', flexWrap:'wrap' } },
                                            refField,
                                            h(Btn, { variant:'secondary', disabled: busyId===r.id || refVal === (r.reference || ''), mutedColor:mutedColor, borderColor:borderColor, onClick:function(){ act(r.id, 'set-reference', { reference: refVal }); } }, 'Save reference')))
                                );
                            })
                        )
                    )
                ),

                totalPages > 1 && h('div', { style:{ display:'flex', justifyContent:'flex-end', marginTop:S.lg } },
                    h(Pager, { page:page, totalPages:totalPages, onPage:setPage, mutedColor:mutedColor, borderColor:borderColor })
                )
            );
        }

        /* ══════════════════════════════════════════════════════════════════
         * VIEW: SETTINGS
         * ══════════════════════════════════════════════════════════════════ */
        function Settings() {
            var [loading, setLoading] = useState(true);
            var [form, setForm] = useState(null);
            var [saving, setSaving] = useState(false);
            var [msg, setMsg] = useState(null);
            var [open, setOpen] = useState({ general:true, commission:true, payouts:false, fraud:false, assets:false, terms:false });

            function toggleSection(sid) { setOpen(function(p){ var n = Object.assign({}, p); n[sid] = !n[sid]; return n; }); }
            var edCtx = { mutedColor:mutedColor, borderColor:borderColor, textColor:textColor, open:open, onToggle:toggleSection };

            function normalize(s) {
                s = s || {};
                var tiers = [];
                if (s.commission_tiers) { try { tiers = JSON.parse(s.commission_tiers) || []; } catch(e) { tiers = []; } }
                var assets = [];
                if (s.marketing_assets) { try { assets = JSON.parse(s.marketing_assets) || []; } catch(e) { assets = []; } }
                return {
                    enabled:                  asBool(s.enabled),
                    commission_type:          s.commission_type || 'percent',
                    commission_rate:          s.commission_rate != null ? String(s.commission_rate) : '10.0000',
                    commission_tiers:         tiers,
                    cookie_days:              s.cookie_days != null ? String(s.cookie_days) : '30',
                    attribution_model:        s.attribution_model || 'last',
                    auto_approve:             asBool(s.auto_approve),
                    auto_approve_commissions: asBool(s.auto_approve_commissions),
                    block_self_referral:      asBool(s.block_self_referral),
                    dedupe_click_minutes:     s.dedupe_click_minutes != null ? String(s.dedupe_click_minutes) : '30',
                    payout_minimum:           s.payout_minimum != null ? String(s.payout_minimum) : '50.0000',
                    payout_currency:          s.payout_currency || 'USD',
                    default_payout_method:    s.default_payout_method || 'paypal',
                    tax_threshold:            s.tax_threshold != null ? String(s.tax_threshold) : '600.0000',
                    program_terms:            s.program_terms || '',
                    marketing_assets:         assets,
                };
            }

            function load() {
                setLoading(true);
                afApiFetch('/settings').then(function(d) {
                    var data = unwrap(d) || {};
                    setForm(normalize(data.settings || {}));
                    setLoading(false);
                }).catch(function(e) { setMsg({ type:'error', text:e.message || 'Failed to load settings.' }); setLoading(false); });
            }
            useEffect(function(){ load(); }, []);
            function set(k, v) { setForm(function(f){ var n = Object.assign({}, f); n[k] = v; return n; }); }

            function save() {
                if (!form) return;
                setSaving(true); setMsg(null);
                var body = {
                    enabled:                  form.enabled ? '1' : '0',
                    commission_type:          form.commission_type,
                    commission_rate:          form.commission_rate,
                    commission_tiers:         form.commission_tiers && form.commission_tiers.length ? form.commission_tiers : '',
                    cookie_days:              form.cookie_days,
                    attribution_model:        form.attribution_model,
                    auto_approve:             form.auto_approve ? '1' : '0',
                    auto_approve_commissions: form.auto_approve_commissions ? '1' : '0',
                    block_self_referral:      form.block_self_referral ? '1' : '0',
                    dedupe_click_minutes:     form.dedupe_click_minutes,
                    payout_minimum:           form.payout_minimum,
                    payout_currency:          form.payout_currency,
                    default_payout_method:    form.default_payout_method,
                    tax_threshold:            form.tax_threshold,
                    program_terms:            form.program_terms,
                    marketing_assets:         form.marketing_assets && form.marketing_assets.length ? form.marketing_assets : '',
                };
                afApiFetch('/settings', { method:'PUT', body:body }).then(function(d) {
                    var data = unwrap(d) || {};
                    setForm(normalize(data.settings || {}));
                    setMsg({ type:'success', text:'Settings saved.' });
                    setSaving(false);
                }).catch(function(e) {
                    var t = e.message || 'Save failed.';
                    if (e.code === 'tcaf_license_required') t = 'An active license is required to enable the affiliate program.';
                    setMsg({ type:'error', text:t });
                    setSaving(false);
                });
            }

            if (loading || !form) return h(Spinner, { mutedColor:mutedColor });

            function tierRows() {
                return h('div', null,
                    (form.commission_tiers || []).map(function(row, i) {
                        return h('div', { key:i, style:{ display:'flex', gap:S.sm, alignItems:'center', marginBottom:S.xs } },
                            h('input', { type:'number', step:'0.01', value:row.min_sales != null ? row.min_sales : '', placeholder:'Min sales $',
                                onChange:function(e){ setForm(function(f){ var n=Object.assign({},f); n.commission_tiers=n.commission_tiers.slice(); n.commission_tiers[i]=Object.assign({},n.commission_tiers[i],{min_sales:e.target.value}); return n; }); },
                                style:Object.assign({}, fs, { maxWidth:160 }) }),
                            h('input', { type:'number', step:'0.01', value:row.rate != null ? row.rate : '', placeholder:'Rate %',
                                onChange:function(e){ setForm(function(f){ var n=Object.assign({},f); n.commission_tiers=n.commission_tiers.slice(); n.commission_tiers[i]=Object.assign({},n.commission_tiers[i],{rate:e.target.value}); return n; }); },
                                style:Object.assign({}, fs, { maxWidth:120 }) }),
                            h('button', { onClick:function(){ setForm(function(f){ var n=Object.assign({},f); n.commission_tiers=n.commission_tiers.filter(function(_,j){return j!==i;}); return n; }); },
                                style:{ border:'none', background:'transparent', color:TC.error, cursor:'pointer', fontSize:18 } }, '\u00D7')
                        );
                    }),
                    h(Btn, { variant:'secondary', mutedColor:mutedColor, borderColor:borderColor,
                        onClick:function(){ setForm(function(f){ var n=Object.assign({},f); n.commission_tiers=(n.commission_tiers||[]).concat([{min_sales:'',rate:''}]); return n; }); } }, '+ Add tier')
                );
            }

            function assetRows() {
                return h('div', null,
                    (form.marketing_assets || []).map(function(row, i) {
                        return h('div', { key:i, style:{ border:'1px solid '+borderColor, borderRadius:8, padding:S.sm, marginBottom:S.sm } },
                            h('div', { style:{ display:'flex', gap:S.sm, marginBottom:S.xs } },
                                h('input', { type:'text', value:row.title || '', placeholder:'Title (e.g. 728\u00D790 banner)',
                                    onChange:function(e){ setForm(function(f){ var n=Object.assign({},f); n.marketing_assets=n.marketing_assets.slice(); n.marketing_assets[i]=Object.assign({},n.marketing_assets[i],{title:e.target.value}); return n; }); },
                                    style:fs }),
                                h('button', { onClick:function(){ setForm(function(f){ var n=Object.assign({},f); n.marketing_assets=n.marketing_assets.filter(function(_,j){return j!==i;}); return n; }); },
                                    style:{ border:'none', background:'transparent', color:TC.error, cursor:'pointer', fontSize:18, flexShrink:0 } }, '\u00D7')),
                            h('input', { type:'text', value:row.image_url || '', placeholder:'Image/creative URL',
                                onChange:function(e){ setForm(function(f){ var n=Object.assign({},f); n.marketing_assets=n.marketing_assets.slice(); n.marketing_assets[i]=Object.assign({},n.marketing_assets[i],{image_url:e.target.value}); return n; }); },
                                style:Object.assign({}, fs, { marginBottom:S.xs, fontFamily:'ui-monospace, Menlo, Consolas, monospace', fontSize:12 }) }),
                            h('input', { type:'text', value:row.landing_url || '', placeholder:'Landing URL (gets ?ref= appended per affiliate)',
                                onChange:function(e){ setForm(function(f){ var n=Object.assign({},f); n.marketing_assets=n.marketing_assets.slice(); n.marketing_assets[i]=Object.assign({},n.marketing_assets[i],{landing_url:e.target.value}); return n; }); },
                                style:Object.assign({}, fs, { fontFamily:'ui-monospace, Menlo, Consolas, monospace', fontSize:12 }) })
                        );
                    }),
                    h(Btn, { variant:'secondary', mutedColor:mutedColor, borderColor:borderColor,
                        onClick:function(){ setForm(function(f){ var n=Object.assign({},f); n.marketing_assets=(n.marketing_assets||[]).concat([{title:'',image_url:'',landing_url:''}]); return n; }); } }, '+ Add asset')
                );
            }

            return h('div', null,
                licenseBanner(),
                flash(msg),
                h('div', { style:{ background:cardBg, border:'1px solid '+borderColor, borderRadius:12, padding:'8px '+S.xl+'px '+S.xl+'px' } },

                    /* General */
                    h(SectionHead, { id:'general', title:'General', ctx:edCtx }),
                    open.general && h('div', null,
                        h(FR, { label:'Enable affiliate program', note: !licenseActive ? 'Requires an active license to enable.' : 'Master switch. When off, no clicks are tracked and no commissions accrue.', ctx:edCtx },
                            h('label', { style:{ display:'inline-flex', alignItems:'center', gap:S.sm, fontSize:14, cursor: licenseActive ? 'pointer' : 'not-allowed', paddingTop:6, opacity: licenseActive ? 1 : 0.55 } },
                                h('input', { type:'checkbox', checked:!!form.enabled, disabled:!licenseActive, onChange:function(e){ set('enabled', e.target.checked); },
                                    style:{ accentColor:TC.accent, cursor: licenseActive ? 'pointer' : 'not-allowed', width:16, height:16 } }),
                                h('span', null, form.enabled ? 'Enabled' : 'Disabled'))),
                        h(FR, { label:'Cookie window (days)', note:'How long after a referral click a purchase is still attributed.', ctx:edCtx },
                            h('input', { type:'number', value:form.cookie_days, onChange:function(e){ set('cookie_days', e.target.value); }, style:Object.assign({}, fs, { maxWidth:120 }) })),
                        h(FR, { label:'Attribution model', note:'Last-click (v1).', ctx:edCtx },
                            h('select', { value:form.attribution_model, onChange:function(e){ set('attribution_model', e.target.value); }, style:Object.assign({}, selS, { maxWidth:200 }) },
                                h('option', { value:'last' }, 'Last click'),
                                h('option', { value:'first' }, 'First click (beta)'))),
                        h(FR, { label:'Auto-approve applications', note:'Off = you review each affiliate signup.', ctx:edCtx },
                            h('label', { style:{ display:'inline-flex', alignItems:'center', gap:S.sm, fontSize:14, cursor:'pointer', paddingTop:6 } },
                                h('input', { type:'checkbox', checked:!!form.auto_approve, onChange:function(e){ set('auto_approve', e.target.checked); }, style:{ accentColor:TC.accent, width:16, height:16 } }),
                                h('span', null, form.auto_approve ? 'On' : 'Off')))
                    ),

                    /* Commission */
                    h(SectionHead, { id:'commission', title:'Default commission', ctx:edCtx }),
                    open.commission && h('div', null,
                        h(FR, { label:'Commission type', ctx:edCtx },
                            h('select', { value:form.commission_type, onChange:function(e){ set('commission_type', e.target.value); }, style:Object.assign({}, selS, { maxWidth:200 }) },
                                h('option', { value:'percent' }, 'Percent of order'),
                                h('option', { value:'flat' }, 'Flat amount'),
                                h('option', { value:'tiered' }, 'Tiered'))),
                        (form.commission_type === 'percent' || form.commission_type === 'flat') && h(FR, { label: form.commission_type === 'flat' ? 'Flat amount ($)' : 'Rate (%)', ctx:edCtx },
                            h('input', { type:'number', step:'0.01', value:form.commission_rate, onChange:function(e){ set('commission_rate', e.target.value); }, style:Object.assign({}, fs, { maxWidth:160 }) })),
                        form.commission_type === 'tiered' && h(FR, { label:'Tiers', note:'Each tier: minimum cumulative sales \u2192 rate %. Highest matching tier applies.', ctx:edCtx }, tierRows())
                    ),

                    /* Payouts */
                    h(SectionHead, { id:'payouts', title:'Payouts', ctx:edCtx }),
                    open.payouts && h('div', null,
                        h(FR, { label:'Minimum payout', note:'Approved balance must reach this before a payout can be created.', ctx:edCtx },
                            h('input', { type:'number', step:'0.01', value:form.payout_minimum, onChange:function(e){ set('payout_minimum', e.target.value); }, style:Object.assign({}, fs, { maxWidth:160 }) })),
                        h(FR, { label:'Payout currency', ctx:edCtx },
                            h('input', { type:'text', maxLength:3, value:form.payout_currency, onChange:function(e){ set('payout_currency', e.target.value.toUpperCase()); }, style:Object.assign({}, fs, { maxWidth:100, textTransform:'uppercase' }) })),
                        h(FR, { label:'Default payout method', ctx:edCtx },
                            h('select', { value:form.default_payout_method, onChange:function(e){ set('default_payout_method', e.target.value); }, style:Object.assign({}, selS, { maxWidth:200 }) },
                                h('option', { value:'paypal' }, 'PayPal'),
                                h('option', { value:'bank' }, 'Bank transfer'),
                                h('option', { value:'manual' }, 'Manual'))),
                        h(FR, { label:'Auto-approve commissions', note:'Off = each conversion sits pending until you approve it.', ctx:edCtx },
                            h('label', { style:{ display:'inline-flex', alignItems:'center', gap:S.sm, fontSize:14, cursor:'pointer', paddingTop:6 } },
                                h('input', { type:'checkbox', checked:!!form.auto_approve_commissions, onChange:function(e){ set('auto_approve_commissions', e.target.checked); }, style:{ accentColor:TC.accent, width:16, height:16 } }),
                                h('span', null, form.auto_approve_commissions ? 'On' : 'Off')))
                    ),

                    /* Fraud */
                    h(SectionHead, { id:'fraud', title:'Fraud & tax', ctx:edCtx }),
                    open.fraud && h('div', null,
                        h(FR, { label:'Block self-referral', note:'An affiliate cannot earn on their own purchase.', ctx:edCtx },
                            h('label', { style:{ display:'inline-flex', alignItems:'center', gap:S.sm, fontSize:14, cursor:'pointer', paddingTop:6 } },
                                h('input', { type:'checkbox', checked:!!form.block_self_referral, onChange:function(e){ set('block_self_referral', e.target.checked); }, style:{ accentColor:TC.accent, width:16, height:16 } }),
                                h('span', null, form.block_self_referral ? 'On' : 'Off'))),
                        h(FR, { label:'De-dupe click window (minutes)', note:'Repeat clicks from the same visitor/IP within this window are ignored.', ctx:edCtx },
                            h('input', { type:'number', value:form.dedupe_click_minutes, onChange:function(e){ set('dedupe_click_minutes', e.target.value); }, style:Object.assign({}, fs, { maxWidth:120 }) })),
                        h(FR, { label:'Tax reporting threshold', note:'Annual commission above this flags the affiliate for 1099 reporting.', ctx:edCtx },
                            h('input', { type:'number', step:'0.01', value:form.tax_threshold, onChange:function(e){ set('tax_threshold', e.target.value); }, style:Object.assign({}, fs, { maxWidth:160 }) }))
                    ),

                    /* Marketing assets */
                    h(SectionHead, { id:'assets', title:'Branded marketing assets', ctx:edCtx }),
                    open.assets && h('div', null,
                        h('div', { style:{ fontSize:13, color:mutedColor, marginBottom:S.sm } }, 'Banners and links affiliates can grab from their dashboard. Each landing URL gets the affiliate\u2019s ?ref= code appended automatically.'),
                        assetRows()
                    ),

                    /* Terms */
                    h(SectionHead, { id:'terms', title:'Program terms', ctx:edCtx }),
                    open.terms && h('div', null,
                        h(FR, { label:'Terms shown to affiliates', note:'Displayed on the affiliate dashboard. Plain text.', ctx:edCtx },
                            h('textarea', { value:form.program_terms, rows:5, onChange:function(e){ set('program_terms', e.target.value); }, style:Object.assign({}, fs, { resize:'vertical', lineHeight:1.5, fontFamily:'inherit' }) }))
                    ),

                    h('div', { style:{ display:'flex', justifyContent:'flex-end', marginTop:S.xl, paddingTop:S.lg, borderTop:'1px solid '+borderColor } },
                        h(Btn, { variant:'primary', onClick:save, disabled:saving || !form, mutedColor:mutedColor, borderColor:borderColor }, saving ? 'Saving\u2026' : 'Save settings')
                    )
                )
            );
        }

        /* ══════════════════════════════════════════════════════════════════
         * VIEW: ANALYTICS
         * ══════════════════════════════════════════════════════════════════ */
        function Analytics() {
            var [loading, setLoading] = useState(true);
            var [data, setData] = useState(null);
            var [err, setErr] = useState('');
            var [range, setRange] = useState(30);

            function load() {
                setLoading(true); setErr('');
                afApiFetch('/analytics?days=' + range).then(function(d) {
                    setData(unwrap(d) || {});
                    setLoading(false);
                }).catch(function(e) { setErr(e.message || 'Failed to load analytics.'); setLoading(false); });
            }
            useEffect(function(){ load(); }, [range]);

            function tile(label, value, color) {
                return h('div', { style:{ flex:'1 1 140px', minWidth:130, background:cardBg, border:'1px solid '+borderColor, borderRadius:12, padding:'16px 18px' } },
                    h('div', { style:{ fontSize:24, fontWeight:700, color: color || textColor, fontVariantNumeric:'tabular-nums', lineHeight:1.1 } }, String(value)),
                    h('div', { style:{ fontSize:12, color:mutedColor, marginTop:4, textTransform:'uppercase', letterSpacing:'.3px' } }, label)
                );
            }

            var RANGES = [[30,'30 days'],[90,'90 days'],[365,'1 year'],[0,'All time']];
            var k = (data && data.kpis) || {};
            var lb = (data && data.leaderboard) || [];
            var ts = (data && data.timeseries) || [];
            var cur = (data && data.currency) || 'USD';
            var maxCommission = ts.reduce(function(m, r){ return Math.max(m, num(r.commission)); }, 0) || 1;

            return h('div', null,
                licenseBanner(),
                err && flash({ type:'error', text:err }),

                h('div', { style:{ display:'flex', gap:S.xs, flexWrap:'wrap', marginBottom:S.lg } },
                    RANGES.map(function(o) {
                        var a = range === o[0];
                        return h('button', { key:o[0], onClick:function(){ setRange(o[0]); },
                            style:{ padding:'6px 14px', borderRadius:99, fontSize:12, fontWeight:500, cursor:'pointer',
                                border:'1px solid ' + (a ? TC.accent : borderColor),
                                background: a ? (dark?'rgba(37,99,235,.12)':TC.accentBg) : 'transparent',
                                color: a ? TC.accent : mutedColor } }, o[1]);
                    })
                ),

                loading ? h(Spinner, { mutedColor:mutedColor })
                : h('div', null,
                    h('div', { style:{ display:'flex', gap:S.md, flexWrap:'wrap', marginBottom:S.xl } },
                        tile('Clicks', num(k.clicks)),
                        tile('Conversions', num(k.conversions)),
                        tile('Conv. rate', num(k.conversion_rate) + '%'),
                        tile('Revenue', money(k.revenue_generated, cur), TC.accent),
                        tile('Commission', money(k.commission_total, cur), TC.success),
                        tile('Outstanding', money(k.outstanding_payable, cur), TC.amber),
                        tile('Active affiliates', num(k.active_affiliates)),
                        tile('Pending apps', num(k.pending_applications), num(k.pending_applications) > 0 ? TC.amber : textColor)
                    ),

                    /* timeseries bars */
                    ts.length > 0 && h('div', { style:{ background:cardBg, border:'1px solid '+borderColor, borderRadius:12, padding:'8px '+S.xl+'px '+S.xl+'px', marginBottom:S.xl } },
                        h(SectionHead, { id:'ts', title:'Commission over time', ctx:{ mutedColor:mutedColor, borderColor:borderColor, textColor:textColor, open:{ ts:true }, onToggle:function(){} } }),
                        h('div', { style:{ display:'flex', alignItems:'flex-end', gap:2, height:120, marginTop:S.sm } },
                            ts.map(function(r, i) {
                                var pct = Math.round((num(r.commission) / maxCommission) * 100);
                                return h('div', { key:i, title: r.date + ': ' + money(r.commission, cur),
                                    style:{ flex:1, minWidth:2, height: Math.max(2, pct) + '%', background:TC.accent, borderRadius:'3px 3px 0 0', opacity:0.85 } });
                            }))
                    ),

                    /* leaderboard */
                    h('div', { style:{ background:cardBg, border:'1px solid '+borderColor, borderRadius:12, padding:'8px '+S.xl+'px '+S.xl+'px' } },
                        h(SectionHead, { id:'lb', title:'Top performers', ctx:{ mutedColor:mutedColor, borderColor:borderColor, textColor:textColor, open:{ lb:true }, onToggle:function(){} } }),
                        lb.length === 0
                            ? h('div', { style:{ fontSize:13, color:mutedColor, padding:'8px 0 4px' } }, 'No conversions in this window yet.')
                            : h('table', { style:{ width:'100%', borderCollapse:'collapse', fontSize:13, marginTop:S.xs } },
                                h('thead', null, h('tr', { style:{ textAlign:'left', color:mutedColor } },
                                    ['#','Affiliate','Conversions','Revenue','Commission'].map(function(c, i){ return h('th', { key:i, style:{ padding:'8px 4px', fontWeight:600, fontSize:11, textTransform:'uppercase', letterSpacing:'.3px' } }, c); }))),
                                h('tbody', null,
                                    lb.map(function(r, i) {
                                        return h('tr', { key:r.affiliate_id, style:{ borderTop:'1px solid '+borderColor, cursor:'pointer' },
                                                onClick:function(){ navigate('affiliates/' + r.affiliate_id); } },
                                            h('td', { style:{ padding:'10px 4px', color:mutedColor, fontWeight:600 } }, String(i + 1)),
                                            h('td', { style:{ padding:'10px 4px', color:textColor } }, r.display_name || r.code),
                                            h('td', { style:{ padding:'10px 4px', color:mutedColor, fontVariantNumeric:'tabular-nums' } }, String(num(r.conversions))),
                                            h('td', { style:{ padding:'10px 4px', color:mutedColor, fontVariantNumeric:'tabular-nums' } }, money(r.revenue, cur)),
                                            h('td', { style:{ padding:'10px 4px', color:textColor, fontVariantNumeric:'tabular-nums', fontWeight:600 } }, money(r.commission, cur))
                                        );
                                    })
                                )
                            )
                    )
                )
            );
        }

        /* ── Which view? ── */
        var tab = 'affiliates';
        if (subId === 'conversions')      tab = 'conversions';
        else if (subId === 'payouts')     tab = 'payouts';
        else if (subId === 'af-settings') tab = 'settings';
        else if (subId === 'analytics')   tab = 'analytics';
        else if (subId && subId !== '')   tab = 'detail';

        var tabs = [
            { v:'affiliates',  l:'Affiliates',  slug:'affiliates' },
            { v:'conversions', l:'Conversions', slug:'affiliates/conversions' },
            { v:'payouts',     l:'Payouts',     slug:'affiliates/payouts' },
            { v:'analytics',   l:'Analytics',   slug:'affiliates/analytics' },
            { v:'settings',    l:'Settings',    slug:'affiliates/af-settings' },
        ];

        return h(PageWrap, { title:'Affiliates', sub:'Run a branded affiliate program \u2014 track clicks & sales, manage commissions, and pay partners.', mutedColor:mutedColor },

            tab !== 'detail' && h('div', { style:{ display:'flex', gap:S.xs, marginBottom:S.xl, borderBottom:'1px solid '+borderColor, paddingBottom:1 } },
                tabs.map(function(t) {
                    var a = tab === t.v;
                    return h('button', { key:t.v, onClick:function(){ navigate(t.slug); },
                        style:{ padding:'10px 18px', border:'none', cursor:'pointer', fontSize:13, fontWeight:a?600:400,
                            background:'transparent', color:a?TC.accent:mutedColor, borderBottom:a?'2px solid '+TC.accent:'2px solid transparent',
                            marginBottom:-1, transition:'all .15s' } }, t.l);
                })
            ),

            tab === 'affiliates'    ? h(AffiliatesList)
            : tab === 'detail'      ? h(AffiliateDetail)
            : tab === 'conversions' ? h(Conversions)
            : tab === 'payouts'     ? h(Payouts)
            : tab === 'settings'    ? h(Settings)
            : tab === 'analytics'   ? h(Analytics)
            : null
        );
    }

    /* ════════════════════════════════════════════════════════════════════
     * SUBSCRIPTIONS PAGE — Recurring billing admin
     * ════════════════════════════════════════════════════════════════════ */
    function SubscriptionsPage({ subId, dark, cardBg, textColor, mutedColor, borderColor, tableHeaderBg }) {
        var fs = fieldSty(dark, borderColor);
        var selS = Object.assign({}, fs, { cursor:'pointer', appearance:'auto' });
        var SUBS_API = (Config.apiUrl || '').replace('/titancart/v1', '/titancart-subs/v1');

        function subsApiFetch(path, opts) {
            opts = opts || {};
            opts.headers = Object.assign({ 'Content-Type':'application/json', 'X-WP-Nonce': Config.nonce }, opts.headers || {});
            if (opts.body && typeof opts.body === 'object') opts.body = JSON.stringify(opts.body);
            return fetch(SUBS_API + path, opts).then(function(r) {
                return r.text().then(function(text) {
                    var data = null;
                    if (text) { try { data = JSON.parse(text); } catch(e) { data = null; } }
                    if (!r.ok) {
                        var msg = data && (data.message || data.error || JSON.stringify(data));
                        var err = new Error(msg || 'HTTP ' + r.status);
                        err.status = r.status; err.data = data;
                        return Promise.reject(err);
                    }
                    return data;
                });
            });
        }

        /* ── Determine active tab ── */
        var tab = 'list';
        if (subId === 'plans') tab = 'plans';
        else if (subId === 'analytics') tab = 'analytics';
        else if (subId === 'sub-settings') tab = 'settings';
        else if (subId && subId !== '') tab = 'detail';

        var tabs = [
            { v:'list', l:'Subscriptions', slug:'subscriptions' },
            { v:'plans', l:'Plans', slug:'subscriptions/plans' },
            { v:'analytics', l:'Analytics', slug:'subscriptions/analytics' },
            { v:'settings', l:'Settings', slug:'subscriptions/sub-settings' },
        ];

        /* ── Shared status config ── */
        var statusColors = { pending:'#D97706', active:'#16A34A', on_hold:'#2563EB', past_due:'#DC2626', cancelled:'#71797E', expired:'#9CA3AF' };
        var statusLabels = { pending:'Pending', active:'Active', on_hold:'On Hold', past_due:'Past Due', cancelled:'Cancelled', expired:'Expired' };
        var periodLabels = { day:'Day', week:'Week', month:'Month', year:'Year' };

        function StatusBadge(st) {
            var c = statusColors[st] || '#71797E';
            return h('span', { style:{ display:'inline-block', fontSize:11, fontWeight:600, padding:'2px 8px', borderRadius:4, color:c, background:c+'18', width:'fit-content' } }, statusLabels[st] || st);
        }

        function billingDesc(price, period, interval, currency) {
            var amt = tcFmtMoney(price, currency);
            var iv = parseInt(interval)||1;
            if (iv === 1) return amt + ' / ' + (period||'month');
            return amt + ' every ' + iv + ' ' + (period||'month') + 's';
        }

        /* ══════════════════════════════════════════════════════════════════
         * TAB: SUBSCRIPTIONS LIST
         * ══════════════════════════════════════════════════════════════════ */
        function SubsList() {
            var [items, setItems] = useState([]);
            var [total, setTotal] = useState(0);
            var [loading, setLoading] = useState(true);
            var [statusFilter, setStatusFilter] = useState('');
            var [search, setSearch] = useState('');
            var [page, setPage] = useState(1);
            var PER = TC_PER_PAGE || 20;

            function load() {
                setLoading(true);
                var q = '?page=' + page + '&per_page=' + PER;
                if (statusFilter) q += '&status=' + statusFilter;
                if (search) q += '&search=' + encodeURIComponent(search);
                subsApiFetch('/subscriptions' + q).then(function(d) {
                    setItems(d.data || d.items || d || []);
                    setTotal((d.meta && d.meta.total) || d.total || 0);
                    setLoading(false);
                }).catch(function() { setLoading(false); });
            }
            useEffect(function() { load(); }, [statusFilter, page]);

            var statTabs = ['', 'active', 'pending', 'on_hold', 'past_due', 'cancelled', 'expired'];

            return h('div', null,
                /* Status filter tabs */
                h('div', { style:{ display:'flex', gap:S.xs, marginBottom:S.lg, flexWrap:'wrap' } },
                    statTabs.map(function(st) {
                        var a = statusFilter === st;
                        return h('button', { key:st, onClick:function(){ setStatusFilter(st); setPage(1); },
                            style:{ padding:'6px 14px', border:'none', cursor:'pointer', fontSize:12, fontWeight:a?600:400,
                                background:a?TC.accent:'transparent', color:a?'#fff':mutedColor, borderRadius:6 } },
                            st ? (statusLabels[st]||st) : 'All'
                        );
                    })
                ),

                /* Search */
                h('div', { style:{ display:'flex', gap:S.sm, marginBottom:S.lg, alignItems:'center' } },
                    h('input', { style:Object.assign({}, fs, { flex:1, maxWidth:300 }), placeholder:'Search by customer or order…', value:search,
                        onChange:function(e){ setSearch(e.target.value); },
                        onKeyDown:function(e){ if (e.key==='Enter') load(); }
                    }),
                    h(Btn, { variant:'secondary', onClick:load, mutedColor, borderColor }, 'Search')
                ),

                /* Table */
                h(Card, { cardBg, borderColor, dark },
                    loading ? h('div', { style:{ padding:40, textAlign:'center', color:mutedColor } }, 'Loading…')
                    : !items.length ? h(EmptyState, { mutedColor, icon:'🔄', title:'No Subscriptions', sub:'Subscriptions will appear here when customers subscribe to plans.' })
                    : h('div', null,
                        h('div', { style:{ display:'grid', gridTemplateColumns:'0.5fr 1.5fr 1fr 1fr 0.8fr 0.8fr 1fr', padding:'10px 16px', borderBottom:'1px solid '+borderColor, background:tableHeaderBg, boxShadow:tableHeaderShadow } },
                            ['ID','Customer','Plan','Billing','Status','Renewals','Next Billing'].map(function(hd) {
                                return h('span', { key:hd, style:{ fontSize:10, fontWeight:500, color:mutedColor, textTransform:'uppercase', letterSpacing:'.5px' } }, hd);
                            })
                        ),
                        items.map(function(s) {
                            return h('div', { key:s.id, onClick:function(){ navigate('subscriptions/' + s.id); },
                                style:{ display:'grid', gridTemplateColumns:'0.5fr 1.5fr 1fr 1fr 0.8fr 0.8fr 1fr', padding:'12px 16px', borderBottom:'1px solid '+borderColor, alignItems:'center', cursor:'pointer', transition:'background .12s' },
                                onMouseEnter:function(e){ e.currentTarget.style.background = dark?'rgba(255,255,255,.02)':'#F9FAFB'; },
                                onMouseLeave:function(e){ e.currentTarget.style.background = 'transparent'; }
                            },
                                h('span', { style:{ fontSize:12, fontWeight:500, color:TC.accent } }, '#' + s.id),
                                h('span', { style:{ fontSize:12 } }, s.customer_name || s.customer_email || 'Customer #' + s.customer_id),
                                h('span', { style:{ fontSize:12 } }, s.plan_name || 'Plan #' + s.plan_id),
                                h('span', { style:{ fontSize:12, color:mutedColor } }, billingDesc(s.billing_amount, s.billing_period, s.billing_interval, s.currency)),
                                StatusBadge(s.status),
                                h('span', { style:{ fontSize:12, color:mutedColor } }, s.renewal_count + (s.max_renewals ? '/' + s.max_renewals : '')),
                                h('span', { style:{ fontSize:11, color:mutedColor } }, s.next_billing_at ? tcFmtDate(s.next_billing_at) : '—')
                            );
                        }),
                        /* Pagination */
                        total > PER && h('div', { style:{ display:'flex', justifyContent:'space-between', alignItems:'center', padding:'12px 16px', borderTop:'1px solid '+borderColor } },
                            h('span', { style:{ fontSize:11, color:mutedColor } }, 'Showing ' + items.length + ' of ' + total),
                            h('div', { style:{ display:'flex', gap:S.xs } },
                                Array.from({ length:Math.ceil(total/PER) }, function(_, i) {
                                    var p = i + 1;
                                    return h('button', { key:p, onClick:function(){ setPage(p); },
                                        style:{ width:28, height:28, borderRadius:6, border:p===page?'none':'1px solid '+borderColor,
                                            background:p===page?TC.accent:'transparent', color:p===page?'#fff':mutedColor, fontSize:11, cursor:'pointer', display:'flex', alignItems:'center', justifyContent:'center' }
                                    }, p);
                                })
                            )
                        )
                    )
                )
            );
        }

        /* ══════════════════════════════════════════════════════════════════
         * TAB: SUBSCRIPTION DETAIL
         * ══════════════════════════════════════════════════════════════════ */
        function SubDetail({ id }) {
            var [sub, setSub] = useState(null);
            var [events, setEvents] = useState([]);
            var [loading, setLoading] = useState(true);
            var [msg, setMsg] = useState(null);
            var [newStatus, setNewStatus] = useState('');

            function load() {
                setLoading(true);
                subsApiFetch('/subscriptions/' + id).then(function(d) {
                    var s = d.data || d.subscription || d;
                    setSub(s);
                    setEvents(s.events || d.events || d.timeline || []);
                    setNewStatus(s.status || '');
                    setLoading(false);
                }).catch(function() { setLoading(false); });
            }
            useEffect(function() { load(); }, [id]);

            function changeStatus() {
                if (!newStatus || newStatus === sub.status) return;
                subsApiFetch('/subscriptions/' + id + '/status', { method:'PUT', body:{ status:newStatus } }).then(function() {
                    setMsg({ type:'success', text:'Status updated.' }); load();
                }).catch(function(e) { setMsg({ type:'error', text:e.message }); });
            }

            if (loading) return h('div', { style:{ padding:40, textAlign:'center', color:mutedColor } }, 'Loading…');
            if (!sub) return h(EmptyState, { mutedColor, icon:'❌', title:'Subscription not found' });

            return h('div', null,
                h('div', { style:{ display:'flex', alignItems:'center', gap:S.md, marginBottom:S.xl } },
                    h('button', { onClick:function(){ navigate('subscriptions'); }, style:{ background:'none', border:'none', cursor:'pointer', color:mutedColor, fontSize:14 } }, '← Back'),
                    h('h2', { style:{ fontSize:18, fontWeight:700, margin:0 } }, 'Subscription #' + id),
                    StatusBadge(sub.status)
                ),

                msg && h('div', { style:{ padding:'10px 16px', borderRadius:8, marginBottom:S.lg, fontSize:13, fontWeight:500,
                    background:msg.type==='success'?(dark?'rgba(22,163,74,.12)':'#ECFDF5'):(dark?'rgba(220,38,38,.12)':'#FEF2F2'),
                    color:msg.type==='success'?'#16A34A':'#DC2626' } }, msg.text),

                h('div', { style:{ display:'grid', gridTemplateColumns:'1fr 360px', gap:S.lg } },
                    /* Left — details + timeline */
                    h('div', { style:{ display:'flex', flexDirection:'column', gap:S.lg } },
                        /* Details card */
                        h(Card, { cardBg, borderColor, dark, style:{ padding:S.xl } },
                            h('h3', { style:{ fontSize:15, fontWeight:600, marginBottom:S.lg } }, 'Subscription Details'),
                            h('div', { style:{ display:'grid', gridTemplateColumns:'1fr 1fr', gap:S.md } },
                                [
                                    ['Plan', sub.plan_name || 'Plan #' + sub.plan_id],
                                    ['Product', sub.product_name ? h('span', { style:{ color:TC.accent, cursor:'pointer' }, onClick:function(){ navigate('products/' + sub.product_id); } }, sub.product_name + (sub.product_code ? ' (' + sub.product_code + ')' : '')) : '—'],
                                    ['Billing', billingDesc(sub.billing_amount, sub.billing_period, sub.billing_interval, sub.currency)],
                                    ['Currency', sub.currency || 'USD'],
                                    ['Renewals', sub.renewal_count + (sub.max_renewals ? ' / ' + sub.max_renewals : ' (unlimited)')],
                                    ['Next Billing', sub.next_billing_at ? tcFmtDate(sub.next_billing_at) : '—'],
                                    ['Last Billed', sub.last_billed_at ? tcFmtDate(sub.last_billed_at) : '—'],
                                    ['Created', tcFmtDateTime(sub.created_at)],
                                    ['Trial End', sub.trial_end_at ? tcFmtDate(sub.trial_end_at) : '—'],
                                ].map(function(pair) {
                                    return h('div', { key:pair[0] },
                                        h('div', { style:{ fontSize:11, color:mutedColor, fontWeight:500, marginBottom:2 } }, pair[0]),
                                        h('div', { style:{ fontSize:13, fontWeight:500 } }, pair[1])
                                    );
                                })
                            )
                        ),

                        /* Timeline */
                        h(Card, { cardBg, borderColor, dark, style:{ padding:S.xl } },
                            h('h3', { style:{ fontSize:15, fontWeight:600, marginBottom:S.lg } }, 'Activity Timeline'),
                            !events.length
                                ? h('div', { style:{ fontSize:13, color:mutedColor, padding:'20px 0' } }, 'No events recorded.')
                                : h('div', { style:{ position:'relative', paddingLeft:24 } },
                                    h('div', { style:{ position:'absolute', left:7, top:8, bottom:8, width:2, background:borderColor } }),
                                    events.map(function(ev, i) {
                                        var evColor = { created:'#2563EB', activated:'#16A34A', renewed:'#16A34A', payment_failed:'#DC2626', paused:'#D97706', resumed:'#2563EB', cancelled:'#71797E', expired:'#9CA3AF', plan_changed:'#8B5CF6', payment_method_updated:'#06B6D4' };
                                        var c = evColor[ev.event] || '#71797E';
                                        return h('div', { key:ev.id || i, style:{ position:'relative', paddingBottom:16, paddingLeft:20 } },
                                            h('div', { style:{ position:'absolute', left:-2, top:4, width:16, height:16, borderRadius:'50%', background:c+'20', border:'3px solid '+c, boxSizing:'border-box' } }),
                                            h('div', { style:{ fontSize:13, fontWeight:500, marginBottom:2 } }, (ev.event||'').replace(/_/g,' ').replace(/\b\w/g, function(l){ return l.toUpperCase(); })),
                                            ev.note && h('div', { style:{ fontSize:12, color:mutedColor, marginBottom:2 } }, ev.note),
                                            h('div', { style:{ fontSize:11, color:mutedColor } },
                                                tcFmtDateTime(ev.created_at),
                                                ev.actor_type && ev.actor_type !== 'system' ? ' · ' + ev.actor_type : ''
                                            )
                                        );
                                    })
                                )
                        )
                    ),

                    /* Right sidebar */
                    h('div', { style:{ display:'flex', flexDirection:'column', gap:S.lg } },
                        /* Change status */
                        h(Card, { cardBg, borderColor, dark, style:{ padding:S.lg } },
                            h('h4', { style:{ fontSize:14, fontWeight:600, marginBottom:S.md } }, 'Update Status'),
                            h('select', { style:selS, value:newStatus, onChange:function(e){ setNewStatus(e.target.value); } },
                                Object.keys(statusLabels).map(function(st) {
                                    return h('option', { key:st, value:st }, statusLabels[st]);
                                })
                            ),
                            h(Btn, { variant:'primary', onClick:changeStatus, disabled:newStatus===sub.status, mutedColor, borderColor, style:{ marginTop:S.md, width:'100%' } }, 'Update Status')
                        ),

                        /* Customer info */
                        h(Card, { cardBg, borderColor, dark, style:{ padding:S.lg } },
                            h('h4', { style:{ fontSize:14, fontWeight:600, marginBottom:S.md } }, 'Customer'),
                            h('div', { style:{ fontSize:13 } }, sub.customer_name || 'Customer #' + sub.customer_id),
                            sub.customer_email && h('div', { style:{ fontSize:12, color:mutedColor, marginTop:4 } }, sub.customer_email),
                            sub.parent_order_id && h('div', { style:{ marginTop:S.md } },
                                h('div', { style:{ fontSize:11, color:mutedColor, marginBottom:2 } }, 'Original Order'),
                                h('span', { style:{ fontSize:13, color:TC.accent, cursor:'pointer' }, onClick:function(){ navigate('orders/' + sub.parent_order_id); } }, '#' + sub.parent_order_id)
                            )
                        ),

                        /* Payment info */
                        h(Card, { cardBg, borderColor, dark, style:{ padding:S.lg } },
                            h('h4', { style:{ fontSize:14, fontWeight:600, marginBottom:S.md } }, 'Payment'),
                            sub.payment_token_last_four
                                ? h('div', { style:{ fontSize:13 } }, (sub.payment_token_card_type||'Card') + ' ending in ' + sub.payment_token_last_four)
                                : h('div', { style:{ fontSize:12, color:mutedColor } }, 'No payment method on file')
                        )
                    )
                )
            );
        }

        /* ══════════════════════════════════════════════════════════════════
         * TAB: PLANS
         * ══════════════════════════════════════════════════════════════════ */
        function PlansTab() {
            var [plans, setPlans] = useState([]);
            var [loading, setLoading] = useState(true);
            var [editing, setEditing] = useState(null); // null=list, 'new'=create, {plan}=edit
            var [form, setForm] = useState({});
            var [saving, setSaving] = useState(false);
            var [msg, setMsg] = useState(null);
            var [custGroups, setCustGroups] = useState([]);

            function load() { setLoading(true); subsApiFetch('/plans').then(function(d) { setPlans(d.data||d.items||d||[]); setLoading(false); }).catch(function(){ setLoading(false); }); }
            useEffect(function(){
                load();
                apiFetch('/customer-groups').then(function(d) { setCustGroups(Array.isArray(d) ? d : d.data || []); }).catch(function(){});
            }, []);

            function openNew() {
                setEditing('new');
                setForm({ name:'', price:'', billing_period:'month', billing_interval:'1', signup_fee:'0', trial_days:'0', max_renewals:'0', site_limit:'1', usergroup_id:'', product_id:'', status:'active' });
            }
            function openEdit(plan) {
                setEditing(plan);
                setForm({ name:plan.name||'', price:plan.price||'', billing_period:plan.billing_period||'month', billing_interval:String(plan.billing_interval||1),
                    signup_fee:plan.signup_fee||'0', trial_days:String(plan.trial_days||0), max_renewals:String(plan.max_renewals||0),
                    site_limit:String(plan.site_limit != null ? plan.site_limit : 1),
                    usergroup_id:String(plan.usergroup_id||''), product_id:String(plan.product_id||''), status:plan.status||'active' });
            }
            function savePlan() {
                setSaving(true);
                var body = Object.assign({}, form, { price:parseFloat(form.price)||0, billing_interval:parseInt(form.billing_interval)||1,
                    signup_fee:parseFloat(form.signup_fee)||0, trial_days:parseInt(form.trial_days)||0, max_renewals:parseInt(form.max_renewals)||0,
                    site_limit:(form.site_limit === '' || form.site_limit == null) ? 1 : (parseInt(form.site_limit) || 0),
                    usergroup_id:form.usergroup_id ? parseInt(form.usergroup_id) : null, product_id:form.product_id ? parseInt(form.product_id) : null });
                var isNew = editing === 'new';
                var url = isNew ? '/plans' : '/plans/' + editing.id;
                subsApiFetch(url, { method:isNew?'POST':'PUT', body:body }).then(function() {
                    setSaving(false); setEditing(null); setMsg({ type:'success', text:isNew?'Plan created.':'Plan updated.' }); load();
                    setTimeout(function(){ setMsg(null); }, 3000);
                }).catch(function(e) { setSaving(false); setMsg({ type:'error', text:e.message }); });
            }
            function deletePlan(plan) {
                if (!confirm('Delete plan "' + (plan.name||'Untitled') + '"?')) return;
                subsApiFetch('/plans/' + plan.id, { method:'DELETE' }).then(function(d) {
                    if (typeof d === 'string') { setMsg({ type:'error', text:d }); } else { setMsg({ type:'success', text:'Plan deleted.' }); load(); }
                    setTimeout(function(){ setMsg(null); }, 3000);
                }).catch(function(e) { setMsg({ type:'error', text:e.message }); });
            }

            function setF(k, v) { setForm(function(prev) { var n = Object.assign({}, prev); n[k]=v; return n; }); }

            if (editing !== null) {
                var isNew = editing === 'new';
                return h('div', null,
                    h('div', { style:{ display:'flex', alignItems:'center', gap:S.md, marginBottom:S.xl } },
                        h('button', { onClick:function(){ setEditing(null); }, style:{ background:'none', border:'none', cursor:'pointer', color:mutedColor, fontSize:14 } }, '← Back'),
                        h('h3', { style:{ fontSize:16, fontWeight:600, margin:0 } }, isNew ? 'Create Plan' : 'Edit Plan')
                    ),
                    msg && h('div', { style:{ padding:'10px 16px', borderRadius:8, marginBottom:S.lg, fontSize:13, fontWeight:500,
                        background:msg.type==='success'?(dark?'rgba(22,163,74,.12)':'#ECFDF5'):(dark?'rgba(220,38,38,.12)':'#FEF2F2'), color:msg.type==='success'?'#16A34A':'#DC2626' } }, msg.text),
                    h(Card, { cardBg, borderColor, dark, style:{ padding:S.xl } },
                        h('div', { style:{ display:'grid', gridTemplateColumns:'1fr 1fr', gap:S.lg } },
                            h('div', null,
                                h('label', { style:{ fontSize:12, fontWeight:500, color:mutedColor, display:'block', marginBottom:S.xs } }, 'Plan Name *'),
                                h('input', { style:fs, value:form.name, onChange:function(e){ setF('name', e.target.value); }, placeholder:'e.g. Monthly, Annual' })
                            ),
                            h('div', null,
                                h('label', { style:{ fontSize:12, fontWeight:500, color:mutedColor, display:'block', marginBottom:S.xs } }, 'Price *'),
                                h('input', { style:fs, type:'number', step:'0.01', value:form.price, onChange:function(e){ setF('price', e.target.value); } })
                            ),
                            h('div', null,
                                h('label', { style:{ fontSize:12, fontWeight:500, color:mutedColor, display:'block', marginBottom:S.xs } }, 'Billing Period'),
                                h('select', { style:selS, value:form.billing_period, onChange:function(e){ setF('billing_period', e.target.value); } },
                                    h('option',{value:'day'},'Day'),h('option',{value:'week'},'Week'),h('option',{value:'month'},'Month'),h('option',{value:'year'},'Year')
                                )
                            ),
                            h('div', null,
                                h('label', { style:{ fontSize:12, fontWeight:500, color:mutedColor, display:'block', marginBottom:S.xs } }, 'Billing Interval'),
                                h('input', { style:fs, type:'number', min:'1', value:form.billing_interval, onChange:function(e){ setF('billing_interval', e.target.value); } }),
                                h('div', { style:{ fontSize:11, color:mutedColor, marginTop:4 } }, 'e.g. 3 + Month = every 3 months')
                            ),
                            h('div', null,
                                h('label', { style:{ fontSize:12, fontWeight:500, color:mutedColor, display:'block', marginBottom:S.xs } }, 'Signup Fee'),
                                h('input', { style:fs, type:'number', step:'0.01', value:form.signup_fee, onChange:function(e){ setF('signup_fee', e.target.value); } })
                            ),
                            h('div', null,
                                h('label', { style:{ fontSize:12, fontWeight:500, color:mutedColor, display:'block', marginBottom:S.xs } }, 'Trial Days'),
                                h('input', { style:fs, type:'number', min:'0', value:form.trial_days, onChange:function(e){ setF('trial_days', e.target.value); } })
                            ),
                            h('div', null,
                                h('label', { style:{ fontSize:12, fontWeight:500, color:mutedColor, display:'block', marginBottom:S.xs } }, 'Max Renewals'),
                                h('input', { style:fs, type:'number', min:'0', value:form.max_renewals, onChange:function(e){ setF('max_renewals', e.target.value); } }),
                                h('div', { style:{ fontSize:11, color:mutedColor, marginTop:4 } }, '0 = unlimited')
                            ),
                            h('div', null,
                                h('label', { style:{ fontSize:12, fontWeight:500, color:mutedColor, display:'block', marginBottom:S.xs } }, 'Site Limit'),
                                h('input', { style:fs, type:'number', min:'0', value:form.site_limit, onChange:function(e){ setF('site_limit', e.target.value); } }),
                                h('div', { style:{ fontSize:11, color:mutedColor, marginTop:4 } }, 'Licensed activations allowed per purchase. 0 = unlimited.')
                            ),
                            h('div', null,
                                h('label', { style:{ fontSize:12, fontWeight:500, color:mutedColor, display:'block', marginBottom:S.xs } }, 'Product ID (optional)'),
                                h('input', { style:fs, type:'number', value:form.product_id, onChange:function(e){ setF('product_id', e.target.value); }, placeholder:'Leave blank for standalone' })
                            ),
                            h('div', null,
                                h('label', { style:{ fontSize:12, fontWeight:500, color:mutedColor, display:'block', marginBottom:S.xs } }, 'Customer Group (optional)'),
                                h('select', { style:selS, value:form.usergroup_id, onChange:function(e){ setF('usergroup_id', e.target.value); } },
                                    h('option', { value:'' }, '— None —'),
                                    custGroups.map(function(g) { return h('option', { key:g.id, value:String(g.id) }, g.name || g.handle || ('Group #'+g.id)); })
                                ),
                                h('div', { style:{ fontSize:11, color:mutedColor, marginTop:4 } }, 'Auto-assign customer to this group when subscription activates')
                            ),
                            h('div', null,
                                h('label', { style:{ fontSize:12, fontWeight:500, color:mutedColor, display:'block', marginBottom:S.xs } }, 'Status'),
                                h('select', { style:selS, value:form.status, onChange:function(e){ setF('status', e.target.value); } },
                                    h('option',{value:'active'},'Active'),h('option',{value:'disabled'},'Disabled')
                                )
                            )
                        ),
                        h('div', { style:{ marginTop:S.xl, display:'flex', gap:S.sm } },
                            h(Btn, { variant:'primary', onClick:savePlan, disabled:saving||!form.name||!form.price, mutedColor, borderColor }, saving?'Saving…':(isNew?'Create Plan':'Save Changes')),
                            h(Btn, { variant:'secondary', onClick:function(){ setEditing(null); }, mutedColor, borderColor }, 'Cancel')
                        )
                    )
                );
            }

            return h('div', null,
                msg && h('div', { style:{ padding:'10px 16px', borderRadius:8, marginBottom:S.lg, fontSize:13, fontWeight:500,
                    background:msg.type==='success'?(dark?'rgba(22,163,74,.12)':'#ECFDF5'):(dark?'rgba(220,38,38,.12)':'#FEF2F2'), color:msg.type==='success'?'#16A34A':'#DC2626' } }, msg.text),
                h('div', { style:{ display:'flex', justifyContent:'flex-end', marginBottom:S.lg } },
                    h(Btn, { variant:'primary', onClick:openNew, mutedColor, borderColor }, '+ Create Plan')
                ),
                h(Card, { cardBg, borderColor, dark },
                    loading ? h('div', { style:{ padding:40, textAlign:'center', color:mutedColor } }, 'Loading…')
                    : !plans.length ? h(EmptyState, { mutedColor, icon:'📋', title:'No Plans', sub:'Create your first subscription plan.' })
                    : h('div', null,
                        h('div', { style:{ display:'grid', gridTemplateColumns:'0.5fr 2fr 1fr 1fr 0.6fr 0.6fr 0.6fr', padding:'10px 16px', borderBottom:'1px solid '+borderColor, background:tableHeaderBg, boxShadow:tableHeaderShadow } },
                            ['ID','Name','Billing','Product','Trial','Status',''].map(function(hd) {
                                return h('span', { key:hd, style:{ fontSize:10, fontWeight:500, color:mutedColor, textTransform:'uppercase', letterSpacing:'.5px' } }, hd);
                            })
                        ),
                        plans.map(function(p) {
                            return h('div', { key:p.id, style:{ display:'grid', gridTemplateColumns:'0.5fr 2fr 1fr 1fr 0.6fr 0.6fr 0.6fr', padding:'12px 16px', borderBottom:'1px solid '+borderColor, alignItems:'center', cursor:'pointer', transition:'background .12s' },
                                onMouseEnter:function(e){ e.currentTarget.style.background = dark?'rgba(255,255,255,.02)':'#F9FAFB'; },
                                onMouseLeave:function(e){ e.currentTarget.style.background = 'transparent'; },
                                onClick:function(){ openEdit(p); }
                            },
                                h('span', { style:{ fontSize:12, fontWeight:500, color:TC.accent } }, '#' + p.id),
                                h('span', { style:{ fontSize:13, fontWeight:500 } }, p.name || '(unnamed)'),
                                h('span', { style:{ fontSize:12, color:mutedColor } }, billingDesc(p.price, p.billing_period, p.billing_interval)),
                                h('span', { style:{ fontSize:12, color:mutedColor } }, p.product_id ? 'Product #' + p.product_id : 'Standalone'),
                                h('span', { style:{ fontSize:12, color:mutedColor } }, p.trial_days > 0 ? p.trial_days + 'd' : '—'),
                                h('span', null, h('span', { style:{ fontSize:10, fontWeight:600, padding:'2px 6px', borderRadius:4,
                                    background:p.status==='active'?'#ecfdf5':'#f5f5f5', color:p.status==='active'?'#16A34A':'#71797E' } }, p.status==='active'?'Active':'Disabled')),
                                h('button', { onClick:function(e){ e.stopPropagation(); deletePlan(p); },
                                    style:{ background:'none', border:'none', cursor:'pointer', color:TC.error, fontSize:12, fontWeight:500 } }, 'Delete')
                            );
                        })
                    )
                )
            );
        }

        /* ══════════════════════════════════════════════════════════════════
         * TAB: ANALYTICS
         * ══════════════════════════════════════════════════════════════════ */
        function AnalyticsTab() {
            var [data, setData] = useState(null);
            var [loading, setLoading] = useState(true);

            useEffect(function() {
                subsApiFetch('/analytics').then(function(d) {
                    setData(d.data || d); setLoading(false);
                }).catch(function() { setLoading(false); });
            }, []);

            if (loading) return h('div', { style:{ padding:40, textAlign:'center', color:mutedColor } }, 'Loading analytics…');
            if (!data) return h(EmptyState, { mutedColor, icon:'📊', title:'No Data', sub:'Analytics will appear once you have active subscriptions.' });

            var statCards = [
                { l:'MRR', v:tcFmtMoney(data.mrr), c:TC.accent },
                { l:'ARR', v:tcFmtMoney(data.arr), c:'#8B5CF6' },
                { l:'Active Subscriptions', v:data.active_count||0, c:TC.success },
                { l:'Churn Rate', v:(parseFloat(data.churn_rate||0)).toFixed(1) + '%', c:TC.error },
            ];

            return h('div', null,
                /* Stats cards */
                h('div', { style:{ display:'grid', gridTemplateColumns:'repeat(4,1fr)', gap:S.md, marginBottom:S.xl } },
                    statCards.map(function(s) {
                        return h(Card, { key:s.l, cardBg, borderColor, dark, style:{ padding:S.lg } },
                            h('div', { style:{ fontSize:11, color:mutedColor, fontWeight:500, marginBottom:S.xs } }, s.l),
                            h('div', { style:{ fontSize:22, fontWeight:700, color:s.c } }, s.v)
                        );
                    })
                ),

                /* Status distribution */
                h('div', { style:{ display:'grid', gridTemplateColumns:'1fr 1fr', gap:S.lg } },
                    h(Card, { cardBg, borderColor, dark, style:{ padding:S.xl } },
                        h('h3', { style:{ fontSize:15, fontWeight:600, marginBottom:S.lg } }, 'Status Distribution'),
                        data.status_counts ? Object.keys(data.status_counts).map(function(st) {
                            var count = data.status_counts[st];
                            var total = Object.values(data.status_counts).reduce(function(a,b){ return a+b; }, 0) || 1;
                            var pct = Math.round((count/total)*100);
                            var c = statusColors[st] || '#71797E';
                            return h('div', { key:st, style:{ display:'flex', alignItems:'center', gap:S.sm, marginBottom:S.md } },
                                h('div', { style:{ width:100, fontSize:12, fontWeight:500 } }, statusLabels[st]||st),
                                h('div', { style:{ flex:1, height:8, background:dark?'#262A36':'#f0f0f0', borderRadius:4, overflow:'hidden' } },
                                    h('div', { style:{ width:pct+'%', height:'100%', background:c, borderRadius:4, transition:'width .3s ease' } })
                                ),
                                h('span', { style:{ fontSize:12, fontWeight:600, color:c, width:50, textAlign:'right' } }, count)
                            );
                        }) : h('div', { style:{ color:mutedColor, fontSize:13 } }, 'No data')
                    ),

                    h(Card, { cardBg, borderColor, dark, style:{ padding:S.xl } },
                        h('h3', { style:{ fontSize:15, fontWeight:600, marginBottom:S.lg } }, 'Revenue by Plan'),
                        data.revenue_by_plan && data.revenue_by_plan.length ? data.revenue_by_plan.map(function(rp) {
                            return h('div', { key:rp.plan_id, style:{ display:'flex', justifyContent:'space-between', alignItems:'center', padding:'10px 0', borderBottom:'1px solid '+borderColor } },
                                h('span', { style:{ fontSize:13, fontWeight:500 } }, rp.plan_name || 'Plan #' + rp.plan_id),
                                h('div', { style:{ textAlign:'right' } },
                                    h('div', { style:{ fontSize:14, fontWeight:600 } }, tcFmtMoney(rp.total_revenue)),
                                    h('div', { style:{ fontSize:11, color:mutedColor } }, (rp.subscriber_count||0) + ' subscribers')
                                )
                            );
                        }) : h('div', { style:{ color:mutedColor, fontSize:13 } }, 'No revenue data yet')
                    )
                )
            );
        }

        /* ══════════════════════════════════════════════════════════════════
         * TAB: SETTINGS
         * ══════════════════════════════════════════════════════════════════ */
        function SettingsTab() {
            var [settings, setSettings] = useState({});
            var [loading, setLoading] = useState(true);
            var [saving, setSaving] = useState(false);
            var [msg, setMsg] = useState(null);

            useEffect(function() {
                subsApiFetch('/settings').then(function(d) { setSettings(d||{}); setLoading(false); }).catch(function(){ setLoading(false); });
            }, []);

            function setS(k, v) { setSettings(function(prev) { var n = Object.assign({}, prev); n[k]=v; return n; }); }

            function save() {
                setSaving(true);
                subsApiFetch('/settings', { method:'PUT', body:settings }).then(function() {
                    setSaving(false); setMsg({ type:'success', text:'Settings saved.' });
                    setTimeout(function(){ setMsg(null); }, 3000);
                }).catch(function(e) { setSaving(false); setMsg({ type:'error', text:e.message }); });
            }

            if (loading) return h('div', { style:{ padding:40, textAlign:'center', color:mutedColor } }, 'Loading settings…');

            function Toggle(key, label, hint) {
                return h('div', { style:{ display:'flex', alignItems:'center', justifyContent:'space-between', padding:'10px 0', borderBottom:'1px solid '+borderColor } },
                    h('div', null,
                        h('div', { style:{ fontSize:13, fontWeight:500 } }, label),
                        hint && h('div', { style:{ fontSize:11, color:mutedColor, marginTop:2 } }, hint)
                    ),
                    h('div', { onClick:function(){ setS(key, settings[key]==='1'?'0':'1'); }, style:{ width:40, height:22, flexShrink:0, marginLeft:12, borderRadius:11, cursor:'pointer', position:'relative', transition:'background .2s',
                        background:settings[key]==='1'?TC.accent:(dark?'#333':'#D1D5DB') } },
                        h('div', { style:{ width:18, height:18, borderRadius:9, background:'#fff', position:'absolute', top:2, left:settings[key]==='1'?20:2, transition:'left .2s', boxShadow:'0 1px 3px rgba(0,0,0,.15)' } })
                    )
                );
            }

            function TextInput(key, label, hint, type) {
                return h('div', { style:{ marginBottom:S.lg, paddingTop:S.md } },
                    h('label', { style:{ fontSize:12, fontWeight:500, color:mutedColor, display:'block', marginBottom:S.xs } }, label),
                    h('input', { style:fs, type:type||'text', value:settings[key]||'', onChange:function(e){ setS(key, e.target.value); } }),
                    hint && h('div', { style:{ fontSize:11, color:mutedColor, marginTop:4 } }, hint)
                );
            }

            return h('div', null,
                msg && h('div', { style:{ padding:'10px 16px', borderRadius:8, marginBottom:S.lg, fontSize:13, fontWeight:500,
                    background:msg.type==='success'?(dark?'rgba(22,163,74,.12)':'#ECFDF5'):(dark?'rgba(220,38,38,.12)':'#FEF2F2'), color:msg.type==='success'?'#16A34A':'#DC2626' } }, msg.text),

                h('div', { style:{ display:'grid', gridTemplateColumns:'1fr 1fr', gap:S.lg } },
                    /* General */
                    h(Card, { cardBg, borderColor, dark, style:{ padding:S.xl } },
                        h('h3', { style:{ fontSize:15, fontWeight:600, marginBottom:S.lg } }, 'General'),
                        Toggle('enabled', 'Enable Subscriptions', 'Master switch for subscription functionality'),
                        Toggle('mixed_checkout', 'Allow Mixed Cart', 'Allow subscriptions and one-time products in the same cart'),
                        Toggle('zero_initial_checkout', 'Zero Initial Checkout', 'Allow $0 initial order without payment method (free trial)'),
                        TextInput('add_to_cart_text', 'Subscribe Button Text', 'Button text on PDP for subscription products'),
                        TextInput('place_order_text', 'Place Order Button Text', 'Checkout button text for subscription orders')
                    ),

                    /* Preorder / Delayed Start */
                    h(Card, { cardBg, borderColor, dark, style:{ padding:S.xl } },
                        h('h3', { style:{ fontSize:15, fontWeight:600, marginBottom:S.lg } }, 'Preorder / Delayed Start'),
                        Toggle('preorder_mode', 'Preorder Mode', 'Take $0 at signup and defer the first charge to the launch date below. New subscriptions are free until then and the card is securely saved for the first charge. Automatically stands down once the launch date passes.'),
                        TextInput('preorder_floor_date', 'Launch / First-Charge Date', 'Subscriptions started while preorder mode is on bill for the first time on this date. Changing it re-schedules every not-yet-charged preorder subscription.', 'date')
                    ),

                    /* Customer Permissions */
                    h(Card, { cardBg, borderColor, dark, style:{ padding:S.xl } },
                        h('h3', { style:{ fontSize:15, fontWeight:600, marginBottom:S.lg } }, 'Customer Permissions'),
                        Toggle('customer_can_pause', 'Allow Pause', 'Customers can pause their subscription'),
                        TextInput('max_suspensions', 'Max Pauses Per Period', 'How many times per billing period', 'number'),
                        Toggle('customer_can_cancel', 'Allow Cancel', 'Customers can cancel their own subscription'),
                        Toggle('customer_can_switch_plan', 'Allow Plan Switch', 'Customers can upgrade/downgrade plans'),
                        Toggle('customer_can_update_payment', 'Allow Payment Update', 'Customers can update their payment method')
                    ),

                    /* Renewals */
                    h(Card, { cardBg, borderColor, dark, style:{ padding:S.xl } },
                        h('h3', { style:{ fontSize:15, fontWeight:600, marginBottom:S.lg } }, 'Renewals'),
                        Toggle('auto_renewal', 'Auto-Renewal', 'Automatically charge customers on billing date'),
                        Toggle('manual_renewal', 'Manual Renewal', 'Allow customers to manually renew'),
                        Toggle('early_renewal', 'Early Renewal', 'Allow customers to renew before billing date')
                    ),

                    /* Retry */
                    h(Card, { cardBg, borderColor, dark, style:{ padding:S.xl } },
                        h('h3', { style:{ fontSize:15, fontWeight:600, marginBottom:S.lg } }, 'Failed Payment Retry'),
                        Toggle('retry_enabled', 'Enable Retry', 'Automatically retry failed payments'),
                        TextInput('retry_max_attempts', 'Max Retry Attempts', 'How many times to retry', 'number'),
                        TextInput('retry_intervals', 'Retry Intervals (days)', 'Comma-separated days between retries, e.g. 1,3,7'),
                        TextInput('grace_period_days', 'Grace Period (days)', 'Days before cancelling a past-due subscription', 'number')
                    ),

                    /* Notifications */
                    h(Card, { cardBg, borderColor, dark, style:{ padding:S.xl } },
                        h('h3', { style:{ fontSize:15, fontWeight:600, marginBottom:S.lg } }, 'Notifications'),
                        TextInput('notify_renewal_days', 'Renewal Reminder (days before)', 'Send email N days before next billing', 'number'),
                        TextInput('notify_expiration_days', 'Expiration Warning (days before)', 'Send email N days before subscription expires', 'number'),
                        Toggle('notify_payment_failed', 'Payment Failed Email', 'Notify customer when payment fails'),
                        Toggle('notify_status_change', 'Status Change Email', 'Notify customer on status changes'),
                        Toggle('notify_manual_renewal', 'Manual Renewal Alert (store)', 'Email the store when a subscription needs manual renewal because it has no saved payment method (e.g. Cash on Delivery). The customer is not emailed.')
                    ),

                    /* Proration */
                    h(Card, { cardBg, borderColor, dark, style:{ padding:S.xl } },
                        h('h3', { style:{ fontSize:15, fontWeight:600, marginBottom:S.lg } }, 'Plan Switching'),
                        h('label', { style:{ fontSize:12, fontWeight:500, color:mutedColor, display:'block', marginBottom:S.xs } }, 'Proration Type'),
                        h('select', { style:selS, value:settings.proration_type||'day', onChange:function(e){ setS('proration_type', e.target.value); } },
                            h('option',{value:'day'},'Daily Proration'),h('option',{value:'none'},'No Proration'),h('option',{value:'full_period'},'Full Period Credit')
                        ),
                        h('div', { style:{ fontSize:11, color:mutedColor, marginTop:6 } }, 'How unused time on the current plan is credited when switching plans')
                    )
                ),

                h('div', { style:{ marginTop:S.xl, display:'flex', justifyContent:'flex-end' } },
                    h(Btn, { variant:'primary', onClick:save, disabled:saving, mutedColor, borderColor }, saving ? 'Saving…' : 'Save Settings')
                )
            );
        }

        /* ══════════════════════════════════════════════════════════════════
         * MAIN RENDER
         * ══════════════════════════════════════════════════════════════════ */
        return h(PageWrap, { title:'Subscriptions', sub:'Recurring billing & membership plans', mutedColor },

            /* Tab bar */
            tab !== 'detail' && h('div', { style:{ display:'flex', gap:S.xs, marginBottom:S.xl, borderBottom:'1px solid '+borderColor, paddingBottom:1 } },
                tabs.map(function(t) {
                    var a = tab === t.v;
                    return h('button', { key:t.v, onClick:function(){ navigate(t.slug); },
                        style:{ padding:'10px 18px', border:'none', cursor:'pointer', fontSize:13, fontWeight:a?600:400,
                            background:'transparent', color:a?TC.accent:mutedColor, borderBottom:a?'2px solid '+TC.accent:'2px solid transparent',
                            marginBottom:-1, transition:'all .15s' } },
                        t.l
                    );
                })
            ),

            tab === 'list' ? h(SubsList)
            : tab === 'detail' ? h(SubDetail, { id:subId })
            : tab === 'plans' ? h(PlansTab)
            : tab === 'analytics' ? h(AnalyticsTab)
            : tab === 'settings' ? h(SettingsTab)
            : null
        );
    }

    /* ════════════════════════════════════════════════════════════════════
     * CALENDAR PAGE — Month-view event calendar
     * ════════════════════════════════════════════════════════════════════ */
    function CalendarPage({ dark, cardBg, textColor, mutedColor, borderColor, tableHeaderBg }) {
        var fs = fieldSty(dark, borderColor);
        var labelSty = { fontSize:12, fontWeight:500, display:'block', marginBottom:S.xs, color:mutedColor };

        var now = new Date();
        var [year, setYear] = useState(now.getFullYear());
        var [month, setMonth] = useState(now.getMonth());
        var [events, setEvents] = useState([]);
        var [loading, setLoading] = useState(true);
        var [msg, setMsg] = useState(null);

        /* ── Selected day panel ── */
        var [selectedDate, setSelectedDate] = useState(null);
        var [dayEvents, setDayEvents] = useState([]);

        /* ── Event modal ── */
        var [modal, setModal] = useState(null);
        var [form, setForm] = useState({});
        var [saving, setSaving] = useState(false);

        function flash(type, text) { setMsg({type,text}); setTimeout(function(){ setMsg(null); }, 3000); }

        var MONTHS = ['January','February','March','April','May','June','July','August','September','October','November','December'];
        var DAYS = ['Sun','Mon','Tue','Wed','Thu','Fri','Sat'];

        var EVENT_COLORS = [
            { value:'#2563EB', label:'Blue' },
            { value:'#16A34A', label:'Green' },
            { value:'#DC2626', label:'Red' },
            { value:'#D97706', label:'Amber' },
            { value:'#7C3AED', label:'Purple' },
            { value:'#0891B2', label:'Teal' },
            { value:'#DB2777', label:'Pink' },
            { value:'#171717', label:'Black' },
        ];

        /* ═══ CALENDAR MATH ═══ */

        function daysInMonth(y, m) { return new Date(y, m + 1, 0).getDate(); }
        function firstDayOfMonth(y, m) { return new Date(y, m, 1).getDay(); }

        function getCalendarGrid() {
            var dim = daysInMonth(year, month);
            var startDay = firstDayOfMonth(year, month);
            var prevDim = month === 0 ? daysInMonth(year - 1, 11) : daysInMonth(year, month - 1);

            var cells = [];
            // Previous month trailing days
            for (var i = startDay - 1; i >= 0; i--) {
                cells.push({ day: prevDim - i, current: false, date: null });
            }
            // Current month
            for (var d = 1; d <= dim; d++) {
                var mm = String(month + 1).padStart(2, '0');
                var dd = String(d).padStart(2, '0');
                cells.push({ day: d, current: true, date: year + '-' + mm + '-' + dd });
            }
            // Next month leading days
            var remaining = 42 - cells.length;
            for (var n = 1; n <= remaining; n++) {
                cells.push({ day: n, current: false, date: null });
            }
            return cells;
        }

        function pad2(n) { return String(n).padStart(2, '0'); }

        function dateRange() {
            var first = new Date(year, month, 1);
            first.setDate(first.getDate() - first.getDay());
            var last = new Date(year, month + 1, 0);
            last.setDate(last.getDate() + (6 - last.getDay()));
            return {
                from: first.getFullYear() + '-' + pad2(first.getMonth()+1) + '-' + pad2(first.getDate()),
                to:   last.getFullYear() + '-' + pad2(last.getMonth()+1) + '-' + pad2(last.getDate())
            };
        }

        function isToday(dateStr) {
            if (!dateStr) return false;
            var t = new Date();
            return dateStr === t.getFullYear() + '-' + pad2(t.getMonth()+1) + '-' + pad2(t.getDate());
        }

        /* ═══ DATA LOADING ═══ */

        function loadEvents() {
            setLoading(true);
            var range = dateRange();
            apiFetch('/calendar/events?from=' + range.from + '&to=' + range.to).then(function(d) {
                setEvents(Array.isArray(d) ? d : []);
                setLoading(false);
            }).catch(function(){ setLoading(false); });
        }

        useEffect(function() { loadEvents(); }, [year, month]);

        function eventsForDate(dateStr) {
            if (!dateStr) return [];
            return events.filter(function(e) { return e.event_date === dateStr; });
        }

        /* ═══ NAVIGATION ═══ */

        function prevMonth() {
            if (month === 0) { setYear(year - 1); setMonth(11); }
            else setMonth(month - 1);
            setSelectedDate(null);
        }
        function nextMonth() {
            if (month === 11) { setYear(year + 1); setMonth(0); }
            else setMonth(month + 1);
            setSelectedDate(null);
        }
        function goToday() {
            var t = new Date();
            setYear(t.getFullYear()); setMonth(t.getMonth());
            setSelectedDate(t.getFullYear() + '-' + pad2(t.getMonth()+1) + '-' + pad2(t.getDate()));
        }

        /* ═══ DAY CLICK ═══ */

        function selectDay(cell) {
            if (!cell.date) return;
            setSelectedDate(cell.date);
            setDayEvents(eventsForDate(cell.date));
        }

        // Keep dayEvents in sync when events reload
        useEffect(function() {
            if (selectedDate) setDayEvents(eventsForDate(selectedDate));
        }, [events, selectedDate]);

        /* ═══ EVENT MODAL ═══ */

        function openNewEvent(dateStr) {
            setForm({ title:'', description:'', event_date:dateStr||selectedDate||'', start_time:'09:00', end_time:'10:00', all_day:false, color:'#2563EB' });
            setModal({ mode:'new' });
        }

        function openEditEvent(evt) {
            setForm({
                title:evt.title||'', description:evt.description||'',
                event_date:evt.event_date||'', start_time:(evt.start_time||'').substring(0,5), end_time:(evt.end_time||'').substring(0,5),
                all_day:!!Number(evt.all_day), color:evt.color||'#2563EB'
            });
            setModal({ mode:'edit', id:evt.id });
        }

        function saveEvent() {
            setSaving(true);
            var body = {
                title:form.title, description:form.description, event_date:form.event_date,
                start_time:form.all_day?null:form.start_time, end_time:form.all_day?null:form.end_time,
                all_day:form.all_day?1:0, color:form.color
            };
            var prom = modal.mode === 'edit'
                ? apiFetch('/calendar/events/' + modal.id, { method:'PUT', body:body })
                : apiFetch('/calendar/events', { method:'POST', body:body });
            prom.then(function() {
                setSaving(false); setModal(null);
                flash('success', modal.mode==='edit' ? 'Event updated.' : 'Event created.');
                loadEvents();
            }).catch(function(e) { setSaving(false); flash('error', e.message||'Failed.'); });
        }

        function deleteEvent(id) {
            if (!confirm('Delete this event?')) return;
            apiFetch('/calendar/events/' + id, { method:'DELETE' }).then(function() {
                flash('success', 'Event deleted.'); loadEvents();
            }).catch(function(){});
        }

        /* ═══ TIME FORMATTING ═══ */

        function fmtTime(t) {
            if (!t) return '';
            var parts = t.substring(0,5).split(':');
            var h = parseInt(parts[0],10);
            var m = parts[1];
            var ampm = h >= 12 ? 'PM' : 'AM';
            if (h === 0) h = 12;
            else if (h > 12) h -= 12;
            return h + ':' + m + ' ' + ampm;
        }

        function fmtDateLong(d) {
            if (!d) return '';
            var parts = d.split('-');
            var dt = new Date(parseInt(parts[0]), parseInt(parts[1])-1, parseInt(parts[2]));
            var opts = { weekday:'long', month:'long', day:'numeric', year:'numeric' };
            if (_tcTZ) opts.timeZone = _tcTZ;
            if (_tcDateFmt === 'dmy') return dt.toLocaleDateString('en-GB', opts);
            return dt.toLocaleDateString('en-US', opts);
        }

        /* ═══ RENDER ═══ */

        var grid = getCalendarGrid();
        var weeks = [];
        for (var w = 0; w < grid.length; w += 7) {
            weeks.push(grid.slice(w, w + 7));
        }

        var cellH = 100;
        var navBtnSty = { background:'transparent', border:'1px solid '+borderColor, borderRadius:8, width:34, height:34, cursor:'pointer', display:'flex', alignItems:'center', justifyContent:'center', color:textColor, fontSize:16, transition:'all .15s' };

        return h(PageWrap, { title:'Calendar', sub:'Schedule & manage events', mutedColor },

            msg && h('div', { style:{ padding:'10px 16px', borderRadius:8, marginBottom:S.lg, fontSize:13, fontWeight:500,
                background:msg.type==='success'?(dark?'rgba(22,163,74,.12)':'#ECFDF5'):(dark?'rgba(220,38,38,.12)':'#FEF2F2'),
                color:msg.type==='success'?'#16A34A':'#DC2626' } }, msg.text),

            /* ── Two-column layout: calendar + day panel ── */
            h('div', { style:{ display:'grid', gridTemplateColumns:selectedDate?'1fr 340px':'1fr', gap:S.xl, transition:'all .2s' } },

                /* ════ LEFT: Calendar ════ */
                h(Card, { cardBg, borderColor, dark },

                    /* ── Toolbar ── */
                    h('div', { style:{ display:'flex', justifyContent:'space-between', alignItems:'center', marginBottom:S.xl } },
                        h('div', { style:{ display:'flex', alignItems:'center', gap:S.sm } },
                            h('button', { onClick:prevMonth, style:navBtnSty }, '‹'),
                            h('button', { onClick:nextMonth, style:navBtnSty }, '›'),
                            h('button', { onClick:goToday, style:Object.assign({}, navBtnSty, { width:'auto', padding:'0 14px', fontSize:13, fontWeight:500 }) }, 'Today')
                        ),
                        h('h2', { style:{ fontSize:22, fontWeight:600, letterSpacing:'-.3px' } }, MONTHS[month] + ' ' + year),
                        h(Btn, { variant:'primary', onClick:function(){ openNewEvent(selectedDate||''); }, mutedColor, borderColor }, '+ New Event')
                    ),

                    /* ── Day-of-week headers ── */
                    h('div', { style:{ display:'grid', gridTemplateColumns:'repeat(7,1fr)', borderBottom:'1px solid '+borderColor } },
                        DAYS.map(function(d) {
                            return h('div', { key:d, style:{ padding:'10px 0', textAlign:'center', fontSize:12, fontWeight:600, color:mutedColor, textTransform:'uppercase', letterSpacing:'.5px' } }, d);
                        })
                    ),

                    /* ── Week rows ── */
                    weeks.map(function(week, wi) {
                        return h('div', { key:wi, style:{ display:'grid', gridTemplateColumns:'repeat(7,1fr)', borderBottom:wi < weeks.length-1 ? '1px solid '+borderColor : 'none' } },
                            week.map(function(cell, ci) {
                                var today = isToday(cell.date);
                                var selected = selectedDate === cell.date;
                                var dayEvts = cell.date ? eventsForDate(cell.date) : [];
                                var isWeekend = ci === 0 || ci === 6;

                                return h('div', { key:ci,
                                    onClick:function(){ selectDay(cell); },
                                    style:{
                                        minHeight:cellH, padding:S.sm, cursor:cell.date?'pointer':'default',
                                        borderRight:ci < 6 ? '1px solid '+borderColor : 'none',
                                        background: selected ? (dark?'rgba(37,99,235,.08)':'rgba(37,99,235,.04)')
                                            : today ? (dark?'rgba(255,220,40,.06)':'rgba(255,220,40,.12)')
                                            : isWeekend ? (dark?'rgba(255,255,255,.01)':'rgba(0,0,0,.01)')
                                            : 'transparent',
                                        transition:'background .12s',
                                        position:'relative',
                                    },
                                    onMouseEnter:function(e){ if(cell.date && !selected && !today) e.currentTarget.style.background=dark?'rgba(255,255,255,.03)':'rgba(0,0,0,.02)'; },
                                    onMouseLeave:function(e){ if(cell.date && !selected && !today) e.currentTarget.style.background=isWeekend?(dark?'rgba(255,255,255,.01)':'rgba(0,0,0,.01)'):'transparent'; }
                                },
                                    /* Day number */
                                    h('div', { style:{ display:'flex', justifyContent:'flex-end', marginBottom:S.xs } },
                                        h('span', { style:{
                                            width:28, height:28, borderRadius:'50%', display:'inline-flex', alignItems:'center', justifyContent:'center',
                                            fontSize:13, fontWeight:today?700:cell.current?500:400,
                                            color: today ? '#fff' : cell.current ? textColor : (dark?'rgba(255,255,255,.25)':'rgba(0,0,0,.25)'),
                                            background: today ? TC.accent : 'transparent',
                                        } }, cell.day)
                                    ),

                                    /* Event pills */
                                    dayEvts.slice(0, 3).map(function(evt) {
                                        return h('div', { key:evt.id, style:{
                                            fontSize:11, fontWeight:500, padding:'2px 6px', borderRadius:4, marginBottom:2,
                                            background:evt.color||'#2563EB', color:'#fff',
                                            overflow:'hidden', textOverflow:'ellipsis', whiteSpace:'nowrap',
                                            cursor:'pointer', lineHeight:'1.4',
                                        }, onClick:function(e){ e.stopPropagation(); openEditEvent(evt); }, title:evt.title },
                                            Number(evt.all_day) ? evt.title : fmtTime(evt.start_time) + ' ' + evt.title
                                        );
                                    }),
                                    dayEvts.length > 3 && h('div', { style:{ fontSize:10, color:TC.accent, fontWeight:600, padding:'1px 6px', cursor:'pointer' }, onClick:function(e){ e.stopPropagation(); selectDay(cell); } },
                                        '+' + (dayEvts.length - 3) + ' more'
                                    )
                                );
                            })
                        );
                    })
                ),

                /* ════ RIGHT: Day detail panel ════ */
                selectedDate && h(Card, { cardBg, borderColor, dark },
                    h('div', { style:{ display:'flex', justifyContent:'space-between', alignItems:'center', marginBottom:S.lg } },
                        h('div', null,
                            h('div', { style:{ fontSize:15, fontWeight:600, marginBottom:2 } }, fmtDateLong(selectedDate)),
                            h('div', { style:{ fontSize:12, color:mutedColor } }, dayEvents.length + ' event' + (dayEvents.length !== 1 ? 's' : ''))
                        ),
                        h('button', { onClick:function(){ setSelectedDate(null); }, style:{ background:'transparent', border:'none', cursor:'pointer', color:mutedColor, fontSize:18 } }, '✕')
                    ),

                    h('button', { onClick:function(){ openNewEvent(selectedDate); },
                        style:{ width:'100%', padding:'10px', border:'1px dashed '+borderColor, borderRadius:8, background:'transparent', cursor:'pointer', color:TC.accent, fontSize:13, fontWeight:500, marginBottom:S.lg, transition:'all .15s' },
                        onMouseEnter:function(e){ e.currentTarget.style.borderColor=TC.accent; },
                        onMouseLeave:function(e){ e.currentTarget.style.borderColor=borderColor; }
                    }, '+ Add Event'),

                    dayEvents.length === 0
                        ? h('div', { style:{ textAlign:'center', padding:S.xl+' 0', color:mutedColor } },
                            h('div', { style:{ fontSize:32, marginBottom:S.sm } }, '📅'),
                            h('div', { style:{ fontSize:13 } }, 'No events scheduled')
                        )
                        : dayEvents.map(function(evt) {
                            return h('div', { key:evt.id, style:{
                                padding:S.md, borderRadius:8, marginBottom:S.sm, cursor:'pointer',
                                borderLeft:'4px solid '+(evt.color||'#2563EB'),
                                background:dark?'rgba(255,255,255,.03)':'#FAFBFC',
                                transition:'all .12s',
                            },
                            onClick:function(){ openEditEvent(evt); },
                            onMouseEnter:function(e){ e.currentTarget.style.background=dark?'rgba(255,255,255,.05)':'#F3F5F7'; },
                            onMouseLeave:function(e){ e.currentTarget.style.background=dark?'rgba(255,255,255,.03)':'#FAFBFC'; }
                            },
                                h('div', { style:{ display:'flex', justifyContent:'space-between', alignItems:'flex-start' } },
                                    h('div', { style:{ flex:1, minWidth:0 } },
                                        h('div', { style:{ fontSize:13, fontWeight:600, marginBottom:3 } }, evt.title||'(Untitled)'),
                                        Number(evt.all_day)
                                            ? h('div', { style:{ fontSize:12, color:TC.accent, fontWeight:500 } }, 'All day')
                                            : h('div', { style:{ fontSize:12, color:mutedColor } }, fmtTime(evt.start_time) + (evt.end_time ? ' – ' + fmtTime(evt.end_time) : '')),
                                        evt.description && h('div', { style:{ fontSize:12, color:mutedColor, marginTop:4, overflow:'hidden', textOverflow:'ellipsis', whiteSpace:'nowrap' } }, evt.description)
                                    ),
                                    h('button', { onClick:function(e){ e.stopPropagation(); deleteEvent(evt.id); },
                                        style:{ background:'transparent', border:'none', cursor:'pointer', color:'#DC2626', fontSize:12, padding:'2px 4px', flexShrink:0, opacity:.6 },
                                        onMouseEnter:function(e){ e.currentTarget.style.opacity='1'; },
                                        onMouseLeave:function(e){ e.currentTarget.style.opacity='.6'; }
                                    }, '🗑')
                                )
                            );
                        })
                )
            ),

            /* ════ EVENT MODAL ════ */
            modal && h('div', { style:{ position:'fixed', inset:0, zIndex:9999, display:'flex', alignItems:'center', justifyContent:'center' } },
                h('div', { style:{ position:'absolute', inset:0, background:'rgba(0,0,0,.45)', backdropFilter:'blur(2px)' }, onClick:function(){ setModal(null); } }),
                h('div', { style:{ position:'relative', background:cardBg, borderRadius:14, padding:S.xxl, width:500, maxWidth:'92vw', maxHeight:'85vh', overflow:'auto', border:'1px solid '+borderColor, boxShadow:'0 24px 64px rgba(0,0,0,.2)' } },

                    h('div', { style:{ display:'flex', justifyContent:'space-between', alignItems:'center', marginBottom:S.xl } },
                        h('h3', { style:{ fontSize:17, fontWeight:600 } }, modal.mode === 'edit' ? 'Edit Event' : 'New Event'),
                        h('button', { onClick:function(){ setModal(null); }, style:{ background:'transparent', border:'none', fontSize:20, cursor:'pointer', color:mutedColor, lineHeight:1 } }, '✕')
                    ),

                    h('label', { style:labelSty }, 'Event Title *'),
                    h('input', { style:fs, value:form.title||'', placeholder:'Meeting, deadline, reminder…', onChange:function(e){ setForm(Object.assign({},form,{title:e.target.value})); } }),

                    h('label', { style:Object.assign({},labelSty,{marginTop:S.lg}) }, 'Date *'),
                    h('input', { style:fs, type:'date', value:form.event_date||'', onChange:function(e){ setForm(Object.assign({},form,{event_date:e.target.value})); } }),

                    /* All-day toggle */
                    h('label', { style:Object.assign({},labelSty,{marginTop:S.lg, display:'flex', alignItems:'center', gap:S.sm, cursor:'pointer'}) },
                        h('input', { type:'checkbox', checked:!!form.all_day, onChange:function(){ setForm(Object.assign({},form,{all_day:!form.all_day})); } }),
                        'All-day event'
                    ),

                    /* Time fields (hidden when all-day) */
                    !form.all_day && h('div', { style:{ display:'grid', gridTemplateColumns:'1fr 1fr', gap:S.md, marginTop:S.md } },
                        h('div', null,
                            h('label', { style:labelSty }, 'Start Time'),
                            h('input', { style:fs, type:'time', value:form.start_time||'', onChange:function(e){ setForm(Object.assign({},form,{start_time:e.target.value})); } })
                        ),
                        h('div', null,
                            h('label', { style:labelSty }, 'End Time'),
                            h('input', { style:fs, type:'time', value:form.end_time||'', onChange:function(e){ setForm(Object.assign({},form,{end_time:e.target.value})); } })
                        )
                    ),

                    /* Color picker */
                    h('label', { style:Object.assign({},labelSty,{marginTop:S.lg}) }, 'Color'),
                    h('div', { style:{ display:'flex', gap:S.sm, flexWrap:'wrap' } },
                        EVENT_COLORS.map(function(c) {
                            var active = form.color === c.value;
                            return h('button', { key:c.value, title:c.label,
                                onClick:function(){ setForm(Object.assign({},form,{color:c.value})); },
                                style:{
                                    width:32, height:32, borderRadius:'50%', border:active?'3px solid '+textColor:'3px solid transparent',
                                    background:c.value, cursor:'pointer', transition:'all .15s',
                                    boxShadow:active?'0 0 0 2px '+(dark?'#fff':'#000'):'none',
                                    outline:'none',
                                }
                            });
                        })
                    ),

                    h('label', { style:Object.assign({},labelSty,{marginTop:S.lg}) }, 'Notes'),
                    h('textarea', { style:Object.assign({},fs,{minHeight:80}), value:form.description||'', placeholder:'Optional notes…', onChange:function(e){ setForm(Object.assign({},form,{description:e.target.value})); } }),

                    /* Actions */
                    h('div', { style:{ display:'flex', justifyContent:'space-between', alignItems:'center', marginTop:S.xxl } },
                        modal.mode === 'edit'
                            ? h('button', { onClick:function(){ deleteEvent(modal.id); setModal(null); },
                                style:{ background:'transparent', border:'none', cursor:'pointer', color:'#DC2626', fontSize:13, fontWeight:500 } }, '🗑 Delete Event')
                            : h('div'),
                        h('div', { style:{ display:'flex', gap:S.sm } },
                            h(Btn, { variant:'outline', onClick:function(){ setModal(null); }, mutedColor, borderColor }, 'Cancel'),
                            h(Btn, { variant:'primary', onClick:saveEvent, disabled:saving||!form.title||!form.event_date, mutedColor, borderColor }, saving ? 'Saving…' : modal.mode==='edit' ? 'Update Event' : 'Create Event')
                        )
                    )
                )
            )
        );
    }

    /* ════════════════════════════════════════════════════════════════════
     * AGENDA PAGE — Admin task management
     * ════════════════════════════════════════════════════════════════════ */
    function TodoPage({ dark, cardBg, textColor, mutedColor, borderColor, tableHeaderBg }) {
        var fs = fieldSty(dark, borderColor);
        var labelSty = { fontSize:12, fontWeight:500, display:'block', marginBottom:S.xs, color:mutedColor };

        var [items, setItems] = useState([]);
        var [loading, setLoading] = useState(true);
        var [stats, setStats] = useState(null);
        var [users, setUsers] = useState([]);
        var [msg, setMsg] = useState(null);
        var [filter, setFilter] = useState('');
        var [prioFilter, setPrioFilter] = useState('');
        var [search, setSearch] = useState('');
        var [sortBy, setSortBy] = useState('');

        /* ── Editor state ── */
        var [editing, setEditing] = useState(null);
        var [form, setForm] = useState({});
        var [saving, setSaving] = useState(false);

        /* ── Quick add state ── */
        var [quickTitle, setQuickTitle] = useState('');
        var [quickPrio, setQuickPrio] = useState('medium');

        function flash(type, text) { setMsg({type,text}); setTimeout(function(){ setMsg(null); }, 3000); }

        var prioColors = { urgent:'#DC2626', high:'#D97706', medium:'#2563EB', low:'#71797E' };
        var prioLabels = { urgent:'Urgent', high:'High', medium:'Medium', low:'Low' };
        var statusLabels = { todo:'Pending', in_progress:'In Progress', done:'Done' };
        var statusColors = { todo:'#2563EB', in_progress:'#D97706', done:'#16A34A' };

        function load() {
            setLoading(true);
            var url = '/todos?';
            if (filter) url += 'status=' + filter + '&';
            if (prioFilter) url += 'priority=' + prioFilter + '&';
            if (search) url += 'search=' + encodeURIComponent(search) + '&';
            if (sortBy) url += 'sort=' + sortBy + '&';
            apiFetch(url).then(function(d) { setItems(Array.isArray(d)?d:[]); setLoading(false); }).catch(function(){ setLoading(false); });
            apiFetch('/todos/stats').then(function(d) { setStats(d); }).catch(function(){});
        }

        function loadUsers() {
            apiFetch('/todos/users').then(function(d) { setUsers(Array.isArray(d)?d:[]); }).catch(function(){});
        }

        useEffect(function() { load(); loadUsers(); }, []);
        useEffect(function() { load(); }, [filter, prioFilter, sortBy]);

        /* ── Quick add ── */
        function quickAdd() {
            if (!quickTitle.trim()) return;
            apiFetch('/todos', { method:'POST', body:{ title:quickTitle.trim(), priority:quickPrio } }).then(function() {
                setQuickTitle(''); setQuickPrio('medium'); flash('success','Task added.'); load();
            }).catch(function(e){ flash('error',e.message||'Failed.'); });
        }

        /* ── Toggle done ── */
        function toggleDone(item) {
            var newStatus = item.status === 'done' ? 'todo' : 'done';
            apiFetch('/todos/' + item.id, { method:'PUT', body:{ status:newStatus } }).then(function() { load(); }).catch(function(){});
        }

        /* ── Edit ── */
        function openEdit(item) {
            setEditing(item);
            setForm({ title:item.title||'', description:item.description||'', priority:item.priority||'medium', status:item.status||'todo', due_date:item.due_date||'', assigned_to:String(item.assigned_to||0) });
        }

        function saveEdit() {
            setSaving(true);
            var body = { title:form.title, description:form.description, priority:form.priority, status:form.status, due_date:form.due_date||null, assigned_to:Number(form.assigned_to)||0 };
            apiFetch('/todos/' + editing.id, { method:'PUT', body:body }).then(function() {
                setSaving(false); setEditing(null); flash('success','Updated.'); load();
            }).catch(function(e){ setSaving(false); flash('error',e.message||'Failed.'); });
        }

        function deleteTodo(item) {
            if (!confirm('Delete "' + (item.title||'Untitled') + '"?')) return;
            apiFetch('/todos/' + item.id, { method:'DELETE' }).then(function() { flash('success','Deleted.'); load(); }).catch(function(){});
        }

        /* ── Helpers ── */
        function isOverdue(item) {
            return item.due_date && item.status !== 'done' && item.due_date < new Date().toISOString().substring(0,10);
        }

        function formatDate(d) {
            if (!d) return '';
            var parts = d.split('-');
            return parts[1] + '/' + parts[2] + '/' + parts[0];
        }

        function prioDot(p) {
            return h('span', { style:{ width:8, height:8, borderRadius:'50%', background:prioColors[p]||'#71797E', display:'inline-block', flexShrink:0 } });
        }

        /* ── Status filter tabs ── */
        var statusTabs = [
            { v:'', l:'All', count:stats?stats.total:0 },
            { v:'todo', l:'Pending', count:stats?stats.todo:0 },
            { v:'in_progress', l:'In Progress', count:stats?stats.in_progress:0 },
            { v:'done', l:'Done', count:stats?stats.done:0 },
        ];

        return h(PageWrap, { title:'Agenda', sub:'Manage your tasks', mutedColor },

            msg && h('div', { style:{ padding:'10px 16px', borderRadius:8, marginBottom:S.lg, fontSize:13, fontWeight:500,
                background:msg.type==='success'?(dark?'rgba(22,163,74,.12)':'#ECFDF5'):(dark?'rgba(220,38,38,.12)':'#FEF2F2'),
                color:msg.type==='success'?'#16A34A':'#DC2626' } }, msg.text),

            /* ── Stats row ── */
            stats && h('div', { style:{ display:'grid', gridTemplateColumns:'repeat(5,1fr)', gap:S.md, marginBottom:S.xl } },
                [
                    { l:'Total Tasks', v:stats.total, c:textColor },
                    { l:'Pending', v:stats.todo, c:'#2563EB' },
                    { l:'In Progress', v:stats.in_progress, c:'#D97706' },
                    { l:'Done', v:stats.done, c:'#16A34A' },
                    { l:'Overdue', v:stats.overdue, c:'#DC2626' },
                ].map(function(s) {
                    return h(Card, { key:s.l, cardBg, borderColor, dark, style:{ padding:S.lg } },
                        h('div', { style:{ fontSize:11, color:mutedColor, fontWeight:500, marginBottom:S.xs } }, s.l),
                        h('div', { style:{ fontSize:22, fontWeight:700, color:s.c } }, s.v)
                    );
                })
            ),

            /* ── Quick add bar ── */
            h(Card, { cardBg, borderColor, dark, style:{ marginBottom:S.lg } },
                h('div', { style:{ display:'flex', gap:S.sm, alignItems:'center' } },
                    h('input', { style:Object.assign({}, fs, { flex:1 }), placeholder:'Add a new task…', value:quickTitle,
                        onChange:function(e){ setQuickTitle(e.target.value); },
                        onKeyDown:function(e){ if (e.key==='Enter') quickAdd(); }
                    }),
                    h('select', { style:Object.assign({}, fs, { width:120 }), value:quickPrio, onChange:function(e){ setQuickPrio(e.target.value); } },
                        h('option',{value:'low'},'Low'),h('option',{value:'medium'},'Medium'),h('option',{value:'high'},'High'),h('option',{value:'urgent'},'Urgent')
                    ),
                    h(Btn, { variant:'primary', onClick:quickAdd, mutedColor, borderColor }, '+ Add')
                )
            ),

            /* ── Filter bar ── */
            h('div', { style:{ display:'flex', justifyContent:'space-between', alignItems:'center', marginBottom:S.lg, flexWrap:'wrap', gap:S.sm } },
                h('div', { style:{ display:'flex', gap:S.xs } },
                    statusTabs.map(function(t) {
                        var a = filter===t.v;
                        return h('button', { key:t.v, onClick:function(){ setFilter(t.v); },
                            style:{ padding:'6px 14px', border:'none', cursor:'pointer', fontSize:12, fontWeight:a?600:400,
                                background:a?TC.accent:'transparent', color:a?'#fff':mutedColor, borderRadius:6, transition:'all .15s' } },
                            t.l + (t.count!==undefined?' ('+t.count+')':'')
                        );
                    })
                ),
                h('div', { style:{ display:'flex', gap:S.sm, alignItems:'center' } },
                    h('select', { style:Object.assign({}, fs, { width:120, fontSize:12 }), value:prioFilter, onChange:function(e){ setPrioFilter(e.target.value); } },
                        h('option',{value:''},'All Priorities'),h('option',{value:'urgent'},'Urgent'),h('option',{value:'high'},'High'),h('option',{value:'medium'},'Medium'),h('option',{value:'low'},'Low')
                    ),
                    h('select', { style:Object.assign({}, fs, { width:120, fontSize:12 }), value:sortBy, onChange:function(e){ setSortBy(e.target.value); } },
                        h('option',{value:''},'Position'),h('option',{value:'priority'},'Priority'),h('option',{value:'due_date'},'Due Date'),h('option',{value:'created_at'},'Newest')
                    ),
                    h('input', { style:Object.assign({}, fs, { width:180, fontSize:12 }), placeholder:'Search tasks…', value:search,
                        onChange:function(e){ setSearch(e.target.value); },
                        onKeyDown:function(e){ if (e.key==='Enter') load(); }
                    })
                )
            ),

            /* ── Edit modal / inline ── */
            editing && h('div', { style:{ position:'fixed', inset:0, zIndex:9999, display:'flex', alignItems:'center', justifyContent:'center' } },
                h('div', { style:{ position:'absolute', inset:0, background:'rgba(0,0,0,.4)' }, onClick:function(){ setEditing(null); } }),
                h('div', { style:{ position:'relative', background:cardBg, borderRadius:12, padding:S.xl, width:560, maxWidth:'92vw', maxHeight:'80vh', overflow:'auto', border:'1px solid '+borderColor, boxShadow:'0 20px 60px rgba(0,0,0,.2)' } },
                    h('div', { style:{ display:'flex', justifyContent:'space-between', alignItems:'center', marginBottom:S.lg } },
                        h('h3', { style:{ fontSize:16, fontWeight:600 } }, 'Edit Task'),
                        h('button', { onClick:function(){ setEditing(null); }, style:{ background:'transparent', border:'none', fontSize:18, cursor:'pointer', color:mutedColor } }, '✕')
                    ),
                    h('label', { style:labelSty }, 'Title *'),
                    h('input', { style:fs, value:form.title||'', onChange:function(e){ setForm(Object.assign({},form,{title:e.target.value})); } }),

                    h('label', { style:Object.assign({},labelSty,{marginTop:S.md}) }, 'Description'),
                    h('textarea', { style:Object.assign({},fs,{minHeight:100}), value:form.description||'', onChange:function(e){ setForm(Object.assign({},form,{description:e.target.value})); } }),

                    h('div', { style:{ display:'grid', gridTemplateColumns:'1fr 1fr', gap:S.md, marginTop:S.md } },
                        h('div', null,
                            h('label', { style:labelSty }, 'Priority'),
                            h('select', { style:fs, value:form.priority||'medium', onChange:function(e){ setForm(Object.assign({},form,{priority:e.target.value})); } },
                                h('option',{value:'low'},'Low'),h('option',{value:'medium'},'Medium'),h('option',{value:'high'},'High'),h('option',{value:'urgent'},'Urgent')
                            )
                        ),
                        h('div', null,
                            h('label', { style:labelSty }, 'Status'),
                            h('select', { style:fs, value:form.status||'todo', onChange:function(e){ setForm(Object.assign({},form,{status:e.target.value})); } },
                                h('option',{value:'todo'},'Pending'),h('option',{value:'in_progress'},'In Progress'),h('option',{value:'done'},'Done')
                            )
                        )
                    ),

                    h('div', { style:{ display:'grid', gridTemplateColumns:'1fr 1fr', gap:S.md, marginTop:S.md } },
                        h('div', null,
                            h('label', { style:labelSty }, 'Due Date'),
                            h('input', { style:fs, type:'date', value:form.due_date||'', onChange:function(e){ setForm(Object.assign({},form,{due_date:e.target.value})); } })
                        ),
                        h('div', null,
                            h('label', { style:labelSty }, 'Assign To'),
                            h('select', { style:fs, value:form.assigned_to||'0', onChange:function(e){ setForm(Object.assign({},form,{assigned_to:e.target.value})); } },
                                h('option',{value:'0'},'Unassigned'),
                                users.map(function(u){ return h('option',{key:u.ID,value:u.ID},u.display_name); })
                            )
                        )
                    ),

                    h('div', { style:{ display:'flex', justifyContent:'flex-end', gap:S.sm, marginTop:S.xl } },
                        h(Btn, { variant:'outline', onClick:function(){ setEditing(null); }, mutedColor, borderColor }, 'Cancel'),
                        h(Btn, { variant:'primary', onClick:saveEdit, disabled:saving, mutedColor, borderColor }, saving?'Saving…':'Save')
                    )
                )
            ),

            /* ── Task list ── */
            h(Card, { cardBg, borderColor, dark },
                loading ? h('div', { style:{ padding:S.xl, textAlign:'center', color:mutedColor } }, 'Loading…')
                : items.length === 0 ? h(EmptyState, { mutedColor, icon:'✅', title:'No tasks', sub: filter ? 'No tasks match the current filter.' : 'Add your first task above.' })
                : items.map(function(item) {
                    var done = item.status === 'done';
                    var overdue = isOverdue(item);
                    return h('div', { key:item.id, style:{ display:'flex', alignItems:'flex-start', gap:S.md, padding:'14px '+S.lg+'px', borderBottom:'1px solid '+borderColor, transition:'background .12s', opacity:done?0.55:1 },
                        onMouseEnter:function(e){ e.currentTarget.style.background=dark?'rgba(255,255,255,.02)':'#F9FAFB'; },
                        onMouseLeave:function(e){ e.currentTarget.style.background='transparent'; } },

                        /* Checkbox */
                        h('button', { onClick:function(){ toggleDone(item); },
                            style:{ width:22, height:22, borderRadius:6, border:'2px solid '+(done?'#16A34A':borderColor), background:done?'#16A34A':'transparent',
                                cursor:'pointer', display:'flex', alignItems:'center', justifyContent:'center', flexShrink:0, marginTop:2, transition:'all .15s' } },
                            done && h('span', { style:{ color:'#fff', fontSize:13, lineHeight:1 } }, '✓')
                        ),

                        /* Content */
                        h('div', { style:{ flex:1, minWidth:0 } },
                            h('div', { style:{ display:'flex', alignItems:'center', gap:S.sm, marginBottom:3 } },
                                prioDot(item.priority),
                                h('span', { style:{ fontSize:13, fontWeight:500, textDecoration:done?'line-through':'none', color:done?mutedColor:textColor } }, item.title||'(Untitled)'),
                                h('span', { style:{ fontSize:10, padding:'1px 7px', borderRadius:4, fontWeight:500, background:dark?'rgba(255,255,255,.05)':'#F3F4F6', color:statusColors[item.status]||mutedColor } }, statusLabels[item.status]||item.status)
                            ),
                            item.description && h('div', { style:{ fontSize:12, color:mutedColor, marginBottom:3, overflow:'hidden', textOverflow:'ellipsis', whiteSpace:'nowrap', maxWidth:500 } }, item.description),
                            h('div', { style:{ display:'flex', gap:S.lg, fontSize:11, color:mutedColor } },
                                item.due_date && h('span', { style:{ color:overdue?'#DC2626':mutedColor, fontWeight:overdue?600:400 } }, (overdue?'⚠ Overdue: ':'📅 ') + formatDate(item.due_date)),
                                item.assigned_name && h('span', null, '👤 ' + item.assigned_name),
                                item.completed_at && h('span', { style:{ color:'#16A34A' } }, '✓ Completed ' + tcFmtDate(item.completed_at))
                            )
                        ),

                        /* Actions */
                        h('div', { style:{ display:'flex', gap:S.xs, flexShrink:0 } },
                            h('button', { onClick:function(){ openEdit(item); }, style:{ background:'transparent', border:'none', cursor:'pointer', color:mutedColor, fontSize:13, padding:'4px' }, title:'Edit' }, '✏️'),
                            h('button', { onClick:function(){ deleteTodo(item); }, style:{ background:'transparent', border:'none', cursor:'pointer', color:'#DC2626', fontSize:13, padding:'4px' }, title:'Delete' }, '🗑')
                        )
                    );
                })
            )
        );
    }

    /* ════════════════════════════════════════════════════════════════════
     * EMAIL CAMPAIGNS PAGE — Newsletters, Templates, Autoresponders,
     * Tracking Campaigns, Subscriber Lists, Subscribers
     * ════════════════════════════════════════════════════════════════════ */

    /* ════════════════════════════════════════════════════════════════════
     * PRODUCT BUNDLES PAGE — "Buy Together" bundle management
     * ════════════════════════════════════════════════════════════════════ */

    /* ════════════════════════════════════════════════════════════════════
     * ACTIVE CARTS PAGE — View live guest/customer carts
     * ════════════════════════════════════════════════════════════════════ */
    function ActiveCartsPage({ subId, dark, cardBg, textColor, mutedColor, borderColor, tableHeaderBg }) {

        // ── Detail view
        if (subId) {
            return h(ActiveCartDetail, { cartId:Number(subId), dark, cardBg, textColor, mutedColor, borderColor, tableHeaderBg });
        }

        // ── List view
        var [carts, setCarts] = useState([]);
        var [loading, setLoading] = useState(true);
        var [total, setTotal] = useState(0);
        var [totalPages, setTotalPages] = useState(1);
        var [page, setPage] = useState(1);
        var [search, setSearch] = useState('');
        var [typeFilter, setTypeFilter] = useState('');
        var [stats, setStats] = useState(null);
        var [msg, setMsg] = useState(null);

        function load() {
            setLoading(true);
            var q = new URLSearchParams({ page:page, per_page:TC_PER_PAGE });
            if (typeFilter) q.set('type', typeFilter);
            if (search) q.set('search', search);
            apiFetch('/admin/carts?' + q.toString()).then(function(d) {
                setCarts(d.carts||[]); setTotal(d.total||0); setTotalPages(d.pages||1); setLoading(false);
            }).catch(function(){ setLoading(false); });
        }
        function loadStats() {
            apiFetch('/admin/carts/stats').then(function(d){ setStats(d); }).catch(function(){});
        }

        useEffect(function(){ load(); loadStats(); }, [page, typeFilter, search]);

        function deleteCart(c) {
            if (!confirm('Delete cart #' + c.id + '? This removes all items.')) return;
            apiFetch('/admin/carts/' + c.id, { method:'DELETE' }).then(function() {
                setMsg({ type:'success', text:'Cart deleted.' }); load(); loadStats();
                setTimeout(function(){ setMsg(null); }, 2500);
            }).catch(function(){});
        }

        function fmtDate(d) { if (!d) return '—'; var dt = tcParseDate(d); var now = new Date(); var diff = (now-dt)/1000; if (diff < 60) return 'Just now'; if (diff < 3600) return Math.floor(diff/60) + ' min ago'; if (diff < 86400) return Math.floor(diff/3600) + ' hr ago'; return tcFmtDateShort(d); }

        var typeTabs = [
            { v:'', l:'All', count: stats ? stats.total_carts : null },
            { v:'customer', l:'Customers', count: stats ? stats.customer_carts : null },
            { v:'guest', l:'Guests', count: stats ? stats.guest_carts : null },
        ];

        var colGrid = '0.5fr 2fr 0.5fr 0.8fr 0.5fr 1fr 0.4fr';

        return h(PageWrap, { title:'Active Carts', sub: stats ? stats.total_carts + ' carts · ' + tcFmtMoney(stats.total_value) + ' potential value' : '', mutedColor },

            msg && h('div', { style:{ padding:'10px 16px', borderRadius:8, marginBottom:S.lg, fontSize:13, fontWeight:500,
                background: msg.type==='success' ? (dark?'rgba(22,163,74,.12)':'#ECFDF5') : (dark?'rgba(220,38,38,.12)':'#FEF2F2'),
                color: msg.type==='success' ? '#16A34A' : '#DC2626' } }, msg.text),

            // Stats row
            stats && h('div', { style:{ display:'grid', gridTemplateColumns:'repeat(4,1fr)', gap:S.md, marginBottom:S.xl } },
                [
                    { label:'Total Carts', value:stats.total_carts, color:TC.accent },
                    { label:'Customer Carts', value:stats.customer_carts, color:'#16A34A' },
                    { label:'Guest Carts', value:stats.guest_carts, color:'#D97706' },
                    { label:'Total Items', value:stats.total_items, color:'#71797E' },
                ].map(function(s) {
                    return h(Card, { key:s.label, cardBg, borderColor, dark, style:{ padding:S.lg } },
                        h('div', { style:{ fontSize:11, color:mutedColor, fontWeight:500, marginBottom:S.xs } }, s.label),
                        h('div', { style:{ fontSize:22, fontWeight:700, letterSpacing:'-.3px', color:s.color } }, s.value)
                    );
                })
            ),

            // Tabs + search
            h('div', { style:{ display:'flex', gap:S.xs, marginBottom:S.lg, flexWrap:'wrap' } },
                typeTabs.map(function(t) {
                    var isActive = typeFilter === t.v;
                    return h('button', { key:t.v, onClick:function(){ setTypeFilter(t.v); setPage(1); },
                        style:{ padding:'6px '+S.md+'px', borderRadius:8, border:'none', cursor:'pointer', fontSize:12, fontWeight:isActive?600:400,
                                background:isActive?TC.accent:'transparent', color:isActive?'#fff':mutedColor, transition:'all .15s' } },
                        t.l + (t.count !== null ? ' (' + t.count + ')' : ''));
                })
            ),

            h('div', { style:{ marginBottom:S.lg } },
                h(SearchBox, { value:search, onChange:function(v){ setSearch(v); setPage(1); }, placeholder:'Search by customer name or email…', borderColor, dark, mutedColor })
            ),

            // Table
            h(Card, { cardBg, borderColor, dark, pad:0 },
                h('div', { style:{ display:'grid', gridTemplateColumns:colGrid, padding:S.md+'px '+S.xl+'px', borderBottom:'1px solid '+borderColor, background:tableHeaderBg, boxShadow:tableHeaderShadow } },
                    ['ID','Customer','Items','Subtotal','Currency','Last Active',''].map(function(col) {
                        return h('span', { key:col, style:{ fontSize:10, fontWeight:500, color:mutedColor, textTransform:'uppercase', letterSpacing:'.5px' } }, col);
                    })
                ),

                loading && h(Spinner, { mutedColor }),
                !loading && carts.length === 0 && h(EmptyState, { mutedColor, icon:'🛒', title:'No active carts', sub:'Carts will appear here when visitors add items.' }),

                !loading && carts.map(function(c, i) {
                    var custName = c.customer_id
                        ? [c.first_name, c.last_name].filter(Boolean).join(' ') || c.email || 'Customer #' + c.customer_id
                        : (c.session_id ? 'Session ' + c.session_id.substring(0, 8) : '');
                    var isGuest = !c.customer_id;
                    return h('div', { key:c.id,
                        style:{ display:'grid', gridTemplateColumns:colGrid, padding:'12px '+S.xl+'px', borderBottom: i<carts.length-1 ? '1px solid '+borderColor : 'none', alignItems:'center', cursor:'pointer', transition:'background .12s' },
                        onMouseEnter:function(e){ e.currentTarget.style.background=dark?'rgba(255,255,255,.02)':'#F9FAFB'; },
                        onMouseLeave:function(e){ e.currentTarget.style.background='transparent'; },
                        onClick:function(){ navigate('active-carts/' + c.id); }
                    },
                        h('span', { style:{ fontSize:12, fontWeight:500, color:TC.accent } }, '#' + c.id),
                        h('div', null,
                            h('div', { style:{ display:'flex', alignItems:'center', gap:S.sm } },
                                custName && h('span', { style:{ fontSize:12, fontWeight:500 } }, custName),
                                isGuest && h('span', { style:{ fontSize:10, padding:'1px 6px', borderRadius:4, background:dark?'rgba(217,164,65,.12)':'#FEF6EC', color:'#D97706', fontWeight:500 } }, 'Guest')
                            ),
                            c.email && h('div', { style:{ fontSize:11, color:mutedColor } }, c.email)
                        ),
                        h('span', { style:{ fontSize:12, fontWeight:500 } }, c.item_count || 0),
                        h('span', { style:{ fontSize:13, fontWeight:600 } }, tcFmtMoney(c.subtotal)),
                        h('span', { style:{ fontSize:11, color:mutedColor } }, c.currency || 'USD'),
                        h('span', { style:{ fontSize:11, color:mutedColor } }, fmtDate(c.updated_at)),
                        h('button', { onClick:function(e){ e.stopPropagation(); deleteCart(c); },
                            style:{ background:'transparent', border:'1px solid rgba(220,38,38,.2)', borderRadius:6, padding:'3px 8px', fontSize:11, cursor:'pointer', color:TC.error } }, '🗑')
                    );
                }),

                !loading && h('div', { style:{ display:'flex', justifyContent:'space-between', alignItems:'center', padding:S.md+'px '+S.xl+'px', borderTop:'1px solid '+borderColor } },
                    h('span', { style:{ fontSize:11, color:mutedColor } }, 'Showing ' + carts.length + ' of ' + total),
                    h(Pager, { page, totalPages, onPage:setPage, mutedColor, borderColor })
                )
            )
        );
    }

    /* ── Active Cart Detail view ── */
    function ActiveCartDetail({ cartId, dark, cardBg, textColor, mutedColor, borderColor, tableHeaderBg }) {
        var [cart, setCart] = useState(null);
        var [loading, setLoading] = useState(true);

        useEffect(function() {
            apiFetch('/admin/carts/' + cartId).then(function(d) { setCart(d); setLoading(false); }).catch(function(){ setLoading(false); });
        }, [cartId]);

        function fmtDate(d) { return tcFmtDateTime(d); }

        if (loading) return h('main', { style:{ flex:1, overflow:'auto', padding:S.xxl } }, h(Spinner, { mutedColor }));
        if (!cart) return h('main', { style:{ flex:1, overflow:'auto', padding:S.xxl } }, h(EmptyState, { mutedColor, icon:'🛒', title:'Cart not found' }));

        var custName = cart.customer_id
            ? [cart.first_name, cart.last_name].filter(Boolean).join(' ') || cart.email || 'Customer #' + cart.customer_id
            : 'Guest';

        return h('main', { style:{ flex:1, overflow:'auto', padding:S.xxl } },
            // Header
            h('div', { style:{ display:'flex', alignItems:'center', gap:S.md, marginBottom:S.xl } },
                h('button', { onClick:function(){ navigate('active-carts'); },
                    style:{ background:'transparent', border:'1px solid '+borderColor, borderRadius:8, padding:'6px 10px', cursor:'pointer', color:mutedColor, display:'flex', alignItems:'center', gap:4, fontSize:13 } },
                    h('span', { style:{ fontSize:16 } }, '←'), ' Active Carts'
                ),
                h('h2', { style:{ fontSize:20, fontWeight:700, letterSpacing:'-.3px' } }, 'Cart #' + cartId)
            ),

            h('div', { style:{ display:'grid', gridTemplateColumns:'1fr 320px', gap:S.lg, alignItems:'start' } },

                // Left — items table
                h(Card, { cardBg, borderColor, dark },
                    h('h3', { style:{ fontSize:14, fontWeight:600, marginBottom:S.lg } }, 'Cart Items (' + (cart.items||[]).length + ')'),
                    (cart.items||[]).length === 0
                        ? h('p', { style:{ fontSize:13, color:mutedColor } }, 'Cart is empty.')
                        : h('div', null,
                            h('div', { style:{ display:'grid', gridTemplateColumns:'1fr 100px 100px 100px', padding:S.sm+'px 0', borderBottom:'1px solid '+borderColor, marginBottom:S.sm } },
                                ['Product','Price','Qty','Total'].map(function(col) { return h('span', { key:col, style:{ fontSize:10, fontWeight:500, color:mutedColor, textTransform:'uppercase', letterSpacing:'.5px' } }, col); })
                            ),
                            (cart.items||[]).map(function(item, i) {
                                var lineTotal = (Number(item.unit_price)||0) * (Number(item.quantity)||0);
                                return h('div', { key:item.id, style:{ display:'grid', gridTemplateColumns:'1fr 100px 100px 100px', padding:'10px 0', alignItems:'center', borderBottom: i<cart.items.length-1 ? '1px solid '+borderColor : 'none' } },
                                    h('div', null,
                                        h('div', { style:{ fontSize:13, fontWeight:500 } }, item.product_name || 'Product #' + item.product_id),
                                        item.product_code && h('div', { style:{ fontSize:11, color:mutedColor } }, item.product_code)
                                    ),
                                    h('span', { style:{ fontSize:12 } }, tcFmtMoney(item.unit_price)),
                                    h('span', { style:{ fontSize:12, fontWeight:500 } }, item.quantity),
                                    h('span', { style:{ fontSize:12, fontWeight:600 } }, tcFmtMoney(lineTotal))
                                );
                            }),
                            h('div', { style:{ display:'flex', justifyContent:'flex-end', paddingTop:S.md, borderTop:'2px solid '+borderColor, marginTop:S.sm } },
                                h('span', { style:{ fontSize:14, fontWeight:700 } }, 'Subtotal: ' + tcFmtMoney(cart.subtotal))
                            )
                        )
                ),

                // Right — cart info
                h('div', null,
                    h(Card, { cardBg, borderColor, dark, style:{ marginBottom:S.lg } },
                        h('h4', { style:{ fontSize:13, fontWeight:600, marginBottom:S.lg, color:mutedColor, textTransform:'uppercase', letterSpacing:'.5px' } }, 'Cart Info'),
                        [
                            ['Customer', custName],
                            ['Email', cart.email || '—'],
                            ['Type', cart.customer_id ? 'Registered' : 'Guest'],
                            ['Currency', cart.currency || 'USD'],
                            ['Coupon', cart.coupon_code || 'None'],
                            ['Created', fmtDate(cart.created_at)],
                            ['Last Active', fmtDate(cart.updated_at)],
                        ].map(function(pair) {
                            return h('div', { key:pair[0], style:{ display:'flex', justifyContent:'space-between', fontSize:12, marginBottom:S.sm } },
                                h('span', { style:{ color:mutedColor } }, pair[0]),
                                h('span', { style:{ fontWeight:500 } }, pair[1])
                            );
                        })
                    ),
                    cart.customer_id && h(Card, { cardBg, borderColor, dark },
                        h('a', { href:'#', onClick:function(e){ e.preventDefault(); navigate('customers/'+cart.customer_id); }, style:{ fontSize:12, color:TC.accent, textDecoration:'none' } }, '→ View customer profile')
                    ),
                    cart.notes && h(Card, { cardBg, borderColor, dark, style:{ marginTop:S.lg } },
                        h('h4', { style:{ fontSize:13, fontWeight:600, marginBottom:S.sm, color:mutedColor, textTransform:'uppercase', letterSpacing:'.5px' } }, 'Notes'),
                        h('p', { style:{ fontSize:12, color:textColor, margin:0 } }, cart.notes)
                    )
                )
            )
        );
    }

    /* ════════════════════════════════════════════════════════════════════
     * GIFT CARDS PAGE — List + Create/Edit
     * ════════════════════════════════════════════════════════════════════ */

    /* ════════════════════════════════
     * PAYMENTS PAGE — Full rebuild
     * List view + Editor view (General & Configure tabs)
     * ════════════════════════════════ */
    function PaymentsPage({ dark, cardBg, textColor, mutedColor, borderColor, tableHeaderBg, backBtn }) {
        /* ── State ── */
        var _view   = useState('list');    var view   = _view[0];   var setView   = _view[1];
        var _methods = useState([]);       var methods = _methods[0]; var setMethods = _methods[1];
        var _loading = useState(true);     var loading = _loading[0]; var setLoading = _loading[1];
        var _editId  = useState(null);     var editId  = _editId[0];  var setEditId  = _editId[1];
        var _form    = useState({});       var form    = _form[0];    var setForm    = _form[1];
        var _saving  = useState(false);    var saving  = _saving[0];  var setSaving  = _saving[1];
        var _tab     = useState('general'); var tab    = _tab[0];     var setTab     = _tab[1];
        var _gateways = useState([]);      var gateways = _gateways[0]; var setGateways = _gateways[1];
        var _gwFields = useState([]);      var gwFields = _gwFields[0]; var setGwFields = _gwFields[1];
        var _loadingFields = useState(false); var loadingFields = _loadingFields[0]; var setLoadingFields = _loadingFields[1];
        var _testing = useState(false);       var testing = _testing[0];       var setTesting = _testing[1];
        var _testResult = useState(null);     var testResult = _testResult[0]; var setTestResult = _testResult[1];

        var fs = fieldSty(dark, borderColor);
        var selSty = Object.assign({}, fs, { cursor:'pointer' });

        /* ── Gateway metadata ── */
        var GW_META = {
            stripe:        { icon:'⚡', color:'#635BFF' },
            paypal:        { icon:'🅿️', color:'#003087' },
            cod:           { icon:'💵', color:'#16A34A' },
            bank_transfer: { icon:'🏦', color:'#2563EB' },
            check:         { icon:'📬', color:'#D97706' },
            authorize_net: { icon:'🔒', color:'#1C4587' },
            square:        { icon:'⬛', color:'#006AFF' }
        };

        /* ── Load methods ── */
        useEffect(function() {
            apiFetch('/payment-methods').then(function(d) {
                setMethods(Array.isArray(d) ? d : (d.methods || []));
                setLoading(false);
            }).catch(function(){ setLoading(false); });
            /* Load available gateway types for the Add form */
            apiFetch('/payment-gateways').then(function(d) {
                setGateways(Array.isArray(d) ? d : []);
            }).catch(function(){});
        }, []);

        function refreshMethods() {
            apiFetch('/payment-methods').then(function(d) {
                setMethods(Array.isArray(d) ? d : (d.methods || []));
            });
        }

        /* ── Open new method ── */
        function openNew() {
            setForm({
                gateway: 'stripe', name:'', description:'', instructions:'',
                status: 'active', position: '0', surcharge: '0', surcharge_type: 'A',
                test_mode: false, settings: {}
            });
            setEditId(null);
            setTab('general');
            setGwFields([]);
            setTestResult(null);
            setView('edit');
            /* Pre-load fields for default gateway */
            loadFieldsForGateway('stripe');
        }

        /* ── Open edit method ── */
        function openEdit(m) {
            var mid = Number(m.id || m.method_id);
            setEditId(mid);
            setTab('general');
            setLoadingFields(true);
            setTestResult(null);
            /* Fetch full method data (includes settings) */
            apiFetch('/payment-methods/' + mid).then(function(full) {
                setForm({
                    gateway: full.gateway || '',
                    name: full.label || full.name || '',
                    description: full.description || '',
                    instructions: full.instructions || '',
                    status: full.status || 'active',
                    position: String(full.position || 0),
                    surcharge: String(full.surcharge || 0),
                    surcharge_type: full.surcharge_type || 'A',
                    test_mode: !!Number(full.test_mode),
                    settings: full.settings || {}
                });
                setView('edit');
                /* Load gateway fields */
                loadFieldsForGateway(full.gateway);
            }).catch(function(){ setLoadingFields(false); });
        }

        /* ── Load dynamic fields for a gateway slug ── */
        function loadFieldsForGateway(slug) {
            if (!slug) { setGwFields([]); setLoadingFields(false); return; }
            setLoadingFields(true);
            apiFetch('/payment-gateways/' + slug + '/fields').then(function(d) {
                setGwFields(d.fields || []);
                setLoadingFields(false);
            }).catch(function(){ setGwFields([]); setLoadingFields(false); });
        }

        /* ── Save method ── */
        function saveMethod() {
            setSaving(true);
            var body = {
                gateway: form.gateway,
                status: form.status,
                position: Number(form.position) || 0,
                surcharge: parseFloat(form.surcharge) || 0,
                surcharge_type: form.surcharge_type || 'A',
                test_mode: form.test_mode ? 1 : 0,
                settings: form.settings || {},
                translations: {
                    en: { name: form.name, description: form.description, instructions: form.instructions }
                }
            };
            var url = editId ? '/payment-methods/' + editId : '/payment-methods';
            var method = editId ? 'PUT' : 'POST';
            apiFetch(url, { method: method, body: body }).then(function(d) {
                setSaving(false);
                refreshMethods();
                if (!editId && d && (d.id || d.method_id)) {
                    setEditId(Number(d.id || d.method_id));
                }
            }).catch(function(){ setSaving(false); });
        }

        /* ── Delete method ── */
        function deleteMethod(m) {
            var mid = Number(m.id || m.method_id);
            if (!confirm(
                'Permanently delete payment gateway "' + (m.label || m.gateway) + '"?\n\n' +
                'Its saved settings and credentials are deleted with it. Past orders keep the ' +
                'payment method name (it is saved to each order before the gateway is removed), ' +
                'and payment transaction records are retained.\n\n' +
                'This cannot be undone. To stop offering it while keeping it, use Disable instead.'
            )) return;
            apiFetch('/payment-methods/' + mid, { method:'DELETE' })
                .then(function() { refreshMethods(); })
                .catch(function(err) { alert('Could not delete payment gateway: ' + (err && err.message ? err.message : 'unknown error')); });
        }

        /* ── Toggle status inline ── */
        function toggleStatus(m) {
            var mid = Number(m.id || m.method_id);
            var newStatus = m.status === 'active' ? 'disabled' : 'active';
            apiFetch('/payment-methods/' + mid, { method:'PUT', body:{ status: newStatus } }).then(function() { refreshMethods(); });
        }

        /* ── Form helpers ── */
        function uf(k,v) { setForm(function(p){ return Object.assign({}, p, {[k]:v}); }); }
        function ufs(k,v) { setForm(function(p){ var ns = Object.assign({}, p.settings || {}, {[k]:v}); return Object.assign({}, p, { settings: ns }); }); }

        /* ── Test connection ── */
        function testConnection() {
            if (!editId) return;
            setTesting(true);
            setTestResult(null);
            apiFetch('/payment-methods/' + editId + '/test', { method:'POST' }).then(function(d) {
                setTesting(false);
                setTestResult(d);
            }).catch(function(err) {
                setTesting(false);
                setTestResult({ success: false, message: err.message || 'Connection test failed.' });
            });
        }

        /* ════════════════════════════════════════════
         * EDITOR VIEW
         * ════════════════════════════════════════════ */
        if (view === 'edit') {
            var allTabs = editId
                ? [{ id:'general', label:'General' }, { id:'configure', label:'Configure' }]
                : [{ id:'general', label:'General' }];

            var tabSty = function(t) {
                var active = tab === t;
                return { flex:1, textAlign:'center', padding:'12px 0', cursor:'pointer', fontSize:13, fontWeight:active?600:400,
                    color: active ? TC.accent : mutedColor,
                    borderBottom: active ? '2px solid '+TC.accent : '2px solid transparent', transition:'all .15s' };
            };

            return h(PageWrap, { title: editId ? 'Edit Payment Gateway' : 'New Payment Gateway', sub:'Configure gateway settings and API credentials', mutedColor:mutedColor,
                action: h('div', { style:{ display:'flex', gap:S.sm } },
                    h(Btn, { onClick:function(){ setView('list'); }, mutedColor:mutedColor, borderColor:borderColor }, '\u2190 Back'),
                    h(Btn, { variant:'primary', onClick:saveMethod, disabled:saving || !form.name, mutedColor:mutedColor, borderColor:borderColor }, saving ? 'Saving\u2026' : (editId ? 'Save' : 'Create'))
                )
            },
                /* Tab bar */
                h(Card, { cardBg:cardBg, borderColor:borderColor, dark:dark, pad:0 },
                    h('div', { style:{ display:'flex', borderBottom:'1px solid '+borderColor } },
                        allTabs.map(function(t) {
                            return h('div', { key:t.id, style:tabSty(t.id), onClick:function(){ setTab(t.id); } }, t.label);
                        })
                    ),

                    h('div', { style:{ padding:24 } },

                    /* ══ GENERAL TAB ══ */
                    (tab === 'general') && h('div', { style:{ display:'grid', gridTemplateColumns:'1fr 1fr', gap:24, alignItems:'start' } },

                        /* ── Left column ── */
                        h('div', { style:{ display:'flex', flexDirection:'column', gap:S.lg } },
                            /* Gateway type */
                            h('div', null,
                                h('label', { style:{ fontSize:13, fontWeight:500, display:'block', marginBottom:4, color:textColor } }, 'Gateway type'),
                                editId
                                    ? h('div', { style:Object.assign({}, fs, { background:dark?'#1a1a1a':'#F5F5F5', cursor:'not-allowed', color:mutedColor }) },
                                        (GW_META[form.gateway] ? GW_META[form.gateway].icon + ' ' : '') +
                                        (gateways.find(function(g){ return g.slug === form.gateway; }) || {}).label || form.gateway
                                      )
                                    : h('select', { value:form.gateway, onChange:function(e){ uf('gateway', e.target.value); loadFieldsForGateway(e.target.value); }, style:selSty },
                                        gateways.map(function(g) {
                                            return h('option', { key:g.slug, value:g.slug }, g.label);
                                        })
                                      ),
                                editId && h('div', { style:{ fontSize:11, color:mutedColor, marginTop:3 } }, 'Gateway type cannot be changed after creation')
                            ),
                            /* Name */
                            h('div', null,
                                h('label', { style:{ fontSize:13, fontWeight:500, display:'block', marginBottom:4, color:textColor } }, 'Name *'),
                                h('input', { type:'text', defaultValue:form.name, onBlur:function(e){ uf('name', e.target.value); }, placeholder:'e.g. Credit / Debit Card', style:fs })
                            ),
                            /* Description */
                            h('div', null,
                                h('label', { style:{ fontSize:13, fontWeight:500, display:'block', marginBottom:4, color:textColor } }, 'Description'),
                                h('input', { type:'text', defaultValue:form.description, onBlur:function(e){ uf('description', e.target.value); }, placeholder:'Shown below the name at checkout', style:fs })
                            ),
                            /* Payment instructions */
                            h('div', null,
                                h('label', { style:{ fontSize:13, fontWeight:500, display:'block', marginBottom:4, color:textColor } }, 'Payment instructions'),
                                h('textarea', { defaultValue:form.instructions, onBlur:function(e){ uf('instructions', e.target.value); }, placeholder:'Shown on the order confirmation page', rows:4,
                                    style:Object.assign({}, fs, { resize:'vertical', minHeight:80 }) }),
                                h('div', { style:{ fontSize:11, color:mutedColor, marginTop:3 } }, 'Displayed on the thank-you page after checkout')
                            )
                        ),

                        /* ── Right column ── */
                        h('div', { style:{ display:'flex', flexDirection:'column', gap:S.lg } },
                            /* Status */
                            h('div', null,
                                h('label', { style:{ fontSize:13, fontWeight:500, display:'block', marginBottom:6, color:textColor } }, 'Status'),
                                h('div', { style:{ display:'flex', gap:S.md } },
                                    ['active','disabled'].map(function(st) {
                                        var active = form.status === st;
                                        return h('label', { key:st, style:{ display:'flex', alignItems:'center', gap:6, cursor:'pointer', fontSize:13, color: active ? textColor : mutedColor } },
                                            h('input', { type:'radio', name:'pm_status', checked:active, onChange:function(){ uf('status', st); },
                                                style:{ accentColor:TC.accent } }),
                                            st === 'active' ? 'Active' : 'Disabled'
                                        );
                                    })
                                )
                            ),
                            /* Position */
                            h('div', null,
                                h('label', { style:{ fontSize:13, fontWeight:500, display:'block', marginBottom:4, color:textColor } }, 'Position'),
                                h('input', { type:'number', defaultValue:form.position, onBlur:function(e){ uf('position', e.target.value); }, style:Object.assign({}, fs, { width:100 }) })
                            ),
                            /* Surcharge */
                            h('div', null,
                                h('label', { style:{ fontSize:13, fontWeight:500, display:'block', marginBottom:4, color:textColor } }, 'Surcharge'),
                                h('div', { style:{ display:'flex', gap:S.sm } },
                                    h('input', { type:'number', step:'0.01', defaultValue:form.surcharge, onBlur:function(e){ uf('surcharge', e.target.value); }, placeholder:'0.00', style:Object.assign({}, fs, { flex:1 }) }),
                                    h('select', { value:form.surcharge_type, onChange:function(e){ uf('surcharge_type', e.target.value); }, style:Object.assign({}, selSty, { width:130 }) },
                                        h('option', { value:'A' }, 'Fixed ($)'),
                                        h('option', { value:'P' }, 'Percent (%)')
                                    )
                                ),
                                h('div', { style:{ fontSize:11, color:mutedColor, marginTop:3 } }, 'Extra fee added to orders using this gateway')
                            ),
                            /* S264 KI-18: inert "Test / Sandbox Mode" toggle removed. The real test/live switch is per-gateway (Stripe key prefix; PayPal/Square/Authorize.net "Sandbox Mode" on the Configure tab). The tc_payment_methods.test_mode column is kept as harmless unused foundation — no consumer, no drop-column migration. */
                            /* Webhook URL — Stripe only (S264 KI-19: PayPal capture is synchronous; no /webhooks/paypal route exists) */
                            (form.gateway === 'stripe') && editId && h('div', { style:{ marginTop:S.sm, padding:S.lg, borderRadius:10, background:dark?'rgba(37,99,235,0.06)':'rgba(37,99,235,0.04)', border:'1px solid '+(dark?'rgba(37,99,235,0.15)':'rgba(37,99,235,0.12)') } },
                                h('div', { style:{ fontSize:12, fontWeight:600, color:TC.accent, marginBottom:4 } }, 'Webhook URL'),
                                h('div', { style:{ fontSize:12, color:mutedColor, fontFamily:'monospace', wordBreak:'break-all' } },
                                    Config.apiUrl + '/webhooks/stripe'
                                ),
                                h('div', { style:{ fontSize:11, color:mutedColor, marginTop:4 } },
                                    'Add this URL in your Stripe Dashboard → Developers → Webhooks'
                                )
                            )
                        )
                    ),

                    /* ══ CONFIGURE TAB ══ */
                    (tab === 'configure') && h('div', null,
                        loadingFields && h(Spinner, { mutedColor:mutedColor }),

                        !loadingFields && gwFields.length === 0 && h('div', { style:{ color:mutedColor, fontSize:13, textAlign:'center', padding:'40px 0' } },
                            'No configurable settings for this gateway type.'
                        ),

                        !loadingFields && gwFields.length > 0 && h('div', null,
                            h('h3', { style:{ fontSize:15, fontWeight:600, marginBottom:S.lg, paddingBottom:S.sm, borderBottom:'1px solid '+borderColor } },
                                'API Credentials & Settings'
                            ),
                            /* Two-column grid for the fields when there are 4+ */
                            h('div', { style:{ display:'grid', gridTemplateColumns: gwFields.length >= 4 ? '1fr 1fr' : '1fr', gap:'16px 24px' } },
                                gwFields.map(function(field) {
                                    var k = field.key;
                                    var val = (form.settings || {})[k];
                                    if (val === undefined) val = field.default !== undefined ? field.default : '';
                                    var hintEl = field.hint ? h('div', { style:{ fontSize:11, color:mutedColor, marginTop:3, lineHeight:'1.4' } }, field.hint) : null;

                                    /* Checkbox / toggle type */
                                    if (field.type === 'checkbox') {
                                        var isChecked = !!Number(val);
                                        return h('div', { key:k, style:{ gridColumn: gwFields.length >= 4 ? 'auto' : '1' } },
                                            h('label', { style:{ display:'flex', alignItems:'center', gap:8, cursor:'pointer', fontSize:13, fontWeight:500, color:textColor } },
                                                h('div', { onClick:function(){ ufs(k, isChecked ? 0 : 1); },
                                                    style:{ width:36, height:20, borderRadius:10, background:isChecked ? TC.accent : (dark?'#333':'#D4D4D8'),
                                                        position:'relative', cursor:'pointer', transition:'background .2s', flexShrink:0 } },
                                                    h('div', { style:{ position:'absolute', top:2, left:isChecked ? 18 : 2, width:16, height:16, borderRadius:8,
                                                        background:'#fff', transition:'left .2s', boxShadow:'0 1px 3px rgba(0,0,0,.2)' } })
                                                ),
                                                field.label
                                            ),
                                            hintEl && h('div', { style:{ paddingLeft:44 } }, hintEl)
                                        );
                                    }

                                    /* Select type */
                                    if (field.type === 'select') {
                                        return h('div', { key:k, style:{ gridColumn: gwFields.length >= 4 ? 'auto' : '1' } },
                                            h('label', { style:{ fontSize:13, fontWeight:500, display:'block', marginBottom:4, color:textColor } }, field.label),
                                            h('select', { value:String(val), onChange:function(e){ ufs(k, e.target.value); }, style:selSty },
                                                Object.keys(field.options || {}).map(function(optKey) {
                                                    return h('option', { key:optKey, value:optKey }, field.options[optKey]);
                                                })
                                            ),
                                            hintEl
                                        );
                                    }

                                    /* Textarea type */
                                    if (field.type === 'textarea') {
                                        return h('div', { key:k, style:{ gridColumn: gwFields.length >= 4 ? '1 / -1' : '1' } },
                                            h('label', { style:{ fontSize:13, fontWeight:500, display:'block', marginBottom:4, color:textColor } }, field.label),
                                            h('textarea', { defaultValue:val, onBlur:function(e){ ufs(k, e.target.value); }, rows:3,
                                                style:Object.assign({}, fs, { resize:'vertical', minHeight:60 }) }),
                                            hintEl
                                        );
                                    }

                                    /* Default: text or password */
                                    return h('div', { key:k, style:{ gridColumn: gwFields.length >= 4 ? 'auto' : '1' } },
                                        h('label', { style:{ fontSize:13, fontWeight:500, display:'block', marginBottom:4, color:textColor } }, field.label),
                                        h('input', { type: field.type === 'password' ? 'password' : 'text',
                                            defaultValue: val, onBlur:function(e){ ufs(k, e.target.value); },
                                            placeholder: field.type === 'password' ? '\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022' : '',
                                            style:fs }),
                                        hintEl
                                    );
                                })
                            ),

                            /* Webhook URL hint inside Configure for Stripe */
                            form.gateway === 'stripe' && h('div', { style:{ marginTop:20, padding:S.lg, borderRadius:10, background:dark?'rgba(37,99,235,0.06)':'rgba(37,99,235,0.04)', border:'1px solid '+(dark?'rgba(37,99,235,0.15)':'rgba(37,99,235,0.12)') } },
                                h('div', { style:{ fontSize:12, fontWeight:600, color:TC.accent, marginBottom:4 } }, 'Webhook URL'),
                                h('div', { style:{ fontSize:12, color:mutedColor, fontFamily:'monospace', wordBreak:'break-all' } },
                                    Config.apiUrl + '/webhooks/stripe'
                                ),
                                h('div', { style:{ fontSize:11, color:mutedColor, marginTop:4 } }, 'Paste this URL in Stripe Dashboard \u2192 Developers \u2192 Webhooks. Events needed: payment_intent.succeeded, payment_intent.payment_failed, setup_intent.succeeded, charge.refunded')
                            ),

                            /* S264 KI-19: PayPal "Webhook URL" block removed — no /webhooks/paypal route exists and PayPal capture is synchronous (no webhook needed). */

                            /* ── Test Connection button + result ── */
                            h('div', { style:{ marginTop:24, paddingTop:20, borderTop:'1px solid '+borderColor, display:'flex', alignItems:'center', gap:S.lg, flexWrap:'wrap' } },
                                h('button', { onClick:testConnection, disabled:testing,
                                    style:{ padding:'9px 20px', borderRadius:8, fontSize:13, fontWeight:500, cursor:testing?'not-allowed':'pointer',
                                        border:'1px solid '+TC.accent, background:dark?'rgba(37,99,235,0.08)':'rgba(37,99,235,0.04)', color:TC.accent,
                                        display:'flex', alignItems:'center', gap:8, transition:'all .15s' } },
                                    /* S464: spinner is ALWAYS in the tree (visibility-toggled) so the button's
                                       child list never changes shape — no sibling insert on the testing flip. */
                                    h('span', { style:{ display:testing?'inline-block':'none', width:14, height:14, border:'2px solid '+TC.accent, borderTopColor:'transparent', borderRadius:'50%', animation:'spin .6s linear infinite' } }),
                                    h('span', null, testing ? 'Testing\u2026' : '\u26A1 Test Connection')
                                ),
                                /* Result panel is always rendered (empty when no result) for the same reason. */
                                h('div', { style:{ flex:1, minWidth:200, padding: testResult ? '10px 14px' : 0, borderRadius:8, fontSize:12, lineHeight:'1.5',
                                    background: !testResult ? 'transparent' : testResult.success ? (dark?'rgba(22,163,74,0.08)':'rgba(22,163,74,0.05)') : (dark?'rgba(220,38,38,0.08)':'rgba(220,38,38,0.05)'),
                                    border: !testResult ? 'none' : '1px solid ' + (testResult.success ? (dark?'rgba(22,163,74,0.2)':'rgba(22,163,74,0.15)') : (dark?'rgba(220,38,38,0.2)':'rgba(220,38,38,0.15)')),
                                    color: !testResult ? 'inherit' : testResult.success ? TC.success : TC.error } },
                                    h('span', { style:{ fontWeight:600 } }, !testResult ? '' : testResult.success ? '\u2705 ' : '\u274C '),
                                    h('span', null, testResult ? String(testResult.message || '') : '')
                                )
                            )
                        )
                    )

                    ) /* end padding div */
                ) /* end Card */
            ); /* end PageWrap */
        }

        /* ════════════════════════════════════════════
         * LIST VIEW (default)
         * ════════════════════════════════════════════ */
        return h(PageWrap, { title:'Payment Gateways', sub:'Manage how customers pay at checkout', mutedColor:mutedColor, backBtn:backBtn,
            action: h(Btn, { variant:'primary', onClick:openNew, mutedColor:mutedColor, borderColor:borderColor }, '+ Add Payment Gateway')
        },
            loading && h(Spinner, { mutedColor:mutedColor }),

            !loading && methods.length === 0 && h(Card, { cardBg:cardBg, borderColor:borderColor, dark:dark },
                h(EmptyState, { mutedColor:mutedColor, icon:'\uD83D\uDCB3', title:'No payment gateways configured', sub:'Add your first payment gateway so customers can check out.' })
            ),

            !loading && methods.length > 0 && h(Card, { cardBg:cardBg, borderColor:borderColor, dark:dark, pad:0 },
                /* Table header */
                h('div', { style:{ display:'grid', gridTemplateColumns:'60px 1fr 140px 120px 100px', padding:S.md+' '+S.xl+'px', borderBottom:'1px solid '+borderColor, background:tableHeaderBg, boxShadow:tableHeaderShadow } },
                    h('span', { style:{ fontSize:10, fontWeight:500, color:mutedColor, textTransform:'uppercase', letterSpacing:'0.5px' } }, 'Pos'),
                    h('span', { style:{ fontSize:10, fontWeight:500, color:mutedColor, textTransform:'uppercase', letterSpacing:'0.5px' } }, 'Gateway'),
                    h('span', { style:{ fontSize:10, fontWeight:500, color:mutedColor, textTransform:'uppercase', letterSpacing:'0.5px' } }, 'Surcharge'),
                    h('span', { style:{ fontSize:10, fontWeight:500, color:mutedColor, textTransform:'uppercase', letterSpacing:'0.5px' } }, 'Status'),
                    h('span', { style:{ fontSize:10, fontWeight:500, color:mutedColor, textTransform:'uppercase', letterSpacing:'0.5px', textAlign:'right' } }, 'Actions')
                ),
                /* Table rows */
                methods.map(function(m, idx) {
                    var mid = Number(m.id || m.method_id);
                    var meta = GW_META[m.gateway] || { icon:'\uD83D\uDCB3', color:mutedColor };
                    var isActive = m.status === 'active';
                    var hasSurcharge = parseFloat(m.surcharge) > 0;
                    return h('div', { key:mid, style:{
                        display:'grid', gridTemplateColumns:'60px 1fr 140px 120px 100px', padding:'14px '+S.xl+'px',
                        borderBottom: idx < methods.length - 1 ? '1px solid '+borderColor : 'none',
                        alignItems:'center', cursor:'pointer', transition:'background .12s'
                    },
                    onMouseEnter:function(e){ e.currentTarget.style.background = dark ? 'rgba(255,255,255,0.02)' : '#F9FAFB'; },
                    onMouseLeave:function(e){ e.currentTarget.style.background = 'transparent'; },
                    onClick:function(){ openEdit(m); }
                    },
                        /* Position */
                        h('span', { style:{ fontSize:12, color:mutedColor, fontWeight:400 } }, String(m.position || 0)),
                        /* Gateway name + type */
                        h('div', { style:{ display:'flex', alignItems:'center', gap:S.md } },
                            h('div', { style:{ width:36, height:36, borderRadius:9, background:dark?'rgba(255,255,255,0.05)':'#F5F5F5', display:'flex', alignItems:'center', justifyContent:'center', fontSize:16, flexShrink:0 } }, meta.icon),
                            h('div', null,
                                h('div', { style:{ fontSize:13, fontWeight:500 } }, m.label || m.gateway),
                                h('div', { style:{ fontSize:11, color:mutedColor, marginTop:1 } },
                                    (gateways.find(function(g){ return g.slug === m.gateway; }) || {}).label || m.gateway,
                                    /* S264 KI-18: list "TEST" badge removed — it read the inert test_mode column (no longer settable), so it could only ever show stale/seed state, not the real per-gateway test/live mode. */
                                    m.config_status === 'needs_setup' ? h('span', {
                                        title: 'A required field is missing. Payments will silently break until configured. Click row to fix.',
                                        style:{ marginLeft:6, fontSize:10, fontWeight:600, color:TC.error, background:dark?'rgba(220,38,38,0.10)':'#FEF2F2', padding:'1px 6px', borderRadius:4 }
                                    }, 'NEEDS SETUP') : null
                                )
                            )
                        ),
                        /* Surcharge */
                        h('span', { style:{ fontSize:12, color: hasSurcharge ? textColor : mutedColor, fontWeight: hasSurcharge ? 500 : 400 } },
                            hasSurcharge
                                ? (m.surcharge_type === 'P' ? parseFloat(m.surcharge) + '%' : '$' + parseFloat(m.surcharge).toFixed(2))
                                : '\u2014'
                        ),
                        /* Status */
                        h('span', null,
                            h(Badge, { status: isActive ? 'active' : 'inactive', dark:dark }, isActive ? 'Active' : 'Disabled')
                        ),
                        /* Actions */
                        h('div', { style:{ display:'flex', gap:S.sm, justifyContent:'flex-end' }, onClick:function(e){ e.stopPropagation(); } },
                            h('button', { onClick:function(){ toggleStatus(m); }, title: isActive ? 'Disable' : 'Enable',
                                style:{ padding:'4px 10px', borderRadius:6, fontSize:11, cursor:'pointer', border:'1px solid '+borderColor, background:'transparent', color:mutedColor, transition:'all .15s' } },
                                isActive ? 'Disable' : 'Enable'
                            ),
                            h('button', { onClick:function(){ deleteMethod(m); }, title:'Delete',
                                style:{ padding:'4px 10px', borderRadius:6, fontSize:11, cursor:'pointer', border:'1px solid '+(dark?'rgba(220,38,38,0.2)':'rgba(220,38,38,0.15)'), background:'transparent', color:TC.error, transition:'all .15s' } },
                                'Delete'
                            )
                        )
                    );
                })
            )
        );
    }

    /* ════════════════════════════════
     * SHIPPING PAGE
     * ════════════════════════════════ */
    function ShippingPage({ dark, cardBg, textColor, mutedColor, borderColor, tableHeaderBg, backBtn }) {
        /* ── State ── */
        var _view    = useState('methods'); var view    = _view[0];    var setView    = _view[1]; /* methods | zones | method-edit | zone-edit */
        var _methods = useState([]);        var methods = _methods[0]; var setMethods = _methods[1];
        var _zones   = useState([]);        var zones   = _zones[0];   var setZones   = _zones[1];
        var _loadM   = useState(true);      var loadM   = _loadM[0];   var setLoadM   = _loadM[1];
        var _loadZ   = useState(true);      var loadZ   = _loadZ[0];   var setLoadZ   = _loadZ[1];
        var _editId  = useState(null);      var editId  = _editId[0];  var setEditId  = _editId[1]; /* null = new */
        var _form    = useState({});        var form    = _form[0];    var setForm    = _form[1];
        var _saving  = useState(false);     var saving  = _saving[0];  var setSaving  = _saving[1];
        var _methTab = useState('general'); var methTab = _methTab[0]; var setMethTab = _methTab[1];
        var _methRates = useState([]); var methRates = _methRates[0]; var setMethRates = _methRates[1];
        var _loadingRates = useState(false); var loadingRates = _loadingRates[0]; var setLoadingRates = _loadingRates[1];
        var _locForm = useState({ type:'country', code:'' });
        var locForm = _locForm[0]; var setLocForm = _locForm[1];
        var _zoneLocs = useState([]); var zoneLocs = _zoneLocs[0]; var setZoneLocs = _zoneLocs[1];
        var _rtMax = useState(3); var realtimeMax = _rtMax[0]; var setRealtimeMax = _rtMax[1]; /* KI-32: cap on total realtime service options across all carriers */

        var fs = fieldSty(dark, borderColor);
        var selSty = Object.assign({}, fs, { cursor:'pointer' });

        /* ── Load data ── */
        useEffect(function() {
            apiFetch('/shipping/methods').then(function(d){ setMethods(Array.isArray(d)?d:(d.methods||[])); setLoadM(false); }).catch(function(){ setLoadM(false); });
            apiFetch('/shipping/zones').then(function(d){ setZones(Array.isArray(d)?d:(d.zones||[])); setLoadZ(false); }).catch(function(){ setLoadZ(false); });
            apiFetch('/shipping/realtime-settings').then(function(d){ if (d && d.realtime_max_options) setRealtimeMax(Number(d.realtime_max_options)||3); }).catch(function(){});
        }, []);

        function refreshMethods() {
            apiFetch('/shipping/methods').then(function(d){ setMethods(Array.isArray(d)?d:(d.methods||[])); });
        }
        function refreshZones() {
            apiFetch('/shipping/zones').then(function(d){ setZones(Array.isArray(d)?d:(d.zones||[])); });
        }

        /* ── Method helpers ── */
        var carrierLabels = { flat:'Manual — By customer\'s address', free:'Free Shipping', table_rate:'Table Rate', usps:'USPS', ups:'UPS', fedex:'FedEx', realtime:'Realtime (Carrier API)' };
        var CARRIER_SERVICES = {
            usps: [
                { value:'usps_ground_advantage', label:'USPS Ground Advantage' },
                { value:'usps_priority', label:'USPS Priority Mail' },
                { value:'usps_priority_express', label:'USPS Priority Mail Express' },
                { value:'usps_media_mail', label:'USPS Media Mail' },
            ],
            ups: [
                { value:'ups_ground', label:'UPS Ground' },
                { value:'ups_3day_select', label:'UPS 3 Day Select' },
                { value:'ups_2nd_day_air', label:'UPS 2nd Day Air' },
                { value:'ups_2nd_day_air_am', label:'UPS 2nd Day Air A.M.' },
                { value:'ups_next_day_air_saver', label:'UPS Next Day Air Saver' },
                { value:'ups_next_day_air', label:'UPS Next Day Air' },
                { value:'ups_next_day_air_early', label:'UPS Next Day Air Early' },
                { value:'ups_standard_intl', label:'UPS Standard (International)' },
                { value:'ups_worldwide_express', label:'UPS Worldwide Express' },
                { value:'ups_worldwide_expedited', label:'UPS Worldwide Expedited' },
                { value:'ups_surepost', label:'UPS SurePost' },
            ],
            fedex: [
                { value:'fedex_ground', label:'FedEx Ground' },
                { value:'fedex_home_delivery', label:'FedEx Home Delivery' },
                { value:'fedex_express_saver', label:'FedEx Express Saver' },
                { value:'fedex_2day', label:'FedEx 2Day' },
                { value:'fedex_2day_am', label:'FedEx 2Day A.M.' },
                { value:'fedex_standard_overnight', label:'FedEx Standard Overnight' },
                { value:'fedex_priority_overnight', label:'FedEx Priority Overnight' },
                { value:'fedex_first_overnight', label:'FedEx First Overnight' },
                { value:'fedex_intl_economy', label:'FedEx International Economy' },
                { value:'fedex_intl_priority', label:'FedEx International Priority' },
                { value:'fedex_ground_economy', label:'FedEx Ground Economy (SmartPost)' },
            ],
        };
        var isRealtimeCarrier = function(c) { return c === 'usps' || c === 'ups' || c === 'fedex' || c === 'realtime'; };

        function openNewMethod() {
            setForm({ name:'', carrier:'flat', status:'active', delivery_time:'', description:'', min_weight:'', max_weight:'', position:'0', service_codes:[],
                /* Configure tab (realtime) */
                api_client_id:'', api_client_secret:'', account_number:'', shipper_name:'', test_mode:false, negotiated_rates:false,
                pickup_type:'daily_pickup', package_type:'unknown', delivery_confirmation:false,
                max_box_weight:'0', box_width:'', box_height:'', box_length:'',
                /* Additional settings */
                tax_class_id:'', require_address:true,
                _zone_times:{},
            });
            setEditId(null);
            setMethTab('general');
            setView('method-edit');
        }
        function openEditMethod(m) {
            var s = m.settings || {};
            setForm({
                name: m.name || '',
                carrier: m.carrier || 'flat',
                status: m.status || 'active',
                delivery_time: s.delivery_time || '',
                description: m.description || '',
                min_weight: m.min_weight != null ? String(m.min_weight) : '',
                max_weight: m.max_weight != null ? String(m.max_weight) : '',
                position: String(m.position || 0),
                service_codes: Array.isArray(s.service_codes) ? s.service_codes.slice() : (s.service_code ? [s.service_code] : []),
                /* Configure tab */
                api_client_id: s.api_client_id || '',
                api_client_secret: s.api_client_secret || '',
                account_number: s.account_number || '',
                shipper_name: s.shipper_name || '',
                test_mode: s.test_mode == 1 || s.test_mode === '1' || s.test_mode === true,
                negotiated_rates: s.negotiated_rates == 1 || s.negotiated_rates === true,
                pickup_type: s.pickup_type || 'daily_pickup',
                package_type: s.package_type || 'unknown',
                delivery_confirmation: s.delivery_confirmation == 1 || s.delivery_confirmation === true,
                max_box_weight: s.max_box_weight != null ? String(s.max_box_weight) : '0',
                box_width: s.box_width != null ? String(s.box_width) : '',
                box_height: s.box_height != null ? String(s.box_height) : '',
                box_length: s.box_length != null ? String(s.box_length) : '',
                /* Additional settings */
                tax_class_id: String(m.tax_class_id || ''),
                require_address: s.require_address !== false && s.require_address !== '0' && s.require_address !== 0,
                _zone_times: s.zone_times || {},
                /* KI-95 — Test Rate Calculation tab. Underscore-prefixed, so the
                 * save-payload builder (which lists its keys explicitly) never
                 * persists them. Seeded with a usable default address so the tab
                 * opens ready to run rather than failing validation on first click. */
                _test_price: '100', _test_weight: '5', _test_items: '1',
                _test_country: 'US', _test_state: '', _test_postcode: '',
                _test_result: null,
            });
            setEditId(Number(m.id || m.method_id));
            setMethTab('general');
            setView('method-edit');
            /* Load rates for this method */
            var mid = Number(m.id || m.method_id);
            setLoadingRates(true);
            apiFetch('/shipping/methods/' + mid + '/rates').then(function(d) {
                setMethRates(Array.isArray(d) ? d : []); setLoadingRates(false);
            }).catch(function(){ setLoadingRates(false); });
        }
        function saveMethod() {
            setSaving(true);
            var body = {
                name: form.name,
                carrier: form.carrier,
                status: form.status,
                position: parseInt(form.position) || 0,
                min_weight: form.min_weight !== '' ? parseFloat(form.min_weight) : null,
                max_weight: form.max_weight !== '' ? parseFloat(form.max_weight) : null,
                settings: {
                    delivery_time: form.delivery_time, service_codes: Array.isArray(form.service_codes) ? form.service_codes : [], service_code: '',
                    api_client_id: form.api_client_id, api_client_secret: form.api_client_secret,
                    account_number: form.account_number, shipper_name: form.shipper_name,
                    test_mode: form.test_mode ? 1 : 0, negotiated_rates: form.negotiated_rates ? 1 : 0,
                    pickup_type: form.pickup_type, package_type: form.package_type,
                    delivery_confirmation: form.delivery_confirmation ? 1 : 0,
                    max_box_weight: form.max_box_weight, box_width: form.box_width, box_height: form.box_height, box_length: form.box_length,
                    require_address: form.require_address ? 1 : 0,
                    zone_times: form._zone_times || {},
                },
                tax_class_id: form.tax_class_id ? parseInt(form.tax_class_id) : null,
            };
            /* When creating, pass translations for the name */
            if (!editId) {
                body.translations = { en: { name: form.name, description: form.description } };
            } else {
                body.translations = { en: { name: form.name, description: form.description } };
            }
            var url = editId ? '/shipping/methods/' + editId : '/shipping/methods';
            var method = editId ? 'PUT' : 'POST';
            apiFetch(url, { method: method, body: body }).then(function(d) {
                setSaving(false);
                refreshMethods();
                if (!editId && d && (d.id || d.method_id)) {
                    setEditId(Number(d.id || d.method_id));
                }
            }).catch(function(){ setSaving(false); });
        }
        function reloadRates() {
            if (!editId) return;
            setLoadingRates(true);
            apiFetch('/shipping/methods/' + editId + '/rates').then(function(d) {
                setMethRates(Array.isArray(d) ? d : []); setLoadingRates(false);
            }).catch(function(){ setLoadingRates(false); });
        }
        function addZoneRate(zoneId, rateType) {
            apiFetch('/shipping/methods/' + editId + '/rates', { method:'POST', body:{ zone_id:zoneId, rate_type:rateType, conditions:[{ from_value:'0', to_value:null, price:'0', price_type:'fixed' }] } })
                .then(function(){ reloadRates(); });
        }
        function addConditionRow(rateId) {
            var rate = null;
            methRates.forEach(function(r){ if ((r.id||r.rate_id) == rateId) rate = r; });
            var conds = (rate && rate.conditions) ? rate.conditions.slice() : [];
            /* If the last row is "and up", give it a concrete to_value first */
            if (conds.length > 0) {
                var last = conds[conds.length - 1];
                if (last.to_value === null || last.to_value === '') {
                    conds[conds.length - 1] = Object.assign({}, last, { to_value: last.from_value ? String(parseFloat(last.from_value) + 100) : '100' });
                }
            }
            conds.push({ from_value: conds.length > 0 ? conds[conds.length-1].to_value : '0', to_value:null, price:'0', price_type:'fixed' });
            apiFetch('/shipping/rates/' + rateId + '/conditions', { method:'PUT', body:{ conditions:conds } })
                .then(function(){ reloadRates(); });
        }
        function updateConditions(rateId, newConds) {
            /* Update local state immediately — no full reload needed */
            setMethRates(function(prev) {
                return prev.map(function(r) {
                    if ((r.id || r.rate_id) == rateId) {
                        return Object.assign({}, r, { conditions: newConds });
                    }
                    return r;
                });
            });
            /* Fire API in background — no reload.
             * KI-98: the local state above is applied OPTIMISTICALLY, so without a
             * catch a rejected write looks identical to a successful one until the
             * page is revisited. Surface it. */
            apiFetch('/shipping/rates/' + rateId + '/conditions', { method:'PUT', body:{ conditions:newConds } })
                .catch(function(err) {
                    alert('Could not save the rate: ' + (err && err.message ? err.message : 'unknown error'));
                    reloadRates();
                });
        }
        /* KI-98: create the zone's BASE flat rate from the header "Rate" box when the
         * zone has no flat rate row yet. Safe alongside condition-based rates: the
         * engine SUMS multiple rates for the same method+zone (ShippingManager
         * L106-108), treating flat as the base and conditions as surcharges. */
        function createBaseRate(zoneId, price) {
            if (!editId) return;
            apiFetch('/shipping/methods/' + editId + '/rates', { method:'POST', body:{ zone_id:zoneId, rate_type:'flat', price:String(price) } })
                .then(function(){ reloadRates(); })
                .catch(function(err) {
                    alert('Could not save the zone rate: ' + (err && err.message ? err.message : 'unknown error'));
                });
        }
        function deleteShipRate(rateId) {
            apiFetch('/shipping/rates/' + rateId, { method:'DELETE' }).then(function(){ reloadRates(); });
        }
        function addAllZones() {
            /* For each zone that doesn't have a rate yet, add a flat rate */
            var existingZoneIds = {};
            methRates.forEach(function(r){ existingZoneIds[r.zone_id] = true; });
            var promises = [];
            zones.forEach(function(z) {
                if (!existingZoneIds[z.id]) {
                    promises.push(apiFetch('/shipping/methods/' + editId + '/rates', { method:'POST', body:{ zone_id:z.id, rate_type:'flat', price:'0' } }));
                }
            });
            if (promises.length > 0) {
                Promise.all(promises).then(function(){ reloadRates(); });
            }
        }

        function deleteMethod(m) {
            /* KI-97: the confirm used to say "Disable" under a button labelled "Del",
             * for a route that only disabled. All three now agree, and the text states
             * both what is destroyed and what survives, because a merchant cannot
             * otherwise tell whether deleting a method damages past orders. */
            if (!confirm(
                'Permanently delete shipping method "' + (m.name || m.carrier) + '"?\n\n' +
                'Its rates and rate conditions are deleted with it. Past orders keep the ' +
                'shipping method name recorded at checkout, and shipments are not affected.\n\n' +
                'This cannot be undone. To stop offering it while keeping it, use Disable instead.'
            )) return;
            apiFetch('/shipping/methods/' + (m.id || m.method_id), { method:'DELETE' })
                .then(function() { refreshMethods(); })
                .catch(function(err) { alert('Could not delete shipping method: ' + (err && err.message ? err.message : 'unknown error')); });
        }
        function toggleMethodStatus(m) {
            var newStatus = m.status === 'active' ? 'disabled' : 'active';
            apiFetch('/shipping/methods/' + (m.id || m.method_id), { method:'PUT', body:{ status: newStatus } }).then(function() { refreshMethods(); });
        }

        /* ── Zone helpers ── */
        function openNewZone() {
            setForm({ name:'', status:'active' });
            setEditId(null);
            setZoneLocs([]);
            setView('zone-edit');
        }
        function openEditZone(z) {
            setForm({ name: z.name || '', status: z.status || 'active' });
            setEditId(Number(z.id || z.zone_id));
            setZoneLocs(z.locations || []);
            setView('zone-edit');
        }
        function saveZone() {
            setSaving(true);
            var url = editId ? '/shipping/zones/' + editId : '/shipping/zones';
            var method = editId ? 'PUT' : 'POST';
            apiFetch(url, { method: method, body: { name: form.name, status: form.status } }).then(function(d) {
                setSaving(false);
                refreshZones();
                if (!editId && d && d.id) {
                    setEditId(Number(d.id));
                }
            }).catch(function(){ setSaving(false); });
        }
        function deleteZone(z) {
            if (!confirm('Delete rate area "' + z.name + '"? This will remove all associated rates.')) return;
            apiFetch('/shipping/zones/' + (z.id || z.zone_id), { method:'DELETE' }).then(function() { refreshZones(); });
        }
        function addLocation() {
            if (!editId || !locForm.code.trim()) return;
            setSaving(true);
            apiFetch('/shipping/zones/' + editId + '/locations', { method:'POST', body:{ type: locForm.type, code: locForm.code.toUpperCase() } })
                .then(function(d) {
                    setSaving(false);
                    if (d && d.id) {
                        setZoneLocs(function(prev){ return prev.concat([d]); });
                    }
                    setLocForm({ type: locForm.type, code:'' });
                }).catch(function(){ setSaving(false); });
        }
        function removeLocation(loc) {
            apiFetch('/shipping/zones/' + editId + '/locations/' + loc.id, { method:'DELETE' }).then(function() {
                setZoneLocs(function(prev){ return prev.filter(function(l){ return l.id !== loc.id; }); });
            });
        }

        /* ════════════════════════════════════════════
         * METHOD EDITOR VIEW — tabbed for realtime carriers
         * ════════════════════════════════════════════ */
        if (view === 'method-edit') {
            var isRT = isRealtimeCarrier(form.carrier);
            var allTabs = editId
                ? ( isRT
                    ? [{ id:'general', label:'General' }, { id:'configure', label:'Configure' }, { id:'rates', label:'Shipping Time & Rates' }, { id:'test', label:'Test Rate Calculation' }, { id:'additional', label:'Additional Settings' }]
                    : [{ id:'general', label:'General' }, { id:'rates', label:'Shipping Time & Rates' }, { id:'test', label:'Test Rate Calculation' }, { id:'additional', label:'Additional Settings' }]
                )
                : [{ id:'general', label:'General' }];
            var tabSty = function(t) {
                var active = methTab === t;
                return { flex:1, textAlign:'center', padding:'12px 0', cursor:'pointer', fontSize:13, fontWeight:active?600:400,
                    color: active ? TC.accent : mutedColor,
                    borderBottom: active ? '2px solid '+TC.accent : '2px solid transparent', transition:'all .15s' };
            };
            /* Helper for form field */
            var fld = function(key, label, type, extra) {
                extra = extra || {};
                var val = form[key] != null ? form[key] : '';
                if (type === 'checkbox') {
                    var isOn = val === true || val === '1' || val === 1;
                    return h('div', { style:{ marginBottom:20 } },
                        h('label', { style:{ display:'flex', alignItems:'center', gap:10, cursor:'pointer' } },
                            h('div', { onClick:function(){ setForm(Object.assign({},form,{[key]:!isOn})); },
                                style:{ width:40, height:22, borderRadius:12, cursor:'pointer', background:isOn?TC.accent:(dark?'#333':'#D4D4D4'), transition:'background .15s', position:'relative', flexShrink:0 } },
                                h('div', { style:{ width:18, height:18, borderRadius:'50%', background:'#fff', position:'absolute', top:2, left:isOn?20:2, transition:'left .15s', boxShadow:'0 1px 3px rgba(0,0,0,.2)' } })
                            ),
                            h('span', { style:{ fontSize:13, color:textColor } }, label)
                        ),
                        extra.hint && h('div', { style:{ fontSize:12, color:mutedColor, marginTop:4, marginLeft:50 } }, extra.hint)
                    );
                }
                if (type === 'select') {
                    return h('div', { style:{ marginBottom:20 } },
                        h('label', { style:{ fontSize:13, fontWeight:500, display:'block', marginBottom:4, color:textColor } }, label),
                        h('select', { value:val, onChange:function(e){ setForm(Object.assign({},form,{[key]:e.target.value})); }, style:Object.assign({},selSty, extra.maxW ? {maxWidth:extra.maxW} : {}) },
                            (extra.options || []).map(function(o){ return h('option', { key:o.value, value:o.value }, o.label); })
                        ),
                        extra.hint && h('div', { style:{ fontSize:12, color:mutedColor, marginTop:4 } }, extra.hint)
                    );
                }
                return h('div', { style:{ marginBottom:20 } },
                    h('label', { style:{ fontSize:13, fontWeight:500, display:'block', marginBottom:4, color:textColor } }, label, extra.required && h('span',{style:{color:TC.error}},' *')),
                    h('input', { type: type === 'password' ? 'password' : (type === 'number' ? 'number' : 'text'), value:val, step: type==='number' ? (extra.step||'any') : undefined,
                        onChange:function(e){ setForm(Object.assign({},form,{[key]:e.target.value})); },
                        placeholder: extra.placeholder || '', style:Object.assign({},fs, extra.maxW ? {maxWidth:extra.maxW} : {}) }),
                    extra.hint && h('div', { style:{ fontSize:12, color:mutedColor, marginTop:4 } }, extra.hint)
                );
            };

            /* Carrier-specific labels */
            var carrierApiLabel = { usps:'USPS', ups:'UPS', fedex:'FedEx' };
            var pickupOptions = [
                { value:'daily_pickup', label:'Daily Pickup' }, { value:'one_time', label:'One Time Pickup' },
                { value:'letter_center', label:'Letter Center' }, { value:'air_service_center', label:'Air Service Center' },
            ];
            var packageOptions = [
                { value:'unknown', label:'UNKNOWN (default)' }, { value:'package', label:'Package' },
                { value:'letter', label:'Letter / Envelope' }, { value:'tube', label:'Tube' },
                { value:'pak', label:'Pak' }, { value:'small_express_box', label:'Small Express Box' },
                { value:'medium_express_box', label:'Medium Express Box' }, { value:'large_express_box', label:'Large Express Box' },
            ];

            return h(PageWrap, { title: editId ? 'Edit Shipping Method' : 'New Shipping Method', sub:'Configure rate calculation, delivery time and availability', mutedColor:mutedColor,
                action: h('div', { style:{ display:'flex', gap:S.sm } },
                    h(Btn, { onClick:function(){ setView('methods'); }, mutedColor:mutedColor, borderColor:borderColor }, '← Back'),
                    h(Btn, { variant:'primary', onClick:saveMethod, disabled:saving || !form.name, mutedColor:mutedColor, borderColor:borderColor }, saving ? 'Saving…' : (editId ? 'Save' : 'Create'))
                )
            },
                h(Card, { cardBg:cardBg, borderColor:borderColor, dark:dark, pad:0 },
                    /* Tab bar */
                    allTabs.length > 1 && h('div', { style:{ display:'flex', borderBottom:'1px solid '+borderColor } },
                        allTabs.map(function(t){ return h('div', { key:t.id, onClick:function(){ setMethTab(t.id); }, style:tabSty(t.id) }, t.label); })
                    ),

                    h('div', { style:{ padding:24 } },

                    /* ══ GENERAL TAB ══ */
                    (methTab === 'general') && h('div', null,
                        /* Rate calculation */
                        h('div', { style:{ marginBottom:20 } },
                            h('label', { style:{ fontSize:13, fontWeight:500, display:'block', marginBottom:4, color:textColor } }, 'Rate calculation'),
                            h('select', { value:form.carrier, onChange:function(e){
                                var c = e.target.value;
                                var upd = { carrier:c };
                                if (!isRealtimeCarrier(c)) upd.service_codes = [];
                                setForm(Object.assign({},form,upd));
                            }, style:selSty },
                                h('option',{value:'flat'},"Manual — By customer's address"),
                                h('option',{value:'free'},'Free shipping'),
                                h('option',{value:'table_rate'},'Table rate — By weight/price/items'),
                            )
                        ),
                        isRealtimeCarrier(form.carrier) && (function(){
                            var svcList  = CARRIER_SERVICES[form.carrier] || [];
                            var selected = Array.isArray(form.service_codes) ? form.service_codes : [];
                            /* Cap is a store-wide TOTAL across ALL realtime methods (KI-32),
                             * not per-carrier. Count services already enabled on OTHER realtime
                             * methods, then add this form's current selection. Once the total
                             * hits realtimeMax, unchecked boxes disable (checked ones stay
                             * toggleable so a merchant can always swap a choice). */
                            var otherCount = 0;
                            methods.forEach(function(m){
                                if (editId && Number(m.id||m.method_id) === editId) return;
                                if (!isRealtimeCarrier(m.carrier)) return;
                                var ms = m.settings || {};
                                var codes = Array.isArray(ms.service_codes) ? ms.service_codes : (ms.service_code ? [ms.service_code] : []);
                                otherCount += codes.length;
                            });
                            var total = otherCount + selected.length;
                            var capReached = total >= realtimeMax;
                            function toggle(v){
                                var next = selected.indexOf(v) !== -1
                                    ? selected.filter(function(x){ return x !== v; })
                                    : selected.concat([v]);
                                setForm(Object.assign({},form,{service_codes:next}));
                            }
                            return h('div', { style:{ marginBottom:20 } },
                                h('label', { style:{ fontSize:13, fontWeight:500, display:'block', marginBottom:4, color:textColor } }, 'Shipping services', h('span',{style:{color:TC.error}},' *')),
                                h('div', { style:{ display:'flex', flexDirection:'column', gap:6, padding:'10px 12px', border:'1px solid '+borderColor, borderRadius:8 } },
                                    svcList.map(function(svc){
                                        var checked  = selected.indexOf(svc.value) !== -1;
                                        var disabled = !checked && capReached;
                                        return h('label', { key:svc.value, style:{ display:'flex', alignItems:'center', gap:8, fontSize:13, color:(disabled?mutedColor:textColor), cursor:(disabled?'not-allowed':'pointer') } },
                                            h('input', { type:'checkbox', checked:checked, disabled:disabled, onChange:function(){ toggle(svc.value); } }),
                                            svc.label
                                        );
                                    })
                                ),
                                h('div', { style:{ fontSize:12, color:(capReached?'#D97706':mutedColor), marginTop:4 } },
                                    capReached
                                        ? ('Maximum of ' + realtimeMax + ' realtime service options reached across all carriers. Uncheck one to choose another.')
                                        : ('Choose which carrier services shoppers can pick at checkout — ' + total + ' of ' + realtimeMax + ' selected across all carriers. Leave all unchecked to offer every service the carrier returns.')
                                )
                            );
                        })(),
                        fld('name', 'Name', 'text', { required:true, placeholder:'e.g. USPS Priority Mail' }),
                        h('div', { style:{ marginBottom:20 } },
                            h('label', { style:{ fontSize:13, fontWeight:500, display:'block', marginBottom:8, color:textColor } }, 'Status', h('span',{style:{color:TC.error}},' *')),
                            h('div', { style:{ display:'flex', gap:20 } },
                                ['active','disabled'].map(function(s) {
                                    var sel = form.status === s;
                                    return h('label', { key:s, style:{ display:'flex', alignItems:'center', gap:6, cursor:'pointer', fontSize:13 } },
                                        h('div', { onClick:function(){ setForm(Object.assign({},form,{status:s})); },
                                            style:{ width:18, height:18, borderRadius:'50%', border:'2px solid '+(sel?TC.accent:borderColor), display:'flex', alignItems:'center', justifyContent:'center', cursor:'pointer' } },
                                            sel && h('div', { style:{ width:10, height:10, borderRadius:'50%', background:TC.accent } })
                                        ),
                                        s.charAt(0).toUpperCase() + s.slice(1)
                                    );
                                })
                            )
                        ),
                        fld('delivery_time', 'Delivery time', 'text', { placeholder:'e.g. 3-5 days', maxW:240, hint:'Displayed next to the method name at checkout.' }),
                        h('div', { style:{ marginBottom:20 } },
                            h('label', { style:{ fontSize:13, fontWeight:500, display:'block', marginBottom:4, color:textColor } }, 'Description'),
                            h('textarea', { value:form.description, onChange:function(e){ setForm(Object.assign({},form,{description:e.target.value})); },
                                rows:3, style:Object.assign({},fs,{resize:'vertical',minHeight:72}), placeholder:'Optional description shown to customers' })
                        ),
                        h('h3', { style:{ fontSize:15, fontWeight:600, marginTop:S.xxl, marginBottom:S.lg, paddingBottom:S.sm, borderBottom:'1px solid '+borderColor } }, 'Availability'),
                        h('div', { style:{ marginBottom:20 } },
                            h('label', { style:{ fontSize:13, fontWeight:500, display:'block', marginBottom:4, color:textColor } }, 'Weight limit'),
                            h('div', { style:{ display:'flex', alignItems:'center', gap:8 } },
                                h('input', { type:'number', step:'0.001', value:form.min_weight, onChange:function(e){ setForm(Object.assign({},form,{min_weight:e.target.value})); }, placeholder:'Min', style:Object.assign({},fs,{maxWidth:120}) }),
                                h('span', { style:{ color:mutedColor } }, '—'),
                                h('input', { type:'number', step:'0.001', value:form.max_weight, onChange:function(e){ setForm(Object.assign({},form,{max_weight:e.target.value})); }, placeholder:'Max', style:Object.assign({},fs,{maxWidth:120}) })
                            ),
                            h('div', { style:{ fontSize:12, color:mutedColor, marginTop:4 } }, 'Leave blank for no weight restrictions.')
                        ),
                        fld('position', 'Position', 'number', { maxW:120, hint:'Sort order. Lower values appear first.' })
                    ),

                    /* ══ CONFIGURE TAB (realtime only) ══ */
                    (methTab === 'configure') && h('div', null,
                        h('h3', { style:{ fontSize:15, fontWeight:600, marginBottom:S.lg, paddingBottom:S.sm, borderBottom:'1px solid '+borderColor } },
                            (carrierApiLabel[form.carrier]||'Carrier') + ' API Credentials'
                        ),
                        fld('api_client_id', (carrierApiLabel[form.carrier]||'API') + ' App client ID', 'text', { placeholder:'Your API client ID' }),
                        fld('api_client_secret', (carrierApiLabel[form.carrier]||'API') + ' App client secret', 'password', { placeholder:'Your API client secret',
                            hint: 'To get Client ID and Client Secret, register an app in your '+(carrierApiLabel[form.carrier]||'carrier')+' account.' }),
                        fld('account_number', (carrierApiLabel[form.carrier]||'Carrier') + ' account number', 'text', { placeholder:'Your account / shipper number',
                            hint: 'Required for live rates. UPS calls this the Shipper Number; FedEx the Account Number.' }),
                        fld('shipper_name', 'Shipper name', 'text', { placeholder:'Defaults to your store name', hint:'Optional — appears on the rate request.' }),
                        h('div', { style:{ margin:'4px 0 20px', padding:'10px 12px', background:'#F5F7FF', border:'1px solid '+borderColor, borderRadius:6, fontSize:12.5, color:mutedColor, lineHeight:1.55 } },
                            h('strong', { style:{ color:textColor } }, 'Ship-from address: '),
                            'Live rates are calculated from your store address set in ',
                            h('a', { href:'#/settings/company', style:{ color:'#2563EB', fontWeight:600, textDecoration:'none' } }, 'Settings → Company'),
                            '. Fill in your company street, city, state, ZIP and country there — if it\'s blank, the carrier can\'t return a rate.'
                        ),
                        fld('negotiated_rates', 'Use negotiated rates', 'checkbox', { hint:'Use your account-specific discounted rates if available.' }),
                        fld('test_mode', 'Test mode', 'checkbox', { hint:'Use the carrier sandbox/test environment instead of production.' }),

                        h('h3', { style:{ fontSize:15, fontWeight:600, marginTop:S.xxl, marginBottom:S.lg, paddingBottom:S.sm, borderBottom:'1px solid '+borderColor } }, 'Package Settings'),
                        fld('pickup_type', 'Pickup type', 'select', {
                            options: pickupOptions,
                            hint:'Select "Daily Pickup" for Daily Rates. Select "One Time Pickup", "Letter Center", or "Air Service Center" for Retail Rates.'
                        }),
                        fld('package_type', 'Package type', 'select', { options: packageOptions }),
                        fld('delivery_confirmation', 'Use Delivery confirmation', 'checkbox'),
                        fld('max_box_weight', 'Max box weight', 'number', { maxW:180, placeholder:'0' }),

                        h('h3', { style:{ fontSize:15, fontWeight:600, marginTop:S.xxl, marginBottom:S.lg, paddingBottom:S.sm, borderBottom:'1px solid '+borderColor } }, 'Default Package Dimensions'),
                        h('div', { style:{ display:'grid', gridTemplateColumns:'1fr 1fr 1fr', gap:12, marginBottom:20 } },
                            h('div', null, h('label',{style:{fontSize:12,fontWeight:500,display:'block',marginBottom:4}},'Width'),
                                h('input',{type:'number',value:form.box_width,onChange:function(e){setForm(Object.assign({},form,{box_width:e.target.value}));},placeholder:'inches',style:fs})),
                            h('div', null, h('label',{style:{fontSize:12,fontWeight:500,display:'block',marginBottom:4}},'Height'),
                                h('input',{type:'number',value:form.box_height,onChange:function(e){setForm(Object.assign({},form,{box_height:e.target.value}));},placeholder:'inches',style:fs})),
                            h('div', null, h('label',{style:{fontSize:12,fontWeight:500,display:'block',marginBottom:4}},'Length'),
                                h('input',{type:'number',value:form.box_length,onChange:function(e){setForm(Object.assign({},form,{box_length:e.target.value}));},placeholder:'inches',style:fs}))
                        )
                    ),

                    /* ══ SHIPPING TIME & RATES TAB ══ */
                    (methTab === 'rates') && h('div', null,
                        /* Realtime carriers — simple auto-calculated display */
                        isRT && h('div', null,
                            h('div', { style:{ fontSize:13, color:mutedColor, marginBottom:S.lg } }, 'Rate areas with per-zone shipping time overrides. Rates are calculated automatically by the carrier API.'),
                            zones.length === 0 && h(EmptyState, { mutedColor:mutedColor, icon:'🗺️', title:'No rate areas', sub:'Create rate areas in the Shipping → Rate Areas section first.' }),
                            zones.map(function(z) {
                                return h('div', { key:z.id, style:{ padding:'16px 0', borderBottom:'1px solid '+borderColor } },
                                    h('div', { style:{ display:'flex', justifyContent:'space-between', alignItems:'center' } },
                                        h('div', null,
                                            h('div', { style:{ fontSize:14, fontWeight:600 } }, z.name || 'Zone ' + z.id),
                                            h('div', { style:{ fontSize:12, color:mutedColor, marginTop:2 } }, (z.locations||[]).length === 0 ? 'Worldwide' : (z.locations||[]).map(function(l){return l.code;}).join(', '))
                                        ),
                                        h('span', { style:{ fontSize:12, color:mutedColor } }, 'Rate: Calculated automatically')
                                    )
                                );
                            })
                        ),

                        /* Manual/flat/table methods — full condition tables */
                        !isRT && h('div', null,
                            /* Add all button */
                            h('div', { style:{ display:'flex', justifyContent:'flex-end', marginBottom:S.lg } },
                                h(Btn, { onClick:addAllZones, mutedColor:mutedColor, borderColor:borderColor }, 'Add all zones')
                            ),
                            loadingRates && h(Spinner, { mutedColor:mutedColor }),
                            !loadingRates && zones.length === 0 && h(EmptyState, { mutedColor:mutedColor, icon:'🗺️', title:'No rate areas', sub:'Create rate areas in the Shipping → Rate Areas section first, then come back here.' }),

                            /* Per-zone sections */
                            !loadingRates && zones.map(function(z) {
                                var zoneId = Number(z.id);
                                var locs = z.locations || [];
                                var locSummary = locs.length === 0 ? 'Worldwide' : locs.map(function(l){ return l.code; }).join(', ');
                                /* Find all rates for this zone */
                                var zoneRates = methRates.filter(function(r){ return Number(r.zone_id) === zoneId; });
                                /* Group by rate_type */
                                var ratesByType = {};
                                zoneRates.forEach(function(r){ ratesByType[r.rate_type] = r; });
                                var flatRate = ratesByType['flat'];
                                var basePrice = flatRate && flatRate.conditions && flatRate.conditions.length > 0 ? flatRate.conditions[0].price : '0.00';

                                var condTypeLabels = { by_price:'Price condition', by_weight:'Weight condition', by_items:'Items condition' };
                                var condTypeUnits = { by_price:'$', by_weight:'lbs', by_items:'item' };

                                /* ── Local editing helpers (edit locally, save on blur) ── */
                                function makeCondKey(rateId, ci, field) { return rateId + ':' + ci + ':' + field; }

                                /* Render a single condition table */
                                function renderCondTable(rType) {
                                    var rate = ratesByType[rType];
                                    if (!rate) return null;
                                    var conds = rate.conditions || [];
                                    var unit = condTypeUnits[rType] || '';
                                    var headerBg = dark ? '#1E2130' : '#F0F2F5';
                                    var rateId = rate.id || rate.rate_id;

                                    function saveCondsNow(newConds) { updateConditions(rateId, newConds); }

                                    return h('div', { style:{ marginTop:S.md, marginBottom:S.md } },
                                        /* Header */
                                        h('div', { style:{ background:headerBg, padding:'6px 12px', borderRadius:'6px 6px 0 0', display:'grid', gridTemplateColumns:'1fr 180px 30px', alignItems:'center' } },
                                            h('span', { style:{ fontSize:11, fontWeight:600, color:mutedColor, textTransform:'uppercase' } }, condTypeLabels[rType] || rType),
                                            h('span', { style:{ fontSize:11, fontWeight:600, color:mutedColor, textTransform:'uppercase' } }, 'Surcharge / Discount'),
                                            h('span', null)
                                        ),
                                        /* Rows */
                                        conds.map(function(c, ci) {
                                            var isLast = ci === conds.length - 1;
                                            var isAndUp = c.to_value === null || c.to_value === '';
                                            /* Each input uses defaultValue + onBlur to avoid the re-render blank-out */
                                            function blurSave(field, val) {
                                                var nc = conds.slice();
                                                nc[ci] = Object.assign({}, nc[ci]);
                                                nc[ci][field] = val;
                                                saveCondsNow(nc);
                                            }
                                            return h('div', { key:rateId+'-'+ci, style:{ display:'grid', gridTemplateColumns:'110px 24px 130px 120px 60px 30px', gap:4, alignItems:'center', padding:'6px 12px', borderBottom:'1px solid '+borderColor } },
                                                /* From */
                                                h('input', { key:'f-'+rateId+'-'+ci, type:'number', step:'0.01',
                                                    defaultValue:c.from_value != null ? c.from_value : '',
                                                    onBlur:function(e){ blurSave('from_value', e.target.value); },
                                                    placeholder:'From ('+unit+')', style:Object.assign({},fs,{padding:'5px 8px',fontSize:12}) }),
                                                /* dash */
                                                h('span', { style:{ textAlign:'center', color:mutedColor, fontSize:12 } }, '—'),
                                                /* To — show input or "and up" label */
                                                isAndUp
                                                    ? h('div', { style:{ display:'flex', alignItems:'center', gap:4 } },
                                                        h('span', { style:{ fontSize:12, color:mutedColor, fontStyle:'italic' } }, 'and up'),
                                                        isLast && h('button', { onClick:function(){
                                                            var nc = conds.slice(); nc[ci] = Object.assign({},nc[ci],{to_value:'9999'}); saveCondsNow(nc);
                                                        }, style:{ background:'transparent', border:'none', cursor:'pointer', color:TC.accent, fontSize:10, padding:0, marginLeft:4 } }, '✎')
                                                    )
                                                    : h('div', { style:{ display:'flex', alignItems:'center', gap:4 } },
                                                        h('input', { key:'t-'+rateId+'-'+ci, type:'number', step:'0.01',
                                                            defaultValue:c.to_value,
                                                            onBlur:function(e){ blurSave('to_value', e.target.value); },
                                                            placeholder:'To ('+unit+')', style:Object.assign({},fs,{padding:'5px 8px',fontSize:12,flex:1}) }),
                                                        isLast && h('button', { onClick:function(){
                                                            var nc = conds.slice(); nc[ci] = Object.assign({},nc[ci],{to_value:null}); saveCondsNow(nc);
                                                        }, title:'Set to "and up"', style:{ background:'transparent', border:'1px solid '+borderColor, borderRadius:4, cursor:'pointer', color:mutedColor, fontSize:9, padding:'2px 4px', flexShrink:0 } }, '∞')
                                                    ),
                                                /* Surcharge value — defaultValue + onBlur to prevent blanking */
                                                h('input', { key:'p-'+rateId+'-'+ci, type:'number', step:'0.01',
                                                    defaultValue:c.price != null ? c.price : '',
                                                    onBlur:function(e){ blurSave('price', e.target.value); },
                                                    placeholder:'0.00', style:Object.assign({},fs,{padding:'5px 8px',fontSize:12}) }),
                                                /* Price type selector — onChange is fine for selects */
                                                h('select', { value:c.price_type || 'fixed',
                                                    onChange:function(e){ blurSave('price_type', e.target.value); },
                                                    style:{ background:dark?'#1E2130':'#fff', border:'1px solid '+borderColor, borderRadius:6, padding:'4px 2px', fontSize:11, color:textColor, cursor:'pointer' } },
                                                    h('option', { value:'fixed' }, '$'),
                                                    h('option', { value:'percent' }, '%')
                                                ),
                                                /* Delete */
                                                h('button', { onClick:function(){
                                                    var nc = conds.filter(function(_,i){ return i!==ci; });
                                                    if (nc.length === 0) { deleteShipRate(rateId); } else { saveCondsNow(nc); }
                                                }, style:{ background:'transparent', border:'none', cursor:'pointer', color:TC.error, fontSize:14, padding:0 } }, '×')
                                            );
                                        }),
                                        /* Add row placeholder */
                                        h('div', { style:{ display:'grid', gridTemplateColumns:'110px 24px 130px 120px 60px 30px', gap:4, alignItems:'center', padding:'6px 12px', opacity:.5 } },
                                            h('input', { placeholder:'From ('+unit+')', disabled:true, style:Object.assign({},fs,{padding:'5px 8px',fontSize:12, opacity:.6}) }),
                                            h('span', { style:{ textAlign:'center', color:mutedColor, fontSize:12 } }, '—'),
                                            h('input', { placeholder:'To ('+unit+')', disabled:true, style:Object.assign({},fs,{padding:'5px 8px',fontSize:12, opacity:.6}) }),
                                            h('input', { placeholder:'Surcharge', disabled:true, style:Object.assign({},fs,{padding:'5px 8px',fontSize:12, opacity:.6}) }),
                                            h('span', null),
                                            h('button', { onClick:function(){ addConditionRow(rateId); }, style:{ background:'transparent', border:'none', cursor:'pointer', color:TC.accent, fontSize:16, padding:0, fontWeight:700 } }, '+')
                                        )
                                    );
                                }

                                /* ── Shipping time stored in method settings as zone_times:{zoneId: '3-5 days'} ── */
                                var zoneTimesObj = form._zone_times || {};
                                var thisZoneTime = zoneTimesObj[zoneId] || '';

                                return h('div', { key:zoneId, style:{ marginBottom:S.xxl, paddingBottom:S.lg, borderBottom:'2px solid '+borderColor } },
                                    /* Zone header row */
                                    h('div', { style:{ display:'flex', alignItems:'center', justifyContent:'space-between', marginBottom:S.sm } },
                                        h('div', { style:{ fontSize:15, fontWeight:700 } }, z.name || 'Zone ' + z.id),
                                        h('div', { style:{ display:'flex', alignItems:'center', gap:S.lg } },
                                            h('div', { style:{ display:'flex', alignItems:'center', gap:6 } },
                                                h('span', { style:{ fontSize:12, color:mutedColor } }, 'Shipping time:'),
                                                h('input', { key:'zt-'+zoneId, defaultValue:thisZoneTime, placeholder:'e.g. 3-5 days',
                                                    onBlur:function(e){
                                                        var newTimes = Object.assign({}, form._zone_times || {});
                                                        newTimes[zoneId] = e.target.value;
                                                        setForm(Object.assign({}, form, { _zone_times:newTimes }));
                                                    },
                                                    style:Object.assign({},fs,{width:100,padding:'5px 8px',fontSize:12}) })
                                            ),
                                            h('div', { style:{ display:'flex', alignItems:'center', gap:6 } },
                                                h('span', { style:{ fontSize:12, color:mutedColor } }, 'Rate:'),
                                                h('span', { style:{ fontSize:12, color:mutedColor } }, '$'),
                                                h('input', { key:'br-'+zoneId, type:'number', step:'0.01', defaultValue:basePrice,
                                                    onBlur:function(e){
                                                        var v = e.target.value;
                                                        if (flatRate) {
                                                            var nc = [{ from_value:'0', to_value:null, price:v, price_type:'fixed' }];
                                                            updateConditions(flatRate.id||flatRate.rate_id, nc);
                                                            return;
                                                        }
                                                        /* KI-98: this zone has no flat rate row, so the box used to
                                                         * display a fallback 0.00, accept typing and DISCARD it with
                                                         * no request and no error. Create the base rate instead.
                                                         * Guarded so merely tabbing through an untouched field never
                                                         * creates a spurious row. */
                                                        if (v === '' || parseFloat(v || '0') === parseFloat(basePrice || '0')) return;
                                                        createBaseRate(zoneId, v);
                                                    },
                                                    style:Object.assign({},fs,{width:80,padding:'5px 8px',fontSize:12}) })
                                            )
                                        )
                                    ),
                                    h('div', { style:{ fontSize:12, color:mutedColor, marginBottom:S.md } }, locSummary),

                                    /* Condition tables for this zone */
                                    renderCondTable('by_price'),
                                    renderCondTable('by_weight'),
                                    renderCondTable('by_items'),

                                    /* Add condition type buttons */
                                    h('div', { style:{ display:'flex', gap:S.sm, marginTop:S.md } },
                                        !ratesByType['by_price'] && h('button', { onClick:function(){ addZoneRate(zoneId, 'by_price'); },
                                            style:{ background:'transparent', border:'1px solid '+borderColor, borderRadius:6, padding:'4px 12px', fontSize:11, cursor:'pointer', color:TC.accent } }, '+ Price condition'),
                                        !ratesByType['by_weight'] && h('button', { onClick:function(){ addZoneRate(zoneId, 'by_weight'); },
                                            style:{ background:'transparent', border:'1px solid '+borderColor, borderRadius:6, padding:'4px 12px', fontSize:11, cursor:'pointer', color:TC.accent } }, '+ Weight condition'),
                                        !ratesByType['by_items'] && h('button', { onClick:function(){ addZoneRate(zoneId, 'by_items'); },
                                            style:{ background:'transparent', border:'1px solid '+borderColor, borderRadius:6, padding:'4px 12px', fontSize:11, cursor:'pointer', color:TC.accent } }, '+ Items condition')
                                    )
                                );
                            })
                        )
                    ),

                    /* ══ TEST RATE CALCULATION TAB ══ */
                    (methTab === 'test') && h('div', null,
                        h('div', { style:{ fontSize:13, color:mutedColor, marginBottom:S.xl } },
                            'Enter test values and an address. This runs the real shipping engine, so the result is exactly what checkout would calculate and charge.'
                        ),
                        h('div', { style:{ display:'grid', gridTemplateColumns:'1fr 1fr', gap:24 } },
                            h('div', null,
                                h('h4', { style:{ fontSize:14, fontWeight:600, marginBottom:S.md } }, 'Test Parameters'),
                                fld('_test_price', 'Cart subtotal ($)', 'number', { placeholder:'100.00', maxW:180 }),
                                fld('_test_weight', 'Total weight (lbs)', 'number', { placeholder:'5', maxW:180 }),
                                fld('_test_items', 'Item count', 'number', { placeholder:'1', maxW:120 }),
                                h('h4', { style:{ fontSize:14, fontWeight:600, marginBottom:S.md, marginTop:S.xl } }, 'Test Address'),
                                fld('_test_country', 'Country code', 'text', { placeholder:'US', maxW:120, required:true, hint:'Two-letter ISO code. Required — rate areas are matched on the address.' }),
                                fld('_test_state', 'State / province code', 'text', { placeholder:'PA', maxW:120 }),
                                fld('_test_postcode', 'Postcode / ZIP', 'text', { placeholder:'18465', maxW:160 }),
                                h('div', { style:{ marginTop:S.lg } },
                                    h(Btn, { variant:'primary', disabled: !!(form._test_result && form._test_result.loading), onClick:function(){
                                        /* ── KI-95: server-side dry run against the REAL engine ──
                                         * Replaces a client-side re-implementation that picked an
                                         * arbitrary zone, re-derived band matching in JS, and had no
                                         * realtime path at all. Everything below is display only —
                                         * the number comes from ShippingManager::get_available_rates(),
                                         * the same call checkout makes. */
                                        var country = String(form._test_country || '').trim().toUpperCase();
                                        if (!country) {
                                            setForm(function(p){ return Object.assign({}, p, { _test_result:{ error:'Enter a country code (for example US). Rate areas are matched on the address, so the engine cannot run without one.' } }); });
                                            return;
                                        }
                                        setForm(function(p){ return Object.assign({}, p, { _test_result:{ loading:true } }); });
                                        apiFetch('/shipping/methods/' + editId + '/test-rate', {
                                            method:'POST',
                                            body:{
                                                subtotal: parseFloat(form._test_price)  || 0,
                                                weight:   parseFloat(form._test_weight) || 0,
                                                items:    parseInt(form._test_items, 10) || 1,
                                                address:{
                                                    country:  country,
                                                    state:    String(form._test_state || '').trim().toUpperCase(),
                                                    postcode: String(form._test_postcode || '').trim()
                                                }
                                            }
                                        }).then(function(d){
                                            setForm(function(p){ return Object.assign({}, p, { _test_result: Object.assign({}, d, { loading:false }) }); });
                                        }).catch(function(e){
                                            var msg = (e && e.message) || 'The rate test could not be completed.';
                                            setForm(function(p){ return Object.assign({}, p, { _test_result:{ loading:false, error:msg } }); });
                                        });
                                    }, mutedColor:mutedColor, borderColor:borderColor }, (form._test_result && form._test_result.loading) ? 'Calculating…' : '↻ Calculate Rate')
                                )
                            ),
                            h('div', null,
                                (function(){
                                    var tr = form._test_result;
                                    var panelSty = { background:dark?'#1E1E1E':'#F9FAFB', borderRadius:12, padding:24, border:'1px solid '+borderColor };
                                    var headSty  = { fontSize:16, fontWeight:600, color:TC.accent, marginBottom:S.lg };
                                    var rowSty   = { display:'flex', justifyContent:'space-between', padding:'8px 0', borderBottom:'1px solid '+borderColor };
                                    var noteSty  = { fontSize:13, color:mutedColor, lineHeight:1.6 };
                                    var row = function(k, label, value, strong){
                                        return h('div', { key:k, style:rowSty },
                                            h('span', { style:{ fontSize:13, color:mutedColor } }, label),
                                            h('span', { style: strong ? { fontSize:15, fontWeight:700, color:TC.accent } : { fontSize:13 } }, value)
                                        );
                                    };

                                    /* ── Idle: never a silent dash panel again (KI-95). The old tab's
                                     * "no result yet" state was an all-dashes object IDENTICAL to its
                                     * failure state, so clicking Calculate produced zero visible
                                     * change and a working button looked dead. ── */
                                    if (!tr) {
                                        return h('div', { style:panelSty },
                                            h('h4', { style:headSty }, 'Calculated Rate'),
                                            h('div', { style:noteSty }, 'Enter your test values and choose Calculate Rate. The result is produced by the live shipping engine — the same calculation checkout performs — so what you see here is what a customer would be charged.')
                                        );
                                    }

                                    if (tr.loading) {
                                        return h('div', { style:panelSty },
                                            h('h4', { style:headSty }, 'Calculating…'),
                                            h('div', { style:noteSty }, isRT ? 'Contacting the carrier for a live quote. This can take a few seconds.' : 'Running the shipping engine.')
                                        );
                                    }

                                    if (tr.error) {
                                        return h('div', { style:panelSty },
                                            h('h4', { style:{ fontSize:16, fontWeight:600, color:TC.error, marginBottom:S.lg } }, 'Rate test failed'),
                                            h('div', { style:noteSty }, tr.error)
                                        );
                                    }

                                    if (!tr.matched) {
                                        return h('div', { style:panelSty },
                                            h('h4', { style:{ fontSize:16, fontWeight:600, color:TC.amber || TC.accent, marginBottom:S.lg } }, 'No rate returned'),
                                            h('div', { style:noteSty }, tr.reason || 'The engine returned no rate for this method with these inputs.'),
                                            (tr.other_count > 0) && h('div', { style:{ fontSize:12, color:mutedColor, marginTop:S.md } },
                                                tr.other_count + ' other shipping ' + (tr.other_count === 1 ? 'method' : 'methods') + ' did return a rate for these inputs, so the address and cart values themselves are fine.')
                                        );
                                    }

                                    return h('div', { style:panelSty },
                                        h('h4', { style:headSty }, tr.rates.length > 1 ? 'Calculated Rates (' + tr.rates.length + ')' : 'Calculated Rate'),
                                        tr.rates.map(function(r, i){
                                            var cost = '$' + (Number(r.cost) || 0).toFixed(2);
                                            return h('div', { key:i, style:{ marginBottom: i < tr.rates.length - 1 ? S.xl : 0 } },
                                                (tr.rates.length > 1 || r.service_code) && h('div', { style:{ fontSize:13, fontWeight:600, marginBottom:6 } }, r.name || ('Method #' + r.method_id)),
                                                row('z', 'Rate area:', r.zone_name || (Number(r.zone_id) ? 'Zone ' + r.zone_id : 'Not zone-gated')),
                                                row('t', 'Delivery time:', r.delivery_time || 'Not set'),
                                                row('c', 'Total cost:', cost, true)
                                            );
                                        }),
                                        h('div', { style:{ fontSize:12, color:mutedColor, marginTop:S.md } },
                                            'Engine result for ' + (tr.cart_totals ? ('$' + Number(tr.cart_totals.subtotal).toFixed(2) + ' subtotal · ' + tr.cart_totals.total_weight + ' lb · ' + tr.cart_totals.item_count + ' item' + (tr.cart_totals.item_count === 1 ? '' : 's')) : 'these values') + '.'),
                                        tr.lines_capped && h('div', { style:{ fontSize:12, color:mutedColor, marginTop:6 } }, 'Item count was capped at 500 for this test.')
                                    );
                                })()
                            )
                        )
                    ),

                    /* ══ ADDITIONAL SETTINGS TAB ══ */
                    (methTab === 'additional') && h('div', null,
                        h('h3', { style:{ fontSize:15, fontWeight:600, marginBottom:S.lg, paddingBottom:S.sm, borderBottom:'1px solid '+borderColor } }, 'Customer Information'),
                        fld('require_address', 'Customer must specify their address', 'checkbox', {
                            hint:"If address isn't required, TitanCart will treat this shipping method as \"Pick-up\"."
                        })
                    )

                    ) /* end padding div */
                )
            );
        }

        /* ════════════════════════════════════════════
         * ZONE (RATE AREA) EDITOR VIEW
         * ════════════════════════════════════════════ */
        if (view === 'zone-edit') {
            var locTypeLabel = { country:'Country', state:'State / Province', postcode:'Zip / Postal Code' };
            return h(PageWrap, { title: editId ? ('Edit Rate Area: ' + form.name) : 'New Rate Area', sub:'Define countries, states and postcodes for this area', mutedColor:mutedColor,
                action: h('div', { style:{ display:'flex', gap:S.sm } },
                    h(Btn, { onClick:function(){ setView('zones'); }, mutedColor:mutedColor, borderColor:borderColor }, '← Back'),
                    h(Btn, { variant:'primary', onClick:saveZone, disabled:saving || !form.name, mutedColor:mutedColor, borderColor:borderColor }, saving ? 'Saving…' : 'Save')
                )
            },
                h(Card, { cardBg:cardBg, borderColor:borderColor, dark:dark },
                    /* Name */
                    h('div', { style:{ marginBottom:20 } },
                        h('label', { style:{ fontSize:13, fontWeight:500, display:'block', marginBottom:4, color:textColor } }, 'Name', h('span',{style:{color:TC.error}},' *')),
                        h('input', { value:form.name, onChange:function(e){ setForm(Object.assign({},form,{name:e.target.value})); }, placeholder:'e.g. USA, Canada, Europe', style:fs })
                    ),
                    /* Status */
                    h('div', { style:{ marginBottom:20 } },
                        h('label', { style:{ fontSize:13, fontWeight:500, display:'block', marginBottom:8, color:textColor } }, 'Status', h('span',{style:{color:TC.error}},' *')),
                        h('div', { style:{ display:'flex', gap:20 } },
                            ['active','disabled'].map(function(s) {
                                var sel = form.status === s;
                                return h('label', { key:s, style:{ display:'flex', alignItems:'center', gap:6, cursor:'pointer', fontSize:13 } },
                                    h('div', { onClick:function(){ setForm(Object.assign({},form,{status:s})); },
                                        style:{ width:18, height:18, borderRadius:'50%', border:'2px solid '+(sel?TC.accent:borderColor), display:'flex', alignItems:'center', justifyContent:'center', cursor:'pointer' } },
                                        sel && h('div', { style:{ width:10, height:10, borderRadius:'50%', background:TC.accent } })
                                    ),
                                    s.charAt(0).toUpperCase() + s.slice(1)
                                );
                            })
                        )
                    ),

                    /* Current locations */
                    editId && h('div', { style:{ marginTop:S.xxl } },
                        h('h3', { style:{ fontSize:15, fontWeight:600, marginBottom:S.lg, paddingBottom:S.sm, borderBottom:'1px solid '+borderColor } }, 'Locations in this Rate Area'),

                        zoneLocs.length === 0 && h('div', { style:{ fontSize:13, color:mutedColor, marginBottom:S.lg } }, 'No locations yet. A rate area with no locations applies to all addresses (default / worldwide).'),

                        zoneLocs.length > 0 && h('div', { style:{ marginBottom:S.lg } },
                            zoneLocs.map(function(loc) {
                                return h('div', { key:loc.id, style:{ display:'flex', alignItems:'center', justifyContent:'space-between', padding:'8px 0', borderBottom:'1px solid '+borderColor } },
                                    h('div', { style:{ display:'flex', alignItems:'center', gap:S.md } },
                                        h('span', { style:{ fontSize:11, fontWeight:500, color:TC.accent, background:dark?'rgba(37,99,235,.1)':'#EFF6FF', padding:'2px 8px', borderRadius:4, textTransform:'uppercase' } }, loc.type),
                                        h('span', { style:{ fontSize:13, fontWeight:500 } }, loc.code)
                                    ),
                                    h('button', { onClick:function(){ removeLocation(loc); }, style:{ background:'transparent', border:'1px solid '+TC.error+'20', borderRadius:6, padding:'3px 8px', fontSize:11, cursor:'pointer', color:TC.error } }, 'Remove')
                                );
                            })
                        ),

                        /* Add location form */
                        h('div', { style:{ display:'flex', alignItems:'flex-end', gap:S.md, marginTop:S.md } },
                            h('div', null,
                                h('label', { style:{ fontSize:12, fontWeight:500, display:'block', marginBottom:4 } }, 'Type'),
                                h('select', { value:locForm.type, onChange:function(e){ setLocForm(Object.assign({},locForm,{type:e.target.value, code:''})); }, style:Object.assign({},selSty,{width:160}) },
                                    h('option', { value:'country' }, 'Country'),
                                    h('option', { value:'state' }, 'State / Province'),
                                    h('option', { value:'postcode' }, 'Zip / Postal Code')
                                )
                            ),
                            h('div', { style:{ flex:1 } },
                                h('label', { style:{ fontSize:12, fontWeight:500, display:'block', marginBottom:4 } }, 'Code'),
                                h('input', { value:locForm.code, onChange:function(e){ setLocForm(Object.assign({},locForm,{code:e.target.value})); },
                                    placeholder: locForm.type === 'country' ? 'e.g. US, CA, GB' : (locForm.type === 'state' ? 'e.g. US:CA, US:NY' : 'e.g. 90210 or 902*'),
                                    style:fs,
                                    onKeyDown: function(e){ if (e.key === 'Enter') addLocation(); },
                                })
                            ),
                            h(Btn, { variant:'primary', onClick:addLocation, disabled:saving || !locForm.code.trim(), mutedColor:mutedColor, borderColor:borderColor }, saving ? '…' : '+ Add')
                        ),
                        h('div', { style:{ fontSize:11, color:mutedColor, marginTop:6 } },
                            locForm.type === 'country' ? 'Use 2-letter ISO country codes (US, CA, GB, DE, etc.).'
                            : locForm.type === 'state' ? 'Use COUNTRY:STATE format (US:CA for California, US:NY for New York).'
                            : 'Exact (90210), wildcard (902*), or range (90000...90999).'
                        )
                    ),
                    !editId && h('div', { style:{ fontSize:13, color:mutedColor, marginTop:S.xl, fontStyle:'italic' } }, 'Save the rate area first, then you can add locations to it.')
                )
            );
        }

        /* ════════════════════════════════════════════
         * RATE AREAS (ZONES) LIST VIEW
         * ════════════════════════════════════════════ */
        if (view === 'zones') {
            return h(PageWrap, { title:'Rate Areas', sub:'Define geographic regions for shipping rate calculation', mutedColor:mutedColor,
                action: h('div', { style:{ display:'flex', gap:S.sm } },
                    h(Btn, { onClick:function(){ setView('methods'); }, mutedColor:mutedColor, borderColor:borderColor }, '← Shipping Methods'),
                    h(Btn, { variant:'primary', onClick:openNewZone, mutedColor:mutedColor, borderColor:borderColor }, '+ Add Rate Area')
                )
            },
                h(Card, { cardBg:cardBg, borderColor:borderColor, dark:dark, pad:0 },
                    h('div', { style:{ display:'grid', gridTemplateColumns:'1fr 200px 80px 60px', padding:'12px 24px', background:tableHeaderBg, borderBottom:'1px solid '+borderColor } },
                        ['Name', 'Locations', 'Status', ''].map(function(col) {
                            return h('span', { key:col, style:{ fontSize:10, fontWeight:500, color:mutedColor, textTransform:'uppercase', letterSpacing:'.5px' } }, col);
                        })
                    ),
                    loadZ && h(Spinner, { mutedColor:mutedColor }),
                    !loadZ && zones.length === 0 && h(EmptyState, { mutedColor:mutedColor, icon:'🗺️', title:'No rate areas', sub:'Create rate areas to define where you ship to.' }),
                    !loadZ && zones.map(function(z, i) {
                        var locs = z.locations || [];
                        var locSummary = locs.length === 0 ? 'Worldwide' : locs.map(function(l){ return l.code; }).join(', ');
                        var statusColor = z.status === 'active' ? TC.success : mutedColor;
                        return h('div', { key:z.id||i,
                            style:{ display:'grid', gridTemplateColumns:'1fr 200px 80px 60px', padding:'12px 24px', alignItems:'center',
                                borderBottom:i<zones.length-1?'1px solid '+borderColor:'none', cursor:'pointer', transition:'background .1s' },
                            onClick: function(){ openEditZone(z); },
                            onMouseEnter: function(e){ e.currentTarget.style.background = dark?'rgba(255,255,255,.02)':'#F9FAFB'; },
                            onMouseLeave: function(e){ e.currentTarget.style.background = 'transparent'; },
                        },
                            h('span', { style:{ fontSize:13, fontWeight:500, color:TC.accent } }, z.name || 'Zone ' + z.id),
                            h('span', { style:{ fontSize:12, color:mutedColor, overflow:'hidden', textOverflow:'ellipsis', whiteSpace:'nowrap' } }, locSummary),
                            h('span', { style:{ fontSize:11, fontWeight:500, color:statusColor, background:z.status==='active'?(dark?'rgba(22,163,74,.1)':'#F0FDF4'):(dark?'rgba(115,115,115,.1)':'#F5F5F5'), padding:'2px 8px', borderRadius:5, width:'fit-content', display:'inline-block' } },
                                z.status === 'active' ? 'Active' : 'Disabled'
                            ),
                            h('button', { onClick:function(e){ e.stopPropagation(); deleteZone(z); },
                                style:{ background:'transparent', border:'1px solid '+TC.error+'20', borderRadius:6, padding:'3px 8px', fontSize:11, cursor:'pointer', color:TC.error } }, 'Del')
                        );
                    })
                )
            );
        }

        /* ════════════════════════════════════════════
         * METHODS LIST VIEW (default)
         * ════════════════════════════════════════════ */
        return h(PageWrap, { title:'Shipping Methods', sub:'Manage how products are delivered to customers', mutedColor:mutedColor, backBtn:backBtn,
            action: h('div', { style:{ display:'flex', gap:S.sm } },
                h(Btn, { onClick:function(){ setView('zones'); }, mutedColor:mutedColor, borderColor:borderColor }, 'Rate Areas'),
                h(Btn, { variant:'primary', onClick:openNewMethod, mutedColor:mutedColor, borderColor:borderColor }, '+ Add Shipping Method')
            )
        },
            h(Card, { cardBg:cardBg, borderColor:borderColor, dark:dark, pad:0 },
                /* Table header */
                h('div', { style:{ display:'grid', gridTemplateColumns:'60px 1fr 120px 140px 90px 80px', padding:'12px 24px', background:tableHeaderBg, borderBottom:'1px solid '+borderColor } },
                    ['Pos.', 'Name', 'Delivery Time', 'Weight Limit', 'Status', ''].map(function(col) {
                        return h('span', { key:col, style:{ fontSize:10, fontWeight:500, color:mutedColor, textTransform:'uppercase', letterSpacing:'.5px' } }, col);
                    })
                ),
                loadM && h(Spinner, { mutedColor:mutedColor }),
                !loadM && methods.length === 0 && h(EmptyState, { mutedColor:mutedColor, icon:'🚚', title:'No shipping methods yet', sub:'Add flat rate, free shipping, or carrier-calculated rates.' }),
                !loadM && methods.map(function(m, i) {
                    var s = m.settings || {};
                    var statusColor = m.status === 'active' ? TC.success : mutedColor;
                    var weightStr = '';
                    if (m.min_weight || m.max_weight) {
                        weightStr = (m.min_weight ? parseFloat(m.min_weight).toFixed(1) : '0') + ' — ' + (m.max_weight ? parseFloat(m.max_weight).toFixed(1) : '∞');
                    }
                    return h('div', { key:m.id||i,
                        style:{ display:'grid', gridTemplateColumns:'60px 1fr 120px 140px 90px 80px', padding:'12px 24px', alignItems:'center',
                            borderBottom:i<methods.length-1?'1px solid '+borderColor:'none', cursor:'pointer', transition:'background .1s' },
                        onClick: function(){ openEditMethod(m); },
                        onMouseEnter: function(e){ e.currentTarget.style.background = dark?'rgba(255,255,255,.02)':'#F9FAFB'; },
                        onMouseLeave: function(e){ e.currentTarget.style.background = 'transparent'; },
                    },
                        h('span', { style:{ fontSize:12, color:mutedColor } }, m.position || '0'),
                        h('div', null,
                            h('div', { style:{ fontSize:13, fontWeight:500, color:TC.accent } }, m.name || carrierLabels[m.carrier] || m.carrier),
                            h('div', { style:{ fontSize:11, color:mutedColor, marginTop:2 } },
                                (function() {
                                    var label = carrierLabels[m.carrier] || m.carrier;
                                    var ss = m.settings || {};
                                    var codes = Array.isArray(ss.service_codes) ? ss.service_codes : (ss.service_code ? [ss.service_code] : []);
                                    if (codes.length && CARRIER_SERVICES[m.carrier]) {
                                        var svcMap = {};
                                        CARRIER_SERVICES[m.carrier].forEach(function(s){ svcMap[s.value] = s.label; });
                                        label = codes.length === 1 ? (svcMap[codes[0]] || label) : (label + ' — ' + codes.length + ' services');
                                    }
                                    return label;
                                })()
                            )
                        ),
                        h('span', { style:{ fontSize:12, color:mutedColor } }, s.delivery_time || '—'),
                        h('span', { style:{ fontSize:12, color:mutedColor } }, weightStr || '—'),
                        h('span', null,
                            h('span', { style:{ fontSize:11, fontWeight:500, color:statusColor, background:m.status==='active'?(dark?'rgba(22,163,74,.1)':'#F0FDF4'):(dark?'rgba(115,115,115,.1)':'#F5F5F5'), padding:'2px 8px', borderRadius:5, width:'fit-content', display:'inline-block', cursor:'pointer' },
                                onClick: function(e){ e.stopPropagation(); toggleMethodStatus(m); },
                            }, m.status === 'active' ? 'Active' : 'Disabled')
                        ),
                        h('button', { onClick:function(e){ e.stopPropagation(); deleteMethod(m); }, title:'Delete permanently',
                            style:{ background:'transparent', border:'1px solid '+TC.error+'20', borderRadius:6, padding:'3px 8px', fontSize:11, cursor:'pointer', color:TC.error } }, 'Delete')
                    );
                })
            )
        );
    }

    /* ════════════════════════════════
     * TAX PAGE
     * ════════════════════════════════ */
    function TaxPage({ dark, cardBg, textColor, mutedColor, borderColor, tableHeaderBg, backBtn }) {
        /* ── State ── */
        var _taxes   = useState([]);       var taxes   = _taxes[0];   var setTaxes   = _taxes[1];
        var _loading = useState(true);     var loading = _loading[0]; var setLoading = _loading[1];
        var _editing = useState(null);     var editing = _editing[0]; var setEditing = _editing[1]; /* null=list, 'new' or class_id */
        var _tab     = useState('general');var tab     = _tab[0];     var setTab     = _tab[1];
        var _form    = useState({});       var form    = _form[0];    var setForm    = _form[1];
        var _saving  = useState(false);    var saving  = _saving[0];  var setSaving  = _saving[1];
        var _rates   = useState([]);       var rates   = _rates[0];   var setRates   = _rates[1];
        var _loadR   = useState(false);    var loadR   = _loadR[0];   var setLoadR   = _loadR[1];
        var _rateForm = useState({ country_code:'US', state_code:'', rate:'', name:'', is_compound:'0' });
        var rateForm = _rateForm[0]; var setRateForm = _rateForm[1];
        var _showRateModal = useState(false);
        var showRateModal = _showRateModal[0]; var setShowRateModal = _showRateModal[1];

        var fs = fieldSty(dark, borderColor);

        /* ── Load taxes ── */
        useEffect(function() {
            apiFetch('/tax/classes').then(function(d) {
                setTaxes(Array.isArray(d) ? d : (d.classes || []));
                setLoading(false);
            }).catch(function(){ setLoading(false); });
        }, []);

        /* ── Load rates when editing ── */
        useEffect(function() {
            if (editing === null || editing === 'new') { setRates([]); return; }
            setLoadR(true);
            apiFetch('/tax/rates?class_id=' + editing).then(function(d) {
                setRates(Array.isArray(d) ? d : (d.rates || []));
                setLoadR(false);
            }).catch(function(){ setLoadR(false); });
        }, [editing]);

        function refreshList() {
            apiFetch('/tax/classes').then(function(d) {
                setTaxes(Array.isArray(d) ? d : (d.classes || []));
            });
        }

        /* ── Open editor ── */
        function openNew() {
            setForm({ name:'', registration_number:'', priority:'0', rates_depend_on:'shipping', status:'active', price_includes_tax:false });
            setTab('general');
            setEditing('new');
        }
        function openEdit(tax) {
            setForm({
                name: tax.name || '',
                registration_number: tax.registration_number || '',
                priority: String(tax.priority || 0),
                rates_depend_on: tax.rates_depend_on || 'shipping',
                status: tax.status || 'active',
                price_includes_tax: tax.price_includes_tax == 1 || tax.price_includes_tax === '1',
            });
            setTab('general');
            setEditing(Number(tax.id || tax.class_id));
        }

        /* ── Save tax ── */
        function saveTax() {
            setSaving(true);
            var body = {
                name: form.name,
                registration_number: form.registration_number,
                priority: parseInt(form.priority) || 0,
                rates_depend_on: form.rates_depend_on,
                status: form.status,
                price_includes_tax: form.price_includes_tax ? 1 : 0,
            };
            var url = editing === 'new' ? '/tax/classes' : '/tax/classes/' + editing;
            var method = editing === 'new' ? 'POST' : 'PUT';
            apiFetch(url, { method: method, body: body }).then(function(d) {
                setSaving(false);
                refreshList();
                if (editing === 'new' && d && (d.id || d.class_id)) {
                    setEditing(Number(d.id || d.class_id));
                }
            }).catch(function(){ setSaving(false); });
        }

        /* ── Delete tax ── */
        function deleteTax(tax) {
            if (!confirm('Delete tax "' + (tax.name||tax.handle) + '"?')) return;
            apiFetch('/tax/classes/' + (tax.id || tax.class_id), { method:'DELETE' }).then(function() { refreshList(); });
        }

        /* ── Save rate inline ── */
        function saveRate(r) {
            var body = { class_id: editing, zone_id: r.zone_id, rate: parseFloat(r.rate) || 0 };
            if (r.rate_id) {
                apiFetch('/tax/rates/' + r.rate_id, { method:'PUT', body: body });
            }
        }
        function updateRateLocal(idx, key, val) {
            setRates(function(prev) {
                var next = prev.slice();
                next[idx] = Object.assign({}, next[idx], { [key]: val });
                return next;
            });
        }
        function addRate() {
            setSaving(true);
            apiFetch('/tax/rates', { method:'POST', body:{
                class_id: editing,
                country_code: rateForm.country_code,
                state_code: rateForm.state_code,
                rate: parseFloat(rateForm.rate) || 0,
                name: rateForm.name,
                is_compound: rateForm.is_compound === '1' ? 1 : 0,
            } })
                .then(function() {
                    setSaving(false);
                    setShowRateModal(false);
                    setRateForm({ country_code:'US', state_code:'', rate:'', name:'', is_compound:'0' });
                    apiFetch('/tax/rates?class_id=' + editing).then(function(d) {
                        setRates(Array.isArray(d) ? d : (d.rates || []));
                    });
                }).catch(function(e){
                    setSaving(false);
                    alert('Failed to add rate. ' + (e && e.message ? e.message : ''));
                });
        }
        function deleteRate(r) {
            apiFetch('/tax/rates/' + (r.rate_id || r.id), { method:'DELETE' }).then(function() {
                setRates(function(prev){ return prev.filter(function(x){ return (x.rate_id||x.id) !== (r.rate_id||r.id); }); });
            });
        }

        /* ════════════════════════════════════════
         * EDIT / NEW VIEW (General + Tax Rates tabs)
         * ════════════════════════════════════════ */
        if (editing !== null) {
            var isNew = editing === 'new';
            var tabStyle = function(t) {
                var active = tab === t;
                return {
                    flex:1, textAlign:'center', padding:'12px 0', cursor:'pointer', fontSize:13, fontWeight:active?600:400,
                    color: active ? TC.accent : mutedColor,
                    borderBottom: active ? '2px solid '+TC.accent : '2px solid transparent',
                    transition:'all .15s',
                };
            };

            return h(PageWrap, { title: isNew ? 'New Tax Rule' : 'Edit Tax Rule', sub:'Define the tax and its rates', mutedColor:mutedColor,
                action: h('div', { style:{ display:'flex', gap:S.sm } },
                    h(Btn, { onClick:function(){ setEditing(null); }, mutedColor:mutedColor, borderColor:borderColor }, '← Back'),
                    h(Btn, { variant:'primary', onClick:saveTax, disabled:saving || !form.name, mutedColor:mutedColor, borderColor:borderColor }, saving ? 'Saving…' : (isNew ? 'Create' : 'Save'))
                )
            },
                h(Card, { cardBg:cardBg, borderColor:borderColor, dark:dark, pad:0 },
                    /* Tab bar */
                    h('div', { style:{ display:'flex', borderBottom:'1px solid '+borderColor } },
                        h('div', { onClick:function(){ setTab('general'); }, style:tabStyle('general') }, 'General'),
                        !isNew && h('div', { onClick:function(){ setTab('rates'); }, style:tabStyle('rates') }, 'Tax Rates')
                    ),

                    /* General tab */
                    tab === 'general' && h('div', { style:{ padding:24, maxWidth:540 } },
                        /* Name */
                        h('div', { style:{ marginBottom:20 } },
                            h('label', { style:{ fontSize:13, fontWeight:500, display:'block', marginBottom:4, color:textColor } }, 'Name', h('span',{style:{color:TC.error}},' *')),
                            h('input', { value:form.name, onChange:function(e){ setForm(Object.assign({},form,{name:e.target.value})); }, style:fs })
                        ),
                        /* Registration number */
                        h('div', { style:{ marginBottom:20 } },
                            h('label', { style:{ fontSize:13, fontWeight:500, display:'block', marginBottom:4, color:textColor } }, 'Registration number'),
                            h('input', { value:form.registration_number, onChange:function(e){ setForm(Object.assign({},form,{registration_number:e.target.value})); }, style:fs }),
                            h('div', { style:{ fontSize:12, color:mutedColor, marginTop:4 } }, 'Registration number of this tax in the store.')
                        ),
                        /* Priority */
                        h('div', { style:{ marginBottom:20 } },
                            h('label', { style:{ fontSize:13, fontWeight:500, display:'block', marginBottom:4, color:textColor } }, 'Priority'),
                            h('input', { type:'number', value:form.priority, onChange:function(e){ setForm(Object.assign({},form,{priority:e.target.value})); }, style:Object.assign({},fs,{maxWidth:180}) })
                        ),
                        /* Rates depend on */
                        h('div', { style:{ marginBottom:20 } },
                            h('label', { style:{ fontSize:13, fontWeight:500, display:'block', marginBottom:4, color:textColor } }, 'Rates depend on', h('span',{style:{color:TC.error}},' *')),
                            h('select', { value:form.rates_depend_on, onChange:function(e){ setForm(Object.assign({},form,{rates_depend_on:e.target.value})); }, style:Object.assign({},fs,{cursor:'pointer'}) },
                                h('option', { value:'shipping' }, 'Shipping address'),
                                h('option', { value:'billing' }, 'Billing address')
                            )
                        ),
                        /* Status */
                        h('div', { style:{ marginBottom:20 } },
                            h('label', { style:{ fontSize:13, fontWeight:500, display:'block', marginBottom:8, color:textColor } }, 'Status', h('span',{style:{color:TC.error}},' *')),
                            h('div', { style:{ display:'flex', gap:20 } },
                                ['active','disabled'].map(function(s) {
                                    var sel = form.status === s;
                                    return h('label', { key:s, style:{ display:'flex', alignItems:'center', gap:6, cursor:'pointer', fontSize:13 } },
                                        h('div', { onClick:function(){ setForm(Object.assign({},form,{status:s})); },
                                            style:{ width:18, height:18, borderRadius:'50%', border:'2px solid '+(sel?TC.accent:borderColor), display:'flex', alignItems:'center', justifyContent:'center', cursor:'pointer' } },
                                            sel && h('div', { style:{ width:10, height:10, borderRadius:'50%', background:TC.accent } })
                                        ),
                                        s.charAt(0).toUpperCase() + s.slice(1)
                                    );
                                })
                            )
                        ),
                        /* Price includes tax */
                        h('div', { style:{ marginBottom:20 } },
                            h('label', { style:{ display:'flex', alignItems:'center', gap:10, cursor:'pointer' } },
                                h('div', {
                                    onClick: function(){ setForm(Object.assign({},form,{price_includes_tax:!form.price_includes_tax})); },
                                    style:{ width:40, height:22, borderRadius:12, cursor:'pointer',
                                        background: form.price_includes_tax ? TC.accent : (dark?'#333':'#D4D4D4'),
                                        transition:'background .15s', position:'relative', flexShrink:0 }
                                },
                                    h('div', { style:{ width:18, height:18, borderRadius:'50%', background:'#fff',
                                        position:'absolute', top:2, left: form.price_includes_tax ? 20 : 2,
                                        transition:'left .15s', boxShadow:'0 1px 3px rgba(0,0,0,.2)' } })
                                ),
                                h('span', { style:{ fontSize:13, color:textColor } }, 'Price includes tax')
                            )
                        )
                    ),

                    /* Tax Rates tab */
                    tab === 'rates' && h('div', { style:{ padding:0 } },
                        h('div', { style:{ display:'flex', justifyContent:'flex-end', padding:'12px 24px', borderBottom:'1px solid '+borderColor } },
                            h(Btn, { variant:'primary', onClick:function(){ setRateForm({ country_code:'US', state_code:'', rate:'', name:'', is_compound:'0' }); setShowRateModal(true); }, disabled:saving, mutedColor:mutedColor, borderColor:borderColor }, '+ Add Rate')
                        ),
                        /* Header */
                        h('div', { style:{ display:'grid', gridTemplateColumns:'1fr 120px 120px 60px', padding:'10px 24px', background:tableHeaderBg, borderBottom:'1px solid '+borderColor } },
                            h('span', { style:{ fontSize:10, fontWeight:500, color:mutedColor, textTransform:'uppercase', letterSpacing:'.5px' } }, 'Rate Area'),
                            h('span', { style:{ fontSize:10, fontWeight:500, color:mutedColor, textTransform:'uppercase', letterSpacing:'.5px' } }, 'Rate Value'),
                            h('span', { style:{ fontSize:10, fontWeight:500, color:mutedColor, textTransform:'uppercase', letterSpacing:'.5px' } }, 'Type'),
                            h('span', null)
                        ),
                        loadR && h(Spinner, { mutedColor:mutedColor }),
                        !loadR && rates.length === 0 && h(EmptyState, { mutedColor:mutedColor, icon:'🏷️', title:'No rates yet', sub:'Add a rate for each country/region.' }),
                        !loadR && rates.map(function(r, i) {
                            var rateId = r.rate_id || r.id;
                            return h('div', { key:rateId||i, style:{ display:'grid', gridTemplateColumns:'1fr 120px 120px 60px', padding:'10px 24px', alignItems:'center', borderBottom:i<rates.length-1?'1px solid '+borderColor:'none' } },
                                h('span', { style:{ fontSize:13 } }, r.zone_name || r.name || r.country_code || 'Default rate area'),
                                h('input', { type:'number', value:r.rate!=null?r.rate:'', onChange:function(e){ updateRateLocal(i,'rate',e.target.value); },
                                    onBlur:function(){ saveRate(r); },
                                    style:Object.assign({},fs,{padding:'6px 8px',fontSize:12}) }),
                                h('span', { style:{ fontSize:12, color:mutedColor } }, (r.is_compound == 1 || r.is_compound === '1') ? 'Compound (%)' : 'Percent (%)'),
                                h('button', { onClick:function(){ deleteRate(r); }, style:{ background:'transparent', border:'1px solid '+TC.error+'20', borderRadius:6, padding:'3px 8px', fontSize:11, cursor:'pointer', color:TC.error } }, 'Del')
                            );
                        })
                    )
                ),

                /* ── Add Rate Modal ── */
                showRateModal && h('div', { style:{ position:'fixed', inset:0, background:'rgba(0,0,0,.5)', display:'flex', alignItems:'center', justifyContent:'center', zIndex:9999 } },
                    h('div', { style:{ background:cardBg, borderRadius:16, padding:24, width:480, boxShadow:'0 20px 60px rgba(0,0,0,.3)' } },
                        h('div', { style:{ display:'flex', justifyContent:'space-between', alignItems:'center', marginBottom:20 } },
                            h('h3', { style:{ fontSize:15, fontWeight:600 } }, 'Add Tax Rate'),
                            h('button', { onClick:function(){ setShowRateModal(false); }, style:{ background:'transparent', border:'none', fontSize:22, cursor:'pointer', color:mutedColor } }, '×')
                        ),
                        /* Rate Area Name */
                        h('div', { style:{ marginBottom:16 } },
                            h('label', { style:{ fontSize:13, fontWeight:500, display:'block', marginBottom:4 } }, 'Rate Area Name'),
                            h('input', { value:rateForm.name, onChange:function(e){ setRateForm(Object.assign({},rateForm,{name:e.target.value})); },
                                placeholder:'e.g. California, USA, Default', style:fs })
                        ),
                        /* Country + State side by side */
                        h('div', { style:{ display:'grid', gridTemplateColumns:'1fr 1fr', gap:12, marginBottom:16 } },
                            h('div', null,
                                h('label', { style:{ fontSize:13, fontWeight:500, display:'block', marginBottom:4 } }, 'Country Code'),
                                h('input', { value:rateForm.country_code, onChange:function(e){ setRateForm(Object.assign({},rateForm,{country_code:e.target.value.toUpperCase()})); },
                                    placeholder:'e.g. US, CA, GB', maxLength:2, style:fs }),
                                h('div', { style:{ fontSize:11, color:mutedColor, marginTop:4 } }, 'Leave blank for all countries.')
                            ),
                            h('div', null,
                                h('label', { style:{ fontSize:13, fontWeight:500, display:'block', marginBottom:4 } }, 'State / Province Code'),
                                h('input', { value:rateForm.state_code, onChange:function(e){ setRateForm(Object.assign({},rateForm,{state_code:e.target.value.toUpperCase()})); },
                                    placeholder:'e.g. CA, NY, TX', maxLength:5, style:fs }),
                                h('div', { style:{ fontSize:11, color:mutedColor, marginTop:4 } }, 'Leave blank for entire country.')
                            )
                        ),
                        /* Rate + Type side by side */
                        h('div', { style:{ display:'grid', gridTemplateColumns:'1fr 1fr', gap:12, marginBottom:20 } },
                            h('div', null,
                                h('label', { style:{ fontSize:13, fontWeight:500, display:'block', marginBottom:4 } }, 'Rate Value (%)'),
                                h('input', { type:'number', step:'0.01', value:rateForm.rate, onChange:function(e){ setRateForm(Object.assign({},rateForm,{rate:e.target.value})); },
                                    placeholder:'e.g. 7.25', style:fs })
                            ),
                            h('div', null,
                                h('label', { style:{ fontSize:13, fontWeight:500, display:'block', marginBottom:4 } }, 'Type'),
                                h('select', { value:rateForm.is_compound, onChange:function(e){ setRateForm(Object.assign({},rateForm,{is_compound:e.target.value})); },
                                    style:Object.assign({},fs,{cursor:'pointer'}) },
                                    h('option', { value:'0' }, 'Percent (standard)'),
                                    h('option', { value:'1' }, 'Compound (tax on tax)')
                                ),
                                h('div', { style:{ fontSize:11, color:mutedColor, marginTop:4 } }, 'Standard sales tax is Percent. Compound calculates on already-taxed total.')
                            )
                        ),
                        h('div', { style:{ display:'flex', gap:8, justifyContent:'flex-end' } },
                            h(Btn, { onClick:function(){ setShowRateModal(false); }, mutedColor:mutedColor, borderColor:borderColor }, 'Cancel'),
                            h(Btn, { variant:'primary', onClick:addRate, disabled:saving || !rateForm.name, mutedColor:mutedColor, borderColor:borderColor }, saving ? 'Adding…' : 'Add Rate')
                        )
                    )
                )
            );
        }

        /* ════════════════════════════════════════
         * LIST VIEW — Flat table of all taxes
         * ════════════════════════════════════════ */
        return h(PageWrap, { title:'Tax Rules', sub:'Manage taxes and their rates', mutedColor:mutedColor, backBtn:backBtn,
            action: h(Btn, { variant:'primary', onClick:openNew, mutedColor:mutedColor, borderColor:borderColor }, '+ Add Tax')
        },
            h(Card, { cardBg:cardBg, borderColor:borderColor, dark:dark, pad:0 },
                /* Table header */
                h('div', { style:{ display:'grid', gridTemplateColumns:'1fr 140px 80px 160px 120px 90px 60px', padding:'12px 24px', background:tableHeaderBg, borderBottom:'1px solid '+borderColor } },
                    ['Name','Reg. Number','Priority','Rates Depend On','Price incl. Tax','Status',''].map(function(col) {
                        return h('span', { key:col, style:{ fontSize:10, fontWeight:500, color:mutedColor, textTransform:'uppercase', letterSpacing:'.5px' } }, col);
                    })
                ),
                loading && h(Spinner, { mutedColor:mutedColor }),
                !loading && taxes.length === 0 && h(EmptyState, { mutedColor:mutedColor, icon:'🏷️', title:'No taxes', sub:'Create your first tax rule.' }),
                !loading && taxes.map(function(tax, i) {
                    var statusColor = tax.status === 'active' ? TC.success : mutedColor;
                    return h('div', { key:tax.id||tax.class_id||i,
                        style:{ display:'grid', gridTemplateColumns:'1fr 140px 80px 160px 120px 90px 60px', padding:'12px 24px', alignItems:'center',
                            borderBottom:i<taxes.length-1?'1px solid '+borderColor:'none', cursor:'pointer', transition:'background .1s' },
                        onClick: function(){ openEdit(tax); },
                        onMouseEnter: function(e){ e.currentTarget.style.background = dark?'rgba(255,255,255,.02)':'#F9FAFB'; },
                        onMouseLeave: function(e){ e.currentTarget.style.background = 'transparent'; },
                    },
                        h('span', { style:{ fontSize:13, fontWeight:500, color:TC.accent } }, tax.name || tax.handle),
                        h('span', { style:{ fontSize:12, color:mutedColor } }, tax.registration_number || '—'),
                        h('span', { style:{ fontSize:12 } }, tax.priority || '0'),
                        h('span', { style:{ fontSize:12 } }, (tax.rates_depend_on === 'billing' ? 'Billing address' : 'Shipping address')),
                        h('span', { style:{ fontSize:12 } }, (tax.price_includes_tax == 1 || tax.price_includes_tax === '1') ? '✓ Yes' : 'No'),
                        h('span', { style:{ fontSize:11, fontWeight:500, color:statusColor, background:tax.status==='active'?(dark?'rgba(22,163,74,.1)':'#F0FDF4'):(dark?'rgba(115,115,115,.1)':'#F5F5F5'), padding:'2px 8px', borderRadius:5, width:'fit-content', display:'inline-block' } },
                            tax.status === 'active' ? 'Active' : 'Disabled'
                        ),
                        h('button', { onClick:function(e){ e.stopPropagation(); deleteTax(tax); },
                            style:{ background:'transparent', border:'1px solid '+TC.error+'20', borderRadius:6, padding:'3px 8px', fontSize:11, cursor:'pointer', color:TC.error } }, 'Del')
                    );
                })
            )
        );
    }

    /* ════════════════════════════════════════════════════════════════════
     * SETTINGS PAGE — Card grid + sub-page routing
     * ════════════════════════════════════════════════════════════════════ */

    /* ── Settings sub-page definitions (left nav inside General Settings) ── */
    var SETTINGS_SUB_PAGES = [
        { slug:'general',       label:'General' },
        { slug:'appearance',    label:'Appearance' },
        { slug:'company',       label:'Company' },
        { slug:'checkout',      label:'Checkout' },
        { slug:'emails',        label:'E-mails' },
        { slug:'thumbnails',    label:'Thumbnails' },
        { slug:'security',      label:'Security' },
        { slug:'logging',       label:'Logging' },
        // S129 Phase 2 — non-schema-driven page (custom layout for the
        // agreement editor + version history). Intercepted in SettingsPage.
        { slug:'download-agreements', label:'Download Agreements' },
    ];

    /* ── Currencies CRUD page ──────────────────────────────────────── */
    function CurrenciesPage({ dark, cardBg, textColor, mutedColor, borderColor, tableHeaderBg, backBtn }) {
        var _list    = useState([]);    var list    = _list[0];    var setList    = _list[1];
        var _loading = useState(true);  var loading = _loading[0]; var setLoading = _loading[1];
        var _modal   = useState(false); var modal   = _modal[0];   var setModal   = _modal[1];
        var _editId  = useState(null);  var editId  = _editId[0];  var setEditId  = _editId[1];
        var _form    = useState({});    var form    = _form[0];    var setForm    = _form[1];
        var _saving  = useState(false); var saving  = _saving[0];  var setSaving  = _saving[1];

        var fs = fieldSty(dark, borderColor);
        var selSty = Object.assign({}, fs, { cursor:'pointer' });

        function refresh() {
            apiFetch('/currencies').then(function(d) { setList(Array.isArray(d) ? d : []); setLoading(false); }).catch(function(){ setLoading(false); });
        }
        useEffect(refresh, []);

        function openNew() {
            setForm({ name:'', code:'', symbol:'$', symbol_after:false, decimals:'2', thousands_sep:',', decimal_sep:'.', exchange_rate:'1.000000', is_primary:false, position:'0', status:'active' });
            setEditId(null);
            setModal(true);
        }
        function openEdit(c) {
            setForm({
                name: c.name || '', code: c.code || '', symbol: c.symbol || '',
                symbol_after: Number(c.symbol_after) === 1, decimals: String(c.decimals != null ? c.decimals : 2),
                thousands_sep: c.thousands_sep != null ? c.thousands_sep : ',',
                decimal_sep: c.decimal_sep != null ? c.decimal_sep : '.',
                exchange_rate: String(c.exchange_rate || '1.000000'),
                is_primary: Number(c.is_primary) === 1,
                position: String(c.position || 0), status: c.status || 'active'
            });
            setEditId(Number(c.id));
            setModal(true);
        }
        function save() {
            setSaving(true);
            var body = {
                name: form.name, code: form.code, symbol: form.symbol,
                symbol_after: form.symbol_after ? 1 : 0,
                decimals: parseInt(form.decimals) || 2,
                thousands_sep: form.thousands_sep,
                decimal_sep: form.decimal_sep,
                exchange_rate: parseFloat(form.exchange_rate) || 1.0,
                is_primary: form.is_primary ? 1 : 0,
                position: parseInt(form.position) || 0,
                status: form.status
            };
            var url = editId ? '/currencies/' + editId : '/currencies';
            var method = editId ? 'PUT' : 'POST';
            apiFetch(url, { method:method, body:body }).then(function(d) {
                setSaving(false); setModal(false); refresh();
            }).catch(function(err){
                setSaving(false);
                alert(err.message || 'Save failed');
            });
        }
        function deleteCurrency(c) {
            if (Number(c.is_primary)) { alert('Cannot delete the primary currency.'); return; }
            if (!confirm('Delete currency "' + c.name + '"?')) return;
            apiFetch('/currencies/' + c.id, { method:'DELETE' }).then(refresh);
        }
        function toggleStatus(c) {
            if (Number(c.is_primary) && c.status === 'active') { alert('Cannot disable the primary currency.'); return; }
            var newStatus = c.status === 'active' ? 'disabled' : 'active';
            apiFetch('/currencies/' + c.id, { method:'PUT', body:{ status:newStatus } }).then(refresh).catch(function(err){ alert(err.message); });
        }
        function uf(k,v) { setForm(function(p){ return Object.assign({},p,{[k]:v}); }); }

        /* ── Format preview ── */
        var previewAmount = 1234.56;
        var prev = (function() {
            var dec = parseInt(form.decimals) || 2;
            var parts = previewAmount.toFixed(dec).split('.');
            var intPart = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, form.thousands_sep || ',');
            var result = intPart + (form.decimal_sep || '.') + (parts[1] || '');
            return form.symbol_after ? result + (form.symbol || '') : (form.symbol || '') + result;
        })();

        /* ── Modal ── */
        var modalEl = modal && h('div', { style:{ position:'fixed', inset:0, zIndex:9999, display:'flex', alignItems:'center', justifyContent:'center' } },
            h('div', { style:{ position:'absolute', inset:0, background:'rgba(0,0,0,.45)' }, onClick:function(){ if(!saving) setModal(false); } }),
            h('div', { style:{ position:'relative', background:cardBg, borderRadius:14, border:'1px solid '+borderColor, width:580, maxHeight:'90vh', overflow:'auto', padding:0, boxShadow:'0 20px 60px rgba(0,0,0,.3)' } },
                /* Header */
                h('div', { style:{ display:'flex', justifyContent:'space-between', alignItems:'center', padding:'16px 24px', borderBottom:'1px solid '+borderColor } },
                    h('h3', { style:{ fontSize:16, fontWeight:600 } }, editId ? 'Edit Currency' : 'Add Currency'),
                    h('button', { onClick:function(){ setModal(false); }, style:{ background:'transparent', border:'none', fontSize:20, cursor:'pointer', color:mutedColor, lineHeight:1 } }, '\u00D7')
                ),
                /* Body — two column grid */
                h('div', { style:{ padding:24, display:'grid', gridTemplateColumns:'1fr 1fr', gap:'16px 24px' } },
                    /* Name */
                    h('div', null,
                        h('label', { style:{ fontSize:13, fontWeight:500, display:'block', marginBottom:5, color:textColor } }, 'Name *'),
                        h('input', { type:'text', defaultValue:form.name, onBlur:function(e){ uf('name',e.target.value); }, placeholder:'e.g. US Dollars', style:fs })
                    ),
                    /* Code */
                    h('div', null,
                        h('label', { style:{ fontSize:13, fontWeight:500, display:'block', marginBottom:5, color:textColor } }, 'Code *'),
                        editId
                            ? h('div', { style:Object.assign({}, fs, { background:dark?'#1a1a1a':'#F5F5F5', cursor:'not-allowed', color:mutedColor }) }, form.code)
                            : h('input', { type:'text', defaultValue:form.code, onBlur:function(e){ uf('code',e.target.value.toUpperCase()); }, placeholder:'USD', maxLength:3, style:fs }),
                        h('div', { style:{ fontSize:11, color:mutedColor, marginTop:3 } }, '3-letter ISO 4217 code')
                    ),
                    /* Rate */
                    h('div', null,
                        h('label', { style:{ fontSize:13, fontWeight:500, display:'block', marginBottom:5, color:textColor } }, 'Exchange Rate *'),
                        h('input', { type:'text', defaultValue:form.exchange_rate,
                            disabled:form.is_primary,
                            onBlur:function(e){ uf('exchange_rate',e.target.value); },
                            style:Object.assign({}, fs, form.is_primary ? { background:dark?'#1a1a1a':'#F5F5F5', cursor:'not-allowed', color:mutedColor } : {}) }),
                        h('div', { style:{ fontSize:11, color:mutedColor, marginTop:3 } }, form.is_primary ? 'Primary currency is always 1.000000' : 'Relative to the primary currency')
                    ),
                    /* Sign */
                    h('div', null,
                        h('label', { style:{ fontSize:13, fontWeight:500, display:'block', marginBottom:5, color:textColor } }, 'Symbol'),
                        h('input', { type:'text', defaultValue:form.symbol, onBlur:function(e){ uf('symbol',e.target.value); }, placeholder:'$', maxLength:8, style:Object.assign({}, fs, { width:80 }) })
                    ),
                    /* Primary checkbox */
                    h('div', null,
                        h('label', { style:{ display:'flex', alignItems:'center', gap:8, cursor:'pointer', fontSize:13, fontWeight:500, color:textColor, marginTop:20 } },
                            h('div', { onClick:function(){ uf('is_primary', !form.is_primary); if(!form.is_primary) uf('exchange_rate','1.000000'); },
                                style:{ width:36, height:20, borderRadius:10, background:form.is_primary ? TC.accent : (dark?'#333':'#D4D4D8'),
                                    position:'relative', cursor:'pointer', transition:'background .2s', flexShrink:0 } },
                                h('div', { style:{ position:'absolute', top:2, left:form.is_primary ? 18 : 2, width:16, height:16, borderRadius:8,
                                    background:'#fff', transition:'left .2s', boxShadow:'0 1px 3px rgba(0,0,0,.2)' } })
                            ),
                            'Primary currency'
                        ),
                        h('div', { style:{ fontSize:11, color:mutedColor, marginTop:3, paddingLeft:44 } }, 'All other rates are relative to this currency')
                    ),
                    /* After sum */
                    h('div', null,
                        h('label', { style:{ display:'flex', alignItems:'center', gap:8, cursor:'pointer', fontSize:13, fontWeight:500, color:textColor, marginTop:20 } },
                            h('div', { onClick:function(){ uf('symbol_after', !form.symbol_after); },
                                style:{ width:36, height:20, borderRadius:10, background:form.symbol_after ? TC.accent : (dark?'#333':'#D4D4D8'),
                                    position:'relative', cursor:'pointer', transition:'background .2s', flexShrink:0 } },
                                h('div', { style:{ position:'absolute', top:2, left:form.symbol_after ? 18 : 2, width:16, height:16, borderRadius:8,
                                    background:'#fff', transition:'left .2s', boxShadow:'0 1px 3px rgba(0,0,0,.2)' } })
                            ),
                            'Symbol after amount'
                        ),
                        h('div', { style:{ fontSize:11, color:mutedColor, marginTop:3, paddingLeft:44 } }, 'e.g. "100 \u20AC" instead of "\u20AC100"')
                    ),
                    /* Thousands separator */
                    h('div', null,
                        h('label', { style:{ fontSize:13, fontWeight:500, display:'block', marginBottom:5, color:textColor } }, 'Thousands Separator'),
                        h('input', { type:'text', defaultValue:form.thousands_sep, onBlur:function(e){ uf('thousands_sep',e.target.value); }, maxLength:4, style:Object.assign({}, fs, { width:80 }) }),
                        h('div', { style:{ fontSize:11, color:mutedColor, marginTop:3 } }, 'e.g. comma, period, or space')
                    ),
                    /* Decimal separator */
                    h('div', null,
                        h('label', { style:{ fontSize:13, fontWeight:500, display:'block', marginBottom:5, color:textColor } }, 'Decimal Separator'),
                        h('input', { type:'text', defaultValue:form.decimal_sep, onBlur:function(e){ uf('decimal_sep',e.target.value); }, maxLength:4, style:Object.assign({}, fs, { width:80 }) }),
                        h('div', { style:{ fontSize:11, color:mutedColor, marginTop:3 } }, 'e.g. period or comma')
                    ),
                    /* Decimals */
                    h('div', null,
                        h('label', { style:{ fontSize:13, fontWeight:500, display:'block', marginBottom:5, color:textColor } }, 'Decimal Places'),
                        h('input', { type:'number', defaultValue:form.decimals, min:0, max:6, onBlur:function(e){ uf('decimals',e.target.value); }, style:Object.assign({}, fs, { width:80 }) }),
                        h('div', { style:{ fontSize:11, color:mutedColor, marginTop:3 } }, 'Number of digits after the decimal')
                    ),
                    /* Position */
                    h('div', null,
                        h('label', { style:{ fontSize:13, fontWeight:500, display:'block', marginBottom:5, color:textColor } }, 'Position'),
                        h('input', { type:'number', defaultValue:form.position, onBlur:function(e){ uf('position',e.target.value); }, style:Object.assign({}, fs, { width:80 }) })
                    ),
                    /* Preview — full width */
                    h('div', { style:{ gridColumn:'1 / -1', padding:'12px 16px', borderRadius:8, background:dark?'rgba(37,99,235,0.06)':'rgba(37,99,235,0.04)', border:'1px solid '+(dark?'rgba(37,99,235,0.15)':'rgba(37,99,235,0.12)') } },
                        h('div', { style:{ fontSize:12, fontWeight:600, color:TC.accent, marginBottom:4 } }, 'Format Preview'),
                        h('div', { style:{ fontSize:18, fontWeight:600, color:textColor } }, prev)
                    )
                ),
                /* Footer */
                h('div', { style:{ display:'flex', justifyContent:'flex-end', gap:S.sm, padding:'16px 24px', borderTop:'1px solid '+borderColor } },
                    h(Btn, { onClick:function(){ setModal(false); }, mutedColor:mutedColor, borderColor:borderColor }, 'Cancel'),
                    h(Btn, { variant:'primary', onClick:save, disabled:saving || !form.name || (!editId && !form.code), mutedColor:mutedColor, borderColor:borderColor }, saving ? 'Saving\u2026' : (editId ? 'Save' : 'Create'))
                )
            )
        );

        /* ── Main list ── */
        return h('main', { style:{ flex:1, overflow:'auto', padding:S.xxl } },
            modalEl,
            /* Header */
            h('div', { style:{ display:'flex', justifyContent:'space-between', alignItems:'center', marginBottom:S.xl } },
                h('div', { style:{ display:'flex', alignItems:'center', gap:S.md } },
                    backBtn || null,
                    h('h2', { style:{ fontSize:20, fontWeight:700, letterSpacing:'-.3px' } }, 'Currencies')
                ),
                h(Btn, { variant:'primary', onClick:openNew, mutedColor:mutedColor, borderColor:borderColor }, '+ Add Currency')
            ),

            loading && h(Spinner, { mutedColor:mutedColor }),

            !loading && list.length === 0 && h(Card, { cardBg:cardBg, borderColor:borderColor, dark:dark },
                h(EmptyState, { mutedColor:mutedColor, icon:'\uD83D\uDCB1', title:'No currencies configured', sub:'Add your first currency to start accepting payments.' })
            ),

            !loading && list.length > 0 && h(Card, { cardBg:cardBg, borderColor:borderColor, dark:dark, pad:0 },
                /* Table header */
                h('div', { style:{ display:'grid', gridTemplateColumns:'1fr 1fr 100px 120px 100px', padding:S.md+' '+S.xl+'px', borderBottom:'1px solid '+borderColor, background:tableHeaderBg, boxShadow:tableHeaderShadow } },
                    h('span', { style:{ fontSize:10, fontWeight:500, color:mutedColor, textTransform:'uppercase', letterSpacing:'0.5px' } }, 'Currency'),
                    h('span', { style:{ fontSize:10, fontWeight:500, color:mutedColor, textTransform:'uppercase', letterSpacing:'0.5px' } }, 'Details'),
                    h('span', { style:{ fontSize:10, fontWeight:500, color:mutedColor, textTransform:'uppercase', letterSpacing:'0.5px' } }, 'Rate'),
                    h('span', { style:{ fontSize:10, fontWeight:500, color:mutedColor, textTransform:'uppercase', letterSpacing:'0.5px' } }, 'Status'),
                    h('span', { style:{ fontSize:10, fontWeight:500, color:mutedColor, textTransform:'uppercase', letterSpacing:'0.5px', textAlign:'right' } }, 'Actions')
                ),
                /* Rows */
                list.map(function(c, idx) {
                    var isPrimary = Number(c.is_primary) === 1;
                    var isActive = c.status === 'active';
                    return h('div', { key:c.id, style:{
                        display:'grid', gridTemplateColumns:'1fr 1fr 100px 120px 100px', padding:'14px '+S.xl+'px',
                        borderBottom: idx < list.length - 1 ? '1px solid '+borderColor : 'none',
                        alignItems:'center', cursor:'pointer', transition:'background .12s'
                    },
                    onMouseEnter:function(e){ e.currentTarget.style.background = dark ? 'rgba(255,255,255,0.02)' : '#F9FAFB'; },
                    onMouseLeave:function(e){ e.currentTarget.style.background = 'transparent'; },
                    onClick:function(){ openEdit(c); }
                    },
                        /* Currency name + primary badge */
                        h('div', { style:{ display:'flex', alignItems:'center', gap:S.sm } },
                            h('span', { style:{ fontSize:13, fontWeight:500 } }, c.name),
                            isPrimary && h('span', { style:{ fontSize:10, fontWeight:500, color:TC.accent, background:dark?'rgba(37,99,235,0.10)':'rgba(37,99,235,0.06)', padding:'1px 7px', borderRadius:4 } }, 'PRIMARY')
                        ),
                        /* Code, sign */
                        h('span', { style:{ fontSize:12, color:mutedColor } }, c.code + ', Sign: ' + (c.symbol || '\u2014')),
                        /* Rate */
                        h('span', { style:{ fontSize:12, color: isPrimary ? mutedColor : textColor, fontWeight:400 } }, isPrimary ? '1.0 (base)' : parseFloat(c.exchange_rate).toFixed(5)),
                        /* Status */
                        h('span', null,
                            h(Badge, { status: isActive ? 'active' : 'inactive', dark:dark }, isActive ? 'Active' : 'Disabled')
                        ),
                        /* Actions */
                        h('div', { style:{ display:'flex', gap:S.sm, justifyContent:'flex-end' }, onClick:function(e){ e.stopPropagation(); } },
                            h('button', { onClick:function(){ toggleStatus(c); }, title: isActive ? 'Disable' : 'Enable',
                                style:{ padding:'4px 10px', borderRadius:6, fontSize:11, cursor:'pointer', border:'1px solid '+borderColor, background:'transparent', color:mutedColor, transition:'all .15s' } },
                                isActive ? 'Disable' : 'Enable'
                            ),
                            !isPrimary && h('button', { onClick:function(){ deleteCurrency(c); }, title:'Delete',
                                style:{ padding:'4px 10px', borderRadius:6, fontSize:11, cursor:'pointer', border:'1px solid '+(dark?'rgba(220,38,38,0.2)':'rgba(220,38,38,0.15)'), background:'transparent', color:TC.error, transition:'all .15s' } },
                                'Delete'
                            )
                        )
                    );
                })
            ),

            /* Info box */
            !loading && list.length > 0 && h('div', { style:{ marginTop:S.lg, padding:S.lg, borderRadius:10, background:dark?'rgba(217,164,65,0.06)':'rgba(217,164,65,0.04)', border:'1px solid '+(dark?'rgba(217,164,65,0.15)':'rgba(217,164,65,0.12)') } },
                h('div', { style:{ fontSize:13, fontWeight:600, color:TC.amber, marginBottom:4 } }, 'Important'),
                h('div', { style:{ fontSize:12, color:mutedColor, lineHeight:'1.5' } }, 'When you hide, disable, or delete a currency, make sure to update the Frontend default currency setting in Settings \u2192 Appearance.')
            )
        );
    }

    /* ── Order Statuses page ──────────────────────────────────────── */
    function OrderStatusesPage({ dark, cardBg, textColor, mutedColor, borderColor, tableHeaderBg, backBtn }) {
        var _tab      = useState('order');   var tab      = _tab[0];      var setTab      = _tab[1];
        var _list     = useState([]);        var list     = _list[0];     var setList     = _list[1];
        var _loading  = useState(true);      var loading  = _loading[0];  var setLoading  = _loading[1];
        var _modal    = useState(false);     var modal    = _modal[0];    var setModal    = _modal[1];
        var _editId   = useState(null);      var editId   = _editId[0];   var setEditId   = _editId[1];
        var _form     = useState({});        var form     = _form[0];     var setForm     = _form[1];
        var _saving   = useState(false);     var saving   = _saving[0];   var setSaving   = _saving[1];

        var fs = fieldSty(dark, borderColor);
        var COLOR_PRESETS = ['#D9A441','#1F6AE1','#71797E','#1F8A70','#C84646','#9333EA','#0891B2','#EA580C','#4F46E5','#059669'];
        var STOCK_OPTIONS = [{ value:'none', label:'None \u2014 no inventory change' },{ value:'restore', label:'Restore \u2014 return stock to inventory' }];
        var TAB_LABELS = { order:'Order Statuses', fulfillment:'Fulfillment Statuses', gift_card:'Gift Card Statuses' };

        function refresh() {
            setLoading(true);
            apiFetch('/order-statuses?type=' + tab).then(function(d) { setList(Array.isArray(d)?d:[]); setLoading(false); }).catch(function(){ setLoading(false); });
        }
        useEffect(refresh, [tab]);

        function openNew() {
            setForm({ name:'', handle:'', color:'#71797E', stock_action:'none', notify_customer:false, position:'0', description:'' });
            setEditId(null);
            setModal(true);
        }
        function openEdit(s) {
            setForm({
                name: s.name || '', handle: s.handle || '', color: s.color || '#71797E',
                stock_action: s.stock_action || 'none', notify_customer: Number(s.notify_customer) === 1,
                position: String(s.position || 0), description: s.description || ''
            });
            setEditId(Number(s.id));
            setModal(true);
        }
        function save() {
            setSaving(true);
            var body = {
                name: form.name, handle: form.handle, color: form.color, type: tab,
                stock_action: form.stock_action, notify_customer: form.notify_customer ? 1 : 0,
                position: parseInt(form.position) || 0, description: form.description || ''
            };
            var url = editId ? '/order-statuses/' + editId : '/order-statuses';
            var method = editId ? 'PUT' : 'POST';
            apiFetch(url, { method:method, body:body }).then(function() {
                setSaving(false); setModal(false); refresh();
            }).catch(function(err){ setSaving(false); alert(err.message || 'Save failed'); });
        }
        function deleteStatus(s) {
            if (Number(s.is_default)) { alert('Cannot delete the default status.'); return; }
            if (!confirm('Delete status "' + (s.name || s.handle) + '"?')) return;
            apiFetch('/order-statuses/' + s.id, { method:'DELETE' }).then(refresh).catch(function(err){ alert(err.message); });
        }
        function uf(k,v) { setForm(function(p){ return Object.assign({},p,{[k]:v}); }); }

        function onNameBlur(e) {
            uf('name', e.target.value);
            if (!editId && !form.handle) {
                uf('handle', e.target.value.toLowerCase().replace(/[^a-z0-9]+/g,'_').replace(/^_|_$/g,''));
            }
        }

        /* ── Tab style ── */
        var tabSty = function(t) {
            var active = tab === t;
            return { padding:'10px 20px', cursor:'pointer', fontSize:13, fontWeight:active?600:400,
                color:active?TC.accent:mutedColor, borderBottom:active?'2px solid '+TC.accent:'2px solid transparent', transition:'all .15s' };
        };

        /* ── Show stock/notify fields only for order type ── */
        var isOrderTab = tab === 'order';

        /* ── Modal ── */
        var modalEl = modal && h('div', { style:{ position:'fixed', inset:0, zIndex:9999, display:'flex', alignItems:'center', justifyContent:'center' } },
            h('div', { style:{ position:'absolute', inset:0, background:'rgba(0,0,0,.45)' }, onClick:function(){ if(!saving) setModal(false); } }),
            h('div', { style:{ position:'relative', background:cardBg, borderRadius:14, border:'1px solid '+borderColor, width:520, maxHeight:'90vh', overflow:'auto', boxShadow:'0 20px 60px rgba(0,0,0,.3)' } },
                h('div', { style:{ display:'flex', justifyContent:'space-between', alignItems:'center', padding:'16px 24px', borderBottom:'1px solid '+borderColor } },
                    h('h3', { style:{ fontSize:16, fontWeight:600 } }, editId ? 'Edit Status' : 'Add ' + TAB_LABELS[tab].replace('Statuses','Status')),
                    h('button', { onClick:function(){ setModal(false); }, style:{ background:'transparent', border:'none', fontSize:20, cursor:'pointer', color:mutedColor, lineHeight:1 } }, '\u00D7')
                ),
                h('div', { style:{ padding:24, display:'grid', gridTemplateColumns:'1fr 1fr', gap:'16px 24px' } },
                    /* Name */
                    h('div', null,
                        h('label', { style:{ fontSize:13, fontWeight:500, display:'block', marginBottom:5, color:textColor } }, 'Name *'),
                        h('input', { type:'text', defaultValue:form.name, onBlur:onNameBlur, placeholder:'e.g. Awaiting Shipment', style:fs })
                    ),
                    /* Handle */
                    h('div', null,
                        h('label', { style:{ fontSize:13, fontWeight:500, display:'block', marginBottom:5, color:textColor } }, 'Handle'),
                        editId
                            ? h('div', { style:Object.assign({}, fs, { background:dark?'#1a1a1a':'#F5F5F5', cursor:'not-allowed', color:mutedColor }) }, form.handle)
                            : h('input', { type:'text', defaultValue:form.handle, onBlur:function(e){ uf('handle',e.target.value.toLowerCase().replace(/[^a-z0-9_]/g,'')); }, placeholder:'auto_from_name', style:fs }),
                        h('div', { style:{ fontSize:11, color:mutedColor, marginTop:3 } }, 'Internal identifier. Cannot be changed after creation.')
                    ),
                    /* Color */
                    h('div', { style:{ gridColumn:'1 / -1' } },
                        h('label', { style:{ fontSize:13, fontWeight:500, display:'block', marginBottom:5, color:textColor } }, 'Color'),
                        h('div', { style:{ display:'flex', alignItems:'center', gap:S.sm, flexWrap:'wrap' } },
                            COLOR_PRESETS.map(function(c) {
                                return h('div', { key:c, onClick:function(){ uf('color',c); },
                                    style:{ width:28, height:28, borderRadius:7, background:c, cursor:'pointer',
                                        border: form.color === c ? '2px solid '+textColor : '2px solid transparent',
                                        transition:'border .15s' } });
                            }),
                            h('input', { type:'color', value:form.color || '#71797E', onChange:function(e){ uf('color', e.target.value); },
                                style:{ width:28, height:28, padding:0, border:'none', cursor:'pointer', borderRadius:7 } })
                        )
                    ),
                    /* Stock Action — order statuses only */
                    isOrderTab && h('div', { style:{ gridColumn:'1 / -1' } },
                        h('label', { style:{ fontSize:13, fontWeight:500, display:'block', marginBottom:5, color:textColor } }, 'Inventory'),
                        h('select', { value:form.stock_action, onChange:function(e){ uf('stock_action', e.target.value); }, style:Object.assign({}, fs, { cursor:'pointer' }) },
                            STOCK_OPTIONS.map(function(o) { return h('option', { key:o.value, value:o.value }, o.label); })
                        ),
                        h('div', { style:{ fontSize:11, color:mutedColor, marginTop:3 } }, 'Stock is deducted automatically when payment is taken. Choose Restore to return an order\u2019s items to inventory when it enters this status (e.g. Cancelled or Refunded).')
                    ),
                    /* Notify Customer — order statuses only */
                    isOrderTab && h('div', null,
                        h('label', { style:{ display:'flex', alignItems:'center', gap:8, cursor:'pointer', fontSize:13, fontWeight:500, color:textColor, marginTop:4 } },
                            h('div', { onClick:function(){ uf('notify_customer', !form.notify_customer); },
                                style:{ width:36, height:20, borderRadius:10, background:form.notify_customer ? TC.accent : (dark?'#333':'#D4D4D8'),
                                    position:'relative', cursor:'pointer', transition:'background .2s', flexShrink:0 } },
                                h('div', { style:{ position:'absolute', top:2, left:form.notify_customer ? 18 : 2, width:16, height:16, borderRadius:8,
                                    background:'#fff', transition:'left .2s', boxShadow:'0 1px 3px rgba(0,0,0,.2)' } })
                            ),
                            'Notify customer'
                        ),
                        h('div', { style:{ fontSize:11, color:mutedColor, marginTop:3, paddingLeft:44 } }, 'Send email when order reaches this status')
                    ),
                    /* Position */
                    h('div', null,
                        h('label', { style:{ fontSize:13, fontWeight:500, display:'block', marginBottom:5, color:textColor } }, 'Position'),
                        h('input', { type:'number', defaultValue:form.position, onBlur:function(e){ uf('position', e.target.value); }, style:Object.assign({}, fs, { width:80 }) })
                    )
                ),
                h('div', { style:{ display:'flex', justifyContent:'flex-end', gap:S.sm, padding:'16px 24px', borderTop:'1px solid '+borderColor } },
                    h(Btn, { onClick:function(){ setModal(false); }, mutedColor:mutedColor, borderColor:borderColor }, 'Cancel'),
                    h(Btn, { variant:'primary', onClick:save, disabled:saving || !form.name || (!editId && !form.handle), mutedColor:mutedColor, borderColor:borderColor },
                        saving ? 'Saving\u2026' : (editId ? 'Save' : 'Create'))
                )
            )
        );

        /* ── Main page ── */
        return h('main', { style:{ flex:1, overflow:'auto', padding:S.xxl } },
            modalEl,
            /* Header */
            h('div', { style:{ display:'flex', justifyContent:'space-between', alignItems:'center', marginBottom:S.xl } },
                h('div', { style:{ display:'flex', alignItems:'center', gap:S.md } },
                    backBtn || null,
                    h('h2', { style:{ fontSize:20, fontWeight:700, letterSpacing:'-.3px' } }, TAB_LABELS[tab] || 'Order Statuses')
                ),
                h(Btn, { variant:'primary', onClick:openNew, mutedColor:mutedColor, borderColor:borderColor }, '+ Add Status')
            ),

            /* Tab bar */
            h(Card, { cardBg:cardBg, borderColor:borderColor, dark:dark, pad:0 },
                h('div', { style:{ display:'flex', borderBottom:'1px solid '+borderColor } },
                    h('div', { style:tabSty('order'), onClick:function(){ setTab('order'); } }, 'Order Statuses'),
                    h('div', { style:tabSty('fulfillment'), onClick:function(){ setTab('fulfillment'); } }, 'Fulfillment Statuses'),
                    h('div', { style:tabSty('gift_card'), onClick:function(){ setTab('gift_card'); } }, 'Gift Card Statuses')
                ),

                h('div', { style:{ padding:0 } },
                    loading && h('div', { style:{ padding:40, textAlign:'center' } }, h(Spinner, { mutedColor:mutedColor })),

                    !loading && list.length === 0 && h('div', { style:{ padding:40 } },
                        h(EmptyState, { mutedColor:mutedColor, icon:'\uD83D\uDCCB', title:'No statuses', sub:'Add your first ' + TAB_LABELS[tab].toLowerCase().replace('statuses','status') + '.' })
                    ),

                    !loading && list.length > 0 && list.map(function(s, idx) {
                        var isDef = Number(s.is_default) === 1;
                        return h('div', { key:s.id, onClick:function(){ openEdit(s); }, style:{
                            display:'flex', alignItems:'center', justifyContent:'space-between', padding:'14px '+S.xl+'px',
                            borderBottom: idx < list.length - 1 ? '1px solid '+borderColor : 'none',
                            cursor:'pointer', transition:'background .12s'
                        },
                        onMouseEnter:function(e){ e.currentTarget.style.background = dark?'rgba(255,255,255,0.02)':'#F9FAFB'; },
                        onMouseLeave:function(e){ e.currentTarget.style.background = 'transparent'; }
                        },
                            h('div', { style:{ display:'flex', alignItems:'center', gap:S.md } },
                                h('div', { style:{ width:12, height:12, borderRadius:'50%', background:s.color || '#71797E', flexShrink:0 } }),
                                h('span', { style:{ fontSize:13, fontWeight:500 } }, s.name || s.handle),
                                isDef && h('span', { style:{ fontSize:10, fontWeight:500, color:TC.accent, background:dark?'rgba(37,99,235,0.10)':'rgba(37,99,235,0.06)', padding:'1px 7px', borderRadius:4, marginLeft:6 } }, 'DEFAULT'),
                                isOrderTab && Number(s.notify_customer) === 1 && h('span', { style:{ fontSize:10, color:mutedColor, marginLeft:6 } }, '\uD83D\uDD14 Notifies'),
                                isOrderTab && s.stock_action === 'restore' && h('span', { style:{ fontSize:10, color:TC.success, marginLeft:6 } },
                                    '\u2191 Restores stock'
                                )
                            ),
                            h('div', { style:{ display:'flex', gap:S.sm }, onClick:function(e){ e.stopPropagation(); } },
                                !isDef && h('button', { onClick:function(){ deleteStatus(s); }, title:'Delete',
                                    style:{ padding:'4px 10px', borderRadius:6, fontSize:11, cursor:'pointer',
                                        border:'1px solid '+(dark?'rgba(220,38,38,0.2)':'rgba(220,38,38,0.15)'), background:'transparent', color:TC.error } },
                                    'Delete')
                            )
                        );
                    })
                )
            )
        );
    }

    /* ── Customer Fields page ──────────────────────────────────────── */
    function CustomerFieldsPage({ dark, cardBg, textColor, mutedColor, borderColor, tableHeaderBg, backBtn }) {
        var _sections  = useState(null);   var sections  = _sections[0];  var setSections  = _sections[1];
        var _loading   = useState(true);   var loading   = _loading[0];   var setLoading   = _loading[1];
        var _saving    = useState(false);  var saving    = _saving[0];    var setSaving    = _saving[1];
        var _saved     = useState(false);  var saved     = _saved[0];     var setSaved     = _saved[1];
        var _collapsed = useState({});     var collapsed = _collapsed[0]; var setCollapsed = _collapsed[1];
        /* Local edits — keyed by field id */
        var _edits     = useState({});     var edits     = _edits[0];     var setEdits     = _edits[1];

        /* ── Add custom field (S127.C-1) — inline form to POST /customer-fields ── */
        var _addOpen   = useState(false);  var addOpen   = _addOpen[0];   var setAddOpen   = _addOpen[1];
        var _addBusy   = useState(false);  var addBusy   = _addBusy[0];   var setAddBusy   = _addBusy[1];
        var _addError  = useState('');     var addError  = _addError[0];  var setAddError  = _addError[1];
        var DEFAULT_ADD_FORM = { section:'contact', field_name:'', description:'', field_type:'input', is_visible:1, is_required:0 };
        var _addForm   = useState(DEFAULT_ADD_FORM);
        var addForm    = _addForm[0]; var setAddForm = _addForm[1];

        function setAF(key, val) {
            setAddForm(function(prev){ var n = Object.assign({}, prev); n[key] = val; return n; });
        }

        function resetAdd() {
            setAddForm(DEFAULT_ADD_FORM);
            setAddError('');
        }

        /* Best-effort key sanitizer — server still re-runs sanitize_key, this
           is just so the user sees what their typing will become. */
        function sanitizeKey(s) {
            return String(s || '').toLowerCase().replace(/[^a-z0-9_]+/g, '_').replace(/^_+|_+$/g, '');
        }

        function submitNew() {
            var fname = sanitizeKey(addForm.field_name);
            var desc  = String(addForm.description || '').trim();
            if (!fname || !desc) {
                setAddError('Field name and label are required.');
                return;
            }
            setAddBusy(true);
            setAddError('');
            apiFetch('/customer-fields', {
                method:'POST',
                body:{
                    section:     addForm.section,
                    field_name:  fname,
                    field_type:  addForm.field_type || 'input',
                    description: desc,
                    is_visible:  addForm.is_visible ? 1 : 0,
                    is_required: addForm.is_required ? 1 : 0
                }
            }).then(function() {
                setAddBusy(false);
                setAddOpen(false);
                resetAdd();
                refresh();
            }).catch(function(err) {
                setAddBusy(false);
                setAddError((err && err.message) ? err.message : 'Failed to create field.');
            });
        }

        var TYPE_LABELS = { input:'Input field', phone:'Phone', email:'E-mail', country:'Country \u203A\u203A', state:'State/Province \u203A\u203A', zipcode:'Zip/postal code', select:'Selectbox', checkbox:'Checkbox', textarea:'Textarea', date:'Date' };

        function refresh() {
            apiFetch('/customer-fields').then(function(d) {
                setSections(d);
                setEdits({});
                setLoading(false);
            }).catch(function(){ setLoading(false); });
        }
        useEffect(refresh, []);

        function getField(id) {
            if (!sections) return null;
            var keys = Object.keys(sections);
            for (var i = 0; i < keys.length; i++) {
                var fields = sections[keys[i]].fields || [];
                for (var j = 0; j < fields.length; j++) {
                    if (Number(fields[j].id) === Number(id)) return fields[j];
                }
            }
            return null;
        }

        function getVal(id, key) {
            if (edits[id] && edits[id][key] !== undefined) return edits[id][key];
            var f = getField(id);
            return f ? f[key] : '';
        }

        function setVal(id, key, val) {
            setEdits(function(prev) {
                var e = Object.assign({}, prev[id] || {}, { id:id, [key]:val });
                var n = {}; for (var k in prev) n[k] = prev[k];
                n[id] = e;
                return n;
            });
            setSaved(false);
        }

        function handleSave() {
            /* Merge edits into flat array */
            var allFields = [];
            if (!sections) return;
            var keys = Object.keys(sections);
            keys.forEach(function(sec) {
                (sections[sec].fields || []).forEach(function(f) {
                    var e = edits[f.id] || {};
                    allFields.push({
                        id: Number(f.id),
                        position: e.position !== undefined ? Number(e.position) : Number(f.position),
                        is_visible: e.is_visible !== undefined ? (e.is_visible ? 1 : 0) : Number(f.is_visible),
                        is_required: e.is_required !== undefined ? (e.is_required ? 1 : 0) : Number(f.is_required),
                    });
                });
            });
            setSaving(true);
            setSaved(false);
            apiFetch('/customer-fields', { method:'PUT', body:{ fields:allFields } }).then(function() {
                setSaving(false);
                setSaved(true);
                setTimeout(function(){ setSaved(false); }, 3000);
                refresh();
            }).catch(function(){ setSaving(false); });
        }

        function deleteField(f) {
            if (Number(f.is_system)) { alert('System fields cannot be deleted.'); return; }
            if (!confirm('Delete custom field "' + f.description + '"?')) return;
            apiFetch('/customer-fields/' + f.id, { method:'DELETE' }).then(refresh);
        }

        function toggleSection(sec) {
            setCollapsed(function(prev) {
                var n = {}; for (var k in prev) n[k] = prev[k];
                n[sec] = !n[sec];
                return n;
            });
        }

        /* ── Render ── */
        return h('main', { style:{ flex:1, overflow:'auto', padding:S.xxl } },
            /* Header */
            h('div', { style:{ display:'flex', justifyContent:'space-between', alignItems:'center', marginBottom:S.xl } },
                h('div', { style:{ display:'flex', alignItems:'center', gap:S.md } },
                    backBtn || null,
                    h('h2', { style:{ fontSize:20, fontWeight:700, letterSpacing:'-.3px' } }, 'Customer Fields')
                ),
                h('div', { style:{ display:'flex', alignItems:'center', gap:S.md } },
                    h(Btn, { variant:'secondary', onClick:function(){ if(addOpen){resetAdd();} setAddOpen(!addOpen); }, mutedColor:mutedColor, borderColor:borderColor },
                        addOpen ? 'Cancel' : '+ Add custom field'),
                    saved && h('span', { style:{ fontSize:12, color:TC.success, fontWeight:500 } }, '\u2713 Saved'),
                    h(Btn, { variant:'primary', onClick:handleSave, disabled:saving || loading, mutedColor:mutedColor, borderColor:borderColor },
                        saving ? 'Saving\u2026' : 'Save')
                )
            ),

            /* ── Inline Add Custom Field form (S127.C-1) ── */
            addOpen && h(Card, { cardBg:cardBg, borderColor:borderColor, dark:dark },
                h('h3', { style:{ fontSize:14, fontWeight:600, marginBottom:S.lg, color:textColor } }, 'Add custom field'),

                /* Row 1: Section + Field type */
                h('div', { style:{ display:'grid', gridTemplateColumns:'1fr 1fr', gap:S.lg, marginBottom:S.md } },
                    h('div', null,
                        h('label', { style:{ display:'block', fontSize:11, fontWeight:500, color:mutedColor, marginBottom:6, textTransform:'uppercase', letterSpacing:'0.5px' } }, 'Section'),
                        h('select', {
                            value:addForm.section,
                            onChange:function(e){ setAF('section', e.target.value); },
                            disabled:addBusy,
                            style:{ width:'100%', padding:'8px 12px', borderRadius:6, border:'1px solid '+borderColor, background:dark?'#1a1a1a':'#fff', color:textColor, fontSize:13 }
                        },
                            h('option', { value:'contact' },  'Contact information'),
                            h('option', { value:'shipping' }, 'Shipping address'),
                            h('option', { value:'billing' },  'Billing address')
                        )
                    ),
                    h('div', null,
                        h('label', { style:{ display:'block', fontSize:11, fontWeight:500, color:mutedColor, marginBottom:6, textTransform:'uppercase', letterSpacing:'0.5px' } }, 'Field type'),
                        h('select', {
                            value:addForm.field_type,
                            onChange:function(e){ setAF('field_type', e.target.value); },
                            disabled:addBusy,
                            style:{ width:'100%', padding:'8px 12px', borderRadius:6, border:'1px solid '+borderColor, background:dark?'#1a1a1a':'#fff', color:textColor, fontSize:13 }
                        },
                            Object.keys(TYPE_LABELS).map(function(t){
                                return h('option', { key:t, value:t }, TYPE_LABELS[t]);
                            })
                        )
                    )
                ),

                /* Row 2: Field name + Description (label) */
                h('div', { style:{ display:'grid', gridTemplateColumns:'1fr 1fr', gap:S.lg, marginBottom:S.md } },
                    h('div', null,
                        h('label', { style:{ display:'block', fontSize:11, fontWeight:500, color:mutedColor, marginBottom:6, textTransform:'uppercase', letterSpacing:'0.5px' } }, 'Field name'),
                        h('input', {
                            type:'text',
                            value:addForm.field_name,
                            onChange:function(e){ setAF('field_name', e.target.value); },
                            disabled:addBusy,
                            placeholder:'tax_id',
                            style:{ width:'100%', padding:'8px 12px', borderRadius:6, border:'1px solid '+borderColor, background:dark?'#1a1a1a':'#fff', color:textColor, fontSize:13, fontFamily:'monospace' }
                        }),
                        h('div', { style:{ fontSize:11, color:mutedColor, marginTop:4 } },
                            'Lowercase, underscores ok. Becomes: ',
                            h('code', { style:{ fontSize:11, color:textColor } }, sanitizeKey(addForm.field_name) || 'tax_id')
                        )
                    ),
                    h('div', null,
                        h('label', { style:{ display:'block', fontSize:11, fontWeight:500, color:mutedColor, marginBottom:6, textTransform:'uppercase', letterSpacing:'0.5px' } }, 'Label (shown to customer)'),
                        h('input', {
                            type:'text',
                            value:addForm.description,
                            onChange:function(e){ setAF('description', e.target.value); },
                            disabled:addBusy,
                            placeholder:'Tax ID',
                            style:{ width:'100%', padding:'8px 12px', borderRadius:6, border:'1px solid '+borderColor, background:dark?'#1a1a1a':'#fff', color:textColor, fontSize:13 }
                        })
                    )
                ),

                /* Row 3: Visibility + Required */
                h('div', { style:{ display:'flex', gap:S.xl, marginBottom:S.lg } },
                    h('label', { style:{ display:'flex', alignItems:'center', gap:S.sm, cursor:'pointer', fontSize:13, color:textColor } },
                        h('input', { type:'checkbox', checked:!!addForm.is_visible, disabled:addBusy,
                            onChange:function(e){ setAF('is_visible', e.target.checked ? 1 : 0); },
                            style:{ width:16, height:16, accentColor:TC.accent, cursor:'pointer' } }),
                        'Show on form'
                    ),
                    h('label', { style:{ display:'flex', alignItems:'center', gap:S.sm, cursor:'pointer', fontSize:13, color:textColor } },
                        h('input', { type:'checkbox', checked:!!addForm.is_required, disabled:addBusy,
                            onChange:function(e){ setAF('is_required', e.target.checked ? 1 : 0); },
                            style:{ width:16, height:16, accentColor:dark?'#555':'#333', cursor:'pointer' } }),
                        'Required'
                    )
                ),

                /* Error */
                addError && h('div', {
                    style:{ padding:'10px 14px', borderRadius:8, background:dark?'rgba(220,38,38,0.10)':'rgba(220,38,38,0.06)', border:'1px solid '+(dark?'rgba(220,38,38,0.25)':'rgba(220,38,38,0.20)'), color:TC.error, fontSize:13, marginBottom:S.lg }
                }, addError),

                /* Actions */
                h('div', { style:{ display:'flex', gap:S.md, justifyContent:'flex-end' } },
                    h(Btn, { variant:'ghost', onClick:function(){ setAddOpen(false); resetAdd(); }, mutedColor:mutedColor, borderColor:borderColor }, 'Cancel'),
                    h(Btn, { variant:'primary', onClick:submitNew, disabled:addBusy, mutedColor:mutedColor, borderColor:borderColor },
                        addBusy ? 'Adding\u2026' : 'Add field')
                )
            ),

            loading && h(Spinner, { mutedColor:mutedColor }),

            !loading && sections && h(Card, { cardBg:cardBg, borderColor:borderColor, dark:dark, pad:0 },
                /* Column headers */
                h('div', { style:{ display:'grid', gridTemplateColumns:'60px 1fr 160px 120px 40px', padding:S.md+' '+S.xl+'px', borderBottom:'1px solid '+borderColor, background:tableHeaderBg, boxShadow:tableHeaderShadow } },
                    h('span', { style:{ fontSize:10, fontWeight:500, color:mutedColor, textTransform:'uppercase', letterSpacing:'0.5px' } }, 'Pos.'),
                    h('span', { style:{ fontSize:10, fontWeight:500, color:mutedColor, textTransform:'uppercase', letterSpacing:'0.5px' } }, 'Description'),
                    h('span', { style:{ fontSize:10, fontWeight:500, color:mutedColor, textTransform:'uppercase', letterSpacing:'0.5px' } }, 'Type'),
                    h('span', { style:{ fontSize:10, fontWeight:500, color:mutedColor, textTransform:'uppercase', letterSpacing:'0.5px', textAlign:'center' } }, 'Show / Req'),
                    h('span', null)
                ),

                /* Sections */
                Object.keys(sections).map(function(sec) {
                    var s = sections[sec];
                    var isCollapsed = !!collapsed[sec];
                    return h('div', { key:sec },
                        /* Section header */
                        h('div', { onClick:function(){ toggleSection(sec); },
                            style:{ padding:'10px '+S.xl+'px', background:dark?'rgba(255,255,255,0.03)':'#F5F7FA', cursor:'pointer', display:'flex', alignItems:'center', gap:S.sm, borderBottom:'1px solid '+borderColor } },
                            h('span', { style:{ fontSize:12, color:mutedColor, transition:'transform .15s', transform:isCollapsed?'rotate(-90deg)':'rotate(0)' } }, '\u25BC'),
                            h('span', { style:{ fontSize:13, fontWeight:600, color:textColor } }, s.title)
                        ),
                        /* Fields */
                        !isCollapsed && (s.fields || []).map(function(f, idx) {
                            var fid = Number(f.id);
                            var vis = getVal(fid, 'is_visible');
                            var req = getVal(fid, 'is_required');
                            var isVis = vis === true || vis === 1 || vis === '1';
                            var isReq = req === true || req === 1 || req === '1';
                            return h('div', { key:fid, style:{
                                display:'grid', gridTemplateColumns:'60px 1fr 160px 120px 40px', padding:'10px '+S.xl+'px',
                                borderBottom:'1px solid '+borderColor, alignItems:'center', transition:'background .1s'
                            },
                            onMouseEnter:function(e){ e.currentTarget.style.background = dark?'rgba(255,255,255,0.02)':'#FAFBFC'; },
                            onMouseLeave:function(e){ e.currentTarget.style.background = 'transparent'; }
                            },
                                /* Position */
                                h('input', { type:'number', defaultValue:getVal(fid, 'position'),
                                    onBlur:function(e){ setVal(fid, 'position', e.target.value); },
                                    style:{ width:48, padding:'4px 6px', borderRadius:6, border:'1px solid '+borderColor, background:dark?'#1a1a1a':'#fff', color:textColor, fontSize:12, textAlign:'center' } }),
                                /* Description */
                                h('span', { style:{ fontSize:13, color:textColor } }, f.description),
                                /* Type */
                                h('span', { style:{ fontSize:12, color:mutedColor } }, TYPE_LABELS[f.field_type] || f.field_type),
                                /* Show / Required checkboxes */
                                h('div', { style:{ display:'flex', gap:S.lg, justifyContent:'center' } },
                                    h('input', { type:'checkbox', checked:isVis,
                                        onChange:function(){ setVal(fid, 'is_visible', !isVis); },
                                        title:'Show this field', style:{ width:16, height:16, accentColor:TC.accent, cursor:'pointer' } }),
                                    h('input', { type:'checkbox', checked:isReq,
                                        onChange:function(){ setVal(fid, 'is_required', !isReq); },
                                        title:'Make required', style:{ width:16, height:16, accentColor:dark?'#555':'#333', cursor:'pointer' } })
                                ),
                                /* Delete (non-system only) */
                                !Number(f.is_system) ? h('button', { onClick:function(){ deleteField(f); }, title:'Delete custom field',
                                    style:{ background:'transparent', border:'none', color:TC.error, cursor:'pointer', fontSize:14, padding:0 } }, '\u00D7')
                                : h('span', null)
                            );
                        })
                    );
                })
            ),

            /* Hint */
            !loading && h('div', { style:{ marginTop:S.lg, padding:S.lg, borderRadius:10, background:dark?'rgba(37,99,235,0.06)':'rgba(37,99,235,0.04)', border:'1px solid '+(dark?'rgba(37,99,235,0.15)':'rgba(37,99,235,0.12)') } },
                h('div', { style:{ fontSize:13, fontWeight:600, color:TC.accent, marginBottom:4 } }, 'How it works'),
                h('div', { style:{ fontSize:12, color:mutedColor, lineHeight:'1.6' } },
                    'These fields control what customers see on registration and checkout forms. ',
                    h('strong', null, 'Show'), ' makes the field visible. ',
                    h('strong', null, 'Req'), ' (dark checkbox) makes it required. ',
                    'System fields (like name, email, address) cannot be deleted but can be hidden. ',
                    'Changes take effect after clicking Save.'
                )
            )
        );
    }

    /* ── Account & License page ─────────────────────────────────────────
     *
     * Wired to /wp-json/titancart/v1/license in REST/License/LicenseController.
     * The page reflects three top-level states surfaced by the server:
     *
     *   - 'vendor'      → Vendor-mode bypass (titancart.io / TITANCART_VENDOR_MODE).
     *                     Single info card, no key entry.
     *   - 'licensed'    → Active marketplace key. Shows product + expiry +
     *                     activations + Refresh / Manage / Remove actions.
     *   - everything    → Subscription License entry card. POSTs the key to
     *     else            the /license endpoint — PluginPress decides which
     *                     keys are valid. (The beta access card was removed
     *                     with the beta scaffolding — KI-41, S416.)
     *
     * ⚠️ S405 (KI-40): this used to read "The trial auto-starts at plugin
     * activation (Installer::seed_trial_timestamp) so the customer is never
     * gated until trial + grace_days have elapsed." The trial is RETIRED —
     * Lite is the trial — and the seeder is gone. An unkeyed Pro install runs
     * on the Lite feature set indefinitely; it is never "gated" at all.
     * ────────────────────────────────────────────────────────────────── */
    function AccountPage({ dark, cardBg, textColor, mutedColor, borderColor, tableHeaderBg, backBtn }) {
        var _data    = useState(null);  var data    = _data[0];    var setData    = _data[1];
        var _loading = useState(true);  var loading = _loading[0]; var setLoading = _loading[1];
        var _subKey  = useState('');    var subKey  = _subKey[0];  var setSubKey  = _subKey[1];
        var _busy    = useState(null);  var busy    = _busy[0];    var setBusy    = _busy[1];   /* null | 'sub' | 'remove' | 'refresh' */
        var _result  = useState(null);  var result  = _result[0];  var setResult  = _result[1];

        function loadStatus() {
            return apiFetch('/license').then(function(d) {
                setData(d);
                setLoading(false);
            }).catch(function(err) {
                setLoading(false);
                setResult({ success:false, message: 'Could not load license status: ' + ((err && err.message) || 'unknown error') });
            });
        }

        useEffect(function() { loadStatus(); }, []);

        function activate(which) {
            var k = subKey.trim();
            if (!k) return;
            setBusy(which);
            setResult(null);
            apiFetch('/license', { method:'POST', body:{ key: k } })
                .then(function(d) {
                    setBusy(null);
                    if (d && d.state) setData(d.state);
                    if (d && d.success) {
                        setSubKey('');
                        setResult({ success:true, message: d.message || 'License activated.' });
                    } else {
                        setResult({ success:false, message: (d && d.message) || 'Activation failed.' });
                    }
                })
                .catch(function(err) {
                    setBusy(null);
                    var ed = err && err.data;
                    if (ed && ed.state) setData(ed.state);
                    var msg = (ed && (ed.message || ed.error)) || (err && err.message) || 'Activation failed.';
                    setResult({ success:false, message: msg });
                });
        }

        function deactivate() {
            if (!confirm('Remove this license from this site?\n\nYour store will keep running, but drops to the Lite feature set until a key is entered. You can re-enter a key at any time to restore Pro.')) return;
            setBusy('remove');
            setResult(null);
            apiFetch('/license', { method:'DELETE' })
                .then(function(d) {
                    setBusy(null);
                    if (d && d.state) setData(d.state);
                    setResult({ success: !!(d && d.success), message: (d && d.message) || 'License removed.' });
                })
                .catch(function(err) {
                    setBusy(null);
                    setResult({ success:false, message: (err && err.message) || 'Removal failed.' });
                });
        }

        function refreshStatus() {
            setBusy('refresh');
            setResult(null);
            apiFetch('/license/refresh', { method:'POST' })
                .then(function(d) {
                    setBusy(null);
                    if (d) setData(d);
                    setResult({ success:true, message: 'Status refreshed from the marketplace.' });
                })
                .catch(function(err) {
                    setBusy(null);
                    setResult({ success:false, message: (err && err.message) || 'Refresh failed.' });
                });
        }

        var fs = fieldSty(dark, borderColor);

        /* ── Loading state ── */
        if (loading || !data) {
            return h('main', { style:{ flex:1, overflow:'auto', padding:S.xxl } },
                h('div', { style:{ display:'flex', alignItems:'center', gap:S.md, marginBottom:S.xl } },
                    backBtn || null,
                    h('h2', { style:{ fontSize:20, fontWeight:700, letterSpacing:'-.3px' } }, 'Account & License')
                ),
                h(Spinner, { mutedColor: mutedColor })
            );
        }

        /* ── Compute display state from the payload ── */
        var state    = data.state || 'unlicensed';
        var lic      = data.license || {};

        /* ── S405 (KI-40): 'trial' / 'trial_grace' branches REMOVED. The trial
         *    is retired (Lite is the trial) and titancart_core_license_state() can no
         *    longer return either label, so both were dead. The `data.trial`
         *    sub-object is gone from GET /license for the same reason.
         *
         *    ⚠️ 'expired' and 'unlicensed' used to say "Storefront is offline".
         *    That was FALSE (S366 — no storefront gate has ever existed) and it
         *    mattered the moment the trial went away: an unkeyed Pro install
         *    used to sit in 'trial' and never see this text, but now lands in
         *    'unlicensed' and reads it on day one. Retiring the trial is what
         *    made the lie reachable, so correcting it belongs to this change. */
        var statusColor, statusLabel, statusDetail;
        if (state === 'vendor') {
            statusColor = TC.success;
            statusLabel = 'Vendor mode';
            statusDetail = 'Licensing bypassed on this site.';
        } else if (state === 'licensed') {
            statusColor = TC.success;
            statusLabel = 'Licensed';
            statusDetail = lic.expires_at ? ('Renews ' + lic.expires_at) : 'Active subscription';
        } else if (state === 'lite') {
            statusColor = TC.success;
            statusLabel = 'Lite';
            statusDetail = 'Free edition — no license key required.';
        } else if (state === 'expired') {
            statusColor = TC.amber;
            statusLabel = 'License expired';
            statusDetail = 'Running in Lite mode — renew to restore Pro features.';
        } else {
            statusColor = TC.amber;
            statusLabel = 'No license key';
            statusDetail = 'Running in Lite mode — enter a key to unlock Pro features.';
        }

        var planLabel = state === 'licensed'
            ? (lic.product_name || 'Subscription')
            : (state === 'vendor' ? 'Vendor' : 'Lite');

        /* ── Reusable entry-card builder (Beta + Subscription share this) ── */
        function buildEntryCard(opts) {
            /* opts: { kind, title, subtitle, badge, primary, disabled, buttonLabel, value, onChange } */
            var btnDisabled = opts.disabled || !!busy || !opts.value.trim();
            var btnLabel    = (busy === opts.kind) ? 'Activating\u2026' : opts.buttonLabel;
            return h('div', {
                style: {
                    padding: S.xl,
                    borderRadius: 12,
                    transition: 'all .15s',
                    border: '2px solid ' + (opts.primary ? TC.accent : borderColor),
                    background: opts.primary ? (dark?'rgba(37,99,235,0.06)':'rgba(37,99,235,0.03)') : 'transparent',
                    opacity: opts.disabled ? 0.55 : 1
                } },
                h('div', { style:{ display:'flex', alignItems:'center', justifyContent:'space-between', gap:S.sm, marginBottom:S.md } },
                    h('span', { style:{ fontSize:15, fontWeight:600, color:textColor } }, opts.title),
                    opts.badge && h('span', { style:{
                        fontSize:10, fontWeight:600, letterSpacing:'.4px', textTransform:'uppercase',
                        padding:'3px 8px', borderRadius:6,
                        background: opts.badge.bg, color: opts.badge.color
                    } }, opts.badge.label)
                ),
                h('div', { style:{ fontSize:13, color:mutedColor, lineHeight:'1.6', marginBottom:S.lg } }, opts.subtitle),
                h('label', { style:{ fontSize:13, fontWeight:500, display:'block', marginBottom:5, color:textColor } }, 'License key'),
                h('input', {
                    type: 'text',
                    value: opts.value,
                    onChange: function(e){ if (!opts.disabled) opts.onChange(e.target.value); },
                    placeholder: 'XXXX-XXXX-XXXX-XXXX',
                    disabled: opts.disabled || !!busy,
                    style: Object.assign({}, fs, { fontFamily:'monospace', textTransform:'uppercase' })
                }),
                h('div', { style:{ marginTop:S.lg } },
                    h(Btn, {
                        variant: opts.primary ? 'primary' : 'secondary',
                        onClick: opts.disabled ? null : function(){ activate(opts.kind); },
                        disabled: btnDisabled,
                        mutedColor: mutedColor,
                        borderColor: borderColor
                    }, btnLabel)
                )
            );
        }

        /* ── Active license panel (state === 'licensed') ── */
        function buildActivePanel() {
            var checkedAt = lic.checked_at ? new Date(lic.checked_at * 1000).toLocaleString() : 'Never';
            var activations = (lic.activations || 0) + (lic.max_activations ? (' / ' + lic.max_activations) : ' / unlimited');
            return h(Card, { cardBg:cardBg, borderColor:borderColor, dark:dark },
                h('h3', { style:{ fontSize:15, fontWeight:600, marginBottom:S.md, paddingBottom:S.sm, borderBottom:'1px solid '+borderColor, color:textColor } },
                    '\u2705 License active'
                ),
                h('div', { style:{ display:'grid', gridTemplateColumns:'1fr 1fr', gap:S.lg, marginBottom:S.lg } },
                    h('div', null,
                        h('div', { style:{ fontSize:11, color:mutedColor, textTransform:'uppercase', letterSpacing:'.5px', marginBottom:4 } }, 'Product'),
                        h('div', { style:{ fontSize:14, fontWeight:500, color:textColor } }, lic.product_name || 'TitanCart')
                    ),
                    h('div', null,
                        h('div', { style:{ fontSize:11, color:mutedColor, textTransform:'uppercase', letterSpacing:'.5px', marginBottom:4 } }, 'Renews'),
                        h('div', { style:{ fontSize:14, fontWeight:500, color:textColor } }, lic.expires_at || 'No expiry on file')
                    ),
                    h('div', null,
                        h('div', { style:{ fontSize:11, color:mutedColor, textTransform:'uppercase', letterSpacing:'.5px', marginBottom:4 } }, 'Activations'),
                        h('div', { style:{ fontSize:14, fontWeight:500, color:textColor } }, activations)
                    ),
                    h('div', null,
                        h('div', { style:{ fontSize:11, color:mutedColor, textTransform:'uppercase', letterSpacing:'.5px', marginBottom:4 } }, 'Last checked'),
                        h('div', { style:{ fontSize:14, fontWeight:500, color:textColor } }, checkedAt)
                    )
                ),
                h('div', { style:{ display:'flex', flexWrap:'wrap', gap:S.sm, alignItems:'center' } },
                    h(Btn, { variant:'secondary', onClick:refreshStatus, disabled:!!busy, mutedColor:mutedColor, borderColor:borderColor },
                        busy === 'refresh' ? 'Refreshing\u2026' : 'Refresh status'),
                    h('a', {
                        href: 'https://market.titancart.io/my-account/',
                        target: '_blank',
                        rel: 'noopener noreferrer',
                        style: { fontSize:13, color:TC.accent, textDecoration:'none', padding:'8px 12px' }
                    }, 'Manage subscription \u2197'),
                    h('div', { style:{ flex:1 } }),
                    h(Btn, { variant:'danger', onClick:deactivate, disabled:!!busy, mutedColor:mutedColor, borderColor:borderColor },
                        busy === 'remove' ? 'Removing\u2026' : 'Remove license')
                )
            );
        }

        /* ── Vendor info panel (state === 'vendor') ── */
        function buildVendorPanel() {
            return h(Card, { cardBg:cardBg, borderColor:borderColor, dark:dark },
                h('h3', { style:{ fontSize:15, fontWeight:600, marginBottom:S.sm, color:textColor } }, '\uD83D\uDD27 Vendor mode'),
                h('div', { style:{ fontSize:13, color:mutedColor, lineHeight:'1.6' } },
                    'TitanCart is running in vendor mode on this site. Licensing checks are bypassed because this domain is on the vendor allow-list (titancart.io / *.titancart.io / TITANCART_VENDOR_MODE constant). No license key is required.'
                )
            );
        }

        /* ── Entry panel: single Subscription card (beta scaffolding removed — KI-41, S416) ── */
        function buildEntryPanel() {
            var subCard = buildEntryCard({
                kind: 'sub',
                title: 'Subscription License',
                subtitle: 'Enter your TitanCart subscription key to activate. Subscriptions are billed annually and renew automatically — manage your subscription anytime from your TitanCart account at market.titancart.io.',
                primary: true,
                disabled: false,
                buttonLabel: 'Activate license',
                value: subKey,
                onChange: setSubKey,
                badge: null
            });

            return h(Card, { cardBg:cardBg, borderColor:borderColor, dark:dark },
                h('h3', { style:{ fontSize:15, fontWeight:600, marginBottom:S.md, paddingBottom:S.sm, borderBottom:'1px solid '+borderColor, color:textColor } },
                    'Activate TitanCart'),
                subCard
            );
        }

        /* ── Result toast ── */
        var resultBlock = result && h('div', { style:{
            marginTop:S.lg, padding:'12px 16px', borderRadius:8, fontSize:13, lineHeight:'1.5',
            background: result.success ? (dark?'rgba(22,163,74,0.08)':'rgba(22,163,74,0.05)') : (dark?'rgba(220,38,38,0.08)':'rgba(220,38,38,0.05)'),
            border: '1px solid ' + (result.success ? (dark?'rgba(22,163,74,0.2)':'rgba(22,163,74,0.15)') : (dark?'rgba(220,38,38,0.2)':'rgba(220,38,38,0.15)')),
            color: result.success ? TC.success : TC.error
        } }, (result.success ? '\u2705 ' : '\u274C ') + result.message);

        /* ── Main render ── */
        return h('main', { style:{ flex:1, overflow:'auto', padding:S.xxl } },

            /* Header */
            h('div', { style:{ display:'flex', alignItems:'center', gap:S.md, marginBottom:S.xl } },
                backBtn || null,
                h('h2', { style:{ fontSize:20, fontWeight:700, letterSpacing:'-.3px' } }, 'Account & License')
            ),

            /* Status row: License Status card + Version card */
            h('div', { style:{ display:'grid', gridTemplateColumns:'1fr 1fr', gap:S.lg, marginBottom:S.xl } },
                h(Card, { cardBg:cardBg, borderColor:borderColor, dark:dark },
                    h('div', { style:{ display:'flex', justifyContent:'space-between', alignItems:'flex-start' } },
                        h('div', null,
                            h('div', { style:{ fontSize:12, color:mutedColor, marginBottom:6 } }, 'License Status'),
                            h('div', { style:{ display:'flex', alignItems:'center', gap:S.sm } },
                                h('div', { style:{ width:10, height:10, borderRadius:'50%', background:statusColor } }),
                                h('span', { style:{ fontSize:18, fontWeight:700, color:textColor } }, statusLabel)
                            ),
                            h('div', { style:{ fontSize:12, color:mutedColor, marginTop:4 } }, statusDetail)
                        ),
                        h('div', { style:{ padding:'6px 12px', borderRadius:8, background:dark?'rgba(255,255,255,0.04)':'#F5F5F5', fontSize:12, color:mutedColor } }, 'Plan: ' + planLabel)
                    )
                ),
                h(Card, { cardBg:cardBg, borderColor:borderColor, dark:dark },
                    h('div', { style:{ fontSize:12, color:mutedColor, marginBottom:6 } }, 'TitanCart Version'),
                    h('div', { style:{ fontSize:18, fontWeight:700, color:textColor, marginBottom:4 } }, 'v' + (Config.version || '0.1.0')),
                    h('div', { style:{ fontSize:12, color:mutedColor } },
                        'Production release'
                    )
                )
            ),

            /* Main panel — vendor / active / entry */
            state === 'vendor'   ? buildVendorPanel()
            : state === 'licensed' ? buildActivePanel()
            : buildEntryPanel(),

            /* Result toast */
            resultBlock,

            /* Footer */
            h('div', { style:{ marginTop:S.xl, paddingTop:S.lg, borderTop:'1px solid '+borderColor, fontSize:12, color:mutedColor, display:'flex', justifyContent:'space-between' } },
                h('span', null, 'TitanCart v' + (Config.version || '0.1.0')),
                h('span', null, '\u00A9 ' + new Date().getFullYear() + ' TitanCart')
            )
        );
    }

    /* ── Site Backup page ─────────────────────────────────────────── */

    /* ════════════════════════════════
     * SOCIAL LOGIN PAGE
     * ════════════════════════════════ */

    /* ════════════════════════════════
     * SYSTEM LOGS PAGE
     * ════════════════════════════════ */
    function SystemLogsPage({ dark, cardBg, textColor, mutedColor, borderColor, tableHeaderBg, backBtn }) {
        var _logs     = useState([]);       var logs     = _logs[0];     var setLogs     = _logs[1];
        var _stats    = useState(null);     var stats    = _stats[0];    var setStats    = _stats[1];
        var _loading  = useState(true);     var loading  = _loading[0];  var setLoading  = _loading[1];
        var _page     = useState(1);        var page     = _page[0];    var setPage     = _page[1];
        var _total    = useState(0);        var total    = _total[0];   var setTotal    = _total[1];
        var _totalPg  = useState(0);        var totalPg  = _totalPg[0]; var setTotalPg  = _totalPg[1];
        var _perPage  = useState(TC_PER_PAGE);  var perPage  = _perPage[0]; var setPerPage  = _perPage[1];
        var _expanded = useState(null);     var expanded = _expanded[0]; var setExpanded = _expanded[1];
        var _cleaning = useState(false);    var cleaning = _cleaning[0]; var setCleaning = _cleaning[1];
        var _result   = useState(null);     var result   = _result[0];  var setResult   = _result[1];

        /* Filters */
        var _fLevel    = useState('');      var fLevel    = _fLevel[0];    var setFLevel    = _fLevel[1];
        var _fType     = useState('');      var fType     = _fType[0];     var setFType     = _fType[1];
        var _fAction   = useState('');      var fAction   = _fAction[0];   var setFAction   = _fAction[1];
        var _fSearch   = useState('');      var fSearch   = _fSearch[0];   var setFSearch   = _fSearch[1];
        var _fDateFrom = useState('');      var fDateFrom = _fDateFrom[0]; var setFDateFrom = _fDateFrom[1];
        var _fDateTo   = useState('');      var fDateTo   = _fDateTo[0];   var setFDateTo   = _fDateTo[1];

        var fs = fieldSty(dark, borderColor);
        var selSty = Object.assign({}, fs, { cursor:'pointer', fontSize:12 });

        var LEVELS  = ['debug','info','notice','warning','error','critical'];
        var TYPES   = ['orders','users','products','categories','database','requests','general','system'];

        var LEVEL_COLORS = {
            debug:    { bg: dark ? 'rgba(115,115,115,.12)' : '#F5F5F5',    color: mutedColor },
            info:     { bg: dark ? 'rgba(37,99,235,.10)'   : '#EFF6FF',    color: TC.accent },
            notice:   { bg: dark ? 'rgba(14,165,233,.10)'  : '#F0F9FF',    color: '#0EA5E9' },
            warning:  { bg: dark ? 'rgba(217,164,65,.10)'  : '#FFFBEB',    color: TC.amber },
            error:    { bg: dark ? 'rgba(220,38,38,.10)'   : '#FEF2F2',    color: TC.error },
            critical: { bg: dark ? 'rgba(147,51,234,.10)'  : '#F5F3FF',    color: '#9333EA' },
        };

        /* ── Fetch logs ── */
        function fetchLogs(pg) {
            setLoading(true);
            var params = ['page=' + (pg || page), 'per_page=' + perPage];
            if (fLevel)    params.push('level=' + fLevel);
            if (fType)     params.push('type=' + fType);
            if (fAction)   params.push('action=' + fAction);
            if (fSearch)   params.push('search=' + encodeURIComponent(fSearch));
            if (fDateFrom) params.push('date_from=' + fDateFrom);
            if (fDateTo)   params.push('date_to=' + fDateTo);

            apiFetch('/logs?' + params.join('&')).then(function(d) {
                setLogs(d.items || []);
                setTotal(d.total || 0);
                setTotalPg(d.total_pages || 0);
                setLoading(false);
            }).catch(function() { setLoading(false); });
        }

        function fetchStats() {
            apiFetch('/logs/stats').then(function(d) { setStats(d); });
        }

        useEffect(function() { fetchLogs(1); setPage(1); fetchStats(); }, [fLevel, fType, fAction, fDateFrom, fDateTo]);
        useEffect(function() { fetchLogs(page); }, [page, perPage]);

        /* Debounced search */
        var searchTimer = useRef(null);
        function onSearchChange(e) {
            var val = e.target.value;
            setFSearch(val);
            if (searchTimer.current) clearTimeout(searchTimer.current);
            searchTimer.current = setTimeout(function() { setPage(1); fetchLogs(1); }, 500);
        }

        /* ── Clean logs ── */
        function cleanOld() {
            var days = prompt('Delete log entries older than how many days?', '90');
            if (!days) return;
            setCleaning(true);
            setResult(null);
            apiFetch('/logs?mode=old&days=' + parseInt(days), { method:'DELETE' }).then(function(d) {
                setCleaning(false);
                setResult({ success:true, message:d.message || ('Deleted ' + d.deleted + ' entries.') });
                fetchLogs(1); setPage(1); fetchStats();
            }).catch(function(err) { setCleaning(false); setResult({ success:false, message:err.message || 'Failed.' }); });
        }
        function cleanAll() {
            if (!confirm('DELETE ALL log entries? This cannot be undone.')) return;
            setCleaning(true);
            setResult(null);
            apiFetch('/logs?mode=all', { method:'DELETE' }).then(function(d) {
                setCleaning(false);
                setResult({ success:true, message:d.message || 'All logs deleted.' });
                fetchLogs(1); setPage(1); fetchStats();
            }).catch(function(err) { setCleaning(false); setResult({ success:false, message:err.message || 'Failed.' }); });
        }
        function deleteSingle(id) {
            apiFetch('/logs/' + id, { method:'DELETE' }).then(function() {
                fetchLogs(page); fetchStats();
            });
        }

        function resetFilters() {
            setFLevel(''); setFType(''); setFAction(''); setFSearch(''); setFDateFrom(''); setFDateTo('');
        }

        var hasFilters = fLevel || fType || fAction || fSearch || fDateFrom || fDateTo;

        /* ── Level badge ── */
        function LevelBadge({ level }) {
            var lc = LEVEL_COLORS[level] || LEVEL_COLORS.info;
            return h('span', { style:{ fontSize:10, fontWeight:600, textTransform:'uppercase', letterSpacing:'.5px',
                padding:'2px 8px', borderRadius:4, background:lc.bg, color:lc.color, whiteSpace:'nowrap' } }, level);
        }

        /* ── Format timestamp ── */
        function fmtDate(dt) {
            if (!dt) return '—';
            try {
                var ts = dt.indexOf('T') > -1 || dt.indexOf('Z') > -1 ? dt : dt + 'Z';
                return tcFmtDateTime(ts);
            } catch(e) { return dt; }
        }

        /* ── Stats row ── */
        function StatBox({ label, value, color }) {
            return h('div', { style:{ background:cardBg, borderRadius:10, padding:'16px 20px', border:'1px solid '+borderColor,
                boxShadow:cardShadow(dark), flex:1, minWidth:140, textAlign:'center' } },
                h('div', { style:{ fontSize:11, color:mutedColor, fontWeight:500, marginBottom:6, textTransform:'uppercase', letterSpacing:'.5px' } }, label),
                h('div', { style:{ fontSize:24, fontWeight:700, color:color||textColor, letterSpacing:'-.5px' } }, value)
            );
        }

        /* ── Expand row — context + backtrace ── */
        function ExpandedRow({ log }) {
            var ctx = log.context;
            var bt  = ctx && ctx.backtrace;
            var ctxCopy = {};
            if (ctx) { for (var k in ctx) { if (k !== 'backtrace') ctxCopy[k] = ctx[k]; } }
            var hasCtx = Object.keys(ctxCopy).length > 0;

            return h('div', { style:{ padding:'16px 24px', background:dark?'rgba(255,255,255,.02)':'#FAFBFD',
                borderBottom:'1px solid '+borderColor } },

                /* Meta row */
                h('div', { style:{ display:'grid', gridTemplateColumns:'1fr 1fr 1fr', gap:16, marginBottom:hasCtx||bt?16:0 } },
                    h('div', null,
                        h('div', { style:{ fontSize:10, color:mutedColor, textTransform:'uppercase', letterSpacing:'.5px', marginBottom:3 } }, 'IP Address'),
                        h('div', { style:{ fontSize:12, fontFamily:'monospace' } }, log.ip_address || '—')
                    ),
                    h('div', null,
                        h('div', { style:{ fontSize:10, color:mutedColor, textTransform:'uppercase', letterSpacing:'.5px', marginBottom:3 } }, 'User Agent'),
                        h('div', { style:{ fontSize:11, maxWidth:400, overflow:'hidden', textOverflow:'ellipsis', whiteSpace:'nowrap' } }, log.user_agent || '—')
                    ),
                    log.object_type && h('div', null,
                        h('div', { style:{ fontSize:10, color:mutedColor, textTransform:'uppercase', letterSpacing:'.5px', marginBottom:3 } }, 'Object'),
                        h('div', { style:{ fontSize:12 } }, log.object_type + ' #' + log.object_id)
                    )
                ),

                /* Context JSON */
                hasCtx && h('div', { style:{ marginBottom:bt?16:0 } },
                    h('div', { style:{ fontSize:10, color:mutedColor, textTransform:'uppercase', letterSpacing:'.5px', marginBottom:6 } }, 'Context'),
                    h('pre', { style:{ fontSize:11, lineHeight:'1.5', padding:12, borderRadius:8,
                        background:dark?'rgba(0,0,0,.3)':'#F1F3F5', border:'1px solid '+borderColor,
                        overflow:'auto', maxHeight:300, fontFamily:"'JetBrains Mono','Fira Code',monospace", color:textColor,
                        whiteSpace:'pre-wrap', wordBreak:'break-all' } },
                        JSON.stringify(ctxCopy, null, 2)
                    )
                ),

                /* Backtrace */
                bt && Array.isArray(bt) && bt.length > 0 && h('div', null,
                    h('div', { style:{ fontSize:10, color:mutedColor, textTransform:'uppercase', letterSpacing:'.5px', marginBottom:6 } }, 'Backtrace'),
                    h('div', { style:{ fontSize:11, lineHeight:'1.6', padding:12, borderRadius:8,
                        background:dark?'rgba(0,0,0,.3)':'#F1F3F5', border:'1px solid '+borderColor,
                        fontFamily:"'JetBrains Mono','Fira Code',monospace", overflow:'auto', maxHeight:240 } },
                        bt.map(function(frame, i) {
                            return h('div', { key:i, style:{ color: i===0 ? TC.accent : textColor } },
                                h('span', { style:{ color:mutedColor } }, '#' + i + '  '),
                                h('span', { style:{ color:TC.accent } }, frame.function || '?'),
                                h('span', { style:{ color:mutedColor } }, '  at  '),
                                h('span', null, (frame.file || '?') + ':' + (frame.line || '?'))
                            );
                        })
                    )
                ),

                /* Delete single */
                h('div', { style:{ marginTop:12, display:'flex', justifyContent:'flex-end' } },
                    h('button', { onClick:function(e){ e.stopPropagation(); deleteSingle(log.id); setExpanded(null); },
                        style:{ fontSize:11, color:TC.error, background:'transparent', border:'1px solid '+TC.error+'25',
                            borderRadius:6, padding:'3px 12px', cursor:'pointer' } }, 'Delete this entry')
                )
            );
        }

        /* ── Pagination ── */
        function Pagination() {
            if (totalPg <= 1) return null;
            var pages = [];
            var start = Math.max(1, page - 2);
            var end   = Math.min(totalPg, page + 2);
            if (start > 1) { pages.push(1); if (start > 2) pages.push('…'); }
            for (var i = start; i <= end; i++) pages.push(i);
            if (end < totalPg) { if (end < totalPg - 1) pages.push('…'); pages.push(totalPg); }

            return h('div', { style:{ display:'flex', justifyContent:'space-between', alignItems:'center',
                padding:'12px 24px', borderTop:'1px solid '+borderColor } },
                h('span', { style:{ fontSize:12, color:mutedColor } },
                    'Showing ' + ((page-1)*perPage+1) + '–' + Math.min(page*perPage, total) + ' of ' + total + ' entries'),
                h('div', { style:{ display:'flex', gap:4 } },
                    pages.map(function(p, idx) {
                        var isCur = p === page;
                        return h('button', { key:idx,
                            onClick: typeof p === 'number' ? function(){ setPage(p); setExpanded(null); } : undefined,
                            disabled: typeof p !== 'number',
                            style:{ width:typeof p==='number'?30:24, height:30, borderRadius:6,
                                border: isCur ? 'none' : '1px solid '+borderColor,
                                background: isCur ? TC.accent : 'transparent',
                                color: isCur ? '#fff' : mutedColor,
                                fontSize:12, fontWeight: isCur ? 600 : 400, cursor: typeof p==='number'?'pointer':'default',
                                display:'flex', alignItems:'center', justifyContent:'center' } },
                            String(p));
                    })
                )
            );
        }

        /* ══════════════════════════════════════════════
         * RENDER
         * ══════════════════════════════════════════════ */
        return h('main', { style:{ flex:1, overflow:'auto', padding:S.xxl } },

            /* ── Header ── */
            h('div', { style:{ display:'flex', justifyContent:'space-between', alignItems:'center', marginBottom:S.xl } },
                h('div', { style:{ display:'flex', alignItems:'center', gap:S.md } },
                    backBtn || null,
                    h('h2', { style:{ fontSize:20, fontWeight:700, letterSpacing:'-.3px' } }, 'System Logs')
                ),
                h('div', { style:{ display:'flex', gap:S.sm } },
                    h(Btn, { onClick:cleanOld, disabled:cleaning, mutedColor:mutedColor, borderColor:borderColor },
                        cleaning ? 'Cleaning\u2026' : 'Clean Old'),
                    h(Btn, { onClick:cleanAll, disabled:cleaning, mutedColor:mutedColor, borderColor:borderColor }, 'Clean All'),
                    h(Btn, { variant:'primary', onClick:function(){ fetchLogs(page); fetchStats(); }, mutedColor:mutedColor, borderColor:borderColor }, '\u21BB Refresh')
                )
            ),

            /* ── Result banner ── */
            result && h('div', { style:{ marginBottom:S.lg, padding:'10px 16px', borderRadius:8, fontSize:13,
                background: result.success ? (dark?'rgba(22,163,74,.08)':'rgba(22,163,74,.05)') : (dark?'rgba(220,38,38,.08)':'rgba(220,38,38,.05)'),
                border: '1px solid ' + (result.success ? TC.success+'25' : TC.error+'25'),
                color: result.success ? TC.success : TC.error } },
                result.message
            ),

            /* ── Stats row ── */
            stats && h('div', { style:{ display:'flex', gap:S.lg, marginBottom:S.xl } },
                h(StatBox, { label:'Total Entries', value:String(stats.total || 0) }),
                h(StatBox, { label:'Today', value:String(stats.today || 0), color:TC.accent }),
                h(StatBox, { label:'Errors (24h)', value:String(stats.errors_24h || 0), color:stats.errors_24h > 0 ? TC.error : TC.success }),
                h(StatBox, { label:'Oldest Entry', value: stats.oldest ? fmtDate(stats.oldest).split('  ')[0] : '\u2014' })
            ),

            /* ── Filter bar ── */
            h(Card, { cardBg:cardBg, borderColor:borderColor, dark:dark },
                h('div', { style:{ display:'flex', gap:S.md, flexWrap:'wrap', alignItems:'flex-end' } },
                    /* Level */
                    h('div', { style:{ minWidth:120 } },
                        h('label', { style:{ fontSize:10, fontWeight:500, color:mutedColor, display:'block', marginBottom:3, textTransform:'uppercase', letterSpacing:'.5px' } }, 'Level'),
                        h('select', { value:fLevel, onChange:function(e){ setFLevel(e.target.value); }, style:selSty },
                            h('option', { value:'' }, 'All levels'),
                            LEVELS.map(function(l){ return h('option', { key:l, value:l }, l.charAt(0).toUpperCase()+l.slice(1)); })
                        )
                    ),
                    /* Type */
                    h('div', { style:{ minWidth:120 } },
                        h('label', { style:{ fontSize:10, fontWeight:500, color:mutedColor, display:'block', marginBottom:3, textTransform:'uppercase', letterSpacing:'.5px' } }, 'Type'),
                        h('select', { value:fType, onChange:function(e){ setFType(e.target.value); }, style:selSty },
                            h('option', { value:'' }, 'All types'),
                            TYPES.map(function(t){ return h('option', { key:t, value:t }, t.charAt(0).toUpperCase()+t.slice(1)); }),
                            /* Also add dynamic types from stats */
                            stats && stats.by_type ? stats.by_type.filter(function(bt){ return TYPES.indexOf(bt.type) === -1 && bt.type; }).map(function(bt){
                                return h('option', { key:bt.type, value:bt.type }, bt.type.charAt(0).toUpperCase()+bt.type.slice(1));
                            }) : null
                        )
                    ),
                    /* Action */
                    h('div', { style:{ minWidth:120 } },
                        h('label', { style:{ fontSize:10, fontWeight:500, color:mutedColor, display:'block', marginBottom:3, textTransform:'uppercase', letterSpacing:'.5px' } }, 'Action'),
                        h('select', { value:fAction, onChange:function(e){ setFAction(e.target.value); }, style:selSty },
                            h('option', { value:'' }, 'All actions'),
                            stats && stats.actions ? stats.actions.map(function(a){ return h('option', { key:a, value:a }, a); }) : null
                        )
                    ),
                    /* Date from */
                    h('div', { style:{ minWidth:130 } },
                        h('label', { style:{ fontSize:10, fontWeight:500, color:mutedColor, display:'block', marginBottom:3, textTransform:'uppercase', letterSpacing:'.5px' } }, 'From'),
                        h('input', { type:'date', value:fDateFrom, onChange:function(e){ setFDateFrom(e.target.value); },
                            style:Object.assign({}, fs, { fontSize:12 }) })
                    ),
                    /* Date to */
                    h('div', { style:{ minWidth:130 } },
                        h('label', { style:{ fontSize:10, fontWeight:500, color:mutedColor, display:'block', marginBottom:3, textTransform:'uppercase', letterSpacing:'.5px' } }, 'To'),
                        h('input', { type:'date', value:fDateTo, onChange:function(e){ setFDateTo(e.target.value); },
                            style:Object.assign({}, fs, { fontSize:12 }) })
                    ),
                    /* Search */
                    h('div', { style:{ flex:1, minWidth:180 } },
                        h('label', { style:{ fontSize:10, fontWeight:500, color:mutedColor, display:'block', marginBottom:3, textTransform:'uppercase', letterSpacing:'.5px' } }, 'Search'),
                        h('input', { type:'text', value:fSearch, onChange:onSearchChange, placeholder:'Search messages\u2026',
                            style:Object.assign({}, fs, { fontSize:12 }) })
                    ),
                    /* Reset */
                    hasFilters && h('button', { onClick:resetFilters,
                        style:{ fontSize:11, color:TC.accent, background:'transparent', border:'none', cursor:'pointer', padding:'8px 0', fontWeight:500, whiteSpace:'nowrap' } },
                        '\u2715 Clear filters')
                )
            ),

            /* ── spacer ── */
            h('div', { style:{ height:S.lg } }),

            /* ── Logs table ── */
            h(Card, { cardBg:cardBg, borderColor:borderColor, dark:dark, pad:0 },
                /* Table header */
                h('div', { style:{ display:'grid', gridTemplateColumns:'100px 80px 90px 1fr 160px 140px',
                    padding:'12px 24px', background:tableHeaderBg, borderBottom:'1px solid '+borderColor } },
                    ['Level','Type','Action','Message','User','Time'].map(function(col) {
                        return h('span', { key:col, style:{ fontSize:10, fontWeight:500, color:mutedColor,
                            textTransform:'uppercase', letterSpacing:'.5px' } }, col);
                    })
                ),

                loading && h('div', { style:{ padding:40, textAlign:'center' } }, h(Spinner, { mutedColor:mutedColor })),

                !loading && logs.length === 0 && h('div', { style:{ padding:40 } },
                    h(EmptyState, { mutedColor:mutedColor, icon:'\uD83D\uDCCB',
                        title: hasFilters ? 'No matching log entries' : 'No log entries yet',
                        sub: hasFilters ? 'Try adjusting your filters.' : 'Log entries will appear here as events occur in your store.' })
                ),

                !loading && logs.map(function(log, idx) {
                    var isExp = expanded === log.id;
                    var userName = log.user ? log.user.name : (log.user_id ? 'User #'+log.user_id : '\u2014');

                    return h('div', { key:log.id },
                        /* Row */
                        h('div', {
                            onClick: function(){ setExpanded(isExp ? null : log.id); },
                            style:{ display:'grid', gridTemplateColumns:'100px 80px 90px 1fr 160px 140px',
                                padding:'12px 24px', alignItems:'center', cursor:'pointer',
                                borderBottom: (isExp || idx < logs.length - 1) ? '1px solid '+borderColor : 'none',
                                transition:'background .1s',
                                background: isExp ? (dark?'rgba(37,99,235,.04)':'rgba(37,99,235,.02)') : 'transparent' },
                            onMouseEnter:function(e){ if (!isExp) e.currentTarget.style.background = dark?'rgba(255,255,255,.02)':'#F9FAFB'; },
                            onMouseLeave:function(e){ if (!isExp) e.currentTarget.style.background = 'transparent'; },
                        },
                            /* Level */
                            h('span', null, h(LevelBadge, { level:log.level })),
                            /* Type */
                            h('span', { style:{ fontSize:12, color:mutedColor, fontWeight:400 } }, log.channel || '\u2014'),
                            /* Action */
                            h('span', { style:{ fontSize:12, color:textColor, fontWeight:500 } }, log.action || '\u2014'),
                            /* Message */
                            h('div', { style:{ fontSize:12, color:textColor, overflow:'hidden', textOverflow:'ellipsis',
                                whiteSpace:'nowrap', paddingRight:12 } },
                                log.message || '\u2014'
                            ),
                            /* User */
                            h('div', null,
                                h('div', { style:{ fontSize:12, fontWeight:400 } }, userName),
                                log.user && log.user.email && h('div', { style:{ fontSize:10, color:mutedColor } }, log.user.email)
                            ),
                            /* Time */
                            h('span', { style:{ fontSize:11, color:mutedColor } }, fmtDate(log.created_at))
                        ),
                        /* Expanded detail */
                        isExp && h(ExpandedRow, { log:log })
                    );
                }),

                /* Pagination */
                !loading && logs.length > 0 && h(Pagination, null)
            ),

            /* ── Per-page + counts footer ── */
            !loading && total > 0 && h('div', { style:{ display:'flex', justifyContent:'space-between', alignItems:'center', marginTop:S.lg } },
                h('div', { style:{ display:'flex', alignItems:'center', gap:S.sm } },
                    h('span', { style:{ fontSize:12, color:mutedColor } }, 'Rows per page:'),
                    h('select', { value:perPage, onChange:function(e){ setPerPage(Number(e.target.value)); setPage(1); },
                        style:Object.assign({}, selSty, { width:70 }) },
                        [25,50,100].map(function(n){ return h('option', { key:n, value:n }, String(n)); })
                    )
                ),
                stats && stats.by_level && h('div', { style:{ display:'flex', gap:S.md } },
                    stats.by_level.map(function(bl) {
                        var lc = LEVEL_COLORS[bl.level] || LEVEL_COLORS.info;
                        return h('span', { key:bl.level, style:{ fontSize:11, color:lc.color } },
                            bl.level + ': ' + bl.cnt);
                    })
                )
            ),

            /* ── Hint box ── */
            !loading && h('div', { style:{ marginTop:S.lg, padding:S.lg, borderRadius:10,
                background:dark?'rgba(37,99,235,0.06)':'rgba(37,99,235,0.04)',
                border:'1px solid '+(dark?'rgba(37,99,235,0.15)':'rgba(37,99,235,0.12)') } },
                h('div', { style:{ fontSize:13, fontWeight:600, color:TC.accent, marginBottom:4 } }, 'About System Logs'),
                h('div', { style:{ fontSize:12, color:mutedColor, lineHeight:'1.6' } },
                    'Logs record store activity including product changes, order events, user logins, database operations, and API requests. ',
                    'Control what gets logged in ', h('span', { style:{ color:TC.accent, cursor:'pointer', fontWeight:500 },
                        onClick:function(){ navigate('settings/logging'); } }, 'Settings \u2192 Logging'), '. ',
                    'The retention period determines when old entries are automatically removed. ',
                    'Click any row to view full details including context data, IP address, and call stack.'
                )
            )
        );
    }

    /* ── Schema-driven settings sub-page ─────────────────────────────── */
    /* ════════════════════════════════════════════════════════════════════
     * TITANCART ESSENTIALS HUB (S313)
     *
     * Free bundle's admin home. Manifest-driven: fetches the feature list +
     * settings from the extension over /essentials/features (core titancart/v1
     * namespace) and renders a General-Settings-style layout - left sidebar of
     * features, right pane = the active feature's master on/off toggle + its
     * settings fields. New features added in the extension appear here with NO
     * further SPA change. Route: settings/essentials[/<feature_id>].
     * ════════════════════════════════════════════════════════════════════ */
    function EssentialsPage({ dark, cardBg, textColor, mutedColor, borderColor }) {
        var hash  = (window.location.hash || '').replace(/^#\/?/, '');
        var parts = hash.split('/');            // ['settings','essentials', <featId?>]

        var _feats   = useState(null);  var feats   = _feats[0];   var setFeats   = _feats[1];
        var _loading = useState(true);  var loading = _loading[0]; var setLoading = _loading[1];
        var _saving  = useState(false); var saving  = _saving[0];  var setSaving  = _saving[1];
        var _saved   = useState(false); var saved   = _saved[0];   var setSaved   = _saved[1];
        var _error   = useState('');    var error   = _error[0];   var setError   = _error[1];

        useEffect(function() {
            if (!Config.essentialsInstalled) { setLoading(false); return; }
            setLoading(true);
            fetch(Config.apiUrl + '/essentials/features', { headers:{ 'X-WP-Nonce':Config.nonce } })
                .then(function(r){ return r.json(); })
                .then(function(d){ setFeats((d && d.features) ? d.features : []); setLoading(false); })
                .catch(function(){ setError('Failed to load TitanCart Essentials.'); setLoading(false); });
        }, []);

        function go(featId){ navigate('settings/essentials/' + featId); }
        function back(){ navigate('settings'); }

        function iconFor(key) {
            if (key === 'fire') {
                return ico([h('path',{d:'M12 2c1.6 2.6.6 4.2-.8 5.6C9.8 9 8.4 10.8 8.4 13.2A3.6 3.6 0 0012 16.8a3.6 3.6 0 003.6-3.6c0-1.3-.5-2.4-1.3-3.3.2 1.3-.8 2-1.5 1.7 1.1-1.8-.2-3.3-1-4.1C13.6 5 14.2 3.4 12 2z'})], 18, '0 0 24 24');
            }
            if (key === 'pin') {
                return ico([h('path',{d:'M12 21s-6-5.4-6-10a6 6 0 1112 0c0 4.6-6 10-6 10z'}),h('circle',{cx:12,cy:11,r:2.4})], 18, '0 0 24 24');
            }
            if (key === 'wrench') {
                return ico([h('path',{d:'M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z'})], 18, '0 0 24 24');
            }
            if (key === 'truck') {
                return ico([h('path',{d:'M14 18V6a2 2 0 0 0-2-2H4a2 2 0 0 0-2 2v11a1 1 0 0 0 1 1h2'}),h('path',{d:'M15 18H9'}),h('path',{d:'M19 18h2a1 1 0 0 0 1-1v-3.65a1 1 0 0 0-.22-.62l-3.48-4.35A1 1 0 0 0 18.52 8H14'}),h('circle',{cx:17,cy:18,r:2}),h('circle',{cx:7,cy:18,r:2})], 18, '0 0 24 24');
            }
            if (key === 'palette') {
                return ico([h('path',{d:'M12 22a1 1 0 0 1 0-20 10 9 0 0 1 10 9 5 5 0 0 1-5 5h-2.25a1.75 1.75 0 0 0-1.4 2.8l.3.4a1.75 1.75 0 0 1-1.4 2.8z'}),h('circle',{cx:13.5,cy:6.5,r:.5,fill:'currentColor'}),h('circle',{cx:17.5,cy:10.5,r:.5,fill:'currentColor'}),h('circle',{cx:6.5,cy:12.5,r:.5,fill:'currentColor'}),h('circle',{cx:8.5,cy:7.5,r:.5,fill:'currentColor'})], 18, '0 0 24 24');
            }
            if (key === 'activity') {
                return ico([h('polyline',{points:'22 12 18 12 15 21 9 3 6 12 2 12'})], 18, '0 0 24 24');
            }
            if (key === 'image') {
                return ico([h('rect',{width:18,height:18,x:3,y:3,rx:2,ry:2}),h('circle',{cx:9,cy:9,r:2}),h('path',{d:'m21 15-3.09-3.09a2 2 0 0 0-2.82 0L6 21'})], 18, '0 0 24 24');
            }
            if (key === 'copy') {
                return ico([h('rect',{width:14,height:14,x:8,y:8,rx:2,ry:2}),h('path',{d:'M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2'})], 18, '0 0 24 24');
            }
            if (key === 'bolt') {
                return ico([h('path',{d:'M4 14a1 1 0 0 1-.78-1.63l9.9-10.2a.5.5 0 0 1 .86.46l-1.92 6.02A1 1 0 0 0 13 10h7a1 1 0 0 1 .78 1.63l-9.9 10.2a.5.5 0 0 1-.86-.46l1.92-6.02A1 1 0 0 0 11 14z'})], 18, '0 0 24 24');
            }
            if (key === 'megaphone') {
                return ico([h('path',{d:'m3 11 18-5v12L3 14v-3z'}),h('path',{d:'M11.6 16.8a3 3 0 1 1-5.8-1.6'})], 18, '0 0 24 24');
            }
            if (key === 'bell') {
                return ico([h('path',{d:'M10.268 21a2 2 0 0 0 3.464 0'}),h('path',{d:'M3.262 15.326A1 1 0 0 0 4 17h16a1 1 0 0 0 .74-1.673C19.41 13.956 18 12.499 18 8A6 6 0 0 0 6 8c0 4.499-1.411 5.956-2.738 7.326'})], 18, '0 0 24 24');
            }
            if (key === 'help') {
                return ico([h('circle',{cx:12,cy:12,r:10}),h('path',{d:'M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3'}),h('path',{d:'M12 17h.01'})], 18, '0 0 24 24');
            }
            return ico([h('circle',{cx:12,cy:12,r:8})], 18, '0 0 24 24');
        }

        // Immutable patch of a feature's enabled flag or one field value.
        function patchFeature(featId, patch) {
            setFeats(function(prev){
                if (!prev) return prev;
                return prev.map(function(f){
                    if (f.id !== featId) return f;
                    var nf = Object.assign({}, f);
                    nf.values = Object.assign({}, f.values || {});
                    if (patch.enabled !== undefined) {
                        nf.enabled = patch.enabled;
                        nf.values.enabled = patch.enabled ? '1' : '0';
                    }
                    if (patch.key !== undefined) {
                        nf.values[patch.key] = patch.val;
                    }
                    return nf;
                });
            });
            setSaved(false);
        }

        function handleSave(feat) {
            setSaving(true); setSaved(false); setError('');
            var body = Object.assign({}, feat.values || {});
            fetch(Config.apiUrl + '/essentials/features/' + feat.id, {
                method: 'PUT',
                headers: { 'Content-Type':'application/json', 'X-WP-Nonce':Config.nonce },
                body: JSON.stringify(body),
            })
            .then(function(r){ return r.json(); })
            .then(function(d){
                setSaving(false);
                if (d && d.values) {
                    setFeats(function(prev){
                        return prev ? prev.map(function(f){
                            return f.id === feat.id ? Object.assign({}, f, { values:d.values, enabled:d.enabled }) : f;
                        }) : prev;
                    });
                    setSaved(true);
                    setTimeout(function(){ setSaved(false); }, 2500);
                } else {
                    setError('Save failed.');
                }
            })
            .catch(function(){ setSaving(false); setError('Network error. Check your connection.'); });
        }

        function toggleSwitch(on, onClick) {
            return h('div', { onClick:onClick, style:{
                    width:40, height:22, borderRadius:12, cursor:'pointer',
                    background: on ? TC.accent : (dark?'#333':'#D4D4D4'),
                    transition:'background .15s', position:'relative', flexShrink:0 } },
                h('div', { style:{
                    width:18, height:18, borderRadius:'50%', background:'#fff',
                    position:'absolute', top:2, left: on ? 20 : 2,
                    transition:'left .15s', boxShadow:'0 1px 3px rgba(0,0,0,.2)' } })
            );
        }

        function fieldInput(feat, field) {
            var fs  = fieldSty(dark, borderColor);
            var val = (feat.values && feat.values[field.key] != null) ? feat.values[field.key] : (field.default != null ? field.default : '');

            if (field.type === 'checkbox') {
                var on = (val === '1' || val === 1 || val === true);
                return toggleSwitch(on, function(){ patchFeature(feat.id, { key:field.key, val: on ? '0' : '1' }); });
            }
            if (field.type === 'select') {
                var opts = field.options || {};
                return h('select', {
                    value: val,
                    onChange: function(e){ patchFeature(feat.id, { key:field.key, val:e.target.value }); },
                    style: Object.assign({}, fs, { cursor:'pointer' }),
                }, Object.keys(opts).map(function(k){ return h('option', { key:k, value:k }, opts[k]); }));
            }
            if (field.type === 'color') {
                return h('div', { style:{ display:'flex', alignItems:'center', gap:10 } },
                    h('input', { type:'color', value:val,
                        onChange:function(e){ patchFeature(feat.id, { key:field.key, val:e.target.value }); },
                        style:{ width:46, height:34, padding:0, border:'1px solid '+borderColor, borderRadius:8, background:'transparent', cursor:'pointer' } }),
                    h('input', { type:'text', value:val,
                        onChange:function(e){ patchFeature(feat.id, { key:field.key, val:e.target.value }); },
                        style: Object.assign({}, fs, { maxWidth:120 }) })
                );
            }
            if (field.type === 'textarea') {
                return h('textarea', { value:val, rows:3,
                    onChange:function(e){ patchFeature(feat.id, { key:field.key, val:e.target.value }); },
                    style: Object.assign({}, fs, { resize:'vertical', minHeight:72 }) });
            }
            if (field.type === 'datetime') {
                return h('input', { type:'datetime-local', value:val,
                    onChange:function(e){ patchFeature(feat.id, { key:field.key, val:e.target.value }); },
                    style: Object.assign({}, fs, { maxWidth:260 }) });
            }
            var it = field.type === 'number' ? 'number' : 'text';
            return h('input', { type:it, value:val,
                onChange:function(e){ patchFeature(feat.id, { key:field.key, val:e.target.value }); },
                style: Object.assign({}, fs, { maxWidth: field.type === 'number' ? 180 : 'none' }) });
        }

        // ── Header (back + title) ───────────────────────────────────────────
        var header = h('div', { style:{ display:'flex', alignItems:'center', gap:12, marginBottom:S.xl } },
            h('button', { onClick:back, style:{
                    appearance:'none', border:'1px solid '+borderColor, background:cardBg, color:mutedColor,
                    width:32, height:32, borderRadius:8, cursor:'pointer', fontSize:16, lineHeight:1, flexShrink:0 } }, '\u2190'),
            h('div', null,
                h('h2', { style:{ fontSize:22, fontWeight:700, letterSpacing:'-.3px', color:textColor } }, 'TitanCart Essentials'),
                h('div', { style:{ fontSize:12, color:mutedColor, marginTop:2 } }, 'Free mini-features - turn on only what you need.')
            )
        );

        if (!Config.essentialsInstalled) {
            return h('main', { style:{ flex:1, overflow:'auto', padding:S.xxl } }, header,
                h('div', { style:{ padding:S.xxl, textAlign:'center', color:mutedColor, fontSize:14 } },
                    'The TitanCart Essentials plugin is not active. Install and activate it to manage these features.'));
        }
        if (loading) {
            return h('main', { style:{ flex:1, overflow:'auto', padding:S.xxl } }, header,
                h('div', { style:{ padding:S.xxl, color:mutedColor, fontSize:14 } }, 'Loading\u2026'));
        }
        if (!feats || !feats.length) {
            return h('main', { style:{ flex:1, overflow:'auto', padding:S.xxl } }, header,
                h('div', { style:{ padding:S.xxl, color:mutedColor, fontSize:14 } }, error || 'No features available.'));
        }

        var activeId   = parts[2] || feats[0].id;
        var activeMatch = feats.filter(function(f){ return f.id === activeId; });
        var activeFeat = activeMatch.length ? activeMatch[0] : feats[0];

        // ── Left sidebar (feature list) ─────────────────────────────────────
        var leftNav = h('div', { style:{ width:230, flexShrink:0, paddingRight:S.xl, borderRight:'1px solid '+borderColor } },
            feats.map(function(f){
                var active = f.id === activeFeat.id;
                return h('div', {
                    key:f.id,
                    onClick:function(){ go(f.id); },
                    style:{
                        display:'flex', alignItems:'center', gap:10,
                        padding:'10px 12px', borderRadius:8, marginBottom:3, cursor:'pointer',
                        background: active ? (dark?'rgba(37,99,235,.12)':'rgba(37,99,235,.06)') : 'transparent',
                        transition:'all .12s' },
                },
                    h('div', { style:{ color: active ? TC.accent : mutedColor, display:'flex', flexShrink:0 } }, iconFor(f.icon)),
                    h('div', { style:{ minWidth:0, flex:1 } },
                        h('div', { style:{ fontSize:13, fontWeight: active?600:500, color: active?TC.accent:textColor, whiteSpace:'nowrap', overflow:'hidden', textOverflow:'ellipsis' } }, f.name)
                    ),
                    h('span', { style:{ width:8, height:8, borderRadius:'50%', flexShrink:0,
                        background: f.enabled ? TC.success : (dark?'#444':'#D4D4D4') },
                        title: f.enabled ? 'On' : 'Off' })
                );
            })
        );

        // ── Right pane (active feature) ─────────────────────────────────────
        var enabled = !!activeFeat.enabled;
        var rightPane = h('div', { style:{ flex:1, minWidth:0, paddingLeft:S.xl } },
            // Feature header
            h('div', { style:{ marginBottom:S.lg } },
                h('h3', { style:{ fontSize:17, fontWeight:700, color:textColor, marginBottom:4 } }, activeFeat.name),
                h('div', { style:{ fontSize:13, color:mutedColor, lineHeight:1.5 } }, activeFeat.desc)
            ),
            // Master toggle card
            h('div', { style:{ display:'flex', alignItems:'center', justifyContent:'space-between',
                    padding:'14px 16px', background:cardBg, border:'1px solid '+borderColor, borderRadius:10, marginBottom:S.lg } },
                h('div', null,
                    h('div', { style:{ fontSize:14, fontWeight:600, color:textColor } }, enabled ? 'Enabled' : 'Disabled'),
                    h('div', { style:{ fontSize:12, color:mutedColor, marginTop:2 } }, 'Turn this feature ' + (enabled ? 'off' : 'on') + ' for your store.')
                ),
                toggleSwitch(enabled, function(){ patchFeature(activeFeat.id, { enabled: !enabled }); })
            ),
            // Settings fields
            (activeFeat.fields && activeFeat.fields.length)
                ? h('div', { style:{ background:cardBg, border:'1px solid '+borderColor, borderRadius:10, padding:S.xl, opacity: enabled ? 1 : 0.6 } },
                    activeFeat.fields.map(function(field, i){
                        return h('div', { key:field.key, style:{ marginBottom: i === activeFeat.fields.length-1 ? 0 : S.lg } },
                            h('label', { style:{ display:'block', fontSize:13, fontWeight:600, color:textColor, marginBottom:6 } }, field.label),
                            fieldInput(activeFeat, field),
                            field.hint ? h('div', { style:{ fontSize:12, color:mutedColor, marginTop:6, lineHeight:1.5 } }, field.hint) : null
                        );
                    })
                )
                : h('div', { style:{ fontSize:13, color:mutedColor, fontStyle:'italic' } }, 'This feature has no additional settings.'),
            // Notify-me signups summary + CSV export (maintenance only)
            activeFeat.id === 'maintenance_mode' ? h('div', { style:{ marginTop:S.lg, padding:'14px 16px', background:cardBg, border:'1px solid '+borderColor, borderRadius:10, display:'flex', alignItems:'center', justifyContent:'space-between', gap:12 } },
                h('div', { style:{ minWidth:0 } },
                    h('div', { style:{ fontSize:14, fontWeight:600, color:textColor } }, 'Notify-me signups'),
                    h('div', { style:{ fontSize:12, color:mutedColor, marginTop:2, lineHeight:1.5 } },
                        (activeFeat.signup_count || 0) + ((activeFeat.signup_count === 1) ? ' email' : ' emails') + ' collected. They are emailed automatically when the countdown ends or you turn maintenance off.')
                ),
                h('button', {
                    onClick: function(){
                        fetch(Config.apiUrl + '/essentials/notify-signups/export', { headers:{ 'X-WP-Nonce':Config.nonce } })
                            .then(function(r){ return r.json(); })
                            .then(function(d){
                                var blob = new Blob([ (d && d.csv) || '' ], { type:'text/csv;charset=utf-8' });
                                var url = URL.createObjectURL(blob);
                                var a = document.createElement('a');
                                a.href = url; a.download = 'maintenance-signups.csv';
                                document.body.appendChild(a); a.click(); document.body.removeChild(a);
                                URL.revokeObjectURL(url);
                            })
                            .catch(function(){});
                    },
                    disabled: !activeFeat.signup_count,
                    style:{ flexShrink:0, appearance:'none', border:'1px solid '+borderColor, background:'transparent',
                        color: activeFeat.signup_count ? textColor : mutedColor, fontSize:13, fontWeight:600,
                        padding:'8px 14px', borderRadius:8, cursor: activeFeat.signup_count ? 'pointer' : 'default' } },
                    'Export CSV')
            ) : null,
            // Save row
            h('div', { style:{ display:'flex', alignItems:'center', gap:14, marginTop:S.xl } },
                h('button', { onClick:function(){ handleSave(activeFeat); }, disabled:saving, style:{
                        appearance:'none', border:'none', cursor: saving ? 'default' : 'pointer',
                        background:TC.accent, color:'#fff', fontSize:14, fontWeight:600,
                        padding:'10px 22px', borderRadius:8, opacity: saving ? 0.7 : 1 } },
                    saving ? 'Saving\u2026' : 'Save changes'),
                saved ? h('span', { style:{ fontSize:13, color:TC.success, fontWeight:600 } }, 'Saved') : null,
                error ? h('span', { style:{ fontSize:13, color:TC.error || '#DC2626', fontWeight:600 } }, error) : null
            )
        );

        return h('main', { style:{ flex:1, overflow:'auto', padding:S.xxl } }, header,
            h('div', { style:{ display:'flex', alignItems:'flex-start' } }, leftNav, rightPane)
        );
    }

    function SettingsSubPage({ dark, cardBg, textColor, mutedColor, borderColor, tableHeaderBg }) {
        var hash = (window.location.hash || '').replace(/^#\/?/, '');
        var parts = hash.split('/');
        /* parts[0] = 'settings', parts[1] = sub-page slug */
        var activeSub = parts[1] || 'general';

        var _schema  = useState(null);    var schema  = _schema[0];  var setSchema  = _schema[1];
        var _values  = useState({});      var values  = _values[0];  var setValues  = _values[1];
        var _loading = useState(true);    var loading = _loading[0]; var setLoading = _loading[1];
        var _saving  = useState(false);   var saving  = _saving[0];  var setSaving  = _saving[1];
        var _saved   = useState(false);   var saved   = _saved[0];   var setSaved   = _saved[1];
        var _error   = useState('');      var error   = _error[0];   var setError   = _error[1];

        // S268-oob — E-mails section "send test email" panel. States hoisted to
        // the top of the component (focus-safe rule #106): if they lived inside a
        // nested component, typing in the recipient input would drop focus on
        // every keystroke. Only surfaced when activeSub === 'emails'.
        var _testTo     = useState('');    var testTo     = _testTo[0];     var setTestTo     = _testTo[1];
        var _testing    = useState(false); var testing    = _testing[0];    var setTesting    = _testing[1];
        var _testResult = useState(null);  var testResult = _testResult[0]; var setTestResult = _testResult[1];

        var fs = fieldSty(dark, borderColor);
        var selSty = Object.assign({}, fs, { cursor:'pointer' });

        /* Fetch schema + values when sub changes */
        useEffect(function() {
            setLoading(true);
            setSaved(false);
            setError('');
            fetch(Config.apiUrl + '/settings/' + activeSub, { headers:{ 'X-WP-Nonce':Config.nonce } })
                .then(function(r){ return r.json(); })
                .then(function(data){
                    if (data && data.schema) {
                        setSchema(data.schema);
                        setValues(data.values || {});
                    } else {
                        setSchema(null);
                        setError('Section not found.');
                    }
                    setLoading(false);
                })
                .catch(function(e){
                    setError('Failed to load settings.');
                    setLoading(false);
                });
        }, [activeSub]);

        function onChange(key, val) {
            setValues(function(prev) {
                var next = {};
                for (var k in prev) next[k] = prev[k];
                next[key] = val;
                return next;
            });
            setSaved(false);
        }

        function handleSave() {
            setSaving(true);
            setSaved(false);
            setError('');
            fetch(Config.apiUrl + '/settings/' + activeSub, {
                method: 'PUT',
                headers: { 'Content-Type':'application/json', 'X-WP-Nonce':Config.nonce },
                body: JSON.stringify(values),
            })
            .then(function(r){ return r.json(); })
            .then(function(data){
                if (data && data.success) {
                    setSaved(true);
                    setTimeout(function(){ setSaved(false); }, 3000);
                } else {
                    setError(data.message || 'Save failed.');
                }
                setSaving(false);
            })
            .catch(function(){
                setError('Network error. Check your connection.');
                setSaving(false);
            });
        }

        // S268-oob — Send a test email through the LIVE mail pipeline (the same
        // wp_mail path real order emails use). A green result means real order
        // emails will send; a red result surfaces the exact error, including the
        // SMTP transport actually used. Reads STORED settings, so save first.
        function doTestEmail() {
            if (testing) return;
            if (!/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test((testTo || '').trim())) {
                setTestResult({ success:false, message:'Enter a valid recipient email address.' });
                return;
            }
            setTesting(true); setTestResult(null);
            fetch(Config.apiUrl + '/email/test', {
                method:'POST',
                credentials:'include',
                headers:{ 'Content-Type':'application/json', 'X-WP-Nonce':Config.nonce || '' },
                body: JSON.stringify({ type:'order_confirmation', to:(testTo || '').trim() }),
            })
            .then(function(r){ return r.json().then(function(d){ return { ok:r.ok, body:d }; }); })
            .then(function(wrap){ setTesting(false); setTestResult(wrap.body || { success:false, message:'Empty response from server.' }); })
            .catch(function(e){ setTesting(false); setTestResult({ success:false, message:e.message || 'Network error.' }); });
        }

        /* ── Render a single field based on type ── */
        function renderField(field) {
            var val = values[field.key] != null ? values[field.key] : (field.default || '');

            if (field.type === 'checkbox') {
                var isOn = val === '1' || val === 1 || val === true;
                return h('label', { style:{ display:'flex', alignItems:'center', gap:10, cursor:'pointer' } },
                    h('div', {
                        onClick: function(){ onChange(field.key, isOn ? '0' : '1'); },
                        style:{
                            width:40, height:22, borderRadius:12, cursor:'pointer',
                            background: isOn ? TC.accent : (dark?'#333':'#D4D4D4'),
                            transition:'background .15s', position:'relative', flexShrink:0,
                        }
                    },
                        h('div', { style:{
                            width:18, height:18, borderRadius:'50%', background:'#fff',
                            position:'absolute', top:2, left: isOn ? 20 : 2,
                            transition:'left .15s', boxShadow:'0 1px 3px rgba(0,0,0,.2)',
                        }})
                    ),
                    h('span', { style:{ fontSize:13, color:textColor } }, field.label)
                );
            }

            if (field.type === 'select') {
                return h('select', {
                    value: val,
                    onChange: function(e){ onChange(field.key, e.target.value); },
                    style: selSty,
                },
                    (field.options || []).map(function(opt){
                        return h('option', { key:opt.value, value:opt.value }, opt.label);
                    })
                );
            }

            if (field.type === 'textarea') {
                return h('textarea', {
                    value: val,
                    onChange: function(e){ onChange(field.key, e.target.value); },
                    rows: 3,
                    style: Object.assign({}, fs, { resize:'vertical', minHeight:72 }),
                });
            }

            if (field.type === 'color') {
                var cval = /^#[0-9a-fA-F]{6}$/.test(val) ? val : (field.default || '#000000');
                return h('div', { style:{ display:'flex', alignItems:'center', gap:10 } },
                    h('input', { type:'color', value:cval,
                        onChange:function(e){ onChange(field.key, e.target.value); },
                        style:{ width:46, height:34, padding:0, border:'1px solid '+(dark?'#404040':'#D4D4D4'), borderRadius:8, background:'transparent', cursor:'pointer' } }),
                    h('input', { type:'text', value:val, placeholder:'#RRGGBB',
                        onChange:function(e){ onChange(field.key, e.target.value); },
                        style:Object.assign({}, fs, { maxWidth:120, fontFamily:'monospace' }) })
                );
            }

            /* text, number, password */
            var inputType = field.type === 'number' ? 'number' : (field.type === 'password' ? 'password' : 'text');
            return h('input', {
                type: inputType,
                value: val,
                autoComplete: field.type === 'password' ? 'new-password' : undefined,
                placeholder: field.type === 'password' ? 'Leave blank to keep current' : undefined,
                onChange: function(e){ onChange(field.key, e.target.value); },
                style: Object.assign({}, fs, { maxWidth: field.type === 'number' ? 180 : 'none' }),
            });
        }

        /* ── Left sub-nav ── */
        var navWidth = 200;
        var leftNav = h('div', { style:{ width:navWidth, flexShrink:0, paddingRight:S.xl, borderRight:`1px solid ${borderColor}` } },
            SETTINGS_SUB_PAGES.map(function(sp) {
                var active = sp.slug === activeSub;
                return h('div', {
                    key: sp.slug,
                    onClick: function(){ navigate('settings/' + sp.slug); },
                    style:{
                        padding:'9px 14px', borderRadius:8, marginBottom:2, cursor:'pointer', fontSize:13,
                        fontWeight: active ? 600 : 400,
                        color: active ? TC.accent : mutedColor,
                        background: active ? (dark?'rgba(37,99,235,.12)':'rgba(37,99,235,.06)') : 'transparent',
                        transition:'all .12s',
                    },
                    onMouseEnter: function(e){ if (!active) e.currentTarget.style.background = dark?'rgba(255,255,255,.03)':'rgba(0,0,0,.02)'; },
                    onMouseLeave: function(e){ if (!active) e.currentTarget.style.background = 'transparent'; },
                }, sp.label);
            })
        );

        /* ── Right content ── */
        var currentSubLabel = '';
        SETTINGS_SUB_PAGES.forEach(function(sp){ if (sp.slug === activeSub) currentSubLabel = sp.label; });

        var rightContent;

        if (loading) {
            rightContent = h('div', { style:{ padding:40, textAlign:'center', color:mutedColor, fontSize:13 } }, 'Loading settings…');
        } else if (!schema) {
            rightContent = h(EmptyState, { mutedColor:mutedColor, icon:'🚧', title: currentSubLabel + ' Settings', sub: error || 'This settings section will be built in a future session.' });
        } else {
            rightContent = h('div', null,
                schema.groups.map(function(group) {
                    var useGrid = group.fields.length >= 3;
                    return h('div', { key:group.id, style:{ marginBottom:S.xxl } },
                        h('h3', { style:{ fontSize:15, fontWeight:600, color:textColor, marginBottom:S.lg,
                                          paddingBottom:S.sm, borderBottom:`1px solid ${borderColor}` } },
                            group.title
                        ),
                        h('div', { style:{ display:'grid', gridTemplateColumns: useGrid ? '1fr 1fr' : '1fr', gap:'16px 24px' } },
                            group.fields.map(function(field) {
                                var spanFull = field.type === 'textarea';
                                var baseWrap = { display:'flex', flexDirection:'column', gap:5 };
                                var wrapStyle = spanFull && useGrid ? Object.assign({}, baseWrap, { gridColumn:'1 / -1' }) : baseWrap;
                                /* Checkboxes render inline (toggle + label) */
                                if (field.type === 'checkbox') {
                                    return h('div', { key:field.key, style:wrapStyle },
                                        renderField(field),
                                        field.hint && h('div', { style:{ fontSize:12, color:mutedColor, marginLeft:50, marginTop:2 } }, field.hint)
                                    );
                                }
                                /* Other fields: label above input */
                                return h('div', { key:field.key, style:wrapStyle },
                                    h('label', { style:{ fontSize:13, fontWeight:500, color:textColor } }, field.label),
                                    renderField(field),
                                    field.hint && h('div', { style:{ fontSize:12, color:mutedColor, marginTop:2 } }, field.hint)
                                );
                            })
                        )
                    );
                })
            );
        }

        // S268-oob — On the E-mails section, append the test panel beneath the
        // form (inside the right content column). Built here so it sees the
        // hoisted test states; gated to activeSub === 'emails'.
        if (activeSub === 'emails' && schema && !loading) {
            var tr = testResult;
            var emailTestCard = h('div', { style:{ marginTop:S.xxl } },
                h('h3', { style:{ fontSize:15, fontWeight:600, color:textColor, marginBottom:S.sm,
                                  paddingBottom:S.sm, borderBottom:`1px solid ${borderColor}` } }, 'Test E-mail Delivery'),
                h('p', { style:{ fontSize:13, color:mutedColor, marginTop:12, marginBottom:16 } },
                    'Sends a sample order confirmation through the live mail pipeline using the settings saved above \u2014 a green result means real order e-mails will deliver, and a red result shows the exact error (including the SMTP transport used). Save any changes first; the test reads the stored configuration, not unsaved edits.'),
                h('div', { style:{ display:'flex', gap:S.md, alignItems:'center', flexWrap:'wrap' } },
                    h('input', {
                        type:'email', value:testTo, placeholder:'you@example.com', autoComplete:'off',
                        onChange:function(e){ setTestTo(e.target.value); },
                        style:Object.assign({}, fs, { maxWidth:280 }),
                    }),
                    h(Btn, { variant:'primary', onClick:doTestEmail, disabled:testing, mutedColor:mutedColor, borderColor:borderColor },
                        testing ? 'Sending\u2026' : 'Send test email')
                ),
                tr && h('div', { style:{ marginTop:14, padding:'10px 14px', borderRadius:8, fontSize:13,
                        border:'1px solid '+(tr.success ? TC.success : TC.error),
                        background:(tr.success ? TC.success : TC.error) + '14',
                        color:(tr.success ? TC.success : TC.error) } },
                    (tr.success ? '\u2713 ' : '\u2715 ') + (tr.message || (tr.success ? 'Test e-mail sent.' : 'Test failed.'))
                )
            );
            rightContent = h('div', null, rightContent, emailTestCard);
        }

        return h('main', { style:{ flex:1, overflow:'auto', padding:S.xxl } },
            /* Header row: back + title + save */
            h('div', { style:{ display:'flex', justifyContent:'space-between', alignItems:'center', marginBottom:S.xl } },
                h('div', { style:{ display:'flex', alignItems:'center', gap:S.md } },
                    h('button', { onClick:function(){ navigate('settings'); },
                        style:{ background:'transparent', border:`1px solid ${borderColor}`, borderRadius:7, padding:'5px 12px', fontSize:12, cursor:'pointer', color:mutedColor, display:'flex', alignItems:'center', gap:4 } },
                        h('span', { style:{ fontSize:14 } }, '←'),
                        'Settings'
                    ),
                    h('h2', { style:{ fontSize:20, fontWeight:700, letterSpacing:'-.3px' } }, 'General Settings')
                ),
                h('div', { style:{ display:'flex', alignItems:'center', gap:S.md } },
                    saved && h('span', { style:{ fontSize:12, color:TC.success, fontWeight:500 } }, '✓ Saved'),
                    error && h('span', { style:{ fontSize:12, color:TC.error, fontWeight:500 } }, error),
                    h(Btn, { variant:'primary', onClick:handleSave, disabled:saving || loading, mutedColor:mutedColor, borderColor:borderColor },
                        saving ? 'Saving…' : 'Save Changes'
                    )
                )
            ),
            /* Two-column layout: left nav + right form inside a card */
            h(Card, { cardBg:cardBg, borderColor:borderColor, dark:dark, pad:0 },
                h('div', { style:{ display:'flex', minHeight:500 } },
                    /* Left nav */
                    h('div', { style:{ width:navWidth, flexShrink:0, padding:'20px 0 20px 16px', borderRight:`1px solid ${borderColor}` } },
                        SETTINGS_SUB_PAGES.map(function(sp) {
                            var active = sp.slug === activeSub;
                            return h('div', {
                                key: sp.slug,
                                onClick: function(){ navigate('settings/' + sp.slug); },
                                style:{
                                    padding:'9px 14px', borderRadius:8, marginBottom:2, cursor:'pointer', fontSize:13,
                                    fontWeight: active ? 600 : 400,
                                    color: active ? TC.accent : mutedColor,
                                    background: active ? (dark?'rgba(37,99,235,.12)':'rgba(37,99,235,.06)') : 'transparent',
                                    transition:'all .12s',
                                },
                                onMouseEnter: function(e){ if (!active) e.currentTarget.style.background = dark?'rgba(255,255,255,.03)':'rgba(0,0,0,.02)'; },
                                onMouseLeave: function(e){ if (!active) e.currentTarget.style.background = 'transparent'; },
                            }, sp.label);
                        })
                    ),
                    /* Right content */
                    h('div', { style:{ flex:1, padding:24, overflow:'auto' } },
                        rightContent
                    )
                )
            )
        );
    }

    /* ════════════════════════════════════════════════════════════════════
     * EXTENSIONS PAGE — Installed Extensions / Marketplace / Update Center
     * Three sidebar children route here via the `tab` prop.
     * ════════════════════════════════════════════════════════════════════ */

    /* ── Built-in extension icon/color map (frontend-only cosmetics) ──
     * S454: regrouped so the Lite-stripped slugs sit in one contiguous region
     * (object key order is behavior-neutral). A map row for an absent definitions
     * slug never renders, but absent beats dead — same principle as the def-* strips. */
    var EXT_ICONS = {
        'bestsellers':{ icon:'🏆', color:'#D97706' }, 'reviews':{ icon:'⭐', color:'#F59E0B' },
        'gdpr':{ icon:'🛡️', color:'#6366F1' },
        'recaptcha':{ icon:'🤖', color:'#4285F4' }, 'sitemap':{ icon:'🗺️', color:'#16A34A' },
        'maps':{ icon:'📍', color:'#EF4444' },
        'variations':{ icon:'🔀', color:'#14B8A6' },
        'seo':{ icon:'🔍', color:'#2563EB' },
        'price-per-unit':{ icon:'💲', color:'#0D9488' },
        'wishlist':{ icon:'❤️', color:'#E11D48' },
        'social-buttons':{ icon:'📢', color:'#0284C7' },
        'age-verification':{ icon:'🔞', color:'#B91C1C' },
    };


    /* ── License status helpers (module scope — never define component funcs inside ExtensionsPage) ──
       Status shape from LicenseManager:
         { valid, status:'active'|'expired'|'invalid'|'inactive'|'unknown',
           expires_at, activations, max_activations, product_name, checked_at, error, vendor_site? } */
    function tcLicensePillProps(status) {
        var s = (status && typeof status === 'object' && status.status) ? status.status : 'unknown';
        var map = {
            active:   { bg:TC.successBg, color:TC.success, label:'Active',   title:'License is valid and active.' },
            expired:  { bg:TC.errorBg,   color:TC.error,   label:'Expired',  title:'License has expired. Renew from the marketplace to continue receiving updates.' },
            invalid:  { bg:TC.errorBg,   color:TC.error,   label:'Invalid',  title:'License key is not recognized by the marketplace.' },
            inactive: { bg:'#F5F5F5',    color:TC.muted,   label:'Inactive', title:'License is not currently activated.' },
            unknown:  { bg:TC.amberBg,   color:TC.amber,   label:'Unknown',  title:'License status has not been checked yet. Click Refresh to verify with the marketplace.' }
        };
        return map[s] || map.unknown;
    }
    function tcLicenseExpirySeats(status) {
        if (!status || typeof status !== 'object') return '';
        var parts = [];
        if (status.expires_at) parts.push('Expires ' + tcFmtDate(status.expires_at));
        var max = Number(status.max_activations || 0);
        var act = Number(status.activations || 0);
        if (max > 0) {
            if (act > max) parts.push(act + ' of ' + max + ' seats — over limit');
            else           parts.push(act + ' of ' + max + ' seat' + (max === 1 ? '' : 's') + ' used');
        }
        /* max === 0 = unlimited or vendor-site — no seats line. */
        return parts.join(' · ');
    }
    function tcLicenseIsOverLimit(status) {
        if (!status) return false;
        var max = Number(status.max_activations || 0);
        var act = Number(status.activations || 0);
        return max > 0 && act > max;
    }

    /* ══════════════════════════════════════════════════════════════════════
       S356 — Pro catalog (distribution-model step 4, the in-app installer)

       Module scope, never inside ExtensionsPage: a component redefined on each
       render loses focus and remounts its subtree.
       ══════════════════════════════════════════════════════════════════════ */

    /* The marketplace returns product descriptions as HTML authored on another
       host. It is never injected — only reduced to text and used as a title
       attribute / single-line summary. */
    function tcCatText(html) {
        return String(html || '').replace(/<[^>]*>/g, ' ').replace(/&nbsp;/g, ' ')
            .replace(/&amp;/g, '&').replace(/&#8217;/g, '\u2019')
            .replace(/\s+/g, ' ').trim();
    }

    /* Semver-ish "is a > b", tolerant of nulls and ragged segment counts. */
    function tcVerGt(a, b) {
        if (!a || !b) return false;
        var pa = String(a).split('.'), pb = String(b).split('.');
        for (var i = 0; i < Math.max(pa.length, pb.length); i++) {
            var na = parseInt(pa[i] || '0', 10) || 0;
            var nb = parseInt(pb[i] || '0', 10) || 0;
            if (na > nb) return true;
            if (na < nb) return false;
        }
        return false;
    }

    /* Row state for one catalog entry joined against the installed plugin.

       ⚠️ INSTALLED-NESS IS TESTED BEFORE PUBLISHED-NESS, and the order is
       load-bearing. An extension can be installed and running on a store while
       the marketplace has not published a product row for it yet (live on
       demoxpert: titancart-tip-donation is published:false and active). Testing
       `published` first paints "Coming soon" over a running plugin. */
    function tcCatRowState(cat, inst) {
        if (inst) {
            if (inst.status !== 'active') return 'enable';
            if (cat.version && tcVerGt(cat.version, inst.version)) return 'update';
            return 'installed';
        }
        return cat.downloadable ? 'install' : 'coming_soon';
    }

    var TC_CAT_BTN = {
        install:     { label:'Install',   primary:true  },
        update:      { label:'Update',    primary:true  },
        enable:      { label:'Enable',    primary:true  },
        installed:   { label:'Installed', primary:false },
        coming_soon: { label:'Coming soon', primary:false }
    };

    function tcCatRow(props) {
        var cat    = props.cat;
        var inst   = props.inst;
        var busy   = props.busy;           /* slug currently mid-action, or null */
        var state  = tcCatRowState(cat, inst);
        var isBusy = busy === cat.slug;
        var meta   = TC_CAT_BTN[state];
        var dark   = props.dark;
        var name   = (cat.name && cat.name !== cat.slug) ? cat.name : cat.slug;
        var blurb  = tcCatText(cat.description);

        var actionable = (state === 'install' || state === 'update' || state === 'enable');
        var version    = inst ? inst.version : (cat.version || '');

        function btn(label, onClick, primary, danger) {
            return h('button', {
                onClick: (isBusy || !onClick) ? null : onClick,
                disabled: isBusy || !onClick,
                style:{ padding:'5px 14px', borderRadius:6, fontSize:12, fontWeight:600, whiteSpace:'nowrap',
                    border: primary ? 'none' : '1px solid ' + props.borderColor,
                    background: primary ? (danger ? TC.error : TC.accent) : 'transparent',
                    color: primary ? '#fff' : (danger ? TC.error : props.mutedColor),
                    cursor: (isBusy || !onClick) ? 'not-allowed' : 'pointer',
                    opacity: (isBusy || !onClick) ? 0.5 : 1 }
            }, label);
        }

        return h('div', { style:{ display:'grid', gridTemplateColumns:'1fr 90px 190px', alignItems:'center',
            padding:'14px 20px', borderBottom:'1px solid '+props.borderColor, gap:16 } },

            /* Name + one-line summary */
            h('div', { style:{ display:'flex', alignItems:'center', gap:14, minWidth:0 } },
                h('div', { style:{ width:36, height:36, borderRadius:9, flexShrink:0,
                    background: dark ? TC.accent+'18' : TC.accentBg,
                    display:'flex', alignItems:'center', justifyContent:'center',
                    fontSize:13, fontWeight:700, color:TC.accent } },
                    String(name).replace(/^TitanCart\s*/i, '').split(/[\s-]+/).map(function(w){ return w[0] || ''; }).slice(0,2).join('').toUpperCase()
                ),
                h('div', { style:{ minWidth:0 } },
                    h('div', { style:{ fontSize:13, fontWeight:600, color:props.textColor, marginBottom:2 } }, name),
                    h('div', { title: blurb, style:{ fontSize:11.5, color:props.mutedColor, lineHeight:1.4,
                        whiteSpace:'nowrap', overflow:'hidden', textOverflow:'ellipsis' } },
                        state === 'coming_soon' ? (cat.message || 'Not yet published on the marketplace.') : blurb)
                )
            ),

            /* Version */
            h('div', { style:{ fontSize:12, color:props.mutedColor, whiteSpace:'nowrap' } },
                version ? ('v' + version) : '—'),

            /* Actions */
            h('div', { style:{ display:'flex', gap:8, justifyContent:'flex-end', alignItems:'center' } },
                isBusy
                    ? h('span', { style:{ fontSize:12, color:props.mutedColor } }, 'Working…')
                    : h(Fragment, null,
                        state === 'installed'
                            ? h('span', { style:{ fontSize:12, color:TC.success, fontWeight:600 } }, '✓ Installed')
                            : btn(meta.label,
                                  state === 'install' ? function(){ props.onInstall(cat.slug); }
                                : state === 'update'  ? function(){ props.onUpdate(cat.slug); }
                                : state === 'enable'  ? function(){ props.onEnable(cat.slug); }
                                : null,
                                actionable && meta.primary, false),

                        /* Disable is only offered for a running extension; Remove
                           whenever it is on disk. Neither is offered for a row
                           that was never installed. */
                        inst && inst.status === 'active' && btn('Disable', function(){ props.onDisable(cat.slug); }, false, false),
                        inst && btn('Remove', function(){ props.onRemove(cat.slug); }, false, true)
                    )
            )
        );
    }

    function ExtensionsPage({ tab, dark, cardBg, textColor, mutedColor, borderColor, tableHeaderBg }) {
        var _filter    = useState('all');      var filter    = _filter[0];    var setFilter    = _filter[1];
        var _search    = useState('');         var search    = _search[0];    var setSearch    = _search[1];
        var _exts      = useState([]);         var exts      = _exts[0];     var setExts      = _exts[1];
        var _mkSearch  = useState('');         var mkSearch  = _mkSearch[0];  var setMkSearch  = _mkSearch[1];
        var _loading   = useState(true);       var loading   = _loading[0];  var setLoading   = _loading[1];
        var _toggling  = useState(null);       var toggling  = _toggling[0]; var setToggling  = _toggling[1];

        var fs = fieldSty(dark, borderColor);

        /* ── Load extensions from API ── */
        function loadExts() {
            fetch(Config.apiUrl + '/extensions', { headers:{ 'X-WP-Nonce': Config.nonce||'' } })
                .then(function(r){ return r.json(); })
                .then(function(data) {
                    var list = (Array.isArray(data) ? data : []).map(function(e) {
                        var cosmetics = EXT_ICONS[e.id] || { icon:'🔌', color:'#737373' };
                        return Object.assign({}, e, cosmetics);
                    });
                    setExts(list);
                    setLoading(false);
                })
                .catch(function(){ setLoading(false); });
        }
        useEffect(function(){ loadExts(); }, []);

        /* ── Load purchased (marketplace) extensions ── */
        var _purchased = useState([]); var purchased = _purchased[0]; var setPurchased = _purchased[1];
        var _licInputs = useState({}); var licInputs = _licInputs[0]; var setLicInputs = _licInputs[1];
        var _licSaving = useState(null); var licSaving = _licSaving[0]; var setLicSaving = _licSaving[1];
        var _licEditing = useState({}); var licEditing = _licEditing[0]; var setLicEditing = _licEditing[1];
        var _licRefreshing = useState(null); var licRefreshing = _licRefreshing[0]; var setLicRefreshing = _licRefreshing[1];
        var _licRemoving   = useState(null); var licRemoving   = _licRemoving[0];   var setLicRemoving   = _licRemoving[1];
        var _licConfirmRm  = useState(null); var licConfirmRm  = _licConfirmRm[0];  var setLicConfirmRm  = _licConfirmRm[1];
        var _licError      = useState({});   var licError      = _licError[0];     var setLicError      = _licError[1];

        /* ── Update check state ── */
        var _updates        = useState([]);    var updates        = _updates[0];        var setUpdates        = _updates[1];
        var _checkingUpdates = useState(false); var checkingUpdates = _checkingUpdates[0]; var setCheckingUpdates = _checkingUpdates[1];
        var _updatingSlug   = useState(null);  var updatingSlug   = _updatingSlug[0];   var setUpdatingSlug   = _updatingSlug[1];
        var _updateMsg      = useState(null);  var updateMsg      = _updateMsg[0];      var setUpdateMsg      = _updateMsg[1];

        /* ── Marketplace products (fetched from market.titancart.io) ── */
        var _mkProducts   = useState([]);    var mkProducts   = _mkProducts[0];   var setMkProducts   = _mkProducts[1];
        var _mkLoading    = useState(false); var mkLoading    = _mkLoading[0];    var setMkLoading    = _mkLoading[1];
        var _mkLoaded     = useState(false); var mkLoaded     = _mkLoaded[0];     var setMkLoaded     = _mkLoaded[1];

        /* ── S356: Pro entitlement catalog (in-app installer) ── */
        var _catalog    = useState(null);  var catalog    = _catalog[0];    var setCatalog    = _catalog[1];
        var _catLoading = useState(false); var catLoading = _catLoading[0]; var setCatLoading = _catLoading[1];
        var _catError   = useState(null);  var catError   = _catError[0];   var setCatError   = _catError[1];
        var _catBusy    = useState(null);  var catBusy    = _catBusy[0];    var setCatBusy    = _catBusy[1];
        var _catMsg     = useState(null);  var catMsg     = _catMsg[0];     var setCatMsg     = _catMsg[1];
        var _catConfirm = useState(null);  var catConfirm = _catConfirm[0]; var setCatConfirm = _catConfirm[1];

        function loadCatalog(force) {
            /* A Lite store has no catalog route at all — don't even ask. */
            if (!Config.isPro) return;
            setCatLoading(true);
            setCatError(null);
            fetch(Config.apiUrl + '/extensions/catalog' + (force ? '?force=1' : ''),
                  { headers:{ 'X-WP-Nonce': Config.nonce||'' } })
                .then(function(r){ return r.json().then(function(j){ return { ok:r.ok, body:j }; }); })
                .then(function(o){
                    setCatLoading(false);
                    if (o.ok && o.body && Array.isArray(o.body.extensions)) {
                        setCatalog(o.body);
                    } else {
                        /* Surface the marketplace's own verdict. "No subscription
                           key on this site" and "your subscription lapsed" need
                           different answers, and a vendor/trial store lands here
                           legitimately with tc_no_core_key. */
                        setCatError({
                            code: (o.body && o.body.error) || 'tc_catalog_error',
                            message: (o.body && o.body.message) || 'Could not load your subscription catalog.'
                        });
                    }
                })
                .catch(function(){
                    setCatLoading(false);
                    setCatError({ code:'tc_catalog_unreachable', message:'Could not reach the server.' });
                });
        }

        /* Reload the installed list after any action that changes the disk. */
        function refreshPurchased() {
            return fetch(Config.apiUrl + '/extensions/purchased', { headers:{ 'X-WP-Nonce': Config.nonce||'' } })
                .then(function(r){ return r.json(); })
                .then(function(d){ if (Array.isArray(d)) setPurchased(d); })
                .catch(function(){});
        }

        function catAction(slug, url, opts, successText) {
            setCatBusy(slug);
            setCatMsg(null);
            fetch(url, Object.assign({ headers:{ 'Content-Type':'application/json', 'X-WP-Nonce': Config.nonce||'' } }, opts))
                .then(function(r){ return r.json().then(function(j){ return { ok:r.ok, body:j }; }); })
                .then(function(o){
                    setCatBusy(null);
                    if (o.ok && o.body && o.body.success) {
                        setCatMsg({ type:'success', text:o.body.message || successText });
                        refreshPurchased();
                    } else {
                        setCatMsg({ type:'error', text:(o.body && o.body.message) || 'The action failed.' });
                    }
                })
                .catch(function(){
                    setCatBusy(null);
                    setCatMsg({ type:'error', text:'Could not reach the server.' });
                });
        }

        function catInstall(slug) {
            catAction(slug, Config.apiUrl + '/extensions/install/' + slug, { method:'POST' }, 'Installed.');
        }
        function catSetState(slug, active) {
            catAction(slug, Config.apiUrl + '/extensions/purchased/' + slug + '/state',
                { method:'POST', body: JSON.stringify({ active: !!active }) },
                active ? 'Enabled.' : 'Disabled.');
        }
        function catRemove(slug) {
            setCatConfirm(null);
            catAction(slug, Config.apiUrl + '/extensions/purchased/' + slug, { method:'DELETE' }, 'Removed.');
        }
        /* Same endpoint the Update Center uses, but reported through this
           section's own banner — doUpdate() writes to updateMsg, which is
           rendered on a tab the user is not currently looking at. */
        function catUpdate(slug) {
            catAction(slug, Config.apiUrl + '/extensions/update/' + slug, { method:'POST' }, 'Updated.');
        }

        useEffect(function(){ if (tab === 'extensions-marketplace') loadCatalog(false); }, [tab]);


        function checkForUpdates() {
            /* /extensions/check-updates is stripped from the Lite package. */
            if (_noPhoneHome) return;
            setCheckingUpdates(true);
            setUpdateMsg(null);
            fetch(Config.apiUrl + '/extensions/check-updates', { headers:{ 'X-WP-Nonce': Config.nonce||'' } })
                .then(function(r){
                    if (!r.ok) {
                        return r.text().then(function(t) {
                            throw new Error('HTTP ' + r.status + ': ' + t.substring(0, 200));
                        });
                    }
                    return r.json();
                })
                .then(function(data) {
                    setCheckingUpdates(false);
                    if (Array.isArray(data)) {
                        setUpdates(data);
                        var avail = data.filter(function(u){ return u.update_available; });
                        if (avail.length > 0) {
                            setUpdateMsg({ type:'success', text:avail.length + ' update' + (avail.length > 1 ? 's' : '') + ' available.' });
                        } else {
                            setUpdateMsg({ type:'success', text:'All extensions are up to date.' });
                        }
                    }
                })
                .catch(function(err){ setCheckingUpdates(false); setUpdateMsg({ type:'error', text:err && err.message ? err.message : 'Could not reach the server.' }); });
        }

        function doUpdate(slug) {
            setUpdatingSlug(slug);
            setUpdateMsg(null);
            fetch(Config.apiUrl + '/extensions/update/' + slug, {
                method: 'POST',
                headers: { 'Content-Type':'application/json', 'X-WP-Nonce': Config.nonce||'' }
            })
            .then(function(r){
                if (!r.ok) {
                    return r.text().then(function(t) {
                        try { var j = JSON.parse(t); throw new Error(j.message || 'HTTP ' + r.status); }
                        catch(e2) { if (e2.message) throw e2; throw new Error('HTTP ' + r.status + ': ' + t.substring(0, 200)); }
                    });
                }
                return r.json();
            })
            .then(function(data) {
                setUpdatingSlug(null);
                if (data.success) {
                    setUpdateMsg({ type:'success', text:data.message || 'Updated successfully.' });
                    /* Refresh purchased list to show new version */
                    fetch(Config.apiUrl + '/extensions/purchased', { headers:{ 'X-WP-Nonce': Config.nonce||'' } })
                        .then(function(r){ return r.json(); })
                        .then(function(d){ if (Array.isArray(d)) setPurchased(d); });
                    /* Remove this extension from updates list */
                    setUpdates(function(prev){ return prev.map(function(u){ return u.slug === slug ? Object.assign({}, u, { update_available:false, message:'Updated to ' + (data.new_version||'latest') }) : u; }); });
                } else {
                    setUpdateMsg({ type:'error', text:data.message || 'Update failed.' });
                }
            })
            .catch(function(){ setUpdatingSlug(null); setUpdateMsg({ type:'error', text:'Could not reach the server.' }); });
        }

        useEffect(function(){
            /* KI-36: /extensions/purchased is stripped from the Lite package.
               This effect runs on EVERY mount of ExtensionsPage — including the
               Installed Extensions tab, which Lite keeps — so hiding the two
               phoneHome tabs would not have prevented this 404. Guard the fetch,
               not the tab. */
            if (_noPhoneHome) return;
            fetch(Config.apiUrl + '/extensions/purchased', { headers:{ 'X-WP-Nonce': Config.nonce||'' } })
                .then(function(r){ return r.json(); })
                .then(function(data){ if (Array.isArray(data)) setPurchased(data); })
                .catch(function(){});
        }, []);

        function saveLicense(slug) {
            var key = (licInputs[slug] || '').trim();
            if (!key) return;
            setLicSaving(slug);
            setLicError(function(p){ var n=Object.assign({},p); delete n[slug]; return n; });
            fetch(Config.apiUrl + '/extensions/purchased/' + slug + '/license', {
                method: 'PUT',
                headers: { 'Content-Type':'application/json', 'X-WP-Nonce': Config.nonce||'' },
                body: JSON.stringify({ license_key: key })
            })
            .then(function(r){ return r.json(); })
            .then(function(data) {
                setLicSaving(null);
                if (data.success) {
                    setPurchased(function(prev) {
                        return prev.map(function(p) {
                            return p.slug === slug ? Object.assign({}, p, { license_key: data.license_key, license_status: data.status || null }) : p;
                        });
                    });
                    setLicInputs(function(prev) { var n = Object.assign({}, prev); delete n[slug]; return n; });
                    setLicEditing(function(prev) { var n = Object.assign({}, prev); delete n[slug]; return n; });
                } else {
                    /* Marketplace rejection — surface verbatim so the customer sees the real reason. */
                    setLicError(function(p){ var n=Object.assign({},p); n[slug] = data.message || data.error || 'License activation failed.'; return n; });
                }
            })
            .catch(function(){
                setLicSaving(null);
                /* Transport failure — phrased to distinguish from a marketplace rejection. */
                setLicError(function(p){ var n=Object.assign({},p); n[slug] = 'Could not reach the marketplace. Check your connection and try again.'; return n; });
            });
        }

        function refreshLicenseStatus(slug) {
            setLicRefreshing(slug);
            setLicError(function(p){ var n=Object.assign({},p); delete n[slug]; return n; });
            fetch(Config.apiUrl + '/extensions/purchased/' + slug + '/license/status', {
                headers: { 'X-WP-Nonce': Config.nonce||'' }
            })
            .then(function(r){
                if (!r.ok) {
                    return r.text().then(function(t) {
                        try { var j = JSON.parse(t); throw new Error(j.message || 'HTTP ' + r.status); }
                        catch(e2) { if (e2.message) throw e2; throw new Error('HTTP ' + r.status); }
                    });
                }
                return r.json();
            })
            .then(function(data) {
                setLicRefreshing(null);
                if (data && data.success && data.status) {
                    setPurchased(function(prev) {
                        return prev.map(function(p) {
                            return p.slug === slug ? Object.assign({}, p, { license_status: data.status }) : p;
                        });
                    });
                } else {
                    setLicError(function(p){ var n=Object.assign({},p); n[slug] = (data && data.message) || 'Could not refresh status.'; return n; });
                }
            })
            .catch(function(){
                setLicRefreshing(null);
                setLicError(function(p){ var n=Object.assign({},p); n[slug] = 'Could not reach the marketplace. Try again.'; return n; });
            });
        }

        function removeLicense(slug) {
            setLicRemoving(slug);
            setLicConfirmRm(null);
            setLicError(function(p){ var n=Object.assign({},p); delete n[slug]; return n; });
            fetch(Config.apiUrl + '/extensions/purchased/' + slug + '/license', {
                method: 'DELETE',
                headers: { 'X-WP-Nonce': Config.nonce||'' }
            })
            .then(function(r){ return r.json().catch(function(){ return { success:false, message:'Invalid response.' }; }); })
            .then(function(data) {
                setLicRemoving(null);
                /* DELETE is fail-open locally — mirror LicenseManager::deactivate and clear local state
                   regardless of whether the marketplace round-trip succeeded. */
                setPurchased(function(prev) {
                    return prev.map(function(p) {
                        return p.slug === slug ? Object.assign({}, p, { license_key:'', license_status:null }) : p;
                    });
                });
                setLicInputs(function(prev) { var n = Object.assign({}, prev); delete n[slug]; return n; });
                setLicEditing(function(prev) { var n = Object.assign({}, prev); delete n[slug]; return n; });
                if (data && !data.success) {
                    setLicError(function(p){ var n=Object.assign({},p); n[slug] = 'Removed locally. Marketplace note: ' + (data.message || data.error || 'deactivation could not be confirmed'); return n; });
                }
            })
            .catch(function(){
                setLicRemoving(null);
                /* Transport failure — still clear locally, warn the seat may linger until heartbeat. */
                setPurchased(function(prev) {
                    return prev.map(function(p) {
                        return p.slug === slug ? Object.assign({}, p, { license_key:'', license_status:null }) : p;
                    });
                });
                setLicError(function(p){ var n=Object.assign({},p); n[slug] = 'Removed locally. Could not reach the marketplace — the seat may still count there until the next heartbeat.'; return n; });
            });
        }

        /* ── Toggle extension status via API ── */
        function toggleExt(id) {
            var ext = exts.find(function(e){ return e.id === id; });
            if (!ext || toggling) return;
            var newStatus = ext.status === 'active' ? 'disabled' : 'active';
            setToggling(id);

            /* Optimistic UI update */
            _extMap[id] = newStatus;
            if (window._tcBumpExtVer) window._tcBumpExtVer();
            setExts(function(prev) {
                return prev.map(function(e) {
                    if (e.id !== id) return e;
                    return Object.assign({}, e, { status: newStatus });
                });
            });

            fetch(Config.apiUrl + '/extensions/' + id, {
                method: 'PUT',
                headers: { 'Content-Type':'application/json', 'X-WP-Nonce': Config.nonce||'' },
                body: JSON.stringify({ status: newStatus })
            })
            .then(function(r){ return r.json(); })
            .then(function(data) {
                setToggling(null);
                if (!data.success) {
                    /* Revert on failure */
                    _extMap[id] = ext.status;
                    if (window._tcBumpExtVer) window._tcBumpExtVer();
                    setExts(function(prev) {
                        return prev.map(function(e) {
                            if (e.id !== id) return e;
                            return Object.assign({}, e, { status: ext.status });
                        });
                    });
                }
            })
            .catch(function() {
                setToggling(null);
                /* Revert */
                _extMap[id] = ext.status;
                if (window._tcBumpExtVer) window._tcBumpExtVer();
                setExts(function(prev) {
                    return prev.map(function(e) {
                        if (e.id !== id) return e;
                        return Object.assign({}, e, { status: ext.status });
                    });
                });
            });
        }

        /* ── Filter logic ── */
        function filteredExts() {
            var list = exts;
            if (filter === 'active')   list = list.filter(function(e){ return e.status === 'active'; });
            if (filter === 'disabled') list = list.filter(function(e){ return e.status === 'disabled'; });
            if (search) {
                var q = search.toLowerCase();
                list = list.filter(function(e){ return e.name.toLowerCase().indexOf(q) !== -1 || e.desc.toLowerCase().indexOf(q) !== -1; });
            }
            return list;
        }

        /* ── Tab nav ──
         * The two phoneHome tabs vanish in the Lite package (KI-36): their REST
         * routes are stripped, and the marketplace tab additionally fetches
         * market.titancart.io from the browser, which readme.txt promises Lite
         * never does. Same `phoneHome` key + same predicate as the sidebar. */
        var TABS = [
            { id:'installed-extensions', label:'Installed Extensions' },
            { id:'updates',              label:'Update Center', phoneHome:true },
            { id:'extensions-marketplace',label:'Extensions Marketplace', phoneHome:true },
        ].filter(navVisible);
        function tabSty(id) {
            var active = tab === id;
            return { padding:'12px 20px', border:'none', cursor:'pointer', fontSize:13, fontWeight:active?600:400,
                     background:'transparent', borderBottom:active?'2px solid '+TC.accent:'2px solid transparent',
                     color:active?TC.accent:mutedColor, marginBottom:-1, whiteSpace:'nowrap', transition:'color .15s' };
        }

        /* ── Toggle switch component ── */
        function Toggle(props) {
            var on = props.on;
            return h('div', { onClick: props.onToggle,
                style:{ width:40, height:22, borderRadius:11, cursor:'pointer', opacity:1,
                background:on?(dark?'rgba(37,99,235,.8)':TC.accent):(dark?'#333':'#D4D4D8'),
                position:'relative', transition:'background .2s', flexShrink:0 } },
                h('div', { style:{ width:16, height:16, borderRadius:8, background:'#fff',
                    position:'absolute', top:3, left:on?21:3, transition:'left .2s',
                    boxShadow:'0 1px 3px rgba(0,0,0,.2)' } })
            );
        }

        /* ── Extension row for installed list ── */
        function ExtRow(props) {
            var ext = props.ext;
            var isActive = ext.status === 'active';
            var proUpsell = false, lockActivate = false;
            return h('div', { style:{ display:'flex', alignItems:'center', gap:16, padding:'16px 20px',
                borderBottom:'1px solid '+borderColor, transition:'background .1s' },
                onMouseEnter:function(e){ e.currentTarget.style.background = dark?'rgba(255,255,255,.02)':'#FAFAFA'; },
                onMouseLeave:function(e){ e.currentTarget.style.background = 'transparent'; }
            },
                /* Initials badge */
                h('div', { style:{ width:40, height:40, borderRadius:10, flexShrink:0,
                    background:dark?ext.color+'18':ext.color+'12', color:ext.color,
                    display:'flex', alignItems:'center', justifyContent:'center',
                    fontSize:18, lineHeight:1 } }, ext.icon),
                /* Name + desc */
                h('div', { style:{ flex:1, minWidth:0 } },
                    h('div', { style:{ display:'flex', alignItems:'center', gap:8, marginBottom:3 } },
                        h('span', { style:{ fontSize:14, fontWeight:600, color:textColor } }, ext.name),
                        h('span', { style:{ fontSize:11, color:mutedColor, fontWeight:400 } }, 'v'+ext.version),
                    ),
                    h('div', { style:{ fontSize:12, color:mutedColor, lineHeight:1.5, whiteSpace:'nowrap', overflow:'hidden', textOverflow:'ellipsis', maxWidth:500 } }, ext.desc)
                ),
                /* Status badge */
                h(Badge, { status:isActive?'active':'inactive', dark:dark }, isActive?'Active':'Disabled'),
                /* Toggle */
                h(Toggle, { on:isActive, locked:lockActivate, onToggle:function(){ toggleExt(ext.id); } }),
                /* Settings gear — only if extension has settings */
                ext.has_settings && h('button', { onClick:function(){
                    navigate('ext-settings/' + ext.id);
                },
                    style:{ background:'transparent', border:'1px solid '+borderColor, borderRadius:7,
                            padding:'6px 8px', cursor:'pointer', color:mutedColor, display:'flex',
                            transition:'opacity .15s' },
                    title:'Settings',
                    onMouseEnter:function(e){ e.currentTarget.style.opacity='1'; },
                    onMouseLeave:function(e){ e.currentTarget.style.opacity='0.6'; } },
                    ico([h('circle',{cx:10,cy:10,r:2}),h('path',{d:'M10 2.5v2M10 15.5v2M4 4l1.4 1.4M14.6 14.6l1.4 1.4M2.5 10h2M15.5 10h2M4 16l1.4-1.4M14.6 5.4L16 4'})], 16, '0 0 20 20')
                ),
                !ext.has_settings && h('div', { style:{ width:36 } }) /* spacer */
            );
        }

        /* ── Marketplace row ── */
        function MkRow(props) {
            var ext = props.ext;
            return h('div', { style:{ display:'grid', gridTemplateColumns:'1fr 80px 140px 64px', alignItems:'center',
                padding:'14px 20px', borderBottom:'1px solid '+borderColor, transition:'background .1s', gap:16 },
                onMouseEnter:function(e){ e.currentTarget.style.background = dark?'rgba(255,255,255,.02)':'#FAFAFA'; },
                onMouseLeave:function(e){ e.currentTarget.style.background = 'transparent'; }
            },
                /* Name + desc */
                h('div', { style:{ display:'flex', alignItems:'center', gap:14, minWidth:0 } },
                    h('div', { style:{ width:36, height:36, borderRadius:9, flexShrink:0,
                        background:dark?TC.accent+'18':TC.accentBg,
                        display:'flex', alignItems:'center', justifyContent:'center',
                        fontSize:13, fontWeight:700, color:TC.accent } },
                        ext.name.split(' ').map(function(w){ return w[0]; }).slice(0,2).join('')
                    ),
                    h('div', { style:{ minWidth:0 } },
                        h('div', { style:{ fontSize:13, fontWeight:600, color:textColor, marginBottom:2 } }, ext.name),
                        h('div', { style:{ fontSize:11.5, color:mutedColor, lineHeight:1.4, whiteSpace:'nowrap', overflow:'hidden', textOverflow:'ellipsis' } }, ext.desc)
                    )
                ),
                /* Developer */
                h('div', { style:{ fontSize:12, color:mutedColor } }, ext.developer),
                /* Price */
                h('div', { style:{ textAlign:'right', whiteSpace:'nowrap' } },
                    ext.priceLabel
                        ? h('span', { style:{ fontSize:13, fontWeight:600, color:textColor } }, ext.priceLabel)
                        : ext.salePrice
                            ? h(Fragment, null,
                                h('span', { style:{ fontSize:12, color:mutedColor, textDecoration:'line-through', marginRight:4 } }, '$'+ext.price),
                                h('span', { style:{ fontSize:14, fontWeight:700, color:TC.success } }, '$'+ext.salePrice)
                            )
                            : h('span', { style:{ fontSize:14, fontWeight:600, color:textColor } }, '$'+ext.price)
                ),
                /* Buy */
                h('div', { style:{ textAlign:'right' } },
                    h('button', { onClick:function(){
                        window.open(ext.url || ('https://market.titancart.io/product/'+ext.id+'/'), '_blank');
                    }, style:{ padding:'5px 14px', borderRadius:6, border:'none', fontSize:12, fontWeight:600,
                        background:TC.accent, color:'#fff', cursor:'pointer', whiteSpace:'nowrap' } }, 'Buy')
                )
            );
        }

        /* ═══════════════════════ RENDER ═══════════════════════ */
        return h('main', { style:{ flex:1, overflow:'auto', padding:0, display:'flex', flexDirection:'column' } },

            /* ── Header ── */
            h('div', { style:{ padding:'20px 24px 0', display:'flex', justifyContent:'space-between', alignItems:'center' } },
                h('div', null,
                    h('h2', { style:{ fontSize:20, fontWeight:600, letterSpacing:'-.3px', marginBottom:2 } }, 'Extensions'),
                    h('div', { style:{ fontSize:14, color:mutedColor } }, 'Manage built-in and purchased extensions')
                ),
                tab === 'extensions-marketplace' && h('button', { onClick:function(){ window.open('https://market.titancart.io/shop/', '_blank'); },
                    style:{ padding:'8px 16px', borderRadius:8, border:'none', fontSize:13, fontWeight:500,
                        background:TC.accent, color:'#fff', cursor:'pointer', display:'flex', alignItems:'center', gap:6 } },
                    'Visit TitanCart Store',
                    h('span', { style:{ fontSize:11 } }, '↗')
                )
            ),

            /* ── Tab bar ── */
            h('div', { style:{ padding:'0 24px', display:'flex', gap:0, borderBottom:'1px solid '+borderColor, marginTop:16 } },
                TABS.map(function(t) {
                    return h('button', { key:t.id, onClick:function(){ navigate(t.id); }, style:tabSty(t.id) }, t.label);
                })
            ),

            /* ── Tab content ── */
            h('div', { style:{ padding:24, flex:1 } },

                /* ═══════ INSTALLED EXTENSIONS TAB ═══════ */
                tab === 'installed-extensions' && loading && h(Spinner, { mutedColor:mutedColor }),
                tab === 'installed-extensions' && !loading && h(Fragment, null,

                    /* Toolbar: filter pills + search */
                    h('div', { style:{ display:'flex', justifyContent:'space-between', alignItems:'center', marginBottom:16 } },
                        h('div', { style:{ display:'flex', gap:6 } },
                            ['all','active','disabled'].map(function(f) {
                                var active = filter === f;
                                var label  = f === 'all' ? 'All ('+exts.length+')' : f === 'active' ? 'Active ('+exts.filter(function(e){return e.status==='active';}).length+')' : 'Disabled ('+exts.filter(function(e){return e.status==='disabled';}).length+')';
                                return h('button', { key:f, onClick:function(){ setFilter(f); },
                                    style:{ padding:'6px 14px', borderRadius:99, fontSize:12, fontWeight:active?600:400, cursor:'pointer',
                                        background:active?TC.accent:'transparent', color:active?'#fff':mutedColor,
                                        border:active?'none':'1px solid '+borderColor, transition:'all .15s' } }, label);
                            })
                        ),
                        h('div', { style:{ position:'relative' } },
                            h('input', { value:search, onChange:function(e){ setSearch(e.target.value); },
                                placeholder:'Search extensions…',
                                style:Object.assign({}, fs, { width:240, paddingLeft:32, fontSize:13 }) }),
                            h('span', { style:{ position:'absolute', left:10, top:'50%', transform:'translateY(-50%)', color:mutedColor, pointerEvents:'none' } },
                                ico([h('circle',{cx:8.5,cy:8.5,r:5}),h('path',{d:'M12.5 12.5L16 16'})], 14, '0 0 18 18')
                            )
                        )
                    ),

                    /* ── Built-in extensions card ── */
                    h(Card, { cardBg:cardBg, borderColor:borderColor, dark:dark, pad:0 },
                        h('div', { style:{ padding:'16px 20px', borderBottom:'1px solid '+borderColor, display:'flex', justifyContent:'space-between', alignItems:'center' } },
                            h('div', null,
                                h('div', { style:{ fontSize:14, fontWeight:600, color:textColor } }, 'Built-in Extensions'),
                                h('div', { style:{ fontSize:12, color:mutedColor, marginTop:2 } }, 'Ship with TitanCart — always available, enable or disable as needed')
                            ),
                            h('span', { style:{ fontSize:12, color:mutedColor, background:dark?'#262626':'#F5F5F5', padding:'4px 10px', borderRadius:6, fontWeight:500 } },
                                filteredExts().length + ' extension' + (filteredExts().length !== 1 ? 's' : ''))
                        ),
                        /* Table header */
                        h('div', { style:{ display:'flex', alignItems:'center', gap:16, padding:'10px 20px',
                            borderBottom:'1px solid '+borderColor, background:tableHeaderBg, boxShadow:tableHeaderShadow } },
                            h('div', { style:{ width:40 } }),
                            h('div', { style:{ flex:1, fontSize:11, fontWeight:500, color:mutedColor, textTransform:'uppercase', letterSpacing:'.04em' } }, 'Extension'),
                            h('div', { style:{ width:80, fontSize:11, fontWeight:500, color:mutedColor, textTransform:'uppercase', letterSpacing:'.04em', textAlign:'center' } }, 'Status'),
                            h('div', { style:{ width:40, textAlign:'center', fontSize:11, fontWeight:500, color:mutedColor, textTransform:'uppercase', letterSpacing:'.04em' } }, ''),
                            h('div', { style:{ width:36 } })
                        ),
                        /* Rows */
                        filteredExts().length > 0
                            ? filteredExts().map(function(ext) { return h(ExtRow, { key:ext.id, ext:ext }); })
                            : h(EmptyState, { mutedColor:mutedColor, icon:'🔌', title:'No extensions found', sub:'Try adjusting your search or filter.' })
                    ),

                    /* ── Purchased extensions (dynamic) ──
                       Hidden on Lite: /extensions/purchased does not exist in
                       that package, so this card could only ever render its
                       empty state pointing at a marketplace Lite cannot reach. */
                    !_noPhoneHome && h(Card, { cardBg:cardBg, borderColor:borderColor, dark:dark, pad:0, style:{ marginTop:20 } },
                        h('div', { style:{ padding:'16px 20px', borderBottom:'1px solid '+borderColor } },
                            h('div', { style:{ fontSize:14, fontWeight:600, color:textColor } }, 'Purchased Extensions'),
                            h('div', { style:{ fontSize:12, color:mutedColor, marginTop:2 } }, 'Extensions purchased from the TitanCart Marketplace')
                        ),
                        purchased.length === 0
                            ? h(EmptyState, { mutedColor:mutedColor, icon:'🛒', title:'No purchased extensions',
                                sub:'Visit the Extensions Marketplace to browse and purchase additional extensions.' })
                            : h('div', null, purchased.map(function(pe) {
                                var statusBg = pe.status === 'active' ? (dark?'rgba(22,163,74,.1)':TC.successBg) : (dark?'rgba(113,121,126,.1)':'#F5F5F5');
                                var statusColor = pe.status === 'active' ? TC.success : mutedColor;
                                return h('div', { key:pe.id, style:{ padding:'16px 20px', borderBottom:'1px solid '+borderColor, display:'flex', alignItems:'center', gap:14 } },
                                    h('div', { style:{ width:40, height:40, borderRadius:10, background:dark?'#262A36':'#F0F2F5',
                                        display:'flex', alignItems:'center', justifyContent:'center', fontSize:18, flexShrink:0 } }, '🧩'),
                                    h('div', { style:{ flex:1, minWidth:0 } },
                                        h('div', { style:{ display:'flex', alignItems:'center', gap:8, marginBottom:3 } },
                                            h('span', { style:{ fontSize:13, fontWeight:600, color:textColor } }, pe.name),
                                            h('span', { style:{ fontSize:11, color:TC.accent, fontWeight:500 } }, 'v' + pe.version)
                                        ),
                                        h('div', { style:{ fontSize:12, color:mutedColor, overflow:'hidden', textOverflow:'ellipsis', whiteSpace:'nowrap' } }, pe.description)
                                    ),
                                    h('span', { style:{ fontSize:11, fontWeight:500, padding:'3px 10px', borderRadius:99,
                                        background:statusBg, color:statusColor, flexShrink:0 },
                                        title: pe.status === 'active' ? 'This plugin is enabled in WordPress' : 'This plugin is installed but not enabled in WordPress' },
                                        pe.status === 'active' ? 'Enabled' : 'Disabled'),
                                    /* S436: a Pro-bundled slug with NO stored key is entitled via the
                                       core subscription ("Pro is the key", S354) — render the pill,
                                       never a key prompt. A bundled slug WITH a stored key (pre-flip
                                       buyer) keeps the full keyed UI below so the seat can still be
                                       managed/freed (KI-48: the per-extension key path stays intact). */
                                    pe.pro_bundled && !pe.license_key
                                        ? h('div', { style:{ display:'flex', alignItems:'center', flexShrink:0 } },
                                            h('span', { style:{ fontSize:11, fontWeight:500, padding:'3px 10px', borderRadius:99,
                                                background: dark ? TC.accent+'18' : TC.accentBg, color:TC.accent, whiteSpace:'nowrap' },
                                                title:'Entitlement comes from your TitanCart Pro subscription — no separate license key is needed for this extension.' },
                                                'Included in Pro')
                                          )
                                        : pe.license_key && !licEditing[pe.slug]
                                        ? h('div', { style:{ display:'flex', flexDirection:'column', alignItems:'flex-end', gap:4, flexShrink:0 } },
                                            h('div', { style:{ display:'flex', gap:6, alignItems:'center', flexWrap:'wrap', justifyContent:'flex-end' } },
                                                /* License status pill (from license_status.status) */
                                                (function(){
                                                    var pp = tcLicensePillProps(pe.license_status);
                                                    return h('span', { style:{ fontSize:11, fontWeight:500, padding:'3px 10px', borderRadius:99, background:pp.bg, color:pp.color, whiteSpace:'nowrap' }, title: pp.title }, pp.label);
                                                })(),
                                                /* Expiry + seats meta — shown only when non-empty */
                                                tcLicenseExpirySeats(pe.license_status)
                                                    ? h('span', { style:{ fontSize:11, color: tcLicenseIsOverLimit(pe.license_status) ? TC.error : mutedColor, whiteSpace:'nowrap' } }, tcLicenseExpirySeats(pe.license_status))
                                                    : null,
                                                /* Action buttons — either [Refresh][Remove][Change] or [Yes, remove][Cancel] when confirming */
                                                licConfirmRm === pe.slug
                                                    ? h(Fragment, null,
                                                        h('span', { style:{ fontSize:11, color:mutedColor, marginRight:4 } }, 'Remove license?'),
                                                        h('button', { onClick:function(){ removeLicense(pe.slug); }, disabled:licRemoving === pe.slug,
                                                            style:{ fontSize:11, padding:'3px 10px', borderRadius:4, border:'none',
                                                                background:TC.error, color:'#fff', cursor:'pointer', fontWeight:500 } },
                                                            licRemoving === pe.slug ? 'Removing…' : 'Yes, remove'),
                                                        h('button', { onClick:function(){ setLicConfirmRm(null); },
                                                            style:{ fontSize:11, padding:'3px 8px', borderRadius:4, border:'1px solid '+borderColor,
                                                                background:'transparent', color:mutedColor, cursor:'pointer' } }, 'Cancel')
                                                      )
                                                    : h(Fragment, null,
                                                        h('button', { onClick:function(){ refreshLicenseStatus(pe.slug); }, disabled:licRefreshing === pe.slug, title:'Re-check this license with the marketplace',
                                                            style:{ fontSize:11, padding:'3px 8px', borderRadius:4, border:'1px solid '+borderColor,
                                                                background:'transparent', color:mutedColor, cursor: (licRefreshing === pe.slug ? 'wait' : 'pointer') } },
                                                            licRefreshing === pe.slug ? 'Refreshing…' : 'Refresh'),
                                                        h('button', { onClick:function(){ setLicConfirmRm(pe.slug); }, title:'Remove this license and free the seat on the marketplace',
                                                            style:{ fontSize:11, padding:'3px 8px', borderRadius:4, border:'1px solid '+borderColor,
                                                                background:'transparent', color:TC.error, cursor:'pointer' } }, 'Remove'),
                                                        h('button', { onClick:function(){ setLicEditing(function(p){ var n=Object.assign({},p); n[pe.slug]=true; return n; }); setLicInputs(function(p){ var n=Object.assign({},p); n[pe.slug]=pe.license_key; return n; }); },
                                                            style:{ fontSize:11, padding:'3px 8px', borderRadius:4, border:'1px solid '+borderColor,
                                                                background:'transparent', color:mutedColor, cursor:'pointer' } }, 'Change')
                                                      )
                                            ),
                                            /* Error row — marketplace rejection OR transport failure, distinguished in the copy from the handlers */
                                            licError[pe.slug] && h('div', { style:{ fontSize:11, color:TC.error, textAlign:'right', maxWidth:360, lineHeight:1.4, marginTop:2 } }, licError[pe.slug])
                                          )
                                        : h('div', { style:{ display:'flex', flexDirection:'column', alignItems:'flex-end', gap:4, flexShrink:0 } },
                                            h('div', { style:{ display:'flex', gap:6, alignItems:'center' } },
                                                h('input', { type:'text', placeholder:'Enter license key',
                                                    value: licInputs[pe.slug] || '',
                                                    onChange: function(e) { var v=e.target.value; setLicInputs(function(p){ var n=Object.assign({},p); n[pe.slug]=v; return n; }); },
                                                    onKeyDown: function(e) { if (e.key === 'Enter') saveLicense(pe.slug); },
                                                    style:{ fontSize:12, padding:'5px 10px', borderRadius:6, border:'1px solid '+borderColor,
                                                        background:dark?'#1E2130':TC.lightBg, color:textColor, width:180,
                                                        outline:'none', fontFamily:'monospace', letterSpacing:'.5px' } }),
                                                h('button', { onClick:function(){ saveLicense(pe.slug); },
                                                    disabled: licSaving === pe.slug,
                                                    style:{ fontSize:12, fontWeight:500, padding:'5px 12px', borderRadius:6, border:'none',
                                                        background:TC.accent, color:'#fff', cursor:'pointer', whiteSpace:'nowrap' } },
                                                    licSaving === pe.slug ? 'Saving…' : 'Activate'),
                                                licEditing[pe.slug] && h('button', { onClick:function(){
                                                        setLicEditing(function(p){ var n=Object.assign({},p); delete n[pe.slug]; return n; });
                                                        setLicError(function(p){ var n=Object.assign({},p); delete n[pe.slug]; return n; });
                                                    },
                                                    style:{ fontSize:12, padding:'5px 10px', borderRadius:6, border:'1px solid '+borderColor,
                                                        background:'transparent', color:mutedColor, cursor:'pointer' } }, 'Cancel')
                                            ),
                                            /* Error row in the input state too (activation rejections) */
                                            licError[pe.slug] && h('div', { style:{ fontSize:11, color:TC.error, textAlign:'right', maxWidth:360, lineHeight:1.4, marginTop:2 } }, licError[pe.slug])
                                          )
                                );
                            }))
                    )
                ),

                /* ═══════ MARKETPLACE TAB ═══════ */
                tab === 'extensions-marketplace' && h(Fragment, null,

                    /* ── S356: Included in your subscription ──
                       Hidden entirely on Lite: there is no catalog route in that
                       build, and nothing here to upsell that the Buy list below
                       does not already say. */
                    !!Config.isPro && h(Fragment, null,

                        catMsg && h('div', { style:{ padding:'10px 16px', borderRadius:8, marginBottom:16, fontSize:13, fontWeight:500,
                            background: catMsg.type === 'success' ? (dark?'rgba(22,163,74,.1)':TC.successBg) : (dark?'rgba(220,38,38,.1)':'#FEF2F2'),
                            color: catMsg.type === 'success' ? TC.success : TC.error,
                            display:'flex', alignItems:'center', gap:8 } },
                            h('span', null, catMsg.type === 'success' ? '✓' : '⚠'),
                            catMsg.text
                        ),

                        h(Card, { cardBg:cardBg, borderColor:borderColor, dark:dark, pad:0, style:{ marginBottom:24 } },
                            h('div', { style:{ padding:'16px 20px', borderBottom:'1px solid '+borderColor,
                                display:'flex', justifyContent:'space-between', alignItems:'center', gap:16 } },
                                h('div', null,
                                    h('div', { style:{ fontSize:14, fontWeight:600, color:textColor } }, 'Included in your subscription'),
                                    h('div', { style:{ fontSize:12, color:mutedColor, marginTop:2 } },
                                        catalog
                                            ? (catalog.available + ' of ' + catalog.count + ' available to install \u00b7 no separate license key needed')
                                            : 'Extensions bundled with TitanCart Pro')
                                ),
                                h('button', { onClick:function(){ loadCatalog(true); }, disabled: catLoading,
                                    style:{ padding:'6px 14px', borderRadius:6, border:'1px solid '+borderColor, fontSize:12, fontWeight:500,
                                        background:'transparent', color: catLoading ? mutedColor : TC.accent,
                                        cursor: catLoading ? 'default' : 'pointer', whiteSpace:'nowrap' } },
                                    catLoading ? 'Refreshing…' : 'Refresh')
                            ),

                            catLoading && !catalog
                                ? h(Spinner, { mutedColor:mutedColor })
                                : catError
                                    ? h('div', { style:{ padding:'20px' } },
                                        h('div', { style:{ fontSize:13, color:textColor, marginBottom:6, fontWeight:600 } },
                                            catError.code === 'tc_no_core_key' ? 'No subscription key on this site' : 'Catalog unavailable'),
                                        h('div', { style:{ fontSize:12.5, color:mutedColor, lineHeight:1.6, maxWidth:640 } }, catError.message),
                                        catError.code === 'tc_no_core_key' && h('button', { onClick:function(){ navigate('settings/account'); },
                                            style:{ marginTop:12, padding:'6px 14px', borderRadius:6, border:'none', fontSize:12, fontWeight:600,
                                                background:TC.accent, color:'#fff', cursor:'pointer' } }, 'Add your license key')
                                      )
                                    : catalog && catalog.extensions.length
                                        ? h('div', null, catalog.extensions.map(function(cat) {
                                            var inst = null;
                                            for (var i = 0; i < purchased.length; i++) {
                                                if (purchased[i].slug === cat.slug) { inst = purchased[i]; break; }
                                            }
                                            if (catConfirm === cat.slug) {
                                                return h('div', { key:cat.slug, style:{ padding:'14px 20px', borderBottom:'1px solid '+borderColor,
                                                    display:'flex', alignItems:'center', justifyContent:'space-between', gap:16,
                                                    background: dark?'rgba(220,38,38,.06)':'#FEF2F2' } },
                                                    h('div', { style:{ fontSize:12.5, color:textColor, lineHeight:1.5 } },
                                                        h('strong', null, 'Remove ' + (cat.name || cat.slug) + '?'),
                                                        h('div', { style:{ color:mutedColor, marginTop:2 } },
                                                            'Its files are deleted from this site. Settings stored in the database are kept, and you can reinstall it at any time.')
                                                    ),
                                                    h('div', { style:{ display:'flex', gap:8, flexShrink:0 } },
                                                        h('button', { onClick:function(){ setCatConfirm(null); },
                                                            style:{ padding:'5px 14px', borderRadius:6, border:'1px solid '+borderColor, fontSize:12,
                                                                background:'transparent', color:mutedColor, cursor:'pointer' } }, 'Cancel'),
                                                        h('button', { onClick:function(){ catRemove(cat.slug); },
                                                            style:{ padding:'5px 14px', borderRadius:6, border:'none', fontSize:12, fontWeight:600,
                                                                background:TC.error, color:'#fff', cursor:'pointer' } }, 'Remove')
                                                    )
                                                );
                                            }
                                            return h(tcCatRow, {
                                                key: cat.slug, cat: cat, inst: inst, busy: catBusy,
                                                dark: dark, textColor: textColor, mutedColor: mutedColor, borderColor: borderColor,
                                                onInstall: catInstall,
                                                onUpdate:  catUpdate,
                                                onEnable:  function(s){ catSetState(s, true); },
                                                onDisable: function(s){ catSetState(s, false); },
                                                onRemove:  function(s){ setCatConfirm(s); }
                                            });
                                        }))
                                        : h(EmptyState, { mutedColor:mutedColor, icon:'📦', title:'Nothing to show yet',
                                            sub:'Your subscription catalog is empty.' })
                        )
                    ),

                    /* Search bar */
                    h('div', { style:{ marginBottom:16, position:'relative' } },
                        h('input', { value:mkSearch, onChange:function(e){ setMkSearch(e.target.value); },
                            placeholder:'Search the marketplace for extensions…',
                            style:Object.assign({}, fs, { width:'100%', maxWidth:420, paddingLeft:34, fontSize:13 }) }),
                        h('span', { style:{ position:'absolute', left:10, top:'50%', transform:'translateY(-50%)', color:mutedColor, pointerEvents:'none' } },
                            ico([h('circle',{cx:8.5,cy:8.5,r:5}),h('path',{d:'M12.5 12.5L16 16'})], 14, '0 0 18 18')
                        )
                    ),

                    /* Marketplace card */
                    h(Card, { cardBg:cardBg, borderColor:borderColor, dark:dark, pad:0 },
                        /* Header */
                        h('div', { style:{ display:'grid', gridTemplateColumns:'1fr 80px 140px 64px', padding:'12px 20px',
                            borderBottom:'1px solid '+borderColor, background:tableHeaderBg, gap:16 } },
                            h('div', { style:{ fontSize:11, fontWeight:500, color:mutedColor, textTransform:'uppercase', letterSpacing:'.04em' } }, 'Name'),
                            h('div', { style:{ fontSize:11, fontWeight:500, color:mutedColor, textTransform:'uppercase', letterSpacing:'.04em' } }, 'Developer'),
                            h('div', { style:{ fontSize:11, fontWeight:500, color:mutedColor, textTransform:'uppercase', letterSpacing:'.04em', textAlign:'right' } }, 'Price'),
                            h('div', null)
                        ),
                        /* Rows */
                        mkLoading
                            ? h(Spinner, { mutedColor:mutedColor })
                            : (function() {
                                var list = mkProducts;
                                /* Don't sell a subscriber what their subscription
                                   already includes.

                                   Join on `product_slug`, NOT `slug`. The Buy list
                                   is keyed by the STOREFRONT product slug
                                   ("multi-vendor-marketplace"); the catalog is keyed
                                   by the PLUGIN slug ("titancart-marketplace"). Two
                                   different identifiers for one thing, and only the
                                   marketplace server holds both — which is why it
                                   publishes the join key rather than the client
                                   guessing a second spelling of the first (KI-42).

                                   Anything the catalog does not list — Printful, any
                                   future third-party add-on — still shows Buy. The
                                   add-on exception surfaces here by ABSENCE, with no
                                   exclusion list to drift. */
                                if (catalog && Array.isArray(catalog.extensions)) {
                                    var owned = {};
                                    catalog.extensions.forEach(function(c){
                                        if (c.product_slug) owned[c.product_slug] = true;
                                    });
                                    list = list.filter(function(e){ return !owned[e.id]; });
                                }
                                if (mkSearch) {
                                    var q = mkSearch.toLowerCase();
                                    list = list.filter(function(e){ return e.name.toLowerCase().indexOf(q) !== -1 || e.desc.toLowerCase().indexOf(q) !== -1; });
                                }
                                return list.length > 0
                                    ? list.map(function(ext){ return h(MkRow, { key:ext.id, ext:ext }); })
                                    : h(EmptyState, { mutedColor:mutedColor, icon: mkLoaded ? '🔍' : '⚠', title: mkLoaded ? 'No results' : 'Could not load marketplace', sub: mkLoaded ? 'Try different search terms.' : 'Check your internet connection and try again.' });
                            })(),
                        /* Footer */
                        h('div', { style:{ padding:'14px 20px', borderTop:'1px solid '+borderColor, display:'flex', justifyContent:'space-between', alignItems:'center' } },
                            h('span', { style:{ fontSize:12, color:mutedColor } }, (function(){
                                /* Count what is actually rendered below, not what
                                   the marketplace returned — the entitled ones are
                                   filtered out above, on product_slug. */
                                var n = mkProducts.length;
                                if (catalog && Array.isArray(catalog.extensions)) {
                                    var owned = {};
                                    catalog.extensions.forEach(function(c){
                                        if (c.product_slug) owned[c.product_slug] = true;
                                    });
                                    n = mkProducts.filter(function(e){ return !owned[e.id]; }).length;
                                }
                                return n + ' extension' + (n !== 1 ? 's' : '') + ' available';
                            })()),
                            h('button', { onClick:function(){ window.open('https://market.titancart.io/shop/', '_blank'); },
                                style:{ padding:'6px 14px', borderRadius:6, border:'1px solid '+borderColor, fontSize:12, fontWeight:500,
                                    background:'transparent', color:TC.accent, cursor:'pointer' } },
                                'View more extensions →')
                        )
                    )
                ),

                /* ═══════ UPDATE CENTER TAB ═══════ */
                tab === 'updates' && h(Fragment, null,

                    /* Status message banner */
                    updateMsg && h('div', { style:{ padding:'10px 16px', borderRadius:8, marginBottom:16, fontSize:13, fontWeight:500,
                        background: updateMsg.type === 'success' ? (dark?'rgba(22,163,74,.1)':TC.successBg) : (dark?'rgba(220,38,38,.1)':'#FEF2F2'),
                        color: updateMsg.type === 'success' ? TC.success : TC.error,
                        display:'flex', alignItems:'center', gap:8 } },
                        h('span', null, updateMsg.type === 'success' ? '✓' : '⚠'),
                        updateMsg.text
                    ),

                    /* Current version card */
                    h(Card, { cardBg:cardBg, borderColor:borderColor, dark:dark, style:{ marginBottom:20 } },
                        h('div', { style:{ display:'flex', justifyContent:'space-between', alignItems:'center' } },
                            h('div', { style:{ display:'flex', alignItems:'center', gap:16 } },
                                h('div', { style:{ width:48, height:48, borderRadius:12,
                                    background:'linear-gradient(135deg, '+TC.accent+', '+TC.accentLight+')',
                                    display:'flex', alignItems:'center', justifyContent:'center',
                                    color:'#fff', fontWeight:700, fontSize:18, flexShrink:0 } }, 'TC'),
                                h('div', null,
                                    h('div', { style:{ fontSize:16, fontWeight:700, color:textColor } }, 'TitanCart'),
                                    h('div', { style:{ fontSize:13, color:mutedColor, marginTop:2 } }, 'Version ' + (Config.version || '1.0.0')),
                                    h('div', { style:{ display:'flex', alignItems:'center', gap:6, marginTop:6 } },
                                        h('div', { style:{ width:8, height:8, borderRadius:4, background:TC.success } }),
                                        h('span', { style:{ fontSize:12, color:TC.success, fontWeight:500 } }, 'Up to date')
                                    )
                                )
                            ),
                            h('button', { onClick:checkForUpdates, disabled:checkingUpdates,
                                style:{ padding:'8px 18px', borderRadius:8, border:'1px solid '+borderColor, fontSize:13, fontWeight:500,
                                    background:'transparent', color:checkingUpdates?mutedColor:textColor, cursor:checkingUpdates?'default':'pointer',
                                    display:'flex', alignItems:'center', gap:6, opacity:checkingUpdates?0.7:1, transition:'all .15s' } },
                                checkingUpdates
                                    ? h(Fragment, null, '⟳ ', 'Checking…')
                                    : h(Fragment, null,
                                        ico([h('path',{d:'M1 4v6h6'}),h('path',{d:'M3.5 15a8 8 0 1 0 .5-4'})], 14, '0 0 18 18'),
                                        'Check for Updates')
                            )
                        )
                    ),

                    /* Extension updates list */
                    h(Card, { cardBg:cardBg, borderColor:borderColor, dark:dark, pad:0 },
                        h('div', { style:{ padding:'16px 20px', borderBottom:'1px solid '+borderColor } },
                            h('div', { style:{ fontSize:14, fontWeight:600, color:textColor } }, 'Extension Updates'),
                            h('div', { style:{ fontSize:12, color:mutedColor, marginTop:2 } }, 'Available updates for installed extensions')
                        ),
                        (function() {
                            var available = updates.filter(function(u){ return u.update_available; });
                            var errors    = updates.filter(function(u){ return !u.update_available && u.error; });

                            if (updates.length === 0) {
                                return h(EmptyState, { mutedColor:mutedColor, icon:'🔍', title:'No update data yet',
                                    sub:'Click "Check for Updates" above to scan the marketplace.' });
                            }

                            if (available.length === 0 && errors.length === 0) {
                                return h(EmptyState, { mutedColor:mutedColor, icon:'✅', title:'Everything is up to date',
                                    sub:'All your extensions are running the latest version.' });
                            }

                            return h('div', null,
                                /* Available updates */
                                available.map(function(u) {
                                    var isUpdating = updatingSlug === u.slug;
                                    return h('div', { key:u.slug, style:{ padding:'16px 20px', borderBottom:'1px solid '+borderColor,
                                        display:'flex', alignItems:'center', gap:14 } },
                                        h('div', { style:{ width:40, height:40, borderRadius:10, background:dark?TC.accent+'18':'#EEF4FE',
                                            display:'flex', alignItems:'center', justifyContent:'center', fontSize:18, flexShrink:0, color:TC.accent } }, '🔄'),
                                        h('div', { style:{ flex:1, minWidth:0 } },
                                            h('div', { style:{ display:'flex', alignItems:'center', gap:8, marginBottom:3 } },
                                                h('span', { style:{ fontSize:13, fontWeight:600, color:textColor } }, u.name),
                                                h('span', { style:{ fontSize:11, color:mutedColor } }, u.current_version + ' → '),
                                                h('span', { style:{ fontSize:11, color:TC.accent, fontWeight:600 } }, u.new_version)
                                            ),
                                            u.changelog && h('div', { style:{ fontSize:12, color:mutedColor, marginTop:2,
                                                overflow:'hidden', textOverflow:'ellipsis', whiteSpace:'nowrap', maxWidth:400 } }, u.changelog)
                                        ),
                                        h('button', { onClick:function(){ doUpdate(u.slug); }, disabled:isUpdating || !!updatingSlug,
                                            style:{ padding:'7px 16px', borderRadius:7, border:'none', fontSize:12, fontWeight:600,
                                                background:isUpdating?mutedColor:TC.accent, color:'#fff', cursor:isUpdating?'default':'pointer',
                                                display:'flex', alignItems:'center', gap:6, whiteSpace:'nowrap',
                                                opacity:(!!updatingSlug && !isUpdating)?0.5:1, transition:'all .15s' } },
                                            isUpdating ? h(Fragment, null, '⟳ ', 'Updating…') : 'Update Now')
                                    );
                                }),
                                /* Errors / warnings */
                                errors.map(function(u) {
                                    var errMsg = u.message || u.error || 'Unknown error';
                                    var isLicense = u.error === 'no_license' || u.error === 'forbidden' || u.error === 'invalid' || u.error === 'expired';
                                    return h('div', { key:u.slug, style:{ padding:'14px 20px', borderBottom:'1px solid '+borderColor,
                                        display:'flex', alignItems:'center', gap:14 } },
                                        h('div', { style:{ width:40, height:40, borderRadius:10,
                                            background:dark?TC.amber+'18':'#FEF6EC',
                                            display:'flex', alignItems:'center', justifyContent:'center', fontSize:16, flexShrink:0 } }, '⚠'),
                                        h('div', { style:{ flex:1, minWidth:0 } },
                                            h('div', { style:{ fontSize:13, fontWeight:500, color:textColor } }, u.name),
                                            h('div', { style:{ fontSize:12, color:TC.amber, marginTop:2 } }, errMsg)
                                        ),
                                        isLicense && h('button', { onClick:function(){ navigate('installed-extensions'); },
                                            style:{ padding:'6px 12px', borderRadius:6, border:'1px solid '+borderColor, fontSize:12, fontWeight:500,
                                                background:'transparent', color:TC.accent, cursor:'pointer', whiteSpace:'nowrap' } }, 'Enter License')
                                    );
                                })
                            );
                        })()
                    ),

                    /* Update history card */
                    h(Card, { cardBg:cardBg, borderColor:borderColor, dark:dark, pad:0, style:{ marginTop:20 } },
                        h('div', { style:{ padding:'16px 20px', borderBottom:'1px solid '+borderColor } },
                            h('div', { style:{ fontSize:14, fontWeight:600, color:textColor } }, 'Update History'),
                            h('div', { style:{ fontSize:12, color:mutedColor, marginTop:2 } }, 'Recent updates applied to your store')
                        ),
                        h('div', { style:{ padding:'14px 20px' } },
                            [
                                { name:'TitanCart Core', from:'0.9.0', to:'1.0.0', date:'Mar 20, 2026' },
                            ].map(function(u, i) {
                                return h('div', { key:i, style:{ display:'flex', alignItems:'center', gap:12, padding:'10px 0',
                                    borderBottom:i < 0 ? '1px solid '+borderColor : 'none' } },
                                    h('div', { style:{ width:32, height:32, borderRadius:8, background:dark?TC.success+'18':TC.successBg,
                                        display:'flex', alignItems:'center', justifyContent:'center', flexShrink:0, color:TC.success, fontSize:14 } }, '✓'),
                                    h('div', { style:{ flex:1 } },
                                        h('div', { style:{ fontSize:13, fontWeight:500, color:textColor } }, u.name),
                                        h('div', { style:{ fontSize:12, color:mutedColor, marginTop:1 } }, u.from + ' → ' + u.to)
                                    ),
                                    h('span', { style:{ fontSize:12, color:mutedColor } }, u.date)
                                );
                            })
                        )
                    )
                )
            )
        );
    }

    /* ════════════════════════════════════════════════════════════════════
     * EXTENSION SETTINGS PAGE — Schema-driven settings for a single extension
     * Route: #/ext-settings/{ext_id}
     * Reuses the same schema-driven field rendering as SettingsSubPage.
     * ════════════════════════════════════════════════════════════════════ */

    function ExtensionSettingsPage({ subId, dark, cardBg, textColor, mutedColor, borderColor, tableHeaderBg }) {
        var extId = subId || '';
        var _schema  = useState(null);    var schema  = _schema[0];  var setSchema  = _schema[1];
        var _values  = useState({});      var values  = _values[0];  var setValues  = _values[1];
        var _title   = useState('');      var title   = _title[0];   var setTitle   = _title[1];
        var _loading = useState(true);    var loading = _loading[0]; var setLoading = _loading[1];
        var _saving  = useState(false);   var saving  = _saving[0];  var setSaving  = _saving[1];
        var _saved   = useState(false);   var saved   = _saved[0];   var setSaved   = _saved[1];
        var _error   = useState('');      var error   = _error[0];   var setError   = _error[1];

        // S240 — Newsletter SMTP test (only used when extId === 'newsletters').
        // Hoisted to top of component per the focus-safe nested-component rule
        // (#106): if these states lived inside a nested component definition,
        // typing in the recipient input would lose focus on every keystroke.
        var _testTo     = useState('');    var testTo     = _testTo[0];     var setTestTo     = _testTo[1];
        var _testing    = useState(false); var testing    = _testing[0];    var setTesting    = _testing[1];
        var _testResult = useState(null);  var testResult = _testResult[0]; var setTestResult = _testResult[1];

        var fs = fieldSty(dark, borderColor);
        var selSty = Object.assign({}, fs, { cursor:'pointer' });

        /* ── Fetch schema + values ── */
        useEffect(function() {
            if (!extId) return;
            setLoading(true); setSaved(false); setError('');
            fetch(Config.apiUrl + '/extensions/' + extId + '/settings', { headers:{ 'X-WP-Nonce':Config.nonce||'' } })
                .then(function(r){ return r.json(); })
                .then(function(data) {
                    if (data && data.schema) {
                        setSchema(data.schema);
                        setTitle(data.schema.title || extId);
                        setValues(data.values || {});
                    } else {
                        setSchema(null);
                        setError(data.message || 'No settings available.');
                    }
                    setLoading(false);
                })
                .catch(function(){ setError('Failed to load settings.'); setLoading(false); });
        }, [extId]);

        function onChange(key, val) {
            setValues(function(prev) {
                var next = {};
                for (var k in prev) next[k] = prev[k];
                next[key] = val;
                return next;
            });
            setSaved(false);
        }

        function handleSave() {
            // S240 / 1C — save-time validation for newsletters override:
            // toggle=ON + host blank is a misconfiguration. Catch it here
            // rather than letting it silently fall through to General →
            // Emails at send time (per NewsletterMailer apply_override's
            // L113-117 guard). Cleaner UX: error caught at config time.
            if (extId === 'newsletters'
                && (values.mailer_use_override === '1' || values.mailer_use_override === 1 || values.mailer_use_override === true)
                && !(values.mailer_smtp_host || '').toString().trim()) {
                setError("SMTP host is required when \u201CUse custom SMTP for newsletters\u201D is on. Fill in the host below, or turn the toggle off to use the General \u2192 Emails configuration.");
                return;
            }

            setSaving(true); setSaved(false); setError('');
            fetch(Config.apiUrl + '/extensions/' + extId + '/settings', {
                method:'PUT',
                headers:{ 'Content-Type':'application/json', 'X-WP-Nonce':Config.nonce||'' },
                body: JSON.stringify(values),
            })
            .then(function(r){ return r.json(); })
            .then(function(data) {
                if (data && data.success) { setSaved(true); setTimeout(function(){ setSaved(false); }, 3000); }
                else { setError(data.message || 'Save failed.'); }
                setSaving(false);
            })
            .catch(function(){ setError('Network error.'); setSaving(false); });
        }

        // S240 — Newsletter SMTP test. Hits POST /newsletters/test-smtp
        // which routes through NewsletterMailer::send() (the same pipeline
        // a real dispatch uses), so a green test means real sends will
        // succeed and a red test exposes the exact error a real send would
        // have failed with.
        function doTestSmtp() {
            if (!testTo || testing) return;
            setTesting(true); setTestResult(null);
            fetch(Config.apiUrl + '/newsletters/test-smtp', {
                method: 'POST',
                credentials: 'include',
                headers: { 'Content-Type': 'application/json', 'X-WP-Nonce': Config.nonce || '' },
                body: JSON.stringify({ to: testTo })
            })
            .then(function(r){ return r.json().then(function(d){ return { ok: r.ok, body: d }; }); })
            .then(function(wrap) {
                setTesting(false);
                setTestResult(wrap.body);
            })
            .catch(function(e) {
                setTesting(false);
                setTestResult({ success: false, message: e.message || 'Network error.', transport: 'n/a' });
            });
        }

        /* ── Field renderer (identical to SettingsSubPage) ── */
        function renderField(field) {
            var val = values[field.key] != null ? values[field.key] : (field.default || '');

            if (field.type === 'checkbox') {
                var isOn = val === '1' || val === 1 || val === true;
                return h('label', { style:{ display:'flex', alignItems:'center', gap:10, cursor:'pointer' } },
                    h('div', {
                        onClick:function(){ onChange(field.key, isOn ? '0' : '1'); },
                        style:{ width:40, height:22, borderRadius:12, cursor:'pointer',
                            background:isOn?TC.accent:(dark?'#333':'#D4D4D4'),
                            transition:'background .15s', position:'relative', flexShrink:0 }
                    },
                        h('div', { style:{ width:18, height:18, borderRadius:'50%', background:'#fff',
                            position:'absolute', top:2, left:isOn?20:2, transition:'left .15s',
                            boxShadow:'0 1px 3px rgba(0,0,0,.2)' } })
                    ),
                    h('span', { style:{ fontSize:13, color:textColor } }, field.label)
                );
            }

            if (field.type === 'select') {
                return h('select', { value:val, onChange:function(e){ onChange(field.key, e.target.value); }, style:selSty },
                    (field.options||[]).map(function(opt){ return h('option', { key:opt.value, value:opt.value }, opt.label); })
                );
            }

            if (field.type === 'textarea') {
                return h('textarea', { value:val, onChange:function(e){ onChange(field.key, e.target.value); },
                    rows:3, style:Object.assign({}, fs, { resize:'vertical', minHeight:72 }) });
            }

            if (field.type === 'color') {
                var cval = /^#[0-9a-fA-F]{6}$/.test(val) ? val : (field.default || '#000000');
                return h('div', { style:{ display:'flex', alignItems:'center', gap:10 } },
                    h('input', { type:'color', value:cval,
                        onChange:function(e){ onChange(field.key, e.target.value); },
                        style:{ width:46, height:34, padding:0, border:'1px solid '+(dark?'#404040':'#D4D4D4'), borderRadius:8, background:'transparent', cursor:'pointer' } }),
                    h('input', { type:'text', value:val, placeholder:'#RRGGBB',
                        onChange:function(e){ onChange(field.key, e.target.value); },
                        style:Object.assign({}, fs, { maxWidth:120, fontFamily:'monospace' }) })
                );
            }

            var inputType = field.type === 'number' ? 'number' : (field.type === 'password' ? 'password' : 'text');
            return h('input', { type:inputType, value:val,
                autoComplete: field.type === 'password' ? 'new-password' : undefined,
                placeholder: field.type === 'password' ? 'Leave blank to keep current' : undefined,
                onChange:function(e){ onChange(field.key, e.target.value); },
                style:Object.assign({}, fs, { maxWidth:field.type==='number'?180:'none' }) });
        }

        /* ── Cosmetics ── */
        var cosmetics = EXT_ICONS[extId] || { icon:'🔌', color:'#737373' };

        /* ── Render ── */
        var content;
        if (loading) {
            content = h(Spinner, { mutedColor:mutedColor });
        } else if (!schema) {
            content = h(Card, { cardBg:cardBg, borderColor:borderColor, dark:dark },
                h(EmptyState, { mutedColor:mutedColor, icon:'⚙️', title:'No Settings', sub:error || 'This extension has no configurable settings.' })
            );
        } else {
            content = h(Card, { cardBg:cardBg, borderColor:borderColor, dark:dark },
                schema.groups.map(function(group) {
                    var useGrid = group.fields.length >= 3;
                    return h('div', { key:group.id, style:{ marginBottom:24 } },
                        h('h3', { style:{ fontSize:15, fontWeight:600, color:textColor, marginBottom:16,
                                          paddingBottom:8, borderBottom:'1px solid '+borderColor } }, group.title),
                        h('div', { style:{ display:'grid', gridTemplateColumns:useGrid?'1fr 1fr':'1fr', gap:'16px 24px' } },
                            group.fields.map(function(field) {
                                var spanFull = field.type === 'textarea';
                                var baseWrap = { display:'flex', flexDirection:'column', gap:5 };
                                var wrapStyle = spanFull && useGrid ? Object.assign({}, baseWrap, { gridColumn:'1 / -1' }) : baseWrap;
                                if (field.type === 'checkbox') {
                                    return h('div', { key:field.key, style:wrapStyle },
                                        renderField(field),
                                        field.hint && h('div', { style:{ fontSize:12, color:mutedColor, marginLeft:50, marginTop:2 } }, field.hint)
                                    );
                                }
                                return h('div', { key:field.key, style:wrapStyle },
                                    h('label', { style:{ fontSize:13, fontWeight:500, color:textColor } }, field.label),
                                    renderField(field),
                                    field.hint && h('div', { style:{ fontSize:12, color:mutedColor, marginTop:2 } }, field.hint)
                                );
                            })
                        )
                    );
                })
            );
        }

        return h('main', { style:{ flex:1, overflow:'auto', padding:24 } },
            /* Header */
            h('div', { style:{ display:'flex', justifyContent:'space-between', alignItems:'center', marginBottom:24 } },
                h('div', { style:{ display:'flex', alignItems:'center', gap:12 } },
                    h('button', { onClick:function(){ navigate('installed-extensions'); },
                        style:{ background:'transparent', border:'1px solid '+borderColor, borderRadius:7, padding:'5px 12px', fontSize:12, cursor:'pointer', color:mutedColor, display:'flex', alignItems:'center', gap:4 } },
                        h('span', { style:{ fontSize:14 } }, '\u2190'), 'Extensions'
                    ),
                    h('div', { style:{ width:36, height:36, borderRadius:9, background:dark?cosmetics.color+'18':cosmetics.color+'12',
                        color:cosmetics.color, display:'flex', alignItems:'center', justifyContent:'center', fontSize:18, flexShrink:0 } }, cosmetics.icon),
                    h('div', null,
                        h('h2', { style:{ fontSize:20, fontWeight:600, letterSpacing:'-.3px', marginBottom:1 } }, title + ' Settings'),
                        h('div', { style:{ fontSize:13, color:mutedColor } }, 'Configure this extension')
                    )
                ),
                !loading && schema && h('div', { style:{ display:'flex', alignItems:'center', gap:12 } },
                    saved && h('span', { style:{ fontSize:12, color:TC.success, fontWeight:500 } }, '\u2713 Saved'),
                    error && h('span', { style:{ fontSize:12, color:TC.error, fontWeight:500 } }, error),
                    h(Btn, { variant:'primary', onClick:handleSave, disabled:saving||loading, mutedColor:mutedColor, borderColor:borderColor },
                        saving ? 'Saving\u2026' : 'Save Changes')
                )
            ),
            content,

            // S240 — Newsletter SMTP test card (only renders for the newsletters
            // extension; uses POST /newsletters/test-smtp which routes through
            // the SAME NewsletterMailer pipeline a real dispatch uses).
            extId === 'newsletters' && !loading && schema && h('div', { style:{ marginTop:24 } },
                h(Card, { cardBg:cardBg, borderColor:borderColor, dark:dark },
                    h('h3', { style:{ fontSize:15, fontWeight:600, color:textColor, marginBottom:6,
                                      paddingBottom:8, borderBottom:'1px solid '+borderColor } }, 'Test SMTP Connection'),
                    h('p', { style:{ fontSize:13, color:mutedColor, marginTop:12, marginBottom:16 } },
                        'Sends a one-off test email through the exact mailer pipeline a real newsletter dispatch uses. ' +
                        ((values.mailer_use_override === '1' || values.mailer_use_override === 1 || values.mailer_use_override === true)
                            ? ((values.mailer_smtp_host || '').toString().trim()
                                ? 'Override is on with a host configured \u2014 this will test those credentials.'
                                : 'Override is on but the host is blank \u2014 this will silently fall through to General \u2192 Emails.')
                            : 'Override is off \u2014 this will test the General \u2192 Emails configuration.')
                    ),

                    h('div', { style:{ display:'flex', alignItems:'flex-start', gap:12, flexWrap:'wrap' } },
                        h('div', { style:{ flex:1, minWidth:240 } },
                            h('label', { style:{ fontSize:12, fontWeight:500, color:mutedColor, display:'block', marginBottom:6 } }, 'Send test to'),
                            h('input', {
                                type:'email',
                                value:testTo,
                                placeholder:'you@example.com',
                                onChange:function(e){ setTestTo(e.target.value); setTestResult(null); },
                                style:fs
                            })
                        ),
                        h('div', { style:{ alignSelf:'flex-end' } },
                            h(Btn, {
                                variant:'primary',
                                onClick:doTestSmtp,
                                disabled:testing || !testTo,
                                mutedColor:mutedColor, borderColor:borderColor
                            }, testing ? 'Testing\u2026' : '\u26A1 Send Test')
                        )
                    ),

                    // Result chip + detail block
                    testResult && h('div', { style:{ marginTop:16 } },
                        h('div', {
                            style:{
                                display:'inline-block', padding:'8px 14px', borderRadius:6, fontSize:13, fontWeight:500,
                                background: testResult.success ? (dark?'rgba(22,163,74,.1)':'#F0FDF4') : (dark?'rgba(220,38,38,.1)':'#FEF2F2'),
                                color: testResult.success ? TC.success : TC.error,
                            }
                        }, (testResult.success ? '\u2713 ' : '\u2717 ') + (testResult.message || (testResult.success ? 'Sent' : 'Failed'))),

                        // Transport / error detail block
                        h('div', {
                            style:{
                                marginTop:12, padding:'12px 16px', borderRadius:8,
                                background:dark?'rgba(255,255,255,.02)':'#FAFBFC', border:'1px solid '+borderColor,
                                fontSize:12, color:mutedColor, lineHeight:1.6
                            }
                        },
                            h('div', null,
                                'Transport: ',
                                h('strong', { style:{ color:textColor } },
                                    testResult.transport === 'override' ? 'Override (newsletter-specific SMTP)' :
                                    testResult.transport === 'override-fallback' ? 'Override fallback \u2014 host was blank, used General \u2192 Emails' :
                                    testResult.transport === 'general' ? 'General \u2192 Emails' :
                                    (testResult.transport || 'unknown')
                                )
                            ),
                            testResult.host && h('div', { style:{ marginTop:4 } },
                                'Host: ', h('strong', { style:{ color:textColor } }, testResult.host + (testResult.port ? ':' + testResult.port : ''))
                            ),
                            !testResult.success && testResult.error && h('div', { style:{ marginTop:8, color:TC.error, fontFamily:'ui-monospace,Menlo,Monaco,Consolas,monospace', fontSize:11, wordBreak:'break-word' } },
                                testResult.error
                            )
                        )
                    )
                )
            )
        );
    }

    /* ═══════════════════════════════════════════════════════════════════
     *  WOO IMPORT PAGE
     * ═══════════════════════════════════════════════════════════════════ */

    function WooImportPage({ dark, cardBg, textColor, mutedColor, borderColor, backBtn }) {
        var _s = useState(null),  detection = _s[0], setDetection = _s[1];
        var _l = useState(true),  loading   = _l[0], setLoading   = _l[1];
        var _r = useState(false), running   = _r[0], setRunning   = _r[1];
        var _p = useState(null),  progress  = _p[0], setProgress  = _p[1];
        var _c = useState({ categories:true, tags:true, products:true, customers:true, orders:true, reviews:true, coupons:true }),
            checked = _c[0], setChecked = _c[1];
        var _d = useState(false), done = _d[0], setDone = _d[1];

        var pollRef = useRef(null);

        /* Detect WooCommerce on mount */
        useEffect(function() {
            apiFetch('/woo-import/detect').then(function(r) {
                setDetection(r);
                setLoading(false);
            }).catch(function() { setLoading(false); });
            return function() { if (pollRef.current) clearInterval(pollRef.current); };
        }, []);

        /* Toggle entity checkbox */
        function toggle(key) {
            var next = Object.assign({}, checked);
            next[key] = !next[key];
            setChecked(next);
        }

        /* Start import */
        function startImport() {
            var entities = Object.keys(checked).filter(function(k) { return checked[k] && detection && detection.counts[k] > 0; });
            if (!entities.length) return;
            setRunning(true);
            setDone(false);
            setProgress({ status:'running', current:'', done:0, total:0, log:[], errors:[] });

            /* Poll progress while running */
            pollRef.current = setInterval(function() {
                apiFetch('/woo-import/progress').then(function(p) {
                    if (p) setProgress(p);
                    if (p && p.status === 'complete') {
                        clearInterval(pollRef.current);
                        pollRef.current = null;
                        setRunning(false);
                        setDone(true);
                    }
                });
            }, 1500);

            /* Fire the import — this is long-running */
            apiFetch('/woo-import/run', { method:'POST', body:JSON.stringify({ entities:entities }), headers:{'Content-Type':'application/json'} })
                .then(function(r) {
                    setProgress(r);
                    setRunning(false);
                    setDone(true);
                    if (pollRef.current) { clearInterval(pollRef.current); pollRef.current = null; }
                })
                .catch(function(err) {
                    setRunning(false);
                    setDone(true);
                    if (pollRef.current) { clearInterval(pollRef.current); pollRef.current = null; }
                    setProgress(function(prev) { return Object.assign({}, prev, { status:'error', errors:[(prev && prev.errors || []).concat('Request failed: ' + (err.message || err))] }); });
                });
        }

        var accent = '#2563EB';
        var successGreen = '#16A34A';
        var chipBg = dark ? 'rgba(37,99,235,.08)' : 'rgba(37,99,235,.06)';

        /* Entity display info */
        var entityInfo = {
            categories: { label:'Categories', icon:'📁' },
            tags:       { label:'Tags',       icon:'🏷️' },
            products:   { label:'Products',   icon:'📦' },
            customers:  { label:'Customers',  icon:'👤' },
            orders:     { label:'Orders',     icon:'🛒' },
            reviews:    { label:'Reviews',    icon:'⭐' },
            coupons:    { label:'Coupons',    icon:'🎟️' },
        };

        var selectedCount = Object.keys(checked).filter(function(k) { return checked[k] && detection && detection.counts[k] > 0; }).length;

        return h(PageWrap, { title:'WooCommerce Migration', sub:'Import your WooCommerce store data into TitanCart', mutedColor:mutedColor, backBtn:backBtn },

            /* Loading state */
            loading && h(Card, { cardBg:cardBg, borderColor:borderColor, dark:dark },
                h('div', { style:{ display:'flex', alignItems:'center', gap:12, padding:'8px 0' } },
                    h(Spinner, null),
                    h('span', { style:{ color:mutedColor } }, 'Scanning for WooCommerce data...')
                )
            ),

            /* No WooCommerce found */
            !loading && detection && !detection.wc_active && !detection.counts.products && h(Card, { cardBg:cardBg, borderColor:borderColor, dark:dark },
                h(EmptyState, { mutedColor:mutedColor, icon:'🔌', title:'WooCommerce Not Detected',
                    sub:'No WooCommerce installation or data was found on this site. Install WooCommerce with data first, then come back here to migrate.' })
            ),

            /* WooCommerce detected — main UI */
            !loading && detection && (detection.wc_active || detection.counts.products > 0) && h(Fragment, null,

                /* Detection summary card */
                h(Card, { cardBg:cardBg, borderColor:borderColor, dark:dark },
                    h('div', { style:{ display:'flex', alignItems:'center', gap:12, marginBottom:16 } },
                        h('div', { style:{ width:40, height:40, borderRadius:10, background:'linear-gradient(135deg, #7B2D8E, #96588A)', display:'flex', alignItems:'center', justifyContent:'center', color:'#fff', fontSize:18, flexShrink:0 } }, 'W'),
                        h('div', null,
                            h('div', { style:{ fontWeight:600, fontSize:15 } }, 'WooCommerce Detected'),
                            h('div', { style:{ fontSize:12, color:mutedColor, marginTop:2 } },
                                'Version ' + (detection.wc_version || 'unknown') +
                                (detection.hpos_enabled ? ' • HPOS enabled' : ' • Legacy order storage')
                            )
                        ),
                        detection.wc_active && h('span', { style:{ marginLeft:'auto', fontSize:11, fontWeight:500, color:successGreen, background:dark ? 'rgba(22,163,74,.1)' : '#ECFDF5', padding:'4px 10px', borderRadius:6 } }, '● Active')
                    ),

                    /* Entity selection grid */
                    h('div', { style:{ display:'grid', gridTemplateColumns:'repeat(auto-fill, minmax(200px, 1fr))', gap:10, marginTop:8 } },
                        Object.keys(entityInfo).map(function(key) {
                            var info  = entityInfo[key];
                            var count = detection.counts[key] || 0;
                            var on    = checked[key] && count > 0;
                            var disabled = count === 0;

                            return h('div', {
                                key:key,
                                onClick: function() { if (!disabled && !running) toggle(key); },
                                style:{
                                    display:'flex', alignItems:'center', gap:10,
                                    padding:'12px 14px', borderRadius:8, cursor: disabled || running ? 'default' : 'pointer',
                                    border:'1.5px solid ' + (on ? accent : borderColor),
                                    background: on ? chipBg : 'transparent',
                                    opacity: disabled ? 0.4 : 1,
                                    transition:'all .15s ease',
                                }
                            },
                                h('div', { style:{ width:18, height:18, borderRadius:4, border:'2px solid ' + (on ? accent : (dark ? '#555' : '#ccc')), background: on ? accent : 'transparent', display:'flex', alignItems:'center', justifyContent:'center', flexShrink:0, transition:'all .15s ease' } },
                                    on && h('svg', { width:12, height:12, viewBox:'0 0 12 12', fill:'none', stroke:'#fff', strokeWidth:2, strokeLinecap:'round' },
                                        h('path', { d:'M2.5 6l2.5 2.5 4.5-5' })
                                    )
                                ),
                                h('span', { style:{ fontSize:16 } }, info.icon),
                                h('div', { style:{ flex:1 } },
                                    h('div', { style:{ fontSize:13, fontWeight:500 } }, info.label),
                                    h('div', { style:{ fontSize:11, color:mutedColor } }, count.toLocaleString() + ' found')
                                )
                            );
                        })
                    ),

                    /* Import button */
                    !done && h('div', { style:{ marginTop:20, display:'flex', alignItems:'center', gap:12 } },
                        h('button', {
                            onClick:startImport,
                            disabled: running || selectedCount === 0,
                            style:{
                                background: running || selectedCount === 0 ? (dark ? '#333' : '#ddd') : accent,
                                color: running || selectedCount === 0 ? mutedColor : '#fff',
                                border:'none', borderRadius:8, padding:'10px 24px', fontSize:13, fontWeight:600,
                                cursor: running || selectedCount === 0 ? 'not-allowed' : 'pointer',
                                display:'flex', alignItems:'center', gap:8,
                            }
                        },
                            running && h(Spinner, { size:14, color:'#fff' }),
                            running ? 'Importing...' : 'Start Migration'
                        ),
                        !running && selectedCount > 0 && h('span', { style:{ fontSize:12, color:mutedColor } },
                            selectedCount + ' entit' + (selectedCount === 1 ? 'y' : 'ies') + ' selected'
                        )
                    )
                ),

                /* Progress card */
                (running || done) && progress && h(Card, { cardBg:cardBg, borderColor:borderColor, dark:dark },
                    h('div', { style:{ display:'flex', alignItems:'center', gap:10, marginBottom:14 } },
                        done
                            ? h('span', { style:{ fontSize:18 } }, progress.errors && progress.errors.length ? '⚠️' : '✅')
                            : h(Spinner, null),
                        h('div', null,
                            h('div', { style:{ fontWeight:600, fontSize:14 } },
                                done ? (progress.errors && progress.errors.length ? 'Completed with warnings' : 'Migration Complete') : 'Importing ' + (progress.current || '...')
                            ),
                            progress.total > 0 && h('div', { style:{ fontSize:12, color:mutedColor, marginTop:2 } },
                                (progress.done || 0) + ' / ' + progress.total + ' ' + (progress.current || '')
                            )
                        )
                    ),

                    /* Progress bar */
                    progress.total > 0 && h('div', { style:{ height:6, borderRadius:3, background:dark ? '#262A36' : '#E5E7EB', overflow:'hidden', marginBottom:14 } },
                        h('div', { style:{
                            height:'100%', borderRadius:3, transition:'width .3s ease',
                            width: Math.min(100, Math.round((progress.done || 0) / progress.total * 100)) + '%',
                            background: done ? successGreen : accent,
                        } })
                    ),

                    /* Log output */
                    progress.log && progress.log.length > 0 && h('div', {
                        style:{
                            maxHeight:220, overflowY:'auto', fontSize:11, fontFamily:'monospace',
                            background:dark ? '#0F1119' : '#F9FAFB', borderRadius:6, padding:12,
                            border:'1px solid ' + borderColor, lineHeight:1.7,
                            color: mutedColor,
                        }
                    },
                        progress.log.map(function(line, i) {
                            var isError = line.indexOf('ERROR') !== -1;
                            return h('div', { key:i, style:{ color: isError ? '#DC2626' : mutedColor } }, line);
                        })
                    ),

                    /* Errors summary */
                    done && progress.errors && progress.errors.length > 0 && h('div', { style:{ marginTop:12, padding:'10px 14px', borderRadius:6, background:dark ? 'rgba(220,38,38,.08)' : '#FEF2F2', border:'1px solid ' + (dark ? 'rgba(220,38,38,.2)' : '#FECACA'), fontSize:12, color:'#DC2626' } },
                        h('strong', null, progress.errors.length + ' error(s) during import:'),
                        h('ul', { style:{ margin:'6px 0 0 16px', padding:0 } },
                            progress.errors.slice(0, 20).map(function(e, i) {
                                return h('li', { key:i, style:{ marginBottom:2 } }, e);
                            })
                        )
                    ),

                    /* Done — action buttons */
                    done && h('div', { style:{ marginTop:14, display:'flex', gap:10 } },
                        h('button', {
                            onClick:function() { navigate('products'); },
                            style:{ background:accent, color:'#fff', border:'none', borderRadius:8, padding:'8px 18px', fontSize:12, fontWeight:600, cursor:'pointer' }
                        }, 'View Products'),
                        h('button', {
                            onClick:function() { navigate('orders'); },
                            style:{ background:'transparent', color:textColor, border:'1px solid ' + borderColor, borderRadius:8, padding:'8px 18px', fontSize:12, fontWeight:500, cursor:'pointer' }
                        }, 'View Orders')
                    )
                ),

                /* Info card */
                !running && !done && h(Card, { cardBg:cardBg, borderColor:borderColor, dark:dark },
                    h('div', { style:{ fontSize:13, fontWeight:600, marginBottom:8 } }, 'Before you import'),
                    h('div', { style:{ fontSize:12, color:mutedColor, lineHeight:1.7 } },
                        '• Back up your database before starting a large migration.',
                        h('br'),
                        '• Categories are imported first, then products are linked to them automatically.',
                        h('br'),
                        '• Products are matched by Product Code (SKU) — duplicates are skipped.',
                        h('br'),
                        '• Orders are matched by order number — existing orders won\'t be duplicated.',
                        h('br'),
                        '• Product images are linked by URL from your existing media library — no re-upload needed.',
                        h('br'),
                        '• WooCommerce coupons are converted into TitanCart promotions with coupon codes.',
                        h('br'),
                        '• SEO meta from Yoast or Rank Math is imported automatically.'
                    )
                )
            )
        );
    }

    /* ════════════════════════════════════════════════════════════════════
     * DOWNLOAD AGREEMENTS PAGE (S129 Phase 2)
     * Route: #/settings/download-agreements
     * ════════════════════════════════════════════════════════════════════
     * Manages the license agreements presented to customers when they buy
     * a downloadable product (Phase 5 will gate the cart-add on acceptance).
     *
     * Two scopes:
     *   - Global default (is_global=1, product_id=null) — applies to every
     *     downloadable product unless overridden.
     *   - Per-product (is_global=0, product_id set) — overrides the global
     *     for one product.
     *
     * Editing an agreement creates a NEW row with version+1 in the same
     * scope and archives the old row. Acceptances remain bound to the
     * version they were captured against (snapshotted via accepted_content_hash)
     * so chargeback evidence is never invalidated by edits.
     * ════════════════════════════════════════════════════════════════════ */
    function DownloadAgreementsPage(props) {
        var dark         = props.dark;
        var cardBg       = props.cardBg;
        var textColor    = props.textColor;
        var mutedColor   = props.mutedColor;
        var borderColor  = props.borderColor;

        var [agreements, setAgreements] = useState([]);
        var [includeArchived, setIncludeArchived] = useState(false);
        var [loading, setLoading]   = useState(true);
        var [selectedId, setSelectedId] = useState(null);
        var [history, setHistory]   = useState([]);
        var [mode, setMode]         = useState('view'); // 'view' | 'create' | 'edit'
        var [busy, setBusy]         = useState(false);
        var [msg, setMsg]           = useState(null);

        // Form state
        var [fIsGlobal, setFIsGlobal]     = useState(true);
        var [fProductId, setFProductId]   = useState('');
        var [fTitle, setFTitle]           = useState('');
        var [fContent, setFContent]       = useState('');
        var [fLanguageCode, setFLanguageCode] = useState('en');
        var [fRequired, setFRequired]     = useState(true);

        function flash(text, ok) { setMsg({ text:text, ok: !!ok }); setTimeout(function(){ setMsg(null); }, 4000); }

        function loadAgreements() {
            setLoading(true);
            apiFetch('/admin/agreements?include_archived=' + (includeArchived ? 1 : 0))
                .then(function(d) { setAgreements((d && d.agreements) || []); setLoading(false); })
                .catch(function(e) { setLoading(false); flash(e.message || 'Could not load agreements.', false); });
        }
        function loadDetail(id) {
            apiFetch('/admin/agreements/' + id)
                .then(function(d) {
                    var a = d && d.agreement;
                    if (!a) return;
                    setSelectedId(a.id);
                    setHistory((d && d.history) || []);
                    setFIsGlobal(!!a.is_global);
                    setFProductId(a.product_id ? String(a.product_id) : '');
                    setFTitle(a.title || '');
                    setFContent(a.content || '');
                    setFLanguageCode(a.language_code || 'en');
                    setFRequired(!!Number(a.required));
                    setMode('view');
                })
                .catch(function(e) { flash(e.message || 'Could not load agreement.', false); });
        }

        useEffect(function() { loadAgreements(); /* eslint-disable-next-line */ }, [includeArchived]);

        function startCreate() {
            setSelectedId(null);
            setHistory([]);
            setFIsGlobal(true);
            setFProductId('');
            setFTitle('');
            setFContent('');
            setFLanguageCode('en');
            setFRequired(true);
            setMode('create');
        }
        function startEdit() {
            if (!selectedId) return;
            setMode('edit');
        }
        function cancel() {
            if (selectedId) loadDetail(selectedId); else { setMode('view'); }
        }

        function save() {
            if (!fTitle.trim()) { flash('Title is required.', false); return; }
            if (!fContent.trim()) { flash('Content is required.', false); return; }
            if (!fIsGlobal && (!fProductId || parseInt(fProductId, 10) <= 0)) {
                flash('Per-product agreements need a product id.', false);
                return;
            }
            setBusy(true);

            if (mode === 'create') {
                apiFetch('/admin/agreements', {
                    method: 'POST',
                    body: {
                        is_global: fIsGlobal ? 1 : 0,
                        product_id: fIsGlobal ? null : parseInt(fProductId, 10),
                        title: fTitle,
                        content: fContent,
                        language_code: fLanguageCode,
                        required: fRequired ? 1 : 0,
                        status: 'active',
                    }
                })
                    .then(function(d) {
                        setBusy(false);
                        flash('Agreement created.', true);
                        loadAgreements();
                        if (d && d.agreement) loadDetail(d.agreement.id);
                    })
                    .catch(function(e) { setBusy(false); flash(e.message || 'Save failed.', false); });
                return;
            }

            // mode === 'edit' — server creates version N+1 and archives the old row
            apiFetch('/admin/agreements/' + selectedId, {
                method: 'PUT',
                body: { title: fTitle, content: fContent, required: fRequired ? 1 : 0 }
            })
                .then(function(d) {
                    setBusy(false);
                    flash('New version created.', true);
                    loadAgreements();
                    if (d && d.agreement) loadDetail(d.agreement.id);
                })
                .catch(function(e) { setBusy(false); flash(e.message || 'Save failed.', false); });
        }

        function archiveCurrent() {
            if (!selectedId) return;
            if (!confirm('Archive this agreement? It will stop being shown to new customers, but existing acceptances remain valid for chargeback evidence.')) return;
            apiFetch('/admin/agreements/' + selectedId, { method: 'DELETE' })
                .then(function() { flash('Agreement archived.', true); loadAgreements(); setSelectedId(null); setMode('view'); })
                .catch(function(e) { flash(e.message || 'Archive failed.', false); });
        }

        // Group agreements: globals first, then per-product. Within each group, latest version per scope at top.
        function groupedAgreements() {
            var globals = [], perProduct = [];
            agreements.forEach(function(a) {
                if (a.is_global) globals.push(a);
                else perProduct.push(a);
            });
            return { globals: globals, perProduct: perProduct };
        }

        // ── Layout pieces ───────────────────────────────────────────────────
        var headerSty = {
            display: 'flex', alignItems: 'center', justifyContent: 'space-between',
            padding: S.lg + 'px ' + S.xxl + 'px',
            background: cardBg, borderBottom: '1px solid ' + borderColor,
        };
        var labelSty   = { fontSize: 11, fontWeight: 600, color: mutedColor, textTransform: 'uppercase', letterSpacing: '0.5px', marginBottom: 6, display: 'block' };
        var inputSty   = { width: '100%', padding: '8px 10px', fontSize: 13, borderRadius: 6, border: '1px solid ' + borderColor, background: dark ? '#0A0A0A' : '#FFFFFF', color: textColor, boxSizing: 'border-box' };

        function renderHeader() {
            return h('div', { style: headerSty },
                h('div', { style: { display: 'flex', alignItems: 'center', gap: S.md } },
                    h('button', {
                        onClick: function(){ window.location.hash = '#/settings'; },
                        style: { background: 'none', border: '1px solid ' + borderColor, padding: '6px 12px', borderRadius: 6, cursor: 'pointer', color: mutedColor, fontSize: 12 }
                    }, '← Settings'),
                    h('div', null,
                        h('h1', { style: { fontSize: 20, fontWeight: 700, margin: 0, letterSpacing: '-0.3px' } }, 'Download Agreements'),
                        h('p', { style: { fontSize: 12, color: mutedColor, margin: '2px 0 0' } },
                            'License agreements presented to customers when they buy a downloadable product.')
                    )
                ),
                h('div', { style: { display: 'flex', gap: S.sm } },
                    h('label', { style: { display: 'flex', alignItems: 'center', gap: 6, fontSize: 12, color: mutedColor, cursor: 'pointer' } },
                        h('input', { type: 'checkbox', checked: includeArchived, onChange: function(e){ setIncludeArchived(!!e.target.checked); } }),
                        'Show archived'
                    ),
                    h(Btn, { variant: 'primary', onClick: startCreate, mutedColor: mutedColor, borderColor: borderColor }, '+ New agreement')
                )
            );
        }

        function renderEmpty() {
            return h('div', {
                style: { background: cardBg, border: '1px solid ' + borderColor, borderRadius: 10, padding: S.xxl + 'px ' + S.xl + 'px', textAlign: 'center' }
            },
                h('h3', { style: { fontSize: 15, fontWeight: 600, margin: '0 0 6px' } }, 'No agreement selected'),
                h('p', { style: { fontSize: 13, color: mutedColor, margin: 0 } },
                    'Pick an agreement from the list, or click "+ New agreement" to create one.'),
                agreements.length === 0 && h('p', { style: { fontSize: 12, color: mutedColor, marginTop: S.md } },
                    'Tip: Start with a Global default agreement that applies to every downloadable product. Per-product agreements override it for specific products.')
            );
        }

        function renderListRow(a) {
            var isSel  = a.id === selectedId;
            var pillBg = a.status === 'active'
                ? (dark ? 'rgba(22,163,74,.12)' : '#ECFDF5')
                : (dark ? 'rgba(115,115,115,.18)' : '#F4F4F5');
            var pillFg = a.status === 'active' ? '#16A34A' : mutedColor;

            return h('div', {
                key: a.id,
                onClick: function(){ loadDetail(a.id); },
                style: {
                    padding: '10px 14px',
                    borderRadius: 8,
                    cursor: 'pointer',
                    background: isSel ? (dark?'rgba(37,99,235,.12)':'#EFF6FF') : 'transparent',
                    border: '1px solid ' + (isSel ? TC.accent : 'transparent'),
                    marginBottom: 4,
                }
            },
                h('div', { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 8 } },
                    h('div', { style: { fontSize: 13, fontWeight: 500, color: textColor, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } },
                        a.title || '(untitled)'),
                    h('span', { style: { fontSize: 9, fontWeight: 600, color: pillFg, background: pillBg, padding: '2px 6px', borderRadius: 3, textTransform: 'capitalize', flexShrink: 0 } },
                        a.status)
                ),
                h('div', { style: { fontSize: 11, color: mutedColor, marginTop: 3 } },
                    'v' + a.version,
                    a.is_global ? ' · Global' : (a.product_id ? (' · Product #' + a.product_id) : ''),
                    ' · ' + a.acceptance_count + ' acceptances'
                )
            );
        }

        function renderList() {
            var grouped = groupedAgreements();
            return h('div', {
                style: { background: cardBg, border: '1px solid ' + borderColor, borderRadius: 10, padding: S.md, height: 'fit-content' }
            },
                loading
                    ? h('div', { style: { padding: S.xl, textAlign: 'center' } }, h(Spinner, { mutedColor: mutedColor }))
                    : agreements.length === 0
                        ? h('div', { style: { padding: S.xl, textAlign: 'center', fontSize: 12, color: mutedColor } }, 'No agreements yet.')
                        : h('div', null,
                            grouped.globals.length > 0 && h('div', { style: { padding: '6px 10px 4px', fontSize: 10, fontWeight: 600, color: mutedColor, textTransform: 'uppercase', letterSpacing: '0.5px' } }, 'Global'),
                            grouped.globals.map(renderListRow),
                            grouped.perProduct.length > 0 && h('div', { style: { padding: '14px 10px 4px', fontSize: 10, fontWeight: 600, color: mutedColor, textTransform: 'uppercase', letterSpacing: '0.5px' } }, 'Per-product'),
                            grouped.perProduct.map(renderListRow)
                        )
            );
        }

        function renderEditor() {
            if (!selectedId && mode !== 'create') return renderEmpty();
            var isCreating = mode === 'create';
            var isEditing  = mode === 'edit';
            var readonly   = mode === 'view';

            return h('div', {
                style: { background: cardBg, border: '1px solid ' + borderColor, borderRadius: 10, padding: S.xl, display: 'flex', flexDirection: 'column', gap: S.lg }
            },
                msg && h('div', {
                    style: {
                        padding: '10px 14px', borderRadius: 8, fontSize: 13,
                        background: msg.ok ? (dark?'rgba(22,163,74,.12)':'#ECFDF5') : (dark?'rgba(220,38,38,.12)':'#FEF2F2'),
                        color: msg.ok ? '#16A34A' : '#DC2626',
                    }
                }, msg.text),

                isEditing && h('div', { style: { padding: 12, borderRadius: 8, background: dark?'rgba(217,119,6,.12)':'#FFFBEB', color: '#D97706', fontSize: 12, border: '1px solid #FDE68A' } },
                    h('strong', null, 'Editing creates a new version.'),
                    ' The current row will be archived. Existing acceptances are snapshotted against the version they were captured under and stay valid for chargeback evidence.'),

                // Scope (only on create)
                isCreating && h('div', null,
                    h('label', { style: labelSty }, 'Scope'),
                    h('div', { style: { display: 'flex', gap: 12, alignItems: 'center', paddingTop: 4 } },
                        h('label', { style: { display: 'flex', alignItems: 'center', gap: 6, cursor: 'pointer', fontSize: 13 } },
                            h('input', { type: 'radio', name: 'tcDlAgreementScope', checked: fIsGlobal, onChange: function(){ setFIsGlobal(true); setFProductId(''); } }),
                            'Global default'),
                        h('label', { style: { display: 'flex', alignItems: 'center', gap: 6, cursor: 'pointer', fontSize: 13 } },
                            h('input', { type: 'radio', name: 'tcDlAgreementScope', checked: !fIsGlobal, onChange: function(){ setFIsGlobal(false); } }),
                            'Per-product')
                    ),
                    !fIsGlobal && h('div', { style: { marginTop: S.sm } },
                        h('label', { style: labelSty }, 'Product ID'),
                        h('input', {
                            type: 'number', min: '1',
                            style: inputSty,
                            value: fProductId,
                            placeholder: 'e.g. 155',
                            onChange: function(e){ setFProductId(e.target.value); }
                        }),
                        h('div', { style: { fontSize: 11, color: mutedColor, marginTop: 4 } },
                            'You can find a product id in the products list URL or the product editor.')
                    )
                ),

                // Title
                h('div', null,
                    h('label', { style: labelSty }, 'Title'),
                    h('input', {
                        style: inputSty,
                        value: fTitle,
                        readOnly: readonly,
                        onChange: function(e){ setFTitle(e.target.value); }
                    })
                ),

                // Content
                h('div', null,
                    h('label', { style: labelSty }, 'Content (HTML allowed)'),
                    h('textarea', {
                        style: Object.assign({}, inputSty, { fontFamily: 'inherit', resize: 'vertical', minHeight: 220 }),
                        value: fContent,
                        readOnly: readonly,
                        onChange: function(e){ setFContent(e.target.value); }
                    }),
                    h('div', { style: { fontSize: 11, color: mutedColor, marginTop: 4 } },
                        'Shown verbatim to customers. Plain HTML is allowed (paragraphs, lists, headings, links). The exact text is hashed at acceptance time so editing later does not retroactively change what a customer agreed to.')
                ),

                // Required + language (side-by-side)
                h('div', { style: { display: 'grid', gridTemplateColumns: '1fr 1fr', gap: S.md } },
                    h('div', null,
                        h('label', { style: labelSty }, 'Language code'),
                        h('input', {
                            style: inputSty,
                            value: fLanguageCode,
                            disabled: !isCreating,  // language is part of the scope; locked after create
                            onChange: function(e){ setFLanguageCode(e.target.value.toLowerCase().slice(0, 2)); }
                        }),
                        h('div', { style: { fontSize: 11, color: mutedColor, marginTop: 4 } }, '2-letter code (e.g. en, de, fr).')
                    ),
                    h('div', null,
                        h('label', { style: labelSty }, 'Required'),
                        h('label', { style: { display: 'flex', alignItems: 'center', gap: 8, paddingTop: 6, fontSize: 13, cursor: readonly?'default':'pointer' } },
                            h('input', { type: 'checkbox', checked: fRequired, disabled: readonly, onChange: function(e){ setFRequired(!!e.target.checked); } }),
                            'Customers must accept before adding to cart'
                        )
                    )
                ),

                // Action buttons
                h('div', { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: S.sm, paddingTop: S.md, borderTop: '1px solid ' + borderColor } },
                    // Left side — archive (only when viewing an active agreement)
                    h('div', null,
                        readonly && selectedId && agreements.find(function(a){ return a.id===selectedId && a.status==='active'; })
                            && h('button', { onClick: archiveCurrent,
                                style: { background:'none', border:'1px solid '+borderColor, padding:'6px 12px', borderRadius:6, fontSize:12, color: TC.error, cursor:'pointer' } }, 'Archive')
                    ),
                    // Right side — primary actions
                    h('div', { style: { display: 'flex', gap: S.sm } },
                        readonly && selectedId
                            && h(Btn, { variant: 'primary', onClick: startEdit, mutedColor: mutedColor, borderColor: borderColor }, 'Edit (creates new version)'),
                        (isCreating || isEditing)
                            && h(Btn, { onClick: cancel, mutedColor: mutedColor, borderColor: borderColor, disabled: busy }, 'Cancel'),
                        (isCreating || isEditing)
                            && h(Btn, { variant: 'primary', onClick: save, mutedColor: mutedColor, borderColor: borderColor, disabled: busy },
                                busy ? 'Saving…' : (isCreating ? 'Create' : 'Save new version'))
                    )
                ),

                // Version history (read-only summary)
                history.length > 1 && h('div', { style: { borderTop: '1px solid ' + borderColor, paddingTop: S.md } },
                    h('h4', { style: { fontSize: 13, fontWeight: 600, marginBottom: S.sm } }, 'Version history'),
                    h('div', { style: { display: 'flex', flexDirection: 'column', gap: 4 } },
                        history.map(function(v) {
                            var isCurrent = v.id === selectedId;
                            return h('div', { key: v.id, style: {
                                display: 'flex', alignItems: 'center', justifyContent: 'space-between',
                                padding: '6px 10px', borderRadius: 6, fontSize: 12,
                                background: isCurrent ? (dark?'rgba(37,99,235,.12)':'#EFF6FF') : 'transparent',
                            } },
                                h('span', null,
                                    h('strong', null, 'v' + v.version),
                                    isCurrent && h('span', { style: { color: TC.accent, marginLeft: 6 } }, '(current view)')
                                ),
                                h('span', { style: { color: mutedColor, fontSize: 11 } },
                                    v.status + ' · ' + v.acceptance_count + ' acceptances · ' + (v.created_at || '').slice(0, 10))
                            );
                        })
                    )
                )
            );
        }

        return h('main', { style: { flex: 1, overflow: 'auto', background: dark ? '#0A0A0A' : TC.lightBg } },
            renderHeader(),
            h('div', { style: { padding: S.xxl, display: 'grid', gridTemplateColumns: '300px 1fr', gap: S.lg, alignItems: 'start' } },
                renderList(),
                renderEditor()
            )
        );
    }

    /* ════════════════════════════════════════════════════════════════════
     *  MARKETPLACE SETTINGS PAGE — Track A Module A1 (S157)
     *
     *  Lives in the core admin SPA, not in the marketplace plugin's bundle,
     *  because the SPA is monolithic and ships from TC core. Reads/writes
     *  the marketplace section of tc_settings via the titancart-marketplace
     *  REST namespace registered by SettingsController.php in the plugin.
     *
     *  License-gating: when Config.marketplaceLicenseActive is false
     *  (plugin installed but license missing/invalid), all form controls
     *  render disabled and Save is hidden. The license banner explains why
     *  and links the admin to the Installed Extensions page.
     *
     *  Card visibility: the Settings landing card for Marketplace only
     *  renders when the plugin is installed (i.e. the bootstrap had a
     *  chance to add `marketplaceLicenseActive` to Config — see the
     *  CARDS filter in SettingsPage below).
     * ════════════════════════════════════════════════════════════════════ */
    function MarketplaceSettingsPage({ dark, cardBg, textColor, mutedColor, borderColor, backBtn }) {

        /* ── REST helper scoped to the marketplace namespace ──────────────
           Mirrors subsApiFetch (line ~14390) so cross-extension settings
           pages share an identical fetch shape. */
        var MKT_API = (Config.apiUrl || '').replace('/titancart/v1', '/titancart-marketplace/v1');
        function mktApiFetch(path, opts) {
            opts = opts || {};
            opts.headers = Object.assign({ 'Content-Type':'application/json', 'X-WP-Nonce': Config.nonce || '' }, opts.headers || {});
            if (opts.body && typeof opts.body === 'object') opts.body = JSON.stringify(opts.body);
            return fetch(MKT_API + path, opts).then(function(r) {
                return r.text().then(function(text) {
                    var data = null;
                    if (text) { try { data = JSON.parse(text); } catch(e) { data = null; } }
                    if (!r.ok) {
                        var msg = data && (data.message || data.error || JSON.stringify(data));
                        var err = new Error(msg || 'HTTP ' + r.status);
                        err.status = r.status; err.data = data;
                        return Promise.reject(err);
                    }
                    return data;
                });
            });
        }

        /* ── State ────────────────────────────────────────────────────── */
        var _values  = useState({ enabled:'0', mode:'digital', marketplace_name:'', accent_color:'#2563EB', default_brand:'', default_commission_rate:'10.00' });
        var values   = _values[0]; var setValues = _values[1];
        var _loading = useState(true);  var loading = _loading[0]; var setLoading = _loading[1];
        var _saving  = useState(false); var saving  = _saving[0];  var setSaving  = _saving[1];
        var _saved   = useState(false); var saved   = _saved[0];   var setSaved   = _saved[1];
        var _error   = useState('');    var error   = _error[0];   var setError   = _error[1];

        /* License state — TC core's Admin/Assets.php surfaces this directly
           into the SPA Config (via wp_localize_script), hardcoded as
           `defined('TCMP_VERSION') && MarketplacePlugin::license_active()`.
           Same shape as `subscriptionsActive`. Treat undefined/missing as
           false (= plugin not installed → page shouldn't be reachable, but
           defend in depth). */
        var licenseActive = !!Config.marketplaceLicenseActive;

        /* ── Field styles ────────────────────────────────────────────── */
        var fs = fieldSty(dark, borderColor);
        var labelSty = { fontSize:13, fontWeight:500, color:textColor, display:'block', marginBottom:S.xs };
        var hintSty  = { fontSize:12, color:mutedColor, marginTop:S.xs, lineHeight:1.5 };

        /* ── Load on mount ───────────────────────────────────────────── */
        useEffect(function() {
            setLoading(true); setError('');
            mktApiFetch('/settings').then(function(data) {
                if (data && typeof data === 'object') {
                    setValues(function(prev) {
                        return {
                            enabled:          data.enabled          != null ? String(data.enabled)          : prev.enabled,
                            mode:             data.mode             != null ? String(data.mode)             : prev.mode,
                            marketplace_name: data.marketplace_name != null ? String(data.marketplace_name) : prev.marketplace_name,
                            accent_color:     data.accent_color     != null ? String(data.accent_color)     : prev.accent_color,
                            default_brand:    data.default_brand    != null ? String(data.default_brand)    : prev.default_brand,
                            default_commission_rate: data.default_commission_rate != null ? String(data.default_commission_rate) : prev.default_commission_rate,
                        };
                    });
                }
                setLoading(false);
            }).catch(function(err) {
                setError((err && err.message) || 'Failed to load marketplace settings.');
                setLoading(false);
            });
        }, []);

        /* ── Setter helper ──────────────────────────────────────────── */
        function setField(key, val) {
            setValues(function(prev) {
                var next = {};
                for (var k in prev) next[k] = prev[k];
                next[key] = val;
                return next;
            });
            setSaved(false);
            setError('');
        }

        /* ── Save handler ───────────────────────────────────────────── */
        function handleSave() {
            if (!licenseActive) return; /* defensive — controls are locked anyway */
            setSaving(true); setSaved(false); setError('');
            mktApiFetch('/settings', { method:'PUT', body: values }).then(function(data) {
                if (data && data.success) {
                    setSaved(true);
                    setTimeout(function(){ setSaved(false); }, 3000);
                } else {
                    setError((data && data.message) || 'Save failed.');
                }
                setSaving(false);
            }).catch(function(err) {
                /* Surface the license-required error code distinctly so the
                   admin understands the gate. The license banner is already
                   visible when this fires; the error span confirms the
                   server-side rejection. */
                if (err && err.data && err.data.code === 'tcmp_license_required') {
                    setError('License required to enable marketplace features.');
                } else {
                    setError((err && err.message) || 'Network error.');
                }
                setSaving(false);
            });
        }

        /* ── License banner ─────────────────────────────────────────── */
        function renderLicenseBanner() {
            if (licenseActive) return null;
            return h('div', { style:{
                background: dark ? 'rgba(217,119,6,.10)' : '#FEF6EC',
                border: '1px solid ' + (dark ? 'rgba(217,119,6,.28)' : '#FCD9A6'),
                borderRadius: 10, padding: S.lg, marginBottom: S.lg,
                display: 'flex', alignItems: 'flex-start', gap: S.md,
            } },
                h('div', { style:{ fontSize:18, lineHeight:1, paddingTop:1 } }, '\uD83D\uDD12'),
                h('div', { style:{ flex:1 } },
                    h('div', { style:{ fontSize:13, fontWeight:600, color:textColor, marginBottom:3 } },
                        'License required'),
                    h('div', { style:{ fontSize:12, color:mutedColor, lineHeight:1.55 } },
                        'Activate your TitanCart Marketplace license to configure marketplace settings. ',
                        h('a', { href:'#/installed-extensions',
                            onClick: function(e) { e.preventDefault(); navigate('installed-extensions'); },
                            style:{ color:TC.accent, textDecoration:'underline' } },
                            'Open Installed Extensions'),
                        ' to enter your license key.'
                    )
                )
            );
        }

        /* ── Form rows ──────────────────────────────────────────────── */
        var disabled = !licenseActive;
        var disabledSty = disabled ? { opacity:0.55, pointerEvents:'none', userSelect:'none' } : {};

        function renderToggle(key, label, hint) {
            var isOn = values[key] === '1';
            return h('div', { style:Object.assign({}, { display:'flex', alignItems:'flex-start', gap:S.md }, disabledSty) },
                h('div', {
                    onClick: function(){ if (!disabled) setField(key, isOn ? '0' : '1'); },
                    style: { width:40, height:22, borderRadius:12, cursor: disabled ? 'not-allowed' : 'pointer',
                             background: isOn ? TC.accent : (dark ? '#333' : '#D4D4D4'),
                             transition:'background .15s', position:'relative', flexShrink:0, marginTop:1 }
                },
                    h('div', { style:{ width:18, height:18, borderRadius:'50%', background:'#fff',
                        position:'absolute', top:2, left:isOn?20:2, transition:'left .15s',
                        boxShadow:'0 1px 3px rgba(0,0,0,.2)' } })
                ),
                h('div', { style:{ flex:1 } },
                    h('div', { style:{ fontSize:13, fontWeight:500, color:textColor } }, label),
                    hint ? h('div', { style:hintSty }, hint) : null
                )
            );
        }

        function renderModeRadio() {
            var options = [
                { v:'digital',  l:'Digital marketplace', d:'Extension stores, template marketplaces, plugin marketplaces, asset stores. Live preview, compatibility, version, instant download.' },
                { v:'physical', l:'Physical marketplace', d:'Handmade goods, art, niche physical goods. Lead time, made-to-order, materials, dimensions.' },
            ];
            return h('div', { style:Object.assign({}, { display:'flex', flexDirection:'column', gap:S.sm }, disabledSty) },
                options.map(function(opt) {
                    var checked = values.mode === opt.v;
                    return h('label', { key:opt.v,
                        style:{ display:'flex', alignItems:'flex-start', gap:S.md,
                                padding:S.md+'px '+S.base+'px', borderRadius:8,
                                border: '1px solid ' + (checked ? TC.accent : borderColor),
                                background: checked ? (dark ? 'rgba(37,99,235,.08)' : 'rgba(37,99,235,.04)') : 'transparent',
                                cursor: disabled ? 'not-allowed' : 'pointer',
                                transition:'all .12s ease' } },
                        h('input', { type:'radio', name:'tcmp-mode', value:opt.v, checked:checked,
                            disabled:disabled,
                            onChange:function(){ if (!disabled) setField('mode', opt.v); },
                            style:{ marginTop:3, accentColor: TC.accent, flexShrink:0 } }),
                        h('div', { style:{ flex:1 } },
                            h('div', { style:{ fontSize:13, fontWeight:600, color:textColor, marginBottom:3 } }, opt.l),
                            h('div', { style:{ fontSize:12, color:mutedColor, lineHeight:1.5 } }, opt.d)
                        )
                    );
                })
            );
        }

        function renderRow(label, hint, control) {
            return h('div', { style:{ marginBottom:S.xl } },
                h('label', { style:labelSty }, label),
                control,
                hint ? h('div', { style:hintSty }, hint) : null
            );
        }

        /* ── Render ──────────────────────────────────────────────────── */
        var content;
        if (loading) {
            content = h('div', { style:{ padding:40, textAlign:'center', color:mutedColor } }, 'Loading\u2026');
        } else {
            content = h(Card, { cardBg:cardBg, borderColor:borderColor, dark:dark },
                renderLicenseBanner(),

                /* Master enable toggle */
                h('div', { style:{ marginBottom:S.xl, paddingBottom:S.xl, borderBottom:'1px solid '+borderColor } },
                    renderToggle('enabled',
                        'Enable marketplace features',
                        'Master switch. When off, marketplace customer-facing surfaces are hidden, vendor-side cron jobs pause, and no marketplace UX renders. Existing marketplace data is preserved — re-enable to resume.'
                    )
                ),

                /* Mode */
                renderRow('Marketplace mode',
                    'Drives conditional UX throughout the marketplace presentation. Switch any time — both modes share the underlying data model, only the customer-facing surfaces change.',
                    renderModeRadio()
                ),

                /* Marketplace name */
                renderRow('Marketplace name',
                    'Display name shown on the shop archive header, account section, and "powered by" footers. Leave blank to use your site title.',
                    h('input', { type:'text', value: values.marketplace_name, disabled: disabled,
                        onChange: function(e){ setField('marketplace_name', e.target.value); },
                        placeholder: 'e.g. TitanCart Market',
                        style: Object.assign({}, fs, disabledSty, { maxWidth: 400 }) })
                ),

                /* Accent color */
                renderRow('Accent color',
                    'Used by Track A presentation surfaces — primary CTA buttons, sidebar highlights, "More Extensions" grid accents. Use a hex value (e.g. #2563EB).',
                    h('div', { style:Object.assign({ display:'flex', alignItems:'center', gap:S.sm }, disabledSty) },
                        h('input', { type:'color', value: values.accent_color || '#2563EB', disabled: disabled,
                            onChange: function(e){ setField('accent_color', e.target.value); },
                            style: { width:48, height:38, padding:2, border:'1px solid '+borderColor, borderRadius:8, background:cardBg, cursor: disabled ? 'not-allowed' : 'pointer' } }),
                        h('input', { type:'text', value: values.accent_color, disabled: disabled,
                            onChange: function(e){ setField('accent_color', e.target.value); },
                            placeholder: '#2563EB',
                            style: Object.assign({}, fs, { maxWidth: 140, fontFamily:'ui-monospace, SFMono-Regular, Menlo, monospace', fontSize:12 }) })
                    )
                ),

                /* Default brand */
                renderRow('Default brand',
                    'Brand label assigned to products that don\'t have an explicit brand attribute. Track B will let vendors set their own brand — this is the fallback.',
                    h('input', { type:'text', value: values.default_brand, disabled: disabled,
                        onChange: function(e){ setField('default_brand', e.target.value); },
                        placeholder: 'e.g. TitanCart',
                        style: Object.assign({}, fs, disabledSty, { maxWidth: 400 }) })
                ),

                /* Default commission rate */
                renderRow('Default commission rate (%)',
                    'The platform\u2019s percentage cut of each vendor sale, applied to any vendor without a per-vendor override. At 10%, the vendor keeps 90% of each sale. Set a per-vendor rate from Vendors → (vendor) → Commission.',
                    h('div', { style:Object.assign({ display:'flex', alignItems:'center', gap:S.sm }, disabledSty) },
                        h('input', { type:'number', step:'0.01', min:0, max:100,
                            value: values.default_commission_rate, disabled: disabled,
                            onChange: function(e){ setField('default_commission_rate', e.target.value); },
                            placeholder: '10.00',
                            style: Object.assign({}, fs, { maxWidth: 140 }) }),
                        h('span', { style:{ fontSize:13, color:mutedColor } }, '%')
                    )
                )
            );
        }

        /* ── Header (back button + title + save controls) ────────────── */
        return h('main', { style:{ flex:1, overflow:'auto', padding:S.xxl } },
            h('div', { style:{ display:'flex', justifyContent:'space-between', alignItems:'center', marginBottom:S.xl } },
                h('div', { style:{ display:'flex', alignItems:'center', gap:S.md } },
                    backBtn,
                    h('div', null,
                        h('h2', { style:{ fontSize:20, fontWeight:600, letterSpacing:'-.3px', marginBottom:1 } }, 'Marketplace'),
                        h('div', { style:{ fontSize:13, color:mutedColor } },
                            'Configure marketplace mode, branding, and master enable toggle.')
                    )
                ),
                !loading && licenseActive && h('div', { style:{ display:'flex', alignItems:'center', gap:S.md } },
                    saved && h('span', { style:{ fontSize:12, color:TC.success, fontWeight:500 } }, '\u2713 Saved'),
                    error && h('span', { style:{ fontSize:12, color:TC.error, fontWeight:500 } }, error),
                    h(Btn, { variant:'primary', onClick: handleSave, disabled: saving || loading, mutedColor:mutedColor, borderColor:borderColor },
                        saving ? 'Saving\u2026' : 'Save Changes')
                )
            ),
            content
        );
    }

    /* ════════════════════════════════════════════════════════════════════
     * THEME OPTIONS PAGE — storefront preset picker + per-preset controls
     *
     * Custom (non schema-driven) settings page laid out like General Settings:
     * a left side-menu (200px) inside a card, with a full-page panel on the right.
     * Reads /settings/theme_options (schema + values). The menu lists "Active Theme"
     * (the global preset picker + accent override) on top, then one entry per
     * theme-provided preset group; the active preset is badged. Selecting an entry
     * swaps the right panel (local `sel` state, NOT hash routing — it is one settings
     * section). A preset panel offers a "Set as active theme" button when it isn't the
     * active one. Fields render generically (text/select/number/image/checkbox/
     * textarea/color) in a responsive grid; image fields use wp.media when present,
     * else a URL input. Saves via PUT to /settings/theme_options.
     *
     * renderField / renderFields / navItem are plain helpers returning elements (NOT
     * nested components), so controlled inputs keep focus across re-renders.
     * ════════════════════════════════════════════════════════════════════ */
    /* ── DemoInstallPanel — Prism "Install demo" field (type:'demo') ──────────
       MODULE-SCOPE component (never nested). Rendered by ThemeOptionsPage's
       renderField for schema fields the theme injects per preset:
         { type:'demo', key:'<id>_demo', preset:'<id>', manifest:'https://…/demos/index.json' }
       Talks to the plugin's /demo/* routes (DemoImportService, S471):
         status → install (creates a stepped job) → step…step → done.
       The "fitting room" rules live server-side; this panel only reflects them:
         - nothing installed          → Install demo
         - this preset installed      → Installed · Reinstall (repair) · Clear sample data · Remove
         - other preset, store clean  → Switch to this demo
         - other preset, store dirty  → locked, reasons shown, no switch
       A preset absent from the hosted manifest (e.g. Indigo) shows "not published". */
    function DemoInstallPanel({ field, dark, textColor, mutedColor, borderColor, onChanged }) {
        var _status  = useState(null);  var status  = _status[0];  var setStatus  = _status[1];
        var _entry   = useState(undefined); var entry = _entry[0];  var setEntry   = _entry[1]; /* undefined = not fetched yet, null = fetched + not published */
        var _job     = useState(null);  var job     = _job[0];     var setJob     = _job[1];
        var _busy    = useState('');    var busy    = _busy[0];    var setBusy    = _busy[1];
        var _err     = useState('');    var err     = _err[0];     var setErr     = _err[1];
        var alive    = useRef(true);
        var preset   = field.preset || '';
        var hdr      = { 'X-WP-Nonce': Config.nonce };

        function api(path, body){
            return fetch(Config.apiUrl + path, body === undefined
                ? { headers: hdr, credentials:'same-origin' }
                : { method:'POST', credentials:'same-origin', headers: Object.assign({ 'Content-Type':'application/json' }, hdr), body: JSON.stringify(body) })
                .then(function(r){ return r.json().then(function(j){ return { ok:r.ok, body:j }; }); });
        }
        function loadStatus(){
            return api('/demo/status').then(function(res){ if (alive.current && res.ok) { setStatus(res.body); if (res.body.job && res.body.job.status === 'running') { setJob(res.body.job); pump(); } } });
        }
        /* The hosted manifest is fetched ONLY on an action click (S472): opening the
           panel must not make an outbound call on its own — the WP.org readme promises
           Lite never contacts anywhere uninvited. entry: undefined = not fetched yet,
           null = fetched and this preset isn't published, object = published. */
        function ensureEntry(){
            if (entry !== undefined) { return Promise.resolve(entry); }
            if (!field.manifest) { setEntry(null); return Promise.resolve(null); }
            return api('/demo/manifest?url=' + encodeURIComponent(field.manifest)).then(function(res){
                var presets = (res.ok && res.body && res.body.presets) ? res.body.presets : {};
                var e = presets[preset] || null;
                if (alive.current) { setEntry(e); }
                return e;
            }).catch(function(){ if (alive.current) { setEntry(null); } return null; });
        }
        useEffect(function(){
            alive.current = true;
            loadStatus();
            return function(){ alive.current = false; };
        }, []);

        /* Step loop: each /demo/step does one slice; keep going until done/error. */
        function pump(){
            api('/demo/step', {}).then(function(res){
                if (!alive.current) { return; }
                var j = res.body || {};
                setJob(j);
                if (j.status === 'running') { setTimeout(pump, 150); }
                else {
                    setBusy('');
                    loadStatus();
                    if (j.status === 'done' && onChanged) { onChanged(); }
                    if (j.status === 'error') { setErr(j.message || j.error || 'Install failed.'); }
                }
            }).catch(function(e){ if (alive.current) { setBusy(''); setErr(e.message); } });
        }
        function install(force){
            setErr(''); setBusy('install');
            ensureEntry().then(function(e){
                if (!alive.current) { return; }
                if (!e) { setBusy(''); return; } /* not published — the panel now says so */
                return api('/demo/install', { url: e.package_url, force: !!force }).then(function(res){
                    if (!alive.current) { return; }
                    if (!res.ok) { setBusy(''); setErr((res.body && res.body.error) || 'Could not start the demo install.'); return; }
                    setJob(res.body); pump();
                });
            }).catch(function(e){ if (alive.current) { setBusy(''); setErr(e.message); } });
        }
        function act(path, label){
            setErr(''); setBusy(label);
            api(path, {}).then(function(res){
                if (!alive.current) { return; }
                setBusy('');
                if (!res.ok) { setErr((res.body && res.body.error) || 'Request failed.'); return; }
                if (res.body && res.body.status) { setStatus(res.body.status); }
                if (onChanged) { onChanged(); }
            }).catch(function(e){ if (alive.current) { setBusy(''); setErr(e.message); } });
        }

        var installedPreset = (status && status.installed && status.installed.preset) || '';
        var isThis   = installedPreset === preset;
        var isOther  = installedPreset !== '' && !isThis;
        var locked   = !!(status && status.locked);
        var reasons  = (status && status.dirty) || [];
        var running  = !!(job && job.status === 'running');
        var counts   = (status && status.counts) || {};
        var sampleN  = (counts.order || 0) + (counts.customer || 0) + (counts.review || 0);

        var btn = function(label, onClick, opts){
            opts = opts || {};
            var disabled = !!opts.disabled || running || busy !== '';
            return h('button', { onClick: disabled ? null : onClick, disabled: disabled,
                style:{ padding:'9px 14px', borderRadius:8, fontSize:12.5, fontWeight:600, cursor: disabled ? 'not-allowed' : 'pointer',
                        border:'1px solid ' + (opts.primary ? TC.accent : (opts.danger ? TC.error : borderColor)),
                        background: opts.primary ? TC.accent : 'transparent',
                        color: opts.primary ? '#fff' : (opts.danger ? TC.error : textColor),
                        opacity: disabled ? .55 : 1 } }, label);
        };

        var body;
        if (status === null) {
            body = h('div', { style:{ fontSize:12.5, color:mutedColor } }, 'Checking demo status\u2026');
        } else if (running) {
            var pct = Math.max(0, Math.min(100, job.percent || 0));
            body = h('div', null,
                h('div', { style:{ height:8, borderRadius:4, background: dark ? '#333' : '#E5E5E5', overflow:'hidden', marginBottom:8 } },
                    h('div', { style:{ width: pct + '%', height:'100%', background:TC.accent, transition:'width .2s' } })),
                h('div', { style:{ fontSize:12.5, color:textColor } }, (job.message || 'Working\u2026') + ' ' + pct + '%'),
                h('div', { style:{ fontSize:11.5, color:mutedColor, marginTop:4 } }, 'Keep this page open until the install finishes.')
            );
        } else if (entry === null && !isThis) {
            body = h('div', { style:{ fontSize:12.5, color:mutedColor } },
                'Demo content for this theme isn\u2019t published yet. Check back after the next TitanCart release.');
        } else {
            var meta = [];
            if (entry && entry.products) { meta.push(entry.products + ' products'); }
            if (entry && entry.version)  { meta.push('updated ' + entry.version); }
            var actions = [];
            if (isThis) {
                if (entry !== null) { actions.push(btn('Reinstall (repair)', function(){ install(true); })); }
                if (sampleN > 0) { actions.push(btn('Clear demo sample data', function(){ act('/demo/clear-sample-data', 'clear'); })); }
                actions.push(btn('Remove demo', function(){ if (window.confirm('Remove every product, category, image and setting this demo installed? Anything you created yourself is kept.')) { act('/demo/remove', 'remove'); } }, { danger:true }));
            } else if (isOther) {
                actions.push(btn(locked ? 'Switching is locked' : 'Switch to this demo', function(){ if (window.confirm('This replaces the "' + installedPreset + '" demo content with this theme\u2019s and makes this theme the active storefront design. Continue?')) { install(false); } }, { primary: !locked, disabled: locked }));
            } else {
                actions.push(btn('Install demo', function(){ install(false); }, { primary:true }));
            }
            body = h('div', null,
                h('div', { style:{ display:'flex', alignItems:'center', gap:10, flexWrap:'wrap', marginBottom:10 } },
                    isThis ? h('span', { style:{ fontSize:11, fontWeight:700, color:TC.success, background: dark ? 'rgba(22,163,74,.18)' : 'rgba(22,163,74,.10)', padding:'3px 8px', borderRadius:999, textTransform:'uppercase', letterSpacing:'.05em' } }, 'Installed') : null,
                    meta.length ? h('span', { style:{ fontSize:12, color:mutedColor } }, meta.join(' \u00b7 ')) : null),
                h('div', { style:{ display:'flex', gap:8, flexWrap:'wrap' } }, actions),
                (isOther && locked) ? h('div', { style:{ fontSize:12, color:TC.amber, marginTop:8, lineHeight:1.5 } },
                    'This store has been customised, so demos can no longer be switched: ' + reasons.join('; ') + '.') : null,
                (isThis && entry === null) ? h('div', { style:{ fontSize:12, color:TC.amber, marginTop:8, lineHeight:1.5 } },
                    'Demo content for this theme is no longer published, so it cannot be reinstalled. Clearing sample data and removing the demo still work.') : null,
                isThis && sampleN > 0 ? h('div', { style:{ fontSize:11.5, color:mutedColor, marginTop:8, lineHeight:1.45 } },
                    'Sample data currently on this store: ' + (counts.order || 0) + ' orders, ' + (counts.customer || 0) + ' customers, ' + (counts.review || 0) + ' reviews. Clearing it keeps every product, category and image.') : null,
                (!isThis && !isOther) ? h('div', { style:{ fontSize:11.5, color:mutedColor, marginTop:8, lineHeight:1.45 } },
                    'Installs this theme\u2019s products, categories, attributes and banner images, then generates sample orders, customers and reviews so the dashboard has something to show. Installing also makes this theme the active storefront design. You can switch to another theme\u2019s demo freely until you start customising.') : null,
                h('div', { style:{ fontSize:11, color:mutedColor, marginTop:8, lineHeight:1.45 } },
                    'Demo packages are downloaded from market.titancart.io only when you click an action here. Nothing is fetched by opening this page.')
            );
        }

        return h('div', { style:{ gridColumn:'1 / -1', border:'1px solid ' + borderColor, borderRadius:10, padding:'14px 16px', background: dark ? '#1f1f1f' : '#FAFAFA' } },
            h('div', { style:{ fontSize:13, fontWeight:600, color:textColor, marginBottom:8 } }, field.label || 'Demo content'),
            body,
            err ? h('div', { style:{ fontSize:12, color:TC.error, marginTop:8 } }, err) : null,
            (job && job.status === 'done' && job.warnings && job.warnings.length) ? h('div', { style:{ fontSize:11.5, color:TC.amber, marginTop:6 } }, job.warnings.length + ' image(s) could not be downloaded and were skipped.') : null
        );
    }

    function ThemeOptionsPage({ dark, cardBg, textColor, mutedColor, borderColor, backBtn }) {
        var _schema  = useState(null);  var schema  = _schema[0];  var setSchema  = _schema[1];
        var _values  = useState({});    var values  = _values[0];  var setValues  = _values[1];
        var _loading = useState(true);  var loading = _loading[0]; var setLoading = _loading[1];
        var _saving  = useState(false); var saving  = _saving[0];  var setSaving  = _saving[1];
        var _saved   = useState(false); var saved   = _saved[0];   var setSaved   = _saved[1];
        var _err     = useState('');    var err     = _err[0];     var setErr     = _err[1];
        /* sel = the side-menu entry currently shown in the right panel:
           '__active__' = the global Active-Theme panel; otherwise a preset group id. */
        var _sel     = useState('__active__'); var sel = _sel[0];   var setSel     = _sel[1];

        function loadThemeOptions(keepSel){
            return fetch(Config.apiUrl + '/settings/theme_options', { headers:{ 'X-WP-Nonce':Config.nonce } })
                .then(function(r){ return r.json(); })
                .then(function(d){
                    var sc   = (d && d.schema)  ? d.schema  : { groups:[] };
                    var vals = (d && d.values)  ? d.values  : {};
                    var active = vals.active_preset || 'default';
                    /* Land on the active preset's own panel so the user sees "their"
                       theme's controls first; fall back to the global Active-Theme panel. */
                    var initSel = '__active__';
                    (sc.groups || []).forEach(function(g){ if (g.preset && g.preset === active) { initSel = g.id; } });
                    setSchema(sc); setValues(vals); if (!keepSel) { setSel(initSel); } setLoading(false);
                })
                .catch(function(){ setErr('Could not load theme options.'); setLoading(false); });
        }
        useEffect(function(){ loadThemeOptions(false); }, []);

        function setVal(key, v){
            setValues(function(prev){ var n = Object.assign({}, prev); n[key] = v; return n; });
            setSaved(false);
        }

        function onPresetChange(v){
            setVal('active_preset', v);
            /* Jump the side-menu to the newly activated preset's panel. */
            var gid = '';
            ((schema && schema.groups) || []).forEach(function(g){ if (g.preset === v) { gid = g.id; } });
            if (gid) { setSel(gid); }
        }

        /* pickImageUrl — open the media library (or a URL prompt when wp.media is
           absent) and hand the chosen URL to cb. Shared by the 'image' field and
           each device slot of an 'image_set' (0.2.27). */
        function pickImageUrl(cb){
            if (window.wp && window.wp.media) {
                var frame = window.wp.media({ title:'Select image', multiple:false, library:{ type:'image' } });
                frame.on('select', function(){
                    var att = frame.state().get('selection').first().toJSON();
                    cb(att.url || '');
                });
                frame.open();
            } else {
                var url = window.prompt('Image URL');
                if (url !== null) { cb(url.trim()); }
            }
        }
        function pickImage(key){ pickImageUrl(function(url){ setVal(key, url); }); }

        /* image_set (0.2.27) — value is a JSON string:
             {"desktop":{"url":"…","focus":"center"},"tablet":{…},"phone":{…}}
           A plain URL (the value a field held while it was still type 'image')
           reads as the desktop image. parseImageSet never throws; serialize
           collapses an all-blank set to '' so the server clears the row.
           0.2.28 (KI-127): a legacy plain URL takes the field's `focus_default`
           attr (a theme's historical crop anchor, e.g. Prism banner = right)
           as its desktop anchor, so the grid highlights what the storefront
           already renders and a no-change Save cannot silently re-anchor the
           image. A JSON value carries its own anchors and ignores the attr. */
        var IMG_SET_DEVICES = [
            { id:'desktop', label:'Desktop', sub:'1200px and wider' },
            { id:'tablet',  label:'Tablet',  sub:'768\u20131199px \u00b7 blank = desktop photo' },
            { id:'phone',   label:'Phone',   sub:'under 768px \u00b7 blank = tablet, then desktop' }
        ];
        var IMG_SET_FOCUS = [
            ['top-left','top','top-right'],
            ['left','center','right'],
            ['bottom-left','bottom','bottom-right']
        ];
        function isImageSetFocus(f){
            return typeof f === 'string' && IMG_SET_FOCUS.some(function(row){ return row.indexOf(f) !== -1; });
        }
        function parseImageSet(raw, focusDefault){
            var out = { desktop:{url:'',focus:'center',inherit:true}, tablet:{url:'',focus:'center',inherit:true}, phone:{url:'',focus:'center',inherit:true} };
            if (raw && typeof raw === 'object') { raw = JSON.stringify(raw); }
            if (typeof raw !== 'string') { return out; }
            raw = raw.trim();
            if (raw === '') { return out; }
            if (raw.charAt(0) !== '{') {
                out.desktop.url = raw;
                if (isImageSetFocus(focusDefault)) { out.desktop.focus = focusDefault; }
                return out;
            }
            var obj;
            try { obj = JSON.parse(raw); } catch (e) { return out; }
            if (!obj || typeof obj !== 'object') { return out; }
            IMG_SET_DEVICES.forEach(function(d){
                var slot = obj[d.id];
                if (typeof slot === 'string') { slot = { url: slot }; }
                if (!slot || typeof slot !== 'object') { return; }
                out[d.id].url   = (typeof slot.url === 'string') ? slot.url.trim() : '';
                out[d.id].focus = isImageSetFocus(slot.focus) ? slot.focus : 'center';
                /* 0.2.30 (KI-139): a blank slot follows the larger device's anchor
                   unless the owner explicitly chose one — stored as inherit:false.
                   Absent key (all pre-0.2.30 values) = follow. */
                out[d.id].inherit = !(slot.inherit === false);
            });
            return out;
        }
        function serializeImageSet(set){
            var any = IMG_SET_DEVICES.some(function(d){ return set[d.id].url !== ''; });
            if (!any) { return ''; }
            var out = {};
            IMG_SET_DEVICES.forEach(function(d){
                var s = set[d.id];
                out[d.id] = { url: s.url, focus: s.focus };
                if (s.url === '' && s.inherit === false) { out[d.id].inherit = false; } // explicit anchor on an inherited image
            });
            return JSON.stringify(out);
        }

        function save(){
            setSaving(true); setErr('');
            /* KI-127 (0.2.28): an image_set field still holding a legacy plain URL is
               sent as the serialised set with the field's `focus_default` applied, so
               the anchor the grid shows is the anchor that gets stored — the server
               applies the same default (ImageSet::sanitize), this just makes the
               PUT self-describing for any core. JSON values pass through untouched. */
            var body = Object.assign({}, values);
            ((schema && schema.groups) || []).forEach(function(g){
                (g.fields || []).forEach(function(f){
                    if (f.type !== 'image_set') { return; }
                    var v = body[f.key];
                    if (typeof v === 'string' && v.trim() !== '' && v.trim().charAt(0) !== '{') {
                        body[f.key] = serializeImageSet(parseImageSet(v, f.focus_default));
                    }
                });
            });
            fetch(Config.apiUrl + '/settings/theme_options', {
                method:'PUT', credentials:'same-origin',
                headers:{ 'Content-Type':'application/json', 'X-WP-Nonce':Config.nonce },
                body: JSON.stringify(body)
            }).then(function(r){ return r.json().then(function(j){ return { ok:r.ok, body:j }; }); })
              .then(function(res){
                  setSaving(false);
                  if (res.ok && res.body && res.body.success) { setSaved(true); }
                  else { setErr((res.body && (res.body.message || res.body.code)) || 'Save failed.'); }
              }).catch(function(e){ setSaving(false); setErr(e.message); });
        }

        var inputBase = { width:'100%', padding:'8px 11px', borderRadius:8, border:'1px solid '+borderColor,
                          background:dark?'#262626':'#fff', color:textColor, fontSize:13, outline:'none', boxSizing:'border-box' };

        /* renderField — PLAIN HELPER (NOT a nested component) so controlled inputs
           keep focus across re-renders. Returns one field cell; image + textarea +
           the active_preset picker span the full grid width. Supports
           text/select/number/image/checkbox/textarea/color so the per-preset schema
           has headroom for many more controls without a JS change. */
        function renderField(f){
            var key = f.key;
            var val = (values[key] !== undefined && values[key] !== null) ? values[key] : (f.default || '');
            var spanFull = (f.type === 'image' || f.type === 'image_set' || f.type === 'textarea' || key === 'active_preset');
            var ctl;
            if (f.type === 'image_set') {
                /* Per-device art direction (0.2.27): three device tiles, each with its
                   own picture + a 3x3 crop-anchor grid. Plain-helper markup (no nested
                   component) so the URL inputs keep focus while typing. */
                var iset = parseImageSet(val, f.focus_default);
                var setSlot = function(dev, patch){
                    var next = { desktop:Object.assign({}, iset.desktop), tablet:Object.assign({}, iset.tablet), phone:Object.assign({}, iset.phone) };
                    next[dev] = Object.assign(next[dev], patch);
                    setVal(key, serializeImageSet(next));
                };
                /* Effective (cascaded) image per device — what the storefront will show. */
                var eff = { desktop: iset.desktop.url, tablet: iset.tablet.url || iset.desktop.url };
                eff.phone = iset.phone.url || eff.tablet;
                /* 0.2.30 (KI-139): effective ANCHOR per device mirrors ImageSet::resolve():
                   own image → own anchor; blank slot → own anchor if explicit, else the
                   larger device's effective anchor. */
                var effFocus = { desktop: iset.desktop.focus };
                effFocus.tablet = iset.tablet.url !== '' ? iset.tablet.focus : (iset.tablet.inherit === false ? iset.tablet.focus : effFocus.desktop);
                effFocus.phone  = iset.phone.url  !== '' ? iset.phone.focus  : (iset.phone.inherit  === false ? iset.phone.focus  : effFocus.tablet);
                var tiles = IMG_SET_DEVICES.map(function(d){
                    var slot = iset[d.id];
                    var shown = eff[d.id];
                    var inherited = (slot.url === '' && shown !== '');
                    var following = inherited && slot.inherit !== false;       // anchor follows the larger device
                    var activeFocus = effFocus[d.id];
                    var focusGrid = h('div', { style:{ display:'grid', gridTemplateColumns:'repeat(3, 18px)', gap:3, flexShrink:0 }, title: following ? 'Following the larger device\u2019s anchor \u2014 click to choose a different part of the photo for this device' : 'Crop anchor \u2014 which part of the photo stays in view when it is cropped to fit' },
                        IMG_SET_FOCUS.reduce(function(acc, row){ return acc.concat(row); }, []).map(function(fk){
                            var on = (activeFocus === fk);
                            return h('button', { key:fk, type:'button', 'aria-label':'Anchor ' + fk.replace('-', ' ') + (on && following ? ' (following)' : ''), 'aria-pressed':on,
                                onClick:function(){ setSlot(d.id, { focus: fk, inherit: false }); },
                                style:{ width:18, height:18, padding:0, borderRadius:4, cursor:'pointer',
                                        border:'1px solid ' + (on ? TC.accent : borderColor),
                                        background: on ? (following ? 'transparent' : TC.accent) : (dark ? '#2a2a2a' : '#F3F4F6'),
                                        boxShadow: (on && following) ? ('inset 0 0 0 3px ' + TC.accent + '66') : 'none' } });
                        }));
                    var followNote = inherited ? h('div', { style:{ flexBasis:'100%', fontSize:11.5, color:mutedColor, display:'flex', gap:8, alignItems:'center' } },
                        following
                            ? 'Following ' + (d.id === 'phone' && iset.tablet.url !== '' ? 'Tablet' : 'Desktop') + '\u2019s anchor (' + activeFocus.replace('-', ' ') + ')'
                            : h('span', null, 'Own anchor (' + activeFocus.replace('-', ' ') + ') on the larger photo \u2014 ',
                                h('a', { href:'#', onClick:function(e){ e.preventDefault(); setSlot(d.id, { inherit: true, focus:'center' }); }, style:{ color:TC.accent } }, 'follow the larger device again'))
                    ) : null;
                    return h('div', { key:d.id, style:{ border:'1px solid '+borderColor, borderRadius:10, padding:'12px 14px', background:dark?'#1f1f1f':'#FAFAFA' } },
                        h('div', { style:{ display:'flex', alignItems:'baseline', gap:8, marginBottom:8 } },
                            h('span', { style:{ fontSize:12.5, fontWeight:600, color:textColor } }, d.label),
                            h('span', { style:{ fontSize:11, color:mutedColor } }, d.sub)),
                        h('div', { style:{ display:'flex', alignItems:'center', gap:10, flexWrap:'wrap' } },
                            shown ? h('img', { src:shown, style:{ width:56, height:56, objectFit:'cover', objectPosition: activeFocus.replace('-', ' '), borderRadius:8, border:'1px solid '+borderColor, opacity: inherited ? .45 : 1 } })
                                  : h('div', { style:{ width:56, height:56, borderRadius:8, border:'1px dashed '+borderColor, flexShrink:0 } }),
                            h('input', { type:'text', value:slot.url, placeholder: inherited ? 'Using the larger photo' : 'Image URL', onChange:function(e){ setSlot(d.id, { url:e.target.value }); },
                                style: Object.assign({}, inputBase, { flex:'1 1 180px', minWidth:0 }) }),
                            h('button', { type:'button', onClick:function(){ pickImageUrl(function(url){ setSlot(d.id, { url:url }); }); },
                                style:{ padding:'8px 12px', borderRadius:8, border:'1px solid '+borderColor, background:'transparent', color:textColor, fontSize:12, cursor:'pointer' } }, 'Choose\u2026'),
                            slot.url ? h('button', { type:'button', onClick:function(){ setSlot(d.id, { url:'' }); },
                                style:{ padding:'8px 12px', borderRadius:8, border:'1px solid '+borderColor, background:'transparent', color:TC.error, fontSize:12, cursor:'pointer' } }, 'Remove') : null,
                            focusGrid,
                            followNote)
                    );
                });
                ctl = h('div', { style:{ display:'grid', gridTemplateColumns:'repeat(auto-fit, minmax(300px, 1fr))', gap:12 } }, tiles);
            } else if (f.type === 'demo') {
                /* Prism demo installer — stateful module-scope component (S471). Its
                   onChanged reloads values in place (active_preset + scope keys change
                   server-side on install) without resetting the side-menu selection. */
                return h(DemoInstallPanel, { key:key, field:f, dark:dark, textColor:textColor, mutedColor:mutedColor, borderColor:borderColor,
                    onChanged:function(){ loadThemeOptions(true); } });
            }
            if (f.type === 'note') {
                return h('div', { key:key, style:{ gridColumn:'1 / -1', marginBottom:0 } },
                    f.label ? h('div', { style:{ fontSize:13, fontWeight:500, color:textColor, marginBottom:4 } }, f.label) : null,
                    h('div', { style:{ fontSize:12.5, color:mutedColor, lineHeight:1.5 } }, f.hint || '')
                );
            }
            if (f.type === 'select') {
                ctl = h('select', { value:val,
                    onChange:function(e){ key === 'active_preset' ? onPresetChange(e.target.value) : setVal(key, e.target.value); },
                    style: Object.assign({}, inputBase, { cursor:'pointer', appearance:'auto', maxWidth:300 }) },
                    (f.options || []).map(function(o){ return h('option', { key:o.value, value:o.value }, o.label); }));
            } else if (f.type === 'number') {
                ctl = h('input', { type:'number', value:val, onChange:function(e){ setVal(key, e.target.value); },
                    style: Object.assign({}, inputBase, { maxWidth:160 }) });
            } else if (f.type === 'color') {
                ctl = h('div', { style:{ display:'flex', alignItems:'center', gap:10 } },
                    h('input', { type:'color', value:val || '#000000', onChange:function(e){ setVal(key, e.target.value); },
                        style:{ width:44, height:32, padding:0, border:'1px solid '+borderColor, borderRadius:8, background:'transparent', cursor:'pointer' } }),
                    h('input', { type:'text', value:val, placeholder:'#RRGGBB', onChange:function(e){ setVal(key, e.target.value); },
                        style: Object.assign({}, inputBase, { maxWidth:140 }) })
                );
            } else if (f.type === 'checkbox') {
                var isOn = (val === '1' || val === 1 || val === true);
                return h('div', { key:key, style:{ marginBottom:0 } },
                    h('label', { style:{ display:'flex', alignItems:'center', gap:10, cursor:'pointer' } },
                        h('div', { onClick:function(){ setVal(key, isOn ? '0' : '1'); },
                            style:{ width:40, height:22, borderRadius:12, cursor:'pointer', background:isOn?TC.accent:(dark?'#333':'#D4D4D4'), transition:'background .15s', position:'relative', flexShrink:0 } },
                            h('div', { style:{ width:18, height:18, borderRadius:'50%', background:'#fff', position:'absolute', top:2, left:isOn?20:2, transition:'left .15s', boxShadow:'0 1px 3px rgba(0,0,0,.2)' } })
                        ),
                        h('span', { style:{ fontSize:13, color:textColor } }, f.label)
                    ),
                    f.hint ? h('div', { style:{ fontSize:11.5, color:mutedColor, marginTop:5, marginLeft:50, lineHeight:1.45 } }, f.hint) : null
                );
            } else if (f.type === 'textarea') {
                ctl = h('textarea', { value:val, rows:3, onChange:function(e){ setVal(key, e.target.value); },
                    style: Object.assign({}, inputBase, { resize:'vertical', minHeight:72 }) });
            } else if (f.type === 'image') {
                ctl = h('div', { style:{ display:'flex', alignItems:'center', gap:10, flexWrap:'wrap' } },
                    val ? h('img', { src:val, style:{ width:56, height:56, objectFit:'cover', borderRadius:8, border:'1px solid '+borderColor } }) : null,
                    h('input', { type:'text', value:val, placeholder:'Image URL', onChange:function(e){ setVal(key, e.target.value); },
                        style: Object.assign({}, inputBase, { maxWidth:340 }) }),
                    h('button', { onClick:function(){ pickImage(key); },
                        style:{ padding:'8px 12px', borderRadius:8, border:'1px solid '+borderColor, background:'transparent', color:textColor, fontSize:12, cursor:'pointer' } }, 'Choose\u2026'),
                    val ? h('button', { onClick:function(){ setVal(key, ''); },
                        style:{ padding:'8px 12px', borderRadius:8, border:'1px solid '+borderColor, background:'transparent', color:TC.error, fontSize:12, cursor:'pointer' } }, 'Remove') : null
                );
            } else if (f.type === 'multiselect') {
                /* Checkbox list. Value is a CSV of ids (matches the plugin
                   'multiselect' sanitizer). Self-returns a full-width cell. */
                var msSel = (typeof val === 'string' && val !== '')
                    ? val.split(',').map(function(s){ return s.trim(); }).filter(Boolean)
                    : (Array.isArray(val) ? val.map(String) : []);
                var msOpts = f.options || [];
                var toggleMs = function(ov){
                    var cur = msSel.slice();
                    var i = cur.indexOf(String(ov));
                    if (i === -1) { cur.push(String(ov)); } else { cur.splice(i, 1); }
                    setVal(key, cur.join(','));
                };
                var msBody = msOpts.length
                    ? h('div', { style:{ display:'flex', flexDirection:'column', gap:6, maxHeight:240, overflowY:'auto',
                        border:'1px solid '+borderColor, borderRadius:8, padding:'10px 12px', background:dark?'#262626':'#fff' } },
                        msOpts.map(function(o){
                            var on = msSel.indexOf(String(o.value)) !== -1;
                            return h('label', { key:o.value, style:{ display:'flex', alignItems:'center', gap:9, cursor:'pointer', fontSize:13, color:textColor } },
                                h('input', { type:'checkbox', checked:on, onChange:function(){ toggleMs(o.value); }, style:{ accentColor:TC.accent, cursor:'pointer' } }),
                                h('span', null, o.label)
                            );
                        }))
                    : h('div', { style:{ fontSize:12.5, color:mutedColor, padding:'10px 12px', border:'1px dashed '+borderColor, borderRadius:8 } },
                        'No categories found. Add categories in TitanCart first, then return here.');
                return h('div', { key:key, style:{ marginBottom:0, gridColumn:'1 / -1' } },
                    h('label', { style:{ display:'block', fontSize:13, fontWeight:500, color:textColor, marginBottom:6 } }, f.label),
                    msBody,
                    f.hint ? h('div', { style:{ fontSize:11.5, color:mutedColor, marginTop:5, lineHeight:1.45 } }, f.hint) : null
                );
            } else if (f.type === 'datetime') {
                ctl = h('input', { type:'datetime-local', value:val, onChange:function(e){ setVal(key, e.target.value); },
                    style: Object.assign({}, inputBase, { maxWidth:260 }) });
            } else if (f.type === 'image_set') {
                /* ctl already built above (kept out of this chain so the tile
                   markup sits next to its helpers) — do NOT fall into the text default. */
            } else {
                ctl = h('input', { type:'text', value:val, onChange:function(e){ setVal(key, e.target.value); },
                    style: Object.assign({}, inputBase, { maxWidth:340 }) });
            }
            return h('div', { key:key, style:{ marginBottom:0, gridColumn: spanFull ? '1 / -1' : 'auto' } },
                h('label', { style:{ display:'block', fontSize:13, fontWeight:500, color:textColor, marginBottom:6 } }, f.label),
                ctl,
                f.hint ? h('div', { style:{ fontSize:11.5, color:mutedColor, marginTop:5, lineHeight:1.45 } }, f.hint) : null
            );
        }

        /* Lay a group's fields out grouped by section sub-heading, each a responsive
           grid (the full-page panel). Fields with no `section` render first under no
           heading (e.g. the Active Theme group's preset picker + accent). Plain helper
           — returns elements, never rendered via h(Comp), so no focus loss. */
        function renderFields(fields){
            fields = fields || [];
            var order = [];
            var bySection = {};
            fields.forEach(function(f){
                var sec = f.section || '';
                if (!(sec in bySection)) { bySection[sec] = []; order.push(sec); }
                bySection[sec].push(f);
            });
            function grid(fs){
                return h('div', { style:{ display:'grid', gridTemplateColumns:'repeat(auto-fit, minmax(260px, 1fr))', gap:'18px 24px', alignItems:'start' } },
                    fs.map(renderField));
            }
            return h('div', null, order.map(function(sec){
                if (sec === '') {
                    return h('div', { key:'__nosec', style:{ marginBottom:S.lg } }, grid(bySection[sec]));
                }
                return h('div', { key:sec, style:{ marginBottom:S.xl } },
                    h('h4', { style:{ fontSize:12.5, fontWeight:700, color:TC.accent, textTransform:'uppercase', letterSpacing:'.06em', margin:'0 0 '+S.base+'px', padding:'9px 12px', background:(dark?'rgba(37,99,235,0.16)':'rgba(37,99,235,0.07)'), borderLeft:'3px solid '+TC.accent, borderRadius:6 } }, sec),
                    grid(bySection[sec])
                );
            }));
        }

        if (loading) {
            return h(PageWrap, { title:'Theme Options', sub:'Loading\u2026', mutedColor:mutedColor, backBtn:backBtn });
        }

        var groups       = (schema && schema.groups) ? schema.groups : [];
        var activeGroup  = groups.filter(function(g){ return !g.preset; })[0];
        var presetGroups = groups.filter(function(g){ return !!g.preset; });
        var activePreset = values.active_preset || 'default';

        /* ── Left side-menu: "Active Theme" (global picker) on top, then one entry
           per preset; the active preset is badged. Selecting an entry swaps the
           right panel (local state, NOT hash routing — it's one settings section). ── */
        var navWidth = 210;
        function navItem(id, label, badged){
            var selected = (sel === id);
            return h('div', { key:id, onClick:function(){ setSel(id); },
                style:{ display:'flex', alignItems:'center', justifyContent:'space-between', gap:8,
                        padding:'9px 14px', borderRadius:8, marginBottom:2, cursor:'pointer', fontSize:13,
                        fontWeight: selected ? 600 : 400,
                        color: selected ? TC.accent : mutedColor,
                        background: selected ? (dark?'rgba(37,99,235,.12)':'rgba(37,99,235,.06)') : 'transparent',
                        transition:'all .12s' },
                onMouseEnter:function(e){ if (!selected) e.currentTarget.style.background = dark?'rgba(255,255,255,.03)':'rgba(0,0,0,.02)'; },
                onMouseLeave:function(e){ if (!selected) e.currentTarget.style.background = 'transparent'; } },
                h('span', null, label),
                badged ? h('span', { style:{ fontSize:9.5, fontWeight:600, letterSpacing:'.04em', textTransform:'uppercase', color:TC.accent, background:TC.accent+'1A', padding:'2px 7px', borderRadius:999 } }, 'Active') : null
            );
        }

        var leftNav = h('div', { style:{ width:navWidth, flexShrink:0, padding:'20px 0 20px 16px', borderRight:'1px solid '+borderColor } },
            navItem('__active__', 'Active Theme', false),
            presetGroups.length ? h('div', { style:{ height:1, background:borderColor, margin:'10px 14px 12px', opacity:.7 } }) : null,
            presetGroups.length ? h('div', { style:{ fontSize:10.5, fontWeight:600, letterSpacing:'.05em', textTransform:'uppercase', color:mutedColor, padding:'0 14px', marginBottom:8 } }, 'Themes') : null,
            presetGroups.map(function(g){ return navItem(g.id, g.title, g.preset === activePreset); })
        );

        /* ── Right panel: the selected entry's full-page controls. ── */
        var panel;
        if (sel === '__active__') {
            panel = h('div', null,
                h('h3', { style:{ fontSize:15, fontWeight:600, color:textColor, marginBottom:S.lg, paddingBottom:S.sm, borderBottom:'1px solid '+borderColor } }, 'Active Theme'),
                activeGroup ? renderFields(activeGroup.fields) : h('div', { style:{ fontSize:13, color:mutedColor } }, 'No theme has registered options.'),
                h('div', { style:{ fontSize:12, color:mutedColor, marginTop:S.lg } },
                    'Changes apply to the storefront after saving. You can also flip presets in-context from the floating switcher on the storefront.')
            );
        } else {
            var grp = presetGroups.filter(function(g){ return g.id === sel; })[0];
            if (!grp) {
                panel = h(EmptyState, { mutedColor:mutedColor, icon:'\uD83C\uDFA8', title:'Select a theme', sub:'Choose a theme from the left to configure its controls.' });
            } else {
                var grpActive = (grp.preset === activePreset);
                panel = h('div', null,
                    h('div', { style:{ display:'flex', alignItems:'center', justifyContent:'space-between', gap:S.md, marginBottom:S.lg, paddingBottom:S.sm, borderBottom:'1px solid '+borderColor, flexWrap:'wrap' } },
                        h('div', { style:{ display:'flex', alignItems:'center', gap:10 } },
                            h('h3', { style:{ fontSize:15, fontWeight:600, color:textColor } }, grp.title),
                            grpActive ? h('span', { style:{ fontSize:10, fontWeight:600, letterSpacing:'.04em', textTransform:'uppercase', color:TC.accent, background:TC.accent+'1A', padding:'2px 8px', borderRadius:999 } }, 'Active theme') : null
                        ),
                        grpActive ? null : h('button', { onClick:function(){ onPresetChange(grp.preset); },
                            style:{ padding:'7px 13px', borderRadius:8, border:'1px solid '+TC.accent, background:'transparent', color:TC.accent, fontSize:12, fontWeight:500, cursor:'pointer' } }, 'Set as active theme')
                    ),
                    (grp.fields && grp.fields.length) ? renderFields(grp.fields) : h('div', { style:{ fontSize:13, color:mutedColor } }, 'This theme has no configurable controls yet.')
                );
            }
        }

        return h('main', { style:{ flex:1, overflow:'auto', padding:S.xxl } },
            h('div', { style:{ display:'flex', justifyContent:'space-between', alignItems:'center', marginBottom:S.xl } },
                h('div', { style:{ display:'flex', alignItems:'center', gap:S.md } },
                    backBtn,
                    h('div', null,
                        h('h2', { style:{ fontSize:20, fontWeight:600, letterSpacing:'-.3px', marginBottom:1 } }, 'Theme Options'),
                        h('div', { style:{ fontSize:13, color:mutedColor } }, 'Storefront preset and per-preset design controls.')
                    )
                ),
                h('div', { style:{ display:'flex', alignItems:'center', gap:S.md } },
                    saved && h('span', { style:{ fontSize:12, color:TC.success, fontWeight:500 } }, '\u2713 Saved'),
                    err && h('span', { style:{ fontSize:12, color:TC.error, fontWeight:500 } }, err),
                    h(Btn, { variant:'primary', onClick:save, disabled:saving, mutedColor:mutedColor, borderColor:borderColor },
                        saving ? 'Saving\u2026' : 'Save Changes')
                )
            ),
            /* Two-column layout mirroring General Settings: left nav + right panel inside a card */
            h(Card, { cardBg:cardBg, borderColor:borderColor, dark:dark, pad:0 },
                h('div', { style:{ display:'flex', minHeight:500 } },
                    leftNav,
                    h('div', { style:{ flex:1, padding:24, overflow:'auto' } }, panel)
                )
            )
        );
    }

    function SettingsPage({ subId, dark, cardBg, textColor, mutedColor, borderColor, tableHeaderBg }) {
        var subProps = { dark:dark, cardBg:cardBg, textColor:textColor, mutedColor:mutedColor, borderColor:borderColor, tableHeaderBg:tableHeaderBg };

        /* KI-36 — hiding the Account & License card does not unmake its route.
           AccountPage is backed by GET /license, which the Lite package strips.
           Collapse the sub-route to the card grid BEFORE any dispatch: falling
           through instead would reach the generic `if (subId) → SettingsSubPage`
           branch below, which fetches a /settings/account schema section that
           has never existed. A dead page is not an improvement on a dead menu
           item. Inert on every store where noPhoneHome is false. */
        if (subId === 'account' && Config.noPhoneHome) { subId = null; }

        /* Reusable back button element — passed as prop so each page integrates it into its own header */
        var backEl = h('button', { onClick:function(){ navigate('settings'); },
            style:{ background:'transparent', border:'1px solid '+borderColor, borderRadius:7, padding:'5px 12px', fontSize:12, cursor:'pointer', color:mutedColor, display:'flex', alignItems:'center', gap:4, flexShrink:0 } },
            h('span', { style:{ fontSize:14 } }, '\u2190'), 'Settings'
        );

        /* Pages with real components */
        var standalonePages = {
            'currencies':    CurrenciesPage,
            'account':       AccountPage,
            'profile-fields':CustomerFieldsPage,
            'statuses':      OrderStatusesPage,
            'logs':          SystemLogsPage,
            'payments':      PaymentsPage,
            'shipping':      ShippingPage,
            'tax':           TaxPage,
            'email':         EmailPage,
            'import-export': ImportExportPage,
            'woo-import':    WooImportPage,
            'marketplace':   MarketplaceSettingsPage,
            'theme-options': ThemeOptionsPage,
        };

        /* Stub pages (not yet built) */
        var stubPages = {
        };

        /* S454 (KI-36 shape, self-adapting): 'social-login' and 'api' are real
           standalonePages rows in Pro, but the Lite build strips both rows AND
           both page components. A typed #/settings/social-login hash would then
           fall through to the generic SettingsSubPage branch below and fetch a
           /settings/{section} schema that does not exist. When the row is absent
           from standalonePages, collapse to the card grid instead — on Pro the
           rows exist, so this line never fires. A dead page is not an
           improvement on a dead menu item. */
        if ((subId === 'social-login' || subId === 'api') && !standalonePages[subId]) { subId = null; }

        if (subId && standalonePages[subId]) {
            return h(standalonePages[subId], Object.assign({}, subProps, { backBtn: backEl }));
        }

        if (subId && stubPages[subId]) {
            var stubTitle = stubPages[subId];
            return h(PageWrap, { title:stubTitle, sub:'Coming soon', mutedColor:mutedColor, backBtn:backEl },
                h(Card, { cardBg:cardBg, borderColor:borderColor, dark:dark },
                    h(EmptyState, { mutedColor:mutedColor, icon:'\uD83D\uDEA7', title:stubTitle, sub:'This section will be available in a future update.' })
                )
            );
        }

        /* S129 Phase 2 — Download Agreements has a custom (non schema-driven)
           layout; intercept BEFORE SettingsSubPage so we don't try to fetch
           a /settings/download-agreements section that doesn't exist. */
        if (subId === 'download-agreements') {
            return h(DownloadAgreementsPage, subProps);
        }

        /* S313 — TitanCart Essentials hub (manifest-driven, non schema-driven);
           intercept BEFORE SettingsSubPage so we don't try to fetch a
           /settings/essentials section that doesn't exist. Matches both
           settings/essentials and settings/essentials/<feature_id>. */
        if (subId === 'essentials') {
            return h(EssentialsPage, subProps);
        }

        /* Schema-driven sub-pages (General, Appearance, etc.) — SettingsSubPage has its own back button */
        if (subId) {
            return h(SettingsSubPage, subProps);
        }

        // ── Settings card definitions ─────────────────────────────────────────
        var CARDS = [
            {
                slug:'general',
                icon: ico([h('path',{d:'M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2'}),h('circle',{cx:12,cy:7,r:4})], 20, '0 0 24 24'),
                title:'General Settings',
                desc:'Configure your company info and basic principles of how your site works.',
            },
            {
                slug:'theme-options',
                icon: ico([h('path',{d:'M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z'}),h('circle',{cx:12,cy:12,r:3})], 20, '0 0 24 24'),
                title:'Theme Options',
                desc:'Choose the active storefront theme preset and configure its per-preset design controls.',
                requires:'themeOptionsAvailable',
            },
            {
                slug:'payments',
                icon: ico([h('rect',{x:2,y:5,width:20,height:14,rx:2}),h('path',{d:'M2 10h20'}),h('path',{d:'M6 15h4'})], 20, '0 0 24 24'),
                title:'Payment Gateways',
                desc:'Set up the payment options available to your customers.',
            },
            {
                slug:'shipping',
                icon: ico([h('path',{d:'M14 18V6a2 2 0 0 0-2-2H4a2 2 0 0 0-2 2v11a1 1 0 0 0 1 1h2'}),h('path',{d:'M15 18H9'}),h('path',{d:'M19 18h2a1 1 0 0 0 1-1v-3.65a1 1 0 0 0-.22-.62l-3.48-4.35A1 1 0 0 0 17.52 8H14'}),h('circle',{cx:7,cy:18,r:2}),h('circle',{cx:17,cy:18,r:2})], 20, '0 0 24 24'),
                title:'Shipping Methods',
                desc:'Set up shipping options, their rates and availability by location.',
            },
            {
                slug:'tax',
                icon: ico([h('circle',{cx:12,cy:12,r:10}),h('path',{d:'M16 8h-6a2 2 0 1 0 0 4h4a2 2 0 1 1 0 4H8'}),h('path',{d:'M12 18V6'})], 20, '0 0 24 24'),
                title:'Tax Rules',
                desc:'Configure what taxes you show, how you do it, and tax rates by location.',
            },
            {
                slug:'currencies',
                icon: ico([h('path',{d:'M12 2C6.5 2 2 6.5 2 12s4.5 10 10 10 10-4.5 10-10S17.5 2 12 2z'}),h('path',{d:'M12 6v2m0 8v2m-4-6h8'})], 20, '0 0 24 24'),
                title:'Currencies',
                desc:'Set up the currencies in which you show the prices to customers.',
            },
            {
                slug:'account',
                icon: ico([h('rect',{x:3,y:11,width:18,height:11,rx:2}),h('path',{d:'M7 11V7a5 5 0 0 1 10 0v4'})], 20, '0 0 24 24'),
                title:'Account & License',
                desc:'Manage your TitanCart license, view plan details, and activate your store.',
                /* KI-36 — src/REST/License/ is excluded from the Lite package, so
                   GET /license 404s there and this card leads nowhere. Hidden,
                   not lock+badged: there is no capability to upsell, the route
                   simply does not exist. */
                hiddenWhen:'noPhoneHome',
            },
            {
                slug:'statuses',
                icon: ico([h('path',{d:'M9 11l3 3L22 4'}),h('path',{d:'M21 12v7a2 2 0 01-2 2H5a2 2 0 01-2-2V5a2 2 0 012-2h11'})], 20, '0 0 24 24'),
                title:'Order Statuses',
                desc:'Edit the possible statuses for orders, shipments, etc.',
            },
            {
                slug:'email',
                icon: ico([h('rect',{x:2,y:4,width:20,height:16,rx:2}),h('path',{d:'m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7'})], 20, '0 0 24 24'),
                title:'Notifications',
                desc:'Manage the notifications sent to customers, administrators and sellers.',
            },
            {
                slug:'profile-fields',
                icon: ico([h('path',{d:'M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z'}),h('path',{d:'M14 2v6h6'}),h('path',{d:'M8 13h8'}),h('path',{d:'M8 17h5'})], 20, '0 0 24 24'),
                title:'Customer Fields',
                desc:'Manage information collected from customers and sellers.',
            },
            {
                slug:'import-export',
                icon: ico([h('path',{d:'M8 21V7'}),h('path',{d:'M4 11l4-4 4 4'}),h('path',{d:'M16 3v14'}),h('path',{d:'M12 13l4 4 4-4'})], 20, '0 0 24 24'),
                title:'Import / Export',
                desc:'Bulk import and export products, orders, customers and more via CSV.',
            },
            {
                slug:'addons',
                icon: ico([h('path',{d:'M19.439 7.85c-.049.322.059.648.289.878l1.568 1.568c.47.47.706 1.087.706 1.704s-.235 1.233-.706 1.704l-1.611 1.611a.98.98 0 0 1-.837.276c-.47-.07-.802-.48-.968-.925a2.501 2.501 0 1 0-3.214 3.214c.446.166.855.497.925.968a.979.979 0 0 1-.276.837l-1.61 1.61a2.404 2.404 0 0 1-1.705.707 2.402 2.402 0 0 1-1.704-.706l-1.568-1.568a1.026 1.026 0 0 0-.877-.29c-.493.074-.84.504-1.02.968a2.5 2.5 0 1 1-3.237-3.237c.464-.18.894-.527.967-1.02a1.026 1.026 0 0 0-.289-.877l-1.568-1.568A2.402 2.402 0 0 1 1.998 12c0-.617.236-1.234.706-1.704L4.23 8.77c.24-.24.581-.353.917-.303.515.077.877.528 1.073 1.01a2.5 2.5 0 1 0 3.259-3.259c-.482-.196-.933-.558-1.01-1.073-.05-.336.063-.676.303-.917l1.525-1.525A2.402 2.402 0 0 1 12 1.998c.617 0 1.234.236 1.704.706l1.568 1.568c.23.23.556.338.877.29.493-.074.84-.504 1.02-.968a2.5 2.5 0 1 1 3.237 3.237c-.464.18-.894.527-.967 1.02Z'})], 20, '0 0 24 24'),
                title:'Extensions',
                desc:'Manage built-in extensions, purchase new ones, and check for updates.',
            },
            /* S157 — Marketplace card. The `requires` field gates visibility
               by checking a Config flag set in TC core's Admin/Assets.php
               (analogous to `subscriptionsActive`). The card is visible
               whenever the plugin is INSTALLED (regardless of license state)
               — license state controls the form-lock UX once you click in,
               not the card's visibility. On installs without the marketplace
               plugin the constant TCMP_VERSION is undefined, the Config key
               is `false`, and the card is hidden entirely. */
            {
                slug:'marketplace',
                icon: ico([h('path',{d:'M3 9l1-5h16l1 5'}),h('path',{d:'M4 9v10a1 1 0 001 1h14a1 1 0 001-1V9'}),h('path',{d:'M9 14h6'}),h('path',{d:'M3 9c0 2 1.5 3 3 3s3-1 3-3'}),h('path',{d:'M9 9c0 2 1.5 3 3 3s3-1 3-3'}),h('path',{d:'M15 9c0 2 1.5 3 3 3s3-1 3-3'})], 20, '0 0 24 24'),
                title:'Marketplace',
                desc:'Configure marketplace mode (digital or physical), branding, and master enable toggle.',
                requires:'marketplaceInstalled',
            },
            {
                slug:'logs',
                icon: ico([h('path',{d:'M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z'}),h('path',{d:'M14 2v6h6'}),h('path',{d:'M8 10h8'}),h('path',{d:'M8 14h4'})], 20, '0 0 24 24'),
                title:'System Logs',
                desc:'Store activity log.',
            },
            {
                slug:'download-agreements',
                icon: ico([h('path',{d:'M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z'}),h('path',{d:'M14 2v6h6'}),h('path',{d:'M9 13h6'}),h('path',{d:'M9 17h4'})], 20, '0 0 24 24'),
                title:'Download Agreements',
                desc:'License agreements presented to customers when buying downloadable products. Supports a global default and per-product overrides.',
            },
            {
                slug:'upgrade',
                icon: ico([h('path',{d:'M12 2v10'}),h('path',{d:'M8 6l4-4 4 4'}),h('path',{d:'M2 17l.6 2.4A2 2 0 004.5 21h11a2 2 0 001.9-1.6L18 17'}),h('path',{d:'M2 17h16'})], 20, '0 0 24 24'),
                title:'Update Center',
                desc:'Get the latest versions of TitanCart and its extensions.',
                /* S454 (KI-36, same shape as the Account & License card): the
                   /extensions/check-updates + /extensions/update routes are
                   stripped from the Lite package and the 'updates' nav node is
                   phoneHome-hidden — but this card routed there via openCard's
                   topLevel map regardless, landing on a Stub. Hidden, not
                   lock+badged: the routes simply do not exist. Inert on every
                   store where noPhoneHome is false. */
                hiddenWhen:'noPhoneHome',
            },
            {
                slug:'woo-import',
                icon: ico([h('rect',{x:3,y:3,width:18,height:18,rx:2}),h('path',{d:'M9 3v18'}),h('path',{d:'M15 3v18'}),h('path',{d:'M3 9h18'}),h('path',{d:'M3 15h18'})], 20, '0 0 24 24'),
                title:'WooCommerce Migration',
                desc:'Detect WooCommerce and transfer your existing products, orders, and customers into TitanCart.',
            },
            /* S313 — TitanCart Essentials (free bundle). Gated on the
               essentialsInstalled Config flag (set in Admin/Assets.php when the
               plugin is active). openCard routes it to settings/essentials,
               which the SettingsPage dispatch intercepts to EssentialsPage. */
            {
                slug:'essentials',
                icon: ico([h('path',{d:'M12 2l9 5-9 5-9-5 9-5z'}),h('path',{d:'M3 12l9 5 9-5'}),h('path',{d:'M3 17l9 5 9-5'})], 20, '0 0 24 24'),
                title:'TitanCart Essentials',
                desc:'A free bundle of mini-features - low-stock urgency badges, a sticky add-to-cart bar, and more. Each has its own on/off switch.',
                requires:'essentialsInstalled',
            },
        ];

        // Route the card click — most go to settings sub-pages, some to top-level
        function openCard(slug) {
            var topLevel = { 'addons':'installed-extensions', 'upgrade':'updates' };
            navigate(topLevel[slug] ? topLevel[slug] : 'settings/' + slug);
        }

        // ── Card grid ─────────────────────────────────────────────────────────
        return h('main', { style:{ flex:1, overflow:'auto', padding:S.xxl } },
            h('div', { style:{ display:'flex', justifyContent:'space-between', alignItems:'center', marginBottom:S.xl } },
                h('h2', { style:{ fontSize:22, fontWeight:700, letterSpacing:'-.3px' } }, 'Settings')
            ),
            h('div', { style:{ display:'grid', gridTemplateColumns:'repeat(3, 1fr)', gap:S.lg } },
                CARDS.filter(function(card){
                    /* S157 — gate plugin-provided cards behind a Config flag.
                       Cards with no `requires` always render. Cards WITH a
                       `requires` only render when Config[card.requires] is
                       truthy — this is set by TC core's Admin/Assets.php
                       (e.g. `marketplaceInstalled` via `defined('TCMP_VERSION')`).
                       Same shape as the existing `subscriptionsActive` pattern.

                       S358 — `hiddenWhen` is the inverse: a card with a
                       `hiddenWhen` is suppressed when Config[card.hiddenWhen] is
                       truthy. Needed because `requires` can only express "show
                       if present", and the Lite gate is a fact about a route's
                       ABSENCE. One Config key (`noPhoneHome`), one meaning — not
                       a second positive flag that could drift out of step. */
                    if (card.hiddenWhen && !!Config[card.hiddenWhen]) return false;
                    return !card.requires || !!Config[card.requires];
                }).map(function(card) {
                    return h('div', { key:card.slug,
                        onClick:function(){openCard(card.slug);},
                        style:{ display:'flex', alignItems:'flex-start', gap:S.lg, padding:S.xl,
                                background:cardBg, borderRadius:12, border:'1px solid '+borderColor,
                                boxShadow:cardShadow(dark), cursor:'pointer', transition:'all .15s ease' },
                        onMouseEnter:function(e){
                            e.currentTarget.style.borderColor = TC.accent + '60';
                            e.currentTarget.style.transform   = 'translateY(-1px)';
                        },
                        onMouseLeave:function(e){
                            e.currentTarget.style.borderColor = borderColor;
                            e.currentTarget.style.transform   = 'translateY(0)';
                        },
                    },
                        h('div', { style:{ width:40, height:40, borderRadius:10, background:TC.accent,
                                           display:'flex', alignItems:'center', justifyContent:'center',
                                           flexShrink:0, color:'#fff' } },
                            card.icon
                        ),
                        h('div', null,
                            h('div', { style:{ fontSize:14, fontWeight:600, marginBottom:5, color:textColor } }, card.title),
                            h('div', { style:{ fontSize:12, color:mutedColor, lineHeight:1.5 } }, card.desc)
                        )
                    );
                })
            )
        );
    }

    /* ════════════════════════════════════════════════════════════════════
     * NOTIFICATIONS PAGE — 4 tabs: Customer / Admin / Email Templates / Documents
     * Replaces the old EmailPage stub. Function name kept as EmailPage for router compat.
     * ════════════════════════════════════════════════════════════════════ */
    function EmailPage({ dark, cardBg, textColor, mutedColor, borderColor, tableHeaderBg, backBtn }) {
        var _tab = useState('customer'); var tab = _tab[0]; var setTab = _tab[1];

        /* ── Shared state ── */
        var _events      = useState([]); var events       = _events[0];      var setEvents      = _events[1];
        var _settings    = useState({}); var settings     = _settings[0];    var setSettings    = _settings[1];
        var _templates   = useState([]); var templates    = _templates[0];   var setTemplates   = _templates[1];
        var _documents   = useState([]); var documents    = _documents[0];   var setDocuments   = _documents[1];
        var _loading     = useState(true); var loading   = _loading[0];     var setLoading     = _loading[1];
        var _saving      = useState(false); var saving   = _saving[0];     var setSaving      = _saving[1];
        var _saved       = useState(false); var saved    = _saved[0];      var setSaved       = _saved[1];
        var _dirty       = useState(false); var dirty    = _dirty[0];      var setDirty       = _dirty[1];
        var _tplTab      = useState('customer'); var tplTab = _tplTab[0]; var setTplTab = _tplTab[1];
        var _editing     = useState(null); var editing  = _editing[0];    var setEditing     = _editing[1];
        var _tplForm     = useState({ subject:'', body_html:'' }); var tplForm = _tplForm[0]; var setTplForm = _tplForm[1];
        var _tplSaving   = useState(false); var tplSaving = _tplSaving[0]; var setTplSaving = _tplSaving[1];
        var _tplSaved    = useState(false); var tplSaved  = _tplSaved[0];  var setTplSaved  = _tplSaved[1];
        var _editorTab   = useState('code'); var editorTab = _editorTab[0]; var setEditorTab = _editorTab[1];

        /* Test Email tab state (must be at component top level — hooks rule) */
        var _testTo   = useState(''); var testTo = _testTo[0]; var setTestTo = _testTo[1];
        var _testType = useState('order_confirmation'); var testType = _testType[0]; var setTestType = _testType[1];
        var _testSending = useState(false); var testSending = _testSending[0]; var setTestSending = _testSending[1];
        var _testResult  = useState(null);  var testResult  = _testResult[0];  var setTestResult  = _testResult[1];

        var fs = fieldSty(dark, borderColor);
        var labelSty = { fontSize:12, fontWeight:500, display:'block', marginBottom:S.xs, color:mutedColor };

        /* ── Sample data for preview ── */
        var SAMPLE_DATA = {
            '{site_name}':       'TitanCart Demo Store',
            '{site_url}':        'https://store.example.com',
            '{order_id}':        '4821',
            '{order_status}':    'Paid',
            '{order_total}':     '$284.50',
            '{customer_name}':   'Sarah Mitchell',
            '{customer_email}':  'sarah@example.com',
            '{billing_address}': '123 Main St<br>Suite 4B<br>New York, NY 10001<br>United States',
            '{shipping_address}':'456 Oak Ave<br>Apt 7<br>Los Angeles, CA 90001<br>United States',
            '{order_items}':     '<table width="100%" cellpadding="8" cellspacing="0" style="border-collapse:collapse;font-size:13px;">'
                + '<tr style="background:#f9fafb;border-bottom:1px solid #e5e7eb;"><td style="font-weight:600;">Product</td><td style="font-weight:600;text-align:center;">Qty</td><td style="font-weight:600;text-align:right;">Price</td></tr>'
                + '<tr style="border-bottom:1px solid #e5e7eb;"><td>Wireless Headphones Pro</td><td style="text-align:center;">1</td><td style="text-align:right;">$199.00</td></tr>'
                + '<tr style="border-bottom:1px solid #e5e7eb;"><td>USB-C Hub Adapter</td><td style="text-align:center;">2</td><td style="text-align:right;">$85.50</td></tr>'
                + '</table>',
            '{order_url}':       'https://store.example.com/my-account/orders/4821',
            '{order_admin_url}': 'https://store.example.com/wp-admin/#/orders/4821',
            '{reset_url}':       'https://store.example.com/reset-password?token=abc123',
            '{product_name}':    'Wireless Headphones Pro',
            '{product_url}':     'https://store.example.com/product/wireless-headphones-pro',
            '{product_code}':    'WHP-001',
            '{stock_amount}':    '3',
            '{tracking_number}': '1Z999AA10123456784',
            '{tracking_url}':    'https://www.ups.com/track?tracknum=1Z999AA10123456784',
            '{shipping_method}': 'UPS Ground',
            '{gift_card_amount}':'$50.00',
            '{gift_card_code}':  'GIFT-X7K9-M2PQ',
            '{group_name}':      'Wholesale',
            '{review_rating}':   '5',
            '{reviewer_name}':   'James Cooper',
            '{review_text}':     'Absolutely love these headphones! The noise cancellation is incredible and the battery lasts all day. Highly recommended.',
            '{reply_text}':      'Thank you for your kind review, James! We\'re glad you\'re enjoying them.',
            '{discount_amount}': '15%',
            '{coupon_code}':     'SAVE15NOW',
            '{points_earned}':   '142',
            '{points_balance}':  '1,350',
            '{error_message}':   'USPS API: Rate request timeout after 30 seconds. Server returned HTTP 503.',
        };

        function previewHtml(html) {
            var result = html || '';
            Object.keys(SAMPLE_DATA).forEach(function(key) {
                result = result.split(key).join(SAMPLE_DATA[key]);
            });
            return result;
        }

        /* ── Load all data ── */
        useEffect(function() {
            setLoading(true);
            Promise.all([
                apiFetch('/notifications/events').catch(function(){ return []; }),
                apiFetch('/notifications/settings').catch(function(){ return {}; }),
                apiFetch('/email/templates').catch(function(){ return []; }),
                apiFetch('/notifications/documents').catch(function(){ return []; }),
            ]).then(function(res) {
                setEvents(Array.isArray(res[0]) ? res[0] : []);
                setSettings(res[1] && typeof res[1] === 'object' && !Array.isArray(res[1]) ? res[1] : {});
                setTemplates(Array.isArray(res[2]) ? res[2] : []);
                setDocuments(Array.isArray(res[3]) ? res[3] : []);
                setLoading(false);
            });
        }, []);

        /* ── Toggle helper ── */
        function toggleEvent(eventId, receiver) {
            var key = eventId + '|' + receiver + '|email';
            var cur = settings[key];
            var val = (cur === undefined || cur === null) ? 0 : (cur ? 0 : 1);
            var next = Object.assign({}, settings);
            next[key] = val;
            setSettings(next);
            setDirty(true);
            setSaved(false);
        }

        function isEnabled(eventId, receiver) {
            var key = eventId + '|' + receiver + '|email';
            var v = settings[key];
            return v === undefined || v === null || v === 1 || v === '1' || v === true;
        }

        /* ── Save notification settings ── */
        function saveSettings() {
            setSaving(true);
            apiFetch('/notifications/settings', { method:'PUT', body:settings })
                .then(function() { setSaving(false); setSaved(true); setDirty(false); })
                .catch(function() { setSaving(false); });
        }

        /* ── Toggle template status ── */
        function toggleTplStatus(tpl) {
            var newStatus = tpl.status === 'active' ? 'disabled' : 'active';
            apiFetch('/email/templates/' + tpl.id + '/status', { method:'PUT', body:{ status:newStatus } })
                .then(function() {
                    setTemplates(function(prev) { return prev.map(function(t) { return t.id === tpl.id ? Object.assign({},t,{status:newStatus}) : t; }); });
                });
        }

        /* ── Open template editor ── */
        function openTplEdit(tpl) {
            apiFetch('/email/templates/' + tpl.id).then(function(full) {
                setEditing(full);
                setTplForm({ subject: full.subject||'', body_html: full.body_html||'' });
                setTplSaved(false);
                setEditorTab('code');
            });
        }

        function saveTpl() {
            setTplSaving(true);
            apiFetch('/email/templates/' + editing.id, { method:'PUT', body:{ subject:tplForm.subject, body_html:tplForm.body_html } })
                .then(function() {
                    setTplSaving(false); setTplSaved(true);
                    setTemplates(function(prev) { return prev.map(function(t) { return t.id === editing.id ? Object.assign({},t,{subject:tplForm.subject}) : t; }); });
                }).catch(function() { setTplSaving(false); });
        }

        /* ── Checkbox component ── */
        function Chk(props) {
            return h('div', { style:{ display:'flex', alignItems:'center', justifyContent:'center' } },
                h('input', {
                    type:'checkbox',
                    checked: props.checked,
                    onChange: props.onChange,
                    style:{ width:16, height:16, accentColor:TC.accent, cursor:'pointer' }
                })
            );
        }

        /* ── Status dot ── */
        function StatusDot(props) {
            var isActive = props.status === 'active';
            return h('div', {
                onClick: props.onClick,
                style:{ display:'inline-flex', alignItems:'center', gap:6, cursor:'pointer', padding:'2px 8px', borderRadius:5, fontSize:12, fontWeight:500,
                    color: isActive ? TC.success : mutedColor,
                    background: isActive ? (dark?'rgba(22,163,74,.08)':'#F0FDF4') : (dark?'rgba(115,115,115,.08)':'#F5F5F5'),
                }
            },
                h('span', { style:{ width:7, height:7, borderRadius:'50%', background: isActive ? TC.success : '#A3A3A3' } }),
                isActive ? 'Active' : 'Disabled'
            );
        }

        /* ══════════════════════════════════════════════════════
         * TEMPLATE EDITOR VIEW — with Code / Preview tabs
         * ══════════════════════════════════════════════════════ */
        if (editing) {
            /* Build preview HTML for the iframe */
            var previewSrc = '';
            if (editorTab === 'preview' && tplForm.body_html) {
                previewSrc = previewHtml(tplForm.body_html);
            }

            return h('main', { style:{ flex:1, overflow:'auto', padding:S.xxl } },
                /* ── Header bar ── */
                h('div', { style:{ display:'flex', justifyContent:'space-between', alignItems:'center', marginBottom:S.lg } },
                    h('div', { style:{ display:'flex', alignItems:'center', gap:S.md } },
                        h('button', { onClick:function(){ setEditing(null); setTplSaved(false); setEditorTab('code'); },
                            style:{ background:'transparent', border:'1px solid '+borderColor, borderRadius:7, padding:'5px 12px', fontSize:12, cursor:'pointer', color:mutedColor } }, '\u2190 Back to Templates'),
                        h('h2', { style:{ fontSize:18, fontWeight:700 } }, editing.name || editing.slug)
                    ),
                    h('div', { style:{ display:'flex', gap:S.sm, alignItems:'center' } },
                        tplSaved && h('span', { style:{ fontSize:12, color:TC.success } }, '\u2713 Saved'),
                        h(Btn, { onClick:function(){ setEditing(null); setTplSaved(false); setEditorTab('code'); }, mutedColor:mutedColor, borderColor:borderColor }, 'Cancel'),
                        h(Btn, { variant:'primary', onClick:saveTpl, disabled:tplSaving, mutedColor:mutedColor, borderColor:borderColor }, tplSaving?'Saving\u2026':'Save Template')
                    )
                ),

                /* ── Subject line (always visible) ── */
                h(Card, { cardBg:cardBg, borderColor:borderColor, dark:dark, style:{ marginBottom:S.lg } },
                    h('div', { style:{ display:'flex', alignItems:'center', gap:S.lg } },
                        h('div', { style:{ flex:1 } },
                            h('label', { style:labelSty }, 'Subject Line'),
                            h('input', { value:tplForm.subject, onChange:function(e){ var v=e.target.value; setTplForm(function(p){ return Object.assign({},p,{subject:v}); }); }, style:fs })
                        ),
                        editorTab === 'preview' && h('div', { style:{ flexShrink:0, paddingTop:18 } },
                            h('div', { style:{ fontSize:12, color:mutedColor, marginBottom:2 } }, 'Preview subject:'),
                            h('div', { style:{ fontSize:14, fontWeight:600 } }, previewHtml(tplForm.subject))
                        )
                    )
                ),

                /* ── Code / Preview tab toggle ── */
                h('div', { style:{ display:'flex', gap:0, marginBottom:S.lg } },
                    ['code', 'preview'].map(function(t) {
                        var active = t === editorTab;
                        var icon = t === 'code'
                            ? h('svg', { width:14, height:14, viewBox:'0 0 24 24', fill:'none', stroke:'currentColor', strokeWidth:2, strokeLinecap:'round', strokeLinejoin:'round', style:{ marginRight:6 } },
                                h('polyline', { points:'16 18 22 12 16 6' }), h('polyline', { points:'8 6 2 12 8 18' }))
                            : h('svg', { width:14, height:14, viewBox:'0 0 24 24', fill:'none', stroke:'currentColor', strokeWidth:2, strokeLinecap:'round', strokeLinejoin:'round', style:{ marginRight:6 } },
                                h('path', { d:'M2 12s3-7 10-7 10 7 10 7-3 7-10 7-10-7-10-7Z' }), h('circle', { cx:12, cy:12, r:3 }));
                        return h('button', {
                            key:t,
                            onClick:function(){ setEditorTab(t); },
                            style:{
                                display:'inline-flex', alignItems:'center',
                                padding:'8px 20px', fontSize:13, fontWeight: active?600:400, cursor:'pointer',
                                background: active ? (dark?'rgba(37,99,235,.08)':'#EFF6FF') : 'transparent',
                                border: '1px solid '+(active ? TC.accent : borderColor),
                                borderRadius: t==='code' ? '8px 0 0 8px' : '0 8px 8px 0',
                                color: active ? TC.accent : mutedColor,
                                borderLeft: t==='preview' ? 'none' : undefined,
                            }
                        }, icon, t === 'code' ? 'HTML Code' : 'Preview');
                    })
                ),

                /* ── CODE MODE ── */
                editorTab === 'code' && h('div', { style:{ display:'grid', gridTemplateColumns:'1fr 280px', gap:S.lg, alignItems:'start' } },
                    h('div', null,
                        h(Card, { cardBg:cardBg, borderColor:borderColor, dark:dark },
                            h('div', null,
                                h('label', { style:labelSty }, 'Email Body (HTML)'),
                                h('textarea', { value:tplForm.body_html, onChange:function(e){ var v=e.target.value; setTplForm(function(p){ return Object.assign({},p,{body_html:v}); }); },
                                    rows:28, style:Object.assign({},fs,{ resize:'vertical', fontFamily:'\'SF Mono\',\'Fira Code\',monospace', fontSize:12, lineHeight:1.6, tabSize:2 }) })
                            )
                        )
                    ),
                    h(Card, { cardBg:cardBg, borderColor:borderColor, dark:dark },
                        h('h4', { style:{ fontSize:13, fontWeight:600, marginBottom:S.md } }, 'Available Variables'),
                        h('p', { style:{ fontSize:11, color:mutedColor, marginBottom:S.md } }, 'Click to insert at cursor position. These placeholders are replaced with real data when the email is sent.'),
                        [
                            ['{site_name}',       'Store name'],
                            ['{site_url}',        'Store URL'],
                            ['{order_id}',        'Order number'],
                            ['{order_status}',    'Current status'],
                            ['{order_total}',     'Order total'],
                            ['{order_url}',       'Order detail link'],
                            ['{order_items}',     'Items table (HTML)'],
                            ['{customer_name}',   'Customer name'],
                            ['{customer_email}',  'Customer email'],
                            ['{billing_address}', 'Billing address'],
                            ['{shipping_address}','Shipping address'],
                            ['{tracking_number}', 'Tracking number'],
                            ['{tracking_url}',    'Tracking link'],
                            ['{shipping_method}', 'Shipping method'],
                            ['{product_name}',    'Product name'],
                            ['{product_url}',     'Product link'],
                            ['{product_code}',    'Product SKU'],
                            ['{stock_amount}',    'Stock quantity'],
                            ['{reset_url}',       'Password reset link'],
                            ['{gift_card_code}',  'Gift card code'],
                            ['{gift_card_amount}','Gift card value'],
                            ['{coupon_code}',     'Coupon code'],
                            ['{discount_amount}', 'Discount amount'],
                            ['{group_name}',      'Customer group'],
                            ['{points_earned}',   'Points earned'],
                            ['{points_balance}',  'Points balance'],
                            ['{review_rating}',   'Review rating'],
                            ['{reviewer_name}',   'Reviewer name'],
                            ['{review_text}',     'Review content'],
                            ['{reply_text}',      'Reply content'],
                        ].map(function(pair) {
                            return h('div', { key:pair[0], style:{ marginBottom:4, display:'flex', alignItems:'baseline', gap:4 } },
                                h('code', { style:{ fontSize:10, background:dark?'#171717':'#F5F5F5', padding:'1px 5px', borderRadius:3, color:TC.accent, cursor:'pointer', whiteSpace:'nowrap' },
                                    title:'Click to copy',
                                    onClick:function(){ if(navigator.clipboard) navigator.clipboard.writeText(pair[0]); }
                                }, pair[0]),
                                h('span', { style:{ fontSize:10, color:mutedColor } }, pair[1])
                            );
                        })
                    )
                ),

                /* ── PREVIEW MODE ── */
                editorTab === 'preview' && h('div', null,
                    h(Card, { cardBg:cardBg, borderColor:borderColor, dark:dark, pad:0 },
                        /* Preview header bar */
                        h('div', { style:{ display:'flex', alignItems:'center', justifyContent:'space-between', padding:'12px 20px', borderBottom:'1px solid '+borderColor, background:dark?'#1a1a2e':'#f9fafb' } },
                            h('div', { style:{ display:'flex', alignItems:'center', gap:S.sm } },
                                h('div', { style:{ width:10, height:10, borderRadius:'50%', background:'#ef4444' } }),
                                h('div', { style:{ width:10, height:10, borderRadius:'50%', background:'#f59e0b' } }),
                                h('div', { style:{ width:10, height:10, borderRadius:'50%', background:'#22c55e' } })
                            ),
                            h('span', { style:{ fontSize:11, color:mutedColor, fontWeight:500 } }, 'Email Preview \u2014 Sample Data')
                        ),
                        /* Iframe with preview content */
                        tplForm.body_html
                            ? h('iframe', {
                                srcDoc: previewSrc,
                                style:{ width:'100%', minHeight:700, border:'none', display:'block', background:'#f4f5f7' },
                                sandbox:'allow-same-origin',
                                title:'Email template preview',
                            })
                            : h('div', { style:{ padding:S.xxl, textAlign:'center', color:mutedColor } },
                                h('p', { style:{ fontSize:14, marginBottom:S.sm } }, 'No template content to preview.'),
                                h('p', { style:{ fontSize:12 } }, 'Switch to HTML Code and add email content first.')
                            )
                    ),
                    /* Sample data legend */
                    h('div', { style:{ marginTop:S.lg, padding:S.lg, background:dark?'rgba(37,99,235,.06)':'#EFF6FF', borderRadius:8, border:'1px solid '+(dark?'rgba(37,99,235,.15)':'#DBEAFE') } },
                        h('div', { style:{ fontSize:12, fontWeight:600, color:TC.accent, marginBottom:S.sm } }, 'Preview uses sample data'),
                        h('div', { style:{ fontSize:12, color:mutedColor, lineHeight:1.5 } },
                            'This preview replaces all {placeholders} with realistic sample data so you can see exactly how the email will look. ',
                            'The actual email will use real order, customer, and product data when sent.'
                        )
                    )
                )
            );
        }

        /* ══════════════════════════════════════════════════════
         * TABS
         * ══════════════════════════════════════════════════════ */
        var TABS = [
            { id:'customer',  label:'Customer Notifications' },
            { id:'admin',     label:'Admin Notifications' },
            { id:'templates', label:'Email Templates' },
            { id:'documents', label:'Documents' },
            { id:'test',      label:'Test Email' },
        ];

        function tabBar() {
            return h('div', { style:{ display:'flex', gap:0, borderBottom:'2px solid '+borderColor, marginBottom:S.xl } },
                TABS.map(function(t) {
                    var active = t.id === tab;
                    return h('button', {
                        key:t.id,
                        onClick:function(){ setTab(t.id); setSaved(false); },
                        style:{
                            padding: '10px 20px', fontSize:13, fontWeight: active?600:400, cursor:'pointer',
                            background:'transparent', border:'none', borderBottom: active ? '2px solid '+TC.accent : '2px solid transparent',
                            color: active ? TC.accent : mutedColor, marginBottom:-2, transition:'all .15s',
                        }
                    }, t.label);
                })
            );
        }

        /* ══════════════════════════════════════════════════════
         * NOTIFICATION TOGGLE TABLE (Customer or Admin)
         * ══════════════════════════════════════════════════════ */
        function renderEventTable(receiver) {
            return h('div', null,
                events.map(function(group) {
                    /* Filter events to only those with this receiver */
                    var filtered = group.events.filter(function(ev) {
                        return ev.receivers.indexOf(receiver) !== -1;
                    });
                    if (filtered.length === 0) return null;

                    return h('div', { key:group.group, style:{ marginBottom:S.xxl } },
                        h('h3', { style:{ fontSize:15, fontWeight:700, marginBottom:S.md, paddingBottom:S.sm, borderBottom:'1px solid '+borderColor } }, group.group),
                        h('div', { style:{ display:'grid', gridTemplateColumns:'1fr 80px', gap:0 } },
                            /* Header row */
                            h('span', { style:{ fontSize:11, fontWeight:500, color:mutedColor, textTransform:'uppercase', letterSpacing:'.5px', padding:S.sm+'px 0' } }, 'Notification type'),
                            h('span', { style:{ fontSize:11, fontWeight:500, color:mutedColor, textTransform:'uppercase', letterSpacing:'.5px', padding:S.sm+'px 0', textAlign:'center' } }, 'E-mail'),
                            /* Event rows */
                            filtered.map(function(ev) {
                                var enabled = isEnabled(ev.id, receiver);
                                return [
                                    h('span', { key:ev.id+'_name', style:{ fontSize:13, padding:'10px 0', borderTop:'1px solid '+(dark?'#262626':'#F5F5F5') } }, ev.name),
                                    h('div', { key:ev.id+'_chk', style:{ display:'flex', alignItems:'center', justifyContent:'center', borderTop:'1px solid '+(dark?'#262626':'#F5F5F5'), padding:'10px 0' } },
                                        h(Chk, { checked:enabled, onChange:function(){ toggleEvent(ev.id, receiver); } })
                                    ),
                                ];
                            })
                        )
                    );
                }),
                /* Save bar */
                h('div', { style:{ display:'flex', gap:S.md, alignItems:'center', marginTop:S.xl, paddingTop:S.lg, borderTop:'1px solid '+borderColor } },
                    h(Btn, { variant:'primary', onClick:saveSettings, disabled:saving, mutedColor:mutedColor, borderColor:borderColor }, saving ? 'Saving\u2026' : 'Save'),
                    saved && h('span', { style:{ fontSize:12, color:TC.success } }, '\u2713 Settings saved'),
                    dirty && !saved && h('span', { style:{ fontSize:12, color:TC.amber } }, 'Unsaved changes')
                )
            );
        }

        /* ══════════════════════════════════════════════════════
         * EMAIL TEMPLATES TAB
         * ══════════════════════════════════════════════════════ */
        function renderTemplates() {
            var custTpls = templates.filter(function(t){ return t.receiver === 'customer'; });
            var adminTpls = templates.filter(function(t){ return t.receiver === 'admin'; });
            var display = tplTab === 'customer' ? custTpls : adminTpls;

            return h('div', null,
                /* Sub-tabs: Customer / Admin templates */
                h('div', { style:{ display:'flex', gap:0, marginBottom:S.xl } },
                    ['customer','admin'].map(function(st) {
                        var active = st === tplTab;
                        var label = st === 'customer' ? 'Customer notifications' : 'Administrator notifications';
                        return h('button', {
                            key:st,
                            onClick:function(){ setTplTab(st); },
                            style:{
                                flex:1, padding:'10px 0', fontSize:13, fontWeight:active?600:400, cursor:'pointer',
                                background: active ? (dark?'rgba(37,99,235,.08)':'#EFF6FF') : (dark?'#1E1E1E':'#F9FAFB'),
                                border: active ? '1px solid '+TC.accent : '1px solid '+borderColor,
                                borderRadius: st==='customer' ? '8px 0 0 8px' : '0 8px 8px 0',
                                color: active ? TC.accent : mutedColor,
                            }
                        }, label);
                    })
                ),
                /* Template list */
                h(Card, { cardBg:cardBg, borderColor:borderColor, dark:dark, pad:0 },
                    display.length === 0
                        ? h('div', { style:{ padding:S.xxl, textAlign:'center', color:mutedColor, fontSize:13 } }, 'No templates found.')
                        : display.map(function(tpl, i) {
                            return h('div', { key:tpl.id||tpl.slug,
                                style:{
                                    display:'flex', justifyContent:'space-between', alignItems:'center',
                                    padding:'12px '+S.xl+'px',
                                    borderBottom: i < display.length-1 ? '1px solid '+borderColor : 'none',
                                    cursor:'pointer', transition:'background .12s',
                                },
                                onMouseEnter:function(e){ e.currentTarget.style.background = dark?'rgba(255,255,255,.02)':'#F9FAFB'; },
                                onMouseLeave:function(e){ e.currentTarget.style.background = 'transparent'; },
                                onClick:function(){ if(tpl.id) openTplEdit(tpl); },
                            },
                                h('span', { style:{ fontSize:13, fontWeight:400 } }, tpl.name),
                                h('div', { style:{ display:'flex', alignItems:'center', gap:S.md } },
                                    h(StatusDot, { status:tpl.status, onClick:function(e){ e.stopPropagation(); toggleTplStatus(tpl); } })
                                )
                            );
                        })
                )
            );
        }

        /* ══════════════════════════════════════════════════════
         * DOCUMENTS TAB
         * ══════════════════════════════════════════════════════ */
        function renderDocuments() {
            return h('div', null,
                h(Card, { cardBg:cardBg, borderColor:borderColor, dark:dark, pad:0 },
                    /* Header */
                    h('div', { style:{ display:'grid', gridTemplateColumns:'1fr 1fr', padding:S.md+'px '+S.xl+'px', borderBottom:'1px solid '+borderColor, background:tableHeaderBg, boxShadow:tableHeaderShadow } },
                        h('span', { style:{ fontSize:11, fontWeight:500, color:mutedColor, textTransform:'uppercase', letterSpacing:'.5px' } }, 'Name'),
                        h('span', { style:{ fontSize:11, fontWeight:500, color:mutedColor, textTransform:'uppercase', letterSpacing:'.5px' } }, 'Code')
                    ),
                    documents.map(function(doc, i) {
                        return h('div', { key:doc.code,
                            style:{
                                display:'grid', gridTemplateColumns:'1fr 1fr',
                                padding:'12px '+S.xl+'px',
                                borderBottom: i < documents.length-1 ? '1px solid '+borderColor : 'none',
                            }
                        },
                            h('span', { style:{ fontSize:13, fontWeight:400 } }, doc.name),
                            h('span', { style:{ fontSize:12, color:mutedColor, fontFamily:'monospace' } }, doc.code)
                        );
                    })
                ),
                h('div', { style:{ marginTop:S.lg, padding:S.lg, background:dark?'rgba(217,164,65,.06)':'#FFFBEB', borderRadius:8, border:'1px solid '+(dark?'rgba(217,164,65,.15)':'#FEF3C7') } },
                    h('div', { style:{ fontSize:12, color:TC.amber, fontWeight:500, marginBottom:S.xs } }, 'Document Templates'),
                    h('div', { style:{ fontSize:12, color:mutedColor } }, 'The Invoice is a branded printable document \u2014 open it from any order via "Print Invoice", from the customer\u2019s account, or the link in order confirmation emails. Per-store document template editing is planned for a future update.')
                )
            );
        }

        /* ══════════════════════════════════════════════════════
         * TEST EMAIL TAB
         * ══════════════════════════════════════════════════════ */
        function renderTestEmail() {
            var emailTypes = [
                { value:'order_confirmation',  label:'Order Confirmation',    desc:'Sent to customer after successful order placement.' },
                { value:'new_order_admin',     label:'New Order (Admin)',     desc:'Sent to store admin when a new order is placed.' },
                { value:'order_status_update', label:'Order Status Update',   desc:'Sent to customer when order status changes (e.g. Processing → Shipped).' },
            ];

            function doSend() {
                if (!testTo || testSending) return;
                setTestSending(true); setTestResult(null);
                apiFetch('/email/test', { method:'POST', body:{ type:testType, to:testTo } })
                    .then(function(r) { setTestSending(false); setTestResult(r); })
                    .catch(function(e) { setTestSending(false); setTestResult({ success:false, message:e.message||'Request failed.' }); });
            }

            var cs = { background:cardBg, border:'1px solid '+borderColor, borderRadius:12, padding:S.xl };

            return h('div', null,
                h('div', { style:cs },
                    h('h3', { style:{ fontSize:15, fontWeight:600, marginBottom:S.xs } }, 'Send Test Email'),
                    h('p', { style:{ fontSize:13, color:mutedColor, marginBottom:S.xl } },
                        'Verify your email transport is working. Sends a sample email using the active method configured in Settings \u2192 E-mails.'
                    ),

                    /* Email type selector */
                    h('div', { style:{ marginBottom:S.lg } },
                        h('label', { style:{ fontSize:12, fontWeight:500, color:mutedColor, display:'block', marginBottom:S.sm } }, 'Email Type'),
                        h('div', { style:{ display:'flex', flexDirection:'column', gap:S.sm } },
                            emailTypes.map(function(et) {
                                var sel = testType === et.value;
                                return h('label', {
                                    key:et.value,
                                    style:{
                                        display:'flex', alignItems:'flex-start', gap:S.md, padding:'12px 16px',
                                        borderRadius:8, cursor:'pointer', border:'1px solid '+(sel ? TC.accent : borderColor),
                                        background: sel ? (dark ? 'rgba(31,106,225,.06)' : '#F0F6FF') : 'transparent',
                                        transition:'all .15s',
                                    }
                                },
                                    h('input', { type:'radio', name:'tc_test_type', value:et.value, checked:sel,
                                        onChange:function(){ setTestType(et.value); setTestResult(null); },
                                        style:{ marginTop:2, accentColor:TC.accent }
                                    }),
                                    h('div', null,
                                        h('div', { style:{ fontSize:13, fontWeight:sel?600:400, color:sel?TC.accent:textColor } }, et.label),
                                        h('div', { style:{ fontSize:11, color:mutedColor, marginTop:2 } }, et.desc)
                                    )
                                );
                            })
                        )
                    ),

                    /* Recipient */
                    h('div', { style:{ marginBottom:S.lg } },
                        h('label', { style:{ fontSize:12, fontWeight:500, color:mutedColor, display:'block', marginBottom:S.sm } }, 'Send To'),
                        h('input', {
                            type:'email', value:testTo, placeholder:'your@email.com',
                            onChange:function(e){ setTestTo(e.target.value); setTestResult(null); },
                            style:Object.assign({}, fs, { maxWidth:360 })
                        })
                    ),

                    /* Send button + result */
                    h('div', { style:{ display:'flex', alignItems:'center', gap:S.md, flexWrap:'wrap' } },
                        h(Btn, {
                            variant:'primary', onClick:doSend,
                            disabled:testSending || !testTo,
                            mutedColor:mutedColor, borderColor:borderColor
                        }, testSending ? 'Sending\u2026' : '\u2709 Send Test Email'),

                        testResult && h('span', {
                            style:{
                                fontSize:13, fontWeight:500, padding:'6px 14px', borderRadius:6,
                                background: testResult.success ? (dark?'rgba(22,163,74,.1)':'#F0FDF4') : (dark?'rgba(220,38,38,.1)':'#FEF2F2'),
                                color: testResult.success ? TC.success : TC.error,
                            }
                        }, testResult.success ? ('\u2713 ' + testResult.message) : ('\u2717 ' + testResult.message))
                    ),

                    /* Transport info */
                    testResult && testResult.method && h('div', { style:{
                        marginTop:S.lg, padding:'12px 16px', borderRadius:8,
                        background:dark?'rgba(255,255,255,.02)':'#FAFBFC', border:'1px solid '+borderColor,
                        fontSize:12, color:mutedColor,
                    } },
                        'Transport method: ', h('strong', { style:{ color:textColor } },
                            testResult.method === 'php_mail' ? 'PHP mail()' :
                            testResult.method === 'smtp' ? 'SMTP' :
                            testResult.method === 'sendmail' ? 'Sendmail' : testResult.method
                        ),
                        ' \u2014 configure in Settings \u2192 E-mails'
                    )
                ),

                /* Tips card */
                h('div', { style:Object.assign({}, cs, { marginTop:S.lg }) },
                    h('h4', { style:{ fontSize:13, fontWeight:600, marginBottom:S.sm } }, 'Troubleshooting Tips'),
                    h('ul', { style:{ fontSize:12, color:mutedColor, lineHeight:1.8, paddingLeft:20, margin:0 } },
                        h('li', null, 'If using PHP mail(): check your hosting provider supports it and check the server mail log.'),
                        h('li', null, 'If using SMTP: verify host, port, encryption, and credentials in Settings \u2192 E-mails.'),
                        h('li', null, 'If using Sendmail: ensure the sendmail binary path is correct.'),
                        h('li', null, 'Check your spam/junk folder \u2014 test emails with [TEST] subject prefix may be filtered.'),
                        h('li', null, 'wp_mail() errors are logged to the PHP error log (wp-content/debug.log if WP_DEBUG_LOG is on).')
                    )
                )
            );
        }

        /* ══════════════════════════════════════════════════════
         * MAIN RENDER
         * ══════════════════════════════════════════════════════ */
        return h(PageWrap, { title:'Notifications', sub:'Manage emails and alerts sent to customers and administrators', mutedColor:mutedColor, backBtn:backBtn },
            loading ? h(Spinner, { mutedColor:mutedColor })
            : h('div', null,
                tabBar(),
                tab === 'customer'  ? renderEventTable('customer')
                : tab === 'admin'   ? renderEventTable('admin')
                : tab === 'templates' ? renderTemplates()
                : tab === 'documents' ? renderDocuments()
                : tab === 'test' ? renderTestEmail()
                : null
            )
        );
    }

    /* ════════════════════════════════════════════════════════════════════
     * PRODUCT OPTIONS PAGE — Manage option sets used on products
     * ════════════════════════════════════════════════════════════════════ */

    /* ════════════════════════════════
     * API PAGE  (Module 14)
     * ════════════════════════════════ */

    /* ════════════════════════════════
     * IMPORT/EXPORT PAGE  (Module 13)
     * ════════════════════════════════ */
    function ImportExportPage({ dark, cardBg, textColor, mutedColor, borderColor, tableHeaderBg, backBtn }) {
        const { useState: useState2, useEffect: useEffect2, useRef } = wp.element;

        const [tab,       setTab]       = useState('import');
        const [ieType,    setIeType]    = useState('products');

        // ── Import state ──
        const [impStep,   setImpStep]   = useState('upload');   // upload | map | progress | done
        const [jobId,     setJobId]     = useState(null);
        const [headers,   setHeaders]   = useState([]);
        const [preview,   setPreview]   = useState([]);
        const [fieldMap,  setFieldMap]  = useState({});
        const [colMap,    setColMap]    = useState({});         // { colIndex: tc_field }
        const [impPct,    setImpPct]    = useState(0);
        const [impStatus, setImpStatus] = useState('');
        const [impErrors, setImpErrors] = useState(0);
        const [impDone,   setImpDone]   = useState(null);       // final job obj
        const [valErrors, setValErrors] = useState([]);
        const [jobList,   setJobList]   = useState([]);
        const [listLoad,  setListLoad]  = useState(false);
        const [uploading, setUploading] = useState(false);
        const [starting,  setStarting]  = useState(false);
        const fileRef = useRef(null);


        // ── Import settings ──
        const [importStrategy, setImportStrategy] = useState('import_all');
        const [deleteImages,   setDeleteImages]   = useState(false);
        const [setQtyZero,     setSetQtyZero]     = useState(false);
        const [testImport,     setTestImport]     = useState(false);
        const [imagesDir,      setImagesDir]      = useState('wp-content/uploads/titancart/import-images/');

        // ── Export state ──
        const [expCols,   setExpCols]   = useState([]);
        const [selCols,   setSelCols]   = useState([]);
        const [expFilters,setExpFilters]= useState({ status:'', date_from:'', date_to:'' });
        const [exporting, setExporting] = useState(false);
        const [exportUrl, setExportUrl] = useState('');
        const [exportRows,setExportRows]= useState(0);

        const API = Config.apiUrl || '/wp-json/titancart/v1';
        const nonce = Config.nonce || '';

        function apiFetch(path, opts) {
            var url = API + path;
            if (API.indexOf('?') !== -1 && path.indexOf('?') !== -1) url = API + path.replace('?','&');
            return fetch(url, Object.assign({ headers: { 'X-WP-Nonce': nonce, 'Content-Type': 'application/json' } }, opts || {}))
                .then(function(r){ return r.json(); });
        }

        const inputSty = { background:dark?'#262626':'#F5F5F5', border:`1px solid ${borderColor}`, borderRadius:8, padding:'9px 12px', fontSize:14, color:textColor, width:'100%', outline:'none' };
        const selSty   = Object.assign({}, inputSty, { cursor:'pointer' });

        // Load job history and export columns whenever tab or type changes.
        useEffect(function() {
            if (tab === 'import') {
                setListLoad(true);
                apiFetch('/import/jobs').then(function(d){ setJobList(Array.isArray(d)?d:[]); setListLoad(false); }).catch(function(){ setListLoad(false); });
                apiFetch('/import/fields/' + ieType).then(function(d){
                    setFieldMap(d);
                }).catch(function(){});
            } else {
                setExportUrl(''); setExportRows(0);
                apiFetch('/export/columns/' + ieType).then(function(d){
                    const cols = Array.isArray(d) ? d : [];
                    setExpCols(cols);
                    setSelCols(cols.map(function(c){ return c.key; }));
                }).catch(function(){});
            }
        }, [tab, ieType]);

        // Poll import progress while running.
        useEffect(function() {
            if (impStep !== 'progress' || !jobId) return;
            const interval = setInterval(function() {
                apiFetch('/import/progress/' + jobId).then(function(d) {
                    setImpPct(d.percent || 0);
                    setImpStatus(d.status || '');
                    setImpErrors(d.errors || 0);
                    if (d.status === 'completed' || d.status === 'failed') {
                        clearInterval(interval);
                        setImpStep('done');
                        setImpDone(d);
                    }
                }).catch(function(){});
            }, 1500);
            return function(){ clearInterval(interval); };
        }, [impStep, jobId]);

        // ── Upload handler ───────────────────────────────────────────────────
        function handleFileSelect(e) {
            const file = e.target.files[0];
            if (!file) return;
            setUploading(true);
            const fd = new FormData();
            fd.append('file', file);
            fd.append('type', ieType);
            fd.append('options', JSON.stringify({
                import_strategy: importStrategy,
                delete_additional_images: deleteImages,
                set_quantity_zero: setQtyZero,
                test_import: testImport,
                images_directory: imagesDir,
                update_existing: importStrategy !== 'create_only',
                skip_errors: true,
            }));
            fetch(API + '/import/upload', {
                method: 'POST',
                headers: { 'X-WP-Nonce': nonce },
                body: fd,
            }).then(function(r){ return r.json(); }).then(function(d) {
                setUploading(false);
                if (d.error) { alert(d.error); return; }
                setJobId(d.job_id);
                setHeaders(d.headers || []);
                setPreview(d.preview || []);
                setFieldMap(d.field_map || {});
                // Auto-suggest column mapping by matching header names to TC field names.
                const auto = {};
                const labels = d.field_map.labels || {};
                (d.headers || []).forEach(function(h, i) {
                    const norm = h.toLowerCase().replace(/[^a-z0-9]/g,'_').replace(/^_+|_+$/g,'');
                    const hLower = h.toLowerCase().trim();
                    const all_fields = [...(d.field_map.required||[]), ...(d.field_map.optional||[])];
                    // 1) Direct key match (e.g. "product_code" === "product_code")
                    var match = all_fields.find(function(f){ return f === norm; });
                    // 2) Match against labels (e.g. CSV "Product Code" → label "Product Code")
                    if (!match) {
                        match = all_fields.find(function(f){
                            if (!labels[f]) return false;
                            return labels[f].toLowerCase() === hLower;
                        });
                    }
                    // 3) Fuzzy key match (substring)
                    if (!match) {
                        match = all_fields.find(function(f){ return f.includes(norm) || norm.includes(f); });
                    }
                    if (match) auto[i] = match;
                });
                var initialMap = auto;
                setColMap(initialMap);
                setImpStep('map');
            }).catch(function(){ setUploading(false); alert('Upload failed.'); });
        }

        // ── Validate then start ──────────────────────────────────────────────
        function handleStart() {
            setStarting(true);
            // Validate first.
            apiFetch('/import/validate', { method:'POST', body: JSON.stringify({ job_id: jobId, column_map: colMap }) })
                .then(function(v) {
                    setValErrors(v.errors || []);
                    if ((v.errors || []).length > 0 && !confirm(`Found ${v.error_count} validation error(s). Continue anyway?`)) {
                        setStarting(false); return;
                    }
                    return apiFetch('/import/start', { method:'POST', body: JSON.stringify({ job_id: jobId, column_map: colMap }) });
                }).then(function(d) {
                    setStarting(false);
                    if (!d) return;
                    if (d.error) { alert(d.error); return; }
                    setImpStep('progress');
                    setImpPct(0);
                    setImpStatus('running');
                }).catch(function(){ setStarting(false); alert('Failed to start import.'); });
        }


        // ── Export ───────────────────────────────────────────────────────────
        function handleExport() {
            setExporting(true); setExportUrl('');
            apiFetch('/export/generate', {
                method: 'POST',
                body: JSON.stringify({ type: ieType, columns: selCols, filters: expFilters }),
            }).then(function(d) {
                setExporting(false);
                if (d.error) { alert(d.error); return; }
                setExportUrl(d.url || '');
                setExportRows(d.rows || 0);
            }).catch(function(){ setExporting(false); alert('Export failed.'); });
        }

        // ── Helpers ──────────────────────────────────────────────────────────
        function resetImport() {
            setImpStep('upload'); setJobId(null); setHeaders([]); setPreview([]);
            setColMap({}); setImpPct(0); setImpStatus(''); setImpErrors(0); setImpDone(null);
            setValErrors([]);
            if (fileRef.current) fileRef.current.value = '';
        }

        /**
         * Convert a raw field key (e.g. "product_code", "feature:2") into a
         * human-readable dropdown label using the labels map from the API.
         */
        function fieldLabel(key) {
            // Use the labels map (short clean names matching sample CSV headers)
            if (fieldMap.labels && fieldMap.labels[key]) return fieldMap.labels[key];
            // Fallback: humanize underscore key → "Product Code"
            return key.replace(/^feature:/, 'Attr #').replace(/_/g, ' ').replace(/\b\w/g, function(c) { return c.toUpperCase(); });
        }

        function statusBadge(status) {
            const colors = { queued: mutedColor, running: TC.accent, completed: TC.success, failed: TC.error };
            const bgs    = { queued:'transparent', running: dark?'rgba(37,99,235,.1)':TC.accentBg, completed: dark?'rgba(22,163,74,.1)':TC.successBg, failed: dark?'rgba(220,38,38,.1)':TC.errorBg };
            return h('span', { style:{ fontSize:10, fontWeight:500, padding:'2px 8px', borderRadius:4, color:colors[status]||mutedColor, background:bgs[status]||'transparent' } }, status);
        }

        const TYPES = [{ v:'products', l:'Products' }, { v:'categories', l:'Categories' }, { v:'orders', l:'Orders' }, { v:'order_items', l:'Order Items' }, { v:'reviews', l:'Product Reviews' }, { v:'waitlist', l:'Waitlist' }, { v:'customers', l:'Customers' }];
        const EXP_TYPES = [{ v:'products', l:'Products' }, { v:'categories', l:'Categories' }, { v:'orders', l:'Orders' }, { v:'reviews', l:'Product Reviews' }, { v:'waitlist', l:'Waitlist' }, { v:'customers', l:'Customers' }];

        // ── Render ───────────────────────────────────────────────────────────
        return h(PageWrap, { title:'Import / Export', sub:'Bulk data operations', mutedColor, backBtn:backBtn },

            // Tab bar
            h('div', { style:{ display:'flex', gap:S.xs, marginBottom:S.xl } },
                ...['Import', 'Export'].map(function(t) {
                    const v = t.toLowerCase();
                    return h('button', { key:v, onClick:function(){ setTab(v); resetImport(); setExportUrl(''); },
                        style:{ padding:`8px ${S.lg}px`, borderRadius:8, border:'none', cursor:'pointer', fontSize:13, fontWeight:500,
                            background:tab===v?TC.accent:'transparent', color:tab===v?'#fff':mutedColor } }, t);
                })
            ),

            // ── IMPORT TAB ───────────────────────────────────────────────────
            tab === 'import' && h('div', { style:{ display:'grid', gridTemplateColumns:'1fr 340px', gap:S.lg, alignItems:'start' } },

                // Left — wizard
                h('div', null,

                    // Step 1: Upload
                    impStep === 'upload' && h(Card, { cardBg, borderColor, dark },
                        h('h3', { style:{ fontSize:15, fontWeight:600, marginBottom:S.md } }, 'Step 1 — Upload CSV'),
                        h('div', { style:{ marginBottom:S.lg } },
                            h('label', { style:{ fontSize:12, fontWeight:500, display:'block', marginBottom:S.xs } }, 'Entity Type'),
                            h('select', { value:ieType, onChange:function(e){ setIeType(e.target.value); resetImport(); }, style:selSty },
                                TYPES.map(function(t){ return h('option', { key:t.v, value:t.v }, t.l); })
                            )
                        ),


                        /* ── Import Settings ── */
                        h('div', { style:{ marginBottom:S.lg, padding:S.lg, background:dark?'rgba(255,255,255,.02)':'#F9FAFB', borderRadius:8, border:'1px solid '+borderColor } },
                            h('div', { style:{ fontSize:13, fontWeight:600, marginBottom:S.md } }, 'Import Settings'),

                            /* Import strategy */
                            h('div', { style:{ marginBottom:S.md } },
                                h('label', { style:{ fontSize:12, fontWeight:500, display:'block', marginBottom:S.xs } }, 'Import Strategy'),
                                h('select', { value:importStrategy, onChange:function(e){ setImportStrategy(e.target.value); }, style:selSty },
                                    h('option', { value:'import_all' }, 'Import all products from the file'),
                                    h('option', { value:'update_only' }, 'Update existing products only'),
                                    h('option', { value:'create_only' }, 'Create new products only')
                                ),
                                h('div', { style:{ fontSize:11, color:mutedColor, marginTop:S.xs } },
                                    importStrategy === 'import_all' ? 'New products will be created and existing products (matched by product code) will be updated.'
                                    : importStrategy === 'update_only' ? 'Products not already in the database will be skipped.'
                                    : 'Products that already exist (matched by product code) will be skipped.'
                                )
                            ),

                            /* Checkboxes row */
                            h('div', { style:{ display:'flex', flexDirection:'column', gap:S.md } },
                                h('label', { style:{ display:'flex', alignItems:'center', gap:S.sm, fontSize:12, cursor:'pointer' } },
                                    h('input', { type:'checkbox', checked:testImport, onChange:function(){ setTestImport(!testImport); }, style:{ accentColor:TC.accent, width:15, height:15 } }),
                                    h('div', null,
                                        h('span', { style:{ fontWeight:500 } }, 'Perform test import'),
                                        h('div', { style:{ fontSize:11, color:mutedColor } }, 'Only the first 5 products will be processed. Use this to verify your mapping is correct.')
                                    )
                                ),
                                h('label', { style:{ display:'flex', alignItems:'center', gap:S.sm, fontSize:12, cursor:'pointer' } },
                                    h('input', { type:'checkbox', checked:setQtyZero, onChange:function(){ setSetQtyZero(!setQtyZero); }, style:{ accentColor:TC.accent, width:15, height:15 } }),
                                    h('div', null,
                                        h('span', { style:{ fontWeight:500 } }, 'Set quantity of all products to zero'),
                                        h('div', { style:{ fontSize:11, color:mutedColor } }, 'Products without a quantity column in the file will have their stock set to 0.')
                                    )
                                ),
                                h('label', { style:{ display:'flex', alignItems:'center', gap:S.sm, fontSize:12, cursor:'pointer' } },
                                    h('input', { type:'checkbox', checked:deleteImages, onChange:function(){ setDeleteImages(!deleteImages); }, style:{ accentColor:TC.accent, width:15, height:15 } }),
                                    h('div', null,
                                        h('span', { style:{ fontWeight:500 } }, 'Delete existing additional images'),
                                        h('div', { style:{ fontSize:11, color:mutedColor } }, 'When updating products, remove all additional images before importing new ones. Prevents duplicates when re-importing.')
                                    )
                                )
                            ),

                            /* Images directory */
                            h('div', { style:{ marginTop:S.lg, paddingTop:S.md, borderTop:'1px solid '+borderColor } },
                                h('label', { style:{ fontSize:12, fontWeight:500, display:'block', marginBottom:S.xs } }, 'Images Directory'),
                                h('input', { value:imagesDir, onChange:function(e){ setImagesDir(e.target.value); },
                                    placeholder:'wp-content/uploads/titancart/import-images/',
                                    style:Object.assign({}, selSty, { fontFamily:'monospace', fontSize:12 })
                                }),
                                h('div', { style:{ fontSize:11, color:mutedColor, marginTop:S.xs } },
                                    'Path relative to your WordPress root. If image names in your CSV don\'t include a full URL (e.g. just "product.jpg" instead of "https://…"), the system will look for them in this directory.'
                                )
                            )
                        ),

                        h('div', {
                            style:{ border:`2px dashed ${borderColor}`, borderRadius:10, padding:40, textAlign:'center', cursor:'pointer' },
                            onClick:function(){ fileRef.current && fileRef.current.click(); }
                        },
                            uploading
                                ? h(Spinner, { mutedColor })
                                : h('div', null,
                                    h('div', { style:{ fontSize:32, marginBottom:S.md } }, '📂'),
                                    h('div', { style:{ fontSize:14, fontWeight:500, marginBottom:S.sm } }, 'Click to select a CSV file'),
                                    h('div', { style:{ fontSize:11, color:mutedColor } }, 'Max 10 MB · UTF-8 · First row must be column headers')
                                ),
                            h('input', { ref:fileRef, type:'file', accept:'.csv,text/csv', style:{ display:'none' }, onChange:handleFileSelect })
                        ),

                        /* Sample CSV download link */
                        h('div', { style:{ marginTop:S.md, display:'flex', alignItems:'center', gap:S.sm } },
                            h('svg', { width:14, height:14, viewBox:'0 0 24 24', fill:'none', stroke:TC.accent, strokeWidth:2, strokeLinecap:'round', strokeLinejoin:'round' },
                                h('path', { d:'M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4' }),
                                h('polyline', { points:'7 10 12 15 17 10' }),
                                h('line', { x1:12, y1:15, x2:12, y2:3 })
                            ),
                            h('a', {
                                href: (Config.assetsUrl || '/wp-content/plugins/titancart/assets/') + 'samples/sample-' + ieType.replace('_','-') + '.csv',
                                download: true,
                                style:{ fontSize:12, color:TC.accent, fontWeight:500, textDecoration:'none' }
                            }, 'Download sample CSV for ' + (TYPES.find(function(t){ return t.v === ieType; }) || {}).l),
                            h('span', { style:{ fontSize:11, color:mutedColor } }, '— see the expected format and headers')
                        )
                    ),

                    // Step 2: Column mapping
                    impStep === 'map' && h(Card, { cardBg, borderColor, dark },
                        h('div', { style:{ display:'flex', justifyContent:'space-between', alignItems:'center', marginBottom:S.lg } },
                            h('h3', { style:{ fontSize:15, fontWeight:600 } }, 'Step 2 — Map Columns'),
                            h(Btn, { onClick:resetImport, mutedColor, borderColor }, '← Back')
                        ),
                        h('div', { style:{ fontSize:12, color:mutedColor, marginBottom:S.lg } },
                            headers.length + ' columns detected · Map each to a TitanCart field (or leave as "Skip")'
                        ),
                        // Required fields notice
                        fieldMap.required && fieldMap.required.length > 0 && h('div', { style:{ background:dark?'rgba(37,99,235,.08)':TC.accentBg, borderRadius:8, padding:`${S.sm}px ${S.md}px`, marginBottom:S.lg, fontSize:12, color:TC.accent } },
                            '⚡ Required: ' + fieldMap.required.map(function(f){ return fieldLabel(f); }).join(', ')
                        ),
                        // Mapping table header
                        h('div', { style:{ display:'grid', gridTemplateColumns:'minmax(120px,1fr) minmax(140px,1.2fr) minmax(80px,1fr)', gap:S.sm, marginBottom:S.xs } },
                            h('div', { style:{ fontSize:10, fontWeight:600, color:mutedColor, textTransform:'uppercase', letterSpacing:'0.5px', padding:`0 ${S.md}px` } }, 'CSV Column'),
                            h('div', { style:{ fontSize:10, fontWeight:600, color:mutedColor, textTransform:'uppercase', letterSpacing:'0.5px' } }, 'Map To'),
                            h('div', { style:{ fontSize:10, fontWeight:600, color:mutedColor, textTransform:'uppercase', letterSpacing:'0.5px' } }, 'Sample Data')
                        ),
                        // Mapping rows
                        h('div', { style:{ display:'grid', gridTemplateColumns:'minmax(120px,1fr) minmax(140px,1.2fr) minmax(80px,1fr)', gap:S.sm, marginBottom:S.xl } },
                            headers.map(function(h2, i) {
                                var allFields = [...(fieldMap.required||[]), ...(fieldMap.optional||[])];
                                // Grab sample value from first preview row for this column
                                var sample = (preview.length > 0 && preview[0][i] != null) ? String(preview[0][i]) : '';
                                if (sample.length > 50) sample = sample.substring(0, 47) + '…';
                                return h('div', { key:i, style:{ display:'contents' } },
                                    h('div', { style:{ padding:`${S.sm}px ${S.md}px`, background:tableHeaderBg, borderRadius:6, fontSize:12, fontWeight:500, display:'flex', alignItems:'center', overflow:'hidden', whiteSpace:'nowrap', textOverflow:'ellipsis' } }, h2),
                                    h('select', {
                                        value: colMap[i] || '',
                                        onChange: function(e){ setColMap(function(prev){ var n = Object.assign({},prev); n[i] = e.target.value; return n; }); },
                                        style: Object.assign({}, selSty, { width:'100%' })
                                    },
                                        h('option', { value:'' }, '— Skip —'),
                                        allFields.map(function(f){ return h('option', { key:f, value:f }, fieldLabel(f) + ((fieldMap.required||[]).includes(f) ? ' *' : '')); })
                                    ),
                                    h('div', { style:{ padding:`${S.sm}px ${S.md}px`, fontSize:11, color:mutedColor, fontStyle:sample ? 'normal' : 'italic', overflow:'hidden', whiteSpace:'nowrap', textOverflow:'ellipsis', display:'flex', alignItems:'center' } },
                                        sample || 'empty'
                                    )
                                );
                            })
                        ),
                        // Validation errors (after first validate attempt)
                        valErrors.length > 0 && h('div', { style:{ marginTop:S.lg, background:dark?'rgba(220,38,38,.08)':TC.errorBg, borderRadius:8, padding:S.md } },
                            h('div', { style:{ fontSize:12, fontWeight:500, color:TC.error, marginBottom:S.sm } }, valErrors.length + ' row(s) have validation errors:'),
                            valErrors.slice(0,5).map(function(e2, i){ return h('div', { key:i, style:{ fontSize:11, color:TC.error, marginBottom:2 } }, '• ' + (e2.errors||[]).join(', ')); })
                        ),
                        h('div', { style:{ display:'flex', justifyContent:'space-between', alignItems:'center', marginTop:S.xl } },
                            h('span', null),
                            h(Btn, { variant:'primary', onClick:handleStart, disabled:starting, mutedColor, borderColor }, starting ? 'Starting…' : 'Start Import →')
                        )
                    ),

                    // Step 3: Progress
                    impStep === 'progress' && h(Card, { cardBg, borderColor, dark },
                        h('h3', { style:{ fontSize:15, fontWeight:600, marginBottom:S.xl } }, 'Step 3 — Importing…'),
                        h('div', { style:{ background:dark?'#262626':'#F5F5F5', borderRadius:8, height:10, overflow:'hidden', marginBottom:S.md } },
                            h('div', { style:{ height:'100%', borderRadius:8, background:TC.accent, width:impPct+'%', transition:'width 0.4s ease' } })
                        ),
                        h('div', { style:{ display:'flex', justifyContent:'space-between', fontSize:12, color:mutedColor, marginBottom:S.xl } },
                            h('span', null, impPct + '% complete'),
                            impErrors > 0 && h('span', { style:{ color:TC.error } }, impErrors + ' error(s)')
                        ),
                        h('div', { style:{ textAlign:'center', color:mutedColor, fontSize:13 } }, '⏳ Processing in background…')
                    ),

                    // Step 4: Done
                    impStep === 'done' && impDone && h(Card, { cardBg, borderColor, dark },
                        h('div', { style:{ textAlign:'center', padding:S.xl } },
                            h('div', { style:{ fontSize:40, marginBottom:S.md } }, impDone.status === 'completed' ? '✅' : '❌'),
                            h('h3', { style:{ fontSize:16, fontWeight:600, marginBottom:S.sm } }, impDone.status === 'completed' ? 'Import Complete' : 'Import Failed'),
                            h('div', { style:{ fontSize:13, color:mutedColor, marginBottom:S.xl } },
                                impDone.processed + ' rows processed' + (impDone.errors > 0 ? ` · ${impDone.errors} error(s)` : '')
                            ),
                            impDone.log && h('pre', { style:{ textAlign:'left', background:dark?'#171717':'#FAFAFA', borderRadius:8, padding:S.md, fontSize:11, color:TC.error, maxHeight:150, overflowY:'auto', marginBottom:S.xl } }, impDone.log),
                            h(Btn, { variant:'primary', onClick:function(){ resetImport(); setTab('import'); }, mutedColor, borderColor }, '← Import Another File')
                        )
                    )
                ),

                // Right — recent jobs sidebar
                h(Card, { cardBg, borderColor, dark, pad:0 },
                    h('div', { style:{ padding:`${S.lg}px ${S.xl}px`, borderBottom:`1px solid ${borderColor}` } },
                        h('h3', { style:{ fontSize:14, fontWeight:600 } }, 'Recent Imports')
                    ),
                    listLoad && h(Spinner, { mutedColor }),
                    !listLoad && jobList.length === 0 && h('div', { style:{ padding:S.lg, color:mutedColor, fontSize:12, textAlign:'center' } }, 'No imports yet'),
                    !listLoad && jobList.slice(0,5).map(function(j, i) {
                        return h('div', { key:j.id, style:{ padding:`${S.md}px ${S.xl}px`, borderBottom:i<jobList.length-1?`1px solid ${borderColor}`:'none' } },
                            h('div', { style:{ display:'flex', justifyContent:'space-between', alignItems:'center', marginBottom:2 } },
                                h('div', { style:{ fontSize:12, fontWeight:500 } }, j.type + ' #' + j.id),
                                statusBadge(j.status)
                            ),
                            h('div', { style:{ fontSize:11, color:mutedColor } }, j.filename),
                            h('div', { style:{ fontSize:11, color:mutedColor } },
                                j.processed + ' / ' + j.total_rows + ' rows'
                                + (j.errors > 0 ? ` · ${j.errors} errors` : '')
                            ),
                            j.status === 'running' && h('div', { style:{ marginTop:4, background:dark?'#262626':'#F5F5F5', borderRadius:4, height:4, overflow:'hidden' } },
                                h('div', { style:{ height:'100%', borderRadius:4, background:TC.accent, width:j.percent+'%' } })
                            )
                        );
                    })
                )
            ),

            // ── EXPORT TAB ───────────────────────────────────────────────────
            tab === 'export' && h('div', { style:{ display:'grid', gridTemplateColumns:'1fr 280px', gap:S.lg, alignItems:'start' } },

                h(Card, { cardBg, borderColor, dark },
                    h('h3', { style:{ fontSize:15, fontWeight:600, marginBottom:S.lg } }, 'Export Data as CSV'),

                    // Entity type
                    h('div', { style:{ marginBottom:S.lg } },
                        h('label', { style:{ fontSize:12, fontWeight:500, display:'block', marginBottom:S.xs } }, 'Export Type'),
                        h('select', { value:ieType, onChange:function(e){ setIeType(e.target.value); setExportUrl(''); }, style:selSty },
                            EXP_TYPES.map(function(t){ return h('option', { key:t.v, value:t.v }, t.l); })
                        )
                    ),

                    // Filters
                    h('div', { style:{ display:'grid', gridTemplateColumns:'1fr 1fr 1fr', gap:S.md, marginBottom:S.xl } },
                        h('div', null,
                            h('label', { style:{ fontSize:12, fontWeight:500, display:'block', marginBottom:S.xs } }, 'Status'),
                            h('select', { value:expFilters.status, onChange:function(e){ setExpFilters(Object.assign({},expFilters,{status:e.target.value})); setExportUrl(''); }, style:selSty },
                                h('option', { value:'' }, 'All'),
                                ieType === 'products'   && [h('option',{key:'active',value:'active'},'Active'), h('option',{key:'disabled',value:'disabled'},'Disabled'), h('option',{key:'hidden',value:'hidden'},'Hidden')],
                                ieType === 'categories' && [h('option',{key:'active',value:'active'},'Active'), h('option',{key:'disabled',value:'disabled'},'Disabled')],
                                ieType === 'orders'     && [h('option',{key:'completed',value:'completed'},'Completed'), h('option',{key:'processing',value:'processing'},'Processing'), h('option',{key:'cancelled',value:'cancelled'},'Cancelled')]
                            )
                        ),
                        h('div', null,
                            h('label', { style:{ fontSize:12, fontWeight:500, display:'block', marginBottom:S.xs } }, 'Date From'),
                            h('input', { type:'date', value:expFilters.date_from, onChange:function(e){ setExpFilters(Object.assign({},expFilters,{date_from:e.target.value})); setExportUrl(''); }, style:inputSty })
                        ),
                        h('div', null,
                            h('label', { style:{ fontSize:12, fontWeight:500, display:'block', marginBottom:S.xs } }, 'Date To'),
                            h('input', { type:'date', value:expFilters.date_to, onChange:function(e){ setExpFilters(Object.assign({},expFilters,{date_to:e.target.value})); setExportUrl(''); }, style:inputSty })
                        )
                    ),

                    // Column selector
                    h('div', { style:{ marginBottom:S.xl } },
                        h('div', { style:{ display:'flex', justifyContent:'space-between', alignItems:'center', marginBottom:S.sm } },
                            h('label', { style:{ fontSize:12, fontWeight:500 } }, 'Columns to Include'),
                            h('div', { style:{ display:'flex', gap:S.sm } },
                                h('button', { onClick:function(){ setSelCols(expCols.map(function(c){ return c.key; })); }, style:{ background:'transparent', border:'none', fontSize:11, color:TC.accent, cursor:'pointer' } }, 'Select All'),
                                h('button', { onClick:function(){ setSelCols([]); }, style:{ background:'transparent', border:'none', fontSize:11, color:mutedColor, cursor:'pointer' } }, 'Clear')
                            )
                        ),
                        h('div', { style:{ display:'grid', gridTemplateColumns:'repeat(3, 1fr)', gap:S.sm } },
                            expCols.map(function(col) {
                                const checked = selCols.includes(col.key);
                                return h('label', { key:col.key, style:{ display:'flex', alignItems:'center', gap:S.xs, fontSize:12, cursor:'pointer', padding:'4px 0' } },
                                    h('input', { type:'checkbox', checked:checked, onChange:function(e){
                                        if (e.target.checked) { setSelCols(function(p){ return [...p, col.key]; }); }
                                        else                  { setSelCols(function(p){ return p.filter(function(k){ return k !== col.key; }); }); }
                                        setExportUrl('');
                                    } }),
                                    col.label
                                );
                            })
                        )
                    ),

                    h('div', { style:{ display:'flex', alignItems:'center', gap:S.md } },
                        h(Btn, { variant:'primary', onClick:handleExport, disabled:exporting || selCols.length === 0, mutedColor, borderColor },
                            exporting ? 'Generating…' : '⬇ Generate CSV'
                        ),
                        exportUrl && h('a', { href:exportUrl, download:ieType + '_export.csv',
                            style:{ fontSize:12, color:TC.accent, fontWeight:500, textDecoration:'none' } },
                            '⬇ Download (' + exportRows + ' rows)'
                        )
                    )
                ),

                // Right panel — tips
                h(Card, { cardBg, borderColor, dark },
                    h('h4', { style:{ fontSize:13, fontWeight:600, marginBottom:S.lg } }, '💡 Export Tips'),
                    ...[
                        'Files are UTF-8 with BOM for full Excel compatibility.',
                        'Exported CSVs can be re-imported using the Import tab.',
                        'Use Date filters to export only new or recent records.',
                        'Files are available for 24 hours then auto-deleted.',
                        'Large exports (1 000+ rows) may take a few seconds.',
                    ].map(function(tip, i){
                        return h('div', { key:i, style:{ fontSize:12, color:mutedColor, marginBottom:S.md, paddingLeft:S.md, borderLeft:`2px solid ${borderColor}` } }, tip);
                    })
                )
            )
        );
    }

    /* ════════════════════════════════════════════════════════════════════
     * RETURNS (RMA) — List + Detail Page
     * ════════════════════════════════════════════════════════════════════ */

    /* ════════════════════════════════
     * VendorProductsPage — B3.5b (S181)
     *
     * Vendor's "My Products" list — stub view that mirrors the admin
     * products list shape but consumes the vendor-scoped catalog endpoint
     * (/wp-json/titancart-marketplace/v1/me/vendor/catalog).
     *
     * Minimum viable for B3.5b:
     *   - Fetch on mount; show loading state
     *   - Render a basic table: image | name | code | status | price | stock
     *   - Empty state when the vendor has no products
     *   - Error state when fetch fails
     *
     * Deliberately NOT in scope here (lands at B3.5c–e):
     *   - Create / Edit / Delete actions
     *   - Pagination controls (catalog endpoint supports them; UI lands later)
     *   - Filters / sort UI
     *   - Bulk actions
     *
     * Distinct from the admin ProductsPage on purpose — the admin page
     * uses TC core's REST + its own column logic; this one uses the
     * marketplace's vendor-scoped REST. Sharing component code is a
     * B3.5c+ exercise once the create/edit flow exists.
     * ════════════════════════════════ */
    function VendorProductsPage({ subId, dark, cardBg, textColor, mutedColor, borderColor, tableHeaderBg }) {
        // S308 — "Save & Add New" reset (new → new) for the vendor editor, mirroring
        // ProductsPage. Declared up front with the rest of the hooks (S316): the router
        // does NOT remount on toggle (it reuses ONE instance across the list and editor
        // routes), so every hook must run on every render. See the S316 note below.
        const [edNonce, setEdNonce] = useState(0);
        useEffect(function() {
            function onNew(){ setEdNonce(function(n){ return n + 1; }); }
            window.addEventListener('tc:product-editor-new', onNew);
            return function(){ window.removeEventListener('tc:product-editor-new', onNew); };
        }, []);

        // S316 #300 FIX — declare EVERY hook unconditionally, BEFORE the editor
        // early-return, mirroring the admin ProductsPage router. The router renders
        // this same component for both 'vendor-products' (list) and
        // 'vendor-products/{id}' (editor) — same 'route', so React reuses ONE instance
        // and does NOT remount on toggle. Previously the list hooks (useState x8 +
        // the fetch useEffect) sat AFTER 'if (subId) return', so the hook count changed
        // between the list render (subId empty, ~10 hooks) and the editor render
        // (subId set, 2 hooks) -> "Minified React error #300" (rendered fewer hooks
        // than expected) the moment a vendor clicked Add Product. A fixed hook set fixes it.
        const [products, setProducts] = useState([]);
        const [total, setTotal]       = useState(0);
        const [loading, setLoading]   = useState(true);
        const [error, setError]       = useState(null);
        // Bump this to force the list to re-fetch (e.g. after a delete).
        const [reloadKey, setReloadKey] = useState(0);
        // S183 — Pulse modal state for delete confirmation.
        // confirmDelete = product pending confirmation (null when modal closed).
        // deleting      = true while DELETE request in flight; disables confirm.
        // errorBanner   = dismissable message replacing native alert() on failure.
        const [confirmDelete, setConfirmDelete] = useState(null);
        const [deleting, setDeleting] = useState(false);
        const [errorBanner, setErrorBanner] = useState(null);

        useEffect(() => {
            // Editor mode (subId set) doesn't show the list — skip the catalog fetch.
            if (subId) return;
            const base = (Config.vendor_api_url || '/wp-json/titancart-marketplace/v1').replace(/\/+$/,'');
            const url  = base + '/me/vendor/catalog?per_page=100';
            fetch(url, { headers: { 'X-WP-Nonce': Config.nonce || '' } })
                .then(r => {
                    if (!r.ok) throw new Error('HTTP ' + r.status);
                    return r.json();
                })
                .then(data => {
                    setProducts(Array.isArray(data.products) ? data.products : []);
                    setTotal(parseInt(data.total, 10) || 0);
                    setLoading(false);
                })
                .catch(err => {
                    console.error('[TitanCart] VendorProductsPage fetch failed:', err);
                    setError(String(err && err.message ? err.message : err));
                    setLoading(false);
                });
        }, [reloadKey, subId]);

        // S182 — Editor branch, now AFTER all hooks. When subId is present, render the
        // shared ProductEditorPage in vendor mode (isVendor resolved inside the editor
        // via Config.spa_mode). Thin-router pattern, matching the admin ProductsPage.
        if (subId) {
            return h(ProductEditorPage, {
                key: 'ved-' + subId + '-' + edNonce,
                productId: subId,
                dark, cardBg, textColor, mutedColor, borderColor, tableHeaderBg
            });
        }

        /* B3.5c (S182, refactored S183) — Vendor delete via /me/vendor/catalog/{id}.
         * S183: native confirm() / alert() replaced with the Pulse ConfirmModal
         * helper (module scope). The function now ONLY queues the product for
         * confirmation; the actual DELETE fires from confirmDeleteAction below
         * when the user clicks the modal's confirm button.
         *
         * Server-side cascade (Decision 7) soft-deletes the junction AND flips
         * product status to 'hidden' atomically — list reload shows the post-
         * cascade state. */
        function requestDelete(p) {
            setErrorBanner(null);
            setConfirmDelete(p);
        }
        function cancelDelete() {
            if (deleting) return;        // can't cancel mid-flight; modal stays open
            setConfirmDelete(null);
        }
        function confirmDeleteAction() {
            if (!confirmDelete || deleting) return;
            const p = confirmDelete;
            setDeleting(true);
            apiFetchVendor('/me/vendor/catalog/' + p.id, { method:'DELETE' })
                .then(() => {
                    setConfirmDelete(null);
                    setDeleting(false);
                    setReloadKey(k => k + 1);
                })
                .catch(err => {
                    console.error('[TitanCart] Vendor product delete failed:', err);
                    const msg = (err && err.message) ? err.message : 'unknown error';
                    setConfirmDelete(null);
                    setDeleting(false);
                    setErrorBanner('Delete failed: ' + msg);
                });
        }

        /* ── Status pill helper — mirrors the storefront product status badge. */
        function statusPill(status) {
            const s = String(status || '').toLowerCase();
            const map = {
                'active':   { bg: dark ? 'rgba(22,163,74,.15)'  : '#F0FDF4', color: TC.success, label: 'Active' },
                'disabled': { bg: dark ? 'rgba(115,115,115,.15)' : '#F5F5F5', color: mutedColor, label: 'Disabled' },
                'hidden':   { bg: dark ? 'rgba(217,119,6,.15)'  : '#FFFBEB', color: TC.amber,   label: 'Draft' },
                'd':        { bg: dark ? 'rgba(217,119,6,.15)'  : '#FFFBEB', color: TC.amber,   label: 'Draft' },
            };
            const v = map[s] || { bg: dark ? 'rgba(255,255,255,.05)' : '#F5F5F5', color: mutedColor, label: status || '—' };
            return h('span', { style: { display:'inline-block', padding:'3px 10px', borderRadius:999, background:v.bg, color:v.color, fontSize:11, fontWeight:500 } }, v.label);
        }

        /* ── Currency formatting reuses the global helper if present. */
        function fmtPrice(p) {
            const n = parseFloat(p);
            if (!isFinite(n)) return '—';
            try {
                return (typeof fmtMoney === 'function') ? fmtMoney(n) : ('$' + n.toFixed(2));
            } catch (e) { return '$' + n.toFixed(2); }
        }

        const cellStyle = { padding:'12px 16px', borderBottom:`1px solid ${borderColor}`, fontSize:13, color:textColor, verticalAlign:'middle' };
        const headStyle = { padding:'10px 16px', textAlign:'left', fontSize:11, fontWeight:600, color:mutedColor, textTransform:'uppercase', letterSpacing:'.04em', borderBottom:`1px solid ${borderColor}`, background:tableHeaderBg };

        return h('main', { style:{ flex:1, overflow:'auto', padding:'24px 32px' } },
            /* Header */
            h('div', { style:{ display:'flex', alignItems:'baseline', justifyContent:'space-between', marginBottom:20 } },
                h('div', null,
                    h('h1', { style:{ fontSize:20, fontWeight:600, color:textColor, margin:0 } }, 'My Products'),
                    h('p',  { style:{ fontSize:13, color:mutedColor, margin:'4px 0 0' } }, total === 1 ? '1 product' : (total + ' products'))
                ),
                /* B3.5c — Add New Product CTA. Routes to vendor-products/new which
                 * dispatches to ProductEditorPage in vendor mode (isVendor resolved
                 * from Config.spa_mode). */
                h('button', {
                    onClick: () => navigate('vendor-products/new'),
                    style:{ padding:'8px 16px', borderRadius:8, border:'none', background:TC.accent, color:'#fff', fontSize:13, fontWeight:500, cursor:'pointer', display:'inline-flex', alignItems:'center', gap:6 }
                }, h('span', { style:{ fontSize:16, lineHeight:1 } }, '+'), 'Add new product')
            ),

            /* Error banner — Pulse-styled inline error replacing native alert().
             * S183: surfaces DELETE failures (or any other write failures wired
             * here later) above the list. Dismissable via the × button. */
            errorBanner && h('div', { style:{ display:'flex', alignItems:'flex-start', gap:10, padding:'12px 16px', marginBottom:16, borderRadius:10, background: dark ? 'rgba(220,38,38,.08)' : TC.errorBg, border:'1px solid '+TC.error+'40', color:TC.error, fontSize:13, lineHeight:1.5 } },
                h('svg', { width:16, height:16, viewBox:'0 0 20 20', fill:'none', stroke:'currentColor', strokeWidth:'1.75', strokeLinecap:'round', strokeLinejoin:'round', style:{ marginTop:2, flexShrink:0 } },
                    h('circle', { cx:10, cy:10, r:8 }),
                    h('path', { d:'M10 6v5' }),
                    h('path', { d:'M10 14h.01' })
                ),
                h('div', { style:{ flex:1 } }, errorBanner),
                h('button', {
                    onClick: () => setErrorBanner(null),
                    'aria-label': 'Dismiss',
                    style:{ background:'transparent', border:'none', color:TC.error, fontSize:18, lineHeight:1, cursor:'pointer', padding:0, marginLeft:4 }
                }, '\u00D7')
            ),

            /* Body — loading / error / empty / table */
            loading
                ? h('div', { style:{ padding:48, textAlign:'center', color:mutedColor, fontSize:13 } }, 'Loading your products…')
                : error
                    ? h('div', { style:{ padding:24, borderRadius:12, background:TC.errorBg || 'rgba(220,38,38,.06)', border:`1px solid ${TC.error}30`, color:TC.error, fontSize:13 } },
                        'Could not load products: ', error)
                    : products.length === 0
                        ? h('div', { style:{ padding:'56px 24px', textAlign:'center', background:cardBg, borderRadius:12, border:`1px solid ${borderColor}` } },
                            /* Empty-state icon — Pulse-style accent disc with package SVG.
                             * S183: replaced 📦 emoji for cross-platform visual consistency
                             * (emojis render differently across OS, often misaligned). */
                            h('div', { style:{ width:56, height:56, borderRadius:28, background: dark ? 'rgba(37,99,235,.12)' : TC.accentBg, color:TC.accent, display:'inline-flex', alignItems:'center', justifyContent:'center', marginBottom:16 } },
                                h('svg', { width:26, height:26, viewBox:'0 0 24 24', fill:'none', stroke:'currentColor', strokeWidth:'1.75', strokeLinecap:'round', strokeLinejoin:'round' },
                                    h('path', { d:'M21 8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16Z' }),
                                    h('path', { d:'m3.3 7 8.7 5 8.7-5' }),
                                    h('path', { d:'M12 22V12' })
                                )
                            ),
                            h('div', { style:{ fontSize:16, fontWeight:600, color:textColor, marginBottom:6 } }, 'No products yet'),
                            h('div', { style:{ fontSize:13.5, color:mutedColor, marginBottom:20, maxWidth:380, marginLeft:'auto', marginRight:'auto', lineHeight:1.55 } },
                                'Add your first product to start building your catalog. Drafts stay private until an admin reviews and approves them.'),
                            h('button', {
                                onClick: () => navigate('vendor-products/new'),
                                style:{ padding:'10px 20px', borderRadius:8, border:'none', background:TC.accent, color:'#fff', fontSize:13.5, fontWeight:500, cursor:'pointer', display:'inline-flex', alignItems:'center', gap:6 }
                            },
                                h('svg', { width:14, height:14, viewBox:'0 0 24 24', fill:'none', stroke:'currentColor', strokeWidth:'2.5', strokeLinecap:'round', strokeLinejoin:'round' },
                                    h('path', { d:'M5 12h14' }),
                                    h('path', { d:'M12 5v14' })
                                ),
                                'Add your first product')
                          )
                        : h('div', { style:{ background:cardBg, borderRadius:12, border:`1px solid ${borderColor}`, overflow:'hidden' } },
                            h('table', { style:{ width:'100%', borderCollapse:'collapse' } },
                                h('thead', null,
                                    h('tr', null,
                                        h('th', { style:Object.assign({}, headStyle, { width:64 }) }, ''),
                                        h('th', { style:headStyle }, 'Name'),
                                        h('th', { style:headStyle }, 'Code'),
                                        h('th', { style:headStyle }, 'Status'),
                                        h('th', { style:Object.assign({}, headStyle, { textAlign:'right' }) }, 'Price'),
                                        h('th', { style:Object.assign({}, headStyle, { textAlign:'right' }) }, 'Stock'),
                                        h('th', { style:Object.assign({}, headStyle, { textAlign:'right', width:120 }) }, 'Actions')
                                    )
                                ),
                                h('tbody', null,
                                    products.map(p => {
                                        // ProductRepository::get_list returns image as a nested object
                                        // { url, thumb, alt }. Fall through to flat keys as defensive
                                        // backstop in case the shape evolves.
                                        var img = p.image || {};
                                        var imgUrl = img.thumb || img.url || p.main_image_url || p.image_url || '';
                                        // B3.5c — Whole-row click navigates to editor; per-cell
                                        // Delete button uses event.stopPropagation to avoid the
                                        // row click firing too.
                                        var rowClick = () => navigate('vendor-products/' + p.id);
                                        return h('tr', { key:p.id, onClick:rowClick, style:{ cursor:'pointer' } },
                                            h('td', { style:cellStyle },
                                                imgUrl
                                                    ? h('img', { src:imgUrl, alt:'', style:{ width:40, height:40, objectFit:'cover', borderRadius:6, background:tableHeaderBg } })
                                                    : h('div', { style:{ width:40, height:40, borderRadius:6, background:tableHeaderBg, display:'flex', alignItems:'center', justifyContent:'center', color:mutedColor, fontSize:11 } }, '—')
                                            ),
                                            h('td', { style:Object.assign({}, cellStyle, { fontWeight:500 }) }, p.name || p.product_name || ('Product #' + p.id)),
                                            h('td', { style:Object.assign({}, cellStyle, { color:mutedColor, fontFamily:'ui-monospace, monospace', fontSize:12 }) }, p.sku || p.product_code || '—'),
                                            h('td', { style:cellStyle }, statusPill(p.status)),
                                            h('td', { style:Object.assign({}, cellStyle, { textAlign:'right' }) }, fmtPrice(p.price)),
                                            h('td', { style:Object.assign({}, cellStyle, { textAlign:'right', color:mutedColor }) }, (p.stock_quantity !== undefined && p.stock_quantity !== null) ? p.stock_quantity : '—'),
                                            h('td', { style:Object.assign({}, cellStyle, { textAlign:'right' }) },
                                                h('button', {
                                                    onClick: (e) => { e.stopPropagation(); requestDelete(p); },
                                                    style:{ padding:'5px 10px', borderRadius:6, border:`1px solid ${borderColor}`, background:'transparent', color:TC.error, fontSize:11, fontWeight:500, cursor:'pointer' }
                                                }, 'Delete')
                                            )
                                        );
                                    })
                                )
                            )
                          ),

            /* Pulse delete-confirmation modal. Renders position:fixed so its
             * placement as the last child of <main> is purely structural —
             * it overlays the entire viewport when open. S183 (B3.5c). */
            h(ConfirmModal, {
                open: !!confirmDelete,
                title: 'Delete product?',
                detail: confirmDelete && (confirmDelete.name || confirmDelete.product_name || ('Product #' + confirmDelete.id)),
                message: 'This will remove the product from your catalog and move it back to draft. An admin can restore it later if needed.',
                confirmLabel: 'Delete product',
                busy: deleting,
                onConfirm: confirmDeleteAction,
                onCancel: cancelDelete,
                dark, cardBg, borderColor, textColor, mutedColor,
            })
        );
    }

    /* ════════════════════════════════
     * VendorSalesPage (S187)
     * ════════════════════════════════
     * Storefront SPA view of orders containing the current vendor's product
     * line items. Read-only — vendors see the orders their products appear
     * in but do not own the order lifecycle (admin handles fulfillment +
     * status transitions). Hits GET /me/vendor/sales which delegates to TC
     * core OrdersController via Pattern #160 + Pattern #158 (S187 graduation
     * event — vendor REST handler force-injects vendor_id, listener at
     * titancart_orders_list_where injects EXISTS subquery against
     * tc_vendor_products). Response shape matches admin /titancart/v1/orders
     * exactly: orders/total/pages/page/per_page plus translated
     * status_label + status_color per row.
     *
     * Why no order-detail navigation in V1:
     *   Vendor's relationship to an order is partial (their items, not the
     *   whole order). Order detail surfacing vendor-scoped items, status
     *   history, customer comms is a B4+ scope (closely tied to order
     *   splitting per the Track B plan). V1 is a flat read-only list.
     *
     * Pagination: client-side state with server pagination. Per-page 20.
     * ════════════════════════════════ */
    function VendorSalesPage({ dark, cardBg, textColor, mutedColor, borderColor, tableHeaderBg }) {
        const [orders, setOrders]   = useState([]);
        const [total, setTotal]     = useState(0);
        const [loading, setLoading] = useState(true);
        const [error, setError]     = useState(null);
        const [page, setPage]       = useState(1);
        const PER_PAGE = 20;

        useEffect(() => {
            const base = (Config.vendor_api_url || '/wp-json/titancart-marketplace/v1').replace(/\/+$/,'');
            const url  = base + '/me/vendor/sales?per_page=' + PER_PAGE + '&page=' + page;
            setLoading(true);
            setError(null);
            fetch(url, { headers: { 'X-WP-Nonce': Config.nonce || '' } })
                .then(r => {
                    if (!r.ok) throw new Error('HTTP ' + r.status);
                    return r.json();
                })
                .then(data => {
                    setOrders(Array.isArray(data.orders) ? data.orders : []);
                    setTotal(parseInt(data.total, 10) || 0);
                    setLoading(false);
                })
                .catch(err => {
                    console.error('[TitanCart] VendorSalesPage fetch failed:', err);
                    setError(String(err && err.message ? err.message : err));
                    setLoading(false);
                });
        }, [page]);

        /* Status pill — consumes translated status_label + status_color from
         * the delegation response. Bg tint derived from status_color via
         * color-mix; falls back to neutral for missing values. */
        function statusPill(order) {
            const label = order.status_label || order.status_handle || '—';
            const color = order.status_color || mutedColor;
            const bg    = (color && color !== mutedColor)
                ? 'color-mix(in srgb, ' + color + ' ' + (dark ? '18%' : '12%') + ', transparent)'
                : (dark ? 'rgba(255,255,255,.05)' : '#F5F5F5');
            return h('span', { style: { display:'inline-block', padding:'3px 10px', borderRadius:999, background:bg, color:color, fontSize:11, fontWeight:500 } }, label);
        }

        function fmtMoneyLocal(v) {
            const n = parseFloat(v);
            if (!isFinite(n)) return '—';
            try {
                return (typeof fmtMoney === 'function') ? fmtMoney(n) : new Intl.NumberFormat(undefined, { style:'currency', currency:'USD' }).format(n);
            } catch (e) { return '$' + n.toFixed(2); }
        }

        /* Server emits 'YYYY-MM-DD HH:MM:SS' UTC (Pattern #146). Parse as
         * UTC and render in user's local timezone via toLocaleDateString. */
        function fmtDate(s) {
            if (!s) return '—';
            const iso = String(s).replace(' ', 'T') + 'Z';
            const d   = new Date(iso);
            if (isNaN(d.getTime())) return s;
            return d.toLocaleDateString(undefined, { year:'numeric', month:'short', day:'numeric' });
        }

        function customerName(o) {
            const first = (o.billing_first_name || '').trim();
            const last  = (o.billing_last_name  || '').trim();
            const full  = (first + ' ' + last).trim();
            return full || o.billing_email || '—';
        }

        const cellStyle = { padding:'12px 16px', borderBottom:`1px solid ${borderColor}`, fontSize:13, color:textColor, verticalAlign:'middle' };
        const headStyle = { padding:'10px 16px', textAlign:'left', fontSize:11, fontWeight:600, color:mutedColor, textTransform:'uppercase', letterSpacing:'.04em', borderBottom:`1px solid ${borderColor}`, background:tableHeaderBg };
        const pages     = Math.max(1, Math.ceil(total / PER_PAGE));

        return h('main', { style:{ flex:1, overflow:'auto', padding:'24px 32px' } },
            /* Header */
            h('div', { style:{ marginBottom:20 } },
                h('h1', { style:{ fontSize:20, fontWeight:600, color:textColor, margin:0 } }, 'Sales'),
                h('p',  { style:{ fontSize:13, color:mutedColor, margin:'4px 0 0' } },
                    total === 1
                        ? '1 order containing your products'
                        : (total + ' orders containing your products'))
            ),

            /* Body — loading / error / empty / table */
            loading
                ? h('div', { style:{ padding:48, textAlign:'center', color:mutedColor, fontSize:13 } }, 'Loading your sales…')
                : error
                    ? h('div', { style:{ padding:24, borderRadius:12, background:TC.errorBg || 'rgba(220,38,38,.06)', border:`1px solid ${TC.error}30`, color:TC.error, fontSize:13 } },
                        'Could not load sales: ', error)
                    : orders.length === 0
                        ? h('div', { style:{ padding:'56px 24px', textAlign:'center', background:cardBg, borderRadius:12, border:`1px solid ${borderColor}` } },
                            /* Empty-state icon — shopping cart accent disc, matches
                             * the nav icon. Same Pulse pattern as VendorProductsPage. */
                            h('div', { style:{ width:56, height:56, borderRadius:28, background: dark ? 'rgba(37,99,235,.12)' : TC.accentBg, color:TC.accent, display:'inline-flex', alignItems:'center', justifyContent:'center', marginBottom:16 } },
                                h('svg', { width:26, height:26, viewBox:'0 0 24 24', fill:'none', stroke:'currentColor', strokeWidth:'1.75', strokeLinecap:'round', strokeLinejoin:'round' },
                                    h('circle', { cx:8, cy:21, r:1 }),
                                    h('circle', { cx:19, cy:21, r:1 }),
                                    h('path', { d:'M2.05 2.05h2l2.66 12.42a2 2 0 0 0 2 1.58h9.78a2 2 0 0 0 1.95-1.57l1.65-7.43H5.12' })
                                )
                            ),
                            h('div', { style:{ fontSize:16, fontWeight:600, color:textColor, marginBottom:6 } }, 'No sales yet'),
                            h('div', { style:{ fontSize:13.5, color:mutedColor, maxWidth:380, marginLeft:'auto', marginRight:'auto', lineHeight:1.55 } },
                                'When customers buy products from your catalog, the orders will appear here.')
                          )
                        : h('div', { style:{ background:cardBg, borderRadius:12, border:`1px solid ${borderColor}`, overflow:'hidden' } },
                            h('table', { style:{ width:'100%', borderCollapse:'collapse' } },
                                h('thead', null,
                                    h('tr', null,
                                        h('th', { style:headStyle }, 'Order'),
                                        h('th', { style:headStyle }, 'Date'),
                                        h('th', { style:headStyle }, 'Status'),
                                        h('th', { style:headStyle }, 'Customer'),
                                        h('th', { style:Object.assign({}, headStyle, { textAlign:'right' }) }, 'Total')
                                    )
                                ),
                                h('tbody', null,
                                    ...orders.map(o => h('tr', { key:o.id },
                                        h('td', { style:cellStyle },
                                            h('div', { style:{ fontWeight:500, color:textColor } }, o.order_number || ('#' + o.id))
                                        ),
                                        h('td', { style:cellStyle }, fmtDate(o.created_at)),
                                        h('td', { style:cellStyle }, statusPill(o)),
                                        h('td', { style:cellStyle }, customerName(o)),
                                        h('td', { style:Object.assign({}, cellStyle, { textAlign:'right', fontVariantNumeric:'tabular-nums' }) }, fmtMoneyLocal(o.total))
                                    ))
                                )
                            ),

                            /* Pagination footer — only renders when more than one page. */
                            pages > 1 && h('div', { style:{ display:'flex', alignItems:'center', justifyContent:'space-between', padding:'12px 16px', borderTop:`1px solid ${borderColor}`, fontSize:12, color:mutedColor } },
                                h('div', null, 'Page ' + page + ' of ' + pages + ' · ' + total + ' total'),
                                h('div', { style:{ display:'inline-flex', gap:8 } },
                                    h('button', {
                                        disabled: page <= 1,
                                        onClick: () => setPage(p => Math.max(1, p - 1)),
                                        style:{ padding:'6px 12px', borderRadius:6, border:`1px solid ${borderColor}`, background:'transparent', color:page <= 1 ? mutedColor : textColor, cursor:page <= 1 ? 'not-allowed' : 'pointer', opacity:page <= 1 ? 0.5 : 1, fontSize:12 }
                                    }, 'Previous'),
                                    h('button', {
                                        disabled: page >= pages,
                                        onClick: () => setPage(p => Math.min(pages, p + 1)),
                                        style:{ padding:'6px 12px', borderRadius:6, border:`1px solid ${borderColor}`, background:'transparent', color:page >= pages ? mutedColor : textColor, cursor:page >= pages ? 'not-allowed' : 'pointer', opacity:page >= pages ? 0.5 : 1, fontSize:12 }
                                    }, 'Next')
                                )
                            )
                          )
        );
    }

    /* ════════════════════════════════
     * VendorStripeConnectCta (S193)
     * ════════════════════════════════
     * Right-column CTA on the VendorPayoutsPage Stripe Connection card.
     * State-driven button that POSTs to /me/vendor/stripe-connect/start
     * and redirects the vendor's browser to Stripe-hosted onboarding.
     *
     * Defined at MODULE scope (not nested inside VendorPayoutsPage) per
     * the Critical Coding Rule — nested function components reinstantiate
     * on every parent render and lose internal state (focus, hover,
     * useState values) mid-interaction.
     *
     * State machine (props.status drives label + color):
     *   not_connected → "Connect with Stripe"    (TC.accent)
     *   pending       → "Start onboarding"        (TC.accent)
     *   onboarding    → "Continue onboarding"     (TC.accent)
     *   incomplete    → "Complete verification"   (TC.amber)
     *   active        → no CTA (left-column detail rows show the
     *                   connected-account info; future B7 may add a
     *                   "Manage account" link via Stripe login_links)
     *
     * Failure modes:
     *   - Network error → inline red error message under button.
     *   - REST 4xx/5xx  → server message (or "HTTP N") under button.
     *   - User can retry by clicking the button again.
     *
     * Pattern #157 (mode-aware fetch): uses Config.vendor_api_url base.
     * ════════════════════════════════ */
    function VendorStripeConnectCta({ status, dark, borderColor, mutedColor }) {
        const [connecting, setConnecting] = useState(false);
        const [error, setError]           = useState(null);

        // active state renders no CTA — left-column rows already show
        // the connected-account details. Return an empty flex spacer
        // so the row layout stays consistent across status values.
        if (status === 'active') {
            return h('div', { style:{ flex:'0 0 auto' } });
        }

        // Map status → button label + accent color. `incomplete` is the
        // only amber state (Stripe needs more info to verify the
        // account); all others are accent-blue.
        let label, accent;
        switch (status) {
            case 'onboarding':
                label  = 'Continue onboarding';
                accent = TC.accent;
                break;
            case 'incomplete':
                label  = 'Complete verification';
                accent = TC.amber;
                break;
            case 'pending':
                label  = 'Start onboarding';
                accent = TC.accent;
                break;
            case 'not_connected':
            default:
                label  = 'Connect with Stripe';
                accent = TC.accent;
                break;
        }

        function onClick() {
            if (connecting) return;
            setConnecting(true);
            setError(null);

            const base = (Config.vendor_api_url || '/wp-json/titancart-marketplace/v1').replace(/\/+$/,'');
            const url  = base + '/me/vendor/stripe-connect/start';

            fetch(url, {
                method: 'POST',
                headers: {
                    'X-WP-Nonce':   Config.nonce || '',
                    'Content-Type': 'application/json'
                }
            })
            .then(r => r.json().then(json => ({ status: r.status, json: json })))
            .then(res => {
                if (res.status >= 200 && res.status < 300 && res.json && res.json.ok && res.json.url) {
                    // Success — navigate the browser to Stripe-hosted
                    // onboarding. Vendor returns via /stripe-connect/return.
                    window.location = res.json.url;
                } else {
                    const message = (res.json && (res.json.message || (res.json.data && res.json.data.message)))
                        || ('HTTP ' + res.status);
                    console.error('[TitanCart] stripe-connect/start failed:', message, res.json);
                    setError(String(message));
                    setConnecting(false);
                }
            })
            .catch(err => {
                console.error('[TitanCart] stripe-connect/start fetch failed:', err);
                setError(String(err && err.message ? err.message : err));
                setConnecting(false);
            });
        }

        return h('div', { style:{ flex:'0 0 auto', display:'flex', flexDirection:'column', alignItems:'flex-end', gap:6, maxWidth:280 } },
            h('button', {
                onClick:  onClick,
                disabled: connecting,
                style: {
                    padding:      '10px 18px',
                    borderRadius: 8,
                    border:       connecting ? `1px solid ${borderColor}` : 'none',
                    background:   connecting ? (dark ? 'rgba(255,255,255,.04)' : '#F9FAFB') : accent,
                    color:        connecting ? mutedColor : '#FFFFFF',
                    fontSize:     13,
                    fontWeight:   500,
                    cursor:       connecting ? 'wait' : 'pointer',
                    transition:   'background 120ms ease',
                    whiteSpace:   'nowrap',
                }
            }, connecting ? 'Connecting\u2026' : label),
            error && h('span', { style:{ fontSize:11.5, color:TC.error, textAlign:'right', lineHeight:1.45, maxWidth:280 } }, error)
        );
    }

    /* ════════════════════════════════
     * VendorPayoutsPage (S188 + S191)
     * ════════════════════════════════
     * Storefront SPA view of the vendor's payout state — Stripe Connect
     * onboarding status + commission-split earnings YTD + commission rate.
     * Hits GET /me/vendor/payouts which bundles three reads (Stripe schema
     * view, per-line earnings aggregate via
     * VendorOrderScopeFilter::sum_completed_in_year, commission_rate
     * column).
     *
     * Why a separate route from vendor-sales:
     *   Sales is a transactional log (orders containing the vendor's
     *   products); Payouts is a settlement view (what the vendor takes home
     *   + their connection to the payout rail). Different conceptual
     *   surfaces — they answer different questions for the vendor.
     *
     * S191 surface scope (B5 commission math):
     *   The earnings tile now displays a four-way split — Your Share +
     *   Gross + Platform Cut + Commission Rate — computed inline from the
     *   B4 per-line vendor_id stamp (S190 Migration 42) via the marketplace's
     *   CommissionResolver. Pre-B4 orders are excluded by the stamp filter;
     *   the since_date footer copy surfaces that boundary cleanly so vendors
     *   understand the historical-orders gap.
     *
     *   Stripe state remains read-only — pre-B6 there's nothing to actually
     *   connect (Stripe Connect Express onboarding lands at B6).
     *
     * Layout:
     *   Row 1: Stripe Connection card (full width) — pill + helper text +
     *     disabled CTA (until B6).
     *   Row 2: Auto-fit grid (min 220px columns) — Your Share + Gross +
     *     Platform Cut + Commission Rate tiles. Stacks down to single
     *     column on narrow viewports.
     *   Row 3: Since-date boundary footer — "Calculated from completed
     *     orders since [date]" copy.
     *
     * Pattern compliance:
     *   - Pattern #109 (verify-first): /me/vendor/payouts response shape
     *     re-verified against the S191 controller docblock before this
     *     component was refactored.
     *   - Pattern #146: server emits UTC 'YYYY-MM-DD HH:MM:SS' for
     *     onboarding timestamps; 'YYYY-MM-DD' for since_date — parsed
     *     and rendered local.
     *   - Pattern #157 (mode-aware fetch): hits vendor_api_url base URL,
     *     not Config.apiUrl.
     *   - Pattern #161 (graduated S188): third SPA-mounted section.
     * ════════════════════════════════ */
    function VendorPayoutsPage({ dark, cardBg, textColor, mutedColor, borderColor }) {
        const [data, setData]       = useState(null);
        const [loading, setLoading] = useState(true);
        const [error, setError]     = useState(null);

        useEffect(() => {
            const base = (Config.vendor_api_url || '/wp-json/titancart-marketplace/v1').replace(/\/+$/,'');
            const url  = base + '/me/vendor/payouts';
            setLoading(true);
            setError(null);
            fetch(url, { headers: { 'X-WP-Nonce': Config.nonce || '' } })
                .then(r => {
                    if (!r.ok) throw new Error('HTTP ' + r.status);
                    return r.json();
                })
                .then(json => {
                    setData(json && json.ok ? json : null);
                    setLoading(false);
                })
                .catch(err => {
                    console.error('[TitanCart] VendorPayoutsPage fetch failed:', err);
                    setError(String(err && err.message ? err.message : err));
                    setLoading(false);
                });
        }, []);

        /* Stripe status → label + colors. Status codes are stable contracts
         * the controller emits; the SPA maps them to user-facing copy here. */
        function stripeStatusMeta(status) {
            switch (status) {
                case 'active':
                    return { label: 'Connected', color: TC.success, helper: 'Your Stripe account is fully set up and ready to receive payouts.' };
                case 'onboarding':
                    return { label: 'Onboarding in progress', color: TC.accent, helper: 'You started Stripe onboarding but haven\u2019t finished yet. Resume to start receiving payouts.' };
                case 'incomplete':
                    return { label: 'Action needed', color: TC.amber, helper: 'Stripe needs more information before it can enable your account for charges and payouts.' };
                case 'pending':
                    return { label: 'Pending', color: TC.amber, helper: 'A Stripe account is associated with your vendor profile but onboarding hasn\u2019t begun yet.' };
                case 'not_connected':
                default:
                    return { label: 'Not connected', color: mutedColor, helper: 'Connect a Stripe account to start receiving payouts for your sales.' };
            }
        }

        function pill(label, color) {
            const bg = (color && color !== mutedColor)
                ? 'color-mix(in srgb, ' + color + ' ' + (dark ? '18%' : '12%') + ', transparent)'
                : (dark ? 'rgba(255,255,255,.05)' : '#F5F5F5');
            return h('span', { style: { display:'inline-block', padding:'4px 12px', borderRadius:999, background:bg, color:color, fontSize:12, fontWeight:500 } }, label);
        }

        function fmtMoneyLocal(v) {
            const n = parseFloat(v);
            if (!isFinite(n)) return '\u2014';
            try {
                return (typeof fmtMoney === 'function') ? fmtMoney(n) : new Intl.NumberFormat(undefined, { style:'currency', currency:'USD' }).format(n);
            } catch (e) { return '$' + n.toFixed(2); }
        }

        /* Server emits 'YYYY-MM-DD HH:MM:SS' UTC. Parse as UTC and render
         * in user's local timezone via toLocaleDateString. */
        function fmtDate(s) {
            if (!s) return '\u2014';
            const iso = String(s).replace(' ', 'T') + 'Z';
            const d   = new Date(iso);
            if (isNaN(d.getTime())) return s;
            return d.toLocaleDateString(undefined, { year:'numeric', month:'short', day:'numeric' });
        }

        /* since_date is plain 'YYYY-MM-DD' (no time component). Parse the
         * components explicitly and construct a LOCAL-midnight Date so the
         * calendar day stays intact when rendered via toLocaleDateString.
         *
         * The naive `new Date(s + 'T00:00:00Z')` approach treated the date
         * as UTC midnight, which `toLocaleDateString` would then convert
         * back to local time — shifting the displayed calendar day west of
         * UTC (S191 in-smoke catch: 2026-05-13 rendered as "May 12, 2026"
         * in US Eastern). The date here represents a calendar boundary,
         * not a moment in time, so we render the calendar day as-is. */
        function fmtSinceDate(s) {
            if (!s) return '\u2014';
            const m = String(s).match(/^(\d{4})-(\d{2})-(\d{2})$/);
            if (!m) return s;
            const d = new Date(parseInt(m[1], 10), parseInt(m[2], 10) - 1, parseInt(m[3], 10));
            if (isNaN(d.getTime())) return s;
            return d.toLocaleDateString(undefined, { year:'numeric', month:'long', day:'numeric' });
        }

        /* Format a rate like 15.00 → "15%" with no trailing zeros. Whole
         * numbers render without decimals; fractional rates keep up to
         * 2 places ("12.5%", "10.25%"). */
        function fmtRate(r) {
            const n = parseFloat(r);
            if (!isFinite(n)) return '\u2014';
            // toFixed(2) then strip trailing zeros + trailing dot.
            return n.toFixed(2).replace(/\.?0+$/, '') + '%';
        }

        /* Mask the Stripe account_id for display — show prefix + last 4
         * characters to confirm identity without exposing the full id.
         * (Defense-in-depth even though /me/vendor/payouts only returns
         * the vendor's own account_id to the vendor themselves.) */
        function maskAccountId(id) {
            if (!id || typeof id !== 'string') return '';
            if (id.length <= 12) return id;
            return id.slice(0, 8) + '\u2026' + id.slice(-4);
        }

        const cardStyle = {
            background: cardBg,
            borderRadius: 12,
            border: `1px solid ${borderColor}`,
            padding: '24px 28px',
        };

        /* "Your Share" tile gets a subtle success-tone accent so it reads
         * as the headline number among the four tiles. Falls back gracefully
         * if color-mix isn't supported. */
        const yourShareCardStyle = Object.assign({}, cardStyle, {
            background: `color-mix(in srgb, ${TC.success} ${dark ? '6%' : '4%'}, ${cardBg})`,
            borderColor: `color-mix(in srgb, ${TC.success} 35%, ${borderColor})`,
        });

        /* Loading + error states return early before the body grid. */
        if (loading) {
            return h('main', { style:{ flex:1, overflow:'auto', padding:'24px 32px' } },
                h('div', { style:{ marginBottom:20 } },
                    h('h1', { style:{ fontSize:20, fontWeight:600, color:textColor, margin:0 } }, 'Payouts')
                ),
                h('div', { style:{ padding:48, textAlign:'center', color:mutedColor, fontSize:13 } }, 'Loading your payouts\u2026')
            );
        }

        if (error || !data) {
            return h('main', { style:{ flex:1, overflow:'auto', padding:'24px 32px' } },
                h('div', { style:{ marginBottom:20 } },
                    h('h1', { style:{ fontSize:20, fontWeight:600, color:textColor, margin:0 } }, 'Payouts')
                ),
                h('div', { style:{ padding:24, borderRadius:12, background:TC.errorBg || 'rgba(220,38,38,.06)', border:`1px solid ${TC.error}30`, color:TC.error, fontSize:13 } },
                    'Could not load payouts: ', error || 'no data')
            );
        }

        const stripe          = data.stripe   || {};
        const earnings        = data.earnings || {};
        const commission      = data.commission_rate;  // raw override, null when unset
        const meta            = stripeStatusMeta(stripe.status);
        const year            = earnings.year || new Date().getFullYear();
        const completedCt     = parseInt(earnings.completed_orders_count, 10) || 0;
        const gross           = earnings.gross           || '0';
        const vendorShare     = earnings.vendor_share    || '0';
        const platformShare   = earnings.platform_share  || '0';
        const rateApplied     = (earnings.rate_applied !== undefined && earnings.rate_applied !== null)
            ? earnings.rate_applied
            : 0;
        const sinceDate       = earnings.since_date || null;
        const ordersCopy      = completedCt === 1
            ? '1 completed order'
            : (completedCt + ' completed orders');

        return h('main', { style:{ flex:1, overflow:'auto', padding:'24px 32px' } },
            /* Header */
            h('div', { style:{ marginBottom:24 } },
                h('h1', { style:{ fontSize:20, fontWeight:600, color:textColor, margin:0 } }, 'Payouts'),
                h('p',  { style:{ fontSize:13, color:mutedColor, margin:'4px 0 0' } },
                    'Manage your payout account and review your earnings.')
            ),

            /* Row 1 — Stripe Connection card */
            h('div', { style:Object.assign({}, cardStyle, { marginBottom:16 }) },
                h('div', { style:{ display:'flex', alignItems:'flex-start', justifyContent:'space-between', gap:24, flexWrap:'wrap' } },
                    h('div', { style:{ flex:'1 1 320px', minWidth:0 } },
                        h('div', { style:{ display:'flex', alignItems:'center', gap:12, marginBottom:10 } },
                            h('h2', { style:{ fontSize:15, fontWeight:600, color:textColor, margin:0 } }, 'Stripe Connect'),
                            pill(meta.label, meta.color)
                        ),
                        h('p', { style:{ fontSize:13.5, color:mutedColor, lineHeight:1.55, margin:'0 0 16px', maxWidth:520 } }, meta.helper),

                        /* Detail rows — only render when there's meaningful data
                         * to show. Pre-onboarding all are null/false; suppress
                         * the empty list to keep the not_connected state clean. */
                        stripe.account_id && h('div', { style:{ fontSize:12.5, color:mutedColor, fontFamily:'ui-monospace, SFMono-Regular, Consolas, monospace' } },
                            'Account: ', h('span', { style:{ color:textColor } }, maskAccountId(stripe.account_id))
                        ),
                        stripe.onboarding_started_at && h('div', { style:{ fontSize:12.5, color:mutedColor, marginTop:4 } },
                            'Onboarding started: ', h('span', { style:{ color:textColor } }, fmtDate(stripe.onboarding_started_at))
                        ),
                        stripe.onboarding_completed_at && h('div', { style:{ fontSize:12.5, color:mutedColor, marginTop:4 } },
                            'Onboarding completed: ', h('span', { style:{ color:textColor } }, fmtDate(stripe.onboarding_completed_at))
                        ),
                        (stripe.account_id && (stripe.charges_enabled || stripe.payouts_enabled)) && h('div', { style:{ fontSize:12.5, color:mutedColor, marginTop:4 } },
                            'Capabilities: ',
                            h('span', { style:{ color: stripe.charges_enabled ? TC.success : mutedColor, marginRight:8 } }, (stripe.charges_enabled ? '\u2713' : '\u00d7') + ' Charges'),
                            h('span', { style:{ color: stripe.payouts_enabled ? TC.success : mutedColor } }, (stripe.payouts_enabled ? '\u2713' : '\u00d7') + ' Payouts')
                        )
                    ),

                    /* CTA column — S193 makes this interactive end-to-end.
                     *
                     * State machine (read from stripe.status):
                     *   not_connected → "Connect with Stripe" (accent button)
                     *   pending       → "Start onboarding"     (accent button)
                     *   onboarding    → "Continue onboarding"  (accent button)
                     *   incomplete    → "Complete verification"(amber button)
                     *   active        → no CTA (left-column detail rows show
                     *                   the connected-account info; future B7
                     *                   may add a "Manage account" link to a
                     *                   Stripe-hosted login link).
                     *
                     * Click handler POSTs to /me/vendor/stripe-connect/start.
                     * The handler returns { ok: true, url: 'https://connect.
                     * stripe.com/setup/...' } — the SPA does window.location
                     * to that URL. On error the inline error block renders
                     * below the button without leaving the page.
                     */
                    h(VendorStripeConnectCta, {
                        status: stripe.status,
                        dark: dark,
                        borderColor: borderColor,
                        mutedColor: mutedColor
                    })
                )
            ),

            /* Row 2 — Earnings split tiles (S191, B5 commission math)
             *
             * Four tiles, auto-fit so they collapse gracefully on narrow
             * viewports. Order matters: Your Share leads because it's the
             * number vendors care about most. Gross + Platform Cut provide
             * context; Commission Rate explains the math.
             */
            h('div', { style:{ display:'grid', gridTemplateColumns:'repeat(auto-fit, minmax(220px, 1fr))', gap:16, marginBottom:12 } },
                /* Your Share tile — headline, success-tinted */
                h('div', { style:yourShareCardStyle },
                    h('div', { style:{ display:'flex', alignItems:'center', justifyContent:'space-between', marginBottom:14 } },
                        h('h3', { style:{ fontSize:12, fontWeight:600, color:TC.success, margin:0, textTransform:'uppercase', letterSpacing:'.04em' } }, 'Your Share'),
                        h('span', { style:{ fontSize:11, color:mutedColor } }, year)
                    ),
                    h('div', { style:{ fontSize:28, fontWeight:600, color:textColor, fontVariantNumeric:'tabular-nums', marginBottom:6 } },
                        fmtMoneyLocal(vendorShare)
                    ),
                    h('div', { style:{ fontSize:12.5, color:mutedColor } },
                        ordersCopy
                    )
                ),

                /* Gross revenue tile */
                h('div', { style:cardStyle },
                    h('div', { style:{ display:'flex', alignItems:'center', justifyContent:'space-between', marginBottom:14 } },
                        h('h3', { style:{ fontSize:12, fontWeight:600, color:mutedColor, margin:0, textTransform:'uppercase', letterSpacing:'.04em' } }, 'Gross Revenue'),
                        h('span', { style:{ fontSize:11, color:mutedColor } }, year)
                    ),
                    h('div', { style:{ fontSize:24, fontWeight:600, color:textColor, fontVariantNumeric:'tabular-nums', marginBottom:6 } },
                        fmtMoneyLocal(gross)
                    ),
                    h('div', { style:{ fontSize:12.5, color:mutedColor } },
                        'Pre-tax, post-discount')
                ),

                /* Platform cut tile */
                h('div', { style:cardStyle },
                    h('div', { style:{ display:'flex', alignItems:'center', justifyContent:'space-between', marginBottom:14 } },
                        h('h3', { style:{ fontSize:12, fontWeight:600, color:mutedColor, margin:0, textTransform:'uppercase', letterSpacing:'.04em' } }, 'Platform Cut'),
                        h('span', { style:{ fontSize:11, color:mutedColor } }, fmtRate(rateApplied))
                    ),
                    h('div', { style:{ fontSize:24, fontWeight:600, color:textColor, fontVariantNumeric:'tabular-nums', marginBottom:6 } },
                        fmtMoneyLocal(platformShare)
                    ),
                    h('div', { style:{ fontSize:12.5, color:mutedColor } },
                        'Marketplace commission')
                ),

                /* Commission rate tile — shows resolved rate, with override
                 * vs site-default context. commission (raw override) being
                 * null tells us the resolver fell back to site default. */
                h('div', { style:cardStyle },
                    h('div', { style:{ display:'flex', alignItems:'center', justifyContent:'space-between', marginBottom:14 } },
                        h('h3', { style:{ fontSize:12, fontWeight:600, color:mutedColor, margin:0, textTransform:'uppercase', letterSpacing:'.04em' } }, 'Commission Rate'),
                        h('span', { style:{ fontSize:11, color:mutedColor } }, 'Per sale')
                    ),
                    h('div', { style:{ fontSize:24, fontWeight:600, color:textColor, fontVariantNumeric:'tabular-nums', marginBottom:6 } },
                        fmtRate(rateApplied)
                    ),
                    h('div', { style:{ fontSize:12.5, color:mutedColor } },
                        commission === null || commission === undefined
                            ? 'Site default rate.'
                            : 'Custom rate for your account.'
                    )
                )
            ),

            /* Row 3 — Since-date boundary footer
             *
             * Pre-B4 orders have NULL vendor_id stamps and are naturally
             * excluded from per-vendor commission SQL. This copy surfaces
             * that boundary so vendors don't wonder why their historical
             * orders aren't contributing.
             */
            sinceDate && h('div', {
                style: {
                    fontSize: 11.5,
                    color: mutedColor,
                    padding: '14px 18px',
                    borderRadius: 8,
                    background: dark ? 'rgba(255,255,255,.02)' : '#FAFAFA',
                    border: `1px dashed ${borderColor}`,
                    lineHeight: 1.55,
                }
            },
                'Calculated from completed orders placed on or after ',
                h('strong', { style:{ color:textColor, fontWeight:600 } }, fmtSinceDate(sinceDate)),
                '. Orders placed before this date are not attributed per vendor and are excluded from the totals above. Refunded orders are excluded from earnings; refund commission reversal lands with Stripe Connect in a future update.'
            )
        );
    }

    /* ════════════════════════════════
     * VendorProfilePage — S189
     * Final B3 sub-feature: vendor edits their public profile via PUT /me/vendor.
     * 13 editable fields per VendorSelfController::VENDOR_EDITABLE_FIELDS:
     *   display_name, slug, description, logo_file_id, address_1, address_2,
     *   city, state, postcode, country, business_email, business_phone,
     *   external_url
     *
     * logo_file_id: L3-hybrid placeholder card (mirrors S188 Payouts disabled
     * Stripe CTA pattern). No upload endpoint exists yet; the form surfaces
     * the slot with a "Available in a future update" subtext rather than
     * rendering a working integer field that has no consumer.
     *
     * country: <select> sourced from Config.countries (~250 ISO 3166-1
     * entries injected by VendorSpaAssets at page load).
     * state: <select> when Config.states_by_country[country] has entries
     * (US + CA at seed time), <input type="text"> otherwise.
     *
     * Form state model: `original` holds the server-confirmed snapshot;
     * `form` holds the working draft. `dirty` is the shallow set of fields
     * where form !== original — only those keys are sent on PUT (handler's
     * empty-body short-circuit means a noop PUT is harmless, but trimming
     * the payload keeps the slug-uniqueness check from firing when slug
     * wasn't touched).
     *
     * Error handling:
     *   slug_taken (409) → red inline error below slug input
     *   empty_display_name (400) → red inline error below display_name input
     *   other errors → page-level error banner
     *   network → page-level error banner
     * ════════════════════════════════ */
    function VendorProfilePage({ dark, cardBg, textColor, mutedColor, borderColor }) {
        const [original, setOriginal] = useState(null);
        const [form, setForm]         = useState(null);
        const [loading, setLoading]   = useState(true);
        const [error, setError]       = useState(null);
        const [saving, setSaving]     = useState(false);
        const [toast, setToast]       = useState(null); /* { kind:'ok'|'err', text:string } */
        const [fieldErrors, setFieldErrors] = useState({}); /* { slug?:string, display_name?:string } */

        const apiBase = (Config.vendor_api_url || '/wp-json/titancart-marketplace/v1').replace(/\/+$/,'');

        useEffect(() => {
            const url = apiBase + '/me/vendor';
            setLoading(true);
            setError(null);
            fetch(url, { headers: { 'X-WP-Nonce': Config.nonce || '' } })
                .then(r => {
                    if (!r.ok) throw new Error('HTTP ' + r.status);
                    return r.json();
                })
                .then(json => {
                    if (!json || !json.ok || !json.vendor) {
                        throw new Error('Bad response shape');
                    }
                    /* Normalize nullable string fields to '' so controlled
                     * inputs don't flip uncontrolled → controlled on first
                     * keystroke. logo_file_id stays nullable for the
                     * disabled placeholder. */
                    const v = json.vendor;
                    const snapshot = {
                        display_name:   v.display_name   == null ? '' : String(v.display_name),
                        slug:           v.slug           == null ? '' : String(v.slug),
                        description:    v.description    == null ? '' : String(v.description),
                        logo_file_id:   v.logo_file_id   == null ? null : v.logo_file_id,
                        address_1:      v.address_1      == null ? '' : String(v.address_1),
                        address_2:      v.address_2      == null ? '' : String(v.address_2),
                        city:           v.city           == null ? '' : String(v.city),
                        state:          v.state          == null ? '' : String(v.state),
                        postcode:       v.postcode       == null ? '' : String(v.postcode),
                        country:        v.country        == null ? '' : String(v.country),
                        business_email: v.business_email == null ? '' : String(v.business_email),
                        business_phone: v.business_phone == null ? '' : String(v.business_phone),
                        external_url:   v.external_url   == null ? '' : String(v.external_url),
                    };
                    setOriginal(snapshot);
                    setForm(Object.assign({}, snapshot));
                    setLoading(false);
                })
                .catch(err => {
                    console.error('[TitanCart] VendorProfilePage GET failed:', err);
                    setError(String(err && err.message ? err.message : err));
                    setLoading(false);
                });
        // eslint-disable-next-line react-hooks/exhaustive-deps
        }, []);

        /* Toast auto-dismiss after 4s. */
        useEffect(() => {
            if (!toast) return;
            const t = setTimeout(() => setToast(null), 4000);
            return () => clearTimeout(t);
        }, [toast]);

        /* Field setter — clears field-level error for that key on any edit. */
        function set(key, value) {
            setForm(prev => Object.assign({}, prev, { [key]: value }));
            if (fieldErrors[key]) {
                setFieldErrors(prev => {
                    const next = Object.assign({}, prev);
                    delete next[key];
                    return next;
                });
            }
        }

        /* When country changes, clear state if the new country's seeded
         * state list doesn't contain the existing state code. Prevents
         * orphan "CA" sitting in state when vendor switches US → GB. */
        function setCountry(newCC) {
            setForm(prev => {
                const next = Object.assign({}, prev, { country: newCC });
                const states = (Config.states_by_country || {})[newCC];
                if (states && prev.state && !states[prev.state]) {
                    next.state = '';
                } else if (!states) {
                    /* Country with no seeded state list — keep whatever's
                     * in state since it becomes free-text input. */
                }
                return next;
            });
            if (fieldErrors.country) {
                setFieldErrors(prev => {
                    const next = Object.assign({}, prev);
                    delete next.country;
                    return next;
                });
            }
        }

        /* Shallow dirty diff — returns only keys where form differs from
         * original. Submits this subset to PUT /me/vendor. */
        function dirtyDiff() {
            if (!form || !original) return {};
            const out = {};
            const keys = Object.keys(form);
            for (let i = 0; i < keys.length; i++) {
                const k = keys[i];
                if (form[k] !== original[k]) {
                    out[k] = form[k];
                }
            }
            return out;
        }

        const isDirty = (() => {
            if (!form || !original) return false;
            const keys = Object.keys(form);
            for (let i = 0; i < keys.length; i++) {
                if (form[keys[i]] !== original[keys[i]]) return true;
            }
            return false;
        })();

        function handleSubmit() {
            if (!form || saving) return;

            /* Client-side gate: display_name must not be empty/whitespace.
             * Server doesn't enforce this for updates (only creates), so
             * we hold the line here. Empty strings would normalize to ''
             * in the DB which would break vendor listings. */
            if (!String(form.display_name || '').trim()) {
                setFieldErrors({ display_name: 'Display name is required.' });
                return;
            }

            const diff = dirtyDiff();
            if (Object.keys(diff).length === 0) {
                setToast({ kind:'ok', text:'No changes to save.' });
                return;
            }

            setSaving(true);
            setFieldErrors({});

            fetch(apiBase + '/me/vendor', {
                method: 'PUT',
                headers: {
                    'Content-Type': 'application/json',
                    'X-WP-Nonce': Config.nonce || '',
                },
                body: JSON.stringify(diff),
            })
                .then(r => r.json().then(json => ({ status:r.status, json })))
                .then(({ status, json }) => {
                    setSaving(false);
                    if (json && json.ok) {
                        /* Re-fetch the server-returned vendor shape into both
                         * original AND form so dirty state clears. */
                        const v = json.vendor || {};
                        const fresh = Object.assign({}, form);
                        /* Normalize the returned snapshot the same way the
                         * initial GET does, so subsequent dirty diffs are
                         * stable. */
                        const keys = ['display_name','slug','description','address_1','address_2','city','state','postcode','country','business_email','business_phone','external_url'];
                        for (let i = 0; i < keys.length; i++) {
                            const k = keys[i];
                            fresh[k] = v[k] == null ? '' : String(v[k]);
                        }
                        fresh.logo_file_id = v.logo_file_id == null ? null : v.logo_file_id;
                        setOriginal(fresh);
                        setForm(fresh);
                        setToast({ kind:'ok', text: json.noop ? 'No changes saved.' : 'Profile saved.' });
                        return;
                    }
                    /* Error path — map server error codes to fields where
                     * possible. */
                    const code = (json && json.error) || ('http_' + status);
                    if (code === 'slug_taken') {
                        setFieldErrors({ slug: 'This shop URL is already taken. Try another.' });
                        setToast({ kind:'err', text:'Slug conflict — see field below.' });
                        return;
                    }
                    if (code === 'empty_display_name') {
                        setFieldErrors({ display_name: 'Display name is required.' });
                        setToast({ kind:'err', text:'Display name is required.' });
                        return;
                    }
                    if (code === 'invalid_body') {
                        setToast({ kind:'err', text:'The form data was rejected by the server. Please try again.' });
                        return;
                    }
                    if (code === 'update_failed' || code === 'save_failed') {
                        setToast({ kind:'err', text:'The server could not save the profile. Please try again.' });
                        return;
                    }
                    setToast({ kind:'err', text:'Save failed (' + code + ').' });
                })
                .catch(err => {
                    console.error('[TitanCart] VendorProfilePage PUT failed:', err);
                    setSaving(false);
                    setToast({ kind:'err', text:'Network error — please try again.' });
                });
        }

        function handleRevert() {
            if (!original || saving) return;
            setForm(Object.assign({}, original));
            setFieldErrors({});
            setToast(null);
        }

        /* Style tokens reused across rows. */
        const cardStyle = {
            background: cardBg,
            borderRadius: 12,
            border: `1px solid ${borderColor}`,
            padding: '24px 28px',
        };
        const labelStyle = {
            display: 'block',
            fontSize: 12.5,
            fontWeight: 500,
            color: textColor,
            marginBottom: 6,
        };
        const helpStyle = {
            fontSize: 11.5,
            color: mutedColor,
            marginTop: 4,
            lineHeight: 1.4,
        };
        const inputStyle = {
            width: '100%',
            padding: '9px 12px',
            borderRadius: 8,
            border: `1px solid ${borderColor}`,
            background: dark ? 'rgba(255,255,255,.03)' : '#FFFFFF',
            color: textColor,
            fontSize: 13.5,
            fontFamily: 'inherit',
            outline: 'none',
        };
        const errorTextStyle = {
            fontSize: 12,
            color: TC.error,
            marginTop: 4,
        };

        /* Loading + error states return early. */
        if (loading) {
            return h('main', { style:{ flex:1, overflow:'auto', padding:'24px 32px' } },
                h('div', { style:{ marginBottom:20 } },
                    h('h1', { style:{ fontSize:20, fontWeight:600, color:textColor, margin:0 } }, 'Profile')
                ),
                h('div', { style:{ padding:48, textAlign:'center', color:mutedColor, fontSize:13 } }, 'Loading your profile\u2026')
            );
        }

        if (error || !form) {
            return h('main', { style:{ flex:1, overflow:'auto', padding:'24px 32px' } },
                h('div', { style:{ marginBottom:20 } },
                    h('h1', { style:{ fontSize:20, fontWeight:600, color:textColor, margin:0 } }, 'Profile')
                ),
                h('div', { style:{ padding:24, borderRadius:12, background:TC.errorBg || 'rgba(220,38,38,.06)', border:`1px solid ${TC.error}30`, color:TC.error, fontSize:13 } },
                    'Could not load profile: ', error || 'no data')
            );
        }

        /* Country + state list resolution. */
        const countries = Config.countries || {};
        const statesMap = Config.states_by_country || {};
        const currentStates = form.country ? statesMap[form.country] : null;
        const hasSeededStates = !!(currentStates && Object.keys(currentStates).length > 0);

        /* Public URL preview for the slug field. Pulled from current
         * page URL since vendor doesn't have direct access to the
         * marketplace's vendor-storefront URL pattern at SPA layer. */
        const slugPreviewBase = (function() {
            try {
                const origin = window.location.origin;
                return origin + '/vendor/';
            } catch (e) { return '/vendor/'; }
        })();

        /* Render helpers — kept inline (not separate components) so React
         * doesn't lose input focus on each parent re-render. The handoff
         * standing reminder calls this out explicitly. */
        function fieldError(key) {
            const msg = fieldErrors[key];
            if (!msg) return null;
            return h('div', { style: errorTextStyle }, msg);
        }

        return h('main', { style:{ flex:1, overflow:'auto', padding:'24px 32px' } },
            /* Header */
            h('div', { style:{ marginBottom:24 } },
                h('h1', { style:{ fontSize:20, fontWeight:600, color:textColor, margin:0 } }, 'Profile'),
                h('p',  { style:{ fontSize:13, color:mutedColor, margin:'4px 0 0' } },
                    'Edit how your shop appears to customers and update your contact details.')
            ),

            /* Toast — sits below header, above cards, dismisses after 4s. */
            toast && h('div', {
                style: {
                    marginBottom: 16,
                    padding: '10px 14px',
                    borderRadius: 8,
                    fontSize: 13,
                    background: toast.kind === 'ok'
                        ? (dark ? 'rgba(22,163,74,.12)' : 'rgba(22,163,74,.08)')
                        : (TC.errorBg || 'rgba(220,38,38,.08)'),
                    border: '1px solid ' + (toast.kind === 'ok' ? TC.success + '40' : TC.error + '40'),
                    color: toast.kind === 'ok' ? TC.success : TC.error,
                }
            }, toast.text),

            /* Card 1 — Identity */
            h('div', { style:Object.assign({}, cardStyle, { marginBottom:16 }) },
                h('h2', { style:{ fontSize:15, fontWeight:600, color:textColor, margin:'0 0 16px' } }, 'Identity'),

                /* display_name — required */
                h('div', { style:{ marginBottom:16 } },
                    h('label', { style:labelStyle, htmlFor:'tcm-pf-display-name' },
                        'Shop name ', h('span', { style:{ color: TC.error } }, '*')
                    ),
                    h('input', {
                        id:'tcm-pf-display-name',
                        type:'text',
                        value: form.display_name,
                        onChange: e => set('display_name', e.target.value),
                        style: Object.assign({}, inputStyle, fieldErrors.display_name ? { borderColor: TC.error } : null),
                        maxLength: 191,
                        autoComplete: 'organization',
                    }),
                    fieldError('display_name'),
                    h('div', { style:helpStyle }, 'How your shop appears in product listings, the cart, and customer-facing pages.')
                ),

                /* slug */
                h('div', { style:{ marginBottom:16 } },
                    h('label', { style:labelStyle, htmlFor:'tcm-pf-slug' }, 'Shop URL'),
                    h('div', { style:{ display:'flex', alignItems:'center', gap:0, border:`1px solid ${fieldErrors.slug ? TC.error : borderColor}`, borderRadius:8, overflow:'hidden', background: dark ? 'rgba(255,255,255,.03)' : '#FFFFFF' } },
                        h('span', { style:{ padding:'9px 10px 9px 12px', color:mutedColor, fontSize:12.5, fontFamily:'ui-monospace, SFMono-Regular, Consolas, monospace', whiteSpace:'nowrap', borderRight:`1px solid ${borderColor}`, background: dark ? 'rgba(255,255,255,.02)' : '#F9FAFB' } }, slugPreviewBase),
                        h('input', {
                            id:'tcm-pf-slug',
                            type:'text',
                            value: form.slug,
                            onChange: e => set('slug', e.target.value),
                            style: Object.assign({}, inputStyle, { border:'none', borderRadius:0, padding:'9px 12px', flex:1 }),
                            maxLength: 100,
                            placeholder: 'your-shop',
                            autoComplete: 'off',
                        })
                    ),
                    fieldError('slug'),
                    h('div', { style:helpStyle }, 'Lowercase letters, numbers, and hyphens. Leave blank to keep the current URL.')
                ),

                /* description */
                h('div', { style:{ marginBottom:0 } },
                    h('label', { style:labelStyle, htmlFor:'tcm-pf-description' }, 'About your shop'),
                    h('textarea', {
                        id:'tcm-pf-description',
                        value: form.description,
                        onChange: e => set('description', e.target.value),
                        rows: 6,
                        style: Object.assign({}, inputStyle, { resize:'vertical', minHeight:120, fontFamily:'inherit', lineHeight:1.5 }),
                        maxLength: 5000,
                    }),
                    h('div', { style:helpStyle }, 'A short description of your shop and what you sell. Plain text; HTML is not rendered.')
                )
            ),

            /* Card 2 — Logo (L3-hybrid disabled placeholder, matches S188 Payouts pattern) */
            h('div', { style:Object.assign({}, cardStyle, { marginBottom:16 }) },
                h('div', { style:{ display:'flex', alignItems:'flex-start', justifyContent:'space-between', gap:24, flexWrap:'wrap' } },
                    h('div', { style:{ flex:'1 1 320px', minWidth:0 } },
                        h('div', { style:{ display:'flex', alignItems:'center', gap:12, marginBottom:10 } },
                            h('h2', { style:{ fontSize:15, fontWeight:600, color:textColor, margin:0 } }, 'Shop logo'),
                            h('span', { style:{ display:'inline-block', padding:'4px 12px', borderRadius:999, background:(dark ? 'rgba(255,255,255,.05)' : '#F5F5F5'), color:mutedColor, fontSize:12, fontWeight:500 } }, 'Coming soon')
                        ),
                        h('p', { style:{ fontSize:13.5, color:mutedColor, lineHeight:1.55, margin:'0 0 0', maxWidth:520 } },
                            'Logo uploads will let you replace your shop\u2019s default avatar with a custom image. Your existing customer profile avatar continues to show until then.')
                    ),
                    h('div', { style:{ flex:'0 0 auto', display:'flex', flexDirection:'column', alignItems:'flex-end', gap:6 } },
                        h('button', {
                            disabled: true,
                            title: 'Logo upload lands in a future update.',
                            style: {
                                padding: '10px 18px',
                                borderRadius: 8,
                                border: `1px solid ${borderColor}`,
                                background: dark ? 'rgba(255,255,255,.04)' : '#F9FAFB',
                                color: mutedColor,
                                fontSize: 13,
                                fontWeight: 500,
                                cursor: 'not-allowed',
                                opacity: 0.7,
                            }
                        }, 'Upload logo'),
                        h('span', { style:{ fontSize:11.5, color:mutedColor } }, 'Available in a future update')
                    )
                )
            ),

            /* Card 3 — Address */
            h('div', { style:Object.assign({}, cardStyle, { marginBottom:16 }) },
                h('h2', { style:{ fontSize:15, fontWeight:600, color:textColor, margin:'0 0 16px' } }, 'Business address'),

                h('div', { style:{ marginBottom:16 } },
                    h('label', { style:labelStyle, htmlFor:'tcm-pf-address-1' }, 'Address line 1'),
                    h('input', {
                        id:'tcm-pf-address-1',
                        type:'text',
                        value: form.address_1,
                        onChange: e => set('address_1', e.target.value),
                        style: inputStyle,
                        maxLength: 255,
                        autoComplete: 'address-line1',
                    })
                ),

                h('div', { style:{ marginBottom:16 } },
                    h('label', { style:labelStyle, htmlFor:'tcm-pf-address-2' }, 'Address line 2'),
                    h('input', {
                        id:'tcm-pf-address-2',
                        type:'text',
                        value: form.address_2,
                        onChange: e => set('address_2', e.target.value),
                        style: inputStyle,
                        maxLength: 255,
                        autoComplete: 'address-line2',
                        placeholder: 'Apartment, suite, unit, etc. (optional)',
                    })
                ),

                /* Row: country + state side-by-side on wide screens. */
                h('div', { style:{ display:'grid', gridTemplateColumns:'repeat(auto-fit, minmax(220px, 1fr))', gap:16, marginBottom:16 } },
                    h('div', null,
                        h('label', { style:labelStyle, htmlFor:'tcm-pf-country' }, 'Country'),
                        h('select', {
                            id:'tcm-pf-country',
                            value: form.country,
                            onChange: e => setCountry(e.target.value),
                            style: Object.assign({}, inputStyle, { appearance:'auto' }),
                            autoComplete: 'country',
                        },
                            h('option', { value:'' }, '\u2014 Select country \u2014'),
                            Object.keys(countries).map(cc =>
                                h('option', { key:cc, value:cc }, countries[cc])
                            )
                        )
                    ),
                    h('div', null,
                        h('label', { style:labelStyle, htmlFor:'tcm-pf-state' }, 'State / Province'),
                        hasSeededStates
                            ? h('select', {
                                id:'tcm-pf-state',
                                value: form.state,
                                onChange: e => set('state', e.target.value),
                                style: Object.assign({}, inputStyle, { appearance:'auto' }),
                                autoComplete: 'address-level1',
                            },
                                h('option', { value:'' }, '\u2014 Select \u2014'),
                                Object.keys(currentStates).map(sc =>
                                    h('option', { key:sc, value:sc }, currentStates[sc])
                                )
                            )
                            : h('input', {
                                id:'tcm-pf-state',
                                type:'text',
                                value: form.state,
                                onChange: e => set('state', e.target.value),
                                style: inputStyle,
                                maxLength: 100,
                                autoComplete: 'address-level1',
                                placeholder: form.country ? '' : 'Select a country first',
                                disabled: !form.country,
                            })
                    )
                ),

                h('div', { style:{ display:'grid', gridTemplateColumns:'repeat(auto-fit, minmax(220px, 1fr))', gap:16, marginBottom:0 } },
                    h('div', null,
                        h('label', { style:labelStyle, htmlFor:'tcm-pf-city' }, 'City'),
                        h('input', {
                            id:'tcm-pf-city',
                            type:'text',
                            value: form.city,
                            onChange: e => set('city', e.target.value),
                            style: inputStyle,
                            maxLength: 100,
                            autoComplete: 'address-level2',
                        })
                    ),
                    h('div', null,
                        h('label', { style:labelStyle, htmlFor:'tcm-pf-postcode' }, 'ZIP / Postal code'),
                        h('input', {
                            id:'tcm-pf-postcode',
                            type:'text',
                            value: form.postcode,
                            onChange: e => set('postcode', e.target.value),
                            style: inputStyle,
                            maxLength: 20,
                            autoComplete: 'postal-code',
                        })
                    )
                )
            ),

            /* Card 4 — Contact */
            h('div', { style:Object.assign({}, cardStyle, { marginBottom:24 }) },
                h('h2', { style:{ fontSize:15, fontWeight:600, color:textColor, margin:'0 0 16px' } }, 'Contact'),

                h('div', { style:{ display:'grid', gridTemplateColumns:'repeat(auto-fit, minmax(220px, 1fr))', gap:16, marginBottom:16 } },
                    h('div', null,
                        h('label', { style:labelStyle, htmlFor:'tcm-pf-business-email' }, 'Business email'),
                        h('input', {
                            id:'tcm-pf-business-email',
                            type:'email',
                            value: form.business_email,
                            onChange: e => set('business_email', e.target.value),
                            style: inputStyle,
                            maxLength: 191,
                            autoComplete: 'email',
                            placeholder: 'you@yourshop.com',
                        }),
                        h('div', { style:helpStyle }, 'Public-facing email customers can reach you at. Different from your account login email.')
                    ),
                    h('div', null,
                        h('label', { style:labelStyle, htmlFor:'tcm-pf-business-phone' }, 'Business phone'),
                        h('input', {
                            id:'tcm-pf-business-phone',
                            type:'tel',
                            value: form.business_phone,
                            onChange: e => set('business_phone', e.target.value),
                            style: inputStyle,
                            maxLength: 40,
                            autoComplete: 'tel',
                        })
                    )
                ),

                h('div', null,
                    h('label', { style:labelStyle, htmlFor:'tcm-pf-external-url' }, 'Website'),
                    h('input', {
                        id:'tcm-pf-external-url',
                        type:'url',
                        value: form.external_url,
                        onChange: e => set('external_url', e.target.value),
                        style: inputStyle,
                        maxLength: 500,
                        autoComplete: 'url',
                        placeholder: 'https://yourshop.com',
                    }),
                    h('div', { style:helpStyle }, 'Your shop\u2019s external website, if you have one. Leave blank otherwise.')
                )
            ),

            /* Action row — Save + Revert, right-aligned. */
            h('div', { style:{ display:'flex', justifyContent:'flex-end', gap:10, alignItems:'center' } },
                isDirty && h('span', { style:{ fontSize:12.5, color:mutedColor, marginRight:8 } }, 'You have unsaved changes.'),
                h('button', {
                    type:'button',
                    onClick: handleRevert,
                    disabled: !isDirty || saving,
                    style: {
                        padding: '10px 18px',
                        borderRadius: 8,
                        border: `1px solid ${borderColor}`,
                        background: dark ? 'rgba(255,255,255,.04)' : '#FFFFFF',
                        color: textColor,
                        fontSize: 13,
                        fontWeight: 500,
                        cursor: (!isDirty || saving) ? 'not-allowed' : 'pointer',
                        opacity: (!isDirty || saving) ? 0.5 : 1,
                    }
                }, 'Revert'),
                h('button', {
                    type:'button',
                    onClick: handleSubmit,
                    disabled: !isDirty || saving,
                    style: {
                        padding: '10px 22px',
                        borderRadius: 8,
                        border: 'none',
                        background: (!isDirty || saving) ? (dark ? '#404040' : '#A3A3A3') : TC.accent,
                        color: '#FFFFFF',
                        fontSize: 13,
                        fontWeight: 600,
                        cursor: (!isDirty || saving) ? 'not-allowed' : 'pointer',
                    }
                }, saving ? 'Saving\u2026' : 'Save changes')
            )
        );
    }

    /* ════════════════════════════════
     * Root App
     * ════════════════════════════════ */
    function App() {
        const [collapsed,    setCollapsed]    = useState(false);
        const [dark,         setDark]         = useState(function() {
            /* Frontend (storefront) chrome: follow the storefront theme, not the
             * admin's own tc_dark_mode toggle (which the vendor can't reach from
             * the storefront). The storefront marks dark via data-theme="dark"
             * (core theme) or data-tcm-theme="dark" (marketplace skin) on <html>.
             * S246 — vendor SPA sections (products/sales/payouts/profile) rendered
             * white-on-dark before this because dark stayed false here. */
            try {
                if (typeof Config !== 'undefined' && Config && Config.spa_chrome === 'frontend') {
                    var de = document.documentElement;
                    return de.getAttribute('data-theme') === 'dark' || de.getAttribute('data-tcm-theme') === 'dark';
                }
                return localStorage.getItem('tc_dark_mode') === '1';
            } catch(e) { return false; }
        });
        const [expandedMenu, setExpandedMenu] = useState('Products');
        /* Extension toggle forces sidebar re-render via this counter */
        const [extVer,       setExtVer]       = useState(0);
        window._tcBumpExtVer = function(){ setExtVer(function(v){ return v + 1; }); };

        /* Persist dark mode preference. In frontend (storefront) chrome the SPA
         * mirrors the storefront theme and must NOT write tc_dark_mode, or a
         * vendor browsing the dark storefront would silently flip their wp-admin
         * SPA to dark too. S246. */
        useEffect(function() {
            if (typeof Config !== 'undefined' && Config && Config.spa_chrome === 'frontend') return;
            try { localStorage.setItem('tc_dark_mode', dark ? '1' : '0'); } catch(e) {}
        }, [dark]);

        /* Frontend chrome: keep the SPA theme in sync when the storefront's
         * dark-mode toggle flips data-theme / data-tcm-theme on <html> live
         * (no reload). Admin chrome keeps its own in-SPA toggle instead. S246. */
        useEffect(function() {
            if (!(typeof Config !== 'undefined' && Config && Config.spa_chrome === 'frontend')) return;
            var de = document.documentElement;
            var read = function() {
                return de.getAttribute('data-theme') === 'dark' || de.getAttribute('data-tcm-theme') === 'dark';
            };
            var obs = new MutationObserver(function() { setDark(read()); });
            obs.observe(de, { attributes: true, attributeFilter: ['data-theme', 'data-tcm-theme'] });
            return function() { obs.disconnect(); };
        }, []);
        const { route, subId } = useRoute();
        const currentFullSlug = subId ? route + '/' + subId : route;

        /* Pulse UI theme tokens — neutral palette with dark mode */
        const bg            = dark ? '#0A0A0A'  : TC.lightBg;
        const cardBg        = dark ? '#171717'  : TC.card;
        const textColor     = dark ? '#FAFAFA'  : TC.text;
        const mutedColor    = dark ? '#A3A3A3'  : TC.muted;
        const borderColor   = dark ? '#262626'  : TC.border;
        const sidebarBg     = dark ? '#18181B'  : TC.sidebarBg;
        const sidebarBorder = dark ? '#27272A'  : TC.sidebarBorder;
        const activeBg      = dark ? 'rgba(37,99,235,.12)' : 'rgba(37,99,235,.08)';
        const inactiveIcon  = dark ? '#71717A'  : '#52525B';
        const inactiveText  = dark ? '#A1A1AA'  : '#3F3F46';
        const tableHeaderBg = dark ? 'rgba(255,255,255,.05)' : '#FFFFFF';
        tableHeaderShadow = dark ? '0 1px 2px rgba(0,0,0,.15)' : '0 1px 3px rgba(0,0,0,.05), inset 0 -1px 0 rgba(0,0,0,.04)';

        /* CSS keyframes injected once */
        const style = `
            *{box-sizing:border-box;margin:0;padding:0;}
            ::-webkit-scrollbar{width:6px;}
            ::-webkit-scrollbar-track{background:transparent;}
            ::-webkit-scrollbar-thumb{background:${dark?'rgba(255,255,255,.12)':'rgba(0,0,0,.12)'};border-radius:10px;}
            ::-webkit-scrollbar-thumb:hover{background:${dark?'rgba(255,255,255,.2)':'rgba(0,0,0,.2)'};}
            @keyframes tcFadeSlide{from{opacity:0;transform:translateY(6px);}to{opacity:1;transform:translateY(0);}}
            @keyframes tcPulse{0%,100%{opacity:1;}50%{opacity:.4;}}
            @keyframes spin{to{transform:rotate(360deg);}}
            ${dark ? `input[type=number]::-webkit-inner-spin-button,
            input[type=number]::-webkit-outer-spin-button{filter:invert(1) brightness(2);}` : ''}
            #wpfooter{display:none!important;}
            #wpcontent,#wpbody-content{padding-bottom:0!important;}
            #titancart-app h1,#titancart-app h2,#titancart-app h3,#titancart-app h4,#titancart-app h5,#titancart-app h6{color:inherit!important;}
            #titancart-app a{color:inherit;}
            #titancart-app,#titancart-app *{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;}
            #titancart-app{display:block;text-rendering:optimizeLegibility;}
        `;

        /* Active route label */
        const currentLabel = (() => {
            const allItems = currentNavSections().flatMap(s => s.items);
            for (const item of allItems) {
                if (item.children) {
                    const child = item.children.find(c => c.slug === currentFullSlug);
                    if (child) return child.label;
                    if (item.slug === route) return item.label;
                } else if (item.slug === route) {
                    return item.label;
                }
            }
            return 'Dashboard';
        })();

        /* Sub-page label for breadcrumbs */
        const subLabel = (() => {
            if (!subId) return null;
            const subLabels = {
                'general':'General Settings','appearance':'Appearance','company':'Company',
                'checkout':'Checkout','emails':'E-mails','thumbnails':'Thumbnails',
                'security':'Security','logging':'Logging',
                'currencies':'Currencies','storage':'Site Backup','account':'Account & License',
                'profile-fields':'Customer Fields','statuses':'Order Statuses',
                'logs':'System Logs','social-login':'Social Login',
                'payments':'Payment Gateways','shipping':'Shipping Methods','tax':'Tax Rules',
                'email':'Notifications','import-export':'Import / Export','api':'API & Integrations',
                'addons':'Installed Extensions','upgrade':'Update Center','woo-import':'WooCommerce Migration',
            };
            if (route === 'settings' && subLabels[subId]) return subLabels[subId];
            if (route === 'products' && subId) return 'Edit Product';
            if (route === 'attributes' && subId) return 'Edit Attribute';
            if (route === 'ext-settings' && subId) return 'Extension Settings';
            if (route === 'orders' && subId) return 'Order #' + subId;
            if (route === 'customers' && subId === 'add') return 'Add Customer';
            if (route === 'customers' && subId) return 'Edit Customer';
            if (route === 'vendors' && subId) return 'Vendor #' + subId;
            if (route === 'fulfillment' && subId === 'new') return 'Create Shipment';
            if (route === 'fulfillment' && subId) return 'Shipment #' + subId;
            if (route === 'active-carts' && subId) return 'Cart #' + subId;
            if (route === 'subscriptions' && subId === 'plans') return 'Plans';
            if (route === 'subscriptions' && subId === 'analytics') return 'Analytics';
            if (route === 'subscriptions' && subId === 'sub-settings') return 'Settings';
            if (route === 'subscriptions' && subId) return 'Subscription #' + subId;
            if (route === 'wishlists' && subId === 'wl-settings') return 'Settings';
            if (route === 'datafeeds' && subId === 'new') return 'New Feed';
            if (route === 'datafeeds' && subId === 'df-settings') return 'Settings';
            if (route === 'datafeeds' && subId) return 'Edit Feed';
            if (route === 'cartrecovery' && subId === 'sequence') return 'Sequence';
            if (route === 'cartrecovery' && subId === 'analytics') return 'Analytics';
            if (route === 'cartrecovery' && subId === 'cr-settings') return 'Settings';
            if (route === 'cartrecovery' && subId) return 'Recovery #' + subId;
            if (route === 'smsnotifications' && subId === 'log') return 'Message log';
            if (route === 'smsnotifications' && subId === 'subscribers') return 'Subscribers';
            if (route === 'smsnotifications' && subId === 'sms-settings') return 'Settings';
            if (route === 'affiliates' && subId === 'conversions') return 'Conversions';
            if (route === 'affiliates' && subId === 'payouts') return 'Payouts';
            if (route === 'affiliates' && subId === 'af-settings') return 'Settings';
            if (route === 'affiliates' && subId === 'analytics') return 'Analytics';
            if (route === 'affiliates' && subId && /^\d+$/.test(subId)) return 'Affiliate #' + subId;
            return null;
        })();

        /* Common page props */
        const pageProps = { dark, cardBg, textColor, mutedColor, borderColor, tableHeaderBg, tableHeaderShadow };

        /* S184 — SPA chrome mode. When Config.spa_chrome === 'frontend' the
         * SPA is mounted inside the marketplace storefront theme rather than
         * the wp-admin shell.
         *
         * In frontend mode the SPA renders ONLY the page content: no internal
         * sidebar (the storefront's My Account sidebar handles vendor nav),
         * no internal header (the storefront's breadcrumb-hero serves as the
         * page header), no fixed 100vh height (content flows inside the
         * storefront's content column and grows with its natural size).
         *
         * In admin mode (default) we keep the full chrome — sidebar, header,
         * collapse button, dark-mode toggle, breadcrumb, etc. — exactly as
         * before, since the admin SPA is the full surface there.
         *
         * The routing tree is identical in both modes; only the wrapping
         * container differs.
         */
        const isFrontendChrome = (Config && Config.spa_chrome === 'frontend');

        /* Routed page — same selection logic in both chrome modes. Built
         * once here, then placed inside the appropriate container below. */
        const routedPage = (
            route === 'dashboard'      ? h(DashboardPage,      pageProps)
            : route === 'vendor-products' ? h(VendorProductsPage, { subId, ...pageProps })
            : route === 'vendor-sales'    ? h(VendorSalesPage,    pageProps)
            : route === 'vendor-payouts'  ? h(VendorPayoutsPage,  pageProps)
            : route === 'vendor-profile'  ? h(VendorProfilePage,  pageProps)
            : route === 'products'     ? h(ProductsPage,       { subId, ...pageProps })
            : route === 'categories'   ? h(CategoriesPage,     { subId, ...pageProps })
            : route === 'attributes'   ? h(FeaturesPage,       pageProps)
            : route === 'filters'      ? h(FiltersPage,        pageProps)
            : route === 'reviews'      ? h(ReviewsPage,        pageProps)
            : route === 'orders'       ? h(OrdersPage,         { subId, ...pageProps })
            : route === 'customers'    ? h(CustomersPage,      { subId, ...pageProps })
            : route === 'customer-groups' ? h(CustomerGroupsPage, pageProps)
            : route === 'administrators' ? h(AdministratorsPage, pageProps)
            : route === 'vendors'      ? h(AdminVendorsPage,    { subId, ...pageProps })
            : route === 'shipping'     ? h(ShippingPage,       pageProps)
            : route === 'fulfillment'  ? h(FulfillmentPage,    { subId, ...pageProps })
            : route === 'active-carts' ? h(ActiveCartsPage,    { subId, ...pageProps })
            : route === 'tags'         ? h(TagsPage,          pageProps)
            : route === 'seo'          ? h(SeoPage,           pageProps)
            : route === 'sitemap'      ? h(SitemapPage,       pageProps)
            : route === 'pages'        ? h(PagesPage,         pageProps)
            : route === 'testimonials' ? h(TestimonialsPage, pageProps)
            : route === 'faq'          ? h(FaqPage,          pageProps)
            : route === 'agenda'       ? h(TodoPage, pageProps)
            : route === 'calendar'     ? h(CalendarPage, pageProps)
            : route === 'subscriptions' ? h(SubscriptionsPage, { subId, ...pageProps })
            : route === 'wishlists'    ? h(WishlistPlusPage,   { subId, ...pageProps })
            : route === 'tipdonation'  ? h(TipDonationPage,    { subId, ...pageProps })
            : route === 'printful'     ? h(PrintfulPage,       { subId, ...pageProps })
            : route === 'datafeeds'    ? h(DataFeedsPage,      { subId, ...pageProps })
            : route === 'cartrecovery' ? h(AbandonedCartPage,  { subId, ...pageProps })
            : route === 'smsnotifications' ? h(SmsNotificationsPage, { subId, ...pageProps })
            : route === 'affiliates'   ? h(AffiliatesPage,     { subId, ...pageProps })
            : route === 'payments'     ? h(PaymentsPage,       pageProps)
            : route === 'tax'          ? h(TaxPage,            pageProps)
            : route === 'email'        ? h(EmailPage,          pageProps)
            : route === 'settings'     ? h(SettingsPage,       { subId, ...pageProps })
            : route === 'import-export'? h(ImportExportPage,   pageProps)
            : route === 'installed-extensions' ? h(ExtensionsPage, { tab:'installed-extensions', ...pageProps })
            /* KI-36: these two routes are backed by REST routes the Lite package
               strips, and the marketplace one additionally reaches out to
               market.titancart.io. Hiding their nav nodes does not make the
               routes unreachable — a typed hash still lands here. Fall through
               to the Stub, which renders a title and nothing else. */
            : (route === 'extensions-marketplace' && !Config.noPhoneHome) ? h(ExtensionsPage, { tab:'extensions-marketplace', ...pageProps })
            : (route === 'updates' && !Config.noPhoneHome) ? h(ExtensionsPage, { tab:'updates', ...pageProps })
            : route === 'ext-settings' ? h(ExtensionSettingsPage, { subId, ...pageProps })
            : h(Stub, { title: currentLabel, ...pageProps })
        );

        /* ── Frontend-chrome render path (S184) ──────────────────────────
         * Used when the SPA is mounted inside the storefront's My Account
         * content column. Renders only the page body — no sidebar, no
         * header, no fixed-height flex container. The storefront supplies
         * all surrounding chrome (My Account sidebar, breadcrumb-hero,
         * site header/footer).
         *
         * Page components use `flex:1, overflow:auto` on a wrapping main
         * element internally; without the outer flex container they
         * naturally fall back to block layout and grow with their content,
         * which is the behavior we want inside a storefront column.
         *
         * The style block is trimmed too — no need for wpcontent padding
         * overrides or wpfooter hides; those wp-admin-specific rules don't
         * apply on the frontend and stripping them avoids leaking admin
         * style assumptions into the storefront theme.
         */
        if (isFrontendChrome) {
            // Inter is supplied by the host that mounts frontend chrome (the
            // marketplace extension enqueues it as `tcm-fonts` on every vendor
            // SPA section), so the bundle itself makes no font request. The
            // fontFamily stack below falls back to system fonts if absent.
            const frontendStyle = `
                #titancart-app, #titancart-app *{box-sizing:border-box;}
                #titancart-app{display:block;}
                #titancart-app h1,#titancart-app h2,#titancart-app h3,#titancart-app h4,#titancart-app h5,#titancart-app h6{color:inherit;}
                #titancart-app a{color:inherit;}
                @keyframes tcFadeSlide{from{opacity:0;transform:translateY(6px);}to{opacity:1;transform:translateY(0);}}
                @keyframes tcPulse{0%,100%{opacity:1;}50%{opacity:.4;}}
                @keyframes spin{to{transform:rotate(360deg);}}
            `;
            return h(Fragment, null,
                h('style', null, frontendStyle),
                h('div', {
                    style: {
                        fontFamily: "'Inter','Segoe UI',system-ui,-apple-system,sans-serif",
                        background: bg,
                        color: textColor,
                        fontSize: 14,
                        minHeight: 480,
                    }
                },
                    h(ErrorBoundary, null, routedPage)
                )
            );
        }

        return h(Fragment, null,
            h('style', null, style),
            h('div', { style:{display:'flex', height:'calc(100vh - 32px)', fontFamily:"-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Oxygen-Sans,Ubuntu,Cantarell,'Helvetica Neue',sans-serif", background:bg, color:textColor, overflow:'hidden', fontSize:14} },

                /* ══ SIDEBAR — Pulse UI style ══ */
                h('aside', { style:{width:collapsed?64:260, background:sidebarBg, borderRight:`1px solid ${sidebarBorder}`, display:'flex', flexDirection:'column', transition:'width .2s ease', flexShrink:0, overflow:'hidden'} },

                    /* Logo + brand
                       S149 — Replaced the "TC" gradient text tile with the
                       brand mark PNG (plugin/assets/images/logo.png). The
                       logo's own purple gradient supplies the visual treatment
                       that the gradient background+drop-shadow used to provide,
                       so we strip the chrome (background, color, fontWeight,
                       fontSize, boxShadow). 36x36 keeps the same horizontal
                       footprint so adjacent layout (gap, brand text alignment,
                       64px header height) is unchanged. The image src uses
                       Config.assetsUrl which the server localizes to TITANCART_ASSETS_URL,
                       falling back to the conventional plugin path if missing. */
                    h('div', { style:{padding:collapsed?'16px 12px':'16px 20px', display:'flex', alignItems:'center', gap:12, minHeight:64} },
                        h('img', { src: (Config.assetsUrl || '/wp-content/plugins/titancart/assets/') + 'images/logo.png', alt: 'TitanCart', style:{width:36, height:36, flexShrink:0, display:'block'} }),
                        !collapsed && h('div', { style:{overflow:'hidden', whiteSpace:'nowrap'} },
                            h('div', { style:{fontWeight:600, fontSize:15, letterSpacing:'-.2px', color:textColor} }, 'TitanCart'),
                            h('div', { style:{fontSize:11, color:mutedColor, marginTop:1, fontWeight:400} }, 'eCommerce Engine')
                        )
                    ),

                    /* Nav */
                    h('nav', { style:{flex:1, padding:'8px 12px', overflowY:'auto'} },
                        ...currentNavSections().map((section, si) =>
                            h('div', { key:si, style:{marginBottom:4} },
                                section.title && !collapsed && h('div', { style:{fontSize:11, fontWeight:500, color:mutedColor, letterSpacing:'.05em', padding:'16px 12px 6px', textTransform:'uppercase'} }, section.title),
                                collapsed && section.title && h('div', { style:{height:1, background:sidebarBorder, margin:'8px 6px'} }),
                                ...section.items.filter(navParentVisible).map(item => {
                                    const isExpanded = expandedMenu === item.label;
                                    const isActive   = item.slug === route
                                        || (item.children && item.children.some(c => c.slug === currentFullSlug || c.slug === route));
                                    return h('div', { key:item.label },
                                        h('div', {
                                            onClick: () => { if (collapsed) { navigate(item.collapsedSlug || item.slug); } else if (item.children) { setExpandedMenu(isExpanded ? null : item.label); } else { navigate(item.slug); } },
                                            style: { display:'flex', alignItems:'center', gap:10, padding:collapsed?'10px 0':'8px 12px', justifyContent:collapsed?'center':'flex-start', borderRadius:8, cursor:'pointer', marginBottom:2, background:isActive?activeBg:'transparent', color:isActive?TC.accent:inactiveText, fontWeight:isActive?500:400, transition:'all .15s ease', fontSize:14 },
                                            onMouseEnter: e => { if (!isActive) e.currentTarget.style.background = dark?'rgba(255,255,255,.04)':'rgba(0,0,0,.03)'; },
                                            onMouseLeave: e => { if (!isActive) e.currentTarget.style.background = isActive?activeBg:'transparent'; },
                                        },
                                            h('span', { style:{flexShrink:0, display:'flex', color:isActive?TC.accent:inactiveIcon} }, icons[item.icon]),
                                            !collapsed && h(Fragment, null,
                                                h('span', { style:{flex:1} }, item.label),
                                                item.badge && h('span', { style:{background:item.badgeAccent?TC.accent:(dark?'#27272A':'#F4F4F5'), color:item.badgeAccent?'#fff':mutedColor, fontSize:11, fontWeight:500, padding:'1px 8px', borderRadius:99} }, item.badge),
                                                item.children && h('span', { style:{display:'flex', transform:isExpanded?'rotate(90deg)':'rotate(0)', transition:'transform .2s ease', color:inactiveIcon, opacity:.7} }, icons.chevron)
                                            )
                                        ),
                                        !collapsed && item.children && isExpanded && h('div', { style:{paddingLeft:38, marginBottom:4} },
                                            ...item.children.filter(navVisible).map(child => {
                                                const isChildActive = child.slug === currentFullSlug;
                                                return h('div', {
                                                    key: child.slug,
                                                    onClick: () => navigate(child.slug),
                                                    style:{display:'flex', alignItems:'center', gap:8, padding:'7px 12px', borderRadius:6, cursor:'pointer', fontSize:13, marginBottom:1, color:isChildActive?TC.accent:inactiveText, fontWeight:isChildActive?500:400, position:'relative', transition:'all .12s ease'},
                                                    onMouseEnter: e => { e.currentTarget.style.background = dark?'rgba(255,255,255,.04)':'rgba(0,0,0,.03)'; },
                                                    onMouseLeave: e => { e.currentTarget.style.background = 'transparent'; },
                                                },
                                                    h('span', { style:{position:'absolute', left:-6, top:'50%', transform:'translateY(-50%)', width:5, height:5, borderRadius:'50%', background:isChildActive?TC.accent:(dark?'#3F3F46':'#D4D4D8')} }),
                                                    h('span', { style:{flex:1, overflow:'hidden', textOverflow:'ellipsis', whiteSpace:'nowrap'} }, child.label),
                                                );
                                            })
                                        )
                                    );
                                })
                            )
                        )
                    ),

                    /* Bottom section: docs + support + collapse */
                    h('div', { style:{borderTop:`1px solid ${sidebarBorder}`} },
                        h('div', { style:{padding:'6px 12px'} },
                            ...[
                                { icon:'docs',    label:'Documentation' },
                                { icon:'support', label:'Support'       },
                            ].map(item =>
                                h('div', {
                                    key: item.label,
                                    style:{display:'flex', alignItems:'center', gap:10, padding:collapsed?'10px 0':'8px 12px', justifyContent:collapsed?'center':'flex-start', borderRadius:8, cursor:'pointer', marginBottom:2, color:inactiveText, fontSize:14, transition:'all .15s ease'},
                                    onMouseEnter: e => { e.currentTarget.style.background = dark?'rgba(255,255,255,.04)':'rgba(0,0,0,.03)'; },
                                    onMouseLeave: e => { e.currentTarget.style.background = 'transparent'; },
                                },
                                    h('span', { style:{flexShrink:0, display:'flex', color:inactiveIcon} }, icons[item.icon]),
                                    !collapsed && h('span', { style:{fontWeight:400} }, item.label)
                                )
                            )
                        ),
                        /* Collapse toggle */
                        h('div', { style:{padding:'8px 12px', borderTop:`1px solid ${sidebarBorder}`} },
                            h('button', {
                                onClick: () => setCollapsed(!collapsed),
                                style:{background:'transparent', border:`1px solid ${sidebarBorder}`, borderRadius:8, color:inactiveIcon, cursor:'pointer', padding:'8px', fontSize:13, width:'100%', display:'flex', alignItems:'center', justifyContent:'center', gap:8, transition:'border-color .15s'},
                                onMouseEnter: e => { e.currentTarget.style.borderColor = mutedColor; },
                                onMouseLeave: e => { e.currentTarget.style.borderColor = sidebarBorder; },
                            },
                                h('svg', { width:16, height:16, viewBox:'0 0 16 16', fill:'none', stroke:'currentColor', strokeWidth:'1.5', strokeLinecap:'round' },
                                    collapsed
                                        ? h(Fragment, null, h('path',{d:'M3 4h10'}), h('path',{d:'M3 8h10'}), h('path',{d:'M3 12h10'}))
                                        : h(Fragment, null, h('path',{d:'M11 4l-4 4 4 4'}), h('path',{d:'M3 4h2'}), h('path',{d:'M3 8h2'}), h('path',{d:'M3 12h2'}))
                                ),
                                !collapsed && h('span', { style:{fontSize:13, fontWeight:400} }, 'Collapse')
                            )
                        )
                    )
                ),

                /* ══ MAIN ══ */
                h('div', { style:{flex:1, display:'flex', flexDirection:'column', overflow:'hidden'} },

                    /* Header — Pulse UI style: sticky, clean, breadcrumbs */
                    /* S454 — position:relative + zIndex:100: the backdropFilter creates a
                     * stacking context with z-index auto, so the content column (later in
                     * DOM order) painted OVER the header's children — the NotificationBell
                     * dropdown (zIndex:1000, but trapped inside this context) rendered
                     * behind stat cards / page elements. Lifting the header's own context
                     * above the content column fixes every header dropdown at once. */
                    h('header', { style:{height:64, background:cardBg, borderBottom:`1px solid ${borderColor}`, display:'flex', alignItems:'center', justifyContent:'space-between', padding:'0 24px', flexShrink:0, backdropFilter:'blur(8px)', position:'relative', zIndex:100} },
                        h('div', { style:{display:'flex', alignItems:'center', gap:12} },
                            /* Sidebar trigger for mobile */
                            h('button', {
                                onClick: () => setCollapsed(!collapsed),
                                style:{background:'transparent', border:'none', borderRadius:'50%', width:36, height:36, display:'flex', alignItems:'center', justifyContent:'center', cursor:'pointer', color:mutedColor, transition:'background .15s'},
                                onMouseEnter: e => { e.currentTarget.style.background = dark?'rgba(255,255,255,.06)':'rgba(0,0,0,.04)'; },
                                onMouseLeave: e => { e.currentTarget.style.background = 'transparent'; },
                            },
                                h('svg', { width:20, height:20, viewBox:'0 0 20 20', fill:'none', stroke:'currentColor', strokeWidth:'1.5', strokeLinecap:'round' },
                                    h('path',{d:'M3 5h14'}), h('path',{d:'M3 10h14'}), h('path',{d:'M3 15h14'})
                                )
                            ),
                            /* Separator */
                            h('div', { style:{width:1, height:20, background:borderColor} }),
                            /* Breadcrumb */
                            h('div', { style:{display:'flex', alignItems:'center', gap:6, fontSize:14} },
                                h('span', { style:{color:mutedColor, cursor:'pointer'}, onClick:()=>navigate('dashboard') }, 'Dashboard'),
                                h('span', { style:{color:mutedColor, fontSize:11} }, '/'),
                                subLabel
                                    ? h(Fragment, null,
                                        h('span', { style:{color:mutedColor, cursor:'pointer'}, onClick:()=>navigate(route) }, currentLabel),
                                        h('span', { style:{color:mutedColor, fontSize:11} }, '/'),
                                        h('span', { style:{color:textColor, fontWeight:500} }, subLabel)
                                    )
                                    : h('span', { style:{color:textColor, fontWeight:500} }, currentLabel)
                            )
                        ),
                        h('div', { style:{display:'flex', alignItems:'center', gap:4} },
                            /* Search */
                            h('button', {
                                style:{display:'flex', alignItems:'center', gap:8, background:dark?'#262626':TC.secondary, borderRadius:8, padding:'7px 14px', border:'none', cursor:'pointer', color:mutedColor, fontSize:13, transition:'background .15s'},
                                onMouseEnter: e => { e.currentTarget.style.background = dark?'#333':'#EBEBEB'; },
                                onMouseLeave: e => { e.currentTarget.style.background = dark?'#262626':TC.secondary; },
                                /* roadmap #13 — open the TitanCart-native command palette (CommandPalette
                                 * component), replacing the KI-25 dependency on WordPress core's
                                 * @wordpress/commands. The palette listens for this window event (and ⌘K). */
                                onClick: function(){ try { window.dispatchEvent(new CustomEvent('tc:command-palette-open')); } catch(e){} },
                            },
                                h('span', { style:{display:'flex'} }, icons.search),
                                h('span', null, 'Search…'),
                                h('span', { style:{marginLeft:16, fontSize:11, color:mutedColor, background:dark?'#171717':'#fff', padding:'2px 6px', borderRadius:4, border:`1px solid ${borderColor}`} }, '⌘K')
                            ),
                            /* Dark mode toggle */
                            h('button', { onClick:()=>setDark(!dark), style:{background:'transparent', border:'none', borderRadius:'50%', width:36, height:36, cursor:'pointer', color:mutedColor, display:'flex', alignItems:'center', justifyContent:'center', transition:'background .15s'},
                                onMouseEnter: e => { e.currentTarget.style.background = dark?'rgba(255,255,255,.06)':'rgba(0,0,0,.04)'; },
                                onMouseLeave: e => { e.currentTarget.style.background = 'transparent'; },
                            }, dark ? icons.sun : icons.moon),
                            /* Bell — KI-26 (S304): real notifications consumer (orders + vendor applications) */
                            h(NotificationBell, { dark, cardBg, mutedColor, borderColor, textColor, navigate }),
                            /* Avatar */
                            h('div', { style:{width:36, height:36, borderRadius:'50%', background:`linear-gradient(135deg,${TC.primary},${TC.accent})`, display:'flex', alignItems:'center', justifyContent:'center', color:'#fff', fontSize:13, fontWeight:600, cursor:'pointer', marginLeft:4} }, 'SM')
                        )
                    ),

                    /* Page content — wrapped in ErrorBoundary so one broken page can't freeze nav.
                     * routedPage is computed above and shared with the
                     * frontend-chrome render path (S184). */
                    h(ErrorBoundary, null, routedPage)
                )
            ),

            /* roadmap #13 — TitanCart-native command palette overlay (⌘K / header Search). */
            h(CommandPalette, { dark, cardBg, mutedColor, borderColor, textColor })
        );
    }

    /* ── Mount ── */
    const mount = document.getElementById('titancart-app');
    if (mount) {
        wp.element.render(h(App), mount);
    }

})();
