Nitro Type Mod Menu

Unified Nitro Type mod menu shell at /settings/mods with route takeover, mod tabs, and race-options-inspired settings layout.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください
// ==UserScript==
// @name         Nitro Type Mod Menu
// @namespace    https://greasyfork.org/users/1443935
// @version      1.0.0
// @description  Unified Nitro Type mod menu shell at /settings/mods with route takeover, mod tabs, and race-options-inspired settings layout.
// @author       Captain.Loveridge
// @match        https://www.nitrotype.com/*
// @grant        none
// @run-at       document-start
// @license      MIT
// ==/UserScript==

(function () {
    'use strict';

    const MOD_MENU_PATH = '/settings/mods';
    const DROPDOWN_ITEM_CLASS = 'ntmods-dropdown-item';
    const MANIFEST_PREFIX = 'ntcfg:manifest:';
    const UI_STATE_KEY = 'ntmods:ui-state';
    const PREVIEW_VALUE_PREFIX = 'ntmods:preview:';
    const PREVIEW_FALLBACK_ENABLED = true;
    let renderInProgress = false;
    let handleScheduled = false;

    const toggleField = (key, label, defaultValue, help) => ({
        type: 'toggle',
        key,
        label,
        default: defaultValue,
        help
    });

    const numberField = (key, label, defaultValue, help, min = null, max = null, step = '1') => ({
        type: 'number',
        key,
        label,
        default: defaultValue,
        help,
        min,
        max,
        step
    });

    const selectField = (key, label, defaultValue, options, help) => ({
        type: 'select',
        key,
        label,
        default: defaultValue,
        options,
        help
    });

    const textField = (key, label, defaultValue, help, placeholder = '') => ({
        type: 'text',
        key,
        label,
        default: defaultValue,
        help,
        placeholder
    });

    const colorField = (key, label, defaultValue, help) => ({
        type: 'color',
        key,
        label,
        default: defaultValue,
        help
    });

    const noteField = (message, tone = 'info') => ({
        type: 'note',
        message,
        tone
    });

    const actionField = (key, label, style = 'primary', help = '') => ({
        type: 'action',
        key,
        label,
        style,
        help
    });

    const MODULE_LIBRARY = [
        {
            id: 'race-options',
            label: 'Race Options',
            shortLabel: 'RO',
            accent: '#1c99f4',
            description: 'Race overlays, pacing tools, and theme controls for the race screen.',
            installUrl: 'https://greasyfork.org/en/scripts/567849-nitro-type-race-options',
            previewSections: [
                {
                    id: 'general',
                    title: 'General',
                    subtitle: 'Visual cleanup, overlays, and race helpers.',
                    items: [
                        toggleField('hide_track', 'Hide Track', false, 'Removes the large race track art to keep the page cleaner.'),
                        toggleField('hide_notifications', 'Hide Notifications', true, 'Suppresses popups while a race is active.'),
                        toggleField('enable_mini_map', 'Enable Mini Map', false, 'Shows a compact lane map near the race text.'),
                        selectField('mini_map_position', 'Mini Map Position', 'bottom', [
                            { value: 'bottom', label: 'Bottom' },
                            { value: 'top', label: 'Top' }
                        ], 'Choose where the mini map sits relative to the race UI.'),
                        { ...toggleField('enable_racer_badges_in_race', 'Enable Racer Badges (In-Race)', true, 'Lets racer badges inject race nameplate icons.'), storageKey: 'ntcfg:racer-badges:ENABLE_RACE_BADGES', emitModule: 'racer-badges', emitKey: 'ENABLE_RACE_BADGES' },
                        noteField('This section is a visual shell right now. The live bridge will replace these preview keys with shared mod settings.', 'info')
                    ]
                },
                {
                    id: 'pacing',
                    title: 'Pacing',
                    subtitle: 'Alt WPM and reload flow.',
                    items: [
                        toggleField('enable_alt_wpm_counter', 'Enable Alt. WPM / Countdown', true, 'Displays the draggable pace helper during the race.'),
                        numberField('target_wpm', 'Target WPM', 79.5, 'Target pace used by the helper.', 1, 300, '0.1'),
                        numberField('indicate_wpm_within', 'Yellow Threshold', 2, 'Highlight the helper when pace is within this range.', 0, 30, '0.1'),
                        toggleField('reload_on_stats', 'Enable Auto Reload', true, 'Refresh after the results screen updates.'),
                        numberField('greedy_stats_reload_int', 'Fast Reload Interval (ms)', 50, 'Low values reload sooner but can be heavier.', 25, 500, '5'),
                        actionField('preview_reload_action', 'Preview Save and Reload', 'primary', 'A placeholder action so the footer/button styling can be reviewed.')
                    ]
                },
                {
                    id: 'theme',
                    title: 'Theme',
                    subtitle: 'Text colors, backgrounds, and typing feel.',
                    items: [
                        toggleField('theme_enable_dark_mode', 'Enable Dark Mode', false, 'Uses the darker theme palette on the race page.'),
                        colorField('theme_color_background', 'Background Color', '#0A121E', 'Main background color used for text theming.'),
                        colorField('theme_color_background_active', 'Active Word Background', '#1C99F4', 'Background color for the active word.'),
                        colorField('theme_color_foreground', 'Foreground Color', '#E7EEF8', 'Main text color for upcoming characters.'),
                        selectField('theme_font_family_preset', 'Font Family', 'roboto_mono', [
                            { value: 'roboto_mono', label: 'Roboto Mono' },
                            { value: 'montserrat', label: 'Montserrat' },
                            { value: 'space_mono', label: 'Space Mono' }
                        ], 'Preview typography for the race text.'),
                        toggleField('theme_enable_rainbow_typed_text', 'Rainbow Typed Text', false, 'Applies animated color cycling to typed text.')
                    ]
                }
            ]
        },
        {
            id: 'racer-badges',
            label: 'Racer Badges',
            shortLabel: 'RB',
            accent: '#ffd166',
            description: 'Top racer and top team badge placement, in-race badge rules, and badge presentation.',
            installUrl: 'https://greasyfork.org/en/scripts/555617-nitro-type-top-racer-team-badges',
            previewSections: [
                {
                    id: 'general',
                    title: 'General',
                    subtitle: 'Global badge and banner visibility controls.',
                    items: [
                        toggleField('SHOW_RACER_BADGES', 'Show Racer Badges', true, 'Show small inline top-racer ranking icons next to names in lists.'),
                        toggleField('SHOW_RACER_BANNERS', 'Show Racer Banners', true, 'Show large top-racer ranking images on profile and garage pages.'),
                        toggleField('SHOW_TEAM_BADGES', 'Show Team Badges', true, 'Show small inline top-team ranking icons next to names in lists.'),
                        toggleField('SHOW_TEAM_BANNERS', 'Show Team Banners', true, 'Show large top-team ranking images on team, profile, and garage pages.'),
                        noteField('Install the Racer Badges script for these settings to take effect.', 'info')
                    ]
                },
                {
                    id: 'pages',
                    title: 'Pages',
                    subtitle: 'Per-page badge visibility.',
                    items: [
                        toggleField('ENABLE_RACE_BADGES', 'Enable Race Page', true, 'Show badge overlays inside the race view on racer nameplates.'),
                        toggleField('ENABLE_PROFILE_BADGES', 'Enable Profile Pages', true, 'Show badges and banners on racer profile pages.'),
                        toggleField('ENABLE_TEAM_PAGE_BADGES', 'Enable Team Pages', true, 'Show badges and banners on team pages.'),
                        toggleField('ENABLE_GARAGE_BADGES', 'Enable Garage Pages', true, 'Show badges and banners on garage pages.'),
                        toggleField('ENABLE_FRIENDS_BADGES', 'Enable Friends Page', true, 'Show badges on the friends page.'),
                        toggleField('ENABLE_LEAGUES_BADGES', 'Enable Leagues Page', true, 'Show badges inside league tables.'),
                        toggleField('ENABLE_HEADER_BADGE', 'Enable Header Badge', true, 'Display your own badge in the site header.'),
                        noteField('Install the Racer Badges script for these settings to take effect.', 'info')
                    ]
                },
                {
                    id: 'advanced',
                    title: 'Advanced',
                    subtitle: 'Debug and diagnostic controls.',
                    items: [
                        toggleField('DEBUG_LOGGING', 'Debug Logging', false, 'Enable verbose console logging for troubleshooting.'),
                        noteField('Install the Racer Badges script for these settings to take effect.', 'info')
                    ]
                }
            ]
        },
        {
            id: 'leaderboards',
            label: 'Leaderboards',
            shortLabel: 'LB',
            accent: '#6c8cff',
            description: 'StarTrack leaderboard routing, cached timeframes, and background refresh behavior.',
            installUrl: 'https://greasyfork.org/en/scripts/555066-nitro-type-startrack-leaderboard-integration',
            previewSections: [
                {
                    id: 'views',
                    title: 'Views',
                    subtitle: 'Top-level presentation and default landing behavior.',
                    items: [
                        selectField('default_view', 'Default View', 'individual', [
                            { value: 'individual', label: 'Top Racers' },
                            { value: 'team', label: 'Top Teams' }
                        ], 'Controls which leaderboard view opens first.'),
                        selectField('default_timeframe', 'Default Timeframe', 'season', [
                            { value: 'season', label: 'Season' },
                            { value: '24hr', label: 'Last 24 Hours' },
                            { value: '7day', label: 'Last 7 Days' }
                        ], 'Preview the default timeframe selection.'),
                        toggleField('highlight_position_change', 'Highlight Position Change', true, 'Show movement arrows beside leaderboard entries.'),
                        toggleField('manual_refresh_button', 'Show Manual Refresh Button', true, 'Keep the refresh control visible in the page header.')
                    ]
                },
                {
                    id: 'sync',
                    title: 'Sync',
                    subtitle: 'Refresh cadence and cache freshness.',
                    items: [
                        toggleField('daily_background_sync', 'Daily Background Sync', true, 'Warm cache data for daily-style ranges.'),
                        toggleField('hourly_background_sync', 'Hourly Background Sync', true, 'Refresh fast-moving windows in the background.'),
                        numberField('cache_duration_minutes', 'Cache Duration (minutes)', 15, 'General cache freshness for rolling windows.', 1, 120, '1'),
                        actionField('preview_resync', 'Preview Sync All Tabs', 'primary', 'Placeholder for a future cross-tab refresh action.')
                    ]
                },
                {
                    id: 'presentation',
                    title: 'Presentation',
                    subtitle: 'Spacing, motion, and accent treatment.',
                    items: [
                        colorField('accent_color', 'Accent Color', '#1C99F4', 'Used by route highlights and selected pills.'),
                        toggleField('anti_flicker_mode', 'Anti-Flicker Route Mode', true, 'Hide the default page shell until the custom page is ready.'),
                        toggleField('show_route_tab', 'Show Leaderboards Nav Tab', true, 'Controls the custom nav insertion.'),
                        noteField('The mods page is borrowing this script\'s route takeover pattern for its own shell.', 'info')
                    ]
                }
            ]
        },
        {
            id: 'bot-flag',
            label: 'Bot Flag',
            shortLabel: 'BF',
            accent: '#ff9f1c',
            description: 'StarTrack + NTL flag display controls, cache behavior, and status indicators.',
            installUrl: 'https://greasyfork.org/en/scripts/529360-nitro-type-flag-check-startrack-ntl-legacy',
            previewSections: [
                {
                    id: 'display',
                    title: 'Display',
                    subtitle: 'Visual placement and on-page behavior.',
                    items: [
                        toggleField('show_team_page_flags', 'Show Team Page Flags', true, 'Render badges on team rosters and member lists.'),
                        toggleField('show_friends_page_flags', 'Show Friends Page Flags', true, 'Keep friend page annotations enabled.'),
                        toggleField('show_league_page_flags', 'Show League Page Flags', true, 'Display status icons inside league tables.'),
                        selectField('tooltip_style', 'Tooltip Style', 'rich', [
                            { value: 'rich', label: 'Rich Tooltip' },
                            { value: 'compact', label: 'Compact Tooltip' }
                        ], 'Preview how detailed each hover card should be.')
                    ]
                },
                {
                    id: 'sources',
                    title: 'Sources',
                    subtitle: 'Data source preferences and fetch policy.',
                    items: [
                        toggleField('use_startrack', 'Use StarTrack Data', true, 'Primary flag source for status checks.'),
                        toggleField('use_ntl_legacy', 'Use NTL Legacy Data', true, 'Fallback source for additional history.'),
                        numberField('network_concurrency_limit', 'Network Concurrency Limit', 6, 'Soft cap for simultaneous user lookups.', 1, 20, '1'),
                        toggleField('debug_logging', 'Debug Logging', false, 'Enable verbose logging for troubleshooting.')
                    ]
                },
                {
                    id: 'cache',
                    title: 'Cache',
                    subtitle: 'How long flag results should be kept around.',
                    items: [
                        numberField('startrack_cache_days', 'StarTrack Cache (days)', 7, 'How long StarTrack lookups should be considered fresh.', 1, 30, '1'),
                        toggleField('cross_tab_sync', 'Cross-Tab Sync', true, 'Broadcast fresh data to other open Nitro Type tabs.'),
                        actionField('clear_flag_cache', 'Preview Clear Cache', 'danger', 'Placeholder action button for the maintenance area.'),
                        noteField('Cache maintenance actions are visual-only in this shell pass.', 'warning')
                    ]
                }
            ]
        },
        {
            id: 'music-player',
            label: 'Music Player',
            shortLabel: 'MP',
            accent: '#1db954',
            description: 'Universal music playback shell for Spotify, YouTube, and Apple Music sources.',
            installUrl: 'https://greasyfork.org/en/scripts/567896-nitro-type-universal-music-player',
            previewSections: [
                {
                    id: 'source',
                    title: 'Source',
                    subtitle: 'Where the queue comes from.',
                    items: [
                        textField('source_url', 'Source URL', '', 'Spotify, YouTube, or Apple Music playlist/album/track URL.', 'Paste playlist, album, or track URL'),
                        selectField('default_platform', 'Default Platform', 'spotify', [
                            { value: 'spotify', label: 'Spotify' },
                            { value: 'youtube', label: 'YouTube' },
                            { value: 'apple-music', label: 'Apple Music' }
                        ], 'Controls the label styling and initial queue hint when no source is configured.'),
                        toggleField('session_resume', 'Resume Last Session', true, 'Restores queue position and shuffle mode after reload.'),
                        toggleField('auto_activate', 'Auto Activate on Race Load', true, 'Immediately bring the player shell online on race pages.')
                    ]
                },
                {
                    id: 'playback',
                    title: 'Playback',
                    subtitle: 'Queue behavior and in-race control feel.',
                    items: [
                        selectField('queue_mode', 'Queue Mode', 'shuffle', [
                            { value: 'shuffle', label: 'Shuffle' },
                            { value: 'sequential', label: 'Sequential' }
                        ], 'Controls how tracks are ordered during playback.'),
                        toggleField('show_album_art', 'Show Album Art', true, 'Display album cover artwork in the inline now-playing card.'),
                        toggleField('mute_native_station', 'Mute Native Nitro Type Station', true, 'Silence the built-in race station when this player is active.'),
                        numberField('progress_tick_ms', 'Progress Tick (ms)', 1000, 'How often the inline player UI refreshes progress text.', 250, 5000, '250')
                    ]
                },
                {
                    id: 'advanced',
                    title: 'Advanced',
                    subtitle: 'Debug and diagnostic controls.',
                    items: [
                        toggleField('debug_logging', 'Debug Logging', false, 'Enable verbose console logging for troubleshooting.')
                    ]
                }
            ]
        },
        {
            id: 'bot-hunter',
            label: 'Bot Hunter',
            shortLabel: 'BH',
            accent: '#d62f3a',
            description: 'Bot Hunter observation controls, observer identity, and pattern-analysis presentation.',
            hiddenUnlessInstalled: true,
            previewSections: [
                {
                    id: 'general',
                    title: 'General',
                    subtitle: 'Main enablement and capture behavior.',
                    items: [
                        toggleField('enabled', 'Enable Bot Hunter', true, 'Temporarily toggles Bot Hunter capture on and off.'),
                        toggleField('self_observe_mode', 'Self Observe Mode', true, 'Includes your own races in the payload for testing.'),
                        toggleField('show_floating_toggle', 'Show Floating Toggle', true, 'Keeps the in-page Bot Hunter button visible.'),
                        toggleField('enable_logging', 'Enable Logging', true, 'Writes Bot Hunter diagnostics to the console.')
                    ]
                },
                {
                    id: 'observer',
                    title: 'Observer',
                    subtitle: 'Observer identity and payload metadata.',
                    items: [
                        textField('observer_id', 'Observer ID', 'generated-on-demand', 'Visual placeholder for the observer identity field.', 'Observer UUID'),
                        textField('api_endpoint', 'API Endpoint', 'https://ntstartrack.org/api/bot-observations', 'Current destination for submitted observations.'),
                        toggleField('use_keepalive', 'Use Keepalive on Exit', true, 'Send pending data during page unload or route exit.'),
                        noteField('This tab is ideal for a later "copy observer ID" button once behavior wiring starts.', 'info')
                    ]
                },
                {
                    id: 'analysis',
                    title: 'Analysis',
                    subtitle: 'Pattern heuristics and suspicious-race review.',
                    items: [
                        toggleField('detect_1000ms_pattern', 'Detect 1000ms Pattern', true, 'Flag repeated speed changes near one-second boundaries.'),
                        toggleField('detect_excessive_smoothing', 'Detect Excessive Smoothing', true, 'Flag unnaturally smooth speed transitions.'),
                        numberField('boundary_tolerance_ms', 'Boundary Tolerance (ms)', 100, 'Tolerance used when checking one-second boundaries.', 25, 250, '5'),
                        actionField('preview_recent_observations', 'Preview Recent Observations', 'secondary', 'Layout-only action card for a future review drawer.')
                    ]
                }
            ]
        }
    ];

    const MODULE_META = Object.create(null);
    MODULE_LIBRARY.forEach((module) => {
        MODULE_META[module.id] = module;
    });

    // Section defs are now read from the manifest (manifest.sections) — no hardcoded overrides needed.

    const RACE_OPTIONS_THEME_FONT_FAMILY_DEFAULT_CSS = '"Roboto Mono", "Courier New", Courier, "Lucida Sans Typewriter", "Lucida Typewriter", monospace';
    const RACE_OPTIONS_THEME_FONT_WEIGHT_DEFAULT = 400;
    const RACE_OPTIONS_THEME_DARK_MODE_FOREGROUND = '#E7EEF8';
    const RACE_OPTIONS_THEME_DARK_MODE_FOREGROUND_ACTIVE = '#101623';
    const RACE_OPTIONS_THEME_DARK_MODE_FOREGROUND_TYPED = '#8FA3BA';
    const RACE_OPTIONS_THEME_DARK_MODE_BACKGROUND = '#0A121E';
    const RACE_OPTIONS_THEME_DARK_MODE_BACKGROUND_ACTIVE = '#1C99F4';
    const RACE_OPTIONS_THEME_DARK_MODE_BACKGROUND_INCORRECT = '#D62F3A';
    const RACE_OPTIONS_THEME_FONT_FAMILY_PRESETS = [
        { value: '__default__', label: 'Default', css: RACE_OPTIONS_THEME_FONT_FAMILY_DEFAULT_CSS },
        { value: 'roboto_mono', label: 'Roboto Mono', css: '"Roboto Mono", "Courier New", Courier, monospace' },
        { value: 'montserrat', label: 'Montserrat', css: '"Montserrat", "Helvetica Neue", Helvetica, Arial, sans-serif' },
        { value: 'poppins', label: 'Poppins', css: '"Poppins", "Segoe UI", Tahoma, sans-serif' },
        { value: 'nunito', label: 'Nunito', css: '"Nunito", "Trebuchet MS", sans-serif' },
        { value: 'oswald', label: 'Oswald', css: '"Oswald", "Arial Narrow", sans-serif' },
        { value: 'space_mono', label: 'Space Mono', css: '"Space Mono", "Courier New", monospace' }
    ];
    const RACE_OPTIONS_THEME_FONT_SIZE_PRESETS = [
        { value: '__default__', label: 'Default', px: 18 },
        { value: '8', label: '8px', px: 8 },
        { value: '10', label: '10px', px: 10 },
        { value: '12', label: '12px', px: 12 },
        { value: '14', label: '14px', px: 14 },
        { value: '16', label: '16px', px: 16 },
        { value: '18', label: '18px', px: 18 },
        { value: '20', label: '20px', px: 20 },
        { value: '24', label: '24px', px: 24 },
        { value: '28', label: '28px', px: 28 },
        { value: '32', label: '32px', px: 32 },
        { value: '36', label: '36px', px: 36 }
    ];
    // Section defaults are now computed from item metadata by mountGenericSection.

    function normalizePath(pathname) {
        if (!pathname || pathname === '/') return '/';
        return pathname.replace(/\/+$/, '') || '/';
    }

    function isModMenuRoute(pathname = window.location.pathname) {
        return normalizePath(pathname) === MOD_MENU_PATH;
    }

    function escapeHtml(value) {
        return String(value ?? '')
            .replace(/&/g, '&')
            .replace(/</g, '&lt;')
            .replace(/>/g, '&gt;')
            .replace(/"/g, '&quot;')
            .replace(/'/g, '&#39;');
    }

    function titleCaseKey(value) {
        return String(value || '')
            .replace(/[_-]+/g, ' ')
            .replace(/\s+/g, ' ')
            .trim()
            .replace(/\b\w/g, (m) => m.toUpperCase());
    }

    function slugify(value) {
        return String(value || '')
            .toLowerCase()
            .replace(/[^a-z0-9]+/g, '-')
            .replace(/^-+|-+$/g, '') || 'section';
    }

    function readJson(key, fallback = null) {
        try {
            const raw = localStorage.getItem(key);
            if (raw == null) return fallback;
            return JSON.parse(raw);
        } catch {
            return fallback;
        }
    }

    function writeJson(key, value) {
        try {
            localStorage.setItem(key, JSON.stringify(value));
        } catch {
            // ignore storage errors
        }
    }

    function getPreviewStorageKey(moduleId, fieldKey) {
        return `${PREVIEW_VALUE_PREFIX}${moduleId}:${fieldKey}`;
    }

    function getManifestStorageKey(moduleId, fieldKey) {
        return `ntcfg:${moduleId}:${fieldKey}`;
    }

    function emitManifestChange(moduleId, fieldKey, value) {
        try {
            document.dispatchEvent(new CustomEvent('ntcfg:change', {
                detail: {
                    script: moduleId,
                    key: fieldKey,
                    value
                }
            }));
        } catch {
            // ignore custom event failures
        }
    }

    function convertManifestSetting(fieldKey, meta) {
        const inputType = String(meta?.type || 'text').toLowerCase();
        const common = {
            key: fieldKey,
            label: meta?.label || titleCaseKey(fieldKey),
            default: meta?.default,
            help: meta?.description || meta?.help || ''
        };

        // Pass through extended metadata when present
        if (meta?.visibleWhen) common.visibleWhen = meta.visibleWhen;
        if (meta?.compound) common.compound = meta.compound;
        if (meta?.warn) common.warn = meta.warn;
        if (meta?.presets) common.presets = meta.presets;
        if (meta?.normalize) common.normalize = meta.normalize;
        if (meta?.layout) common.layout = meta.layout;

        if (inputType === 'boolean') {
            return Object.assign({}, common, { type: 'toggle', default: Boolean(meta?.default) });
        }
        if (inputType === 'color') {
            return Object.assign({}, common, { type: 'color', default: meta?.default || '#1C99F4' });
        }
        if (inputType === 'number' || inputType === 'range') {
            return Object.assign({}, common, {
                type: 'number',
                min: meta?.min ?? null,
                max: meta?.max ?? null,
                step: meta?.step ?? '1'
            });
        }
        if (inputType === 'select') {
            return Object.assign({}, common, {
                type: 'select',
                options: Array.isArray(meta?.options) ? meta.options.map((option) => ({
                    value: option?.value,
                    label: option?.label ?? String(option?.value ?? '')
                })) : []
            });
        }
        return Object.assign({}, common, {
            type: 'text',
            placeholder: meta?.placeholder || ''
        });
    }

    function moduleFromManifest(manifest) {
        const base = MODULE_META[manifest.id] || {
            id: manifest.id,
            label: manifest.name || titleCaseKey(manifest.id),
            shortLabel: titleCaseKey(manifest.id).slice(0, 2).toUpperCase(),
            accent: '#1c99f4',
            description: manifest.description || `${manifest.name || titleCaseKey(manifest.id)} settings.`
        };

        const grouped = new Map();
        const settings = manifest?.settings && typeof manifest.settings === 'object' ? manifest.settings : {};

        // Collect compound child keys so they are not rendered as standalone items
        const compoundChildKeys = new Set();
        Object.entries(settings).forEach(([, meta]) => {
            if (Array.isArray(meta?.compound)) {
                meta.compound.forEach((child) => {
                    if (child?.key) compoundChildKeys.add(child.key);
                });
            }
        });

        Object.entries(settings).forEach(([fieldKey, meta]) => {
            if (compoundChildKeys.has(fieldKey)) return; // skip: rendered inline by its parent
            const groupName = meta?.group || 'General';
            if (!grouped.has(groupName)) grouped.set(groupName, []);
            grouped.get(groupName).push(convertManifestSetting(fieldKey, meta));
        });

        // Build a lookup from manifest-provided section metadata (subtitle, ordering)
        const sectionMeta = new Map();
        if (Array.isArray(manifest.sections)) {
            manifest.sections.forEach((sec, i) => sectionMeta.set(sec.id || slugify(sec.title || ''), { index: i, ...sec }));
        }

        const sections = Array.from(grouped.entries()).map(([groupName, items]) => {
            const sectionId = slugify(groupName);
            const meta = sectionMeta.get(sectionId);
            const sec = {
                id: sectionId,
                title: meta?.title || groupName,
                subtitle: meta?.subtitle || `${items.length} synced ${items.length === 1 ? 'setting' : 'settings'} from ${manifest.name || base.label}.`,
                items
            };
            // Pass through section-level metadata
            if (meta?.layout) sec.layout = meta.layout;
            if (meta?.preview) sec.preview = meta.preview;
            if (meta?.resetButton != null) sec.resetButton = meta.resetButton;
            return sec;
        });

        // Sort sections by manifest-defined order if available
        if (sectionMeta.size > 0) {
            sections.sort((a, b) => {
                const ai = sectionMeta.has(a.id) ? sectionMeta.get(a.id).index : Number.MAX_SAFE_INTEGER;
                const bi = sectionMeta.has(b.id) ? sectionMeta.get(b.id).index : Number.MAX_SAFE_INTEGER;
                return ai - bi;
            });
        }

        if (!sections.length) {
            sections.push({
                id: 'general',
                title: 'General',
                subtitle: `${manifest.name || base.label} is registered, but no grouped settings were provided yet.`,
                items: [
                    noteField('This manifest was detected, but it did not expose any structured settings yet.', 'warning')
                ]
            });
        }

        return Object.assign({}, base, {
            source: 'manifest',
            manifestVersion: manifest.version || '',
            sections
        });
    }

    function readManifestModules() {
        const modules = [];
        try {
            Object.keys(localStorage).forEach((key) => {
                if (!key.startsWith(MANIFEST_PREFIX)) return;
                const manifest = readJson(key, null);
                if (!manifest || !manifest.id) return;
                modules.push(moduleFromManifest(manifest));
            });
        } catch {
            return [];
        }

        const orderIndex = new Map(MODULE_LIBRARY.map((module, index) => [module.id, index]));
        modules.sort((a, b) => {
            const left = orderIndex.has(a.id) ? orderIndex.get(a.id) : Number.MAX_SAFE_INTEGER;
            const right = orderIndex.has(b.id) ? orderIndex.get(b.id) : Number.MAX_SAFE_INTEGER;
            if (left !== right) return left - right;
            return a.label.localeCompare(b.label);
        });

        return modules;
    }

    function buildPreviewModule(module) {
        return {
            id: module.id,
            label: module.label,
            shortLabel: module.shortLabel,
            accent: module.accent,
            description: module.description,
            source: 'preview',
            sections: module.previewSections
        };
    }

    function buildVisibleModules() {
        const manifestModules = readManifestModules();
        if (!PREVIEW_FALLBACK_ENABLED) return applyCustomTabOrder(manifestModules);

        const manifestById = new Map(manifestModules.map((module) => [module.id, module]));
        const modules = [];

        MODULE_LIBRARY.forEach((module) => {
            const manifest = manifestById.get(module.id);
            if (manifest) {
                modules.push(manifest);
            } else if (!module.hiddenUnlessInstalled) {
                modules.push(buildPreviewModule(module));
            }
        });

        manifestModules.forEach((module) => {
            if (!MODULE_META[module.id]) {
                modules.push(module);
            }
        });

        return applyCustomTabOrder(modules);
    }

    function applyCustomTabOrder(modules) {
        try {
            const savedOrder = JSON.parse(localStorage.getItem('ntmods:tab-order') || 'null');
            if (Array.isArray(savedOrder) && savedOrder.length) {
                const byId = new Map(modules.map((m) => [m.id, m]));
                const ordered = [];
                savedOrder.forEach((id) => {
                    const m = byId.get(id);
                    if (m) {
                        ordered.push(m);
                        byId.delete(id);
                    }
                });
                byId.forEach((m) => ordered.push(m));
                return ordered;
            }
        } catch { /* ignore */ }
        return modules;
    }

    function getRenderableSections(module) {
        return Array.isArray(module?.sections) ? module.sections : [];
    }

    function readUiState() {
        return readJson(UI_STATE_KEY, {}) || {};
    }

    function writeUiState(nextState) {
        writeJson(UI_STATE_KEY, nextState);
    }

    function parseHashSelection() {
        const hash = String(window.location.hash || '').replace(/^#/, '').trim();
        if (!hash) return null;
        const [moduleId, sectionId] = hash.split('/');
        if (!moduleId) return null;
        return { moduleId, sectionId: sectionId || '' };
    }

    function getInitialSelection(modules) {
        const stored = readUiState();
        const fromHash = parseHashSelection();
        const candidates = [fromHash, stored].filter(Boolean);

        for (const candidate of candidates) {
            const module = modules.find((entry) => entry.id === candidate.moduleId);
            if (!module) continue;
            const moduleSections = getRenderableSections(module);
            const section = moduleSections.find((entry) => entry.id === candidate.sectionId) || moduleSections[0];
            return {
                activeModuleId: module.id,
                activeSectionId: section ? section.id : ''
            };
        }

        const firstModule = modules[0] || null;
        const firstSections = getRenderableSections(firstModule);
        return {
            activeModuleId: firstModule ? firstModule.id : '',
            activeSectionId: firstSections[0] ? firstSections[0].id : ''
        };
    }

    function getFieldValue(module, item) {
        if (!item || !item.key) return item?.default ?? '';
        const storageKey = item.storageKey
            || (module.source === 'manifest'
                ? getManifestStorageKey(module.id, item.key)
                : getPreviewStorageKey(module.id, item.key));

        try {
            const raw = localStorage.getItem(storageKey);
            if (raw == null) return item.default;
            return JSON.parse(raw);
        } catch {
            return item.default;
        }
    }

    function setFieldValue(module, item, value) {
        if (!item || !item.key) return;
        const storageKey = item.storageKey
            || (module.source === 'manifest'
                ? getManifestStorageKey(module.id, item.key)
                : getPreviewStorageKey(module.id, item.key));

        try {
            localStorage.setItem(storageKey, JSON.stringify(value));
        } catch {
            // ignore storage errors
        }

        if (module.source === 'manifest') {
            emitManifestChange(module.id, item.key, value);
        }
        // Linked fields: emit change event for the target module so the badge script picks it up.
        if (item.emitModule && item.emitKey) {
            emitManifestChange(item.emitModule, item.emitKey, value);
        }
    }

    function coerceInputValue(element, item) {
        if (!element || !item) return null;
        if (item.type === 'toggle') return !!element.checked;
        if (item.type === 'number') {
            const parsed = Number(element.value);
            return Number.isFinite(parsed) ? parsed : (item.default ?? 0);
        }
        return String(element.value ?? '');
    }

    function createElement(tagName, className = '', textContent = null) {
        const element = document.createElement(tagName);
        if (className) element.className = className;
        if (textContent != null) element.textContent = textContent;
        return element;
    }

    function getModuleFieldMap(module) {
        const fieldMap = new Map();
        (module?.sections || []).forEach((section) => {
            (section?.items || []).forEach((item) => {
                if (item?.key) fieldMap.set(item.key, item);
            });
        });
        return fieldMap;
    }

    function resolveModuleFieldItem(fieldMap, key, fallback = {}) {
        return fieldMap.get(key) || Object.assign({
            key,
            label: titleCaseKey(key),
            default: '',
            type: 'text',
            help: ''
        }, fallback);
    }

    function readModuleFieldValue(module, fieldMap, key, fallback = {}) {
        return getFieldValue(module, resolveModuleFieldItem(fieldMap, key, fallback));
    }

    function writeModuleFieldValue(module, fieldMap, key, value, fallback = {}) {
        setFieldValue(module, resolveModuleFieldItem(fieldMap, key, fallback), value);
    }

    function normalizeRaceOptionsHexColorValue(value, fallback = '#FFFFFF') {
        const normalizedFallback = /^#[0-9A-Fa-f]{6}$/.test(String(fallback || '').trim())
            ? String(fallback).toUpperCase()
            : '#FFFFFF';
        const raw = String(value || '').trim();
        if (/^#[0-9A-Fa-f]{6}$/.test(raw)) return raw.toUpperCase();
        if (/^#[0-9A-Fa-f]{3}$/.test(raw)) {
            return `#${raw[1]}${raw[1]}${raw[2]}${raw[2]}${raw[3]}${raw[3]}`.toUpperCase();
        }
        return normalizedFallback;
    }

    function hexToRgba(hex, alpha = 1) {
        const h = normalizeRaceOptionsHexColorValue(hex, '#FFFFFF');
        const r = parseInt(h.slice(1, 3), 16);
        const g = parseInt(h.slice(3, 5), 16);
        const b = parseInt(h.slice(5, 7), 16);
        return `rgba(${r}, ${g}, ${b}, ${Math.min(1, Math.max(0, alpha))})`;
    }

    function hexToRgbChannels(hex) {
        const h = normalizeRaceOptionsHexColorValue(hex, '#FFFFFF');
        return [parseInt(h.slice(1, 3), 16), parseInt(h.slice(3, 5), 16), parseInt(h.slice(5, 7), 16)];
    }

    function blendedLuminance(fgHex, bgHex, alpha) {
        const [fr, fg, fb] = hexToRgbChannels(fgHex);
        const [br, bg, bb] = hexToRgbChannels(bgHex);
        const a = Math.min(1, Math.max(0, alpha));
        const r = (fr * a + br * (1 - a)) / 255;
        const g = (fg * a + bg * (1 - a)) / 255;
        const b = (fb * a + bb * (1 - a)) / 255;
        const linear = (ch) => (ch <= 0.03928 ? ch / 12.92 : Math.pow((ch + 0.055) / 1.055, 2.4));
        return (0.2126 * linear(r)) + (0.7152 * linear(g)) + (0.0722 * linear(b));
    }

    function normalizeRaceOptionsHighlightOpacity(value, fallback = 0.5) {
        const normalizedFallback = Number.isFinite(Number(fallback)) ? Number(fallback) : 0.5;
        const parsed = Number(value);
        if (!Number.isFinite(parsed)) return normalizedFallback;
        return Math.min(1, Math.max(0.05, Math.round(parsed * 100) / 100));
    }

    function normalizeRaceOptionsRainbowSpeedSeconds(value, fallback = 10) {
        const normalizedFallback = Number.isFinite(Number(fallback)) ? Number(fallback) : 10;
        const parsed = Number(value);
        if (!Number.isFinite(parsed)) return normalizedFallback;
        return Math.min(60, Math.max(1, parsed));
    }

    function normalizeRaceOptionsThemeFontSizePx(value, fallback = 18) {
        const normalizedFallback = Number.isFinite(Number(fallback)) ? Number(fallback) : 18;
        const parsed = Number(value);
        if (!Number.isFinite(parsed)) return normalizedFallback;
        return Math.min(72, Math.max(8, Math.round(parsed)));
    }

    function normalizeRaceOptionsThemeFontFamilyValue(value, fallback = RACE_OPTIONS_THEME_FONT_FAMILY_DEFAULT_CSS) {
        const raw = String(value ?? '').trim();
        if (!raw) return String(fallback);
        const cleaned = raw.replace(/[{}<>;\r\n]/g, '').trim().slice(0, 180);
        return cleaned || String(fallback);
    }

    function normalizeRaceOptionsThemePresetValue(value, presets, fallback = '__default__') {
        const raw = String(value ?? '').trim();
        if (Array.isArray(presets) && presets.some((preset) => preset?.value === raw)) {
            return raw;
        }
        return fallback;
    }

    function getRaceOptionsThemeOptions(module, fieldMap) {
        const fontFamilyPreset = normalizeRaceOptionsThemePresetValue(
            readModuleFieldValue(module, fieldMap, 'THEME_FONT_FAMILY_PRESET', { default: '__default__' }),
            RACE_OPTIONS_THEME_FONT_FAMILY_PRESETS
        );
        const fontSizePreset = normalizeRaceOptionsThemePresetValue(
            readModuleFieldValue(module, fieldMap, 'THEME_FONT_SIZE_PRESET', { default: '__default__' }),
            RACE_OPTIONS_THEME_FONT_SIZE_PRESETS
        );
        const singleLineFontSizePreset = normalizeRaceOptionsThemePresetValue(
            readModuleFieldValue(module, fieldMap, 'THEME_SINGLE_LINE_FONT_SIZE_PRESET', { default: '__default__' }),
            RACE_OPTIONS_THEME_FONT_SIZE_PRESETS
        );
        const fontFamilyPresetOption = RACE_OPTIONS_THEME_FONT_FAMILY_PRESETS.find((preset) => preset.value === fontFamilyPreset) || RACE_OPTIONS_THEME_FONT_FAMILY_PRESETS[0];
        const fontSizePresetOption = RACE_OPTIONS_THEME_FONT_SIZE_PRESETS.find((preset) => preset.value === fontSizePreset) || RACE_OPTIONS_THEME_FONT_SIZE_PRESETS[0];
        const singleLineFontSizePresetOption = RACE_OPTIONS_THEME_FONT_SIZE_PRESETS.find((preset) => preset.value === singleLineFontSizePreset) || RACE_OPTIONS_THEME_FONT_SIZE_PRESETS[0];
        const darkModeEnabled = !!readModuleFieldValue(module, fieldMap, 'THEME_ENABLE_DARK_MODE', { default: false, type: 'toggle' });
        const darkModeSyncSystem = !!readModuleFieldValue(module, fieldMap, 'THEME_DARK_MODE_SYNC_SYSTEM', { default: false, type: 'toggle' });
        const darkModeEffective = darkModeSyncSystem
            ? !!(typeof window.matchMedia === 'function' && window.matchMedia('(prefers-color-scheme: dark)').matches)
            : darkModeEnabled;

        return {
            darkModeEnabled,
            darkModeSyncSystem,
            darkModeEffective,
            foreground: normalizeRaceOptionsHexColorValue(readModuleFieldValue(module, fieldMap, 'THEME_COLOR_FOREGROUND', { default: '#FFFFFF' }), '#FFFFFF'),
            foregroundActive: normalizeRaceOptionsHexColorValue(readModuleFieldValue(module, fieldMap, 'THEME_COLOR_FOREGROUND_ACTIVE', { default: '#000000' }), '#000000'),
            foregroundTyped: normalizeRaceOptionsHexColorValue(readModuleFieldValue(module, fieldMap, 'THEME_COLOR_FOREGROUND_TYPED', { default: '#5B5B5B' }), '#5B5B5B'),
            background: normalizeRaceOptionsHexColorValue(readModuleFieldValue(module, fieldMap, 'THEME_COLOR_BACKGROUND', { default: '#000000' }), '#000000'),
            backgroundActive: normalizeRaceOptionsHexColorValue(readModuleFieldValue(module, fieldMap, 'THEME_COLOR_BACKGROUND_ACTIVE', { default: '#FFFFFF' }), '#FFFFFF'),
            backgroundIncorrect: normalizeRaceOptionsHexColorValue(readModuleFieldValue(module, fieldMap, 'THEME_COLOR_BACKGROUND_INCORRECT', { default: '#FF0000' }), '#FF0000'),
            overrideForeground: !!readModuleFieldValue(module, fieldMap, 'THEME_OVERRIDE_FOREGROUND', { default: false, type: 'toggle' }),
            overrideForegroundActive: !!readModuleFieldValue(module, fieldMap, 'THEME_OVERRIDE_FOREGROUND_ACTIVE', { default: false, type: 'toggle' }),
            overrideForegroundTyped: !!readModuleFieldValue(module, fieldMap, 'THEME_OVERRIDE_FOREGROUND_TYPED', { default: false, type: 'toggle' }),
            hideTypedText: !!readModuleFieldValue(module, fieldMap, 'THEME_HIDE_TYPED_TEXT', { default: false, type: 'toggle' }),
            overrideBackground: !!readModuleFieldValue(module, fieldMap, 'THEME_OVERRIDE_BACKGROUND', { default: false, type: 'toggle' }),
            overrideBackgroundActive: !!readModuleFieldValue(module, fieldMap, 'THEME_OVERRIDE_BACKGROUND_ACTIVE', { default: false, type: 'toggle' }),
            overrideBackgroundIncorrect: !!readModuleFieldValue(module, fieldMap, 'THEME_OVERRIDE_BACKGROUND_INCORRECT', { default: false, type: 'toggle' }),
            fontFamilyPreset,
            fontFamilyCss: fontFamilyPreset !== '__default__' && typeof fontFamilyPresetOption?.css === 'string'
                ? normalizeRaceOptionsThemeFontFamilyValue(fontFamilyPresetOption.css, RACE_OPTIONS_THEME_FONT_FAMILY_DEFAULT_CSS)
                : null,
            fontSizePreset,
            fontSizePx: fontSizePreset !== '__default__' && Number.isFinite(Number(fontSizePresetOption?.px))
                ? normalizeRaceOptionsThemeFontSizePx(fontSizePresetOption.px, 18)
                : null,
            singleLineFontSizePreset,
            singleLineFontSizePx: singleLineFontSizePreset !== '__default__' && Number.isFinite(Number(singleLineFontSizePresetOption?.px))
                ? normalizeRaceOptionsThemeFontSizePx(singleLineFontSizePresetOption.px, 18)
                : null,
            fontBold: !!readModuleFieldValue(module, fieldMap, 'THEME_FONT_BOLD', { default: false, type: 'toggle' }),
            fontItalic: !!readModuleFieldValue(module, fieldMap, 'THEME_FONT_ITALIC', { default: false, type: 'toggle' }),
            fontWeight: !!readModuleFieldValue(module, fieldMap, 'THEME_FONT_BOLD', { default: false, type: 'toggle' })
                ? Math.min(900, Math.max(100, Math.round(Number(readModuleFieldValue(module, fieldMap, 'THEME_FONT_WEIGHT', { default: 700, type: 'select' })) || 700)))
                : RACE_OPTIONS_THEME_FONT_WEIGHT_DEFAULT,
            enableRainbowTypedText: !!readModuleFieldValue(module, fieldMap, 'THEME_ENABLE_RAINBOW_TYPED_TEXT', { default: false, type: 'toggle' }),
            rainbowTypedTextSpeedSeconds: normalizeRaceOptionsRainbowSpeedSeconds(
                readModuleFieldValue(module, fieldMap, 'THEME_RAINBOW_TYPED_TEXT_SPEED_SECONDS', { default: 10, type: 'number' }),
                10
            )
        };
    }

    function getEffectiveRaceOptionsThemeBackground(module, fieldMap) {
        const themeOptions = getRaceOptionsThemeOptions(module, fieldMap);
        if (themeOptions.overrideBackground) return normalizeRaceOptionsHexColorValue(themeOptions.background, '#FFFFFF');
        if (themeOptions.darkModeEffective) return RACE_OPTIONS_THEME_DARK_MODE_BACKGROUND;
        return '#E9EAEB';
    }

    function getPerfectNitroTextStyle(module, fieldMap, highlightColor, opacity = 0.5) {
        const bgHex = getEffectiveRaceOptionsThemeBackground(module, fieldMap);
        const fgHex = normalizeRaceOptionsHexColorValue(highlightColor, '#FFFFFF');
        const luminance = blendedLuminance(fgHex, bgHex, opacity);
        const color = luminance > 0.5 ? '#101623' : '#E7EEF8';
        return {
            color,
            shadow: color === '#101623' ? '0 1px 1px rgba(255, 255, 255, 0.25)' : '0 1px 1px rgba(0, 0, 0, 0.35)'
        };
    }

    function getRaceOptionsPerfectNitroOptions(module, fieldMap) {
        return {
            enabled: !!readModuleFieldValue(module, fieldMap, 'ENABLE_PERFECT_NITROS', { default: true, type: 'toggle' }),
            highlightColor: normalizeRaceOptionsHexColorValue(readModuleFieldValue(module, fieldMap, 'PERFECT_NITRO_HIGHLIGHT_COLOR', { default: '#FFFFFF' }), '#FFFFFF'),
            enableHighlight: !!readModuleFieldValue(module, fieldMap, 'PERFECT_NITRO_ENABLE_HIGHLIGHT', { default: true, type: 'toggle' }),
            italic: !!readModuleFieldValue(module, fieldMap, 'PERFECT_NITRO_ITALIC', { default: false, type: 'toggle' }),
            rainbow: !!readModuleFieldValue(module, fieldMap, 'PERFECT_NITRO_RAINBOW', { default: false, type: 'toggle' }),
            highlightOpacity: normalizeRaceOptionsHighlightOpacity(readModuleFieldValue(module, fieldMap, 'PERFECT_NITRO_HIGHLIGHT_OPACITY', { default: 0.5, type: 'select' }), 0.5),
            overrideTextColor: !!readModuleFieldValue(module, fieldMap, 'PERFECT_NITRO_OVERRIDE_TEXT_COLOR', { default: false, type: 'toggle' }),
            textColor: normalizeRaceOptionsHexColorValue(readModuleFieldValue(module, fieldMap, 'PERFECT_NITRO_TEXT_COLOR', { default: '#FFFFFF' }), '#FFFFFF')
        };
    }

    function renderPerfectNitroPreview(preview, module, fieldMap) {
        const previewText = 'the quick brown fox jumps';
        const highlightWord = 'jumps';
        const highlightStart = previewText.indexOf(highlightWord);
        const highlightEnd = highlightStart + highlightWord.length;
        const options = getRaceOptionsPerfectNitroOptions(module, fieldMap);
        const themeOptions = getRaceOptionsThemeOptions(module, fieldMap);
        preview.innerHTML = '';

        if (themeOptions.darkModeEffective) {
            preview.style.background = RACE_OPTIONS_THEME_DARK_MODE_BACKGROUND;
            preview.style.color = RACE_OPTIONS_THEME_DARK_MODE_FOREGROUND;
        } else if (themeOptions.overrideBackground) {
            preview.style.background = themeOptions.background;
            preview.style.color = '';
        } else {
            preview.style.background = '';
            preview.style.color = '';
        }

        preview.style.fontFamily = themeOptions.fontFamilyCss || '';

        const bgRgba = options.enableHighlight ? hexToRgba(options.highlightColor, options.highlightOpacity) : null;
        const textStyle = options.overrideTextColor
            ? { color: options.textColor, shadow: null }
            : options.enableHighlight
                ? getPerfectNitroTextStyle(module, fieldMap, options.highlightColor, options.highlightOpacity)
                : { color: null, shadow: null };

        Array.from(previewText).forEach((char, index) => {
            const span = createElement('span');
            span.textContent = char;

            const isHighlighted = index >= highlightStart && index < highlightEnd;
            if (isHighlighted) {
                if (bgRgba) span.style.backgroundColor = bgRgba;
                if (textStyle.color) span.style.color = textStyle.color;
                if (textStyle.shadow) span.style.textShadow = textStyle.shadow;
                if (options.italic) span.style.fontStyle = 'italic';
                if (options.rainbow) span.classList.add('ntcfg-pn-preview-rainbow');
            } else if (themeOptions.overrideForeground) {
                span.style.color = themeOptions.foreground;
            } else if (themeOptions.darkModeEffective) {
                span.style.color = RACE_OPTIONS_THEME_DARK_MODE_FOREGROUND;
            }

            preview.appendChild(span);
        });
    }

    function renderThemePreview(preview, cursorIndex, incorrect, module, fieldMap) {
        const options = getRaceOptionsThemeOptions(module, fieldMap);
        const previewText = 'The Quick Brown Fox is afraid of The Big Black';
        preview.innerHTML = '';

        if (options.darkModeEffective) {
            preview.style.background = RACE_OPTIONS_THEME_DARK_MODE_BACKGROUND;
            preview.style.color = RACE_OPTIONS_THEME_DARK_MODE_FOREGROUND;
        } else if (options.overrideBackground) {
            preview.style.background = options.background;
            preview.style.color = '';
        } else {
            preview.style.background = '';
            preview.style.color = '';
        }

        preview.style.fontFamily = options.fontFamilyCss || '';
        preview.style.fontWeight = String(options.fontWeight || RACE_OPTIONS_THEME_FONT_WEIGHT_DEFAULT);
        preview.style.fontStyle = options.fontItalic ? 'italic' : '';

        Array.from(previewText).forEach((char, index) => {
            const span = createElement('span');
            if (index === cursorIndex) {
                span.classList.add(incorrect ? 'ntcfg-theme-char-incorrect' : 'ntcfg-theme-char-active');
            } else if (index < cursorIndex) {
                span.classList.add('ntcfg-theme-char-typed');
                if (options.enableRainbowTypedText && !options.hideTypedText) {
                    span.classList.add('ntcfg-theme-char-rainbow');
                    span.style.animationDuration = `${options.rainbowTypedTextSpeedSeconds}s`;
                    span.style.webkitAnimationDuration = `${options.rainbowTypedTextSpeedSeconds}s`;
                }
            }

            span.textContent = char;

            if (index > cursorIndex && options.overrideForeground) {
                span.style.color = options.foreground;
            } else if (index > cursorIndex && options.darkModeEffective) {
                span.style.color = RACE_OPTIONS_THEME_DARK_MODE_FOREGROUND;
            }
            if (index === cursorIndex && options.overrideForegroundActive) {
                span.style.color = options.foregroundActive;
            } else if (index === cursorIndex && options.darkModeEffective) {
                span.style.color = RACE_OPTIONS_THEME_DARK_MODE_FOREGROUND_ACTIVE;
            }
            if (index < cursorIndex && options.overrideForegroundTyped && !options.enableRainbowTypedText) {
                span.style.color = options.foregroundTyped;
                span.style.opacity = '1';
            } else if (index < cursorIndex && options.darkModeEffective && !options.enableRainbowTypedText) {
                span.style.color = RACE_OPTIONS_THEME_DARK_MODE_FOREGROUND_TYPED;
                span.style.opacity = '1';
            }
            if (index < cursorIndex && options.hideTypedText) {
                span.style.color = 'transparent';
                span.style.webkitTextFillColor = 'transparent';
                span.style.textShadow = 'none';
                span.style.opacity = '0';
            }
            if (index === cursorIndex && !incorrect && options.overrideBackgroundActive) {
                span.style.background = options.backgroundActive;
            } else if (index === cursorIndex && !incorrect && options.darkModeEffective) {
                span.style.background = RACE_OPTIONS_THEME_DARK_MODE_BACKGROUND_ACTIVE;
            }
            if (index === cursorIndex && incorrect && options.overrideBackgroundIncorrect) {
                span.style.background = options.backgroundIncorrect;
            } else if (index === cursorIndex && incorrect && options.darkModeEffective) {
                span.style.background = RACE_OPTIONS_THEME_DARK_MODE_BACKGROUND_INCORRECT;
            }
            if (Number.isFinite(options.fontSizePx)) {
                span.style.fontSize = `${options.fontSizePx}px`;
            }
            span.style.fontWeight = String(options.fontWeight || RACE_OPTIONS_THEME_FONT_WEIGHT_DEFAULT);
            if (options.fontItalic) span.style.fontStyle = 'italic';
            preview.appendChild(span);
        });
    }

    // ── Extracted helpers (previously closures inside mountRaceOptionsSection) ──

    function createSwitch(checked, onChange) {
        const switchRoot = createElement('span', 'ntcfg-switch');
        const input = document.createElement('input');
        input.type = 'checkbox';
        input.checked = !!checked;
        input.addEventListener('change', () => onChange(!!input.checked));
        const track = createElement('span', 'ntcfg-switch-track');
        switchRoot.append(input, track);
        return { root: switchRoot, input, track };
    }

    function createCompactColorControl(initialValue, onChange, wrapperClass = 'ntcfg-theme-color-compact') {
        const colorWrap = createElement('span', wrapperClass);
        const colorInput = document.createElement('input');
        colorInput.type = 'color';
        colorInput.className = 'ntcfg-color-picker';
        const hexInput = document.createElement('input', 'ntcfg-input');
        hexInput.type = 'text';
        hexInput.maxLength = 7;
        hexInput.placeholder = '#FFFFFF';
        const setValue = (value) => {
            const normalized = normalizeRaceOptionsHexColorValue(value, '#FFFFFF');
            colorInput.value = normalized;
            hexInput.value = normalized;
        };
        setValue(initialValue);
        colorInput.addEventListener('input', () => {
            const normalized = normalizeRaceOptionsHexColorValue(colorInput.value, colorInput.value);
            setValue(normalized);
            onChange(normalized);
        });
        const commitHex = () => {
            const normalized = normalizeRaceOptionsHexColorValue(hexInput.value, colorInput.value || '#FFFFFF');
            setValue(normalized);
            onChange(normalized);
        };
        hexInput.addEventListener('change', commitHex);
        hexInput.addEventListener('blur', commitHex);
        colorWrap.append(colorInput, hexInput);
        return { root: colorWrap, colorInput, hexInput, setValue };
    }

    function genericAppendCheckbox(root, opts, ctx) {
        const { module, fieldMap, refreshPreviews } = ctx;
        const { labelText, key, defaultValue, helpText = '', onChange = null } = opts;
        const row = createElement('label', 'ntcfg-checkbox');
        const copy = createElement('span', 'ntcfg-checkbox-copy');
        copy.appendChild(createElement('span', 'ntcfg-checkbox-label', labelText));
        if (helpText) {
            copy.appendChild(createElement('span', 'ntcfg-checkbox-help', helpText));
        }
        const switchControl = createSwitch(
            readModuleFieldValue(module, fieldMap, key, { default: defaultValue, type: 'toggle' }),
            (checked) => {
                writeModuleFieldValue(module, fieldMap, key, checked, { default: defaultValue, type: 'toggle' });
                if (typeof onChange === 'function') onChange(checked, row);
                refreshPreviews();
            }
        );
        row.append(copy, switchControl.root);
        root.appendChild(row);
        return { row, input: switchControl.input };
    }

    function genericAppendNumberField(root, opts, ctx) {
        const { module, fieldMap, refreshPreviews } = ctx;
        const { labelText, key, defaultValue, helpText = '', minRecommended = null, warningText = '', min = null, max = null, step = '1', onChange = null } = opts;
        const field = createElement('label', 'ntcfg-field');
        field.appendChild(createElement('div', 'ntcfg-field-title', labelText));

        const input = document.createElement('input');
        input.className = 'ntcfg-input';
        input.type = 'number';
        if (min != null) input.min = String(min);
        if (max != null) input.max = String(max);
        if (step != null) input.step = String(step);
        input.value = String(readModuleFieldValue(module, fieldMap, key, { default: defaultValue, type: 'number' }));

        const warning = createElement('div', 'ntcfg-field-help ntcfg-field-warning');
        warning.hidden = true;
        const updateWarningState = (value) => {
            const shouldWarn = Number.isFinite(minRecommended) && Number.isFinite(value) && value < minRecommended;
            warning.hidden = !shouldWarn;
            warning.textContent = shouldWarn ? (warningText || `Warning: values below ${minRecommended} are not recommended.`) : '';
            input.classList.toggle('ntcfg-input-warning', shouldWarn);
        };

        input.addEventListener('input', () => updateWarningState(parseFloat(input.value)));
        input.addEventListener('change', () => {
            const parsed = parseFloat(input.value);
            if (!Number.isFinite(parsed)) return;
            writeModuleFieldValue(module, fieldMap, key, parsed, { default: defaultValue, type: 'number' });
            updateWarningState(parsed);
            if (typeof onChange === 'function') onChange(parsed, field);
            refreshPreviews();
        });

        field.appendChild(input);
        if (helpText) field.appendChild(createElement('div', 'ntcfg-field-help', helpText));
        if (Number.isFinite(minRecommended)) {
            field.appendChild(warning);
            updateWarningState(parseFloat(input.value));
        }
        root.appendChild(field);
        return { field, input, warning };
    }

    function genericAppendSelectField(root, opts, ctx) {
        const { module, fieldMap, refreshPreviews } = ctx;
        const { labelText, key, defaultValue, options = [], helpText = '', onChange = null } = opts;
        const field = createElement('label', 'ntcfg-field');
        field.appendChild(createElement('div', 'ntcfg-field-title', labelText));
        const select = createElement('select', 'ntcfg-input');

        const safeOptions = Array.isArray(options) ? options.filter(Boolean) : [];
        const normalizeSelectedValue = (value) => {
            const raw = String(value ?? '').trim();
            const matched = safeOptions.find((option) => String(option.value) === raw);
            return matched ? matched.value : defaultValue;
        };

        safeOptions.forEach((optionDef) => {
            const option = document.createElement('option');
            option.value = String(optionDef.value);
            option.textContent = String(optionDef.label ?? optionDef.value);
            select.appendChild(option);
        });
        select.value = String(normalizeSelectedValue(readModuleFieldValue(module, fieldMap, key, { default: defaultValue, type: 'select', options })));
        select.addEventListener('change', () => {
            const normalized = normalizeSelectedValue(select.value);
            select.value = String(normalized);
            writeModuleFieldValue(module, fieldMap, key, normalized, { default: defaultValue, type: 'select', options });
            if (typeof onChange === 'function') onChange(normalized, field);
            refreshPreviews();
        });

        field.appendChild(select);
        if (helpText) field.appendChild(createElement('div', 'ntcfg-field-help', helpText));
        root.appendChild(field);
        return { field, select };
    }

    function genericAppendTextField(root, opts, ctx) {
        const { module, fieldMap, refreshPreviews } = ctx;
        const { labelText, key, defaultValue = '', helpText = '', placeholder = '', onChange = null } = opts;
        const field = createElement('label', 'ntcfg-field');
        field.appendChild(createElement('div', 'ntcfg-field-title', labelText));

        const inputRow = createElement('div', 'ntcfg-text-input-row');
        inputRow.style.cssText = 'display:flex;gap:6px;align-items:center;';

        const input = document.createElement('input');
        input.className = 'ntcfg-input';
        input.type = 'text';
        input.placeholder = placeholder || '';
        input.spellcheck = false;
        input.value = String(readModuleFieldValue(module, fieldMap, key, { default: defaultValue, type: 'text' }) ?? '');

        const saveBtn = createElement('button', 'ntcfg-action ntcfg-action--primary ntcfg-text-save-btn', 'Save');
        saveBtn.type = 'button';
        saveBtn.style.cssText = 'padding:6px 14px;font-size:12px;white-space:nowrap;flex-shrink:0;';
        const clearBtn = createElement('button', 'ntcfg-action ntcfg-action--secondary ntcfg-text-clear-btn', 'Clear');
        clearBtn.type = 'button';
        clearBtn.style.cssText = 'padding:6px 10px;font-size:12px;white-space:nowrap;flex-shrink:0;';

        const commit = (value) => {
            writeModuleFieldValue(module, fieldMap, key, value, { default: defaultValue, type: 'text' });
            if (typeof onChange === 'function') onChange(value, field);
            refreshPreviews();
        };

        saveBtn.addEventListener('click', () => {
            commit(input.value.trim());
            showToast('Saved.');
        });

        clearBtn.addEventListener('click', () => {
            input.value = '';
            commit('');
            showToast('Cleared.');
        });

        // Also save on Enter key
        input.addEventListener('keydown', (e) => {
            if (e.key === 'Enter') {
                e.preventDefault();
                commit(input.value.trim());
                showToast('Saved.');
            }
        });

        inputRow.append(input, saveBtn, clearBtn);
        field.appendChild(inputRow);
        if (helpText) field.appendChild(createElement('div', 'ntcfg-field-help', helpText));
        root.appendChild(field);
        return { field, input, saveBtn, clearBtn };
    }

    function genericAppendResetButton(root, labelText, defaults, ctx) {
        const { module, fieldMap, rerender } = ctx;
        const resetButton = createElement('button', 'ntcfg-action ntcfg-inline-action', labelText);
        resetButton.type = 'button';
        resetButton.addEventListener('click', () => {
            Object.entries(defaults).forEach(([settingKey, value]) => {
                writeModuleFieldValue(module, fieldMap, settingKey, value);
            });
            rerender();
        });
        root.appendChild(resetButton);
    }

    function genericAppendThemeSettingToggle(root, labelText, key, defaultValue, ctx, onChange = null) {
        const { module, fieldMap, refreshPreviews } = ctx;
        const row = createElement('div', 'ntcfg-theme-setting');
        row.appendChild(createElement('div', 'ntcfg-theme-setting-title', labelText));
        const controls = createElement('div', 'ntcfg-theme-setting-controls');
        const switchControl = createSwitch(
            readModuleFieldValue(module, fieldMap, key, { default: defaultValue, type: 'toggle' }),
            (checked) => {
                writeModuleFieldValue(module, fieldMap, key, checked, { default: defaultValue, type: 'toggle' });
                if (typeof onChange === 'function') onChange(checked, row);
                refreshPreviews();
            }
        );
        controls.appendChild(switchControl.root);
        row.appendChild(controls);
        root.appendChild(row);
        return { row, input: switchControl.input, controls };
    }

    function genericAppendThemeSettingColor(root, labelText, toggleKey, toggleDefault, colorKey, colorDefault, ctx) {
        const { module, fieldMap, refreshPreviews } = ctx;
        const row = createElement('div', 'ntcfg-theme-setting');
        row.appendChild(createElement('div', 'ntcfg-theme-setting-title', labelText));
        const controls = createElement('div', 'ntcfg-theme-setting-controls');
        const colorControl = createCompactColorControl(
            readModuleFieldValue(module, fieldMap, colorKey, { default: colorDefault, type: 'color' }),
            (value) => {
                writeModuleFieldValue(module, fieldMap, colorKey, value, { default: colorDefault, type: 'color' });
                refreshPreviews();
            }
        );
        const switchControl = createSwitch(
            readModuleFieldValue(module, fieldMap, toggleKey, { default: toggleDefault, type: 'toggle' }),
            (checked) => {
                writeModuleFieldValue(module, fieldMap, toggleKey, checked, { default: toggleDefault, type: 'toggle' });
                refreshPreviews();
            }
        );
        controls.append(colorControl.root, switchControl.root);
        row.append(controls);
        root.appendChild(row);
    }

    function genericAppendThemeSettingSelect(root, labelText, key, defaultValue, options, ctx, className = 'ntcfg-input ntcfg-theme-input-compact ntcfg-theme-input-select', onChange = null) {
        const { module, fieldMap, refreshPreviews } = ctx;
        const row = createElement('div', 'ntcfg-theme-setting');
        row.appendChild(createElement('div', 'ntcfg-theme-setting-title', labelText));
        const controls = createElement('div', 'ntcfg-theme-setting-controls');
        const select = createElement('select', className);
        const safeOptions = Array.isArray(options) ? options.filter(Boolean) : [];
        const normalizeSelectedValue = (value) => {
            const raw = String(value ?? '').trim();
            const match = safeOptions.find((option) => String(option.value) === raw);
            return match ? match.value : defaultValue;
        };
        safeOptions.forEach((optionDef) => {
            const option = document.createElement('option');
            option.value = String(optionDef.value);
            option.textContent = String(optionDef.label ?? optionDef.value);
            select.appendChild(option);
        });
        select.value = String(normalizeSelectedValue(readModuleFieldValue(module, fieldMap, key, { default: defaultValue, type: 'select', options })));
        select.addEventListener('change', () => {
            const normalized = normalizeSelectedValue(select.value);
            select.value = String(normalized);
            writeModuleFieldValue(module, fieldMap, key, normalized, { default: defaultValue, type: 'select', options });
            if (typeof onChange === 'function') onChange(normalized, row, select);
            refreshPreviews();
        });
        controls.appendChild(select);
        row.appendChild(controls);
        root.appendChild(row);
        return { row, select, controls };
    }

    function genericAppendThemeSettingNumber(root, labelText, key, defaultValue, config, ctx) {
        const { module, fieldMap, refreshPreviews } = ctx;
        const { min = 1, max = 60, step = 1, presets = [], normalizeValue = (value, fallback) => {
            const parsed = Number(value);
            return Number.isFinite(parsed) ? parsed : Number(fallback);
        } } = config || {};

        const row = createElement('div', 'ntcfg-theme-setting');
        row.appendChild(createElement('div', 'ntcfg-theme-setting-title', labelText));
        const controls = createElement('div', 'ntcfg-theme-setting-controls');

        const input = createElement('input', 'ntcfg-input ntcfg-theme-input-compact ntcfg-theme-input-number');
        input.type = 'number';
        input.min = String(min);
        input.max = String(max);
        input.step = String(step);
        input.value = String(normalizeValue(readModuleFieldValue(module, fieldMap, key, { default: defaultValue, type: 'number' }), defaultValue));

        const presetSelect = createElement('select', 'ntcfg-input ntcfg-theme-input-compact ntcfg-theme-input-preset');
        const customPresetValue = '__custom__';
        const customOption = document.createElement('option');
        customOption.value = customPresetValue;
        customOption.textContent = 'Custom';
        presetSelect.appendChild(customOption);

        const normalizedPresets = Array.isArray(presets)
            ? presets.map((preset) => {
                if (!preset || typeof preset !== 'object') return null;
                const value = normalizeValue(preset.value, defaultValue);
                return { value, label: String(preset.label || `${value}s`).trim() };
            }).filter(Boolean)
            : [];

        normalizedPresets.forEach((preset) => {
            const option = document.createElement('option');
            option.value = String(preset.value);
            option.textContent = `${preset.label} (${preset.value}s)`;
            presetSelect.appendChild(option);
        });

        const syncPresetFromValue = (value) => {
            const matched = normalizedPresets.find((preset) => preset.value === value);
            presetSelect.value = matched ? String(matched.value) : customPresetValue;
        };

        const save = () => {
            const normalized = normalizeValue(input.value, defaultValue);
            input.value = String(normalized);
            syncPresetFromValue(normalized);
            writeModuleFieldValue(module, fieldMap, key, normalized, { default: defaultValue, type: 'number' });
            refreshPreviews();
        };

        input.addEventListener('change', save);
        input.addEventListener('blur', save);
        presetSelect.addEventListener('change', () => {
            if (presetSelect.value === customPresetValue) return;
            const normalized = normalizeValue(presetSelect.value, defaultValue);
            input.value = String(normalized);
            writeModuleFieldValue(module, fieldMap, key, normalized, { default: defaultValue, type: 'number' });
            refreshPreviews();
        });

        controls.appendChild(input);
        if (normalizedPresets.length) {
            controls.appendChild(presetSelect);
            syncPresetFromValue(normalizeValue(input.value, defaultValue));
        }

        row.appendChild(controls);
        root.appendChild(row);
        return { row, input, presetSelect };
    }

    // ── Preview registry ──
    const PREVIEW_RENDERERS = {
        'theme': function (fields, refreshPreviewFns, module, fieldMap) {
            const stickyWrapper = createElement('div', 'ntcfg-sticky-preview');
            stickyWrapper.appendChild(createElement('div', 'ntcfg-theme-preview-label', 'Preview:'));
            const previewGrid = createElement('div', 'ntcfg-theme-preview-grid');
            const normalPreview = createElement('div', 'ntcfg-theme-preview');
            const incorrectPreview = createElement('div', 'ntcfg-theme-preview');
            previewGrid.append(normalPreview, incorrectPreview);
            stickyWrapper.appendChild(previewGrid);
            fields.appendChild(stickyWrapper);
            refreshPreviewFns.push(() => renderThemePreview(normalPreview, 7, false, module, fieldMap));
            refreshPreviewFns.push(() => renderThemePreview(incorrectPreview, 7, true, module, fieldMap));
        },
        'perfect-nitro': function (fields, refreshPreviewFns, module, fieldMap) {
            const stickyWrapper = createElement('div', 'ntcfg-sticky-preview');
            stickyWrapper.appendChild(createElement('div', 'ntcfg-theme-preview-label', 'Preview:'));
            const preview = createElement('div', 'ntcfg-pn-preview');
            stickyWrapper.appendChild(preview);
            fields.appendChild(stickyWrapper);
            refreshPreviewFns.push(() => renderPerfectNitroPreview(preview, module, fieldMap));
        }
    };

    // ── Generic section mount ──

    function mountGenericSection(module, section) {
        const host = document.querySelector('[data-ntmods-generic-stage]');
        if (!host || !module || !section) return;

        const fieldMap = getModuleFieldMap(module);
        const rerender = () => mountGenericSection(module, section);
        const refreshPreviewFns = [];
        const refreshPreviews = () => { refreshPreviewFns.forEach((fn) => fn()); };
        const ctx = { module, fieldMap, refreshPreviewFns, refreshPreviews, rerender };

        const sectionRoot = createElement('div', 'ntmods-ro-panel');
        const title = createElement('h2', 'ntcfg-panel-title', section.title);
        const subtitle = createElement('p', 'ntcfg-panel-subtitle', section.subtitle || '');
        const fields = createElement('div', 'ntcfg-fields');
        sectionRoot.append(title, subtitle, fields);
        host.innerHTML = '';
        host.appendChild(sectionRoot);

        // Render preview widget if section declares one
        if (section.preview && typeof section.preview === 'object' && section.preview.type) {
            const renderer = PREVIEW_RENDERERS[section.preview.type];
            if (typeof renderer === 'function') {
                renderer(fields, refreshPreviewFns, module, fieldMap);
            }
        }

        const isCompact = section.layout === 'compact';

        // Container for compact layout items
        let compactRoot = null;
        if (isCompact) {
            compactRoot = createElement('div', 'ntcfg-theme-settings');
            fields.appendChild(compactRoot);
        }

        // Track rendered elements for visibleWhen
        const visibilityMap = new Map(); // key -> { element, item }
        const renderedElements = new Map(); // key -> element ref returned

        const items = Array.isArray(section.items) ? section.items : [];

        items.forEach((item) => {
            if (item.type === 'note') {
                const noteEl = createElement('div', `ntmods-note ntmods-note--${item.tone || 'info'}`, item.message || '');
                (isCompact ? compactRoot : fields).appendChild(noteEl);
                return;
            }

            const targetRoot = isCompact ? compactRoot : fields;

            // Handle compound items (toggle with inline child controls)
            if (item.compound && Array.isArray(item.compound) && item.compound.length) {
                renderCompoundRow(targetRoot, item, ctx);
                return;
            }

            let rendered = null;

            if (isCompact) {
                // Compact layout uses ntcfg-theme-setting rows
                if (item.type === 'toggle') {
                    rendered = genericAppendThemeSettingToggle(targetRoot, item.label, item.key, item.default, ctx);
                } else if (item.type === 'select') {
                    rendered = genericAppendThemeSettingSelect(targetRoot, item.label, item.key, item.default, item.options || [], ctx);
                } else if (item.type === 'number') {
                    const numConfig = {
                        min: item.min,
                        max: item.max,
                        step: item.step,
                        presets: item.presets || [],
                        normalizeValue: item.normalize || undefined
                    };
                    rendered = genericAppendThemeSettingNumber(targetRoot, item.label, item.key, item.default, numConfig, ctx);
                } else if (item.type === 'color') {
                    // Render as a compact theme color row (toggle always on)
                    const row = createElement('div', 'ntcfg-theme-setting');
                    row.appendChild(createElement('div', 'ntcfg-theme-setting-title', item.label));
                    const controls = createElement('div', 'ntcfg-theme-setting-controls');
                    const colorControl = createCompactColorControl(
                        readModuleFieldValue(module, fieldMap, item.key, { default: item.default, type: 'color' }),
                        (value) => {
                            writeModuleFieldValue(module, fieldMap, item.key, value, { default: item.default, type: 'color' });
                            refreshPreviews();
                        }
                    );
                    controls.appendChild(colorControl.root);
                    row.appendChild(controls);
                    targetRoot.appendChild(row);
                    rendered = { row };
                }
            } else {
                // Standard layout
                if (item.type === 'toggle') {
                    rendered = genericAppendCheckbox(targetRoot, {
                        labelText: item.label,
                        key: item.key,
                        defaultValue: item.default,
                        helpText: item.help || ''
                    }, ctx);
                } else if (item.type === 'number') {
                    const warnOpts = item.warn || {};
                    rendered = genericAppendNumberField(targetRoot, {
                        labelText: item.label,
                        key: item.key,
                        defaultValue: item.default,
                        helpText: item.help || '',
                        min: item.min,
                        max: item.max,
                        step: item.step || '1',
                        minRecommended: warnOpts.below != null ? warnOpts.below : null,
                        warningText: warnOpts.message || ''
                    }, ctx);
                } else if (item.type === 'select') {
                    rendered = genericAppendSelectField(targetRoot, {
                        labelText: item.label,
                        key: item.key,
                        defaultValue: item.default,
                        options: item.options || [],
                        helpText: item.help || ''
                    }, ctx);
                } else if (item.type === 'color') {
                    rendered = genericAppendCheckbox(targetRoot, {
                        labelText: item.label,
                        key: item.key,
                        defaultValue: item.default,
                        helpText: item.help || ''
                    }, ctx);
                } else if (item.type === 'text') {
                    rendered = genericAppendTextField(targetRoot, {
                        labelText: item.label,
                        key: item.key,
                        defaultValue: item.default || '',
                        helpText: item.help || '',
                        placeholder: item.placeholder || ''
                    }, ctx);
                } else if (item.type === 'action') {
                    const btn = createElement('button', `ntcfg-action ntcfg-inline-action ntcfg-action--${item.style || 'secondary'}`, item.label);
                    btn.type = 'button';
                    btn.addEventListener('click', () => { showToast('Preview action only for now.'); });
                    targetRoot.appendChild(btn);
                    rendered = { row: btn };
                }
            }

            // Store for visibility tracking
            if (item.key && rendered) {
                const el = rendered.row || rendered.field || null;
                if (el) {
                    renderedElements.set(item.key, el);
                }
            }

            // Handle visibleWhen
            if (item.visibleWhen && item.key && rendered) {
                const el = rendered.row || rendered.field || null;
                if (el) {
                    visibilityMap.set(item.key, { element: el, condition: item.visibleWhen });
                }
            }
        });

        // Apply initial visibleWhen state and wire onChange cascades
        visibilityMap.forEach(({ element, condition }) => {
            const depKey = condition.key;
            const depItem = fieldMap.get(depKey);
            if (!depItem) return;
            const currentValue = readModuleFieldValue(module, fieldMap, depKey, { default: depItem.default, type: depItem.type });
            const shouldShow = condition.eq != null ? currentValue === condition.eq : !!currentValue;
            element.hidden = !shouldShow;
        });

        // Wire up onChange cascades: find items that other items depend on
        const depKeys = new Set();
        visibilityMap.forEach(({ condition }) => depKeys.add(condition.key));

        // For each dependency key, we need to re-check visibility on change
        // This is already handled via rerender on toggle changes, but we need to also
        // hook into the initial item rendering. Since all toggle changes trigger refreshPreviews,
        // we hook into refreshPreviews to also update visibility.
        const originalRefreshPreviews = refreshPreviews;
        const augmentedRefreshPreviews = () => {
            originalRefreshPreviews();
            visibilityMap.forEach(({ element, condition }) => {
                const depItem = fieldMap.get(condition.key);
                if (!depItem) return;
                const val = readModuleFieldValue(module, fieldMap, condition.key, { default: depItem.default, type: depItem.type });
                const shouldShow = condition.eq != null ? val === condition.eq : !!val;
                element.hidden = !shouldShow;
            });
        };
        // Patch the ctx refresh so all child controls use the augmented version
        ctx.refreshPreviews = augmentedRefreshPreviews;
        refreshPreviewFns.push(() => {
            visibilityMap.forEach(({ element, condition }) => {
                const depItem = fieldMap.get(condition.key);
                if (!depItem) return;
                const val = readModuleFieldValue(module, fieldMap, condition.key, { default: depItem.default, type: depItem.type });
                const shouldShow = condition.eq != null ? val === condition.eq : !!val;
                element.hidden = !shouldShow;
            });
        });

        // Render reset button
        if (section.resetButton) {
            const label = typeof section.resetButton === 'string'
                ? section.resetButton
                : `Reset ${section.title} to Defaults`;
            // Compute defaults from item metadata
            const defaults = {};
            items.forEach((item) => {
                if (item.key && item.default !== undefined) {
                    defaults[item.key] = item.default;
                }
                // Also collect compound child defaults
                if (item.compound && Array.isArray(item.compound)) {
                    item.compound.forEach((child) => {
                        if (child.key && child.default !== undefined) {
                            defaults[child.key] = child.default;
                        }
                    });
                }
            });
            genericAppendResetButton(fields, label, defaults, ctx);
        }

        refreshPreviews();
    }

    function renderCompoundRow(root, item, ctx) {
        const { module, fieldMap, refreshPreviews } = ctx;
        const isCompactLayout = root.classList.contains('ntcfg-theme-settings');

        if (isCompactLayout) {
            // Compound in compact layout: ntcfg-theme-setting row with inline controls
            const row = createElement('div', 'ntcfg-theme-setting');
            row.appendChild(createElement('div', 'ntcfg-theme-setting-title', item.label));
            const controls = createElement('div', 'ntcfg-theme-setting-controls');

            item.compound.forEach((child) => {
                if (child.type === 'color') {
                    const colorControl = createCompactColorControl(
                        readModuleFieldValue(module, fieldMap, child.key, { default: child.default, type: 'color' }),
                        (value) => {
                            writeModuleFieldValue(module, fieldMap, child.key, value, { default: child.default, type: 'color' });
                            refreshPreviews();
                        }
                    );
                    controls.appendChild(colorControl.root);
                } else if (child.type === 'select') {
                    const safeOptions = Array.isArray(child.options) ? child.options.filter(Boolean) : [];
                    const select = createElement('select', 'ntcfg-input ntcfg-theme-input-compact ntcfg-theme-input-select');
                    safeOptions.forEach((optionDef) => {
                        const option = document.createElement('option');
                        option.value = String(optionDef.value);
                        option.textContent = String(optionDef.label ?? optionDef.value);
                        select.appendChild(option);
                    });
                    const normalizeSelectedValue = (value) => {
                        const raw = String(value ?? '').trim();
                        const matched = safeOptions.find((option) => String(option.value) === raw);
                        return matched ? matched.value : child.default;
                    };
                    select.value = String(normalizeSelectedValue(readModuleFieldValue(module, fieldMap, child.key, { default: child.default, type: 'select', options: child.options })));
                    select.addEventListener('change', () => {
                        const normalized = normalizeSelectedValue(select.value);
                        select.value = String(normalized);
                        writeModuleFieldValue(module, fieldMap, child.key, normalized, { default: child.default, type: 'select', options: child.options });
                        if (child.onChange) child.onChange(normalized, row, select);
                        refreshPreviews();
                    });
                    controls.appendChild(select);
                } else if (child.type === 'toggle') {
                    const switchControl = createSwitch(
                        readModuleFieldValue(module, fieldMap, child.key, { default: child.default, type: 'toggle' }),
                        (checked) => {
                            writeModuleFieldValue(module, fieldMap, child.key, checked, { default: child.default, type: 'toggle' });
                            if (child.onChange) child.onChange(checked, row);
                            refreshPreviews();
                        }
                    );
                    controls.appendChild(switchControl.root);
                }
            });

            // The main item itself is the primary toggle
            if (item.type === 'toggle') {
                const mainSwitch = createSwitch(
                    readModuleFieldValue(module, fieldMap, item.key, { default: item.default, type: 'toggle' }),
                    (checked) => {
                        writeModuleFieldValue(module, fieldMap, item.key, checked, { default: item.default, type: 'toggle' });
                        refreshPreviews();
                    }
                );
                controls.appendChild(mainSwitch.root);
            }

            row.appendChild(controls);
            root.appendChild(row);
        } else {
            // Compound in standard layout: ntcfg-checkbox row with inline controls
            const row = createElement('div', 'ntcfg-checkbox');
            const copy = createElement('span', 'ntcfg-checkbox-copy');
            copy.append(
                createElement('span', 'ntcfg-checkbox-label', item.label),
                createElement('span', 'ntcfg-checkbox-help', item.help || '')
            );
            const controls = createElement('span', 'ntcfg-checkbox-controls');

            item.compound.forEach((child) => {
                if (child.type === 'color') {
                    const colorControl = createCompactColorControl(
                        readModuleFieldValue(module, fieldMap, child.key, { default: child.default, type: 'color' }),
                        (value) => {
                            writeModuleFieldValue(module, fieldMap, child.key, value, { default: child.default, type: 'color' });
                            refreshPreviews();
                        },
                        'ntcfg-checkbox-controls'
                    );
                    controls.appendChild(colorControl.root);
                } else if (child.type === 'select') {
                    const safeOptions = Array.isArray(child.options) ? child.options.filter(Boolean) : [];
                    const select = createElement('select', 'ntcfg-input ntcfg-theme-input-compact');
                    safeOptions.forEach((optionDef) => {
                        const option = document.createElement('option');
                        option.value = String(optionDef.value);
                        option.textContent = String(optionDef.label ?? optionDef.value);
                        select.appendChild(option);
                    });
                    const normalizeSelectedValue = (value) => {
                        const raw = String(value ?? '').trim();
                        const matched = safeOptions.find((option) => String(option.value) === raw);
                        return matched ? matched.value : child.default;
                    };
                    select.value = String(normalizeSelectedValue(readModuleFieldValue(module, fieldMap, child.key, { default: child.default, type: 'select', options: child.options })));
                    select.addEventListener('change', () => {
                        const normalized = normalizeSelectedValue(select.value);
                        select.value = String(normalized);
                        writeModuleFieldValue(module, fieldMap, child.key, normalized, { default: child.default, type: 'select', options: child.options });
                        refreshPreviews();
                    });
                    controls.appendChild(select);
                }
            });

            // Main toggle
            if (item.type === 'toggle') {
                const mainSwitch = createSwitch(
                    readModuleFieldValue(module, fieldMap, item.key, { default: item.default, type: 'toggle' }),
                    (checked) => {
                        writeModuleFieldValue(module, fieldMap, item.key, checked, { default: item.default, type: 'toggle' });
                        // Hide/show compound children based on toggle state
                        item.compound.forEach((child) => {
                            // Find the child control in controls and toggle visibility
                        });
                        refreshPreviews();
                    }
                );
                controls.appendChild(mainSwitch.root);
            }

            row.append(copy, controls);
            root.appendChild(row);
        }
    }

    function buildModuleCountPill(value, label) {
        return `
            <div class="ntmods-cap-stat">
                <span class="ntmods-cap-stat-value">${escapeHtml(value)}</span>
                <span class="ntmods-cap-stat-label">${escapeHtml(label)}</span>
            </div>
        `;
    }

    function getTopTabGrow(module) {
        const label = String(module?.label || '');
        return Math.max(1, Math.min(2.15, (label.length + 4) / 7.5));
    }

    function getModuleIconMarkup(module, variant = 'tab') {
        const iconClass = variant === 'chip'
            ? 'ntmods-mod-icon ntmods-mod-icon--chip'
            : 'ntmods-mod-icon ntmods-mod-icon--tab';

        switch (module?.id) {
            case 'race-options':
                return `
                <svg class="${iconClass}" viewBox="0 0 24 24" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
                    <path d="M4 6h16M4 12h16M4 18h16"></path>
                    <circle cx="9" cy="6" r="2"></circle>
                    <circle cx="15" cy="12" r="2"></circle>
                    <circle cx="11" cy="18" r="2"></circle>
                </svg>
            `;
            case 'music-player':
                return `
                <svg class="${iconClass}" viewBox="0 0 24 24" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round">
                    <path d="M10 18.5a2.5 2.5 0 1 1-2.5-2.5A2.5 2.5 0 0 1 10 18.5Zm9-2a2.5 2.5 0 1 1-2.5-2.5A2.5 2.5 0 0 1 19 16.5Z"></path>
                    <path d="M10 18.5V7.5l9-2v11"></path>
                </svg>
            `;
            case 'bot-flag':
                return `
                <svg class="${iconClass}" viewBox="0 0 24 24" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round">
                    <path d="M6 20V4"></path>
                    <path d="M6 5h11l-2.5 3 2.5 3H6"></path>
                </svg>
            `;
            case 'bot-hunter':
                return `
                <svg class="${iconClass}" viewBox="0 0 24 24" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
                    <circle cx="12" cy="12" r="4.25"></circle>
                    <circle cx="12" cy="12" r="1.2" fill="currentColor" stroke="none"></circle>
                    <path d="M12 3.5V6M12 18V20.5M3.5 12H6M18 12h2.5"></path>
                </svg>
            `;
            case 'leaderboards':
                return `
                <svg class="${iconClass}" viewBox="0 0 24 24" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round">
                    <path d="M4 20V11M10 20V6M16 20v-8M3 20h18"></path>
                </svg>
            `;
            case 'racer-badges':
                return `
                <svg class="${iconClass}" viewBox="0 0 24 24" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
                    <circle cx="12" cy="8.5" r="4.25"></circle>
                    <path d="M9.5 12.5 7.2 20l4.8-2.5 4.8 2.5-2.3-7.5"></path>
                </svg>
            `;
            default:
                return `
                <svg class="${iconClass}" viewBox="0 0 24 24" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
                    <path d="M4 6h16M4 12h16M4 18h16"></path>
                    <circle cx="9" cy="6" r="2"></circle>
                    <circle cx="15" cy="12" r="2"></circle>
                    <circle cx="11" cy="18" r="2"></circle>
                </svg>
            `;
        }
    }

    function buildTopTabHtml(module, isActive) {
        return `
            <button
                type="button"
                class="tab ntmods-top-tab${isActive ? ' is-active' : ''}${module.source === 'preview' ? ' is-preview' : ''}"
                data-module-id="${escapeHtml(module.id)}"
                style="--ntmods-tab-grow:${getTopTabGrow(module)}"
            >
                <div class="bucket bucket--c bucket--xs">
                    <div class="bucket-media">
                        <span class="ntmods-top-tab-glyph">${getModuleIconMarkup(module, 'tab')}</span>
                    </div>
                    <div class="bucket-content">${escapeHtml(module.label)}</div>
                </div>
            </button>
        `;
    }

    function buildSidebarButtonHtml(section, isActive) {
        return `
            <button type="button" class="ntmods-nav-btn${isActive ? ' is-active' : ''}" data-section-id="${escapeHtml(section.id)}">
                <span class="ntmods-nav-btn-title">${escapeHtml(section.title)}</span>
            </button>
        `;
    }

    function buildToggleHtml(module, item) {
        const value = Boolean(getFieldValue(module, item));
        return `
            <label class="ntmods-toggle-card">
                <span class="ntmods-toggle-copy">
                    <span class="ntmods-field-title">${escapeHtml(item.label)}</span>
                    <span class="ntmods-field-help">${escapeHtml(item.help || '')}</span>
                </span>
                <span class="ntmods-switch">
                    <input type="checkbox" data-field-key="${escapeHtml(item.key)}" ${value ? 'checked' : ''}>
                    <span class="ntmods-switch-track"></span>
                </span>
            </label>
        `;
    }

    function buildSelectOptions(item, value) {
        return (item.options || []).map((option) => {
            const selected = String(option.value) === String(value) ? 'selected' : '';
            return `<option value="${escapeHtml(option.value)}" ${selected}>${escapeHtml(option.label)}</option>`;
        }).join('');
    }

    function buildFieldHtml(module, item) {
        const value = getFieldValue(module, item);

        if (item.type === 'toggle') {
            return buildToggleHtml(module, item);
        }

        if (item.type === 'note') {
            return `<div class="ntmods-note ntmods-note--${escapeHtml(item.tone || 'info')}">${escapeHtml(item.message || '')}</div>`;
        }

        if (item.type === 'action') {
            return `
                <div class="ntmods-action-card">
                    <button type="button" class="ntmods-action-btn ntmods-action-btn--${escapeHtml(item.style || 'secondary')}" data-action-key="${escapeHtml(item.key)}">
                        ${escapeHtml(item.label)}
                    </button>
                    ${item.help ? `<div class="ntmods-field-help">${escapeHtml(item.help)}</div>` : ''}
                </div>
            `;
        }

        if (item.type === 'color') {
            const normalized = String(value || item.default || '#1C99F4');
            return `
                <label class="ntmods-field-card">
                    <span class="ntmods-field-title">${escapeHtml(item.label)}</span>
                    <span class="ntmods-color-row">
                        <input type="color" class="ntmods-color-picker" value="${escapeHtml(normalized)}" data-field-key="${escapeHtml(item.key)}" data-field-type="color">
                        <input type="text" class="ntmods-input ntmods-color-input" value="${escapeHtml(normalized)}" data-field-key="${escapeHtml(item.key)}" data-field-type="color-text" spellcheck="false">
                    </span>
                    ${item.help ? `<span class="ntmods-field-help">${escapeHtml(item.help)}</span>` : ''}
                </label>
            `;
        }

        if (item.type === 'select') {
            return `
                <label class="ntmods-field-card">
                    <span class="ntmods-field-title">${escapeHtml(item.label)}</span>
                    <select class="ntmods-input" data-field-key="${escapeHtml(item.key)}" data-field-type="select">
                        ${buildSelectOptions(item, value)}
                    </select>
                    ${item.help ? `<span class="ntmods-field-help">${escapeHtml(item.help)}</span>` : ''}
                </label>
            `;
        }

        if (item.type === 'number') {
            const minAttr = item.min == null ? '' : `min="${escapeHtml(item.min)}"`;
            const maxAttr = item.max == null ? '' : `max="${escapeHtml(item.max)}"`;
            const stepAttr = item.step == null ? '' : `step="${escapeHtml(item.step)}"`;
            return `
                <label class="ntmods-field-card">
                    <span class="ntmods-field-title">${escapeHtml(item.label)}</span>
                    <input type="number" class="ntmods-input" value="${escapeHtml(value)}" data-field-key="${escapeHtml(item.key)}" data-field-type="number" ${minAttr} ${maxAttr} ${stepAttr}>
                    ${item.help ? `<span class="ntmods-field-help">${escapeHtml(item.help)}</span>` : ''}
                </label>
            `;
        }

        return `
            <label class="ntmods-field-card">
                <span class="ntmods-field-title">${escapeHtml(item.label)}</span>
                <input type="text" class="ntmods-input" value="${escapeHtml(value ?? '')}" data-field-key="${escapeHtml(item.key)}" data-field-type="text" placeholder="${escapeHtml(item.placeholder || '')}" spellcheck="false">
                ${item.help ? `<span class="ntmods-field-help">${escapeHtml(item.help)}</span>` : ''}
            </label>
        `;
    }

    function buildSectionHtml(module, section) {
        // Manifest modules use the generic mount (DOM rendered after innerHTML)
        if (module?.source === 'manifest') {
            return `<div class="ntmods-generic-stage" data-ntmods-generic-stage="1"></div>`;
        }

        // Preview (non-manifest) modules use the card layout fallback
        const settingItems = section.items.filter((item) => item.type !== 'note');
        const toggleCount = settingItems.filter((item) => item.type === 'toggle').length;
        const inputCount = settingItems.length - toggleCount;
        const modeLabel = 'Preview Layout';

        return `
            <div class="ntmods-stage-intro">
                <div class="ntmods-stage-intro-copy">
                    <h2 class="ntmods-stage-title">${escapeHtml(section.title)}</h2>
                    <p class="ntmods-stage-subtitle">${escapeHtml(section.subtitle || '')}</p>
                </div>
                <div class="ntmods-stage-pills">
                    <span class="ntmods-pill">${escapeHtml(modeLabel)}</span>
                    <span class="ntmods-pill">${escapeHtml(toggleCount)} toggles</span>
                    <span class="ntmods-pill">${escapeHtml(inputCount)} inputs</span>
                </div>
            </div>
            <div class="ntmods-grid">
                ${section.items.map((item) => buildFieldHtml(module, item)).join('')}
            </div>
        `;
    }

    function buildPageHtml(modules, activeModuleId, activeSectionId) {
        const activeModule = modules.find((module) => module.id === activeModuleId) || modules[0] || null;
        const activeModuleSections = getRenderableSections(activeModule);
        const activeSection = activeModule
            ? activeModuleSections.find((section) => section.id === activeSectionId) || activeModuleSections[0]
            : null;


        if (!activeModule || !activeSection) {
            return `
                <section id="ntmods-app" class="card card--b card--o card--shadow card--f card--grit well well--b well--l ntmods-shell">
                    <div class="card-cap bg--gradient ntmods-cap">
                        <h1 class="h2 tbs ntmods-cap-title">Mod Menu</h1>
                    </div>
                    <div class="well--p well--l_p">
                        <div class="ntmods-empty">No installed mods were detected yet.</div>
                    </div>
                </section>
            `;
        }

        return `
            <section id="ntmods-app" class="card card--b card--o card--shadow card--f card--grit well well--b well--l ntmods-shell">
                <div class="card-cap bg--gradient ntmods-cap">
                    <h1 class="h2 tbs ntmods-cap-title">Mod Menu</h1>
                </div>
                <div class="well--p well--l_p">
                    <div class="tabs tabs--a ntmods-top-tabs">
                        ${modules.map((module) => buildTopTabHtml(module, module.id === activeModule.id)).join('')}
                    </div>
                    <div class="ntmods-module-card">
                        <div class="ntmods-module-head">
                            <div class="ntmods-module-head-copy">
                                <div class="ntmods-module-overline">${activeModule.source === 'manifest' ? 'Connected Module' : 'Not Installed'}</div>
                                <h2 class="ntmods-module-title">${escapeHtml(activeModule.label)}</h2>
                                <p class="ntmods-module-description">${escapeHtml(activeModule.description || '')}</p>
                            </div>
                            <div class="ntmods-module-actions">
                                <button type="button" class="ntmods-rescan-btn" data-action-key="rescan">Rescan Mods</button>
                            </div>
                        </div>
                        ${activeModule.source === 'preview' ? `
                            <div class="ntmods-not-installed">
                                <div class="ntmods-not-installed-icon">
                                    ${getModuleIconMarkup(activeModule, 'large')}
                                </div>
                                <h3 class="ntmods-not-installed-title">Not Installed</h3>
                                <p class="ntmods-not-installed-copy">You don't have <strong>${escapeHtml(activeModule.label)}</strong> installed yet. Install it from Greasyfork to unlock these settings.</p>
                                ${MODULE_META[activeModule.id]?.installUrl ? `
                                    <a href="${escapeHtml(MODULE_META[activeModule.id].installUrl)}" target="_blank" rel="noopener noreferrer" class="ntmods-install-link">
                                        Install ${escapeHtml(activeModule.label)} on Greasyfork &rarr;
                                    </a>
                                ` : ''}
                            </div>
                        ` : `
                            <div class="ntmods-layout">
                                <aside class="ntmods-sidebar">
                                    ${activeModuleSections.map((section) => buildSidebarButtonHtml(section, section.id === activeSection.id)).join('')}
                                </aside>
                                <section class="ntmods-stage">
                                    ${buildSectionHtml(activeModule, activeSection)}
                                </section>
                            </div>
                        `}
                    </div>
                </div>
            </section>
        `;
    }

    function showToast(message) {
        let toast = document.getElementById('ntmods-toast');
        if (!toast) {
            toast = document.createElement('div');
            toast.id = 'ntmods-toast';
            toast.className = 'ntmods-toast';
            document.body.appendChild(toast);
        }
        toast.textContent = message;
        toast.classList.add('is-visible');
        window.clearTimeout(showToast._timer);
        showToast._timer = window.setTimeout(() => {
            toast.classList.remove('is-visible');
        }, 1800);
    }

    function ensureStyles() {
        if (document.getElementById('ntmods-style')) return;
        const style = document.createElement('style');
        style.id = 'ntmods-style';
        style.textContent = `
html.is-mods-route main.structure-content {
    opacity: 0 !important;
    visibility: hidden !important;
}
html.is-mods-route main.structure-content.custom-loaded {
    opacity: 1 !important;
    visibility: visible !important;
    transition: opacity 0.16s ease-in;
}
.ntmods-dropdown-item.is-current .dropdown-link {
    background: rgba(255, 255, 255, 0.08);
    color: #fff;
}
.ntmods-dropdown-item .dropdown-link .icon {
    opacity: 0.92;
}
.ntmods-dropdown-item .dropdown-link .ntmods-inline-icon {
    opacity: 1;
    color: #9fb2c6 !important;
    fill: currentColor !important;
}
.ntmods-dropdown-item .dropdown-link:hover .ntmods-inline-icon,
.ntmods-dropdown-item.is-current .dropdown-link .ntmods-inline-icon {
    color: #c8d5e2 !important;
    fill: currentColor !important;
}
.ntmods-inline-icon {
    width: 16px;
    height: 16px;
    display: inline-block;
    vertical-align: -3px;
    shape-rendering: geometricPrecision;
}
.ntmods-shell {
    color: #eef3ff;
    display: flex;
    flex-direction: column;
}
.ntmods-shell > .well--p.well--l_p {
    padding-bottom: 0 !important;
    flex: 1 1 auto;
    display: flex;
    flex-direction: column;
}
.ntmods-cap {
    display: flex;
    justify-content: flex-start;
    gap: 0;
    align-items: center;
    padding: 12px 18px;
    background: url(/dist/site/images/backgrounds/bg-noise.png) top left repeat, linear-gradient(90deg, #1c99f4 0%, #167ac3 42%, #0f4f86 100%);
    background-attachment: fixed, scroll;
}
.ntmods-cap-copy {
    min-width: 0;
    max-width: 720px;
}
.ntmods-cap-kicker {
    margin-bottom: 8px;
    text-transform: uppercase;
    letter-spacing: 0.12em;
    font-size: 11px;
    font-weight: 700;
    color: rgba(255, 255, 255, 0.82);
}
.ntmods-cap-title {
    margin: 0;
}
.ntmods-cap-subtitle {
    margin: 0;
    font-size: 12px;
    line-height: 1.45;
    color: rgba(255, 255, 255, 0.92);
}
.ntmods-cap-stats {
    display: grid;
    grid-template-columns: repeat(3, minmax(96px, 1fr));
    gap: 10px;
    min-width: 290px;
}
.ntmods-cap-stat {
    display: flex;
    flex-direction: column;
    justify-content: center;
    padding: 12px 14px;
    border-radius: 12px;
    background: rgba(10, 18, 30, 0.24);
    backdrop-filter: blur(1px);
    border: 1px solid rgba(255, 255, 255, 0.12);
    box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.08);
}
.ntmods-cap-stat-value {
    font-family: "Montserrat", sans-serif;
    font-size: 24px;
    line-height: 1;
    font-weight: 700;
    color: #fff;
}
.ntmods-cap-stat-label {
    margin-top: 5px;
    font-size: 11px;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    color: rgba(255, 255, 255, 0.76);
}
.ntmods-banner {
    margin-bottom: 14px;
    padding: 12px 14px;
    border-radius: 12px;
    background: linear-gradient(135deg, rgba(255, 193, 7, 0.18), rgba(255, 146, 43, 0.16));
    border: 1px solid rgba(255, 196, 79, 0.26);
    color: #ffe8b6;
}
.ntmods-banner-title {
    font-family: "Montserrat", sans-serif;
    font-size: 14px;
    font-weight: 700;
    margin-bottom: 5px;
    color: #fff0c9;
}
.ntmods-banner-copy {
    font-size: 12px;
    line-height: 1.55;
}
.ntmods-top-tabs {
    display: flex;
    flex-wrap: nowrap;
    gap: 2px;
    align-items: flex-end;
    overflow: hidden;
    margin: 0 0 0 0;
    padding: 0 2px;
    width: 100%;
    min-width: 0;
    position: relative;
    z-index: 2;
}
.ntmods-top-tab {
    appearance: none;
    flex: var(--ntmods-tab-grow, 1) 1 0;
    min-width: 0;
    border: 1px solid transparent;
    border-bottom: 0;
    border-radius: 8px 8px 0 0;
    background: transparent;
    color: rgba(174, 180, 199, 0.7);
    padding: 14px 14px 13px;
    cursor: pointer;
    position: relative;
    transition: background 0.12s ease, color 0.12s ease, border-color 0.12s ease;
    box-shadow: none;
}
.ntmods-top-tab:hover {
    background: rgba(255, 255, 255, 0.04);
    border-color: rgba(255, 255, 255, 0.05);
    color: #eff3ff;
}
.ntmods-top-tab.is-active {
    background: linear-gradient(180deg, #333752 0%, #2a2d3d 100%);
    color: #fff;
    border-color: rgba(255, 255, 255, 0.09);
    box-shadow: 0 8px 16px rgba(0, 0, 0, 0.12), inset 0 1px 0 rgba(255, 255, 255, 0.06);
    z-index: 3;
}
.ntmods-top-tab.is-active::after {
    content: "";
    position: absolute;
    left: -1px;
    right: -1px;
    bottom: -1px;
    height: 3px;
    background: #2a2d3d;
}
.ntmods-top-tab .bucket {
    width: 100%;
    gap: 6px;
}
.ntmods-top-tab .bucket-content {
    min-width: 0;
    font-family: "Montserrat", sans-serif;
    font-size: 13px;
    font-weight: 600;
    line-height: 1.2;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
.ntmods-top-tab-glyph {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    color: #b8c4da;
}
.ntmods-top-tab.is-active .ntmods-top-tab-glyph {
    color: #edf3ff;
}
.ntmods-mod-icon {
    display: block;
    width: 100%;
    height: 100%;
}
.ntmods-mod-icon--tab {
    width: 15px;
    height: 15px;
}
.ntmods-mod-icon--chip {
    width: 20px;
    height: 20px;
}
.ntmods-module-card {
    display: flex;
    flex-direction: column;
    min-height: 900px;
    flex: 1 1 auto;
    border-radius: 0 0 18px 18px;
    background: url(/dist/site/images/backgrounds/bg-noise.png) top left repeat, linear-gradient(180deg, #2a2d3d 0%, #232636 100%);
    background-attachment: fixed, scroll;
    border: 1px solid rgba(255, 255, 255, 0.09);
    border-top: none;
    box-shadow: 0 18px 50px rgba(0, 0, 0, 0.24);
    overflow: hidden;
    margin-top: 0;
    position: relative;
    z-index: 1;
}
.ntmods-module-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 18px;
    padding: 18px 20px;
    background: transparent;
    border-bottom: 1px solid rgba(255, 255, 255, 0.08);
}
.ntmods-module-head-copy {
    min-width: 0;
}
.ntmods-module-overline {
    margin-bottom: 4px;
    text-transform: uppercase;
    letter-spacing: 0.09em;
    font-size: 11px;
    color: #8ea4d0;
}
.ntmods-module-title {
    margin: 0;
    font-family: "Montserrat", sans-serif;
    font-size: 28px;
    line-height: 1.08;
    color: #fff;
}
.ntmods-module-description {
    margin: 6px 0 0;
    font-size: 13px;
    line-height: 1.5;
    color: #adbbdb;
}
.ntmods-module-actions {
    display: flex;
    align-items: center;
    gap: 0;
    flex: 0 0 auto;
}
.ntmods-rescan-btn {
    appearance: none;
    border: 1px solid rgba(255, 255, 255, 0.12);
    background: rgba(255, 255, 255, 0.04);
    color: #edf3ff;
    border-radius: 10px;
    padding: 10px 12px;
    cursor: pointer;
    font-size: 12px;
    font-weight: 600;
}
.ntmods-rescan-btn:hover {
    background: rgba(255, 255, 255, 0.08);
}
.ntmods-layout {
    display: flex;
    gap: 18px;
    padding: 18px;
    background: transparent;
}
.ntmods-sidebar {
    width: 264px;
    max-width: 264px;
    display: flex;
    flex-direction: column;
    gap: 2px;
}
.ntmods-nav-btn {
    box-shadow: none;
    justify-content: flex-start;
    width: 100%;
    backface-visibility: hidden;
    background: #393c50;
    border: 1px solid transparent;
    color: #a6aac1;
    cursor: pointer;
    display: inline-flex;
    align-items: center;
    font-family: "Montserrat", sans-serif;
    font-size: 14px;
    overflow: hidden;
    padding: 13px 16px;
    position: relative;
    text-align: left;
    transition: all 0.12s linear;
}
.ntmods-nav-btn:first-child {
    border-radius: 5px 5px 0 0;
}
.ntmods-nav-btn:last-child {
    border-radius: 0 0 5px 5px;
}
.ntmods-nav-btn:hover {
    background: #585e7d;
    color: #e2e3eb;
}
.ntmods-nav-btn.is-active {
    background: #167ac3 !important;
    color: #fff;
    text-shadow: 0 2px 2px rgba(2, 2, 2, 0.25);
}
.ntmods-nav-btn-title {
    font-size: 13px;
    font-weight: 500;
}
.ntmods-stage {
    min-width: 0;
    flex: 1;
    border-radius: 10px;
    background: #2b2e3f;
    border: 1px solid rgba(255, 255, 255, 0.08);
    padding: 18px;
}
.ntmods-stage-intro {
    display: flex;
    justify-content: space-between;
    gap: 16px;
    align-items: flex-start;
    margin-bottom: 18px;
}
.ntmods-stage-title {
    margin: 0;
    font-family: "Montserrat", sans-serif;
    font-size: 22px;
    line-height: 1.1;
    color: #fff;
}
.ntmods-stage-subtitle {
    margin: 6px 0 0;
    font-size: 13px;
    line-height: 1.55;
    color: #aebadc;
}
.ntmods-stage-pills {
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
    justify-content: flex-end;
}
.ntmods-pill {
    display: inline-flex;
    align-items: center;
    border-radius: 999px;
    padding: 7px 10px;
    background: rgba(255, 255, 255, 0.06);
    border: 1px solid rgba(255, 255, 255, 0.08);
    font-size: 11px;
    letter-spacing: 0.03em;
    text-transform: uppercase;
    color: #c5d4f6;
}
.ntmods-grid {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 12px;
}
@media (max-width: 1220px) {
    .ntmods-top-tabs {
        gap: 3px;
    }
    .ntmods-top-tab {
        padding: 13px 11px 12px;
    }
    .ntmods-top-tab .bucket {
        gap: 7px;
    }
    .ntmods-top-tab .bucket-content {
        font-size: 12px;
    }
    .ntmods-mod-icon--tab {
        width: 14px;
        height: 14px;
    }
}
.ntmods-field-card,
.ntmods-action-card,
.ntmods-note,
.ntmods-toggle-card {
    box-sizing: border-box;
    min-width: 0;
}
.ntmods-field-card,
.ntmods-action-card {
    display: flex;
    flex-direction: column;
    gap: 8px;
    padding: 14px;
    border-radius: 14px;
    background: #2e3346;
    border: 1px solid rgba(255, 255, 255, 0.08);
}
.ntmods-toggle-card {
    display: flex;
    justify-content: space-between;
    gap: 14px;
    padding: 14px;
    border-radius: 14px;
    background: #2e3346;
    border: 1px solid rgba(255, 255, 255, 0.08);
}
.ntmods-toggle-copy {
    display: flex;
    flex-direction: column;
    gap: 6px;
    min-width: 0;
}
.ntmods-field-title {
    font-size: 13px;
    font-weight: 700;
    color: #edf3ff;
}
.ntmods-field-help {
    font-size: 12px;
    line-height: 1.45;
    color: #9aa8c9;
}
.ntmods-input {
    width: 100%;
    box-sizing: border-box;
    border: 1px solid rgba(255, 255, 255, 0.12);
    border-radius: 10px;
    background: #1d2030;
    color: #eef3ff;
    padding: 10px 11px;
    font-size: 13px;
}
.ntmods-input:focus {
    outline: none;
    border-color: #1c99f4;
    box-shadow: 0 0 0 2px rgba(28, 153, 244, 0.22);
}
.ntmods-color-row {
    display: flex;
    align-items: center;
    gap: 8px;
}
.ntmods-color-picker {
    width: 48px;
    min-width: 48px;
    height: 42px;
    border-radius: 10px;
    background: #1d2030;
    border: 1px solid rgba(255, 255, 255, 0.14);
    padding: 0;
}
.ntmods-color-input {
    text-transform: uppercase;
}
.ntmods-switch {
    position: relative;
    width: 42px;
    height: 24px;
    flex: 0 0 auto;
}
.ntmods-switch input {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    margin: 0;
    opacity: 0;
    cursor: pointer;
    z-index: 2;
}
.ntmods-switch-track {
    display: block;
    width: 100%;
    height: 100%;
    border-radius: 999px;
    background: #5b627f;
    box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.1);
    transition: background 0.14s ease;
}
.ntmods-switch-track::after {
    content: "";
    position: absolute;
    top: 2px;
    left: 2px;
    width: 20px;
    height: 20px;
    border-radius: 50%;
    background: #fff;
    transition: transform 0.12s ease;
}
.ntmods-switch input:checked + .ntmods-switch-track {
    background: #d62f3a;
    box-shadow: 0 4px 18px rgba(214, 47, 58, 0.28);
}
.ntmods-switch input:checked + .ntmods-switch-track::after {
    transform: translateX(18px);
}
.ntmods-action-btn {
    appearance: none;
    border: 0;
    border-radius: 10px;
    padding: 11px 14px;
    cursor: pointer;
    font-size: 13px;
    font-weight: 700;
    color: #fff;
}
.ntmods-action-btn--primary {
    background: #167ac3;
}
.ntmods-action-btn--secondary {
    background: #434965;
}
.ntmods-action-btn--danger {
    background: #a5333d;
}
.ntmods-action-btn:hover {
    filter: brightness(1.08);
}
.ntmods-note {
    grid-column: 1 / -1;
    padding: 14px;
    border-radius: 14px;
    font-size: 12px;
    line-height: 1.55;
    border: 1px solid transparent;
}
.ntmods-note--info {
    background: rgba(28, 153, 244, 0.1);
    border-color: rgba(28, 153, 244, 0.22);
    color: #cde6ff;
}
.ntmods-note--warning {
    background: rgba(255, 176, 32, 0.12);
    border-color: rgba(255, 176, 32, 0.22);
    color: #ffe1a6;
}
.ntmods-empty {
    padding: 28px;
    text-align: center;
    border-radius: 16px;
    background: rgba(255, 255, 255, 0.04);
    color: #d2dcf5;
}
.ntmods-race-options-stage,
.ntmods-generic-stage {
    min-width: 0;
}
.ntmods-ro-panel {
    min-width: 0;
}
.ntcfg-panel-title {
    margin: 0;
    font-size: 20px;
    font-family: "Montserrat", sans-serif;
    color: #fff;
}
.ntcfg-panel-subtitle {
    margin: 6px 0 16px;
    color: #b5bad3;
    font-size: 13px;
    line-height: 1.45;
}
.ntcfg-fields {
    display: flex;
    flex-direction: column;
    gap: 12px;
}
.ntcfg-field {
    display: block;
}
.ntcfg-field-title {
    color: #d8dcf2;
    font-size: 13px;
    margin-bottom: 6px;
}
.ntcfg-field-help {
    margin-top: 5px;
    color: #99a4c5;
    font-size: 12px;
    line-height: 1.35;
}
.ntcfg-field-warning {
    color: #ffb8b8;
}
.ntcfg-input {
    width: 100%;
    box-sizing: border-box;
    border: 1px solid rgba(255, 255, 255, 0.12);
    border-radius: 8px;
    background: #1d2030;
    color: #eef3ff;
    padding: 10px;
    font-size: 13px;
}
.ntcfg-input:focus {
    outline: none;
    border-color: #1c99f4;
    box-shadow: 0 0 0 2px rgba(28, 153, 244, 0.28);
}
.ntcfg-input.ntcfg-input-warning {
    border-color: rgba(255, 90, 90, 0.85);
    box-shadow: 0 0 0 2px rgba(255, 90, 90, 0.2);
}
.ntcfg-color-row {
    display: flex;
    gap: 8px;
}
.ntcfg-color-picker {
    width: 52px;
    min-width: 52px;
    padding: 0;
    border-radius: 8px;
    border: 1px solid rgba(255, 255, 255, 0.18);
    background: #1d2030;
    cursor: pointer;
}
.ntcfg-color-hex {
    flex: 1;
    text-transform: uppercase;
}
.ntcfg-checkbox {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
    border: 1px solid rgba(255, 255, 255, 0.08);
    border-radius: 8px;
    background: #252839;
    padding: 10px 12px;
}
.ntcfg-checkbox-label {
    color: #d8dcf2;
    font-size: 13px;
}
.ntcfg-checkbox-copy {
    display: flex;
    flex-direction: column;
    gap: 4px;
    min-width: 0;
}
.ntcfg-checkbox-help {
    color: #99a4c5;
    font-size: 12px;
    line-height: 1.35;
}
.ntcfg-checkbox-controls {
    display: flex;
    align-items: center;
    gap: 8px;
    flex-shrink: 0;
}
.ntcfg-checkbox-controls .ntcfg-color-picker {
    width: 38px;
    min-width: 38px;
    height: 30px;
}
.ntcfg-checkbox-controls .ntcfg-input {
    width: 84px;
    padding: 6px 8px;
    font-size: 12px;
}
.ntcfg-checkbox-controls select.ntcfg-input {
    width: auto;
}
.ntcfg-switch {
    position: relative;
    width: 40px;
    height: 24px;
    flex: 0 0 auto;
    display: inline-block;
    cursor: pointer;
}
.ntcfg-switch input {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    opacity: 0;
    cursor: pointer;
    margin: 0;
    z-index: 2;
}
.ntcfg-switch-track {
    display: block;
    width: 100%;
    height: 100%;
    border-radius: 999px;
    background: #585e7d;
    transition: background 0.15s ease;
    box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.08);
    pointer-events: none;
}
.ntcfg-switch-track::after {
    content: "";
    position: absolute;
    top: 2px;
    left: 2px;
    width: 20px;
    height: 20px;
    border-radius: 50%;
    background: #fff;
    transition: transform 0.1s ease;
}
.ntcfg-switch input:checked + .ntcfg-switch-track {
    background: #d62f3a;
    box-shadow: 0 2px 20px rgba(214, 47, 58, 0.35);
}
.ntcfg-switch input:checked + .ntcfg-switch-track::after {
    transform: translateX(16px);
}
.ntcfg-action {
    border: 0;
    border-radius: 8px;
    padding: 10px 14px;
    font-size: 13px;
    cursor: pointer;
    color: #fff;
    background: #393c50;
}
.ntcfg-action:hover {
    filter: brightness(1.1);
}
.ntcfg-action.ntcfg-primary {
    background: #167ac3;
}
.ntcfg-inline-action {
    align-self: flex-start;
}
.ntcfg-theme-preview-label {
    color: #d8dcf2;
    font-size: 13px;
    margin-bottom: 4px;
}
.ntcfg-theme-preview-grid {
    display: grid;
    grid-template-columns: 1fr;
    gap: 8px;
    margin-bottom: 12px;
}
.ntcfg-theme-preview {
    width: 100%;
    box-sizing: border-box;
    padding: 12px;
    margin-bottom: 0;
    border-radius: 5px;
    background: #e9eaeb;
    color: #2e3141;
}
.ntcfg-theme-preview span {
    font-family: "Roboto Mono", "Courier New", Courier, "Lucida Sans Typewriter", "Lucida Typewriter", monospace;
    display: inline-block;
    padding: 2px;
    font-size: 18px;
    white-space: pre;
}
.ntcfg-theme-preview span.ntcfg-theme-char-active {
    background: #1c99f4;
    color: #fff;
}
.ntcfg-theme-preview span.ntcfg-theme-char-incorrect {
    background: #d62f3a;
    color: #fff;
}
.ntcfg-theme-preview span.ntcfg-theme-char-typed {
    color: #2e3141;
    opacity: 0.5;
}
.ntcfg-theme-preview span.ntcfg-theme-char-rainbow {
    animation: ntcfg-preview-rainbow-text 10s infinite alternate;
    -webkit-animation: ntcfg-preview-rainbow-text 10s infinite alternate;
    opacity: 1;
}
@keyframes ntcfg-preview-rainbow-text {
    0% { color: blue; }
    10% { color: #ff005d; }
    20% { color: #f0f; }
    30% { color: black; }
    40% { color: #7500ff; }
    50% { color: blue; }
    60% { color: #f0f; }
    70% { color: black; }
    80% { color: black; }
    90% { color: red; }
    100% { color: red; }
}
.ntcfg-sticky-preview {
    position: sticky;
    top: 0;
    z-index: 10;
    background: #2b2e3f;
    padding-bottom: 12px;
    margin-bottom: 4px;
    border-bottom: 1px solid rgba(255, 255, 255, 0.06);
}
.ntcfg-pn-preview {
    width: 100%;
    box-sizing: border-box;
    padding: 12px;
    border-radius: 5px;
    background: #e9eaeb;
    color: #2e3141;
    margin-bottom: 0;
    overflow: hidden;
    word-wrap: break-word;
}
.ntcfg-pn-preview span {
    font-family: "Roboto Mono", "Courier New", Courier, "Lucida Sans Typewriter", "Lucida Typewriter", monospace;
    display: inline-block;
    padding: 2px;
    font-size: 18px;
    white-space: pre;
}
.ntcfg-pn-preview-rainbow {
    animation: ntcfg-pn-preview-rainbow 3s linear infinite;
    -webkit-animation: ntcfg-pn-preview-rainbow 3s linear infinite;
}
@keyframes ntcfg-pn-preview-rainbow {
    0% { color: #FF0000; }
    14% { color: #FF8C00; }
    28% { color: #FFD700; }
    42% { color: #00CC00; }
    57% { color: #0066FF; }
    71% { color: #7B00FF; }
    85% { color: #FF00FF; }
    100% { color: #FF0000; }
}
.ntcfg-theme-settings {
    display: flex;
    flex-direction: column;
    gap: 8px;
}
.ntcfg-theme-setting {
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto;
    align-items: center;
    gap: 10px;
    border: 1px solid rgba(255, 255, 255, 0.08);
    border-radius: 8px;
    background: #252839;
    padding: 8px 10px;
}
.ntcfg-theme-setting-title {
    color: #d8dcf2;
    font-size: 13px;
}
.ntcfg-theme-setting-controls {
    display: flex;
    align-items: center;
    gap: 8px;
}
.ntcfg-theme-color-compact {
    display: flex;
    align-items: center;
    gap: 6px;
}
.ntcfg-theme-color-compact .ntcfg-color-picker {
    width: 38px;
    min-width: 38px;
    height: 30px;
}
.ntcfg-theme-color-compact .ntcfg-input {
    width: 96px;
    padding: 6px 8px;
    font-size: 12px;
}
.ntcfg-theme-input-compact {
    width: auto;
    padding: 6px 8px;
    font-size: 12px;
}
.ntcfg-theme-input-select {
    width: 180px;
}
.ntcfg-theme-input-number {
    width: 96px;
}
.ntcfg-theme-input-preset {
    width: 132px;
}
@media (min-width: 900px) {
    .ntcfg-theme-preview-grid {
        grid-template-columns: 1fr 1fr;
    }
}
.ntmods-toast {
    position: fixed;
    right: 16px;
    bottom: 16px;
    z-index: 100002;
    padding: 12px 14px;
    border-radius: 12px;
    background: rgba(10, 16, 26, 0.96);
    color: #eff5ff;
    border: 1px solid rgba(255, 255, 255, 0.12);
    box-shadow: 0 16px 38px rgba(0, 0, 0, 0.38);
    opacity: 0;
    transform: translateY(8px);
    pointer-events: none;
    transition: opacity 0.16s ease, transform 0.16s ease;
    font-size: 12px;
    line-height: 1.35;
}
.ntmods-toast.is-visible {
    opacity: 1;
    transform: translateY(0);
}
@media (max-width: 1100px) {
    .ntmods-cap {
        flex-direction: column;
    }
    .ntmods-cap-stats {
        grid-template-columns: repeat(3, minmax(0, 1fr));
        min-width: 0;
    }
    .ntmods-module-card {
        min-height: 820px;
    }
}
@media (max-width: 980px) {
    .ntmods-layout {
        flex-direction: column;
    }
    .ntmods-sidebar {
        display: grid;
        grid-template-columns: repeat(2, minmax(0, 1fr));
        width: auto;
        max-width: none;
        gap: 8px;
    }
    .ntmods-stage-intro {
        flex-direction: column;
    }
    .ntmods-grid {
        grid-template-columns: 1fr;
    }
    .ntmods-nav-btn,
    .ntmods-nav-btn:first-child,
    .ntmods-nav-btn:last-child {
        border-radius: 8px;
    }
}
@media (max-width: 720px) {
    .ntmods-cap {
        padding: 18px;
    }
    .ntmods-cap-stats {
        grid-template-columns: 1fr;
    }
    .ntmods-module-head {
        flex-direction: column;
        align-items: stretch;
    }
    .ntmods-module-actions {
        justify-content: space-between;
    }
    .ntmods-sidebar {
        grid-template-columns: 1fr;
    }
    .ntmods-module-card {
        min-height: 700px;
    }
    .ntmods-top-tab {
        min-width: 0;
        padding: 12px 8px 11px;
    }
    .ntmods-top-tab .bucket-content {
        font-size: 11px;
    }
    .ntmods-top-tab-glyph {
    }
    .ntmods-mod-icon--tab {
        width: 13px;
        height: 13px;
    }
}

/* Drag-to-reorder tabs */
.ntmods-tab-dragging {
    opacity: 0.4;
    outline: 2px dashed rgba(255,255,255,0.2);
}
.ntmods-tab-dragover {
    background: rgba(28, 153, 244, 0.15) !important;
    border-color: rgba(28, 153, 244, 0.4) !important;
}
.ntmods-top-tab[draggable="true"] {
    cursor: grab;
}
.ntmods-top-tab[draggable="true"]:active {
    cursor: grabbing;
}

/* Not Installed card */
.ntmods-not-installed {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    text-align: center;
    padding: 60px 30px;
    gap: 12px;
    min-height: 300px;
}
.ntmods-not-installed-icon {
    opacity: 0.3;
    margin-bottom: 8px;
}
.ntmods-not-installed-icon svg {
    width: 64px;
    height: 64px;
}
.ntmods-not-installed-title {
    font-family: "Montserrat", sans-serif;
    font-size: 22px;
    font-weight: 700;
    color: rgba(174, 180, 199, 0.8);
    margin: 0;
}
.ntmods-not-installed-copy {
    font-size: 14px;
    color: rgba(174, 180, 199, 0.6);
    max-width: 420px;
    line-height: 1.5;
    margin: 0;
}
.ntmods-not-installed-copy strong {
    color: rgba(220, 225, 240, 0.8);
}
.ntmods-install-link {
    display: inline-block;
    margin-top: 12px;
    padding: 10px 24px;
    font-family: "Montserrat", sans-serif;
    font-size: 13px;
    font-weight: 600;
    color: #fff;
    background: linear-gradient(135deg, #1c99f4 0%, #1a7ed4 100%);
    border-radius: 6px;
    text-decoration: none;
    transition: background 0.15s ease, transform 0.1s ease;
}
.ntmods-install-link:hover {
    background: linear-gradient(135deg, #2ba6ff 0%, #1c99f4 100%);
    transform: translateY(-1px);
}

/* Preview tab styling (greyed out) */
.ntmods-top-tab.is-preview {
    opacity: 0.5;
}
.ntmods-top-tab.is-preview:hover {
    opacity: 0.75;
}
        `;
        (document.head || document.documentElement).appendChild(style);
    }

    function setActiveDropdownItem() {
        document.querySelectorAll(`.${DROPDOWN_ITEM_CLASS}`).forEach((item) => {
            item.classList.toggle('is-current', isModMenuRoute());
        });
    }

    function setPageTitle() {
        if (isModMenuRoute()) {
            document.title = 'Mods | Nitro Type';
        }
    }

    function ensureEntryStyles() {
        if (document.getElementById('ntmods-entry-style')) return;
        const style = document.createElement('style');
        style.id = 'ntmods-entry-style';
        style.textContent = `
.ntmods-dropdown-item.is-current .dropdown-link {
    background: rgba(255, 255, 255, 0.08);
    color: #fff;
}
.ntmods-dropdown-item .dropdown-link .icon {
    opacity: 0.92;
}
.ntmods-dropdown-item .dropdown-link .ntmods-inline-icon {
    opacity: 1;
    color: #9fb2c6 !important;
    fill: currentColor !important;
}
.ntmods-dropdown-item .dropdown-link:hover .ntmods-inline-icon,
.ntmods-dropdown-item.is-current .dropdown-link .ntmods-inline-icon {
    color: #c8d5e2 !important;
    fill: currentColor !important;
}
.ntmods-inline-icon {
    width: 16px;
    height: 16px;
    min-width: 16px;
    min-height: 16px;
    display: inline-block;
    vertical-align: -3px;
    flex: 0 0 16px;
    shape-rendering: geometricPrecision;
}
        `;
        (document.head || document.documentElement).appendChild(style);
    }

    function getModsMenuIconMarkup() {
        return `
            <svg class="ntmods-inline-icon mrxs" viewBox="0 0 177.5 178" aria-hidden="true">
                <g
                    transform="translate(-12,189) scale(0.1,-0.1)"
                    fill="currentColor"
                    stroke="currentColor"
                    stroke-linecap="round"
                    stroke-linejoin="round"
                >
                    <path
                        stroke-width="102"
                        d="M867 1870 c-18 -15 -28 -37 -38 -86 -8 -42 -19 -68 -29 -71 -8 -3
                        -49 -20 -91 -37 l-76 -32 -34 24 c-59 41 -95 53 -126 41 -15 -5 -61 -42 -100
                        -82 -94 -93 -100 -116 -48 -194 35 -53 37 -59 25 -88 -7 -16 -22 -57 -35 -90
                        l-23 -60 -65 -15 c-103 -24 -107 -30 -107 -177 0 -155 4 -160 133 -187 97 -21
                        111 -17 105 30 -3 27 -8 30 -68 44 -36 8 -71 19 -77 23 -9 5 -13 35 -13 87 0
                        52 4 82 13 87 6 4 41 15 77 23 l64 15 26 75 c14 41 35 92 48 113 30 51 28 68
                        -13 133 -19 30 -35 58 -35 62 0 4 27 34 59 66 l60 59 38 -25 c90 -59 96 -60
                        150 -31 26 14 82 38 123 53 l75 26 16 75 15 74 86 3 c99 3 97 5 117 -90 14
                        -64 19 -70 81 -87 25 -7 75 -29 113 -47 75 -38 72 -38 156 19 l43 30 64 -64
                        64 -64 -42 -63 -41 -63 41 -87 c23 -48 44 -100 48 -117 10 -45 20 -53 82 -66
                        92 -18 92 -18 92 -109 0 -91 0 -91 -92 -109 -65 -14 -72 -20 -88 -81 -6 -25
                        -27 -74 -46 -110 -19 -35 -34 -69 -34 -75 0 -5 18 -37 40 -70 l40 -60 -62 -62
                        c-34 -34 -64 -61 -67 -59 -3 2 -34 20 -69 41 l-64 39 -56 -31 c-31 -17 -87
                        -41 -123 -53 l-66 -21 -19 -77 -19 -77 -78 -3 c-46 -2 -84 2 -92 8 -7 6 -18
                        38 -25 73 -13 69 -24 87 -50 87 -35 0 -44 -26 -32 -93 29 -155 32 -157 196
                        -157 140 0 150 6 172 103 13 57 20 70 42 79 15 6 55 23 89 39 l61 27 54 -36
                        c84 -57 102 -52 203 48 102 101 107 123 51 206 l-35 52 36 83 c19 46 42 89 50
                        96 8 6 37 14 64 18 27 4 59 15 72 25 21 17 22 26 22 150 0 154 -3 159 -105
                        179 -51 10 -63 16 -72 39 -6 15 -24 55 -39 90 l-29 63 35 52 c55 81 50 111
                        -30 196 -72 75 -123 106 -158 95 -12 -4 -44 -22 -70 -39 l-49 -33 -61 27 c-34
                        16 -74 33 -89 39 -22 9 -29 22 -42 79 -22 98 -31 103 -176 103 -102 0 -121 -3
                        -143 -20z"
                    />
                </g>
                <svg x="7" y="24" width="156" height="156" viewBox="0 0 256 256">
                    <g transform="translate(0,256) scale(0.1,-0.1)" fill="currentColor" stroke="none">
                        <path d="M1162 1810 c-45 -11 -105 -31 -133 -46 -78 -43 -154 -125 -197 -212
                            l-37 -77 0 -175 0 -176 -230 -209 c-376 -342 -471 -439 -495 -502 -69 -186 63
                            -377 261 -376 118 0 170 41 469 368 258 283 366 389 387 380 32 -13 157 -24
                            208 -19 61 6 147 31 197 55 71 36 162 133 204 216 l39 78 0 135 c-1 119 -4
                            143 -25 200 -30 78 -61 100 -108 76 -16 -8 -82 -66 -147 -128 -128 -123 -160
                            -148 -189 -148 -27 0 -85 47 -97 78 -13 33 1 52 146 206 134 143 150 167 135
                            195 -16 31 -80 60 -178 81 -104 23 -105 23 -210 0z"/>
                    </g>
                </svg>
            </svg>
        `;
    }

    function insertModsDropdownItem() {
        ensureEntryStyles();
        const dropdownLists = document.querySelectorAll('.dropdown--account .dropdown-items');
        if (!dropdownLists.length) return;

        dropdownLists.forEach((list) => {
            if (list.querySelector(`.${DROPDOWN_ITEM_CLASS}`)) return;

            const li = document.createElement('li');
            li.className = `list-item dropdown-item ${DROPDOWN_ITEM_CLASS}`;
            li.innerHTML = `
                <a class="dropdown-link" href="${MOD_MENU_PATH}">
                    ${getModsMenuIconMarkup()}
                    Mods
                </a>
            `;

            const statsItem = Array.from(list.children).find((item) => {
                const link = item.querySelector('a[href="/stats"]');
                return !!link || item.textContent.trim().includes('My Stats');
            });

            if (statsItem) {
                statsItem.after(li);
            } else {
                list.appendChild(li);
            }
        });

        setActiveDropdownItem();
    }

    function updateRouteStatus() {
        const main = document.querySelector('main.structure-content');
        if (isModMenuRoute()) {
            document.documentElement.classList.add('is-mods-route');
        } else {
            document.documentElement.classList.remove('is-mods-route');
            if (main) main.classList.remove('custom-loaded');
        }
    }

    function getModuleById(modules, moduleId) {
        return modules.find((module) => module.id === moduleId) || modules[0] || null;
    }

    function renderPage(requestedModuleId = '', requestedSectionId = '') {
        if (renderInProgress || !isModMenuRoute()) return;
        const mainContent = document.querySelector('main.structure-content');
        if (!mainContent) return;

        renderInProgress = true;
        try {
            ensureStyles();

            const modules = buildVisibleModules();
            const selection = requestedModuleId
                ? {
                    activeModuleId: requestedModuleId,
                    activeSectionId: requestedSectionId
                }
                : getInitialSelection(modules);

            const activeModule = getModuleById(modules, selection.activeModuleId);
            const activeModuleSections = getRenderableSections(activeModule);
            const activeSection = activeModule
                ? activeModuleSections.find((section) => section.id === selection.activeSectionId) || activeModuleSections[0]
                : null;

            const nextUiState = {
                moduleId: activeModule ? activeModule.id : '',
                sectionId: activeSection ? activeSection.id : ''
            };

            writeUiState(nextUiState);

            mainContent.innerHTML = buildPageHtml(modules, nextUiState.moduleId, nextUiState.sectionId);
            requestAnimationFrame(() => {
                mainContent.classList.add('custom-loaded');
            });

            bindPageEvents(modules);
            setActiveDropdownItem();
            setPageTitle();
        } finally {
            renderInProgress = false;
        }
    }

    function bindPageEvents(modules) {
        document.querySelectorAll('[data-module-id]').forEach((button) => {
            button.addEventListener('click', () => {
                const moduleId = button.getAttribute('data-module-id') || '';
                const module = getModuleById(modules, moduleId);
                const moduleSections = getRenderableSections(module);
                const sectionId = moduleSections[0] ? moduleSections[0].id : '';
                renderPage(moduleId, sectionId);
            });
        });

        // ── Drag-to-reorder tabs ──
        const tabBar = document.querySelector('.ntmods-top-tabs');
        if (tabBar) {
            let dragSrc = null;
            let dragPlaceholder = null;

            tabBar.querySelectorAll('[data-module-id]').forEach((tab) => {
                tab.draggable = true;

                tab.addEventListener('dragstart', (e) => {
                    dragSrc = tab;
                    tab.classList.add('ntmods-tab-dragging');
                    e.dataTransfer.effectAllowed = 'move';
                    e.dataTransfer.setData('text/plain', tab.getAttribute('data-module-id'));
                });

                tab.addEventListener('dragend', () => {
                    if (dragSrc) dragSrc.classList.remove('ntmods-tab-dragging');
                    if (dragPlaceholder) {
                        dragPlaceholder.remove();
                        dragPlaceholder = null;
                    }
                    tabBar.querySelectorAll('.ntmods-tab-dragover').forEach((el) => el.classList.remove('ntmods-tab-dragover'));
                    dragSrc = null;
                });

                tab.addEventListener('dragover', (e) => {
                    e.preventDefault();
                    e.dataTransfer.dropEffect = 'move';
                    if (tab === dragSrc) return;
                    tab.classList.add('ntmods-tab-dragover');
                });

                tab.addEventListener('dragleave', () => {
                    tab.classList.remove('ntmods-tab-dragover');
                });

                tab.addEventListener('drop', (e) => {
                    e.preventDefault();
                    tab.classList.remove('ntmods-tab-dragover');
                    if (!dragSrc || dragSrc === tab) return;

                    // Reorder in DOM
                    const allTabs = [...tabBar.querySelectorAll('[data-module-id]')];
                    const fromIdx = allTabs.indexOf(dragSrc);
                    const toIdx = allTabs.indexOf(tab);
                    if (fromIdx < toIdx) {
                        tab.parentNode.insertBefore(dragSrc, tab.nextSibling);
                    } else {
                        tab.parentNode.insertBefore(dragSrc, tab);
                    }

                    // Persist new order
                    const newOrder = [...tabBar.querySelectorAll('[data-module-id]')].map(
                        (t) => t.getAttribute('data-module-id')
                    );
                    try {
                        localStorage.setItem('ntmods:tab-order', JSON.stringify(newOrder));
                    } catch { /* ignore */ }

                    showToast('Tab order saved.');
                });
            });
        }

        document.querySelectorAll('[data-section-id]').forEach((button) => {
            button.addEventListener('click', () => {
                const uiState = readUiState();
                renderPage(uiState.moduleId || '', button.getAttribute('data-section-id') || '');
            });
        });

        const uiState = readUiState();
        const activeModule = getModuleById(modules, uiState.moduleId);
        const activeModuleSections = getRenderableSections(activeModule);
        const activeSection = activeModule
            ? activeModuleSections.find((section) => section.id === uiState.sectionId) || activeModuleSections[0]
            : null;

        document.querySelectorAll('[data-action-key]').forEach((button) => {
            button.addEventListener('click', () => {
                const actionKey = button.getAttribute('data-action-key') || '';
                if (actionKey === 'rescan') {
                    showToast('Rescanning installed mods...');
                    renderPage();
                    return;
                }
                showToast('Preview action only for now.');
            });
        });

        if (activeModule?.source === 'manifest' && activeSection) {
            mountGenericSection(activeModule, activeSection);
            return;
        }

        const fieldMap = new Map((activeSection?.items || []).filter((item) => item.key).map((item) => [item.key, item]));

        document.querySelectorAll('[data-field-key]').forEach((input) => {
            const fieldKey = input.getAttribute('data-field-key') || '';
            const item = fieldMap.get(fieldKey);
            if (!activeModule || !item) return;

            const inputType = input.getAttribute('data-field-type') || item.type;
            const eventName = item.type === 'toggle' || inputType === 'select' ? 'change' : 'input';

            input.addEventListener(eventName, () => {
                if (inputType === 'color-text') {
                    const colorValue = String(input.value || '').trim();
                    const colorPicker = document.querySelector(`[data-field-key="${CSS.escape(fieldKey)}"][data-field-type="color"]`);
                    if (/^#[0-9A-Fa-f]{6}$/.test(colorValue)) {
                        setFieldValue(activeModule, item, colorValue.toUpperCase());
                        if (colorPicker) colorPicker.value = colorValue.toUpperCase();
                    }
                    return;
                }

                const nextValue = coerceInputValue(input, item);
                setFieldValue(activeModule, item, nextValue);

                if (inputType === 'color') {
                    const colorText = document.querySelector(`[data-field-key="${CSS.escape(fieldKey)}"][data-field-type="color-text"]`);
                    if (colorText) colorText.value = String(nextValue).toUpperCase();
                }
            });

            if (inputType === 'text' || inputType === 'number' || inputType === 'select') {
                input.addEventListener('change', () => {
                    const nextValue = coerceInputValue(input, item);
                    setFieldValue(activeModule, item, nextValue);
                });
            }
        });

    }

    function handlePage() {
        insertModsDropdownItem();
        if (!isModMenuRoute()) {
            setActiveDropdownItem();
            return;
        }

        const main = document.querySelector('main.structure-content');
        if (!main) return;

        const app = document.getElementById('ntmods-app');
        if (app) {
            setActiveDropdownItem();
            setPageTitle();
            return;
        }

        const text = main.textContent || '';
        const shouldRender = main.children.length === 0
            || !!main.querySelector('.error')
            || text.includes('Page Not Found')
            || text.includes('page not found');

        if (shouldRender) {
            renderPage();
        }
    }

    function scheduleHandlePage() {
        if (handleScheduled) return;
        handleScheduled = true;
        requestAnimationFrame(() => {
            handleScheduled = false;
            handlePage();
        });
    }

    function refreshActiveModMenuPage() {
        if (!isModMenuRoute()) return;
        if (document.getElementById('ntmods-app')) {
            renderPage();
            return;
        }
        scheduleHandlePage();
    }

    function fastInject() {
        insertModsDropdownItem();
        if (!isModMenuRoute()) return;

        const waitForMain = new MutationObserver(() => {
            const main = document.querySelector('main.structure-content');
            if (!main) return;
            const text = main.textContent || '';
            if (main.children.length === 0 || !!main.querySelector('.error') || text.includes('Page Not Found')) {
                renderPage();
                waitForMain.disconnect();
            }
        });

        waitForMain.observe(document.documentElement, { childList: true, subtree: true });
    }

    updateRouteStatus();

    const originalPushState = history.pushState;
    history.pushState = function () {
        const result = originalPushState.apply(this, arguments);
        updateRouteStatus();
        scheduleHandlePage();
        return result;
    };

    const originalReplaceState = history.replaceState;
    history.replaceState = function () {
        const result = originalReplaceState.apply(this, arguments);
        updateRouteStatus();
        scheduleHandlePage();
        return result;
    };

    window.addEventListener('popstate', () => {
        updateRouteStatus();
        scheduleHandlePage();
    });

    document.addEventListener('ntcfg:manifest-updated', refreshActiveModMenuPage);
    window.addEventListener('storage', (event) => {
        const key = String(event?.key || '');
        if (!key) return;
        if (key.startsWith(MANIFEST_PREFIX) || key.startsWith('ntcfg:')) {
            refreshActiveModMenuPage();
        }
    });

    fastInject();

    const navObserver = new MutationObserver(() => {
        scheduleHandlePage();
    });

    navObserver.observe(document.documentElement, { childList: true, subtree: true });
})();