Torn RW ThreadSmith

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

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

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

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

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

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

You will need to install a user script manager extension to install this script.

(Tôi đã có Trình quản lý tập lệnh người dùng, hãy cài đặt nó!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         Torn RW ThreadSmith
// @namespace    http://tampermonkey.net/
// @version       4.0.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.0.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: '#888' };

    const UI = {
        panelBg:    '#181818',
        headerBg:   '#202020',
        tabBarBg:   '#1a1a1a',
        cardBg:     '#0f0f0f',
        inputBg:    '#0f0f0f',
        border:     '#2e2e2e',
        borderSoft: '#252525',
        textMain:   '#e0e0e0',
        textDim:    '#b0b0b0',
        textFaint:  '#9a9a9a',
        accent:     '#c9a84c',
        accentBg:   '#291e00',
        accentBgHi: '#372800',
        success:    '#4caf50',
        successBg:  '#0e1f0e',
        danger:     '#ff6b6b',
        dangerBg:   '#250e0e',
        bonusUI:    '#9778ff',
        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:#888;font-style:italic;margin-top:6px;padding-top:6px;border-top:1px solid rgba(255,255,255,0.06);">${note}</div>`;
    };

    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:#888;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;
                const note = GM_getValue('rwts_notes', {})[item.UID];
                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>
                        ${includeNote && note ? `<div style="padding:0 12px 10px;">${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.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: '#a6927d'
            },
            item(item, rarity, includeNote) {
                const c = this.palette;
                const t = normType(item.type);
                let em = '❖&#xFE0E;';
                if (t === 'Primary' || t === 'Secondary') em = '⌖&#xFE0E;';
                else if (t === 'Melee') em = '⚔&#xFE0E;';
                else if (t === 'Armor') em = '⛨&#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.6)',
                darkSubtle:  'rgba(255,255,255,0.8)',
                lightPill:   'rgba(0,0,0,0.1)',
                darkPill:    'rgba(0,0,0,0.2)',
                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;
                const note = GM_getValue('rwts_notes', {})[item.UID];
                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>
                            ${includeNote && note ? `<div style="font-size:10px;color:${sc};font-style:italic;margin-top:2px;">${note}</div>` : ''}
                        </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,.6);font-size:16px;margin:0;">${desc.replace(/\n/g, '<br>')}</p></div>`;
            }
        },

        terminal: {
            label: 'Retro Terminal',
            palette: {
                bg:        '#050505',
                hdr:       '#111',
                hdrBorder: '#333',
                statText:  '#888',
                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};">>_ STATS:</span> ${fmtStats(item)}</div>
                            <div style="color:${SHARED.bonus};font-size:12px;font-weight:bold;"><span style="color:${c.statText};">>_ 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;">> ${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:   '#d7dadc',
                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;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;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:#efefef;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:450px;background:${UI.panelBg};color:${UI.textMain};border:1px solid #333;z-index:9999;font-family:Arial,sans-serif;font-size:12px;border-radius:6px;box-shadow:0 6px 40px rgba(0,0,0,.9)}
        #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.textFaint};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:#666}
        .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:.6}
        .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:#777;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}
        .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:99999}
        #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 pos = GM_getValue('rwts_pos', { top: '50px', right: '20px', left: 'auto' });
    wrap.style.top = pos.top;
    if (pos.left === 'auto') {
        wrap.style.right = pos.right;
    } 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">⚔ 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 ? '▲' : '▼'}</span>
            </div>
        </div>
        <div id="rwts-col" style="display:${minimized ? 'none' : 'block'}">
            <div id="rwts-tabs">
                <div class="rt on" data-tab="load">📦 Sync</div>
                <div class="rt" data-tab="items">🗄 Vault</div>
                <div class="rt" data-tab="gen">🖨 Output</div>
                <div class="rt" data-tab="cfg">⚙ 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">✕ 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 & Restore</div>
                    <div class="rrow">
                        <button class="rb" id="btn-export" style="flex:1">⬇ Export JSON</button>
                        <button class="rb" id="btn-import" style="flex:1">⬆ 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:#777;margin-top:6px;line-height:1.6">
                        <strong style="color:#aaa">Vault</strong>: Persistent storage of every item you've 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 → Active">→ Active</button>
                        <button class="rb sold" id="btn-bulk-sold"   title="Set filtered → Sold">→ Sold</button>
                        <button class="rb"      id="btn-bulk-hidden" title="Set filtered → Hidden">→ Hidden</button>
                        <button class="rb-x"    id="btn-bulk-remove" title="Remove filtered items from vault">✕</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</div>
                    <div class="rrow">
                        <select id="sort-mode" class="ri" style="flex:1">
                            <option value="type">Group: Category</option>
                            <option value="rarity">Group: Rarity</option>
                        </select>
                        <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="tw" style="margin:8px 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">👁 Preview</button>
                        <button class="rb gold" id="btn-gen" style="flex:2">⚡ Generate & Copy</button>
                    </div>
                    <div id="gen-summary" class="stxt" style="text-align:left;color:#777;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">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">About</div>
                    <div class="about">
                        RW ThreadSmith v${VERSION} · GPL-3.0 License<br>
                        by Rowage [3926289]<br>
                        API cadence ≈ 85 req/min<br>
                        ${Object.keys(THEMES).length} designs · Vault storage · Bulk actions · Live preview · 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">✕ Close</button>
                </div>
            </div>
            <iframe id="prev-frame"></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!');
    };

    $('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');
    $('sort-mode').value    = GM_getValue('rwts_sort', 'type');
    $('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);

    $('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);
    $('sort-mode').onchange   = e => GM_setValue('rwts_sort',   e.target.value);
    $('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);

    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').textContent = willMinimize ? '▲' : '▼';
        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;
        $('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;
        };
        document.addEventListener('mousemove', e => {
            if (!dragging) return;
            wrap.style.left = (e.clientX - ox) + 'px';
            wrap.style.top  = (e.clientY - oy) + 'px';
        });
        document.addEventListener('mouseup', () => {
            if (dragging) {
                GM_setValue('rwts_pos', { top: wrap.style.top, left: wrap.style.left, right: 'auto' });
            }
            dragging = false;
        });
    }

    $('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;

        try {
            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 vault = getVault();
            const now = Date.now();

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

            const total = bazItems.length;
            let newCount = 0, refreshedCount = 0, skippedCount = 0;

            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">✕</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:#777;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}">● ${counts.active} active</span> ·
                <span style="color:${STATUS_COLORS.sold}">● ${counts.sold} sold</span> ·
                <span style="color:${STATUS_COLORS.hidden}">● ${counts.hidden} hidden</span>
            </div>
            <div style="margin-top:3px;color:#888">
                ${counts.listed} currently listed · ${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();
        }
    };

    function buildHTML() {
        const sortMode    = $('sort-mode').value;
        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 groups = {};
        let included = 0, skippedHidden = 0, skippedSold = 0, skippedCat = 0;

        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 || '';
            const key = sortMode === 'type' ? normType(item.type) : normRarity(item.rarity);
            (groups[key] ||= []).push(item);
            included++;
        });
        Object.values(groups).forEach(arr => arr.sort((a, b) => a.name.localeCompare(b.name)));

        const userTitle = $('shop-title').value.trim();
        const userDesc  = $('shop-desc').value.trim();
        const theme     = THEMES[designMode] || THEMES.glow;

        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 || '');
        }

        const order = sortMode === 'type' ? TYPE_ORDER : RARITY_ORDER;
        for (const group of order) {
            if (!groups[group]?.length) continue;
            const cnt = showCounts ? ` <span style="color:#888;font-size:13px;">(${groups[group].length})</span>` : '';
            html += `<h2 style="color:#aaa;border-bottom:1px solid #333;padding-bottom:5px;margin:30px 0 15px 0;">${group} Items${cnt}</h2>`;
            for (const item of groups[group]) {
                const color = RARITY_COLORS[normRarity(item.rarity)] || RARITY_COLORS.White;
                html += theme.item(item, color, notesInHtml);
            }
        }
        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(' · ');
    }

    $('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();

})();