Torn Quick-Nav Hotkeys

Fully customizable keyboard hotkeys for fast navigation around Torn. Auto-detects Windows/macOS. Open settings with Alt+, (or Option+, on Mac) or the floating ⚙️ button. Includes customizable Master Kill Switch.

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

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

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

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

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

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

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

Advertisement:

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

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

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

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

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

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

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

Advertisement:

// ==UserScript==
// @name         Torn Quick-Nav Hotkeys
// @namespace    https://www.torn.com
// @version      1.8.0
// @description  Fully customizable keyboard hotkeys for fast navigation around Torn. Auto-detects Windows/macOS. Open settings with Alt+, (or Option+, on Mac) or the floating ⚙️ button. Includes customizable Master Kill Switch.
// @author       Ashbrak
// @match        https://www.torn.com/*
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_addStyle
// @license      MIT
// ==/UserScript==

/* eslint-disable no-multi-spaces */

(function () {
  'use strict';

  // ─── Default preset pages ───────────────────────────────────────────────────
  const PRESET_PAGES = [
    { label: 'Home',               url: '/index.php' },
    { label: 'Gym',                url: '/gym.php' },
    { label: 'Travel',             url: '/page.php?sid=travel' },
    { label: 'Hospital',           url: '/hospitalview.php' },
    { label: 'Jail',               url: '/jailview.php' },
    { label: 'Crimes',             url: '/crimes.php' },
    { label: 'Faction',            url: '/factions.php?step=your' },
    { label: 'Items',              url: '/item.php' },
    { label: 'City / Map',         url: '/city.php' },
    { label: 'Properties',         url: '/properties.php' },
    { label: 'Education',          url: '/education.php' },
    { label: 'Job / Company',      url: '/companies.php' },
    { label: 'Forums',             url: '/forums.php' },
    { label: 'Stock Market',       url: '/page.php?sid=stocks' },
    { label: 'Bank',               url: '/bank.php' },
    { label: 'Casino',             url: '/casino.php' },
    { label: 'View Player Profile',url: '/profiles.php?XID=__USERID__', needsUserId: true },
    { label: 'Attack Player',      url: '/loader.php?sid=attack&user2ID=__USERID__', needsUserId: true },
  ];

  // ─── OS detection ────────────────────────────────────────────────────────────
  const IS_MAC_AUTO = /Mac|iPhone|iPad|iPod/.test(navigator.platform || navigator.userAgent);

  const MODIFIERS_WIN = ['None (single key)', 'Alt', 'Ctrl', 'Shift', 'Alt+Shift', 'Ctrl+Shift'];
  const MODIFIERS_MAC = ['None (single key)', 'Option', 'Cmd', 'Ctrl', 'Option+Shift', 'Cmd+Shift', 'Ctrl+Shift'];

  // Map display label → what checkModifier understands internally
  const MOD_ALIAS = {
    'Option':       'Alt / Option',
    'Cmd':          'Cmd (Mac) / Win',
    'Alt':          'Alt / Option',
  };

  function getModifiers(isMac) {
    return isMac ? MODIFIERS_MAC : MODIFIERS_WIN;
  }

  function isMacMode() {
    const override = settings.osOverride; // 'mac' | 'win' | null
    if (override === 'mac') return true;
    if (override === 'win') return false;
    return IS_MAC_AUTO;
  }

  // ─── Load / Save settings ───────────────────────────────────────────────────
  function loadSettings() {
    const raw = GM_getValue('quicknav_settings', null);
    if (raw) {
      const parsed = JSON.parse(raw);
      // Fallback defaults for customizable master key additions
      if (parsed.masterModifier === undefined) parsed.masterModifier = IS_MAC_AUTO ? 'Cmd+Shift' : 'Ctrl+Shift';
      if (parsed.masterKey === undefined) parsed.masterKey = 'H';
      return parsed;
    }
    return {
      showButton: true,
      allEnabled: true,
      osOverride: null,   // null = auto-detect, 'mac', or 'win'
      masterModifier: IS_MAC_AUTO ? 'Cmd+Shift' : 'Ctrl+Shift',
      masterKey: 'H',
      hotkeys: [],
    };
  }

  function saveSettings(s) {
    GM_setValue('quicknav_settings', JSON.stringify(s));
    updateFabState();
  }

  let settings = loadSettings();

  // ─── Styles ─────────────────────────────────────────────────────────────────
  GM_addStyle(`
    #tqn-fab {
      position: fixed;
      bottom: 20px;
      right: 20px;
      z-index: 999999;
      width: 42px;
      height: 42px;
      border-radius: 50%;
      background: #2c2c2c;
      color: #fff;
      font-size: 20px;
      border: 2px solid #555;
      cursor: pointer;
      display: flex;
      align-items: center;
      justify-content: center;
      box-shadow: 0 2px 10px rgba(0,0,0,0.5);
      transition: background 0.2s, border-color 0.2s;
      user-select: none;
    }
    #tqn-fab:hover { background: #444; }
    #tqn-fab.tqn-fab-active { border-color: #4da64d; background: #1c2e1c; }
    #tqn-fab.tqn-fab-active:hover { background: #253d25; }
    #tqn-fab.tqn-fab-disabled { border-color: #c74d4d; background: #331f1f; }
    #tqn-fab.tqn-fab-disabled:hover { background: #422828; }

    #tqn-overlay {
      position: fixed;
      inset: 0;
      background: rgba(0,0,0,0.6);
      z-index: 1000000;
      display: flex;
      align-items: center;
      justify-content: center;
    }
    #tqn-modal {
      background: #1e1e1e;
      color: #e0e0e0;
      border-radius: 10px;
      width: min(680px, 95vw);
      max-height: 85vh;
      display: flex;
      flex-direction: column;
      box-shadow: 0 8px 40px rgba(0,0,0,0.7);
      font-family: Arial, sans-serif;
      font-size: 14px;
      overflow: hidden;
    }
    #tqn-modal-header {
      padding: 16px 20px;
      background: #141414;
      border-bottom: 1px solid #333;
      display: flex;
      align-items: center;
      justify-content: space-between;
      flex-shrink: 0;
    }
    #tqn-modal-header h2 {
      margin: 0;
      font-size: 16px;
      color: #fff;
    }
    #tqn-modal-header span {
      font-size: 12px;
      color: #888;
    }
    #tqn-modal-body {
      overflow-y: auto;
      padding: 16px 20px;
      flex: 1;
    }
    #tqn-modal-footer {
      padding: 12px 20px;
      background: #141414;
      border-top: 1px solid #333;
      display: flex;
      align-items: center;
      justify-content: space-between;
      flex-shrink: 0;
      gap: 10px;
    }

    .tqn-section-title {
      font-size: 11px;
      text-transform: uppercase;
      color: #888;
      letter-spacing: 0.08em;
      margin: 0 0 10px 0;
    }
    .tqn-hotkey-row {
      display: grid;
      grid-template-columns: 20px 1fr 130px 70px 32px;
      gap: 8px;
      align-items: center;
      margin-bottom: 8px;
    }
    .tqn-master-config-row {
      display: flex;
      gap: 8px;
      align-items: center;
      margin-bottom: 14px;
      padding-left: 46px;
    }
    .tqn-hotkey-row.tqn-disabled input,
    .tqn-hotkey-row.tqn-disabled select {
      opacity: 0.35;
    }
    .tqn-modal-input {
      background: #2a2a2a;
      border: 1px solid #444;
      border-radius: 5px;
      color: #e0e0e0;
      padding: 5px 8px;
      font-size: 13px;
      box-sizing: border-box;
    }
    .tqn-hotkey-row input, .tqn-hotkey-row select, .tqn-master-config-row select {
      background: #2a2a2a;
      border: 1px solid #444;
      border-radius: 5px;
      color: #e0e0e0;
      padding: 5px 8px;
      font-size: 13px;
      width: 100%;
      box-sizing: border-box;
    }
    .tqn-modal-input:focus, .tqn-hotkey-row input:focus, .tqn-hotkey-row select:focus, .tqn-master-config-row select:focus, .tqn-master-config-row input:focus {
      outline: none;
      border-color: #e3a800;
    }
    .tqn-key-input {
      text-align: center;
      font-weight: bold;
      text-transform: uppercase;
    }
    .tqn-btn-del {
      background: #3a1a1a;
      border: 1px solid #622;
      border-radius: 5px;
      color: #e06060;
      cursor: pointer;
      font-size: 16px;
      width: 32px;
      height: 32px;
      display: flex;
      align-items: center;
      justify-content: center;
      flex-shrink: 0;
    }
    .tqn-btn-del:hover { background: #511; }

    .tqn-add-row {
      display: grid;
      grid-template-columns: 80px 1fr;
      gap: 8px;
      margin-bottom: 16px;
    }
    .tqn-add-row select, .tqn-add-row input {
      background: #2a2a2a;
      border: 1px solid #444;
      border-radius: 5px;
      color: #e0e0e0;
      padding: 5px 8px;
      font-size: 13px;
      width: 100%;
      box-sizing: border-box;
    }

    .tqn-btn {
      padding: 7px 16px;
      border-radius: 6px;
      border: none;
      cursor: pointer;
      font-size: 13px;
      font-weight: bold;
    }
    .tqn-btn-primary {
      background: #e3a800;
      color: #111;
    }
    .tqn-btn-primary:hover { background: #f0b800; }
    .tqn-btn-secondary {
      background: #2a2a2a;
      color: #ccc;
      border: 1px solid #444;
    }
    .tqn-btn-secondary:hover { background: #333; }
    .tqn-btn-add {
      background: #1a3a1a;
      color: #6de06d;
      border: 1px solid #3a6a3a;
      width: 100%;
      margin-bottom: 16px;
    }
    .tqn-btn-add:hover { background: #243a24; }

    .tqn-divider { border: none; border-top: 1px solid #333; margin: 16px 0; }

    .tqn-toggle-row {
      display: flex;
      align-items: center;
      gap: 10px;
      margin-bottom: 14px;
    }
    .tqn-toggle {
      appearance: none;
      width: 36px;
      height: 20px;
      background: #444;
      border-radius: 10px;
      position: relative;
      cursor: pointer;
      transition: background 0.2s;
      flex-shrink: 0;
    }
    .tqn-toggle:checked { background: #e3a800; }
    .tqn-toggle::after {
      content: '';
      position: absolute;
      width: 14px; height: 14px;
      background: #fff;
      border-radius: 50%;
      top: 3px; left: 3px;
      transition: left 0.2s;
    }
    .tqn-toggle:checked::after { left: 19px; }

    /* smaller variant used in hotkey rows */
    .tqn-hk-enabled {
      width: 20px !important;
      height: 12px !important;
      border-radius: 6px !important;
    }
    .tqn-hk-enabled::after {
      width: 8px !important;
      height: 8px !important;
      top: 2px !important;
      left: 2px !important;
    }
    .tqn-hk-enabled:checked::after {
      left: 10px !important;
    }

    .tqn-col-headers {
      display: grid;
      grid-template-columns: 20px 1fr 130px 70px 32px;
      gap: 8px;
      font-size: 11px;
      color: #666;
      text-transform: uppercase;
      letter-spacing: 0.06em;
      margin-bottom: 6px;
    }
    .tqn-toast {
      position: fixed;
      bottom: 80px;
      right: 20px;
      background: #2a2a2a;
      border: 1px solid #3a6a3a;
      border-radius: 6px;
      padding: 8px 16px;
      font-size: 13px;
      z-index: 1000001;
      animation: tqn-fadein 0.2s ease;
    }
    .tqn-toast.tqn-toast-error {
      color: #e06060;
      border-color: #622;
    }
    .tqn-toast.tqn-toast-success {
      color: #6de06d;
      border-color: #3a6a3a;
    }
    @keyframes tqn-fadein { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: translateY(0); } }
  `);

  // ─── Floating button (draggable) ─────────────────────────────────────────────
  const fab = document.createElement('div');
  fab.id = 'tqn-fab';
  fab.textContent = '⚙️';

  function getFabTooltip() {
    return `Torn Quick-Nav Settings (Alt+,)\nMaster Toggle (${settings.masterModifier || 'Ctrl+Shift'}+${settings.masterKey || 'H'})`;
  }

  // Restore saved position or default (higher up from bottom edge)
  const savedPos = JSON.parse(GM_getValue('quicknav_fab_pos', 'null'));
  if (savedPos) {
    fab.style.left   = savedPos.x + 'px';
    fab.style.top    = savedPos.y + 'px';
    fab.style.right  = 'auto';
    fab.style.bottom = 'auto';
  } else {
    fab.style.bottom = '80px';
    fab.style.right  = '20px';
  }

  fab.style.display = settings.showButton ? 'flex' : 'none';
  document.body.appendChild(fab);
  updateFabState();

  // Updates the visual green/red state of the gear button
  function updateFabState() {
    if (!fab) return;
    fab.title = getFabTooltip();
    if (settings.allEnabled !== false) {
      fab.classList.remove('tqn-fab-disabled');
      fab.classList.add('tqn-fab-active');
    } else {
      fab.classList.remove('tqn-fab-active');
      fab.classList.add('tqn-fab-disabled');
    }
  }

  // Drag logic — distinguishes click vs drag so modal still opens on click
  let dragging = false, dragOffX = 0, dragOffY = 0, dragMoved = false;
  fab.addEventListener('mousedown', function (e) {
    dragging = true;
    dragMoved = false;
    const rect = fab.getBoundingClientRect();
    dragOffX = e.clientX - rect.left;
    dragOffY = e.clientY - rect.top;
    fab.style.transition = 'none';
    fab.style.cursor = 'grabbing';
    e.preventDefault();
  });
  document.addEventListener('mousemove', function (e) {
    if (!dragging) return;
    dragMoved = true;
    const x = Math.max(0, Math.min(window.innerWidth  - fab.offsetWidth,  e.clientX - dragOffX));
    const y = Math.max(0, Math.min(window.innerHeight - fab.offsetHeight, e.clientY - dragOffY));
    fab.style.left   = x + 'px';
    fab.style.top    = y + 'px';
    fab.style.right  = 'auto';
    fab.style.bottom = 'auto';
  });
  document.addEventListener('mouseup', function () {
    if (!dragging) return;
    dragging = false;
    fab.style.cursor = 'pointer';
    if (dragMoved) {
      GM_setValue('quicknav_fab_pos', JSON.stringify({
        x: parseInt(fab.style.left), y: parseInt(fab.style.top)
      }));
    }
  });
  fab.addEventListener('click', function () { if (!dragMoved) openModal(); });

  // ─── Modal ───────────────────────────────────────────────────────────────────
  function openModal() {
    if (document.getElementById('tqn-overlay')) return;

    const overlay = document.createElement('div');
    overlay.id = 'tqn-overlay';
    overlay.addEventListener('click', e => { if (e.target === overlay) closeModal(); });

    overlay.innerHTML = `
      <div id="tqn-modal">
        <div id="tqn-modal-header">
          <div style="display:flex; flex-direction:column; gap:3px;">
            <h2 style="margin:0; font-size:16px; color:#fff;">⚡ Torn Quick-Nav Hotkeys</h2>
            <a href="https://www.torn.com/profiles.php?XID=3888401" target="_blank" style="font-size:11px; color:#e3a800; text-decoration:none; opacity:0.6; transition:opacity 0.2s;" onmouseover="this.style.opacity='1'" onmouseout="this.style.opacity='0.6'">by Ashbrak</a>
          </div>
          <span>Alt / Option+, to open anytime</span>
        </div>
        <div id="tqn-modal-body">

          <div class="tqn-section-title">General</div>
          <div class="tqn-toggle-row">
            <input type="checkbox" class="tqn-toggle" id="tqn-show-btn" ${settings.showButton ? 'checked' : ''}>
            <label for="tqn-show-btn">Show floating ⚙️ button on page</label>
          </div>
          <div class="tqn-toggle-row" style="margin-bottom: 6px;">
            <input type="checkbox" class="tqn-toggle" id="tqn-all-enabled" ${settings.allEnabled !== false ? 'checked' : ''}>
            <label for="tqn-all-enabled">Hotkeys active <span style="color:#666; font-weight:normal; font-size:12px;">(master switch)</span></label>
          </div>
          
          <div class="tqn-master-config-row">
            <span style="font-size: 12px; color: #aaa; white-space: nowrap;">Hotkey Bind:</span>
            <select id="tqn-master-modifier" style="width: 130px;">
              ${getModifiers(isMacMode()).map(m => `<option value="${m}" ${settings.masterModifier === m ? 'selected' : ''}>${m}</option>`).join('')}
            </select>
            <input type="text" id="tqn-master-key" class="tqn-modal-input tqn-key-input" style="width: 50px;" maxlength="1" value="${escHtml(settings.masterKey)}" placeholder="key">
          </div>

          <div class="tqn-toggle-row" style="gap:12px;">
            <label style="color:#aaa; font-size:13px; white-space:nowrap;">
              OS:
            </label>
            <div style="display:flex; background:#2a2a2a; border:1px solid #444; border-radius:6px; overflow:hidden; font-size:12px;">
              <button class="tqn-os-btn ${!isMacMode() ? 'tqn-os-active' : ''}" data-os="win" style="padding:4px 14px; border:none; cursor:pointer; background:${!isMacMode() ? '#e3a800' : 'transparent'}; color:${!isMacMode() ? '#111' : '#aaa'}; font-weight:${!isMacMode() ? 'bold' : 'normal'}; transition:all 0.15s;">Windows</button>
              <button class="tqn-os-btn ${isMacMode() ? 'tqn-os-active' : ''}" data-os="mac" style="padding:4px 14px; border:none; cursor:pointer; background:${isMacMode() ? '#e3a800' : 'transparent'}; color:${isMacMode() ? '#111' : '#aaa'}; font-weight:${isMacMode() ? 'bold' : 'normal'}; transition:all 0.15s;">macOS</button>
            </div>
            <span style="font-size:11px; color:#555;">${settings.osOverride ? 'manual' : 'auto-detected'}</span>
          </div>

          <hr class="tqn-divider">
          <div class="tqn-section-title">Add a hotkey</div>

          <div class="tqn-add-row">
            <select id="tqn-add-type">
              <option value="preset">Preset</option>
              <option value="custom">Custom URL</option>
            </select>
            <select id="tqn-add-preset">
              ${PRESET_PAGES.map(p => `<option value="${p.url}">${p.label}${p.needsUserId ? ' 🔑' : ''}</option>`).join('')}
            </select>
          </div>
          <div id="tqn-custom-url-row" style="display:none; margin-bottom:8px;">
            <input type="text" id="tqn-custom-label" placeholder="Label (e.g. My Page)" style="margin-bottom:6px;">
            <input type="text" id="tqn-custom-url" placeholder="Full URL or path (e.g. /crimes.php or https://www.torn.com/...)">
          </div>
          <button class="tqn-btn tqn-btn-add" id="tqn-add-btn">+ Add Hotkey</button>

          <hr class="tqn-divider">
          <div class="tqn-section-title">Your hotkeys</div>
          <div class="tqn-col-headers">
            <span>On</span><span>Page / URL</span><span>Modifier</span><span>Key</span><span></span>
          </div>
          <div id="tqn-hotkey-list"></div>

        </div>
        <div id="tqn-modal-footer">
          <div style="font-size:12px; color:#666;">Changes auto-save</div>
          <div style="display:flex; gap:8px;">
            <button class="tqn-btn tqn-btn-secondary" id="tqn-close-btn">Close</button>
            <button class="tqn-btn tqn-btn-primary" id="tqn-save-btn">Save & Close</button>
          </div>
        </div>
      </div>
    `;

    document.body.appendChild(overlay);
    renderHotkeyList();

    // Type toggle
    document.getElementById('tqn-add-type').addEventListener('change', function () {
      const isCustom = this.value === 'custom';
      document.getElementById('tqn-add-preset').style.display = isCustom ? 'none' : '';
      document.getElementById('tqn-custom-url-row').style.display = isCustom ? 'grid' : 'none';
      document.getElementById('tqn-custom-url-row').style.gridTemplateColumns = '1fr';
      document.getElementById('tqn-custom-url-row').style.gap = '6px';
    });

    document.getElementById('tqn-add-btn').addEventListener('click', () => {
      const type = document.getElementById('tqn-add-type').value;
      let label, url;
      if (type === 'preset') {
        const sel = document.getElementById('tqn-add-preset');
        const preset = PRESET_PAGES.find(p => p.url === sel.value);
        url   = preset.url;
        label = preset.label;
        if (preset.needsUserId) {
          const uid = prompt(`Enter the Torn user ID for "${label}":`);
          if (!uid || !uid.trim()) return;
          url = url.replace('__USERID__', uid.trim());
          label = `${label} (${uid.trim()})`;
        }
      } else {
        label = document.getElementById('tqn-custom-label').value.trim();
        url = document.getElementById('tqn-custom-url').value.trim();
        if (!label || !url) { alert('Please fill in both label and URL.'); return; }
      }
      settings.hotkeys.push({ label, url, modifier: 'None (single key)', key: '', enabled: true });
      saveSettings(settings);
      renderHotkeyList();
    });

    document.getElementById('tqn-show-btn').addEventListener('change', function () {
      settings.showButton = this.checked;
      fab.style.display = this.checked ? 'flex' : 'none';
      saveSettings(settings);
    });

    document.getElementById('tqn-all-enabled').addEventListener('change', function () {
      settings.allEnabled = this.checked;
      saveSettings(settings);
      showToast(settings.allEnabled ? 'Quick-Nav: Enabled' : 'Quick-Nav: Disabled', !settings.allEnabled);
    });

    // Custom Master Key Listeners inside Modal
    const masterKeyInput = document.getElementById('tqn-master-key');
    masterKeyInput.addEventListener('keydown', function (e) {
      e.preventDefault();
      if (e.key.length === 1) {
        this.value = e.key.toUpperCase();
        settings.masterKey = this.value;
        saveSettings(settings);
      }
    });

    document.getElementById('tqn-master-modifier').addEventListener('change', function () {
      settings.masterModifier = this.value;
      saveSettings(settings);
    });

    document.querySelectorAll('.tqn-os-btn').forEach(btn => {
      btn.addEventListener('click', function () {
        const os = this.dataset.os;
        const autoMatch = (os === 'mac') === IS_MAC_AUTO;
        settings.osOverride = autoMatch ? null : os;
        
        // Adjust the master modifier mapping context on structural shift
        settings.masterModifier = (os === 'mac' || (os === null && IS_MAC_AUTO)) ? 'Cmd+Shift' : 'Ctrl+Shift';
        
        saveSettings(settings);
        // Re-open modal to refresh modifier dropdowns
        closeModal();
        openModal();
      });
    });

    document.getElementById('tqn-close-btn').addEventListener('click', closeModal);
    document.getElementById('tqn-save-btn').addEventListener('click', () => {
      collectHotkeyEdits();
      saveSettings(settings);
      showToast('Hotkeys saved!');
      closeModal();
    });
  }

  function closeModal() {
    const overlay = document.getElementById('tqn-overlay');
    if (overlay) overlay.remove();
  }

  function renderHotkeyList() {
    const list = document.getElementById('tqn-hotkey-list');
    if (!list) return;
    list.innerHTML = '';

    if (settings.hotkeys.length === 0) {
      list.innerHTML = '<div style="color:#666; font-size:13px; padding: 8px 0;">No hotkeys yet. Add one above!</div>';
      return;
    }

    settings.hotkeys.forEach((hk, i) => {
      const enabled = hk.enabled !== false;
      const row = document.createElement('div');
      row.className = 'tqn-hotkey-row' + (enabled ? '' : ' tqn-disabled');
      row.dataset.index = i;
      row.innerHTML = `
        <input type="checkbox" class="tqn-toggle tqn-hk-enabled" ${enabled ? 'checked' : ''} title="Enable/disable this hotkey">
        <input type="text" class="tqn-hk-label" value="${escHtml(hk.label)}" placeholder="Label or URL" title="${escHtml(hk.url)}">
        <select class="tqn-hk-modifier">
          ${getModifiers(isMacMode()).map(m => `<option value="${m}" ${hk.modifier === m ? 'selected' : ''}>${m}</option>`).join('')}
        </select>
        <input type="text" class="tqn-hk-key tqn-key-input" maxlength="1" value="${escHtml(hk.key)}" placeholder="key">
        <button class="tqn-btn-del" data-del="${i}" title="Remove">×</button>
      `;
      list.appendChild(row);
    });

    list.querySelectorAll('.tqn-hk-enabled').forEach(cb => {
      cb.addEventListener('change', function () {
        const row = this.closest('.tqn-hotkey-row');
        row.classList.toggle('tqn-disabled', !this.checked);
        collectHotkeyEdits();
        saveSettings(settings);
      });
    });

    list.querySelectorAll('.tqn-btn-del').forEach(btn => {
      btn.addEventListener('click', () => {
        collectHotkeyEdits();
        const idx = parseInt(btn.dataset.del);
        settings.hotkeys.splice(idx, 1);
        saveSettings(settings);
        renderHotkeyList();
      });
    });

    // Capture single keypress for key field
    list.querySelectorAll('.tqn-hk-key').forEach(input => {
      input.addEventListener('keydown', function (e) {
        e.preventDefault();
        if (e.key.length === 1) this.value = e.key.toUpperCase();
      });
    });
  }

  function collectHotkeyEdits() {
    const list = document.getElementById('tqn-hotkey-list');
    if (!list) return;
    list.querySelectorAll('.tqn-hotkey-row').forEach(row => {
      const i = parseInt(row.dataset.index);
      if (!settings.hotkeys[i]) return;
      settings.hotkeys[i].label    = row.querySelector('.tqn-hk-label').value.trim();
      settings.hotkeys[i].modifier = row.querySelector('.tqn-hk-modifier').value;
      settings.hotkeys[i].key      = row.querySelector('.tqn-hk-key').value.trim().toUpperCase();
      settings.hotkeys[i].enabled  = row.querySelector('.tqn-hk-enabled').checked;
    });
  }

  // ─── Keyboard listener ───────────────────────────────────────────────────────
  function isTyping() {
    const el = document.activeElement;
    if (!el) return false;
    const tag = el.tagName.toLowerCase();
    if (tag === 'input' || tag === 'textarea' || tag === 'select') return true;
    if (el.isContentEditable) return true;
    if (tag === 'iframe') return true;
    if (el.closest && el.closest('[contenteditable="true"]')) return true;
    return false;
  }
  
  document.addEventListener('keydown', function (e) {
    if (isTyping()) return;

    // Settings hotkey: Alt+, (Win) or Option+, (Mac)
    if (e.altKey && !e.ctrlKey && !e.metaKey && e.key === ',') {
      e.preventDefault();
      if (document.getElementById('tqn-overlay')) closeModal();
      else openModal();
      return;
    }

    // Dynamic Customizable Master Kill Switch verification logic
    const currentMasterKey = settings.masterKey || 'H';
    const currentMasterMod = settings.masterModifier || (isMacMode() ? 'Cmd+Shift' : 'Ctrl+Shift');
    
    const isMasterKeyMatch = e.key.toUpperCase() === currentMasterKey.toUpperCase();
    const isMasterModMatch = checkModifier(e, currentMasterMod);

    if (isMasterKeyMatch && isMasterModMatch) {
      e.preventDefault();
      settings.allEnabled = !settings.allEnabled;
      saveSettings(settings);

      // Dynamically reflect changes inside panel live if open
      const modalCheckbox = document.getElementById('tqn-all-enabled');
      if (modalCheckbox) modalCheckbox.checked = settings.allEnabled;

      showToast(settings.allEnabled ? 'Quick-Nav: Enabled' : 'Quick-Nav: Disabled', !settings.allEnabled);
      return;
    }

    // Abort navigation if master switch is turned OFF
    if (settings.allEnabled === false) return;

    for (const hk of settings.hotkeys) {
      if (!hk.key) continue;
      if (hk.enabled === false) continue;
      const mod = hk.modifier || 'Alt';
      const keyMatch = e.key.toUpperCase() === hk.key.toUpperCase();
      const modMatch = checkModifier(e, mod);
      if (keyMatch && modMatch) {
        e.preventDefault();
        window.location.href = resolveUrl(hk.url);
        return;
      }
    }
  });

  function checkModifier(e, mod) {
    // Resolve Mac display labels to internal keys
    const m = MOD_ALIAS[mod] || mod;
    switch (m) {
      case 'None (single key)': return !e.altKey && !e.ctrlKey && !e.shiftKey && !e.metaKey;
      case 'Alt / Option':      return e.altKey  && !e.ctrlKey && !e.shiftKey && !e.metaKey;
      case 'Ctrl':              return e.ctrlKey && !e.altKey  && !e.shiftKey && !e.metaKey;
      case 'Shift':             return e.shiftKey && !e.altKey && !e.ctrlKey  && !e.metaKey;
      case 'Cmd (Mac) / Win':   return e.metaKey && !e.altKey  && !e.shiftKey && !e.ctrlKey;
      case 'Alt+Shift':
      case 'Option+Shift':      return e.altKey  && e.shiftKey && !e.ctrlKey  && !e.metaKey;
      case 'Ctrl+Shift':        return e.ctrlKey && e.shiftKey && !e.altKey   && !e.metaKey;
      case 'Cmd+Shift':         return e.metaKey && e.shiftKey && !e.altKey   && !e.ctrlKey;
      default:                  return false;
    }
  }

  // ─── URL normalizer — accepts full URL or path, always navigates correctly ───
  function resolveUrl(raw) {
    raw = raw.trim();
    if (raw.startsWith('https://www.torn.com')) return raw; // full URL — use as-is
    if (raw.startsWith('http'))                  return raw; // external URL — use as-is
    // path only — prepend origin
    if (!raw.startsWith('/')) raw = '/' + raw;
    return 'https://www.torn.com' + raw;
  }
  
  function showToast(msg, isError = false) {
    const t = document.createElement('div');
    t.className = 'tqn-toast ' + (isError ? 'tqn-toast-error' : 'tqn-toast-success');
    t.textContent = msg;
    document.body.appendChild(t);
    setTimeout(() => t.remove(), 2000);
  }

  function escHtml(s) {
    return String(s ?? '').replace(/&/g,'&amp;').replace(/"/g,'&quot;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
  }

})();