Torn War Tools

War target tools: travel warnings, available targets, med targets, sorting, shortlists

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 or Violentmonkey to install this script.

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

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

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

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!)

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

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

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

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

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

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

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

// ==UserScript==
// @name         Torn War Tools
// @namespace    https://www.torn.com/
// @version      2.15
// @description  War target tools: travel warnings, available targets, med targets, sorting, shortlists
// @author       Systoned
// @match        https://www.torn.com/*
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_xmlhttpRequest
// @connect      api.torn.com
// ==/UserScript==

(function () {
  'use strict';

  // -- Settings ---------------------------------------------------------

  function getApiKey() { return GM_getValue('wtw_api_key', ''); }
  function setApiKey(v) { GM_setValue('wtw_api_key', v.trim()); }
  function getEnemyFactionId() { return GM_getValue('wtw_enemy_faction_id', ''); }
  function setEnemyFactionId(v) { GM_setValue('wtw_enemy_faction_id', String(v).trim()); }
  function getEnemyFactionName() { return GM_getValue('wtw_enemy_faction_name', ''); }
  function isConfigured() { return getApiKey() !== ''; }

  // -- Shortlist --------------------------------------------------------

  function getShortlistKey() { return 'wtw_shortlist_' + getEnemyFactionId(); }
  function getShortlist() {
    try { return JSON.parse(GM_getValue(getShortlistKey(), '[]')); }
    catch (e) { return []; }
  }
  function saveShortlist(list) { GM_setValue(getShortlistKey(), JSON.stringify(list)); }
  function isShortlisted(id) { return getShortlist().indexOf(id) !== -1; }
  function toggleShortlist(id) {
    var list = getShortlist();
    var idx = list.indexOf(id);
    if (idx === -1) { list.push(id); } else { list.splice(idx, 1); }
    saveShortlist(list);
  }
  function clearShortlistForFaction(factionId) {
    GM_setValue('wtw_shortlist_' + factionId, '[]');
  }

  // -- Cache ------------------------------------------------------------

  var memberCache = { data: null, time: 0 };
  var CACHE_TTL = 60000; // 60 seconds
  var WAR_CHECK_TTL = 1800000; // 30 minutes

  function getCachedMembers() {
    if (memberCache.data && (Date.now() - memberCache.time) < CACHE_TTL) {
      return Promise.resolve(memberCache.data);
    }
    var ensure = getEnemyFactionId() ? Promise.resolve() : resolveEnemyFaction();
    return ensure.then(fetchEnemyMembers).then(function (members) {
      memberCache.data = members;
      memberCache.time = Date.now();
      return members;
    });
  }

  function prefetchMembers() {
    if (!isConfigured()) return;
    maybeRefreshWar().then(function () {
      getCachedMembers().catch(function () {});
    });
  }

  // -- API --------------------------------------------------------------

  function apiGet(path) {
    return new Promise(function (resolve, reject) {
      GM_xmlhttpRequest({
        method: 'GET',
        url: 'https://api.torn.com' + path,
        headers: {
          'Authorization': 'ApiKey ' + getApiKey(),
          'Accept': 'application/json'
        },
        onload: function (r) {
          try {
            var data = JSON.parse(r.responseText);
            if (data.error) { reject(data.error.error || 'API error'); return; }
            resolve(data);
          } catch (e) { reject('Parse error'); }
        },
        onerror: function () { reject('Network error'); }
      });
    });
  }

  function fetchEnemyMembers() {
    return apiGet('/v2/faction/' + getEnemyFactionId() + '/members?striptags=true')
      .then(function (data) { return data.members || []; });
  }

  // Resolve the enemy faction from the current ranked war:
  // 1. /v2/faction/basic  -> own faction id
  // 2. /v2/faction/wars   -> ranked war factions; the one that isn't ours is the enemy
  function resolveEnemyFaction() {
    return apiGet('/v2/faction/basic').then(function (data) {
      var ownId = data.basic && data.basic.id;
      if (!ownId) { return Promise.reject('Could not read own faction from API key'); }
      return apiGet('/v2/faction/wars').then(function (w) {
        var ranked = w.wars && w.wars.ranked;
        if (!ranked || !ranked.factions || ranked.factions.length === 0) {
          return Promise.reject('No active ranked war found');
        }
        for (var i = 0; i < ranked.factions.length; i++) {
          var f = ranked.factions[i];
          if (f.id !== ownId) {
            var old = getEnemyFactionId();
            if (old && old !== String(f.id)) {
              clearShortlistForFaction(old);
              memberCache.data = null;
              memberCache.time = 0;
            }
            setEnemyFactionId(f.id);
            GM_setValue('wtw_enemy_faction_name', f.name || '');
            GM_setValue('wtw_war_checked', Date.now());
            return f.id;
          }
        }
        return Promise.reject('Enemy faction not found in war data');
      });
    });
  }

  function maybeRefreshWar() {
    var last = GM_getValue('wtw_war_checked', 0);
    if (!getEnemyFactionId() || (Date.now() - last) > WAR_CHECK_TTL) {
      return resolveEnemyFaction().catch(function () {});
    }
    return Promise.resolve();
  }

  // -- Threat / Target Logic --------------------------------------------

  // Each traveling/abroad member's destination is parsed out of their status
  // description, then compared country-to-country with the clicked
  // destination, instead of searching for the clicked name as a substring.
  // Alias map covers countries where the travel page and the API use
  // different names.
  var CANONICAL_COUNTRY = {
    'uae': 'united arab emirates',
    'uk': 'united kingdom'
  };

  function normalizeCountry(name) {
    var n = String(name).toLowerCase().trim();
    if (n.indexOf('the ') === 0) n = n.substring(4);
    return CANONICAL_COUNTRY[n] || n;
  }

  // Returns { type: 'traveling' | 'abroad' | 'returning', country } or null
  // Traveling descriptions are "Traveling from X to Y": destination is Y,
  // and Y = Torn means they're coming home (excluded from warnings).
  function parseTravelStatus(m) {
    var state = m.status && m.status.state;
    var desc = (m.status && m.status.description) || '';
    var dl = desc.toLowerCase();
    if (state === 'Traveling') {
      var f = dl.indexOf('traveling from ');
      if (f !== -1) {
        var rest = dl.substring(f + 15);
        var toIdx = rest.lastIndexOf(' to ');
        if (toIdx !== -1) {
          var origin = rest.substring(0, toIdx).trim();
          var dest = rest.substring(toIdx + 4).trim();
          if (dest === 'torn') return { type: 'returning', country: origin };
          return { type: 'traveling', country: dest };
        }
      }
      // Older formats, kept as fallbacks
      var r = dl.indexOf('returning to torn from ');
      if (r !== -1) return { type: 'returning', country: dl.substring(r + 23).trim() };
      var t = dl.indexOf('traveling to ');
      if (t !== -1) return { type: 'traveling', country: dl.substring(t + 13).trim() };
      return null;
    }
    if (state === 'Abroad') {
      if (dl.indexOf('in ') === 0) return { type: 'abroad', country: dl.substring(3).trim() };
      return { type: 'abroad', country: dl };
    }
    return null;
  }

  function findThreats(members, countryName) {
    var threats = [];
    var want = normalizeCountry(countryName);
    for (var i = 0; i < members.length; i++) {
      var m = members[i];
      var p = parseTravelStatus(m);
      if (!p) continue;
      var have = normalizeCountry(p.country);
      if (have === want || have.indexOf(want) !== -1 || want.indexOf(have) !== -1) {
        threats.push({
          id: m.id, name: m.name, level: m.level,
          type: p.type, description: m.status.description
        });
      }
    }
    return threats;
  }

  var currentSort = 'level'; // 'level' or 'activity'
  var sortAsc = false; // false = descending (default), true = ascending

  var activityOrder = { 'Online': 0, 'Idle': 1, 'Offline': 2, 'Unknown': 3 };

  function parseRelativeTime(str) {
    // Convert "X minutes ago", "X hours ago" etc to seconds
    if (!str || str === 'Unknown') return Infinity;
    var match = str.match(/(\d+)\s*(second|minute|hour|day|week|month|year)/i);
    if (!match) return Infinity;
    var num = parseInt(match[1], 10);
    var unit = match[2].toLowerCase();
    var multipliers = { second: 1, minute: 60, hour: 3600, day: 86400, week: 604800, month: 2592000, year: 31536000 };
    return num * (multipliers[unit] || 1);
  }

  function getAvailableTargets(members, sortMode, ascending) {
    var targets = [];
    for (var i = 0; i < members.length; i++) {
      var m = members[i];
      var state = m.status && m.status.state;
      if (state === 'Okay') {
        targets.push({
          id: m.id, name: m.name, level: m.level,
          lastAction: m.last_action ? m.last_action.relative : 'Unknown',
          actionStatus: m.last_action ? m.last_action.status : 'Unknown',
          lastActionSeconds: parseRelativeTime(m.last_action ? m.last_action.relative : 'Unknown')
        });
      }
    }

    if (sortMode === 'activity') {
      targets.sort(function (a, b) {
        var ao = activityOrder[a.actionStatus] !== undefined ? activityOrder[a.actionStatus] : 3;
        var bo = activityOrder[b.actionStatus] !== undefined ? activityOrder[b.actionStatus] : 3;
        if (ao !== bo) return ascending ? bo - ao : ao - bo;
        // Within same status group, sort by recency
        var recency = a.lastActionSeconds - b.lastActionSeconds;
        return ascending ? -recency : recency;
      });
    } else {
      targets.sort(function (a, b) {
        return ascending ? a.level - b.level : b.level - a.level;
      });
    }

    // Pin shortlisted to top
    var shortlist = getShortlist();
    if (shortlist.length > 0) {
      var pinned = [];
      var rest = [];
      for (var j = 0; j < targets.length; j++) {
        if (shortlist.indexOf(targets[j].id) !== -1) {
          pinned.push(targets[j]);
        } else {
          rest.push(targets[j]);
        }
      }
      targets = pinned.concat(rest);
    }

    return targets;
  }

  function getMedTargets(members) {
    var targets = [];
    var now = Math.floor(Date.now() / 1000);
    for (var i = 0; i < members.length; i++) {
      var m = members[i];
      var state = m.status && m.status.state;
      if (state === 'Hospital') {
        var until = m.status.until || 0;
        var remaining = until - now;
        if (remaining < 0) remaining = 0;
        targets.push({
          id: m.id, name: m.name, level: m.level,
          description: m.status.description || '',
          until: until,
          remaining: remaining
        });
      }
    }
    targets.sort(function (a, b) { return a.remaining - b.remaining; });
    return targets;
  }

  function formatTime(seconds) {
    if (seconds <= 0) return 'Now';
    var h = Math.floor(seconds / 3600);
    var m = Math.floor((seconds % 3600) / 60);
    var s = seconds % 60;
    var parts = [];
    if (h > 0) parts.push(h + 'h');
    if (m > 0) parts.push(m + 'm');
    if (h === 0) parts.push(s + 's');
    return parts.join(' ');
  }

  // -- Styles -----------------------------------------------------------
  // Torn-native look: light/dark aware, black gradient title bars,
  // torn-btn used for buttons where possible.

  function injectStyles() {
    var style = document.createElement('style');
    style.textContent = [
      // Overlay
      '.wtw-overlay {',
      '  position: fixed;',
      '  top: 0; left: 0; right: 0; bottom: 0;',
      '  background: rgba(0, 0, 0, 0.65);',
      '  z-index: 999999;',
      '  display: flex;',
      '  align-items: center;',
      '  justify-content: center;',
      '  padding: 20px;',
      '}',

      // Modal panel
      '.wtw-modal {',
      '  background: #f2f2f2;',
      '  border-radius: 5px;',
      '  max-width: 500px;',
      '  width: 100%;',
      '  max-height: 80vh;',
      '  display: flex;',
      '  flex-direction: column;',
      '  overflow: hidden;',
      '  color: #333;',
      '  font-family: Arial, Helvetica, sans-serif;',
      '  font-size: 12px;',
      '  box-shadow: 0 0 15px rgba(0, 0, 0, 0.6);',
      '}',
      'body.dark-mode .wtw-modal { background: #2e2e2e; color: #ccc; }',

      // Title bar (Torn black gradient)
      '.wtw-title {',
      '  background: linear-gradient(180deg, #666 0%, #333 50%, #111 100%);',
      '  color: #fff;',
      '  font-weight: bold;',
      '  padding: 6px 10px;',
      '  display: flex;',
      '  justify-content: space-between;',
      '  align-items: center;',
      '  text-shadow: 0 1px 0 #000;',
      '  border-radius: 5px 5px 0 0;',
      '}',
      '.wtw-title-warn {',
      '  background: linear-gradient(180deg, #a33 0%, #611 50%, #300 100%);',
      '}',
      '.wtw-close {',
      '  background: none;',
      '  border: none;',
      '  color: #bbb;',
      '  font-size: 16px;',
      '  cursor: pointer;',
      '  padding: 0 2px;',
      '  line-height: 1;',
      '}',
      '.wtw-close:hover { color: #fff; }',

      // Body
      '.wtw-body { padding: 10px; overflow-y: auto; }',

      // Buttons (fallback styling in case torn-btn is unavailable)
      '.wtw-btn { cursor: pointer; }',
      '.wtw-actions { display: flex; gap: 8px; margin: 0 0 10px 0; flex-wrap: wrap; }',
      '.wtw-actions .wtw-btn { flex: 1; min-width: 120px; }',
      '.wtw-btn.wtw-active { box-shadow: inset 0 1px 4px rgba(0, 0, 0, 0.5); }',
      '.wtw-form-buttons { display: flex; gap: 8px; margin-top: 12px; }',

      // Settings form
      '.wtw-form-row { margin: 0 0 10px 0; }',
      '.wtw-form-row label {',
      '  display: block;',
      '  color: #666;',
      '  margin: 0 0 3px 0;',
      '  font-size: 11px;',
      '}',
      'body.dark-mode .wtw-form-row label { color: #999; }',
      '.wtw-form-row input {',
      '  width: 100%;',
      '  box-sizing: border-box;',
      '  background: #fff;',
      '  border: 1px solid #ccc;',
      '  color: #333;',
      '  padding: 5px 8px;',
      '  border-radius: 5px;',
      '  font-size: 12px;',
      '  font-family: monospace;',
      '}',
      'body.dark-mode .wtw-form-row input { background: #222; border-color: #444; color: #ccc; }',
      '.wtw-form-row input:focus { outline: none; border-color: #888; }',
      '.wtw-form-hint { font-size: 11px; color: #888; margin-top: 6px; }',

      // Settings bar (shown when configured)
      '.wtw-settings-bar {',
      '  display: flex;',
      '  align-items: center;',
      '  justify-content: space-between;',
      '  padding: 0 0 8px 0;',
      '  margin: 0 0 10px 0;',
      '  border-bottom: 1px solid #ccc;',
      '  font-size: 11px;',
      '}',
      'body.dark-mode .wtw-settings-bar { border-bottom-color: #444; }',
      '.wtw-settings-left { display: flex; align-items: center; gap: 6px; color: #666; }',
      'body.dark-mode .wtw-settings-left { color: #999; }',
      '.wtw-settings-right { display: flex; gap: 6px; }',
      '.wtw-dot { width: 8px; height: 8px; border-radius: 50%; background: #85b200; }',
      '.wtw-settings-edit {',
      '  background: none;',
      '  border: 1px solid #ccc;',
      '  color: #666;',
      '  padding: 2px 8px;',
      '  border-radius: 5px;',
      '  cursor: pointer;',
      '  font-size: 11px;',
      '}',
      '.wtw-settings-edit:hover { border-color: #888; color: #333; }',
      'body.dark-mode .wtw-settings-edit { border-color: #444; color: #999; }',
      'body.dark-mode .wtw-settings-edit:hover { border-color: #666; color: #ccc; }',

      // Cooldown strip
      '.wtw-cooldowns { display: flex; gap: 8px; margin: 0 0 10px 0; }',
      '.wtw-cd {',
      '  flex: 1;',
      '  text-align: center;',
      '  border: 1px solid #ccc;',
      '  border-radius: 5px;',
      '  padding: 5px 4px;',
      '  background: #fff;',
      '}',
      'body.dark-mode .wtw-cd { border-color: #444; background: #333; }',
      '.wtw-cd-label { font-size: 10px; color: #888; text-transform: uppercase; }',
      '.wtw-cd-time { font-weight: bold; margin-top: 2px; }',
      '.wtw-cd-ready { color: #678c00; }',
      'body.dark-mode .wtw-cd-ready { color: #8abe3c; }',

      // Target list (Torn panel style: bordered rounded box, divided rows)
      '.wtw-list {',
      '  margin: 0;',
      '  padding: 0;',
      '  list-style: none;',
      '  border: 1px solid #ccc;',
      '  border-radius: 5px;',
      '  overflow: hidden;',
      '  background: #fff;',
      '}',
      'body.dark-mode .wtw-list { border-color: #444; background: #333; }',
      '.wtw-list-item {',
      '  padding: 6px 10px;',
      '  border-bottom: 1px solid #e5e5e5;',
      '  border-left: 3px solid transparent;',
      '}',
      '.wtw-list-item:last-child { border-bottom: none; }',
      'body.dark-mode .wtw-list-item { border-bottom-color: #444; }',
      '.wtw-list-item-ok { border-left-color: #85b200; }',
      '.wtw-list-item-hosp { border-left-color: #cc8800; }',
      '.wtw-list-item-threat-traveling { border-left-color: #cc8800; }',
      '.wtw-list-item-threat-abroad { border-left-color: #cc3333; }',
      '.wtw-list-item-threat-returning { border-left-color: #4a7ba6; }',
      '.wtw-list-name { font-weight: bold; }',
      '.wtw-list-name a { color: #069; text-decoration: none; }',
      '.wtw-list-name a:hover { text-decoration: underline; }',
      'body.dark-mode .wtw-list-name a { color: #7ab5e2; }',
      '.wtw-list-detail { color: #888; font-size: 11px; margin-top: 2px; }',
      'body.dark-mode .wtw-list-detail { color: #999; }',

      // Tags
      '.wtw-tag {',
      '  display: inline-block;',
      '  padding: 1px 6px;',
      '  border-radius: 3px;',
      '  font-size: 10px;',
      '  font-weight: bold;',
      '  margin-left: 6px;',
      '}',
      '.wtw-tag-online { background: #e4efd0; color: #678c00; }',
      '.wtw-tag-idle { background: #f5e9cc; color: #a67c00; }',
      '.wtw-tag-offline { background: #e5e5e5; color: #888; }',
      '.wtw-tag-traveling { background: #f5e9cc; color: #a67c00; }',
      '.wtw-tag-abroad { background: #f2d5d5; color: #b32d2d; }',
      '.wtw-tag-returning { background: #d8e4ef; color: #33658a; }',
      'body.dark-mode .wtw-tag-online { background: #1a331a; color: #8abe3c; }',
      'body.dark-mode .wtw-tag-idle { background: #332e1a; color: #cc8800; }',
      'body.dark-mode .wtw-tag-offline { background: #2a2a2a; color: #888; }',
      'body.dark-mode .wtw-tag-traveling { background: #332200; color: #cc8800; }',
      'body.dark-mode .wtw-tag-abroad { background: #331111; color: #e05555; }',
      'body.dark-mode .wtw-tag-returning { background: #16222e; color: #6da4d4; }',

      // Status / empty / count
      '.wtw-status { text-align: center; color: #888; padding: 20px 0; }',
      '.wtw-error { color: #b32d2d; }',
      'body.dark-mode .wtw-error { color: #e05555; }',
      '.wtw-count { color: #888; font-size: 11px; margin: 0 0 8px 0; }',

      // Travel warning
      '.wtw-warn-sub { color: #888; font-size: 12px; margin: 0 0 10px 0; }',
      '.wtw-dismiss { display: block; width: 100%; margin-top: 12px; }',

      // Sidebar icon
      '.wtw-sidebar-icon path { fill: currentColor; }',

      // Sort controls
      '.wtw-sort-bar {',
      '  display: flex;',
      '  align-items: center;',
      '  gap: 6px;',
      '  margin: 0 0 8px 0;',
      '  font-size: 11px;',
      '}',
      '.wtw-sort-label { color: #888; }',
      '.wtw-sort-btn {',
      '  background: none;',
      '  border: 1px solid #ccc;',
      '  color: #666;',
      '  padding: 2px 8px;',
      '  border-radius: 5px;',
      '  cursor: pointer;',
      '  font-size: 11px;',
      '}',
      '.wtw-sort-btn:hover { border-color: #888; color: #333; }',
      '.wtw-sort-btn.wtw-sort-active { border-color: #85b200; color: #678c00; }',
      'body.dark-mode .wtw-sort-btn { border-color: #444; color: #999; }',
      'body.dark-mode .wtw-sort-btn:hover { border-color: #666; color: #ccc; }',
      'body.dark-mode .wtw-sort-btn.wtw-sort-active { border-color: #85b200; color: #8abe3c; }',
      '.wtw-sort-arrow { margin-left: 3px; font-size: 9px; }',

      // Shortlist checkbox
      '.wtw-shortlist-cb {',
      '  float: right;',
      '  cursor: pointer;',
      '  width: 14px;',
      '  height: 14px;',
      '  accent-color: #85b200;',
      '  margin: 2px 0 0 0;',
      '}',

      // Shortlist header
      '.wtw-list-header {',
      '  display: flex;',
      '  justify-content: space-between;',
      '  font-size: 10px;',
      '  color: #888;',
      '  padding: 0 10px 4px 10px;',
      '  text-transform: uppercase;',
      '}',

      // Pinned separator
      '.wtw-pinned-sep {',
      '  border: none;',
      '  border-top: 1px dashed #ccc;',
      '  margin: 0;',
      '}',
      'body.dark-mode .wtw-pinned-sep { border-top-color: #555; }'
    ].join('\n');
    document.head.appendChild(style);
  }

  function makeButton(text, extraClass) {
    var btn = document.createElement('button');
    btn.className = 'torn-btn wtw-btn' + (extraClass ? ' ' + extraClass : '');
    btn.textContent = text;
    return btn;
  }

  // -- Modal Framework --------------------------------------------------

  var currentOverlay = null;
  var cooldownTimer = null;

  function stopCooldownTimer() {
    if (cooldownTimer) {
      clearInterval(cooldownTimer);
      cooldownTimer = null;
    }
  }

  function closeModal() {
    stopCooldownTimer();
    if (currentOverlay) {
      currentOverlay.remove();
      currentOverlay = null;
    }
  }

  // Creates overlay + modal with a Torn-style title bar. Returns the body element.
  function createModal(titleText, warnStyle) {
    closeModal();
    var overlay = document.createElement('div');
    overlay.className = 'wtw-overlay';
    overlay.addEventListener('click', function (e) {
      if (e.target === overlay) closeModal();
    });

    var modal = document.createElement('div');
    modal.className = 'wtw-modal';

    var title = document.createElement('div');
    title.className = 'wtw-title' + (warnStyle ? ' wtw-title-warn' : '');
    var titleSpan = document.createElement('span');
    titleSpan.textContent = titleText;
    var closeBtn = document.createElement('button');
    closeBtn.className = 'wtw-close';
    closeBtn.textContent = '\u00D7';
    closeBtn.addEventListener('click', closeModal);
    title.appendChild(titleSpan);
    title.appendChild(closeBtn);

    var body = document.createElement('div');
    body.className = 'wtw-body';

    modal.appendChild(title);
    modal.appendChild(body);
    overlay.appendChild(modal);
    document.body.appendChild(overlay);
    currentOverlay = overlay;
    return body;
  }

  // -- Main War Modal ---------------------------------------------------

  function openWarModal() {
    var content = createModal('War Tools', false);
    if (!isConfigured()) {
      renderSettingsForm(content, false);
    } else {
      renderConfiguredView(content);
    }
  }

  function renderConfiguredView(container) {
    container.innerHTML = '';

    // Enemy faction not yet resolved: detect from ranked war
    if (!getEnemyFactionId()) {
      var status = document.createElement('div');
      status.className = 'wtw-status';
      status.textContent = 'Detecting current ranked war...';
      container.appendChild(status);
      resolveEnemyFaction().then(function () {
        renderConfiguredView(container);
      }).catch(function (err) {
        container.innerHTML = '';
        var errEl = document.createElement('div');
        errEl.className = 'wtw-status wtw-error';
        errEl.textContent = 'Error: ' + err;
        container.appendChild(errEl);
        var buttons = document.createElement('div');
        buttons.className = 'wtw-form-buttons';
        var retryBtn = makeButton('Retry');
        retryBtn.addEventListener('click', function () { renderConfiguredView(container); });
        var editBtn = makeButton('Edit API Key');
        editBtn.addEventListener('click', function () { renderSettingsForm(container, true); });
        buttons.appendChild(retryBtn);
        buttons.appendChild(editBtn);
        container.appendChild(buttons);
      });
      return;
    }

    // Settings bar
    var bar = document.createElement('div');
    bar.className = 'wtw-settings-bar';
    var left = document.createElement('div');
    left.className = 'wtw-settings-left';
    var dot = document.createElement('span');
    dot.className = 'wtw-dot';
    var info = document.createElement('span');
    var name = getEnemyFactionName();
    info.textContent = 'Enemy: ' + (name ? name + ' [' + getEnemyFactionId() + ']' : getEnemyFactionId());
    left.appendChild(dot);
    left.appendChild(info);

    var right = document.createElement('div');
    right.className = 'wtw-settings-right';
    var refreshBtn = document.createElement('button');
    refreshBtn.className = 'wtw-settings-edit';
    refreshBtn.textContent = 'Refresh War';
    refreshBtn.addEventListener('click', function () {
      setEnemyFactionId('');
      renderConfiguredView(container);
    });
    var editBtn = document.createElement('button');
    editBtn.className = 'wtw-settings-edit';
    editBtn.textContent = 'Edit Key';
    editBtn.addEventListener('click', function () {
      renderSettingsForm(container, true);
    });
    right.appendChild(refreshBtn);
    right.appendChild(editBtn);

    bar.appendChild(left);
    bar.appendChild(right);
    container.appendChild(bar);

    // Cooldowns
    renderCooldowns(container);

    // Action buttons
    var actions = document.createElement('div');
    actions.className = 'wtw-actions';

    var availBtn = makeButton('Available');
    availBtn.addEventListener('click', function () {
      setActiveBtn(availBtn);
      loadAvailableTargets(listArea);
    });

    var travelBtn = makeButton('Traveling');
    travelBtn.addEventListener('click', function () {
      setActiveBtn(travelBtn);
      loadTravelingTargets(listArea);
    });

    var medBtn = makeButton('Hosp');
    medBtn.addEventListener('click', function () {
      setActiveBtn(medBtn);
      loadMedTargets(listArea);
    });

    actions.appendChild(availBtn);
    actions.appendChild(travelBtn);
    actions.appendChild(medBtn);
    container.appendChild(actions);

    var listArea = document.createElement('div');
    listArea.id = 'wtw-list-area';
    container.appendChild(listArea);

    function setActiveBtn(active) {
      var btns = actions.querySelectorAll('.wtw-btn');
      for (var i = 0; i < btns.length; i++) {
        btns[i].classList.remove('wtw-active');
      }
      active.classList.add('wtw-active');
    }
  }

  function renderCooldowns(container) {
    var strip = document.createElement('div');
    strip.className = 'wtw-cooldowns';
    container.appendChild(strip);

    apiGet('/v2/user/cooldowns').then(function (data) {
      var cd = data.cooldowns || {};
      var now = Date.now();
      var items = [
        { label: 'Drug', until: now + (cd.drug || 0) * 1000 },
        { label: 'Booster', until: now + (cd.booster || 0) * 1000 },
        { label: 'Med', until: now + (cd.medical || 0) * 1000 }
      ];

      strip.innerHTML = '';
      var timeEls = [];
      for (var i = 0; i < items.length; i++) {
        var box = document.createElement('div');
        box.className = 'wtw-cd';
        var lbl = document.createElement('div');
        lbl.className = 'wtw-cd-label';
        lbl.textContent = items[i].label;
        var time = document.createElement('div');
        time.className = 'wtw-cd-time';
        box.appendChild(lbl);
        box.appendChild(time);
        strip.appendChild(box);
        timeEls.push(time);
      }

      function tick() {
        var n = Date.now();
        for (var i = 0; i < items.length; i++) {
          var remaining = Math.ceil((items[i].until - n) / 1000);
          if (remaining <= 0) {
            timeEls[i].textContent = 'Ready';
            timeEls[i].classList.add('wtw-cd-ready');
          } else {
            timeEls[i].textContent = formatTime(remaining);
            timeEls[i].classList.remove('wtw-cd-ready');
          }
        }
      }

      tick();
      stopCooldownTimer();
      cooldownTimer = setInterval(tick, 1000);
    }).catch(function () {
      strip.remove();
    });
  }

  function renderSettingsForm(container, hasCancel) {
    stopCooldownTimer();
    container.innerHTML = '';

    var row1 = document.createElement('div');
    row1.className = 'wtw-form-row';
    var lbl1 = document.createElement('label');
    lbl1.textContent = 'Torn API Key';
    var inp1 = document.createElement('input');
    inp1.type = 'password';
    inp1.placeholder = 'Enter your API key';
    inp1.value = getApiKey();
    row1.appendChild(lbl1);
    row1.appendChild(inp1);

    var hint = document.createElement('div');
    hint.className = 'wtw-form-hint';
    hint.textContent = 'Requires a Minimal access key. The enemy faction is detected automatically from your current ranked war.';

    var buttons = document.createElement('div');
    buttons.className = 'wtw-form-buttons';

    var saveBtn = makeButton('Save');
    saveBtn.addEventListener('click', function () {
      var key = inp1.value.trim();
      if (!key) { inp1.style.borderColor = '#cc3333'; return; }
      setApiKey(key);
      setEnemyFactionId('');
      GM_setValue('wtw_war_checked', 0);
      memberCache.data = null;
      memberCache.time = 0;
      renderConfiguredView(container);
    });
    buttons.appendChild(saveBtn);

    if (hasCancel) {
      var cancelBtn = makeButton('Cancel');
      cancelBtn.addEventListener('click', function () {
        renderConfiguredView(container);
      });
      buttons.appendChild(cancelBtn);
    }

    container.appendChild(row1);
    container.appendChild(hint);
    container.appendChild(buttons);
  }

  // -- List Renderers ---------------------------------------------------

  function loadAvailableTargets(container) {
    container.innerHTML = '';
    var status = document.createElement('div');
    status.className = 'wtw-status';
    status.textContent = 'Loading...';
    container.appendChild(status);

    getCachedMembers().then(function (members) {
      container.innerHTML = '';
      var targets = getAvailableTargets(members, currentSort, sortAsc);
      var shortlist = getShortlist();

      // Sort bar
      var sortBar = document.createElement('div');
      sortBar.className = 'wtw-sort-bar';
      var sortLabel = document.createElement('span');
      sortLabel.className = 'wtw-sort-label';
      sortLabel.textContent = 'Sort:';
      sortBar.appendChild(sortLabel);

      var levelBtn = document.createElement('button');
      levelBtn.className = 'wtw-sort-btn' + (currentSort === 'level' ? ' wtw-sort-active' : '');
      levelBtn.innerHTML = 'Level' + (currentSort === 'level' ? '<span class="wtw-sort-arrow">' + (sortAsc ? '&#9650;' : '&#9660;') + '</span>' : '');
      levelBtn.addEventListener('click', function () {
        if (currentSort === 'level') {
          sortAsc = !sortAsc;
        } else {
          currentSort = 'level';
          sortAsc = false;
        }
        loadAvailableTargets(container);
      });
      sortBar.appendChild(levelBtn);

      var actBtn = document.createElement('button');
      actBtn.className = 'wtw-sort-btn' + (currentSort === 'activity' ? ' wtw-sort-active' : '');
      actBtn.innerHTML = 'Last Active' + (currentSort === 'activity' ? '<span class="wtw-sort-arrow">' + (sortAsc ? '&#9650;' : '&#9660;') + '</span>' : '');
      actBtn.addEventListener('click', function () {
        if (currentSort === 'activity') {
          sortAsc = !sortAsc;
        } else {
          currentSort = 'activity';
          sortAsc = false;
        }
        loadAvailableTargets(container);
      });
      sortBar.appendChild(actBtn);
      container.appendChild(sortBar);

      var count = document.createElement('div');
      count.className = 'wtw-count';
      var pinnedCount = 0;
      for (var c = 0; c < targets.length; c++) {
        if (shortlist.indexOf(targets[c].id) !== -1) pinnedCount++;
      }
      count.textContent = targets.length + ' available target' + (targets.length !== 1 ? 's' : '') +
        (pinnedCount > 0 ? ' (' + pinnedCount + ' shortlisted)' : '');
      container.appendChild(count);

      if (targets.length === 0) {
        var empty = document.createElement('div');
        empty.className = 'wtw-status';
        empty.textContent = 'No available targets';
        container.appendChild(empty);
        return;
      }

      // Column header
      var listHeader = document.createElement('div');
      listHeader.className = 'wtw-list-header';
      var targetLabel = document.createElement('span');
      targetLabel.textContent = 'Target';
      var shortlistLabel = document.createElement('span');
      shortlistLabel.textContent = 'Shortlist';
      listHeader.appendChild(targetLabel);
      listHeader.appendChild(shortlistLabel);
      container.appendChild(listHeader);

      var list = document.createElement('ul');
      list.className = 'wtw-list';
      var separatorAdded = false;

      for (var i = 0; i < targets.length; i++) {
        var t = targets[i];
        var isPinned = shortlist.indexOf(t.id) !== -1;

        // Add separator after last pinned item
        if (!isPinned && !separatorAdded && pinnedCount > 0) {
          var sep = document.createElement('hr');
          sep.className = 'wtw-pinned-sep';
          list.appendChild(sep);
          separatorAdded = true;
        }

        var li = document.createElement('li');
        li.className = 'wtw-list-item wtw-list-item-ok';

        var cb = document.createElement('input');
        cb.type = 'checkbox';
        cb.className = 'wtw-shortlist-cb';
        cb.checked = isPinned;
        cb.title = isPinned ? 'Remove from shortlist' : 'Add to shortlist';
        (function (tid) {
          cb.addEventListener('change', function () {
            toggleShortlist(tid);
            loadAvailableTargets(container);
          });
        })(t.id);
        li.appendChild(cb);

        var nameEl = document.createElement('div');
        nameEl.className = 'wtw-list-name';
        var link = document.createElement('a');
        link.href = 'https://www.torn.com/profiles.php?XID=' + t.id;
        link.target = '_blank';
        link.textContent = t.name;
        nameEl.appendChild(link);

        var tag = document.createElement('span');
        var tagClass = 'wtw-tag wtw-tag-' + t.actionStatus.toLowerCase();
        tag.className = tagClass;
        tag.textContent = t.actionStatus;
        nameEl.appendChild(tag);

        var detail = document.createElement('div');
        detail.className = 'wtw-list-detail';
        detail.textContent = 'Level ' + t.level + ' \u2014 ' + t.lastAction;

        li.appendChild(nameEl);
        li.appendChild(detail);
        list.appendChild(li);
      }
      container.appendChild(list);
    }).catch(function (err) {
      container.innerHTML = '';
      var errEl = document.createElement('div');
      errEl.className = 'wtw-status wtw-error';
      errEl.textContent = 'Error: ' + err;
      container.appendChild(errEl);
    });
  }

  function loadMedTargets(container) {
    container.innerHTML = '';
    var status = document.createElement('div');
    status.className = 'wtw-status';
    status.textContent = 'Loading...';
    container.appendChild(status);

    getCachedMembers().then(function (members) {
      container.innerHTML = '';
      var targets = getMedTargets(members);

      var count = document.createElement('div');
      count.className = 'wtw-count';
      count.textContent = targets.length + ' hospitalised member' + (targets.length !== 1 ? 's' : '');
      container.appendChild(count);

      if (targets.length === 0) {
        var empty = document.createElement('div');
        empty.className = 'wtw-status';
        empty.textContent = 'No members in hospital';
        container.appendChild(empty);
        return;
      }

      var list = document.createElement('ul');
      list.className = 'wtw-list';
      for (var i = 0; i < targets.length; i++) {
        var t = targets[i];
        var li = document.createElement('li');
        li.className = 'wtw-list-item wtw-list-item-hosp';

        var isPinned = isShortlisted(t.id);
        var cb = document.createElement('input');
        cb.type = 'checkbox';
        cb.className = 'wtw-shortlist-cb';
        cb.checked = isPinned;
        cb.title = isPinned ? 'Remove from shortlist' : 'Add to shortlist';
        (function (tid) {
          cb.addEventListener('change', function () {
            toggleShortlist(tid);
            loadMedTargets(container);
          });
        })(t.id);
        li.appendChild(cb);

        var nameEl = document.createElement('div');
        nameEl.className = 'wtw-list-name';
        var link = document.createElement('a');
        link.href = 'https://www.torn.com/profiles.php?XID=' + t.id;
        link.target = '_blank';
        link.textContent = t.name;
        nameEl.appendChild(link);

        var tag = document.createElement('span');
        tag.className = 'wtw-tag wtw-tag-idle';
        tag.textContent = formatTime(t.remaining);
        nameEl.appendChild(tag);

        var detail = document.createElement('div');
        detail.className = 'wtw-list-detail';
        detail.textContent = 'Level ' + t.level + ' \u2014 ' + t.description;

        li.appendChild(nameEl);
        li.appendChild(detail);
        list.appendChild(li);
      }
      container.appendChild(list);
    }).catch(function (err) {
      container.innerHTML = '';
      var errEl = document.createElement('div');
      errEl.className = 'wtw-status wtw-error';
      errEl.textContent = 'Error: ' + err;
      container.appendChild(errEl);
    });
  }

  function loadTravelingTargets(container) {
    container.innerHTML = '';
    var status = document.createElement('div');
    status.className = 'wtw-status';
    status.textContent = 'Loading...';
    container.appendChild(status);

    getCachedMembers().then(function (members) {
      container.innerHTML = '';

      var travel = [];
      for (var i = 0; i < members.length; i++) {
        var m = members[i];
        var p = parseTravelStatus(m);
        if (!p) continue;
        travel.push({
          id: m.id, name: m.name, level: m.level,
          type: p.type,
          description: (m.status && m.status.description) || ''
        });
      }
      travel.sort(function (a, b) { return b.level - a.level; });

      var count = document.createElement('div');
      count.className = 'wtw-count';
      count.textContent = travel.length + ' member' + (travel.length !== 1 ? 's' : '') + ' traveling or abroad';
      container.appendChild(count);

      if (travel.length === 0) {
        var empty = document.createElement('div');
        empty.className = 'wtw-status';
        empty.textContent = 'No members traveling';
        container.appendChild(empty);
        return;
      }

      var list = document.createElement('ul');
      list.className = 'wtw-list';
      for (var v = 0; v < travel.length; v++) {
        var t = travel[v];
        var li = document.createElement('li');
        li.className = 'wtw-list-item wtw-list-item-threat-' + t.type;

        var isPinned = isShortlisted(t.id);
        var cb = document.createElement('input');
        cb.type = 'checkbox';
        cb.className = 'wtw-shortlist-cb';
        cb.checked = isPinned;
        cb.title = isPinned ? 'Remove from shortlist' : 'Add to shortlist';
        (function (tid) {
          cb.addEventListener('change', function () {
            toggleShortlist(tid);
            loadTravelingTargets(container);
          });
        })(t.id);
        li.appendChild(cb);

        var nameEl = document.createElement('div');
        nameEl.className = 'wtw-list-name';
        var link = document.createElement('a');
        link.href = 'https://www.torn.com/profiles.php?XID=' + t.id;
        link.target = '_blank';
        link.textContent = t.name;
        nameEl.appendChild(link);

        var tag = document.createElement('span');
        tag.className = 'wtw-tag wtw-tag-' + t.type;
        tag.textContent = t.type === 'traveling' ? 'EN ROUTE' : (t.type === 'returning' ? 'RETURNING' : 'ABROAD');
        nameEl.appendChild(tag);

        var detail = document.createElement('div');
        detail.className = 'wtw-list-detail';
        detail.textContent = 'Level ' + t.level + ' \u2014 ' + t.description;

        li.appendChild(nameEl);
        li.appendChild(detail);
        list.appendChild(li);
      }
      container.appendChild(list);
    }).catch(function (err) {
      container.innerHTML = '';
      var errEl = document.createElement('div');
      errEl.className = 'wtw-status wtw-error';
      errEl.textContent = 'Error: ' + err;
      container.appendChild(errEl);
    });
  }

  // -- Travel Warning Modal ---------------------------------------------

  function showTravelWarning(countryName, threats) {
    var body = createModal('War Targets in ' + countryName, true);

    var sub = document.createElement('div');
    sub.className = 'wtw-warn-sub';
    sub.textContent = threats.length + ' enemy member' + (threats.length !== 1 ? 's' : '') + ' detected:';
    body.appendChild(sub);

    var list = document.createElement('ul');
    list.className = 'wtw-list';
    for (var i = 0; i < threats.length; i++) {
      var t = threats[i];
      var li = document.createElement('li');
      li.className = 'wtw-list-item wtw-list-item-threat-' + t.type;

      var nameEl = document.createElement('div');
      nameEl.className = 'wtw-list-name';
      var link = document.createElement('a');
      link.href = 'https://www.torn.com/profiles.php?XID=' + t.id;
      link.target = '_blank';
      link.textContent = t.name;
      nameEl.appendChild(link);

      var tag = document.createElement('span');
      tag.className = 'wtw-tag wtw-tag-' + t.type;
      tag.textContent = t.type === 'traveling' ? 'EN ROUTE' : (t.type === 'returning' ? 'RETURNING' : 'IN COUNTRY');
      nameEl.appendChild(tag);

      var detail = document.createElement('div');
      detail.className = 'wtw-list-detail';
      detail.textContent = 'Level ' + t.level + ' \u2014 ' + t.description;

      li.appendChild(nameEl);
      li.appendChild(detail);
      list.appendChild(li);
    }
    body.appendChild(list);

    var dismiss = makeButton('Dismiss', 'wtw-dismiss');
    dismiss.addEventListener('click', closeModal);
    body.appendChild(dismiss);
  }

  // -- Sidebar Injection ------------------------------------------------
  // Class names are copied from the existing travel nav item at runtime.
  // Hashed suffixes change between builds, so lookups use [class*=...] prefixes.

  var warSvg = '<svg xmlns="http://www.w3.org/2000/svg" class="wtw-sidebar-icon" stroke="transparent" stroke-width="0" width="16" height="16" viewBox="0 0 16 16"><path d="M8,0L0,3v5c0,4.4,3.4,8.5,8,10c4.6-1.5,8-5.6,8-10V3L8,0z M12,9H9v3H7V9H4V7h3V4h2v3h3V9z"/></svg>';

  function copyClass(sourceRoot, selector) {
    var el = sourceRoot.querySelector(selector);
    return el ? el.className : '';
  }

  // Users can hide sidebar links (including Travel Agency) in Torn's settings,
  // so fall back to the last visible area link as the reference/insertion point.
  function findDesktopNavRef() {
    var travel = document.querySelector('#nav-travel_agency');
    if (travel && !travel.closest('[class*="swiper-slide"]')) return travel;
    var candidates = document.querySelectorAll('div[id^="nav-"]');
    var last = null;
    for (var i = 0; i < candidates.length; i++) {
      var el = candidates[i];
      if (el.id.indexOf('nav-war_tools') === 0) continue;
      if (el.closest('[class*="swiper-slide"]')) continue;
      if (!el.querySelector('[class*="area-row"]')) continue;
      last = el;
    }
    return last;
  }

  function findMobileNavRef() {
    var travel = document.querySelector('#sidebar #nav-travel_agency');
    if (travel && travel.closest('[class*="swiper-slide"]')) return travel;
    var candidates = document.querySelectorAll('#sidebar div[id^="nav-"]');
    var last = null;
    for (var i = 0; i < candidates.length; i++) {
      var el = candidates[i];
      if (el.id.indexOf('nav-war_tools') === 0) continue;
      if (!el.closest('[class*="swiper-slide"]')) continue;
      if (!el.querySelector('[class*="area-row"]')) continue;
      last = el;
    }
    return last;
  }

  function injectDesktopSidebar() {
    var refNav = findDesktopNavRef();
    if (!refNav) return false;
    if (document.querySelector('#nav-war_tools')) return true;

    var wrapper = document.createElement('div');
    wrapper.className = refNav.className;
    wrapper.id = 'nav-war_tools';

    var row = document.createElement('div');
    row.className = copyClass(refNav, '[class*="area-row"]');

    var link = document.createElement('a');
    link.href = '#';
    link.className = copyClass(refNav, 'a');
    link.addEventListener('click', function (e) {
      e.preventDefault();
      openWarModal();
    });

    var iconWrap = document.createElement('span');
    iconWrap.className = copyClass(refNav, '[class*="svgIconWrap"]');
    var iconInner = document.createElement('span');
    iconInner.className = copyClass(refNav, '[class*="defaultIcon"]');
    iconInner.innerHTML = warSvg;
    iconWrap.appendChild(iconInner);

    var nameSpan = document.createElement('span');
    nameSpan.className = copyClass(refNav, '[class*="linkName"]');
    nameSpan.textContent = 'War Tools';

    link.appendChild(iconWrap);
    link.appendChild(nameSpan);
    row.appendChild(link);
    wrapper.appendChild(row);

    refNav.parentNode.insertBefore(wrapper, refNav.nextSibling);
    return true;
  }

  function injectMobileSidebar() {
    var refMobile = findMobileNavRef();
    if (!refMobile) return false;
    if (document.querySelector('#nav-war_tools_mobile')) return true;

    var slide = refMobile.closest('[class*="swiper-slide"]');
    if (!slide) return false;

    var newSlide = document.createElement('div');
    newSlide.className = slide.className;
    newSlide.style.cssText = slide.style.cssText;

    var wrapper = document.createElement('div');
    wrapper.className = refMobile.className;
    wrapper.id = 'nav-war_tools_mobile';

    var row = document.createElement('div');
    row.className = copyClass(refMobile, '[class*="area-row"]');

    var link = document.createElement('a');
    link.href = '#';
    link.tabIndex = 0;
    link.className = copyClass(refMobile, 'a');
    link.addEventListener('click', function (e) {
      e.preventDefault();
      openWarModal();
    });

    var iconWrap = document.createElement('span');
    iconWrap.className = copyClass(refMobile, '[class*="svgIconWrap"]');
    var iconInner = document.createElement('span');
    iconInner.className = copyClass(refMobile, '[class*="defaultIcon"]');
    iconInner.innerHTML = warSvg;
    iconWrap.appendChild(iconInner);

    var nameSpan = document.createElement('span');
    nameSpan.textContent = 'War';

    link.appendChild(iconWrap);
    link.appendChild(nameSpan);
    row.appendChild(link);
    wrapper.appendChild(row);
    newSlide.appendChild(wrapper);

    slide.parentNode.insertBefore(newSlide, slide.nextSibling);
    return true;
  }

  function injectSidebar() {
    injectDesktopSidebar();
    injectMobileSidebar();
  }

  // -- Travel Page: Destination Click Interception ----------------------

  function isTravelPage() {
    return window.location.href.indexOf('sid=travel') !== -1 ||
           window.location.href.indexOf('travelagency') !== -1;
  }

  // These use [class^=...] deliberately: Torn's hashed classes sit first in
  // the class attribute on these elements, and [class*=...] over-matches here
  // (e.g. "pin" hits "spinner", "name" hits unrelated elements), returning a
  // wrong country string so no threats matched and no warning showed.

  function getCountryFromDesktopButton(el) {
    var s = el.querySelector('[class^="country"]');
    if (s) return s.textContent.trim();
    var n = el.querySelector('[class^="name"]');
    if (n) {
      var t = n.textContent.trim();
      var d = t.indexOf(' -');
      return d > 0 ? t.substring(0, d).trim() : t;
    }
    return null;
  }

  function getCountryFromMapPin(el) {
    var label = el.getAttribute('aria-label');
    if (label) {
      var d = label.indexOf(' -');
      return d > 0 ? label.substring(0, d).trim() : label.trim();
    }
    var parent = el.closest('[class^="destinationLabel"]');
    if (parent) {
      var radio = parent.querySelector('input[aria-label]');
      if (radio) {
        var rl = radio.getAttribute('aria-label');
        var di = rl.indexOf(' -');
        return di > 0 ? rl.substring(0, di).trim() : rl.trim();
      }
    }
    return null;
  }

  function extractCountryFromClick(e) {
    var target = e.target;

    // Desktop list
    var desktopBtn = target.closest('button[class^="expandButton"]');
    if (desktopBtn) {
      var dest = desktopBtn.closest('[class^="destination"]');
      return getCountryFromDesktopButton(dest || desktopBtn);
    }

    // Map label
    var mapLabel = target.closest('[class^="destinationLabel"]');
    if (mapLabel) {
      var radio = mapLabel.querySelector('input[aria-label]');
      if (radio) return getCountryFromMapPin(radio);
    }

    // Map pin
    var pin = target.closest('[class^="pin"]');
    if (pin) {
      var pl = pin.closest('[class^="destinationLabel"]');
      if (pl) {
        var pr = pl.querySelector('input[aria-label]');
        if (pr) return getCountryFromMapPin(pr);
      }
    }

    return null;
  }

  // Warnings are informational overlays on top of the normal click.
  // Blocking the click with preventDefault left Torn's destination UI in a
  // broken state after dismiss (the expand/travel button vanished), so the
  // click now always passes through and the warning shows over the top.
  // Every destination click re-checks; the travel confirm button inside
  // the expanded destination doesn't match the extraction selectors, so
  // completing a booking isn't interrupted.

  function attachTravelListeners() {
    document.addEventListener('click', function (e) {
      if (!isTravelPage()) return;
      if (!isConfigured()) return;
      if (e.target.closest('.wtw-overlay')) return;

      var country = extractCountryFromClick(e);
      if (!country) return;
      console.debug('[WTW] Destination click:', country);

      // Cache ready: warn immediately
      if (memberCache.data && (Date.now() - memberCache.time) < CACHE_TTL) {
        var threats = findThreats(memberCache.data, country);
        console.debug('[WTW] Threats in', country + ':', threats.length);
        if (threats.length === 0) {
          var descs = [];
          for (var di = 0; di < memberCache.data.length; di++) {
            var dm = memberCache.data[di];
            var ds = dm.status && dm.status.state;
            if (ds === 'Traveling' || ds === 'Abroad') {
              descs.push(dm.status.description);
            }
          }
          console.debug('[WTW] No match. Traveling/Abroad statuses:', descs);
        }
        if (threats.length > 0) {
          showTravelWarning(country, threats);
        }
        return;
      }

      // No cache: fetch in the background, warn when it returns
      getCachedMembers().then(function (members) {
        if (!isTravelPage()) return;
        var threats = findThreats(members, country);
        if (threats.length > 0) {
          showTravelWarning(country, threats);
        }
      }).catch(function (err) {
        console.error('[WTW] Fetch error:', err);
      });
    }, true);
  }

  // -- Init -------------------------------------------------------------

  injectStyles();

  function init() {
    injectSidebar();
    attachTravelListeners();
    prefetchMembers();

    // Re-inject sidebar if SPA removes it
    new MutationObserver(function () {
      if (!document.querySelector('#nav-war_tools')) {
        injectDesktopSidebar();
      }
      if (!document.querySelector('#nav-war_tools_mobile')) {
        injectMobileSidebar();
      }
    }).observe(document.body, { childList: true, subtree: true });

    // Refresh cache periodically
    setInterval(function () {
      if (isConfigured()) prefetchMembers();
    }, CACHE_TTL);
  }

  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', init);
  } else {
    setTimeout(init, 300);
  }

  console.log('[War Tools] v2.15 loaded');
})();