LostMaster Travel

Museum Day, Flushies, and Xanax travel optimizer for Torn

Du musst eine Erweiterung wie Tampermonkey, Greasemonkey oder Violentmonkey installieren, um dieses Skript zu installieren.

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 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.

Sie müssten eine Skript Manager Erweiterung installieren damit sie dieses Skript installieren können

(Ich habe schon ein Skript Manager, Lass mich es installieren!)

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         LostMaster Travel
// @namespace    lostmaster.travel
// @version      2.0.8
// @description  Museum Day, Flushies, and Xanax travel optimizer for Torn
// @author       LostMaster
// @license      MIT
// @match        https://www.torn.com/*
// @grant        GM_xmlhttpRequest
// @connect      yata.yt
// ==/UserScript==

(function () {
    'use strict';

    const APP = 'LostMaster Travel';
    const SHORT_APP = 'LMT';
    const VERSION = '2.0.8';

    const API_BASE = 'https://api.torn.com/v2';
    const YATA_URL =
        'https://yata.yt/api/v1/travel/export/';

    const ROOT_ID = 'lmt-root';
    const STYLE_ID = 'lmt-style';

    const STORAGE = {
        key: 'lmt_api_key',
        inventory: 'lmt_inventory_cache',
        mode: 'lmt_mode',
        profile: 'lmt_travel_profile',
        minimized: 'lmt_minimized',
        position: 'lmt_panel_position',
        country: 'lmt_last_foreign_location',
        yata: 'lmt_yata_cache',
        prices: 'lmt_trusted_price_cache',
        sizeDesktop: 'lmt_panel_size_desktop',
        sizeMobile: 'lmt_panel_size_mobile'
    };

    const YATA_TTL =
        60 * 1000;

    const YATA_FAILURE_RETRY_MS =
        60 * 1000;

    const PRICE_TTL =
        30 * 60 * 1000;

    const INVENTORY_TTL =
        60 * 60 * 1000;

    const STOCK_STALE_MS =
        10 * 60 * 1000;

    const WATCH_MS =
        500;

    const BUY_FREEZE_MS =
        15000;

    /*
     * Torn asks API clients to stop using disabled / invalid keys
     * instead of repeatedly retrying them.
     *
     * 1  = key empty
     * 2  = incorrect key
     * 10 = key owner in federal jail
     * 13 = key disabled due to owner inactivity
     * 16 = access level too low
     * 18 = key paused by owner
     */
    const KEY_STOP_ERROR_CODES =
        new Set([
            1,
            2,
            10,
            13,
            16,
            18
        ]);

    /*
     * Trusted market pricing:
     *
     * - inspect up to the cheapest 1,000 actual units
     * - use the price at the 25% depth point
     *
     * Example:
     * unit 250 of the cheapest 1,000.
     */
    const MARKET_TARGET_UNITS =
        1000;

    const MARKET_DEPTH_PERCENT =
        0.25;

    const MARKET_MAX_PAGES =
        20;

    /*
     * Xanax prices abroad fluctuate.
     *
     * The buffer only affects the amount of cash LMT recommends
     * carrying. It does NOT change estimated profit.
     */
    const XANAX_CASH_BUFFER_PERCENT =
        0.02;

    const XANAX_CASH_BUFFER_MIN =
        250000;

    /*
     * Quick / Extended are strategic destination groups.
     *
     * Travel-speed perks change displayed times and profit/hour,
     * but they do NOT move countries between groups.
     */
    const QUICK_COUNTRIES =
        new Set([
            'Mexico',
            'Cayman Islands',
            'Canada'
        ]);


    // ============================================================
    // TRAVEL DATA
    // ============================================================

    const TRAVEL = {
        'Mexico': {
            minutes: 24,
            city: 'Ciudad Juarez',
            cost: 6500,
            yata: 'mex'
        },

        'Cayman Islands': {
            minutes: 33,
            city: 'George Town',
            cost: 10000,
            yata: 'cay'
        },

        'Canada': {
            minutes: 39,
            city: 'Toronto',
            cost: 9000,
            yata: 'can'
        },

        'Hawaii': {
            minutes: 127,
            city: 'Honolulu',
            cost: 11000,
            yata: 'haw'
        },

        'United Kingdom': {
            minutes: 151,
            city: 'London',
            cost: 18000,
            yata: 'uni'
        },

        'Argentina': {
            minutes: 158,
            city: 'Buenos Aires',
            cost: 21000,
            yata: 'arg'
        },

        'Switzerland': {
            minutes: 166,
            city: 'Zurich',
            cost: 27000,
            yata: 'swi'
        },

        'Japan': {
            minutes: 213,
            city: 'Tokyo',
            cost: 32000,
            yata: 'jap'
        },

        'China': {
            minutes: 229,
            city: 'Beijing',
            cost: 35000,
            yata: 'chi'
        },

        'United Arab Emirates': {
            minutes: 257,
            city: 'Dubai',
            cost: 32000,
            yata: 'uae'
        },

        'South Africa': {
            minutes: 282,
            city: 'Johannesburg',
            cost: 40000,
            yata: 'sou'
        }
    };

    const YATA_COUNTRY =
        Object.fromEntries(
            Object.entries(TRAVEL)
                .map(
                    ([country, data]) => [
                        data.yata,
                        country
                    ]
                )
        );


    // ============================================================
    // MUSEUM DATA
    // ============================================================

    /*
     * Museum mode deliberately excludes:
     *
     * Hawaii:
     *   Orchid
     *
     * Switzerland:
     *   Chamois Plushie
     *   Edelweiss
     *
     * Japan:
     *   Cherry Blossom
     *
     * Torn-native plushies are also not tracked.
     */
    const MUSEUM_ITEMS = [
        {
            name: 'Jaguar Plushie',
            type: 'plushie',
            country: 'Mexico'
        },

        {
            name: 'Stingray Plushie',
            type: 'plushie',
            country: 'Cayman Islands'
        },

        {
            name: 'Wolverine Plushie',
            type: 'plushie',
            country: 'Canada'
        },

        {
            name: 'Nessie Plushie',
            type: 'plushie',
            country: 'United Kingdom'
        },

        {
            name: 'Red Fox Plushie',
            type: 'plushie',
            country: 'United Kingdom'
        },

        {
            name: 'Monkey Plushie',
            type: 'plushie',
            country: 'Argentina'
        },

        {
            name: 'Panda Plushie',
            type: 'plushie',
            country: 'China'
        },

        {
            name: 'Camel Plushie',
            type: 'plushie',
            country: 'United Arab Emirates'
        },

        {
            name: 'Lion Plushie',
            type: 'plushie',
            country: 'South Africa'
        },

        {
            name: 'Dahlia',
            type: 'flower',
            country: 'Mexico'
        },

        {
            name: 'Banana Orchid',
            type: 'flower',
            country: 'Cayman Islands'
        },

        {
            name: 'Crocus',
            type: 'flower',
            country: 'Canada'
        },

        {
            name: 'Heather',
            type: 'flower',
            country: 'United Kingdom'
        },

        {
            name: 'Ceibo Flower',
            type: 'flower',
            country: 'Argentina'
        },

        {
            name: 'Peony',
            type: 'flower',
            country: 'China'
        },

        {
            name: 'Tribulus Omanense',
            type: 'flower',
            country: 'United Arab Emirates'
        },

        {
            name: 'African Violet',
            type: 'flower',
            country: 'South Africa'
        }
    ];

    const MUSEUM_COUNTRIES =
        Object.fromEntries(
            Object.keys(TRAVEL)
                .map(
                    country => [
                        country,

                        MUSEUM_ITEMS
                            .filter(
                                item =>
                                    item.country ===
                                    country
                            )
                            .map(
                                item =>
                                    item.name
                            )
                    ]
                )
                .filter(
                    ([, items]) =>
                        items.length > 0
                )
        );


    // ============================================================
    // FLUSHIES DATA
    // ============================================================

    /*
     * Flushies includes EVERY foreign flower / plushie worth
     * comparing for resale, including locations deliberately
     * excluded from Museum mode.
     */
    const FLUSHIE_NAMES =
        new Set([
            ...MUSEUM_ITEMS.map(
                item =>
                    item.name
            ),

            'Orchid',
            'Chamois Plushie',
            'Edelweiss',
            'Cherry Blossom'
        ]);


    // ============================================================
    // MODES / PROFILE
    // ============================================================

    const MODES = {
        museum: 'Museum Day',
        flushies: 'Flushies',
        xanax: 'Xanax'
    };

    const METHOD_MULTIPLIER = {
        standard: 1,
        airstrip: 0.70,
        wlt: 0.50,
        business: 0.30
    };

    const DEFAULT_PROFILE = {
        method: 'standard',
        mailingBook: false,
        capacity: 28,
        businessTicketsFree: false
    };

    /*
     * Business Class Ticket item ID.
     *
     * Only used when Business Class is selected and tickets
     * are not configured as free.
     */
    const BCT_ITEM_ID =
        396;


    // ============================================================
    // RUNTIME STATE
    // ============================================================

    let settingsOpen =
        false;

    let changeKeyOpen =
        false;

    let inventoryOpen =
        false;

    let inventoryBusy =
        false;

    let dataBusy =
        false;

    let dataError =
        '';

    let yataWarning =
        '';

    let yataRetryAfter =
        0;

    let lastSignature =
        '';

    let buyFrozen =
        false;

    let freezeStarted =
        0;

    let confirmationSeen =
        false;

    let drag =
        null;

    let dragInstalled =
        false;

    let resizeState =
        null;

    let resizeInstalled =
        false;

    let profileSavedUntil =
        0;

    /*
     * Persisted only in runtime so a bad stored key can be removed
     * while still telling the user why LMT returned to Setup.
     */
    let apiKeyNotice =
        '';


    // ============================================================
    // GENERAL HELPERS
    // ============================================================

    function norm(value) {
        return String(
            value || ''
        )
            .trim()
            .toLowerCase();
    }

    function esc(value) {
        const div =
            document.createElement(
                'div'
            );

        div.textContent =
            String(
                value ?? ''
            );

        return div.innerHTML;
    }

    function num(value) {
        const match =
            String(
                value || ''
            )
                .replace(
                    /,/g,
                    ''
                )
                .match(
                    /\d+/
                );

        return match
            ? Number(
                match[0]
            )
            : null;
    }

    function money(value) {
        if (
            !Number.isFinite(
                Number(value)
            )
        ) {
            return '—';
        }

        return '$' +
            Math.round(
                Number(value)
            )
                .toLocaleString();
    }

    function duration(minutes) {
        const m =
            Math.max(
                0,
                Math.round(
                    Number(minutes) ||
                    0
                )
            );

        if (
            m < 60
        ) {
            return m +
                'm';
        }

        return (
            Math.floor(
                m / 60
            ) +
            'h ' +
            (
                m %
                60
            ) +
            'm'
        );
    }

    function sleep(ms) {
        return new Promise(
            resolve =>
                setTimeout(
                    resolve,
                    ms
                )
        );
    }

    function mobilePanelProfile() {
        return (
            window.matchMedia(
                '(pointer: coarse)'
            ).matches ||
            window.innerWidth <= 700
        );
    }

    function panelSizeStorageKey() {
        return mobilePanelProfile()
            ? STORAGE.sizeMobile
            : STORAGE.sizeDesktop;
    }

    function savedPanelSize() {
        return readJson(
            panelSizeStorageKey()
        );
    }

    function panelSizeLimits() {
        return {
            minWidth:
                Math.min(
                    260,
                    Math.max(
                        180,
                        window.innerWidth - 20
                    )
                ),

            minHeight:
                Math.min(
                    220,
                    Math.max(
                        160,
                        window.innerHeight - 20
                    )
                ),

            maxWidth:
                Math.max(
                    180,
                    window.innerWidth - 20
                ),

            maxHeight:
                Math.max(
                    160,
                    window.innerHeight - 20
                )
        };
    }

    function clampPanelSize(
        width,
        height
    ) {
        const limits =
            panelSizeLimits();

        return {
            width:
                Math.min(
                    limits.maxWidth,
                    Math.max(
                        limits.minWidth,
                        Number(width) ||
                        limits.minWidth
                    )
                ),

            height:
                Math.min(
                    limits.maxHeight,
                    Math.max(
                        limits.minHeight,
                        Number(height) ||
                        limits.minHeight
                    )
                )
        };
    }

    function applyPanelSize(
        panel
    ) {
        const saved =
            savedPanelSize();

        if (
            saved &&
            Number.isFinite(
                saved.width
            ) &&
            Number.isFinite(
                saved.height
            )
        ) {
            const size =
                clampPanelSize(
                    saved.width,
                    saved.height
                );

            panel.style.width =
                size.width +
                'px';

            panel.style.height =
                '';

            panel.style.maxHeight =
                size.height +
                'px';

            panel.classList.add(
                'sized'
            );
        }
        else {
            panel.style.width =
                '';

            panel.style.height =
                '';

            panel.style.maxHeight =
                '';

            panel.classList.remove(
                'sized'
            );
        }
    }

    function savePanelSize(
        width,
        height
    ) {
        const size =
            clampPanelSize(
                width,
                height
            );

        writeJson(
            panelSizeStorageKey(),
            size
        );

        return size;
    }


    // ============================================================
    // STORAGE
    // ============================================================

    function readJson(
        key,
        fallback = null
    ) {
        try {
            const value =
                JSON.parse(
                    localStorage.getItem(
                        key
                    ) ||
                    'null'
                );

            return value ??
                fallback;
        }
        catch (_) {
            return fallback;
        }
    }

    function writeJson(
        key,
        value
    ) {
        localStorage.setItem(
            key,
            JSON.stringify(
                value
            )
        );
    }

    function cacheGet(
        key,
        ttl =
            Number.MAX_SAFE_INTEGER
    ) {
        const cache =
            readJson(
                key
            );

        if (
            !cache ||
            !Number.isFinite(
                cache.updated
            )
        ) {
            return null;
        }

        if (
            Date.now() -
                cache.updated >
            ttl
        ) {
            return null;
        }

        return cache.data ??
            null;
    }

    function cacheSet(
        key,
        data
    ) {
        writeJson(
            key,
            {
                updated:
                    Date.now(),

                data
            }
        );
    }

    function getKey() {
        return (
            localStorage.getItem(
                STORAGE.key
            ) ||
            ''
        );
    }

    function setKey(key) {
        localStorage.setItem(
            STORAGE.key,
            String(
                key ||
                ''
            ).trim()
        );

        apiKeyNotice =
            '';
    }

    function removeStoredKey() {
        localStorage.removeItem(
            STORAGE.key
        );
    }

    function keyStopMessage(
        code,
        fallback
    ) {
        if (
            code ===
            16
        ) {
            return (
                'Saved API key does not have enough access. ' +
                'LMT requires a Minimal Access key. ' +
                'Please enter a new key.'
            );
        }

        if (
            code ===
            13
        ) {
            return (
                'Saved API key is disabled because the key owner ' +
                'has been inactive. Please enter an active key.'
            );
        }

        if (
            code ===
            18
        ) {
            return (
                'Saved API key has been paused by its owner. ' +
                'Please enter an active key.'
            );
        }

        if (
            code ===
            10
        ) {
            return (
                'Saved API key cannot currently be used. ' +
                'Please enter another active key.'
            );
        }

        return (
            fallback ||
            'Saved API key is invalid or unavailable. ' +
            'Please enter a new key.'
        );
    }

    function getMode() {
        let mode =
            localStorage.getItem(
                STORAGE.mode
            ) ||
            'museum';

        /*
         * 2.0.1 compatibility.
         */
        if (
            mode ===
            'profit'
        ) {
            mode =
                'flushies';

            localStorage.setItem(
                STORAGE.mode,
                mode
            );
        }

        return MODES[
            mode
        ]
            ? mode
            : 'museum';
    }

    function setMode(mode) {
        if (
            MODES[
                mode
            ]
        ) {
            localStorage.setItem(
                STORAGE.mode,
                mode
            );
        }
    }

    function getProfile() {
        const raw =
            readJson(
                STORAGE.profile,
                {}
            );

        return {
            method:
                METHOD_MULTIPLIER[
                    raw.method
                ] !==
                    undefined
                    ? raw.method
                    : DEFAULT_PROFILE
                        .method,

            mailingBook:
                Boolean(
                    raw.mailingBook
                ),

            capacity:
                Math.max(
                    1,
                    Math.min(
                        100,
                        Number(
                            raw.capacity
                        ) ||
                        DEFAULT_PROFILE
                            .capacity
                    )
                ),

            businessTicketsFree:
                Boolean(
                    raw.businessTicketsFree
                )
        };
    }

    function saveProfile(profile) {
        writeJson(
            STORAGE.profile,
            profile
        );
    }

    function effectiveMinutes(country) {
        const destination =
            TRAVEL[
                country
            ];

        if (
            !destination
        ) {
            return Infinity;
        }

        const profile =
            getProfile();

        const bookMultiplier =
            profile.mailingBook
                ? 0.75
                : 1;

        return Math.max(
            1,
            Math.round(
                destination.minutes *
                METHOD_MULTIPLIER[
                    profile.method
                ] *
                bookMultiplier
            )
        );
    }

    function bucket(country) {
        return QUICK_COUNTRIES
            .has(
                country
            )
            ? 'quick'
            : 'extended';
    }

    function rememberCountry(country) {
        if (
            TRAVEL[
                country
            ]
        ) {
            localStorage.setItem(
                STORAGE.country,
                country
            );
        }
    }

    function rememberedCountry() {
        const country =
            localStorage.getItem(
                STORAGE.country
            );

        return TRAVEL[
            country
        ]
            ? country
            : null;
    }

    function clearCountry() {
        localStorage.removeItem(
            STORAGE.country
        );
    }


    // ============================================================
    // INVENTORY CACHE
    // ============================================================

    function emptyInventory() {
        return Object.fromEntries(
            MUSEUM_ITEMS.map(
                item => [
                    item.name,
                    0
                ]
            )
        );
    }

    function getInventoryCache() {
        const cache =
            readJson(
                STORAGE.inventory
            );

        if (
            !cache?.items
        ) {
            return {
                items:
                    emptyInventory(),

                updated:
                    0
            };
        }

        return cache;
    }

    function saveInventory(items) {
        writeJson(
            STORAGE.inventory,
            {
                items,

                updated:
                    Date.now()
            }
        );
    }


    // ============================================================
    // TRUSTED PRICE CACHE
    // ============================================================

    function getPriceCache() {
        const cache =
            readJson(
                STORAGE.prices,
                {
                    items: {}
                }
            );

        if (
            !cache ||
            typeof cache !==
                'object'
        ) {
            return {
                items: {}
            };
        }

        if (
            !cache.items ||
            typeof cache.items !==
                'object'
        ) {
            cache.items =
                {};
        }

        return cache;
    }

    function savePriceCache(cache) {
        writeJson(
            STORAGE.prices,
            cache
        );
    }

    function cachedTrustedPrice(
        itemId,
        allowStale = false
    ) {
        const cache =
            getPriceCache();

        const entry =
            cache.items[
                String(
                    itemId
                )
            ];

        if (
            !entry ||
            !Number.isFinite(
                entry.updated
            )
        ) {
            return null;
        }

        if (
            !allowStale &&
            Date.now() -
                entry.updated >
                PRICE_TTL
        ) {
            return null;
        }

        return entry;
    }

    function storeTrustedPrice(
        itemId,
        entry
    ) {
        const cache =
            getPriceCache();

        cache.items[
            String(
                itemId
            )
        ] = {
            ...entry,

            updated:
                Date.now()
        };

        savePriceCache(
            cache
        );
    }


    // ============================================================
    // PAGE TEXT
    // ============================================================

    function pageText() {
        if (
            !document.body
        ) {
            return '';
        }

        const clone =
            document.body
                .cloneNode(
                    true
                );

        clone
            .querySelector(
                '#' +
                ROOT_ID
            )
            ?.remove();

        return String(
            clone.textContent ||
            ''
        )
            .replace(
                /\s+/g,
                ' '
            )
            .trim();
    }

    function pageLower() {
        return norm(
            pageText()
        );
    }


    // ============================================================
    // TORN API
    // ============================================================

    async function rawApi(
        path,
        params = {},
        keyOverride = null
    ) {
        const key =
            keyOverride !==
                null
                ? keyOverride
                : getKey();

        if (
            !key
        ) {
            throw new Error(
                'No API key configured.'
            );
        }

        const url =
            new URL(
                API_BASE +
                path
            );

        for (
            const [
                name,
                value
            ]
            of Object.entries(
                params
            )
        ) {
            url.searchParams.set(
                name,
                String(
                    value
                )
            );
        }

        url.searchParams.set(
            'key',
            key
        );

        const response =
            await fetch(
                url.toString()
            );

        if (
            !response.ok
        ) {
            throw new Error(
                'Torn API HTTP ' +
                response.status +
                '.'
            );
        }

        const data =
            await response.json();

        if (
            data?.error
        ) {
            const code =
                Number(
                    data.error.code
                );

            const message =
                data.error.error ||
                'Torn API error.';

            /*
             * Only remove the stored key when this request used the
             * saved key. A bad candidate entered in Change Key must
             * not destroy the currently working saved key.
             */
            if (
                keyOverride ===
                    null &&
                KEY_STOP_ERROR_CODES.has(
                    code
                )
            ) {
                removeStoredKey();

                apiKeyNotice =
                    keyStopMessage(
                        code,
                        message
                    );

                settingsOpen =
                    false;

                changeKeyOpen =
                    false;

                inventoryOpen =
                    false;
            }

            const error =
                new Error(
                    message
                );

            error.tornCode =
                code;

            throw error;
        }

        return data;
    }


    // ============================================================
    // YATA REQUEST
    // ============================================================

    function gmJson(url) {
        return new Promise(
            (
                resolve,
                reject
            ) => {
                if (
                    typeof GM_xmlhttpRequest ===
                    'function'
                ) {
                    GM_xmlhttpRequest({
                        method:
                            'GET',

                        url,

                        headers: {
                            Accept:
                                'application/json'
                        },

                        timeout:
                            20000,

                        onload(
                            response
                        ) {
                            if (
                                response.status <
                                    200 ||
                                response.status >=
                                    300
                            ) {
                                reject(
                                    new Error(
                                        'YATA HTTP ' +
                                        response.status
                                    )
                                );

                                return;
                            }

                            try {
                                resolve(
                                    JSON.parse(
                                        response.responseText ||
                                        '{}'
                                    )
                                );
                            }
                            catch (
                                error
                            ) {
                                reject(
                                    error
                                );
                            }
                        },

                        onerror() {
                            reject(
                                new Error(
                                    'Could not reach YATA.'
                                )
                            );
                        },

                        ontimeout() {
                            reject(
                                new Error(
                                    'YATA request timed out.'
                                )
                            );
                        }
                    });

                    return;
                }

                fetch(
                    url
                )
                    .then(
                        response => {
                            if (
                                !response.ok
                            ) {
                                throw new Error(
                                    'YATA HTTP ' +
                                    response.status
                                );
                            }

                            return response.json();
                        }
                    )
                    .then(
                        resolve
                    )
                    .catch(
                        reject
                    );
            }
        );
    }


    // ============================================================
    // INVENTORY API
    // ============================================================

    async function fetchInventoryForKey(
        key
    ) {
        const result =
            emptyInventory();

        for (
            const category
            of [
                'Flower',
                'Plushie'
            ]
        ) {
            const data =
                await rawApi(
                    '/user/inventory',
                    {
                        cat:
                            category,

                        limit:
                            250
                    },
                    key
                );

            const items =
                data
                    ?.inventory
                    ?.items;

            if (
                !Array.isArray(
                    items
                )
            ) {
                throw new Error(
                    'Unexpected inventory response.'
                );
            }

            for (
                const entry
                of items
            ) {
                const known =
                    MUSEUM_ITEMS.find(
                        item =>
                            norm(
                                item.name
                            ) ===
                            norm(
                                entry.name
                            )
                    );

                if (
                    known
                ) {
                    result[
                        known.name
                    ] =
                        Number(
                            entry.amount
                        ) ||
                        0;
                }
            }
        }

        return result;
    }

    async function refreshInventory(
        forceRender = true
    ) {
        if (
            !getKey() ||
            inventoryBusy
        ) {
            return;
        }

        inventoryBusy =
            true;

        try {
            saveInventory(
                await fetchInventoryForKey(
                    getKey()
                )
            );
        }
        catch (
            error
        ) {
            console.error(
                APP,
                error
            );
        }

        inventoryBusy =
            false;

        if (
            forceRender
        ) {
            render();
        }
    }


    // ============================================================
    // YATA FOREIGN STOCK
    // ============================================================

    function normalizeYata(raw) {
        const result = {};

        for (
            const [
                code,
                countryData
            ]
            of Object.entries(
                raw?.stocks ||
                {}
            )
        ) {
            const country =
                YATA_COUNTRY[
                    code
                ];

            if (
                !country
            ) {
                continue;
            }

            result[
                country
            ] = {
                update:
                    Number(
                        countryData
                            ?.update ||
                        0
                    ),

                items:
                    (
                        countryData
                            ?.stocks ||
                        []
                    )
                        .map(
                            item => ({
                                id:
                                    Number(
                                        item.id
                                    ),

                                name:
                                    String(
                                        item.name ||
                                        ''
                                    ),

                                quantity:
                                    Math.max(
                                        0,
                                        Number(
                                            item.quantity
                                        ) ||
                                        0
                                    ),

                                cost:
                                    Math.max(
                                        0,
                                        Number(
                                            item.cost
                                        ) ||
                                        0
                                    ),

                                nextRestock:
                                    item.nextRestock ||
                                    null
                            })
                        )
            };
        }

        return result;
    }

    function yataCacheAgeText() {
        const cache =
            readJson(
                STORAGE.yata
            );

        if (
            !cache ||
            !Number.isFinite(
                cache.updated
            )
        ) {
            return 'unknown age';
        }

        const ageMinutes =
            Math.max(
                0,
                Math.floor(
                    (
                        Date.now() -
                        cache.updated
                    ) /
                    60000
                )
            );

        if (
            ageMinutes < 1
        ) {
            return 'less than 1m old';
        }

        return ageMinutes +
            'm old';
    }

    async function getYata(
        force = false
    ) {
        if (
            !force
        ) {
            const fresh =
                cacheGet(
                    STORAGE.yata,
                    YATA_TTL
                );

            if (
                fresh
            ) {
                yataWarning =
                    '';

                return fresh;
            }

            if (
                Date.now() <
                yataRetryAfter
            ) {
                const stale =
                    cacheGet(
                        STORAGE.yata
                    );

                if (
                    stale
                ) {
                    return stale;
                }
            }
        }

        try {
            const data =
                normalizeYata(
                    await gmJson(
                        YATA_URL
                    )
                );

            cacheSet(
                STORAGE.yata,
                data
            );

            yataWarning =
                '';

            yataRetryAfter =
                0;

            return data;
        }
        catch (
            error
        ) {
            const stale =
                cacheGet(
                    STORAGE.yata
                );

            if (
                stale
            ) {
                yataRetryAfter =
                    Date.now() +
                    YATA_FAILURE_RETRY_MS;

                yataWarning =
                    'YATA temporarily unavailable (' +
                    String(
                        error?.message ||
                        error ||
                        'request failed'
                    ) +
                    '). Using cached data — ' +
                    yataCacheAgeText() +
                    '.';

                console.warn(
                    APP,
                    yataWarning
                );

                return stale;
            }

            yataRetryAfter =
                Date.now() +
                YATA_FAILURE_RETRY_MS;

            throw error;
        }
    }

    function yataData() {
        return (
            cacheGet(
                STORAGE.yata
            ) ||
            {}
        );
    }

    function yataItem(
        country,
        name
    ) {
        const target =
            norm(
                name
            );

        return (
            yataData()
                ?.[country]
                ?.items
                ?.find(
                    item =>
                        norm(
                            item.name
                        ) ===
                        target
                ) ||
            null
        );
    }


    // ============================================================
    // TRUSTED ITEM MARKET PRICING
    // ============================================================

    async function fetchTrustedSalePrice(
        itemId,
        itemName
    ) {
        if (
            !Number.isFinite(
                Number(
                    itemId
                )
            )
        ) {
            throw new Error(
                'Missing market item ID for ' +
                itemName +
                '.'
            );
        }

        const listings = [];

        let offset =
            0;

        let accumulatedUnits =
            0;

        for (
            let page = 0;
            page <
                MARKET_MAX_PAGES;
            page++
        ) {
            const data =
                await rawApi(
                    '/market/' +
                    itemId +
                    '/itemmarket',
                    {
                        limit:
                            100,

                        offset
                    }
                );

            const pageListings =
                data
                    ?.itemmarket
                    ?.listings;

            if (
                !Array.isArray(
                    pageListings
                ) ||
                pageListings.length ===
                    0
            ) {
                break;
            }

            for (
                const listing
                of pageListings
            ) {
                const price =
                    Number(
                        listing?.price
                    );

                const amount =
                    Math.max(
                        0,
                        Number(
                            listing?.amount
                        ) ||
                        0
                    );

                if (
                    Number.isFinite(
                        price
                    ) &&
                    price > 0 &&
                    amount > 0
                ) {
                    listings.push({
                        price,
                        amount
                    });

                    accumulatedUnits +=
                        amount;
                }
            }

            if (
                accumulatedUnits >=
                MARKET_TARGET_UNITS
            ) {
                break;
            }

            const next =
                data
                    ?._metadata
                    ?.next;

            if (
                !next
            ) {
                break;
            }

            offset +=
                pageListings.length;

            /*
             * Small spacing between pages keeps requests civilized
             * when a thin market requires several pages.
             */
            await sleep(
                80
            );
        }

        if (
            listings.length ===
            0
        ) {
            throw new Error(
                'No Item Market listings found for ' +
                itemName +
                '.'
            );
        }

        listings.sort(
            (a, b) =>
                a.price -
                b.price
        );

        const totalAvailable =
            listings.reduce(
                (
                    total,
                    listing
                ) =>
                    total +
                    listing.amount,
                0
            );

        const sampledUnits =
            Math.min(
                MARKET_TARGET_UNITS,
                totalAvailable
            );

        if (
            sampledUnits <= 0
        ) {
            throw new Error(
                'No usable market units found for ' +
                itemName +
                '.'
            );
        }

        const targetUnit =
            Math.max(
                1,
                Math.ceil(
                    sampledUnits *
                    MARKET_DEPTH_PERCENT
                )
            );

        let running =
            0;

        let estimatedSaleValue =
            listings[
                listings.length -
                1
            ].price;

        for (
            const listing
            of listings
        ) {
            running +=
                listing.amount;

            if (
                running >=
                targetUnit
            ) {
                estimatedSaleValue =
                    listing.price;

                break;
            }
        }

        const result = {
            id:
                Number(
                    itemId
                ),

            name:
                itemName,

            estimatedSaleValue,

            sampledUnits,

            targetUnit
        };

        storeTrustedPrice(
            itemId,
            result
        );

        return {
            ...result,

            updated:
                Date.now()
        };
    }

    async function getTrustedSalePrice(
        itemId,
        itemName,
        force = false
    ) {
        if (
            !force
        ) {
            const cached =
                cachedTrustedPrice(
                    itemId
                );

            if (
                cached
            ) {
                return cached;
            }
        }

        return fetchTrustedSalePrice(
            itemId,
            itemName
        );
    }

    function trustedPriceForId(
        itemId
    ) {
        return cachedTrustedPrice(
            itemId,
            true
        );
    }

    function relevantFinancialItems(
        mode
    ) {
        const unique =
            new Map();

        for (
            const countryData
            of Object.values(
                yataData()
            )
        ) {
            for (
                const item
                of countryData.items ||
                    []
            ) {
                if (
                    !Number.isFinite(
                        item.id
                    )
                ) {
                    continue;
                }

                if (
                    mode ===
                        'flushies' &&
                    FLUSHIE_NAMES.has(
                        item.name
                    )
                ) {
                    unique.set(
                        item.id,
                        {
                            id:
                                item.id,

                            name:
                                item.name
                        }
                    );
                }

                if (
                    mode ===
                        'xanax' &&
                    norm(
                        item.name
                    ) ===
                        'xanax'
                ) {
                    unique.set(
                        item.id,
                        {
                            id:
                                item.id,

                            name:
                                item.name
                        }
                    );
                }
            }
        }

        const profile =
            getProfile();

        if (
            profile.method ===
                'business' &&
            !profile
                .businessTicketsFree
        ) {
            unique.set(
                BCT_ITEM_ID,
                {
                    id:
                        BCT_ITEM_ID,

                    name:
                        'Business Class Ticket'
                }
            );
        }

        return [
            ...unique.values()
        ];
    }

    async function ensureTrustedPrices(
        mode,
        force = false
    ) {
        const items =
            relevantFinancialItems(
                mode
            );

        for (
            const item
            of items
        ) {
            const existing =
                force
                    ? null
                    : cachedTrustedPrice(
                        item.id
                    );

            if (
                existing
            ) {
                continue;
            }

            await getTrustedSalePrice(
                item.id,
                item.name,
                force
            );

            /*
             * Do not burst dozens of requests at once.
             */
            await sleep(
                110
            );
        }
    }


    // ============================================================
    // DATA REFRESH
    // ============================================================

    async function ensureData(
        force = false
    ) {
        if (
            dataBusy ||
            !getKey()
        ) {
            return;
        }

        dataBusy =
            true;

        dataError =
            '';

        try {
            await getYata(
                force
            );

            const mode =
                getMode();

            if (
                mode ===
                    'flushies' ||
                mode ===
                    'xanax'
            ) {
                await ensureTrustedPrices(
                    mode,
                    force
                );
            }

            if (
                mode ===
                'museum'
            ) {
                const inventory =
                    getInventoryCache();

                if (
                    force ||
                    !inventory.updated ||
                    Date.now() -
                        inventory.updated >
                        INVENTORY_TTL
                ) {
                    await refreshInventory(
                        false
                    );
                }
            }
        }
        catch (
            error
        ) {
            dataError =
                String(
                    error?.message ||
                    error ||
                    'Could not load optimizer data.'
                );

            console.error(
                APP,
                error
            );
        }

        dataBusy =
            false;

        lastSignature =
            '';

        render();
    }


    // ============================================================
    // MUSEUM OPTIMIZER
    // ============================================================

    function itemType(name) {
        const item =
            MUSEUM_ITEMS.find(
                entry =>
                    entry.name ===
                    name
            );

        return item?.type ||
            null;
    }

    function typeItems(type) {
        return MUSEUM_ITEMS
            .filter(
                item =>
                    item.type ===
                    type
            );
    }

    function quantity(name) {
        return Number(
            getInventoryCache()
                .items[
                    name
                ] ||
            0
        );
    }

    function typeMinimum(type) {
        const values =
            typeItems(
                type
            )
                .map(
                    item =>
                        quantity(
                            item.name
                        )
                );

        return values.length
            ? Math.min(
                ...values
            )
            : 0;
    }

    function typeProgress(type) {
        return (
            typeMinimum(
                type
            ) *
            typeItems(
                type
            ).length
        );
    }

    function efficiency(type) {
        const count =
            typeItems(
                type
            ).length;

        return count
            ? 10 /
                count
            : 0;
    }

    function museumPriority(name) {
        const type =
            itemType(
                name
            );

        if (
            !type
        ) {
            return null;
        }

        const amount =
            quantity(
                name
            );

        const minimum =
            typeMinimum(
                type
            );

        return {
            amount,

            minimum,

            bottleneck:
                amount ===
                minimum,

            progress:
                typeProgress(
                    type
                ),

            efficiency:
                efficiency(
                    type
                )
        };
    }

    function compareMuseumNames(
        aName,
        bName
    ) {
        const a =
            museumPriority(
                aName
            );

        const b =
            museumPriority(
                bName
            );

        if (
            !a &&
            !b
        ) {
            return 0;
        }

        if (
            !a
        ) {
            return 1;
        }

        if (
            !b
        ) {
            return -1;
        }

        if (
            a.bottleneck !==
            b.bottleneck
        ) {
            return a.bottleneck
                ? -1
                : 1;
        }

        if (
            a.progress !==
            b.progress
        ) {
            return (
                a.progress -
                b.progress
            );
        }

        if (
            a.efficiency !==
            b.efficiency
        ) {
            return (
                b.efficiency -
                a.efficiency
            );
        }

        if (
            a.amount !==
            b.amount
        ) {
            return (
                a.amount -
                b.amount
            );
        }

        return aName
            .localeCompare(
                bName
            );
    }

    function museumItemForCountry(
        country
    ) {
        const items =
            MUSEUM_COUNTRIES[
                country
            ] ||
            [];

        return (
            items
                .slice()
                .sort(
                    compareMuseumNames
                )[0] ||
            null
        );
    }


    // ============================================================
    // STOCK PREDICTION
    // ============================================================

    function stockVerdict(
        country,
        itemName
    ) {
        const countryData =
            yataData()
                ?.[country];

        const item =
            yataItem(
                country,
                itemName
            );

        const profile =
            getProfile();

        const arrival =
            effectiveMinutes(
                country
            );

        const now =
            Date.now();

        if (
            !countryData ||
            !item
        ) {
            return {
                safe:
                    false,

                state:
                    'unknown',

                label:
                    'STOCK UNKNOWN',

                detail:
                    'No current YATA report.'
            };
        }

        const ageMs =
            countryData.update
                ? Math.max(
                    0,
                    now -
                    countryData.update *
                        1000
                )
                : Infinity;

        const ageMinutes =
            Math.floor(
                ageMs /
                    60000
            );

        const stale =
            ageMs >
            STOCK_STALE_MS;

        let restock =
            null;

        if (
            item.nextRestock
        ) {
            const time =
                Date.parse(
                    item.nextRestock
                );

            if (
                Number.isFinite(
                    time
                )
            ) {
                restock =
                    Math.max(
                        0,
                        (
                            time -
                            now
                        ) /
                        60000
                    );
            }
        }

        if (
            item.quantity <=
            0
        ) {
            if (
                restock !==
                    null &&
                restock <=
                    arrival &&
                !stale
            ) {
                return {
                    safe:
                        true,

                    state:
                        'expected_in_stock',

                    label:
                        'EXPECTED IN STOCK',

                    detail:
                        'Restock expected ~' +
                        duration(
                            arrival -
                            restock
                        ) +
                        ' before arrival.'
                };
            }

            const wait =
                restock !==
                    null
                    ? Math.max(
                        0,
                        restock -
                        arrival
                    )
                    : null;

            return {
                safe:
                    false,

                state:
                    'expected_out_of_stock',

                label:
                    'EXPECTED OUT OF STOCK',

                detail:
                    wait !==
                        null
                        ? (
                            'Estimated wait after arrival: ~' +
                            duration(
                                wait
                            ) +
                            '.'
                        )
                        : (
                            'Currently sold out; restock time unknown.'
                        )
            };
        }

        if (
            stale
        ) {
            return {
                safe:
                    false,

                state:
                    'unknown',

                label:
                    'STOCK UNKNOWN',

                detail:
                    'YATA report is ' +
                    ageMinutes +
                    'm old.'
            };
        }

        if (
            item.quantity <
            profile.capacity
        ) {
            return {
                safe:
                    false,

                state:
                    'expected_out_of_stock',

                label:
                    'EXPECTED OUT OF STOCK',

                detail:
                    item.quantity
                        .toLocaleString() +
                    ' reported, below your ' +
                    profile.capacity +
                    '-item capacity.'
            };
        }

        return {
            safe:
                true,

            state:
                'expected_in_stock',

            label:
                'EXPECTED IN STOCK',

            detail:
                item.quantity
                    .toLocaleString() +
                ' reported · ' +
                ageMinutes +
                'm old.'
        };
    }


    // ============================================================
    // FINANCIAL CALCULATIONS
    // ============================================================

    function bctPrice() {
        return (
            trustedPriceForId(
                BCT_ITEM_ID
            )
                ?.estimatedSaleValue ||
            0
        );
    }

    function travelExpense(country) {
        const destination =
            TRAVEL[
                country
            ];

        const profile =
            getProfile();

        if (
            !destination
        ) {
            return 0;
        }

        /*
         * Standard airfare is charged for each leg.
         */
        if (
            profile.method ===
            'standard'
        ) {
            return (
                destination.cost *
                2
            );
        }

        /*
         * One Business Class Ticket covers the return trip.
         */
        if (
            profile.method ===
                'business' &&
            !profile
                .businessTicketsFree
        ) {
            return bctPrice();
        }

        /*
         * Airstrip and WLT flights are treated as free.
         */
        return 0;
    }

    function purchaseCash(
        foreignCost
    ) {
        return (
            foreignCost *
            getProfile()
                .capacity
        );
    }

    function xanaxRecommendedCash(
        foreignCost
    ) {
        const required =
            purchaseCash(
                foreignCost
            );

        const cushion =
            Math.max(
                required *
                    XANAX_CASH_BUFFER_PERCENT,
                XANAX_CASH_BUFFER_MIN
            );

        return Math.ceil(
            required +
            cushion
        );
    }

    function profitFor(
        country,
        foreignCost,
        estimatedSaleValue
    ) {
        return (
            (
                estimatedSaleValue -
                foreignCost
            ) *
            getProfile()
                .capacity -
            travelExpense(
                country
            )
        );
    }

    function profitPerHour(
        country,
        foreignCost,
        estimatedSaleValue
    ) {
        const hours =
            (
                effectiveMinutes(
                    country
                ) *
                2
            ) /
            60;

        return hours >
            0
            ? (
                profitFor(
                    country,
                    foreignCost,
                    estimatedSaleValue
                ) /
                hours
            )
            : -Infinity;
    }


    // ============================================================
    // CANDIDATES
    // ============================================================

    function museumCandidates() {
        return Object.keys(
            MUSEUM_COUNTRIES
        )
            .map(
                country => {
                    const item =
                        museumItemForCountry(
                            country
                        );

                    if (
                        !item
                    ) {
                        return null;
                    }

                    return {
                        country,

                        item,

                        minutes:
                            effectiveMinutes(
                                country
                            ),

                        bucket:
                            bucket(
                                country
                            ),

                        verdict:
                            stockVerdict(
                                country,
                                item
                            )
                    };
                }
            )
            .filter(
                Boolean
            );
    }

    function compareMuseumCandidates(
        a,
        b
    ) {
        const difference =
            compareMuseumNames(
                a.item,
                b.item
            );

        return difference !==
            0
            ? difference
            : (
                a.minutes -
                b.minutes
            );
    }

    function financialCandidates(
        mode
    ) {
        const result = [];

        for (
            const [
                country,
                countryData
            ]
            of Object.entries(
                yataData()
            )
        ) {
            if (
                !TRAVEL[
                    country
                ]
            ) {
                continue;
            }

            for (
                const foreign
                of countryData.items ||
                    []
            ) {
                if (
                    mode ===
                        'flushies' &&
                    !FLUSHIE_NAMES.has(
                        foreign.name
                    )
                ) {
                    continue;
                }

                if (
                    mode ===
                        'xanax' &&
                    norm(
                        foreign.name
                    ) !==
                        'xanax'
                ) {
                    continue;
                }

                const pricing =
                    trustedPriceForId(
                        foreign.id
                    );

                if (
                    !pricing ||
                    !Number.isFinite(
                        pricing
                            .estimatedSaleValue
                    ) ||
                    pricing
                        .estimatedSaleValue <=
                        0
                ) {
                    continue;
                }

                const estimatedSaleValue =
                    pricing
                        .estimatedSaleValue;

                result.push({
                    id:
                        foreign.id,

                    country,

                    item:
                        foreign.name,

                    foreignCost:
                        foreign.cost,

                    estimatedSaleValue,

                    sampledUnits:
                        pricing
                            .sampledUnits,

                    marketTargetUnit:
                        pricing
                            .targetUnit,

                    requiredCash:
                        purchaseCash(
                            foreign.cost
                        ),

                    recommendedCash:
                        mode ===
                            'xanax'
                            ? xanaxRecommendedCash(
                                foreign.cost
                            )
                            : null,

                    profit:
                        profitFor(
                            country,
                            foreign.cost,
                            estimatedSaleValue
                        ),

                    profitPerHour:
                        profitPerHour(
                            country,
                            foreign.cost,
                            estimatedSaleValue
                        ),

                    minutes:
                        effectiveMinutes(
                            country
                        ),

                    bucket:
                        bucket(
                            country
                        ),

                    verdict:
                        stockVerdict(
                            country,
                            foreign.name
                        )
                });
            }
        }

        return result.sort(
            (
                a,
                b
            ) => {
                if (
                    b.profitPerHour !==
                    a.profitPerHour
                ) {
                    return (
                        b.profitPerHour -
                        a.profitPerHour
                    );
                }

                if (
                    b.profit !==
                    a.profit
                ) {
                    return (
                        b.profit -
                        a.profit
                    );
                }

                return (
                    a.minutes -
                    b.minutes
                );
            }
        );
    }

    function candidatesForMode(
        mode
    ) {
        if (
            mode ===
            'museum'
        ) {
            return museumCandidates()
                .sort(
                    compareMuseumCandidates
                );
        }

        return financialCandidates(
            mode
        );
    }

    function pairFor(
        mode,
        desiredBucket
    ) {
        const candidates =
            candidatesForMode(
                mode
            )
                .filter(
                    candidate =>
                        candidate.bucket ===
                        desiredBucket
                );

        const best =
            candidates[
                0
            ] ||
            null;

        const recommended =
            best &&
            !best
                .verdict
                .safe
                ? (
                    candidates.find(
                        candidate =>
                            candidate
                                .verdict
                                .safe
                    ) ||
                    null
                )
                : null;

        return {
            best,
            recommended
        };
    }

    function countryCandidate(
        mode,
        country
    ) {
        return (
            candidatesForMode(
                mode
            )
                .find(
                    candidate =>
                        candidate.country ===
                        country
                ) ||
            null
        );
    }


    // ============================================================
    // PAGE DETECTION
    // ============================================================

    function isTravelAgencyPage() {
        const href =
            location.href
                .toLowerCase();

        const title =
            document.title
                .toLowerCase();

        return (
            href.includes(
                'travelagency.php'
            ) ||
            title.includes(
                'travel agency'
            )
        );
    }

    function flightRouteText() {
        const section =
            document.querySelector(
                'section[class*="flightProgressSection"]'
            );

        return String(
            section?.innerText ||
            section?.textContent ||
            ''
        )
            .replace(
                /\s+/g,
                ' '
            )
            .trim();
    }

    function isTravelPage() {
        const route =
            flightRouteText();

        return Boolean(
            route &&
            /remaining flight time/i
                .test(
                    route
                )
        );
    }

    function travelState() {
        const route =
            norm(
                flightRouteText()
            );

        for (
            const [
                country,
                destination
            ]
            of Object.entries(
                TRAVEL
            )
        ) {
            const city =
                norm(
                    destination.city
                );

            if (
                route.includes(
                    'torn to ' +
                    city
                )
            ) {
                rememberCountry(
                    country
                );

                return {
                    direction:
                        'outbound',

                    country
                };
            }

            if (
                route.includes(
                    city +
                    ' to torn'
                )
            ) {
                return {
                    direction:
                        'return',

                    country
                };
            }
        }

        if (
            route.startsWith(
                'torn to '
            )
        ) {
            return {
                direction:
                    'outbound',

                country:
                    null
            };
        }

        if (
            route.includes(
                ' to torn'
            )
        ) {
            return {
                direction:
                    'return',

                country:
                    null
            };
        }

        return {
            direction:
                'unknown',

            country:
                null
        };
    }

    function isForeignShopPage() {
        const text =
            pageLower();

        const store =
            text.includes(
                'general store'
            ) ||
            text.includes(
                'black market'
            );

        return (
            store &&
            text.includes(
                'stock'
            ) &&
            text.includes(
                'amount'
            ) &&
            text.includes(
                'buy'
            )
        );
    }

    function shoppingCountry() {
        const text =
            pageText();

        const direct =
            text.match(
                /you\s+are\s+in\s+([a-z ]+?)\s+and\s+have/i
            );

        if (
            direct?.[
                1
            ]
        ) {
            const found =
                Object.keys(
                    TRAVEL
                )
                    .find(
                        country =>
                            norm(
                                country
                            ) ===
                            norm(
                                direct[
                                    1
                                ]
                            )
                    );

            if (
                found
            ) {
                rememberCountry(
                    found
                );

                return found;
            }
        }

        return rememberedCountry();
    }

    function pageMode() {
        if (
            isForeignShopPage()
        ) {
            const country =
                shoppingCountry();

            return country
                ? {
                    mode:
                        'shop',

                    country
                }
                : {
                    mode:
                        'hidden'
                };
        }

        if (
            isTravelPage()
        ) {
            return {
                mode:
                    'travel',

                ...travelState()
            };
        }

        if (
            isTravelAgencyPage()
        ) {
            return {
                mode:
                    'agency'
            };
        }

        return {
            mode:
                'hidden'
        };
    }


    // ============================================================
    // FOREIGN STORE LIVE DATA
    // ============================================================

    function carryStatus() {
        const match =
            pageText()
                .match(
                    /purchased\s+([\d,]+)\s*\/\s*([\d,]+)\s+items/i
                );

        if (
            !match
        ) {
            return null;
        }

        const purchased =
            num(
                match[
                    1
                ]
            );

        const capacity =
            num(
                match[
                    2
                ]
            );

        if (
            purchased ===
                null ||
            capacity ===
                null
        ) {
            return null;
        }

        return {
            purchased,

            capacity,

            remaining:
                Math.max(
                    0,
                    capacity -
                    purchased
                )
        };
    }

    function liveStock(name) {
        const target =
            norm(
                name
            );

        const rows =
            [
                ...document.querySelectorAll(
                    'div[class*="row"]'
                )
            ]
                .filter(
                    row =>
                        !row.closest(
                            '#' +
                            ROOT_ID
                        )
                );

        let best =
            null;

        for (
            const row
            of rows
        ) {
            const text =
                String(
                    row.innerText ||
                    row.textContent ||
                    ''
                ).trim();

            if (
                !text ||
                !norm(
                    text
                ).includes(
                    target
                )
            ) {
                continue;
            }

            if (
                !best ||
                text.length <
                    best.text.length
            ) {
                best = {
                    row,
                    text
                };
            }
        }

        if (
            !best
        ) {
            return null;
        }

        if (
            norm(
                best.row
                    .className
            ).includes(
                'soldout'
            )
        ) {
            return 0;
        }

        const match =
            best.text
                .match(
                    /stock\s+([\d,]+)/i
                );

        return match
            ? num(
                match[
                    1
                ]
            )
            : null;
    }


    // ============================================================
    // BUY CONFIRMATION FREEZE
    // ============================================================

    function purchaseConfirmationVisible() {
        const text =
            pageLower();

        if (
            !/buy\s+[\d,]+\s*x\s+.+?\s+for\s+\$[\d,]+\s*\?/i
                .test(
                    text
                )
        ) {
            return false;
        }

        const controls =
            [
                ...document.querySelectorAll(
                    'button, a, span, div'
                )
            ]
                .filter(
                    element =>
                        !element.closest(
                            '#' +
                            ROOT_ID
                        )
                );

        return (
            controls.some(
                element =>
                    norm(
                        element.innerText ||
                        element.textContent
                    ) ===
                    'yes'
            ) &&
            controls.some(
                element =>
                    norm(
                        element.innerText ||
                        element.textContent
                    ) ===
                    'no'
            )
        );
    }

    function startBuyFreeze() {
        if (
            buyFrozen
        ) {
            return;
        }

        buyFrozen =
            true;

        freezeStarted =
            Date.now();

        confirmationSeen =
            false;
    }

    function updateBuyFreeze() {
        if (
            !buyFrozen
        ) {
            return;
        }

        if (
            Date.now() -
                freezeStarted >
            BUY_FREEZE_MS
        ) {
            buyFrozen =
                false;

            lastSignature =
                '';

            render();

            return;
        }

        if (
            purchaseConfirmationVisible()
        ) {
            confirmationSeen =
                true;

            return;
        }

        if (
            confirmationSeen
        ) {
            buyFrozen =
                false;

            lastSignature =
                '';

            render();
        }
    }

    function handleBuyCapture(
        event
    ) {
        if (
            buyFrozen ||
            pageMode().mode !==
                'shop'
        ) {
            return;
        }

        let node =
            event.target instanceof
                Element
                ? event.target
                : event.target
                    ?.parentElement;

        for (
            let depth = 0;
            node &&
            depth < 6;
            depth++,
            node =
                node.parentElement
        ) {
            if (
                node.closest(
                    '#' +
                    ROOT_ID
                )
            ) {
                return;
            }

            if (
                norm(
                    node.innerText ||
                    node.textContent
                ) ===
                'buy'
            ) {
                startBuyFreeze();
                return;
            }
        }
    }


    // ============================================================
    // STYLE
    // ============================================================

    function injectStyle() {
        if (
            document.getElementById(
                STYLE_ID
            )
        ) {
            return;
        }

        const style =
            document.createElement(
                'style'
            );

        style.id =
            STYLE_ID;

        style.textContent = `
            #${ROOT_ID} {
                position: fixed;
                right: 18px;
                bottom: 18px;
                width: 300px;
                max-width: calc(100vw - 20px);
                max-height: calc(100vh - 20px);
                display: flex;
                flex-direction: column;
                z-index: 2147483647;
                background: #1b1b1d;
                color: #eee;
                border: 1px solid #555;
                border-radius: 9px;
                box-shadow: 0 5px 20px rgba(0,0,0,.55);
                font-family: Arial, sans-serif;
                overflow: hidden;
            }

            #${ROOT_ID}.hidden {
                display: none !important;
            }

            #${ROOT_ID}.mini {
                width: 46px !important;
                height: 34px !important;
                right: 0 !important;
                border-radius: 6px 0 0 6px;
            }

            #${ROOT_ID} * {
                box-sizing: border-box;
            }

            #${ROOT_ID} .head {
                display: flex;
                justify-content: space-between;
                align-items: center;
                padding: 8px 10px;
                background: #29292c;
                border-bottom: 1px solid #3b3b3d;
                cursor: move;
                user-select: none;
                touch-action: none;
            }

            #${ROOT_ID} .title {
                font-size: 13px;
                font-weight: bold;
                color: #fff;
            }

            #${ROOT_ID} .actions {
                display: flex;
                gap: 5px;
            }

            #${ROOT_ID} button {
                font-family: inherit;
            }

            #${ROOT_ID} .icon {
                width: 29px;
                height: 29px;
                border: 0;
                border-radius: 5px;
                background: #3d3d40;
                color: #ddd;
                cursor: pointer;
            }

            #${ROOT_ID} .miniButton {
                width: 46px;
                height: 34px;
                display: flex;
                align-items: center;
                justify-content: center;
                background: #29292c;
                color: #fff;
                font-size: 11px;
                font-weight: bold;
                cursor: pointer;
            }

            #${ROOT_ID} .modes {
                display: flex;
                gap: 4px;
                padding: 7px;
                background: #202023;
                border-bottom: 1px solid #3b3b3d;
            }

            #${ROOT_ID} .mode {
                flex: 1;
                padding: 7px 3px;
                border: 1px solid #4a4a4d;
                border-radius: 5px;
                background: #303034;
                color: #bbb;
                font-size: 11px;
                font-weight: bold;
                cursor: pointer;
            }

            #${ROOT_ID} .mode.active {
                background: #505055;
                color: #fff;
                border-color: #777;
            }

            #${ROOT_ID} .body,
            #${ROOT_ID} .settings {
                padding: 11px;
            }

            #${ROOT_ID} .scroll {
                max-height: 350px;
                overflow-y: auto;
            }

            #${ROOT_ID}.sized .scroll {
                flex: 1 1 auto;
                min-height: 0;
                max-height: none;
            }

            #${ROOT_ID}.sized .settings {
                flex: 0 1 auto;
                max-height: 45%;
            }

            #${ROOT_ID} .settings {
                border-top: 1px solid #4b4b50;
                background: #29292d;
                max-height: 360px;
                overflow-y: auto;
            }

            #${ROOT_ID} .label {
                color: #999;
                font-size: 10px;
                font-weight: bold;
                letter-spacing: .8px;
                margin-bottom: 3px;
            }

            #${ROOT_ID} .main {
                color: #fff;
                font-size: 17px;
                font-weight: bold;
                line-height: 1.3;
            }

            #${ROOT_ID} .sub {
                color: #bbb;
                font-size: 12px;
                line-height: 1.4;
                margin-top: 3px;
            }

            #${ROOT_ID} .buy {
                color: #a5e7a8;
            }

            #${ROOT_ID} .warn {
                color: #efc36d;
            }

            #${ROOT_ID} .danger {
                color: #ef9b9b;
            }

            #${ROOT_ID} .divider {
                height: 1px;
                background: #3b3b3b;
                margin: 10px 0;
            }

            #${ROOT_ID} .stock {
                margin-top: 7px;
                padding: 6px 7px;
                border-radius: 5px;
                background: #27272a;
                font-size: 10px;
                line-height: 1.35;
            }

            #${ROOT_ID} .stock.safe {
                color: #a5e7a8;
            }

            #${ROOT_ID} .stock.bad {
                color: #ef9b9b;
            }

            #${ROOT_ID} .stock.unknown {
                color: #efc36d;
            }

            #${ROOT_ID} .moneybox {
                margin-top: 7px;
                padding: 7px;
                background: #242427;
                border-radius: 5px;
                font-size: 11px;
                line-height: 1.45;
                color: #ccc;
            }

            #${ROOT_ID} .buttons {
                display: flex;
                gap: 6px;
                margin-top: 8px;
                flex-wrap: wrap;
            }

            #${ROOT_ID} .button {
                flex: 1;
                min-width: 95px;
                padding: 8px;
                border: 0;
                border-radius: 5px;
                background: #444;
                color: #fff;
                cursor: pointer;
            }

            #${ROOT_ID} .button:disabled {
                cursor: default;
                opacity: 1;
            }

            #${ROOT_ID} .input,
            #${ROOT_ID} select,
            #${ROOT_ID} input[type=number] {
                width: 100%;
                padding: 8px;
                margin-top: 5px;
                background: #111;
                color: #fff;
                border: 1px solid #555;
                border-radius: 5px;
            }

            #${ROOT_ID} .field {
                display: block;
                margin-top: 9px;
                color: #aaa;
                font-size: 11px;
            }

            #${ROOT_ID} .check {
                display: flex;
                align-items: center;
                gap: 7px;
                margin-top: 9px;
                color: #bbb;
                font-size: 11px;
            }

            #${ROOT_ID} .check input {
                width: auto;
            }

            #${ROOT_ID} .note {
                margin-top: 7px;
                color: #888;
                font-size: 10px;
            }

            #${ROOT_ID} .apiDisclosure {
                margin-top: 10px;
                padding: 8px;
                border: 1px solid #4a4a4f;
                border-radius: 5px;
                background: #222225;
                color: #aaa;
                font-size: 10px;
                line-height: 1.45;
            }

            #${ROOT_ID} .apiDisclosure strong {
                color: #ddd;
            }

            #${ROOT_ID} .apiNotice {
                margin-top: 8px;
                padding: 7px;
                border-radius: 5px;
                background: #34282a;
                color: #efb0b0;
                font-size: 11px;
                line-height: 1.4;
            }

            #${ROOT_ID} .inventory {
                margin-top: 10px;
                padding: 8px;
                background: #111;
                border-radius: 6px;
                max-height: 260px;
                overflow-y: auto;
            }

            #${ROOT_ID} .invrow {
                display: flex;
                justify-content: space-between;
                gap: 8px;
                padding: 3px 0;
                color: #bbb;
                font-size: 11px;
                border-bottom: 1px solid #292929;
            }

            #${ROOT_ID} .resizeHandle {
                position: absolute;
                right: 0;
                bottom: 0;
                width: 22px;
                height: 22px;
                cursor: nwse-resize;
                touch-action: none;
                user-select: none;
                z-index: 5;
            }

            #${ROOT_ID} .resizeHandle::after {
                content: '';
                position: absolute;
                right: 4px;
                bottom: 4px;
                width: 10px;
                height: 10px;
                border-right: 2px solid #777;
                border-bottom: 2px solid #777;
            }

            #${ROOT_ID} .resizeHandle:hover::after {
                border-color: #bbb;
            }
        `;

        (
            document.head ||
            document.documentElement
        )
            .appendChild(
                style
            );
    }


    // ============================================================
    // PANEL
    // ============================================================

    function root() {
        let panel =
            document.getElementById(
                ROOT_ID
            );

        if (
            !panel
        ) {
            panel =
                document.createElement(
                    'div'
                );

            panel.id =
                ROOT_ID;

            document.body
                .appendChild(
                    panel
                );
        }

        return panel;
    }

    function positionPanel(
        panel,
        left,
        top
    ) {
        const maxLeft =
            Math.max(
                0,
                innerWidth -
                panel.offsetWidth
            );

        const maxTop =
            Math.max(
                0,
                innerHeight -
                panel.offsetHeight
            );

        const x =
            Math.min(
                Math.max(
                    0,
                    left
                ),
                maxLeft
            );

        const y =
            Math.min(
                Math.max(
                    0,
                    top
                ),
                maxTop
            );

        panel.style.left =
            x +
            'px';

        panel.style.top =
            y +
            'px';

        panel.style.right =
            'auto';

        panel.style.bottom =
            'auto';

        return {
            left:
                x,

            top:
                y
        };
    }

    function savedPosition() {
        return readJson(
            STORAGE.position
        );
    }

    function applyPosition(
        panel
    ) {
        const saved =
            savedPosition();

        if (
            saved &&
            Number.isFinite(
                saved.left
            ) &&
            Number.isFinite(
                saved.top
            )
        ) {
            positionPanel(
                panel,
                saved.left,
                saved.top
            );
        }
    }


    // ============================================================
    // DRAGGING
    // ============================================================

    function installDrag() {
        if (
            dragInstalled
        ) {
            return;
        }

        dragInstalled =
            true;

        document.addEventListener(
            'pointermove',
            event => {
                if (
                    !drag ||
                    drag.type !==
                        'pointer'
                ) {
                    return;
                }

                positionPanel(
                    drag.panel,
                    event.clientX -
                        drag.dx,
                    event.clientY -
                        drag.dy
                );

                event.preventDefault();
            }
        );

        document.addEventListener(
            'pointerup',
            finishDrag
        );

        document.addEventListener(
            'pointercancel',
            finishDrag
        );

        document.addEventListener(
            'touchmove',
            event => {
                if (
                    !drag ||
                    drag.type !==
                        'touch' ||
                    event.touches.length !==
                        1
                ) {
                    return;
                }

                const touch =
                    event.touches[
                        0
                    ];

                positionPanel(
                    drag.panel,
                    touch.clientX -
                        drag.dx,
                    touch.clientY -
                        drag.dy
                );

                event.preventDefault();
            },
            {
                passive:
                    false
            }
        );

        document.addEventListener(
            'touchend',
            finishDrag,
            {
                passive:
                    false
            }
        );

        document.addEventListener(
            'touchcancel',
            finishDrag,
            {
                passive:
                    false
            }
        );
    }

    function finishDrag() {
        if (
            !drag
        ) {
            return;
        }

        const rect =
            drag.panel
                .getBoundingClientRect();

        writeJson(
            STORAGE.position,
            {
                left:
                    rect.left,

                top:
                    rect.top
            }
        );

        drag =
            null;
    }

    function bindDrag(
        panel
    ) {
        const head =
            panel.querySelector(
                '.head'
            );

        if (
            !head
        ) {
            return;
        }

        head.onpointerdown =
            event => {
                if (
                    event.pointerType ===
                        'touch' ||
                    event.target.closest(
                        'button,input,a,select'
                    )
                ) {
                    return;
                }

                const rect =
                    panel
                        .getBoundingClientRect();

                drag = {
                    type:
                        'pointer',

                    panel,

                    dx:
                        event.clientX -
                        rect.left,

                    dy:
                        event.clientY -
                        rect.top
                };

                event.preventDefault();
            };

        head.ontouchstart =
            event => {
                if (
                    event.target.closest(
                        'button,input,a,select'
                    ) ||
                    event.touches.length !==
                        1
                ) {
                    return;
                }

                const touch =
                    event.touches[
                        0
                    ];

                const rect =
                    panel
                        .getBoundingClientRect();

                drag = {
                    type:
                        'touch',

                    panel,

                    dx:
                        touch.clientX -
                        rect.left,

                    dy:
                        touch.clientY -
                        rect.top
                };

                event.preventDefault();
            };
    }


    // ============================================================
    // RESIZING
    // ============================================================

    function installResize() {
        if (
            resizeInstalled
        ) {
            return;
        }

        resizeInstalled =
            true;

        document.addEventListener(
            'pointermove',
            event => {
                if (
                    !resizeState ||
                    resizeState.type !==
                        'pointer'
                ) {
                    return;
                }

                resizePanelToPointer(
                    event.clientX,
                    event.clientY
                );

                event.preventDefault();
            }
        );

        document.addEventListener(
            'pointerup',
            finishResize
        );

        document.addEventListener(
            'pointercancel',
            finishResize
        );

        document.addEventListener(
            'touchmove',
            event => {
                if (
                    !resizeState ||
                    resizeState.type !==
                        'touch' ||
                    event.touches.length !==
                        1
                ) {
                    return;
                }

                const touch =
                    event.touches[
                        0
                    ];

                resizePanelToPointer(
                    touch.clientX,
                    touch.clientY
                );

                event.preventDefault();
            },
            {
                passive:
                    false
            }
        );

        document.addEventListener(
            'touchend',
            finishResize,
            {
                passive:
                    false
            }
        );

        document.addEventListener(
            'touchcancel',
            finishResize,
            {
                passive:
                    false
            }
        );
    }

    function resizePanelToPointer(
        clientX,
        clientY
    ) {
        if (
            !resizeState
        ) {
            return;
        }

        const size =
            clampPanelSize(
                resizeState.startWidth +
                    (
                        clientX -
                        resizeState.startX
                    ),
                resizeState.startHeight +
                    (
                        clientY -
                        resizeState.startY
                    )
            );

        resizeState.panel.style.width =
            size.width +
            'px';

        resizeState.panel.style.height =
            '';

        resizeState.panel.style.maxHeight =
            size.height +
            'px';

        resizeState.requestedHeight =
            size.height;

        resizeState.panel
            .classList.add(
                'sized'
            );

        positionPanel(
            resizeState.panel,
            resizeState.left,
            resizeState.top
        );
    }

    function finishResize() {
        if (
            !resizeState
        ) {
            return;
        }

        const rect =
            resizeState.panel
                .getBoundingClientRect();

        savePanelSize(
            rect.width,
            resizeState.requestedHeight ||
                rect.height
        );

        writeJson(
            STORAGE.position,
            {
                left:
                    rect.left,

                top:
                    rect.top
            }
        );

        resizeState =
            null;
    }

    function beginResize(
        panel,
        clientX,
        clientY,
        type
    ) {
        const rect =
            panel
                .getBoundingClientRect();

        resizeState = {
            type,

            panel,

            startX:
                clientX,

            startY:
                clientY,

            startWidth:
                rect.width,

            startHeight:
                Math.max(
                    rect.height,
                    parseFloat(
                        panel.style.maxHeight
                    ) ||
                    rect.height
                ),

            requestedHeight:
                Math.max(
                    rect.height,
                    parseFloat(
                        panel.style.maxHeight
                    ) ||
                    rect.height
                ),

            left:
                rect.left,

            top:
                rect.top
        };

        panel.classList.add(
            'sized'
        );
    }

    function bindResize(
        panel
    ) {
        const handle =
            panel.querySelector(
                '#lmt-resize-handle'
            );

        if (
            !handle
        ) {
            return;
        }

        handle.onpointerdown =
            event => {
                if (
                    event.pointerType ===
                    'touch'
                ) {
                    return;
                }

                beginResize(
                    panel,
                    event.clientX,
                    event.clientY,
                    'pointer'
                );

                event.preventDefault();
                event.stopPropagation();
            };

        handle.ontouchstart =
            event => {
                if (
                    event.touches.length !==
                    1
                ) {
                    return;
                }

                const touch =
                    event.touches[
                        0
                    ];

                beginResize(
                    panel,
                    touch.clientX,
                    touch.clientY,
                    'touch'
                );

                event.preventDefault();
                event.stopPropagation();
            };
    }


    // ============================================================
    // UI
    // ============================================================

    function resizeHandleUI() {
        return `
            <div
                id="lmt-resize-handle"
                class="resizeHandle"
                title="Resize LostMaster Travel"
                aria-label="Resize LostMaster Travel"
            ></div>
        `;
    }

    function headerUI() {
        return `
            <div class="head">

                <div class="title">
                    ${APP}
                </div>

                <div class="actions">

                    <button
                        id="lmt-refresh"
                        class="icon"
                        title="Refresh"
                    >
                        ↻
                    </button>

                    <button
                        id="lmt-settings"
                        class="icon"
                        title="Settings"
                    >
                        ⚙
                    </button>

                    <button
                        id="lmt-minimize"
                        class="icon"
                        title="Minimize"
                    >
                        _
                    </button>

                </div>

            </div>
        `;
    }

    function modesUI() {
        const mode =
            getMode();

        return `
            <div class="modes">

                <button
                    class="mode ${
                        mode ===
                            'museum'
                            ? 'active'
                            : ''
                    }"
                    data-mode="museum"
                >
                    Museum
                </button>

                <button
                    class="mode ${
                        mode ===
                            'flushies'
                            ? 'active'
                            : ''
                    }"
                    data-mode="flushies"
                >
                    Flushies
                </button>

                <button
                    class="mode ${
                        mode ===
                            'xanax'
                            ? 'active'
                            : ''
                    }"
                    data-mode="xanax"
                >
                    Xanax
                </button>

            </div>
        `;
    }

    function apiDisclosureUI() {
        return `
            <div class="apiDisclosure">

                <strong>
                    API USE
                </strong>

                <div>
                    <strong>Storage:</strong>
                    Your Torn API key and LMT cache data are stored
                    locally in this browser only.
                </div>

                <div>
                    <strong>Sharing:</strong>
                    Your Torn API key is sent only to the official
                    Torn API. It is never sent to YATA or another
                    third party.
                </div>

                <div>
                    <strong>Purpose:</strong>
                    Museum inventory balancing, Flushies profit
                    estimates, Xanax travel estimates, and travel
                    recommendations.
                </div>

                <div>
                    <strong>Required access:</strong>
                    Minimal Access.
                </div>

                <div>
                    <strong>Torn API selections:</strong>
                    user → inventory; market → itemmarket.
                </div>

                <div>
                    LMT separately retrieves public foreign-stock
                    information from YATA without sending your Torn
                    API key.
                </div>

            </div>
        `;
    }

    function setupUI() {
        return `
            <div class="body">

                <div class="main">
                    Setup
                </div>

                <div class="sub">
                    Enter your Torn API key.
                </div>

                <input
                    id="lmt-key"
                    class="input"
                    type="text"
                    placeholder="API key"
                    autocomplete="off"
                >

                ${
                    apiKeyNotice
                        ? `
                            <div class="apiNotice">
                                ${esc(
                                    apiKeyNotice
                                )}
                            </div>
                        `
                        : ''
                }

                <div
                    id="lmt-key-status"
                    class="sub"
                ></div>

                ${apiDisclosureUI()}

                <div class="buttons">

                    <button
                        id="lmt-save-key-first"
                        class="button"
                    >
                        Save
                    </button>

                </div>

            </div>
        `;
    }

    function inventoryUI() {
        if (
            !inventoryOpen
        ) {
            return '';
        }

        if (
            inventoryBusy
        ) {
            return `
                <div class="inventory">
                    Checking Torn API inventory...
                </div>
            `;
        }

        const inventory =
            getInventoryCache();

        return (
            '<div class="inventory">' +

            MUSEUM_ITEMS
                .map(
                    item => `
                        <div class="invrow">
                            <span>
                                ${esc(
                                    item.name
                                )}
                            </span>

                            <span>
                                ${
                                    Number(
                                        inventory
                                            .items[
                                                item.name
                                            ] ||
                                        0
                                    )
                                }
                            </span>
                        </div>
                    `
                )
                .join(
                    ''
                ) +

            '</div>'
        );
    }

    function settingsUI() {
        if (
            !settingsOpen
        ) {
            return '';
        }

        if (
            changeKeyOpen
        ) {
            return `
                <div class="settings">

                    <div class="label">
                        CHANGE API KEY
                    </div>

                    <input
                        id="lmt-new-key"
                        class="input"
                        type="text"
                        placeholder="New API key"
                        autocomplete="off"
                    >

                    <div
                        id="lmt-key-status"
                        class="sub"
                    ></div>

                    ${apiDisclosureUI()}

                    <div class="buttons">

                        <button
                            id="lmt-save-new-key"
                            class="button"
                        >
                            Save
                        </button>

                        <button
                            id="lmt-cancel-key"
                            class="button"
                        >
                            Cancel
                        </button>

                    </div>

                </div>
            `;
        }

        const profile =
            getProfile();

        const saved =
            Date.now() <
            profileSavedUntil;

        return `
            <div class="settings">

                <div class="label">
                    TRAVEL PROFILE
                </div>

                <label class="field">
                    Travel type
                </label>

                <select
                    id="lmt-method"
                >

                    <option
                        value="standard"
                        ${
                            profile.method ===
                                'standard'
                                ? 'selected'
                                : ''
                        }
                    >
                        Standard
                    </option>

                    <option
                        value="airstrip"
                        ${
                            profile.method ===
                                'airstrip'
                                ? 'selected'
                                : ''
                        }
                    >
                        Airstrip
                    </option>

                    <option
                        value="wlt"
                        ${
                            profile.method ===
                                'wlt'
                                ? 'selected'
                                : ''
                        }
                    >
                        WLT
                    </option>

                    <option
                        value="business"
                        ${
                            profile.method ===
                                'business'
                                ? 'selected'
                                : ''
                        }
                    >
                        Business Class
                    </option>

                </select>

                <label class="check">

                    <input
                        id="lmt-book"
                        type="checkbox"
                        ${
                            profile.mailingBook
                                ? 'checked'
                                : ''
                        }
                    >

                    Mailing Yourself Abroad active

                </label>

                <label class="field">
                    Travel capacity
                </label>

                <input
                    id="lmt-capacity"
                    type="number"
                    min="1"
                    max="100"
                    value="${
                        profile.capacity
                    }"
                >

                <label class="check">

                    <input
                        id="lmt-bct-free"
                        type="checkbox"
                        ${
                            profile
                                .businessTicketsFree
                                ? 'checked'
                                : ''
                        }
                    >

                    Business Class tickets are free

                </label>

                <div class="buttons">

                    <button
                        id="lmt-save-profile"
                        class="button"
                        ${
                            saved
                                ? 'disabled'
                                : ''
                        }
                    >
                        ${
                            saved
                                ? 'Saved ✓'
                                : 'Save Profile'
                        }
                    </button>

                </div>

                <div class="divider"></div>

                <div class="buttons">

                    <button
                        id="lmt-change-key"
                        class="button"
                    >
                        Change Key
                    </button>

                    <button
                        id="lmt-inventory"
                        class="button"
                    >
                        API Inventory Check
                    </button>

                </div>

                ${inventoryUI()}

                <div class="note">
                    ${APP} ${VERSION}
                    · YATA 60s cache
                    · trusted prices 30m cache
                </div>

            </div>
        `;
    }

    function stockUI(
        verdict
    ) {
        if (
            !verdict
        ) {
            return '';
        }

        const cls =
            verdict.safe
                ? 'safe'
                : (
                    verdict.state ===
                        'expected_out_of_stock'
                        ? 'bad'
                        : 'unknown'
                );

        return `
            <div class="stock ${cls}">

                <strong>
                    ${esc(
                        verdict.label
                    )}
                </strong>

                <div>
                    ${esc(
                        verdict.detail
                    )}
                </div>

            </div>
        `;
    }

    function financialUI(
        candidate,
        mode
    ) {
        if (
            mode ===
            'museum'
        ) {
            return '';
        }

        const xanaxCash =
            mode ===
                'xanax'
                ? `
                    <div>
                        Recommended cash:
                        <strong>
                            ${money(
                                candidate.recommendedCash
                            )}
                        </strong>
                    </div>

                    <div class="warn">
                        Includes
                        ${money(
                            candidate.recommendedCash -
                            candidate.requiredCash
                        )}
                        cash buffer.
                    </div>
                `
                : '';

        return `
            <div class="moneybox">

                <div>
                    Purchase cash:
                    <strong>
                        ${money(
                            candidate.requiredCash
                        )}
                    </strong>
                </div>

                ${xanaxCash}

                <div>
                    Est. profit:
                    <strong>
                        ${money(
                            candidate.profit
                        )}
                    </strong>
                </div>

                <div>
                    Profit/hour:
                    <strong>
                        ${money(
                            candidate.profitPerHour
                        )}/hr
                    </strong>
                </div>

                <div>
                    Est. sale:
                    <strong>
                        ${money(
                            candidate.estimatedSaleValue
                        )}
                    </strong>
                    each
                </div>

            </div>
        `;
    }

    function candidateUI(
        label,
        candidate,
        mode,
        options = {}
    ) {
        if (
            !candidate
        ) {
            return `
                <div class="label">
                    ${esc(
                        label
                    )}
                </div>

                <div class="main warn">
                    No candidate available
                </div>
            `;
        }

        return `
            <div class="label">
                ${esc(
                    label
                )}
            </div>

            <div class="main">
                ${esc(
                    candidate.country
                )}
            </div>

            <div class="sub buy">
                Buy
                ${esc(
                    candidate.item
                )}
            </div>

            <div class="sub">
                ${duration(
                    candidate.minutes
                )}
                one way
            </div>

            ${financialUI(
                candidate,
                mode
            )}

            ${stockUI(
                candidate.verdict
            )}

            ${
                options.notRecommended
                    ? `
                        <div class="sub danger">
                            Not recommended
                        </div>
                    `
                    : ''
            }
        `;
    }

    function bucketUI(
        mode,
        desiredBucket
    ) {
        const pair =
            pairFor(
                mode,
                desiredBucket
            );

        const label =
            desiredBucket ===
                'quick'
                ? 'QUICK'
                : 'EXTENDED';

        let html =
            candidateUI(
                'BEST ' +
                label,
                pair.best,
                mode,
                {
                    notRecommended:
                        Boolean(
                            pair.best &&
                            !pair.best
                                .verdict
                                .safe
                        )
                }
            );

        if (
            pair.best &&
            !pair.best
                .verdict
                .safe
        ) {
            html +=
                '<div class="divider"></div>';

            if (
                pair.recommended
            ) {
                html +=
                    candidateUI(
                        'RECOMMENDED ' +
                        label,
                        pair.recommended,
                        mode
                    );
            }
            else {
                html += `
                    <div class="sub warn">

                        No stock-safe
                        ${desiredBucket}
                        recommendation
                        currently available.

                    </div>
                `;
            }
        }

        return html;
    }

    function agencyUI() {
        const mode =
            getMode();

        if (
            dataBusy
        ) {
            return `
                <div class="body">

                    <div class="main">
                        Loading
                        ${esc(
                            MODES[
                                mode
                            ]
                        )}
                        data...
                    </div>

                </div>
            `;
        }

        if (
            dataError
        ) {
            return `
                <div class="body">

                    <div class="main warn">
                        Optimizer data unavailable
                    </div>

                    <div class="sub">
                        ${esc(
                            dataError
                        )}
                    </div>

                </div>
            `;
        }

        const yataWarningUI =
            yataWarning
                ? `
                    <div class="stock unknown">
                        <strong>
                            YATA DATA WARNING
                        </strong>

                        <div>
                            ${esc(
                                yataWarning
                            )}
                        </div>
                    </div>

                    <div class="divider"></div>
                `
                : '';

        return `
            <div class="body scroll">

                ${yataWarningUI}

                <div class="label">
                    ${esc(
                        MODES[
                            mode
                        ]
                            .toUpperCase()
                    )}
                </div>

                ${bucketUI(
                    mode,
                    'quick'
                )}

                <div class="divider"></div>

                ${bucketUI(
                    mode,
                    'extended'
                )}

            </div>
        `;
    }

    function travelUI(
        state
    ) {
        if (
            state.direction ===
            'return'
        ) {
            return `
                <div class="body">

                    <div class="label">
                        TRAVELING
                    </div>

                    <div class="main">
                        Returning to Torn
                    </div>

                </div>
            `;
        }

        if (
            state.direction ===
                'outbound' &&
            state.country
        ) {
            const candidate =
                countryCandidate(
                    getMode(),
                    state.country
                );

            if (
                !candidate
            ) {
                return `
                    <div class="body">

                        <div class="label">
                            FLYING TO
                        </div>

                        <div class="main">
                            ${esc(
                                state.country
                            )}
                        </div>

                        <div class="sub">
                            No
                            ${esc(
                                MODES[
                                    getMode()
                                ]
                            )}
                            target for this destination.
                        </div>

                    </div>
                `;
            }

            return `
                <div class="body scroll">

                    <div class="label">
                        FLYING TO
                    </div>

                    <div class="main">
                        ${esc(
                            state.country
                        )}
                    </div>

                    <div class="divider"></div>

                    <div class="label">
                        WHEN YOU LAND
                    </div>

                    <div class="main buy">
                        Buy
                        ${esc(
                            candidate.item
                        )}
                    </div>

                    ${
                        getMode() !==
                            'museum'
                            ? financialUI(
                                candidate,
                                getMode()
                            )
                            : ''
                    }

                    ${stockUI(
                        candidate.verdict
                    )}

                </div>
            `;
        }

        return `
            <div class="body">

                <div class="label">
                    TRAVELING
                </div>

                <div class="main">
                    Flight in progress
                </div>

            </div>
        `;
    }


    // ============================================================
    // FOREIGN STORE PLANS
    // ============================================================

    function museumLivePlan(
        country
    ) {
        const items =
            (
                MUSEUM_COUNTRIES[
                    country
                ] ||
                []
            )
                .slice()
                .sort(
                    compareMuseumNames
                );

        const carry =
            carryStatus();

        const remaining =
            carry?.remaining ??
            getProfile()
                .capacity;

        const purchases = [];

        let left =
            remaining;

        for (
            const item
            of items
        ) {
            if (
                left <=
                0
            ) {
                break;
            }

            const stock =
                liveStock(
                    item
                );

            if (
                stock ===
                    null ||
                stock <=
                    0
            ) {
                continue;
            }

            const amount =
                Math.min(
                    left,
                    stock
                );

            purchases.push({
                item,
                amount
            });

            left -=
                amount;
        }

        return {
            purchases,
            left
        };
    }

    function shopUI(
        country
    ) {
        const carry =
            carryStatus();

        if (
            carry &&
            carry.remaining ===
                0
        ) {
            return `
                <div class="body">

                    <div class="label">
                        CAPACITY FULL
                    </div>

                    <div class="main buy">
                        Travel Home
                    </div>

                </div>
            `;
        }

        const mode =
            getMode();

        if (
            mode ===
            'museum'
        ) {
            const plan =
                museumLivePlan(
                    country
                );

            if (
                !plan
                    .purchases
                    .length
            ) {
                return `
                    <div class="body">

                        <div class="label">
                            ${esc(
                                country
                                    .toUpperCase()
                            )}
                        </div>

                        <div class="main warn">
                            WAIT FOR RESTOCK
                        </div>

                    </div>
                `;
            }

            return (
                '<div class="body scroll">' +

                plan.purchases
                    .map(
                        purchase => `
                            <div class="main buy">
                                Buy
                                ${purchase.amount}
                                ${esc(
                                    purchase.item
                                )}
                            </div>

                            <div class="divider"></div>
                        `
                    )
                    .join(
                        ''
                    ) +

                (
                    plan.left >
                    0
                        ? `
                            <div class="main warn">
                                WAIT FOR RESTOCK
                            </div>
                        `
                        : ''
                ) +

                '</div>'
            );
        }

        const candidate =
            countryCandidate(
                mode,
                country
            );

        if (
            !candidate
        ) {
            return `
                <div class="body">

                    <div class="label">
                        ${esc(
                            country
                                .toUpperCase()
                        )}
                    </div>

                    <div class="main warn">
                        No
                        ${esc(
                            MODES[
                                mode
                            ]
                        )}
                        target here
                    </div>

                </div>
            `;
        }

        const remaining =
            carry?.remaining ??
            getProfile()
                .capacity;

        const stock =
            liveStock(
                candidate.item
            );

        if (
            stock ===
            0
        ) {
            return `
                <div class="body">

                    <div class="label">
                        ${esc(
                            country
                                .toUpperCase()
                        )}
                    </div>

                    <div class="main warn">
                        WAIT FOR RESTOCK
                    </div>

                    <div class="sub">
                        ${esc(
                            candidate.item
                        )}
                        is sold out.
                    </div>

                </div>
            `;
        }

        const amount =
            stock ===
                null
                ? remaining
                : Math.min(
                    remaining,
                    stock
                );

        const purchaseCost =
            amount *
            candidate.foreignCost;

        return `
            <div class="body">

                <div class="label">
                    ${esc(
                        country
                            .toUpperCase()
                    )}
                </div>

                <div class="label">
                    ${esc(
                        MODES[
                            mode
                        ]
                            .toUpperCase()
                    )}
                </div>

                <div class="main buy">

                    Buy
                    ${amount}
                    ${esc(
                        candidate.item
                    )}

                </div>

                <div class="sub">
                    Purchase cost:
                    ${money(
                        purchaseCost
                    )}
                </div>

                ${
                    stock !==
                        null &&
                    stock <
                        remaining
                        ? `
                            <div class="sub warn">

                                Buy available stock,
                                then wait or return home.

                            </div>
                        `
                        : ''
                }

            </div>
        `;
    }


    // ============================================================
    // API KEY
    // ============================================================

    async function saveNewKey(
        value,
        statusId
    ) {
        const status =
            document.getElementById(
                statusId
            );

        const candidate =
            String(
                value ||
                ''
            ).trim();

        if (
            !candidate
        ) {
            if (
                status
            ) {
                status.textContent =
                    'Enter an API key.';
            }

            return;
        }

        if (
            status
        ) {
            status.textContent =
                'Checking key...';
        }

        try {
            const inventory =
                await fetchInventoryForKey(
                    candidate
                );

            setKey(
                candidate
            );

            saveInventory(
                inventory
            );

            changeKeyOpen =
                false;

            settingsOpen =
                false;

            if (
                status
            ) {
                status.textContent =
                    '';
            }

            await ensureData(
                true
            );
        }
        catch (
            error
        ) {
            if (
                status
            ) {
                status.textContent =
                    String(
                        error?.message ||
                        error
                    );
            }
        }
    }


    // ============================================================
    // RENDER
    // ============================================================

    function render() {
        if (
            !document.body ||
            buyFrozen
        ) {
            return;
        }

        injectStyle();

        const panel =
            root();

        const context =
            pageMode();

        if (
            context.mode ===
            'hidden'
        ) {
            panel.className =
                'hidden';

            return;
        }

        if (
            localStorage.getItem(
                STORAGE.minimized
            ) ===
            '1'
        ) {
            panel.className =
                'mini';

            panel.innerHTML = `
                <div
                    id="lmt-mini"
                    class="miniButton"
                    title="${APP}"
                >
                    ${SHORT_APP}
                </div>
            `;

            panel.style.left =
                'auto';

            panel.style.right =
                '0';

            panel.style.top =
                '20%';

            panel.style.bottom =
                'auto';

            panel
                .querySelector(
                    '#lmt-mini'
                )
                .onclick =
                    () => {
                        localStorage.setItem(
                            STORAGE.minimized,
                            '0'
                        );

                        render();
                    };

            return;
        }

        panel.className =
            '';

        let html =
            headerUI();

        if (
            !getKey()
        ) {
            html +=
                setupUI();
        }
        else {
            html +=
                modesUI();

            if (
                context.mode ===
                'agency'
            ) {
                clearCountry();

                html +=
                    agencyUI();
            }
            else if (
                context.mode ===
                'travel'
            ) {
                html +=
                    travelUI(
                        context
                    );
            }
            else if (
                context.mode ===
                'shop'
            ) {
                html +=
                    shopUI(
                        context.country
                    );
            }

            html +=
                settingsUI();
        }

        html +=
            resizeHandleUI();

        panel.innerHTML =
            html;

        applyPanelSize(
            panel
        );

        applyPosition(
            panel
        );

        bind(
            panel
        );

        bindDrag(
            panel
        );

        bindResize(
            panel
        );
    }


    // ============================================================
    // BUTTON BINDINGS
    // ============================================================

    function bind(
        panel
    ) {
        panel
            .querySelector(
                '#lmt-minimize'
            )
            ?.addEventListener(
                'click',
                () => {
                    localStorage.setItem(
                        STORAGE.minimized,
                        '1'
                    );

                    settingsOpen =
                        false;

                    changeKeyOpen =
                        false;

                    inventoryOpen =
                        false;

                    render();
                }
            );

        panel
            .querySelector(
                '#lmt-settings'
            )
            ?.addEventListener(
                'click',
                () => {
                    settingsOpen =
                        !settingsOpen;

                    if (
                        !settingsOpen
                    ) {
                        changeKeyOpen =
                            false;

                        inventoryOpen =
                            false;
                    }

                    render();
                }
            );

        panel
            .querySelector(
                '#lmt-refresh'
            )
            ?.addEventListener(
                'click',
                async () => {
                    if (
                        getMode() ===
                        'museum'
                    ) {
                        await refreshInventory(
                            false
                        );
                    }

                    await ensureData(
                        true
                    );
                }
            );

        panel
            .querySelector(
                '#lmt-save-key-first'
            )
            ?.addEventListener(
                'click',
                () => {
                    saveNewKey(
                        panel
                            .querySelector(
                                '#lmt-key'
                            )
                            ?.value,

                        'lmt-key-status'
                    );
                }
            );

        for (
            const button
            of panel.querySelectorAll(
                '[data-mode]'
            )
        ) {
            button.addEventListener(
                'click',
                async () => {
                    const mode =
                        button
                            .getAttribute(
                                'data-mode'
                            );

                    if (
                        !mode ||
                        mode ===
                            getMode()
                    ) {
                        return;
                    }

                    setMode(
                        mode
                    );

                    dataError =
                        '';

                    render();

                    await ensureData(
                        false
                    );
                }
            );
        }

        panel
            .querySelector(
                '#lmt-change-key'
            )
            ?.addEventListener(
                'click',
                () => {
                    changeKeyOpen =
                        true;

                    inventoryOpen =
                        false;

                    render();
                }
            );

        panel
            .querySelector(
                '#lmt-cancel-key'
            )
            ?.addEventListener(
                'click',
                () => {
                    changeKeyOpen =
                        false;

                    render();
                }
            );

        panel
            .querySelector(
                '#lmt-save-new-key'
            )
            ?.addEventListener(
                'click',
                () => {
                    saveNewKey(
                        panel
                            .querySelector(
                                '#lmt-new-key'
                            )
                            ?.value,

                        'lmt-key-status'
                    );
                }
            );

        panel
            .querySelector(
                '#lmt-save-profile'
            )
            ?.addEventListener(
                'click',
                async () => {
                    saveProfile({
                        method:
                            panel
                                .querySelector(
                                    '#lmt-method'
                                )
                                ?.value ||
                            'standard',

                        mailingBook:
                            Boolean(
                                panel
                                    .querySelector(
                                        '#lmt-book'
                                    )
                                    ?.checked
                            ),

                        capacity:
                            Math.max(
                                1,
                                Math.min(
                                    100,
                                    Number(
                                        panel
                                            .querySelector(
                                                '#lmt-capacity'
                                            )
                                            ?.value
                                    ) ||
                                    28
                                )
                            ),

                        businessTicketsFree:
                            Boolean(
                                panel
                                    .querySelector(
                                        '#lmt-bct-free'
                                    )
                                    ?.checked
                            )
                    });

                    profileSavedUntil =
                        Date.now() +
                        2000;

                    lastSignature =
                        '';

                    render();

                    await ensureData(
                        false
                    );

                    setTimeout(
                        () => {
                            if (
                                Date.now() >=
                                profileSavedUntil
                            ) {
                                profileSavedUntil =
                                    0;

                                lastSignature =
                                    '';

                                render();
                            }
                        },
                        2100
                    );
                }
            );

        panel
            .querySelector(
                '#lmt-inventory'
            )
            ?.addEventListener(
                'click',
                async () => {
                    if (
                        inventoryOpen
                    ) {
                        inventoryOpen =
                            false;

                        inventoryBusy =
                            false;

                        render();

                        return;
                    }

                    inventoryOpen =
                        true;

                    inventoryBusy =
                        true;

                    render();

                    await refreshInventory(
                        false
                    );

                    inventoryBusy =
                        false;

                    render();
                }
            );
    }


    // ============================================================
    // WATCHER
    // ============================================================

    function signature() {
        const context =
            pageMode();

        const yataCache =
            readJson(
                STORAGE.yata
            );

        const priceCache =
            getPriceCache();

        const priceSignature =
            Object.values(
                priceCache.items ||
                {}
            )
                .reduce(
                    (
                        latest,
                        entry
                    ) =>
                        Math.max(
                            latest,
                            Number(
                                entry.updated
                            ) ||
                            0
                        ),
                    0
                );

        return JSON.stringify({
            href:
                location.href,

            context,

            mode:
                getMode(),

            profile:
                getProfile(),

            minimized:
                localStorage.getItem(
                    STORAGE.minimized
                ),

            inventory:
                getInventoryCache()
                    .updated,

            yata:
                yataCache
                    ?.updated ||
                0,

            prices:
                priceSignature
        });
    }

    function pricesNeedRefresh(
        mode
    ) {
        if (
            mode !==
                'flushies' &&
            mode !==
                'xanax'
        ) {
            return false;
        }

        const relevant =
            relevantFinancialItems(
                mode
            );

        return relevant.some(
            item =>
                !cachedTrustedPrice(
                    item.id
                )
        );
    }

    function tick() {
        if (
            buyFrozen
        ) {
            updateBuyFreeze();
            return;
        }

        const current =
            signature();

        if (
            current !==
            lastSignature
        ) {
            lastSignature =
                current;

            const panel =
                document.getElementById(
                    ROOT_ID
                );

            if (
                !panel?.contains(
                    document.activeElement
                )
            ) {
                render();
            }
        }

        if (
            getKey() &&
            pageMode().mode ===
                'agency'
        ) {
            const yataStale =
                !cacheGet(
                    STORAGE.yata,
                    YATA_TTL
                ) &&
                Date.now() >=
                    yataRetryAfter;

            const mode =
                getMode();

            const priceStale =
                pricesNeedRefresh(
                    mode
                );

            const inventory =
                getInventoryCache();

            const inventoryStale =
                mode ===
                    'museum' &&
                (
                    !inventory.updated ||
                    Date.now() -
                        inventory.updated >
                        INVENTORY_TTL
                );

            if (
                (
                    yataStale ||
                    priceStale ||
                    inventoryStale
                ) &&
                !dataBusy
            ) {
                ensureData(
                    false
                );
            }
        }
    }


    // ============================================================
    // START
    // ============================================================

    function start() {
        if (
            !document.body
        ) {
            setTimeout(
                start,
                100
            );

            return;
        }

        injectStyle();

        installDrag();

        installResize();

        document.addEventListener(
            'click',
            handleBuyCapture,
            true
        );

        render();

        if (
            getKey()
        ) {
            ensureData(
                false
            );
        }

        lastSignature =
            signature();

        setInterval(
            tick,
            WATCH_MS
        );
    }

    start();

})();