Torn RW ThreadSmith

Forum thread generator for RW traders. Vault storage, 10 designs, live preview, per-item notes, filters, bulk actions, export/import. FREE

スクリプトをインストールするには、Tampermonkey, GreasemonkeyViolentmonkey のような拡張機能のインストールが必要です。

You will need to install an extension such as Tampermonkey to install this script.

スクリプトをインストールするには、TampermonkeyViolentmonkey のような拡張機能のインストールが必要です。

スクリプトをインストールするには、TampermonkeyUserscripts のような拡張機能のインストールが必要です。

このスクリプトをインストールするには、Tampermonkeyなどの拡張機能をインストールする必要があります。

このスクリプトをインストールするには、ユーザースクリプト管理ツールの拡張機能をインストールする必要があります。

(ユーザースクリプト管理ツールは設定済みなのでインストール!)

このスタイルをインストールするには、Stylusなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus などの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus tなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

(ユーザースタイル管理ツールは設定済みなのでインストール!)

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください
// ==UserScript==
// @name         Torn RW ThreadSmith
// @namespace    http://tampermonkey.net/
// @version       4.4.0
// @description   Forum thread generator for RW traders. Vault storage, 10 designs, live preview, per-item notes, filters, bulk actions, export/import. FREE
// @author        Rowage [3926289]
// @copyright     2026, Rowage [3926289]
// @match         https://www.torn.com/forums.php*
// @grant         GM_xmlhttpRequest
// @grant         GM_setValue
// @grant         GM_getValue
// @grant         GM_setClipboard
// @license       GPL-3.0-or-later
// ==/UserScript==

