Torn War Tools

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         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');
})();