LostMaster Travel

Museum Day, Flushies, and Xanax travel optimizer for Torn

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

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

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

// ==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();

})();