(function () {
    'use strict';

    const VERSION         = '4.4.0';
    const API_THROTTLE_MS = 700;
    const PANEL_MAX_H     = 580;

    const RARITY_COLORS = {
        Yellow: '#ffff00',
        Orange: '#ff8000',
        Red:    '#ff0000',
        White:  '#ffffff'
    };
    const TYPE_ORDER    = ['Primary', 'Secondary', 'Melee', 'Armor', 'Other'];
    const RARITY_ORDER  = ['Yellow', 'Orange', 'Red', 'White'];
    const STATUS_ORDER  = ['active', 'sold', 'hidden'];
    const STATUS_LABELS = { active: 'Active', sold: 'Sold', hidden: 'Hidden' };
    const STATUS_COLORS = { active: '#4caf50', sold: '#ffcc4d', hidden: '#aaa' };

    const UI = {
        panelBg:    '#181818',
        headerBg:   '#202020',
        tabBarBg:   '#1a1a1a',
        cardBg:     '#0f0f0f',
        inputBg:    '#0f0f0f',
        border:     '#2e2e2e',
        borderSoft: '#252525',
        textMain:   '#e8e8e8',
        textDim:    '#c0c0c0',
        textFaint:  '#aaa',
        accent:     '#c9a84c',
        accentBg:   '#291e00',
        accentBgHi: '#372800',
        success:    '#4caf50',
        successBg:  '#0e1f0e',
        danger:     '#ff6b6b',
        dangerBg:   '#250e0e',
        bonusUI:    '#a88fff',
        soldUI:     '#ffcc4d',
        soldBg:     '#2a1f00'
    };

    const SHARED = {
        bonus: '#00ff66',
        stats: '#cccccc'
    };

    const $       = id => document.getElementById(id);
    const sleep   = ms => new Promise(r => setTimeout(r, ms));
    const normType   = t => t === 'Defensive' ? 'Armor' : (t || 'Other');
    const normRarity = r => RARITY_ORDER.includes(r) ? r : 'White';
    const hasBonuses = d => d?.bonuses && Object.keys(d.bonuses).length > 0;
    const normStatus = s => STATUS_ORDER.includes(s) ? s : 'active';

    const fmt = val => {
        if (!val) return 'Offer';
        const mul = { k: 1e3, m: 1e6, b: 1e9 };
        const s = val.toString().toLowerCase().replace(/[\s,]/g, '');
        const sfx = s.match(/[kmb]$/);
        let n = parseFloat(s.replace(/[^0-9.]/g, ''));
        if (isNaN(n)) return 'Offer';
        if (sfx) n *= mul[sfx[0]];
        return '$' + n.toLocaleString('en-US');
    };

    const api = url => new Promise((resolve, reject) => {
        GM_xmlhttpRequest({
            method: 'GET',
            url,
            responseType: 'json',
            onload: r => {
                const d = r.response;
                if (!d) reject(new Error('Empty response'));
                else if (d.error) reject(new Error(d.error.error));
                else resolve(d);
            },
            onerror: reject
        });
    });

    const fmtPrice = item => {
        if (item.status === 'sold') return 'SOLD';
        return fmt(item.manualPrice || item.bazaar_price || '');
    };
    const fmtStats   = item => item.damage
        ? `Q: ${item.quality ?? 'N/A'}% | Dmg: ${item.damage} | Acc: ${item.accuracy ?? 'N/A'}`
        : `Q: ${item.quality ?? 'N/A'}% | Armor: ${item.armor ?? 'N/A'}`;
    const fmtBonuses = item => Object.values(item.bonuses || {})
        .map(b => `${b.bonus} ${b.value}%`).join(' | ');
    const fmtNote = (item, include) => {
        if (!include) return '';
        const note = GM_getValue('rwts_notes', {})[item.UID];
        if (!note) return '';
        return `<div style="font-size:11px;color:#999;font-style:italic;margin-top:6px;padding-top:6px;border-top:1px solid rgba(255,255,255,0.06);">${note}</div>`;
    };

    const parseValue = raw => {
        if (raw == null || raw === '') return 0;
        if (typeof raw === 'number') return isNaN(raw) ? 0 : raw;
        const mul = { k: 1e3, m: 1e6, b: 1e9 };
        const s = raw.toString().toLowerCase().replace(/[\s,$]/g, '');
        const sfx = s.match(/[kmb]$/);
        let n = parseFloat(s.replace(/[^0-9.]/g, ''));
        if (isNaN(n)) return 0;
        if (sfx) n *= mul[sfx[0]];
        return n;
    };

    const valOf  = it => parseValue(it.manualPrice || it.bazaar_price);
    const dmgOf  = it => Number(it.damage)  || 0;
    const armOf  = it => Number(it.armor)   || 0;
    const qualOf = it => Number(it.quality) || 0;
    const bonusCount   = it => Object.keys(it.bonuses || {}).length;
    const primaryBonus = it => {
        const names = Object.values(it.bonuses || {}).map(b => b.bonus).filter(Boolean);
        if (!names.length) return '';
        return names.slice().sort((a, b) => a.localeCompare(b))[0];
    };

    const byName  = (a, b) => a.name.localeCompare(b.name);
    const byNum   = (get, dir) => (a, b) => {
        const x = get(a), y = get(b);
        if (!x && !y) return 0;
        if (!x) return 1;
        if (!y) return -1;
        return dir === 'asc' ? x - y : y - x;
    };
    const byIndex = (get, order) => (a, b) =>
        order.indexOf(get(a)) - order.indexOf(get(b));

    const SORTS = {
        'value-desc':      { label: 'Value: high to low',       field: 'value',      sections: 'both',   fn: byNum(valOf, 'desc') },
        'value-asc':       { label: 'Value: low to high',       field: 'value',      sections: 'both',   fn: byNum(valOf, 'asc') },
        'name-asc':        { label: 'Name: A to Z',             field: 'name',       sections: 'both',   fn: byName },
        'name-desc':       { label: 'Name: Z to A',             field: 'name',       sections: 'both',   fn: (a, b) => byName(b, a) },
        'rarity':          { label: 'Rarity',                   field: 'rarity',     sections: 'both',   fn: byIndex(i => normRarity(i.rarity), RARITY_ORDER) },
        'bonus-asc':       { label: 'Weapon bonus: A to Z',     field: 'bonus',      sections: 'both',   fn: (a, b) => primaryBonus(a).localeCompare(primaryBonus(b)) },
        'bonuscount-desc': { label: 'Bonus count: most first',  field: 'bonuscount', sections: 'both',   fn: byNum(bonusCount, 'desc') },
        'quality-desc':    { label: 'Quality: high to low',     field: 'quality',    sections: 'both',   fn: byNum(qualOf, 'desc') },
        'type':            { label: 'Weapon type',              field: 'type',       sections: 'weapon', fn: byIndex(i => normType(i.type), TYPE_ORDER) },
        'damage-desc':     { label: 'Damage: high to low',      field: 'damage',     sections: 'weapon', fn: byNum(dmgOf, 'desc') },
        'armor-desc':      { label: 'Armor rating: high to low', field: 'armor',     sections: 'armor',  fn: byNum(armOf, 'desc') }
    };

    const fieldOf = key => SORTS[key]?.field || key;
    const buildComparator = (primaryKey, secondaryKey) => {
        const fns = [];
        if (SORTS[primaryKey]) fns.push(SORTS[primaryKey].fn);
        if (SORTS[secondaryKey] && fieldOf(secondaryKey) !== fieldOf(primaryKey)) fns.push(SORTS[secondaryKey].fn);
        fns.push(byName);
        return (a, b) => {
            for (const fn of fns) {
                const r = fn(a, b);
                if (r) return r;
            }
            return 0;
        };
    };

    const GROUPS = {
        none:   { label: 'No grouping (flat list)', sections: 'both',   keyOf: () => '__all',                  order: ['__all'],  title: () => '' },
        type:   { label: 'Weapon type',             sections: 'weapon', keyOf: i => normType(i.type),          order: TYPE_ORDER, title: k => `${k} Items` },
        rarity: { label: 'Rarity',                  sections: 'both',   keyOf: i => normRarity(i.rarity),      order: RARITY_ORDER, title: k => `${k} Items` },
        bonus:  { label: 'Weapon bonus',            sections: 'both',   keyOf: i => primaryBonus(i) || 'No Bonus', order: null,   title: k => k }
    };

    const inSection = (def, section) => def.sections === 'both' || def.sections === section;
    const optsFor = (registry, section) => Object.entries(registry)
        .filter(([, def]) => inSection(def, section))
        .map(([k, def]) => `<option value="${k}">${def.label}</option>`)
        .join('');

    const THEMES = {
        glow: {
            label: 'Neon Box',
            palette: {
                innerBg:   '#1a1a1a',
                border:    '#333',
                priceText: '#000',
                statsText: '#efefef'
            },
            item(item, rarity, includeNote) {
                const c = this.palette;
                return `
                    <div style="max-width:550px;margin:0 auto 15px auto;padding:2px;background:${rarity};border-radius:10px;">
                        <div style="padding:15px;background:${c.innerBg};border-radius:8px;text-align:left;">
                            <div style="display:flex;justify-content:space-between;align-items:center;border-bottom:1px solid ${c.border};padding-bottom:8px;margin-bottom:8px;">
                                <span style="font-size:16px;font-weight:bold;text-transform:uppercase;letter-spacing:1px;"><font color="${rarity}">${item.name}</font></span>
                                <span style="background:${rarity};padding:2px 8px;border-radius:4px;font-weight:bold;font-size:14px;"><font color="${c.priceText}">${fmtPrice(item)}</font></span>
                            </div>
                            <div style="font-size:12px;margin-bottom:4px;"><font color="${c.statsText}">${fmtStats(item)}</font></div>
                            <div style="font-family:monospace;font-weight:bold;font-size:13px;"><font color="${SHARED.bonus}">>> ${fmtBonuses(item)}</font></div>
                            ${fmtNote(item, includeNote)}
                        </div>
                    </div>`;
            },
            header(title, desc) {
                const c = this.palette;
                return `<div style="max-width:600px;margin:0 auto 30px auto;padding:30px;background:${c.innerBg};border:2px solid ${SHARED.bonus};border-radius:12px;"><h1 style="color:#fff;text-transform:uppercase;margin:0 0 15px 0;font-size:28px;">${title}</h1><p style="color:#ccc;font-size:15px;margin:0;">${desc.replace(/\n/g, '<br>')}</p></div>`;
            }
        },

        banner: {
            label: 'Gradient Banner',
            palette: {
                bgDark:    '#222',
                priceText: '#4dd0e1'
            },
            item(item, rarity, includeNote) {
                const c = this.palette;
                return `
                    <div style="max-width:650px;margin:0 auto 12px auto;background:linear-gradient(90deg,${rarity}15 0%,${c.bgDark} 100%);border-right:4px solid ${rarity};padding:15px;text-align:left;border-radius:4px;">
                        <div style="display:flex;justify-content:space-between;align-items:center;">
                            <span style="font-weight:bold;font-size:16px;"><font color="${rarity}">${item.name}</font></span>
                            <span style="font-weight:bold;font-size:15px;"><font color="${c.priceText}">${fmtPrice(item)}</font></span>
                        </div>
                        <div style="margin-top:5px;font-size:11px;"><font color="${SHARED.stats}">${fmtStats(item)}</font> | <font color="${SHARED.bonus}"><b>${fmtBonuses(item)}</b></font></div>
                        ${fmtNote(item, includeNote)}
                    </div>`;
            },
            header(title, desc) {
                const c = this.palette;
                return `<div style="max-width:700px;margin:0 auto 30px auto;background:linear-gradient(90deg,${c.priceText}33 0%,${c.bgDark} 100%);border-left:6px solid ${c.priceText};padding:30px;text-align:left;"><h1 style="color:#fff;margin:0 0 15px 0;font-size:28px;">${title}</h1><p style="color:${SHARED.stats};font-size:16px;margin:0;">${desc.replace(/\n/g, '<br>')}</p></div>`;
            }
        },

        split: {
            label: 'Split Bar',
            palette: {
                bgDark:    '#222',
                bgLight:   '#1a1a1a',
                border:    '#333',
                priceText: '#ffffff'
            },
            item(item, rarity, includeNote) {
                const c = this.palette;
                return `
                    <div style="max-width:500px;margin:0 auto 10px auto;background:${c.bgDark};border-radius:6px;overflow:hidden;border:1px solid ${c.border};">
                        <div style="display:flex;justify-content:space-between;align-items:center;padding:10px;background:${c.bgLight};border-bottom:2px solid ${rarity};">
                            <span style="font-size:14px;font-weight:bold;text-transform:uppercase;"><font color="${rarity}">${item.name}</font></span>
                            <span style="font-size:15px;font-weight:bold;"><font color="${c.priceText}">${fmtPrice(item)}</font></span>
                        </div>
                        <div style="padding:12px;background:${c.bgDark};display:flex;justify-content:space-between;align-items:center;">
                            <span><font color="${SHARED.stats}" style="font-size:11px;">${fmtStats(item)}</font></span>
                            <span style="text-align:right;"><font color="${SHARED.bonus}" style="font-size:12px;font-weight:bold;">${fmtBonuses(item)}</font></span>
                        </div>
                        ${fmtNote(item, includeNote)}
                    </div>`;
            },
            header(title, desc) {
                const c = this.palette;
                return `<div style="max-width:550px;margin:0 auto 30px auto;background:${c.bgDark};border:1px solid ${c.border};border-radius:8px;overflow:hidden;"><div style="background:${c.bgLight};padding:20px;border-bottom:3px solid ${c.priceText};"><h1 style="color:#fff;margin:0;text-transform:uppercase;font-size:26px;">${title}</h1></div><div style="padding:20px;"><p style="color:${SHARED.stats};font-size:15px;margin:0;">${desc.replace(/\n/g, '<br>')}</p></div></div>`;
            }
        },

        stripe: {
            label: 'Thin Stripe',
            palette: {
                bgDark:    '#222',
                priceText: '#81c784'
            },
            item(item, rarity, includeNote) {
                const c = this.palette;
                return `
                    <div style="max-width:600px;margin:0 auto 4px auto;padding:8px 12px;background:${c.bgDark};border-left:33px solid ${rarity};display:flex;justify-content:space-between;align-items:center;">
                        <div style="text-align:left;">
                            <span style="font-weight:bold;font-size:13px;"><font color="${rarity}">${item.name}</font></span>
                            <div style="font-weight:bold;margin-top:2px;"><font color="${SHARED.bonus}" style="font-size:10px;">${fmtBonuses(item)}</font></div>
                            ${fmtNote(item, includeNote)}
                        </div>
                        <div style="text-align:right;">
                            <div style="font-weight:bold;font-size:14px;"><font color="${c.priceText}">${fmtPrice(item)}</font></div>
                            <div style="margin-top:2px;"><font color="${SHARED.stats}" style="font-size:10px;">${fmtStats(item)}</font></div>
                        </div>
                    </div>`;
            },
            header(title, desc) {
                const c = this.palette;
                return `<div style="max-width:650px;margin:0 auto 30px auto;padding:20px 30px;background:${c.bgDark};border-left:6px solid ${c.priceText};text-align:left;"><h1 style="color:#fff;margin:0 0 15px 0;font-size:28px;">${title}</h1><p style="color:${SHARED.stats};font-size:16px;margin:0;">${desc.replace(/\n/g, '<br>')}</p></div>`;
            }
        },

        ledger: {
            label: 'Classic Ledger',
            palette: {
                bg:        '#2c241e',
                border:    '#4a3b2f',
                priceText: '#e0ca82',
                statsText: '#c0a88d'
            },
            item(item, rarity, includeNote) {
                const c = this.palette;
                const t = normType(item.type);
                let em = '&#x2756;&#xFE0E;';
                if (t === 'Primary' || t === 'Secondary') em = '&#x2316;&#xFE0E;';
                else if (t === 'Melee') em = '&#x2694;&#xFE0E;';
                else if (t === 'Armor') em = '&#x26E8;&#xFE0E;';
                return `<div style="max-width:580px;margin:0 auto 15px auto;background:${c.bg};border:2px solid ${c.border};border-radius:2px;box-shadow:inset 0 0 40px rgba(0,0,0,.5);">
                    <div style="border-left:10px solid ${rarity};padding:15px;text-align:left;">
                        <div style="display:flex;justify-content:space-between;align-items:baseline;">
                            <span style="font-family:'Courier New',monospace;font-size:18px;font-weight:bold;text-transform:uppercase;"><font color="${rarity}">${item.name}</font></span>
                            <span style="font-family:'Courier New',monospace;font-size:16px;border-bottom:1px double ${rarity};"><font color="${c.priceText}">${fmtPrice(item)}</font></span>
                        </div>
                        <div style="margin-top:8px;font-family:Georgia,serif;font-style:italic;font-size:13px;color:${c.statsText};">${fmtStats(item)}</div>
                        <div style="margin-top:5px;padding-top:5px;border-top:1px dashed ${c.border};"><font color="${SHARED.bonus}" style="font-size:12px;font-weight:bold;letter-spacing:1px;">${em} ${fmtBonuses(item)}</font></div>
                        ${fmtNote(item, includeNote)}
                    </div>
                </div>`;
            },
            header(title, desc) {
                const c = this.palette;
                return `<div style="max-width:620px;margin:0 auto 30px auto;background:${c.bg};border:2px solid ${c.border};padding:30px;box-shadow:inset 0 0 50px rgba(0,0,0,.6);"><h1 style="font-family:'Courier New',monospace;color:${c.priceText};text-transform:uppercase;margin:0 0 20px 0;border-bottom:2px dashed ${c.border};padding-bottom:15px;font-size:26px;">${title}</h1><p style="font-family:Georgia,serif;font-style:italic;color:${c.statsText};font-size:16px;margin:0;">${desc.replace(/\n/g, '<br>')}</p></div>`;
            }
        },

        minimal: {
            label: 'Modern Minimalist',
            palette: {
                lightTxt:    '#1a1a1a',
                darkTxt:     '#ffffff',
                lightSubtle: 'rgba(0,0,0,0.7)',
                darkSubtle:  'rgba(255,255,255,0.9)',
                lightPill:   'rgba(0,0,0,0.12)',
                darkPill:    'rgba(0,0,0,0.25)',
                border:      'rgba(0,0,0,0.05)'
            },
            isLightRarity: c => c === '#ffff00' || c === '#ffffff',
            item(item, rarity, includeNote) {
                const c = this.palette;
                const light = this.isLightRarity(rarity);
                const tc = light ? c.lightTxt : c.darkTxt;
                const sc = light ? c.lightSubtle : c.darkSubtle;
                const pc = light ? c.lightPill : c.darkPill;
                return `<div style="max-width:520px;margin:0 auto 8px auto;background:${rarity};border-radius:12px;overflow:hidden;box-shadow:0 4px 6px rgba(0,0,0,.2);border:1px solid ${c.border};">
                    <div style="padding:12px 20px;display:flex;justify-content:space-between;align-items:center;">
                        <div style="text-align:left;">
                            <div style="font-size:14px;font-weight:800;color:${tc};letter-spacing:-.5px;">${item.name.toUpperCase()}</div>
                            <div style="font-size:10px;color:${sc};font-weight:600;">${fmtStats(item)}</div>
                            ${fmtNote(item, includeNote)}
                        </div>
                        <div style="text-align:right;">
                            <div style="font-size:16px;font-weight:900;color:${tc};">${fmtPrice(item)}</div>
                            <div style="font-size:10px;color:${tc};font-weight:bold;background:${pc};padding:2px 10px;border-radius:20px;display:inline-block;margin-top:4px;">${fmtBonuses(item)}</div>
                        </div>
                    </div>
                </div>`;
            },
            header(title, desc) {
                return `<div style="max-width:580px;margin:0 auto 30px auto;background:#fff;border-radius:16px;padding:35px;box-shadow:0 6px 12px rgba(0,0,0,.15);"><h1 style="font-family:sans-serif;font-weight:900;color:#1a1a1a;letter-spacing:-1px;margin:0 0 15px 0;font-size:28px;">${title}</h1><p style="font-family:sans-serif;color:rgba(0,0,0,.7);font-size:16px;margin:0;">${desc.replace(/\n/g, '<br>')}</p></div>`;
            }
        },

        terminal: {
            label: 'Retro Terminal',
            palette: {
                bg:        '#050505',
                hdr:       '#111',
                hdrBorder: '#333',
                statText:  '#999',
                priceText: '#00ff00'
            },
            item(item, rarity, includeNote) {
                const c = this.palette;
                return `
                    <div style="max-width:600px;margin:0 auto 10px auto;background:${c.bg};border:1px solid ${c.hdrBorder};font-family:'Courier New',monospace;text-align:left;">
                        <div style="background:${c.hdr};padding:4px 8px;border-bottom:1px solid ${c.hdrBorder};display:flex;justify-content:space-between;font-size:12px;">
                            <span style="color:${c.statText};">torn$ ./inspect ${item.UID || '1337'}</span>
                            <span style="color:${rarity};font-weight:bold;">[${normType(item.type).toUpperCase()}]</span>
                        </div>
                        <div style="padding:10px;">
                            <div style="display:flex;justify-content:space-between;margin-bottom:6px;">
                                <span style="color:${rarity};font-size:15px;font-weight:bold;">${item.name}</span>
                                <span style="color:${c.priceText};font-weight:bold;">${fmtPrice(item)}</span>
                            </div>
                            <div style="color:${SHARED.stats};font-size:11px;margin-bottom:4px;"><span style="color:${c.statText};">&gt;_ STATS:</span> ${fmtStats(item)}</div>
                            <div style="color:${SHARED.bonus};font-size:12px;font-weight:bold;"><span style="color:${c.statText};">&gt;_ BUFFS:</span> ${fmtBonuses(item)}</div>
                            ${fmtNote(item, includeNote)}
                        </div>
                    </div>`;
            },
            header(title, desc) {
                const c = this.palette;
                return `<div style="max-width:650px;margin:0 auto 30px auto;background:${c.bg};border:1px solid ${c.hdrBorder};font-family:'Courier New',monospace;text-align:left;"><div style="background:${c.hdr};padding:10px 15px;border-bottom:1px solid ${c.hdrBorder};color:${c.statText};font-size:14px;">root@torn:~/shop# cat motd.txt</div><div style="padding:20px;"><h1 style="color:${c.priceText};font-size:22px;margin:0 0 15px 0;">&gt; ${title}</h1><p style="color:${SHARED.stats};font-size:15px;margin:0;">${desc.replace(/\n/g, '<br>')}</p></div></div>`;
            }
        },

        tactical: {
            label: 'Military Crate',
            palette: {
                bg:        '#252525',
                hdr:       '#1a1a1a',
                hdrText:   '#e8eaec',
                priceText: '#aed581'
            },
            item(item, rarity, includeNote) {
                const c = this.palette;
                return `
                    <div style="max-width:500px;margin:0 auto 10px auto;background:${c.bg};border:1px solid ${c.hdr};border-left:6px solid ${rarity};font-family:Impact,Arial Black,sans-serif;text-transform:uppercase;">
                        <div style="display:flex;justify-content:space-between;align-items:center;background:${c.hdr};padding:8px 12px;">
                            <span style="color:${c.hdrText};font-size:14px;letter-spacing:1px;">${item.name}</span>
                            <span style="color:${c.priceText};font-size:14px;">${fmtPrice(item)}</span>
                        </div>
                        <div style="padding:10px 12px;font-family:Arial,sans-serif;text-transform:none;">
                            <div style="color:${SHARED.stats};font-size:11px;margin-bottom:4px;"><strong>SPEC:</strong> ${fmtStats(item)}</div>
                            <div style="color:${SHARED.bonus};font-size:11px;"><strong>BONUS:</strong> ${fmtBonuses(item)}</div>
                            ${fmtNote(item, includeNote)}
                        </div>
                    </div>`;
            },
            header(title, desc) {
                const c = this.palette;
                return `<div style="max-width:550px;margin:0 auto 30px auto;background:${c.bg};border:3px solid ${c.hdr};"><div style="background:${c.hdr};padding:15px;"><h1 style="font-family:Impact,Arial Black,sans-serif;text-transform:uppercase;color:${c.hdrText};letter-spacing:3px;margin:0;font-size:26px;">${title}</h1></div><div style="padding:20px;"><p style="font-family:Arial,sans-serif;color:${SHARED.stats};font-size:15px;margin:0;">${desc.replace(/\n/g, '<br>')}</p></div></div>`;
            }
        },

        boutique: {
            label: 'Luxury Boutique',
            palette: {
                bg:        '#0a0a0c',
                border:    '#2c2c35',
                priceText: '#e5c158'
            },
            item(item, rarity, includeNote) {
                const c = this.palette;
                return `
                    <div style="max-width:550px;margin:0 auto 12px auto;background:${c.bg};border:1px solid ${c.border};border-radius:3px;padding:12px 16px;">
                        <div style="display:flex;justify-content:space-between;align-items:flex-end;border-bottom:1px solid ${c.border};padding-bottom:6px;margin-bottom:6px;">
                            <span style="font-family:Georgia,serif;font-size:16px;color:${rarity};font-style:italic;">${item.name}</span>
                            <span style="font-family:Tahoma,sans-serif;font-size:15px;color:${c.priceText};font-weight:bold;">${fmtPrice(item)}</span>
                        </div>
                        <div style="display:flex;justify-content:space-between;align-items:center;font-family:Tahoma,sans-serif;">
                            <span style="font-size:10px;color:${SHARED.stats};text-transform:uppercase;letter-spacing:.5px;">${fmtStats(item)}</span>
                            <span style="font-size:11px;color:${SHARED.bonus};font-weight:bold;">${fmtBonuses(item)}</span>
                        </div>
                        ${fmtNote(item, includeNote)}
                    </div>`;
            },
            header(title, desc) {
                const c = this.palette;
                return `<div style="max-width:600px;margin:0 auto 30px auto;background:${c.bg};border:1px solid ${c.border};border-radius:4px;padding:35px;"><h1 style="font-family:Georgia,serif;font-style:italic;color:${c.priceText};font-size:32px;margin:0 0 20px 0;">${title}</h1><div style="width:70px;height:2px;background:${c.priceText};margin:0 auto 20px auto;"></div><p style="font-family:Tahoma,sans-serif;color:${SHARED.stats};font-size:15px;line-height:1.8;margin:0;">${desc.replace(/\n/g, '<br>')}</p></div>`;
            }
        },

        glass: {
            label: 'Frosted Glass',
            palette: {
                bg:        'rgba(30,30,40,0.6)',
                border:    'rgba(255,255,255,0.1)',
                priceText: '#81d4fa'
            },
            item(item, rarity, includeNote) {
                const c = this.palette;
                return `
                    <div style="max-width:500px;margin:0 auto 10px auto;background:${c.bg};border:1px solid ${c.border};border-radius:16px;padding:12px 16px;-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px);box-shadow:0 4px 15px rgba(0,0,0,.3);">
                        <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px;">
                            <span style="font-family:sans-serif;font-size:15px;font-weight:600;color:${rarity};">${item.name}</span>
                            <span style="background:rgba(0,0,0,.4);padding:4px 8px;border-radius:12px;font-size:13px;font-weight:bold;color:${c.priceText};">${fmtPrice(item)}</span>
                        </div>
                        <div style="display:flex;flex-direction:column;gap:4px;font-family:sans-serif;">
                            <span style="font-size:11px;color:${SHARED.stats};">${fmtStats(item)}</span>
                            <span style="font-size:11px;color:${SHARED.bonus};font-weight:bold;">${fmtBonuses(item)}</span>
                        </div>
                        ${fmtNote(item, includeNote)}
                    </div>`;
            },
            header(title, desc) {
                const c = this.palette;
                return `<div style="max-width:550px;margin:0 auto 30px auto;background:${c.bg};border:1px solid ${c.border};border-radius:20px;padding:35px;-webkit-backdrop-filter:blur(12px);backdrop-filter:blur(12px);box-shadow:0 8px 25px rgba(0,0,0,.4);"><h1 style="font-family:sans-serif;font-weight:600;color:#fff;margin:0 0 15px 0;font-size:28px;">${title}</h1><p style="font-family:sans-serif;color:#f0f0f0;font-size:16px;margin:0;">${desc.replace(/\n/g, '<br>')}</p></div>`;
            }
        }
    };

    let items = [];
    let abortLoad = false;
    let _filteredUIDs = [];

    const getVault   = () => GM_getValue('rwts_vault', {});
    const saveVault  = v => GM_setValue('rwts_vault', v);
    const setItemStatus = (uid, status) => {
        const v = getVault();
        if (v[uid]) {
            v[uid].status = normStatus(status);
            saveVault(v);
        }
    };
    const removeFromVault = uid => {
        const v = getVault();
        delete v[uid];
        saveVault(v);
    };
    const loadItemsFromVault = () => {
        const v = getVault();
        items = Object.values(v).map(i => ({
            ...i,
            status: normStatus(i.status),
            inBazaar: !!i.inBazaar
        }));
    };

    (function migrateOldCacheToVault() {
        if (GM_getValue('rwts_migrated_v41', false)) return;
        const oldCache = GM_getValue('rwts_cache', {});
        const vault = getVault();
        let migrated = 0;
        Object.entries(oldCache).forEach(([uid, entry]) => {
            if (vault[uid]) return;
            const d = entry?.d;
            if (!d || !hasBonuses(d)) return;
            vault[uid] = {
                ...d,
                UID: uid,
                bazaar_price: null,
                inBazaar: false,
                status: 'active',
                addedAt: entry.ts || Date.now(),
                lastSeen: entry.ts || Date.now()
            };
            migrated++;
        });
        if (migrated > 0) saveVault(vault);
        GM_setValue('rwts_migrated_v41', true);
    })();

    const styleEl = document.createElement('style');
    styleEl.textContent = `
        #rwts{position:fixed;width:min(450px,calc(100vw - 24px));background:${UI.panelBg};color:${UI.textMain};border:1px solid #333;z-index:999999;font-family:Arial,sans-serif;font-size:12px;border-radius:6px;box-shadow:0 6px 40px rgba(0,0,0,.9);box-sizing:border-box}
        #rwts-hdr{background:${UI.headerBg};padding:10px 14px;cursor:move;border-bottom:1px solid ${UI.border};display:flex;justify-content:space-between;align-items:center;border-radius:6px 6px 0 0;user-select:none}
        #rwts-hdr-title{font-weight:bold;font-size:13px;color:${UI.accent};letter-spacing:.5px}
        #rwts-tabs{display:flex;background:${UI.tabBarBg};border-bottom:1px solid ${UI.border}}
        .rt{flex:1;padding:7px 4px;text-align:center;cursor:pointer;font-size:11px;color:${UI.textDim};border-bottom:2px solid transparent;transition:all .18s}
        .rt:hover{color:${UI.textMain};background:#222}
        .rt.on{color:${UI.accent};border-bottom-color:${UI.accent};background:${UI.panelBg}}
        #rwts-body{padding:12px;max-height:${PANEL_MAX_H}px;overflow-y:auto;scrollbar-width:thin;scrollbar-color:${UI.border} #111}
        .rp{display:none}.rp.on{display:block}
        .rrow{display:flex;gap:6px;margin-bottom:8px;align-items:center}
        .ri{background:${UI.inputBg};color:${UI.textMain};border:1px solid ${UI.border};padding:5px 8px;border-radius:3px;font-size:12px;box-sizing:border-box}
        .ri:focus{outline:none;border-color:${UI.accent}}
        .ri::placeholder{color:#777}
        .rb{background:#252525;color:${UI.textMain};border:1px solid ${UI.border};padding:6px 10px;cursor:pointer;border-radius:3px;font-size:12px;font-weight:bold;white-space:nowrap;transition:background .15s}
        .rb:hover{background:#303030}
        .rb:disabled{opacity:.35;cursor:default}
        .rb.gold{background:${UI.accentBg};border-color:${UI.accent};color:${UI.accent}}
        .rb.gold:hover{background:${UI.accentBgHi}}
        .rb.grn{background:${UI.successBg};border-color:${UI.success};color:${UI.success}}
        .rb.grn:hover{background:#122512}
        .rb.red{background:${UI.dangerBg};border-color:${UI.danger};color:${UI.danger}}
        .rb.red:hover{background:#301212}
        .rb.sold{background:${UI.soldBg};border-color:${UI.soldUI};color:${UI.soldUI}}
        .rb.sold:hover{background:#3a2a00}
        .rb.full{width:100%}
        .rb-x{background:#2a1010;color:${UI.danger};border:1px solid #4a1818;padding:5px 9px;cursor:pointer;border-radius:3px;font-size:11px;font-weight:bold;line-height:1;flex-shrink:0}
        .rb-x:hover{background:#3a1818}
        .ic{background:${UI.cardBg};border:1px solid ${UI.borderSoft};border-radius:4px;padding:10px;margin-bottom:7px;transition:border-color .15s;border-left:3px solid ${UI.borderSoft}}
        .ic:hover{border-color:#444}
        .ic.st-active{border-left-color:${STATUS_COLORS.active}}
        .ic.st-sold{border-left-color:${STATUS_COLORS.sold}}
        .ic.st-hidden{border-left-color:${STATUS_COLORS.hidden};opacity:.7}
        .ic-name{font-weight:bold;font-size:13px}
        .ic-stats{color:${UI.textDim};font-size:11px;margin-top:2px}
        .ic-bon{color:${UI.bonusUI};font-size:11px;margin-top:2px;font-weight:600}
        .ic-tag{color:${UI.textFaint};font-size:10px;text-transform:uppercase;flex-shrink:0;font-weight:600}
        .ic-baz{color:${UI.textFaint};font-size:10px;flex-shrink:0}
        .baz-yes{color:${UI.success};font-size:9px;font-weight:600;white-space:nowrap}
        .baz-no{color:#aaa;font-size:9px;font-weight:600;white-space:nowrap}
        .item-count{color:${UI.textFaint};font-size:10px;text-align:right;margin-bottom:6px}
        .pbw{background:#080808;border:1px solid ${UI.borderSoft};border-radius:3px;height:6px;margin:6px 0;overflow:hidden}
        .pb{height:100%;background:linear-gradient(90deg,#7a5c00,${UI.accent});border-radius:3px;transition:width .3s}
        .stxt{color:${UI.textDim};font-size:11px;text-align:center;padding:3px 0}
        .lbl{font-size:10px;color:${UI.textFaint};text-transform:uppercase;letter-spacing:.5px;margin-bottom:4px;font-weight:600}
        .sect-lbl{font-size:11px;color:${UI.accent};font-weight:bold;letter-spacing:.5px;margin:10px 0 5px;padding-bottom:3px;border-bottom:1px solid ${UI.borderSoft}}
        .dimmed{opacity:.4;pointer-events:none}
        .div{border:none;border-top:1px solid #222;margin:12px 0}
        .tw{display:flex;align-items:center;gap:8px;margin-bottom:10px}
        .tog{position:relative;width:30px;height:16px;cursor:pointer;flex-shrink:0}
        .tog input{opacity:0;width:0;height:0}
        .tog-sl{position:absolute;inset:0;background:#222;border-radius:16px;transition:.2s}
        .tog-sl:before{content:'';position:absolute;width:12px;height:12px;left:2px;top:2px;background:#777;border-radius:50%;transition:.2s}
        input:checked+.tog-sl{background:${UI.accentBg}}
        input:checked+.tog-sl:before{transform:translateX(14px);background:${UI.accent}}
        .note-i{background:#0a0a0a;color:${UI.textDim};border:1px dashed ${UI.borderSoft};padding:4px 6px;width:100%;box-sizing:border-box;border-radius:2px;font-style:italic;font-size:11px;margin-top:5px}
        .note-i:focus{outline:none;border-color:#3a3a3a;color:${UI.textMain}}
        .about{color:${UI.textDim};font-size:11px;line-height:1.9}
        #btn-rpos{font-size:10px;color:${UI.textFaint};cursor:pointer}
        #btn-rpos:hover{color:${UI.textMain}}
        #btn-min{cursor:pointer;font-size:13px;color:${UI.textDim}}
        #prev-ov{display:none;position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,.92);z-index:9999999}
        #prev-box{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);width:90%;max-width:840px;height:84vh;background:#111;border:1px solid #444;border-radius:8px;overflow:hidden;display:flex;flex-direction:column}
        #prev-bar{background:${UI.headerBg};padding:8px 14px;display:flex;justify-content:space-between;align-items:center;border-bottom:1px solid ${UI.border};flex-shrink:0}
        #prev-frame{flex:1;border:none;background:#fff}
        textarea.ri{resize:vertical;font-family:inherit}
        select.ri{cursor:pointer}
        .bulk-bar{background:#0a0a0a;border:1px solid #2a2a2a;border-radius:3px;padding:5px 6px;display:flex;gap:5px;align-items:center;margin-bottom:8px}
        .bulk-bar .lbl{margin:0;flex-shrink:0}
        .bulk-bar .rb{padding:4px 7px;font-size:10.5px;flex:1}
        .vault-stats{background:#0d0d0d;border:1px solid ${UI.borderSoft};border-radius:3px;padding:8px 10px;margin-top:6px;font-size:11px;line-height:1.7}
        .status-pill{display:inline-block;padding:1px 6px;border-radius:8px;font-size:9px;font-weight:600;text-transform:uppercase;letter-spacing:.5px}
        .sf{flex-shrink:0}
    `;
    document.head.appendChild(styleEl);

    const designOptions = Object.entries(THEMES)
        .map(([key, t]) => `<option value="${key}">${t.label}</option>`)
        .join('');

    const wrap = document.createElement('div');
    wrap.id = 'rwts';

    const clampPos = (top, left, right) => {
        const W = window.innerWidth;
        const H = window.innerHeight;
        const PW = Math.min(450, W - 24);
        if (right !== 'auto') {
            const clampedRight = Math.max(0, Math.min(parseFloat(right) || 20, W - PW - 4));
            return { top: Math.max(0, Math.min(parseFloat(top) || 50, H - 60)) + 'px', right: clampedRight + 'px', left: 'auto' };
        }
        const clampedLeft = Math.max(0, Math.min(parseFloat(left) || 0, W - PW - 4));
        return { top: Math.max(0, Math.min(parseFloat(top) || 50, H - 60)) + 'px', left: clampedLeft + 'px', right: 'auto' };
    };

    const rawPos = GM_getValue('rwts_pos', { top: '50px', right: '20px', left: 'auto' });
    const pos = clampPos(rawPos.top, rawPos.left, rawPos.right);
    wrap.style.top = pos.top;
    if (pos.left === 'auto') {
        wrap.style.right = pos.right;
        wrap.style.left = 'auto';
    } else {
        wrap.style.left = pos.left;
        wrap.style.right = 'auto';
    }

    const minimized = GM_getValue('rwts_min', false);

    wrap.innerHTML = `
        <div id="rwts-hdr">
            <span id="rwts-hdr-title">&#x2694; RW ThreadSmith v${VERSION}</span>
            <div style="display:flex;gap:12px;align-items:center">
                <span id="btn-rpos">Reset</span>
                <span id="btn-min">${minimized ? '&#x25B2;' : '&#x25BC;'}</span>
            </div>
        </div>
        <div id="rwts-col" style="display:${minimized ? 'none' : 'block'}">
            <div id="rwts-tabs">
                <div class="rt on" data-tab="load">&#x1F4E6; Sync</div>
                <div class="rt" data-tab="items">&#x1F5C4; Vault</div>
                <div class="rt" data-tab="gen">&#x1F5A8; Output</div>
                <div class="rt" data-tab="cfg">&#x2699; Config</div>
            </div>
            <div id="rwts-body">

                <div id="tab-load" class="rp on">
                    <div class="lbl">Torn API Key</div>
                    <div class="rrow">
                        <input type="password" id="api-key" class="ri" placeholder="Enter API key..." style="flex:1">
                        <button class="rb gold" id="btn-load">Sync</button>
                    </div>
                    <div id="prog-area" style="display:none">
                        <div class="pbw"><div class="pb" id="pb" style="width:0%"></div></div>
                        <div class="stxt" id="stxt">Starting...</div>
                        <button class="rb red full" id="btn-cancel" style="margin-top:4px">&#x2715; Cancel</button>
                    </div>
                    <div id="load-msg" class="stxt" style="margin-top:6px"></div>

                    <div class="lbl" style="margin-top:10px">Vault Status</div>
                    <div id="vault-stats" class="vault-stats"></div>

                    <hr class="div">
                    <div class="lbl">Backup &amp; Restore</div>
                    <div class="rrow">
                        <button class="rb" id="btn-export" style="flex:1">&#x2B07; Export JSON</button>
                        <button class="rb" id="btn-import" style="flex:1">&#x2B06; Import JSON</button>
                        <input type="file" id="import-file" accept=".json" style="display:none">
                    </div>
                    <div class="rrow">
                        <button class="rb red" id="btn-clr-prices" style="flex:1">Clear Prices/Notes</button>
                        <button class="rb red" id="btn-clr-vault" style="flex:1">Clear Vault</button>
                    </div>
                    <div class="stxt" style="text-align:left;color:${UI.textDim};margin-top:6px;line-height:1.6">
                        <strong style="color:${UI.textMain}">Vault</strong>: Persistent storage of every item you have ever synced.
                        Items remain after you remove them from your bazaar. Toggle their status (Active/Sold/Hidden)
                        in the Vault tab to control which appear in generated threads.
                    </div>
                </div>

                <div id="tab-items" class="rp">
                    <div class="rrow">
                        <input type="text" id="item-search" class="ri" placeholder="Search items..." style="flex:1">
                        <select id="item-sort" class="ri" style="width:120px">
                            <option value="name-asc">Name A-Z</option>
                            <option value="name-desc">Name Z-A</option>
                            <option value="rarity">Rarity</option>
                            <option value="type">Type</option>
                            <option value="status">Status</option>
                        </select>
                    </div>
                    <div class="rrow">
                        <select id="f-bonus" class="ri" style="flex:1"><option value="">All Bonuses</option></select>
                        <select id="f-rarity" class="ri" style="flex:1">
                            <option value="">All Rarities</option>
                            <option value="Yellow">Yellow</option>
                            <option value="Orange">Orange</option>
                            <option value="Red">Red</option>
                            <option value="White">White</option>
                        </select>
                        <select id="f-status" class="ri" style="flex:1">
                            <option value="">All Status</option>
                            <option value="active">Active</option>
                            <option value="sold">Sold</option>
                            <option value="hidden">Hidden</option>
                        </select>
                    </div>
                    <div class="bulk-bar" title="Bulk operations apply to currently filtered items">
                        <span class="lbl">Bulk:</span>
                        <button class="rb grn"  id="btn-bulk-active" title="Set filtered to Active">Active</button>
                        <button class="rb sold" id="btn-bulk-sold"   title="Set filtered to Sold">Sold</button>
                        <button class="rb"      id="btn-bulk-hidden" title="Set filtered to Hidden">Hidden</button>
                        <button class="rb-x"    id="btn-bulk-remove" title="Remove filtered items from vault">&#x2715;</button>
                    </div>
                    <div id="item-list"><div class="stxt">Vault is empty. Sync your bazaar to populate it.</div></div>
                </div>

                <div id="tab-gen" class="rp">
                    <div class="lbl">Thread Header</div>
                    <input type="text" id="shop-title" class="ri" placeholder="Thread title..." style="width:100%;margin-bottom:6px">
                    <textarea id="shop-desc" class="ri" placeholder="Intro / description text..." style="width:100%;height:55px"></textarea>
                    <hr class="div">
                    <div class="lbl">Layout &amp; Sorting</div>
                    <div class="rrow">
                        <span class="lbl sf" style="margin:0;width:78px">Show only</span>
                        <select id="f-cat" class="ri" style="flex:1">
                            <option value="all">All Categories</option>
                            <option value="Primary">Primary</option>
                            <option value="Secondary">Secondary</option>
                            <option value="Melee">Melee</option>
                            <option value="Armor">Armor</option>
                            <option value="Other">Other</option>
                        </select>
                    </div>

                    <div class="sect-lbl">Weapons</div>
                    <div class="rrow">
                        <span class="lbl sf" style="margin:0;width:78px">Group by</span>
                        <select id="w-group" class="ri" style="flex:1">${optsFor(GROUPS, 'weapon')}</select>
                    </div>
                    <div class="rrow">
                        <span class="lbl sf" style="margin:0;width:78px">Sort by</span>
                        <select id="w-sort" class="ri" style="flex:1">${optsFor(SORTS, 'weapon')}</select>
                    </div>
                    <div class="rrow">
                        <span class="lbl sf" style="margin:0;width:78px">then by</span>
                        <select id="w-sort2" class="ri" style="flex:1"><option value="">None (name tiebreak)</option>${optsFor(SORTS, 'weapon')}</select>
                    </div>

                    <div class="tw" style="margin:10px 0 2px"><label class="tog"><input type="checkbox" id="tog-sep-armor" checked><span class="tog-sl"></span></label><span>Separate armor into its own section (bottom)</span></div>
                    <div id="armor-block">
                        <div class="sect-lbl">Armor</div>
                        <div class="rrow">
                            <span class="lbl sf" style="margin:0;width:78px">Group by</span>
                            <select id="a-group" class="ri" style="flex:1">${optsFor(GROUPS, 'armor')}</select>
                        </div>
                        <div class="rrow">
                            <span class="lbl sf" style="margin:0;width:78px">Sort by</span>
                            <select id="a-sort" class="ri" style="flex:1">${optsFor(SORTS, 'armor')}</select>
                        </div>
                        <div class="rrow">
                            <span class="lbl sf" style="margin:0;width:78px">then by</span>
                            <select id="a-sort2" class="ri" style="flex:1"><option value="">None (name tiebreak)</option>${optsFor(SORTS, 'armor')}</select>
                        </div>
                        <div class="rrow">
                            <span class="lbl sf" style="margin:0;width:78px">Heading</span>
                            <input type="text" id="armor-title" class="ri" placeholder="Armor" style="flex:1">
                        </div>
                        <div class="rrow">
                            <span class="lbl sf" style="margin:0;width:78px">Theme</span>
                            <select id="armor-theme" class="ri" style="flex:1"><option value="">Same as weapons</option>${designOptions}</select>
                        </div>
                    </div>

                    <div class="tw" style="margin:10px 0 2px"><label class="tog"><input type="checkbox" id="tog-sep-double"><span class="tog-sl"></span></label><span>Separate double-bonus gear (own block)</span></div>
                    <div class="tw" style="margin:6px 0 4px"><label class="tog"><input type="checkbox" id="tog-include-sold" checked><span class="tog-sl"></span></label><span>Include SOLD items in output</span></div>
                    <div class="lbl" style="margin-top:6px">Design Theme</div>
                    <select id="design-mode" class="ri" style="width:100%;margin-bottom:8px">${designOptions}</select>
                    <div class="rrow">
                        <button class="rb grn" id="btn-preview" style="flex:1">&#x1F441; Preview</button>
                        <button class="rb gold" id="btn-gen" style="flex:2">&#x26A1; Generate &amp; Copy</button>
                    </div>
                    <div id="gen-summary" class="stxt" style="text-align:left;color:${UI.textDim};margin-top:6px"></div>
                    <textarea id="out-box" class="ri" style="width:100%;height:90px;margin-top:8px;font-family:monospace;font-size:10px;display:none" readonly></textarea>
                </div>

                <div id="tab-cfg" class="rp">
                    <div class="lbl">Sync Source</div>
                    <div class="tw"><label class="tog"><input type="checkbox" id="tog-use-itemmarket"><span class="tog-sl"></span></label><span>Scan Item Market instead of Bazaar</span></div>
                    <div style="color:${UI.textDim};font-size:10px;margin-bottom:8px;line-height:1.5">When enabled, the Sync tab fetches your Item Market listings (requires a Limited API key) rather than your Bazaar.</div>
                    <hr class="div">
                    <div class="lbl">Item Card Display</div>
                    <div class="tw"><label class="tog"><input type="checkbox" id="tog-notes"><span class="tog-sl"></span></label><span>Show per-item notes field</span></div>
                    <div class="tw"><label class="tog"><input type="checkbox" id="tog-compact"><span class="tog-sl"></span></label><span>Compact item cards</span></div>
                    <hr class="div">
                    <div class="lbl">Generated HTML Options</div>
                    <div class="tw"><label class="tog"><input type="checkbox" id="tog-notes-html"><span class="tog-sl"></span></label><span>Include item notes in output</span></div>
                    <div class="tw"><label class="tog"><input type="checkbox" id="tog-counts" checked><span class="tog-sl"></span></label><span>Show item count in section headers</span></div>
                    <hr class="div">
                    <div class="lbl">Help &amp; Documentation</div>
                    <button class="rb gold full" id="btn-guide">&#x1F4D6; Open Full Guide (GitHub)</button>
                    <div style="color:${UI.textDim};font-size:10px;margin-top:5px;line-height:1.5">
                        Installation, quickstart, theme gallery, FAQ, and full feature reference.
                    </div>
                    <hr class="div">
                    <div class="lbl">About</div>
                    <div class="about">
                        RW ThreadSmith v${VERSION} &middot; GPL-3.0 License<br>
                        by Rowage [3926289]<br>
                        API cadence approx 85 req/min<br>
                        ${Object.keys(THEMES).length} designs &middot; Two-level sorting &middot; Vault storage &middot; Bulk actions &middot; Live preview &middot; Export/Import
                    </div>
                </div>

            </div>
        </div>
    `;
    document.body.appendChild(wrap);

    const prevOverlay = document.createElement('div');
    prevOverlay.id = 'prev-ov';
    prevOverlay.innerHTML = `
        <div id="prev-box">
            <div id="prev-bar">
                <span style="color:${UI.accent};font-weight:bold;font-size:13px">Thread Preview</span>
                <div style="display:flex;gap:8px">
                    <button class="rb" id="btn-prev-copy" style="font-size:11px">Copy HTML</button>
                    <button class="rb" id="btn-prev-close">&#x2715; Close</button>
                </div>
            </div>
            <iframe id="prev-frame" sandbox="allow-same-origin"></iframe>
        </div>
    `;
    document.body.appendChild(prevOverlay);

    $('btn-prev-close').onclick = () => { prevOverlay.style.display = 'none'; };
    $('btn-prev-copy').onclick = () => {
        GM_setClipboard($('out-box').value);
        alert('Copied!');
    };

    $('btn-guide').onclick = () => {
        window.open('https://github.com/Rowage3/rw-threadsmith#readme', '_blank', 'noopener');
    };

    $('api-key').value      = GM_getValue('rwts_api', '');
    $('shop-title').value   = GM_getValue('rwts_title', '');
    $('shop-desc').value    = GM_getValue('rwts_desc', '');
    $('design-mode').value  = GM_getValue('rwts_design', 'glow');
    const legacyGroup = GM_getValue('rwts_sort', 'type') === 'rarity' ? 'rarity' : 'type';
    $('w-group').value      = GM_getValue('rwts_w_group', legacyGroup);
    $('w-sort').value       = GM_getValue('rwts_w_sort', 'value-desc');
    $('w-sort2').value      = GM_getValue('rwts_w_sort2', '');
    $('a-group').value      = GM_getValue('rwts_a_group', 'none');
    $('a-sort').value       = GM_getValue('rwts_a_sort', 'value-desc');
    $('a-sort2').value      = GM_getValue('rwts_a_sort2', '');
    $('armor-title').value  = GM_getValue('rwts_armor_title', 'Armor');
    $('armor-theme').value  = GM_getValue('rwts_armor_theme', '');
    $('tog-sep-armor').checked  = GM_getValue('rwts_sep_armor', true);
    $('tog-sep-double').checked = GM_getValue('rwts_sep_double', false);
    $('f-cat').value        = GM_getValue('rwts_fcat', 'all');
    $('tog-notes').checked        = GM_getValue('rwts_tog_notes', false);
    $('tog-compact').checked      = GM_getValue('rwts_tog_compact', false);
    $('tog-notes-html').checked   = GM_getValue('rwts_tog_notes_html', false);
    $('tog-counts').checked       = GM_getValue('rwts_tog_counts', true);
    $('tog-include-sold').checked = GM_getValue('rwts_tog_include_sold', true);
    $('tog-use-itemmarket').checked = GM_getValue('rwts_tog_itemmarket', false);

    $('shop-title').oninput   = e => GM_setValue('rwts_title',  e.target.value);
    $('shop-desc').oninput    = e => GM_setValue('rwts_desc',   e.target.value);
    $('design-mode').onchange = e => GM_setValue('rwts_design', e.target.value);
    const refreshSecondary = (primaryId, secondaryId, storeKey) => {
        const pf = fieldOf($(primaryId).value);
        const sec = $(secondaryId);
        [...sec.options].forEach(o => { o.disabled = !!o.value && fieldOf(o.value) === pf; });
        if (sec.selectedOptions[0] && sec.selectedOptions[0].disabled) {
            sec.value = '';
            GM_setValue(storeKey, '');
        }
    };
    $('w-group').onchange   = e => GM_setValue('rwts_w_group', e.target.value);
    $('w-sort').onchange    = e => { GM_setValue('rwts_w_sort',  e.target.value); refreshSecondary('w-sort', 'w-sort2', 'rwts_w_sort2'); };
    $('w-sort2').onchange   = e => GM_setValue('rwts_w_sort2', e.target.value);
    $('a-group').onchange   = e => GM_setValue('rwts_a_group', e.target.value);
    $('a-sort').onchange    = e => { GM_setValue('rwts_a_sort',  e.target.value); refreshSecondary('a-sort', 'a-sort2', 'rwts_a_sort2'); };
    $('a-sort2').onchange   = e => GM_setValue('rwts_a_sort2', e.target.value);
    refreshSecondary('w-sort', 'w-sort2', 'rwts_w_sort2');
    refreshSecondary('a-sort', 'a-sort2', 'rwts_a_sort2');
    $('armor-title').oninput = e => GM_setValue('rwts_armor_title', e.target.value);
    $('armor-theme').onchange = e => GM_setValue('rwts_armor_theme', e.target.value);
    const syncArmorBlock = () => $('armor-block').classList.toggle('dimmed', !$('tog-sep-armor').checked);
    $('tog-sep-armor').onchange  = e => { GM_setValue('rwts_sep_armor',  e.target.checked); syncArmorBlock(); };
    $('tog-sep-double').onchange = e => GM_setValue('rwts_sep_double', e.target.checked);
    syncArmorBlock();
    $('f-cat').onchange       = e => GM_setValue('rwts_fcat',   e.target.value);
    $('tog-notes').onchange        = e => { GM_setValue('rwts_tog_notes',     e.target.checked); renderItems(); };
    $('tog-compact').onchange      = e => { GM_setValue('rwts_tog_compact',   e.target.checked); renderItems(); };
    $('tog-notes-html').onchange   = e => GM_setValue('rwts_tog_notes_html', e.target.checked);
    $('tog-counts').onchange       = e => GM_setValue('rwts_tog_counts',     e.target.checked);
    $('tog-include-sold').onchange = e => GM_setValue('rwts_tog_include_sold', e.target.checked);
    $('tog-use-itemmarket').onchange = e => GM_setValue('rwts_tog_itemmarket', e.target.checked);

    document.querySelectorAll('.rt').forEach(tab => {
        tab.onclick = () => {
            document.querySelectorAll('.rt').forEach(t => t.classList.remove('on'));
            document.querySelectorAll('.rp').forEach(p => p.classList.remove('on'));
            tab.classList.add('on');
            $(`tab-${tab.dataset.tab}`).classList.add('on');
        };
    });

    $('btn-min').onclick = () => {
        const col = $('rwts-col');
        const willMinimize = col.style.display !== 'none';
        col.style.display = willMinimize ? 'none' : 'block';
        $('btn-min').innerHTML = willMinimize ? '&#x25B2;' : '&#x25BC;';
        GM_setValue('rwts_min', willMinimize);
    };

    $('btn-rpos').onclick = () => {
        wrap.style.left  = 'auto';
        wrap.style.right = '20px';
        wrap.style.top   = '50px';
        GM_setValue('rwts_pos', { top: '50px', right: '20px', left: 'auto' });
    };

    {
        let dragging = false, ox = 0, oy = 0;

        const onMouseMove = e => {
            if (!dragging) return;
            const W = window.innerWidth;
            const H = window.innerHeight;
            const PW = wrap.offsetWidth;
            const PH = wrap.offsetHeight;
            const newLeft = Math.max(0, Math.min(e.clientX - ox, W - PW));
            const newTop  = Math.max(0, Math.min(e.clientY - oy, H - PH));
            wrap.style.left = newLeft + 'px';
            wrap.style.top  = newTop + 'px';
        };

        const onMouseUp = () => {
            if (dragging) {
                GM_setValue('rwts_pos', { top: wrap.style.top, left: wrap.style.left, right: 'auto' });
            }
            dragging = false;
        };

        $('rwts-hdr').onmousedown = e => {
            if (['btn-rpos', 'btn-min'].includes(e.target.id)) return;
            dragging = true;
            const r = wrap.getBoundingClientRect();
            if (wrap.style.right !== 'auto') {
                wrap.style.left = r.left + 'px';
                wrap.style.right = 'auto';
            }
            ox = e.clientX - r.left;
            oy = e.clientY - r.top;
        };

        if (!window.__rwts_listeners_attached) {
            window.__rwts_listeners_attached = true;
            document.addEventListener('mousemove', onMouseMove);
            document.addEventListener('mouseup', onMouseUp);
        }
    }

    const normItemMarketBonuses = bonusArr => {
        if (!Array.isArray(bonusArr) || !bonusArr.length) return {};
        const out = {};
        bonusArr.forEach((b, i) => {
            out[i] = { bonus: b.title, value: b.value };
        });
        return out;
    };

    const hasBonusesItemMarket = bonusArr => Array.isArray(bonusArr) && bonusArr.length > 0;

    const fetchAllItemMarketListings = async key => {
        const allListings = [];
        let offset = 0;
        while (true) {
            if (abortLoad) break;
            $('stxt').textContent = `Fetching item market page (offset ${offset})...`;
            const page = await api(`https://api.torn.com/v2/user/itemmarket?offset=${offset}&key=${key}`);
            const listings = page.itemmarket || [];
            if (!listings.length) break;
            allListings.push(...listings);
            if (!page._metadata?.links?.next) break;
            offset += listings.length;
            await sleep(API_THROTTLE_MS);
        }
        return allListings;
    };

    $('btn-load').onclick = async () => {
        const key = $('api-key').value.trim();
        if (!key) return alert('API Key required');
        GM_setValue('rwts_api', key);
        abortLoad = false;
        $('prog-area').style.display = 'block';
        $('load-msg').textContent = '';
        $('btn-load').disabled = true;

        const useItemMarket = $('tog-use-itemmarket').checked;

        try {
            const vault = getVault();
            const now = Date.now();
            let newCount = 0, refreshedCount = 0, skippedCount = 0;

            Object.values(vault).forEach(v => v.inBazaar = false);

            if (useItemMarket) {
                const listings = await fetchAllItemMarketListings(key);
                const total = listings.length;

                for (let i = 0; i < total; i++) {
                    if (abortLoad) break;
                    const listing = listings[i];
                    const it = listing.item;
                    if (!it || !it.uid) { skippedCount++; continue; }

                    const uid = String(it.uid);
                    $('pb').style.width = `${Math.round((i / total) * 100)}%`;
                    $('stxt').textContent = `Processing ${i + 1} / ${total} - ${it.name || 'item'}...`;

                    if (!hasBonusesItemMarket(it.bonuses)) { skippedCount++; continue; }

                    if (vault[uid]) {
                        vault[uid].bazaar_price = listing.price;
                        vault[uid].inBazaar    = true;
                        vault[uid].lastSeen    = now;
                        if (it.name) vault[uid].name = it.name;
                        refreshedCount++;
                    } else {
                        const stats = it.stats || {};
                        vault[uid] = {
                            UID:          uid,
                            name:         it.name,
                            type:         it.type,
                            rarity:       it.rarity ? (it.rarity.charAt(0).toUpperCase() + it.rarity.slice(1)) : 'White',
                            damage:       stats.damage ?? null,
                            accuracy:     stats.accuracy ?? null,
                            armor:        stats.armor ?? null,
                            quality:      stats.quality ?? null,
                            bonuses:      normItemMarketBonuses(it.bonuses),
                            bazaar_price: listing.price,
                            inBazaar:     true,
                            status:       'active',
                            addedAt:      now,
                            lastSeen:     now
                        };
                        newCount++;
                    }
                }
            } else {
                const baz = await api(`https://api.torn.com/user/?selections=bazaar&key=${key}`);
                const rawArr = baz.bazaar || [];
                const bazItems = Array.isArray(rawArr) ? rawArr.filter(i => i.UID) : [];
                const total = bazItems.length;

                for (let i = 0; i < total; i++) {
                    if (abortLoad) break;
                    const it = bazItems[i];
                    $('pb').style.width = `${Math.round((i / total) * 100)}%`;
                    $('stxt').textContent = `Scanning ${i + 1} / ${total} - ${it.name || 'item'}...`;

                    if (vault[it.UID]) {
                        vault[it.UID].bazaar_price = it.price;
                        vault[it.UID].inBazaar    = true;
                        vault[it.UID].lastSeen    = now;
                        if (it.name) vault[it.UID].name = it.name;
                        refreshedCount++;
                        continue;
                    }

                    try {
                        const r = await api(`https://api.torn.com/torn/${it.UID}?selections=itemdetails&key=${key}`);
                        if (hasBonuses(r?.itemdetails)) {
                            vault[it.UID] = {
                                ...r.itemdetails,
                                UID: it.UID,
                                bazaar_price: it.price,
                                inBazaar: true,
                                status: 'active',
                                addedAt: now,
                                lastSeen: now
                            };
                            newCount++;
                        } else {
                            skippedCount++;
                        }
                    } catch {
                        skippedCount++;
                    }
                    await sleep(API_THROTTLE_MS);
                }
            }

            saveVault(vault);
            loadItemsFromVault();

            $('pb').style.width = '100%';
            const totalVault = Object.keys(vault).length;
            $('stxt').textContent = abortLoad
                ? `Cancelled. Vault: ${totalVault}.`
                : `${newCount} new, ${refreshedCount} refreshed${skippedCount ? `, ${skippedCount} skipped` : ''}, vault total: ${totalVault}`;

            buildBonusFilter();
            renderItems();
            updateVaultStats();
            if (newCount > 0 || refreshedCount > 0) {
                document.querySelector('[data-tab="items"]').click();
            }
        } catch (e) {
            $('stxt').textContent = `Error: ${e.message}`;
        }

        $('btn-load').disabled = false;
        setTimeout(() => {
            $('prog-area').style.display = 'none';
            const totalVault = Object.keys(getVault()).length;
            $('load-msg').textContent = totalVault > 0
                ? `Vault: ${totalVault} item${totalVault !== 1 ? 's' : ''}`
                : 'Vault is empty.';
        }, 1800);
    };

    $('btn-cancel').onclick = () => { abortLoad = true; };

    function buildBonusFilter() {
        const sel = $('f-bonus');
        const prevValue = sel.value;
        const bonusSet = new Set();
        items.forEach(item => {
            Object.values(item.bonuses || {}).forEach(b => bonusSet.add(b.bonus));
        });
        sel.innerHTML = '<option value="">All Bonuses</option>';
        [...bonusSet].sort().forEach(b => {
            const o = document.createElement('option');
            o.value = b;
            o.textContent = b;
            sel.appendChild(o);
        });
        if ([...bonusSet].includes(prevValue)) sel.value = prevValue;
    }

    function renderItems() {
        const list = $('item-list');
        loadItemsFromVault();

        const savedPrices  = GM_getValue('rwts_prices', {});
        const savedNotes   = GM_getValue('rwts_notes', {});
        const showNotes    = $('tog-notes').checked;
        const compact      = $('tog-compact').checked;
        const search       = $('item-search').value.toLowerCase();
        const bonusFilter  = $('f-bonus').value;
        const rarityFilter = $('f-rarity').value;
        const statusFilter = $('f-status').value;
        const sortMode     = $('item-sort').value;

        list.innerHTML = '';

        if (!items.length) {
            list.innerHTML = '<div class="stxt">Vault is empty. Sync your bazaar to populate it.</div>';
            _filteredUIDs = [];
            updateVaultStats();
            return;
        }

        const filtered = items.filter(i => {
            if (search && !i.name.toLowerCase().includes(search)) return false;
            if (bonusFilter && !Object.values(i.bonuses || {}).some(b => b.bonus === bonusFilter)) return false;
            if (rarityFilter && normRarity(i.rarity) !== rarityFilter) return false;
            if (statusFilter && (i.status || 'active') !== statusFilter) return false;
            return true;
        });

        filtered.sort((a, b) => {
            if (sortMode === 'name-asc')  return a.name.localeCompare(b.name);
            if (sortMode === 'name-desc') return b.name.localeCompare(a.name);
            if (sortMode === 'rarity')    return RARITY_ORDER.indexOf(normRarity(a.rarity)) - RARITY_ORDER.indexOf(normRarity(b.rarity));
            if (sortMode === 'status')    return STATUS_ORDER.indexOf(a.status || 'active') - STATUS_ORDER.indexOf(b.status || 'active');
            return TYPE_ORDER.indexOf(normType(a.type)) - TYPE_ORDER.indexOf(normType(b.type));
        });

        _filteredUIDs = filtered.map(f => f.UID);

        if (!filtered.length) {
            list.innerHTML = '<div class="stxt">No items match current filters.</div>';
            updateVaultStats();
            return;
        }

        const countEl = document.createElement('div');
        countEl.className = 'item-count';
        countEl.textContent = filtered.length === items.length
            ? `${filtered.length} item${filtered.length !== 1 ? 's' : ''}`
            : `${filtered.length} of ${items.length} items (filtered)`;
        list.appendChild(countEl);

        filtered.forEach(item => {
            const color = RARITY_COLORS[normRarity(item.rarity)] || '#fff';
            const bonuses = Object.values(item.bonuses || {})
                .map(b => `${b.bonus} ${b.value}%`).join(' | ');
            const stats = item.damage
                ? `Q:${item.quality ?? 'N/A'}% Dmg:${item.damage} Acc:${item.accuracy ?? 'N/A'}`
                : `Q:${item.quality ?? 'N/A'}% Armor:${item.armor ?? 'N/A'}`;
            const status = item.status || 'active';
            const inBaz  = !!item.inBazaar;

            const card = document.createElement('div');
            card.className = `ic st-${status}`;
            card.innerHTML = `
                <div style="display:flex;justify-content:space-between;align-items:flex-start;gap:6px">
                    <div style="flex:1;min-width:0">
                        <div class="ic-name" style="color:${color}">${item.name}</div>
                        ${!compact ? `<div class="ic-stats">${stats}</div><div class="ic-bon">${bonuses}</div>` : ''}
                    </div>
                    <div style="display:flex;flex-direction:column;align-items:flex-end;gap:3px;flex-shrink:0">
                        <span class="ic-tag">${normType(item.type)}</span>
                        <span class="${inBaz ? 'baz-yes' : 'baz-no'}">${inBaz ? 'Listed' : 'Vault'}</span>
                    </div>
                </div>
                <div style="display:flex;gap:6px;margin-top:7px;align-items:center">
                    <select class="ri sf" data-uid="${item.UID}" style="width:88px" title="Item status">
                        <option value="active" ${status === 'active' ? 'selected' : ''}>Active</option>
                        <option value="sold"   ${status === 'sold'   ? 'selected' : ''}>Sold</option>
                        <option value="hidden" ${status === 'hidden' ? 'selected' : ''}>Hidden</option>
                    </select>
                    <input type="text" class="ri pf" data-uid="${item.UID}" placeholder="Price (e.g. 5m)" value="${savedPrices[item.UID] || item.bazaar_price || ''}" style="flex:1">
                    <button class="rb-x" data-uid="${item.UID}" title="Remove from vault">&#x2715;</button>
                </div>
                ${showNotes ? `<input type="text" class="note-i nf" data-uid="${item.UID}" placeholder="Item note (optional)..." value="${savedNotes[item.UID] || ''}">` : ''}
            `;

            card.querySelector('.sf').onchange = e => {
                setItemStatus(e.target.dataset.uid, e.target.value);
                renderItems();
                updateVaultStats();
            };
            card.querySelector('.pf').oninput = e => {
                const p = GM_getValue('rwts_prices', {});
                p[e.target.dataset.uid] = e.target.value;
                GM_setValue('rwts_prices', p);
            };
            card.querySelector('.rb-x').onclick = () => {
                if (confirm(`Remove "${item.name}" from vault?\n\nThis only removes the script's stored copy. The item itself in Torn is unaffected.`)) {
                    removeFromVault(item.UID);
                    renderItems();
                    updateVaultStats();
                }
            };
            if (showNotes) {
                card.querySelector('.nf').oninput = e => {
                    const n = GM_getValue('rwts_notes', {});
                    n[e.target.dataset.uid] = e.target.value;
                    GM_setValue('rwts_notes', n);
                };
            }
            list.appendChild(card);
        });

        updateVaultStats();
    }

    function bulkSetStatus(status) {
        if (!_filteredUIDs.length) return alert('No filtered items to update.');
        const label = STATUS_LABELS[status];
        if (!confirm(`Set ${_filteredUIDs.length} filtered item(s) to "${label}"?`)) return;
        const v = getVault();
        _filteredUIDs.forEach(uid => { if (v[uid]) v[uid].status = status; });
        saveVault(v);
        renderItems();
    }
    function bulkRemove() {
        if (!_filteredUIDs.length) return alert('No filtered items to remove.');
        if (!confirm(`Permanently remove ${_filteredUIDs.length} filtered item(s) from vault?\n\nThis cannot be undone (export first if you want a backup).`)) return;
        const v = getVault();
        _filteredUIDs.forEach(uid => delete v[uid]);
        saveVault(v);
        renderItems();
    }
    $('btn-bulk-active').onclick = () => bulkSetStatus('active');
    $('btn-bulk-sold').onclick   = () => bulkSetStatus('sold');
    $('btn-bulk-hidden').onclick = () => bulkSetStatus('hidden');
    $('btn-bulk-remove').onclick = bulkRemove;

    $('item-search').oninput = renderItems;
    $('item-sort').onchange  = renderItems;
    $('f-bonus').onchange    = renderItems;
    $('f-rarity').onchange   = renderItems;
    $('f-status').onchange   = renderItems;

    function updateVaultStats() {
        const el = $('vault-stats');
        if (!el) return;
        const v = getVault();
        const arr = Object.values(v);
        if (!arr.length) {
            el.innerHTML = `<div style="color:${UI.textDim};font-style:italic">Vault is empty - sync your bazaar to begin.</div>`;
            return;
        }
        const counts = { active: 0, sold: 0, hidden: 0, listed: 0, vaulted: 0 };
        arr.forEach(i => {
            counts[normStatus(i.status)]++;
            if (i.inBazaar) counts.listed++; else counts.vaulted++;
        });
        el.innerHTML = `
            <div><strong style="color:${UI.accent}">${arr.length}</strong> item${arr.length !== 1 ? 's' : ''} in vault</div>
            <div style="margin-top:3px">
                <span style="color:${STATUS_COLORS.active}">&#x25CF; ${counts.active} active</span> &middot;
                <span style="color:${STATUS_COLORS.sold}">&#x25CF; ${counts.sold} sold</span> &middot;
                <span style="color:${STATUS_COLORS.hidden}">&#x25CF; ${counts.hidden} hidden</span>
            </div>
            <div style="margin-top:3px;color:${UI.textDim}">
                ${counts.listed} currently listed &middot; ${counts.vaulted} vault-only
            </div>
        `;
    }

    $('btn-export').onclick = () => {
        const data = {
            version: VERSION,
            exportedAt: new Date().toISOString(),
            prices: GM_getValue('rwts_prices', {}),
            notes:  GM_getValue('rwts_notes',  {}),
            vault:  getVault()
        };
        const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
        const a = document.createElement('a');
        a.href = URL.createObjectURL(blob);
        a.download = `rwts-vault-${Date.now()}.json`;
        a.click();
    };

    $('btn-import').onclick = () => $('import-file').click();
    $('import-file').onchange = e => {
        const f = e.target.files[0];
        if (!f) return;
        const reader = new FileReader();
        reader.onload = ev => {
            try {
                const d = JSON.parse(ev.target.result);
                let imported = [];
                if (d.prices) { GM_setValue('rwts_prices', d.prices); imported.push('prices'); }
                if (d.notes)  { GM_setValue('rwts_notes',  d.notes);  imported.push('notes');  }
                if (d.vault)  {
                    const existing = getVault();
                    const merged = { ...existing, ...d.vault };
                    saveVault(merged);
                    imported.push(`vault (${Object.keys(d.vault).length} items)`);
                }
                loadItemsFromVault();
                buildBonusFilter();
                renderItems();
                updateVaultStats();
                alert(imported.length ? `Imported: ${imported.join(', ')}` : 'No recognised fields in file.');
            } catch (err) {
                alert('Invalid JSON file: ' + err.message);
            }
        };
        reader.readAsText(f);
        e.target.value = '';
    };

    $('btn-clr-vault').onclick = () => {
        const total = Object.keys(getVault()).length;
        if (!total) return alert('Vault is already empty.');
        if (confirm(`Clear ENTIRE vault (${total} items)?\n\nThis removes all stored items, statuses, and bazaar history. Cannot be undone - export first if you want a backup.`)) {
            saveVault({});
            GM_setValue('rwts_cache', {});
            loadItemsFromVault();
            renderItems();
            updateVaultStats();
            $('load-msg').textContent = 'Vault cleared.';
        }
    };
    $('btn-clr-prices').onclick = () => {
        if (confirm('Clear all saved prices and notes?\n\n(Vault items themselves are kept.)')) {
            GM_setValue('rwts_prices', {});
            GM_setValue('rwts_notes', {});
            renderItems();
        }
    };

    const groupHeader = (text, count, showCounts) => {
        const cnt = showCounts ? ` <span style="color:#aaa;font-size:13px;">(${count})</span>` : '';
        return `<h2 style="color:#ccc;border-bottom:1px solid #333;padding-bottom:5px;margin:30px 0 15px 0;">${text}${cnt}</h2>`;
    };

    function renderSection(sectionItems, groupKey, sortFn, theme, opts) {
        const { sepDouble, showCounts, notesInHtml } = opts;
        const grouper = GROUPS[groupKey] || GROUPS.none;

        const renderList = arr => arr
            .map(item => theme.item(item, RARITY_COLORS[normRarity(item.rarity)] || RARITY_COLORS.White, notesInHtml))
            .join('');

        const block = (titleText, arr) => {
            if (!arr.length) return '';
            const head = titleText ? groupHeader(titleText, arr.length, showCounts) : '';
            return head + renderList(arr.slice().sort(sortFn));
        };

        let pool = sectionItems.slice();
        let html = '';

        if (sepDouble) {
            const doubles = pool.filter(i => bonusCount(i) >= 2);
            pool = pool.filter(i => bonusCount(i) < 2);
            if (doubles.length) {
                html += block('Double Bonus', doubles);
                if (grouper === GROUPS.none && pool.length) {
                    html += block('Single Bonus', pool);
                    return html;
                }
            }
        }

        if (grouper === GROUPS.none) {
            html += block('', pool);
            return html;
        }

        const buckets = {};
        pool.forEach(i => { const k = grouper.keyOf(i); (buckets[k] ||= []).push(i); });
        const fixed = grouper.order || [];
        const dynamic = Object.keys(buckets).filter(k => !fixed.includes(k)).sort((a, b) => a.localeCompare(b));
        [...fixed, ...dynamic].forEach(k => {
            if (buckets[k]?.length) html += block(grouper.title(k), buckets[k]);
        });
        return html;
    }

    function buildHTML() {
        const designMode  = $('design-mode').value;
        const filterCat   = $('f-cat').value;
        const showCounts  = $('tog-counts').checked;
        const notesInHtml = $('tog-notes-html').checked;
        const includeSold = $('tog-include-sold').checked;
        const savedPrices = GM_getValue('rwts_prices', {});

        const wGroup    = $('w-group').value;
        const wSort     = $('w-sort').value;
        const wSort2    = $('w-sort2').value;
        const aGroup    = $('a-group').value;
        const aSort     = $('a-sort').value;
        const aSort2    = $('a-sort2').value;
        const sepArmor  = $('tog-sep-armor').checked;
        const sepDouble = $('tog-sep-double').checked;
        const armorTitle = ($('armor-title').value || 'Armor').trim() || 'Armor';

        let included = 0, skippedHidden = 0, skippedSold = 0, skippedCat = 0;
        const weapons = [], armor = [];

        items.forEach(item => {
            const status = normStatus(item.status);
            if (status === 'hidden') { skippedHidden++; return; }
            if (status === 'sold' && !includeSold) { skippedSold++; return; }
            if (filterCat !== 'all' && normType(item.type) !== filterCat) { skippedCat++; return; }
            item.manualPrice = savedPrices[item.UID] || item.bazaar_price || '';
            if (sepArmor && normType(item.type) === 'Armor') armor.push(item);
            else weapons.push(item);
            included++;
        });

        const userTitle  = $('shop-title').value.trim();
        const userDesc   = $('shop-desc').value.trim();
        const theme      = THEMES[designMode] || THEMES.glow;
        const armorTheme = THEMES[$('armor-theme').value] || theme;
        const opts = { sepDouble, showCounts, notesInHtml };
        const weaponCmp = buildComparator(wSort, wSort2);
        const armorCmp  = buildComparator(aSort, aSort2);

        let html = `<div style="background:#111;padding:20px;color:#fff;font-family:Arial;border-radius:8px;text-align:center;">`;
        if (userTitle || userDesc) {
            html += theme.header(userTitle || '&nbsp;', userDesc || '');
        }

        html += renderSection(weapons, wGroup, weaponCmp, theme, opts);

        if (sepArmor && armor.length) {
            const cnt = showCounts ? ` <span style="color:#888;font-size:14px;">(${armor.length})</span>` : '';
            html += `<h2 style="color:${UI.accent};border-top:2px solid ${UI.accent};border-bottom:none;padding-top:14px;margin:40px auto 18px auto;max-width:600px;text-transform:uppercase;letter-spacing:2px;">${armorTitle}${cnt}</h2>`;
            html += renderSection(armor, aGroup, armorCmp, armorTheme, opts);
        }

        html += `</div>`;

        return { html, included, skippedHidden, skippedSold, skippedCat };
    }

    function showGenSummary({ included, skippedHidden, skippedSold, skippedCat }) {
        const parts = [`<strong style="color:${UI.accent}">${included}</strong> included`];
        if (skippedHidden) parts.push(`${skippedHidden} hidden`);
        if (skippedSold)   parts.push(`${skippedSold} sold (excluded)`);
        if (skippedCat)    parts.push(`${skippedCat} filtered out`);
        $('gen-summary').innerHTML = parts.join(' &middot; ');
    }

    $('btn-gen').onclick = () => {
        if (!items.length) return alert('Vault is empty. Sync your bazaar first.');
        const result = buildHTML();
        if (!result.included) return alert('Nothing to generate. Check filters and item statuses.');
        $('out-box').value = result.html;
        $('out-box').style.display = 'block';
        showGenSummary(result);
        GM_setClipboard(result.html);
        alert(`HTML generated and copied to clipboard!\n\n${result.included} item(s) included.`);
    };

    $('btn-preview').onclick = () => {
        if (!items.length) return alert('Vault is empty. Sync your bazaar first.');
        const result = buildHTML();
        if (!result.included) return alert('Nothing to preview. Check filters and item statuses.');
        $('out-box').value = result.html;
        showGenSummary(result);
        $('prev-frame').srcdoc = `<!DOCTYPE html><html><body style="margin:0;background:#111;">${result.html}</body></html>`;
        prevOverlay.style.display = 'block';
    };

    loadItemsFromVault();
    buildBonusFilter();
    renderItems();
    updateVaultStats();

})();