Torn Job Planner

Job rank calculator, switch planner, and interview answer helper for Torn City

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         Torn Job Planner
// @namespace    iSatomi
// @version      3.8
// @description  Job rank calculator, switch planner, and interview answer helper for Torn City
// @author       iSatomi [3580191]
// @license      MIT
// @match        https://www.torn.com/jobs.php*
// @match        https://www.torn.com/joblist.php*
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_deleteValue
// @grant        GM_setClipboard
// @grant        GM_xmlhttpRequest
// @run-at       document-idle
// ==/UserScript==

(function () {
    'use strict';

    // =========================================================================
    // SHARED CORE — API key, gmFetch, cross-page stat/perk cache
    // =========================================================================


    // ── Shared persistence (key = 'apiKey' for all modules) ──────────────────
    const _load = (k, d) => { const v = GM_getValue(k, null); return v !== null ? v : d; };
    const _save = (k, v) => GM_setValue(k, v);

    // Cross-page cache helpers — data written on one page, read on another
    const cache = {
        get: (k) => { try { const v = GM_getValue('aio_'+k, null); return v ? JSON.parse(v) : null; } catch(_){ return null; }},
        set: (k, v) => GM_setValue('aio_'+k, JSON.stringify(v)),
    };
    // Keys: apiKey, battlestats {str,spd,def,dex}, workstats {man,int,end},
    //       perks_raw (flat array of all perk strings), edu_reduction (number)

    // ── Shared gmFetch ────────────────────────────────────────────────────────
    function gmFetch(url) {
        return new Promise((resolve, reject) => {
            GM_xmlhttpRequest({
                method:"GET", url, timeout:10000,
                onload:    r => { try { resolve(JSON.parse(r.responseText)); } catch(e) { reject(e); }},
                onerror:   () => reject(new Error("Network error")),
                ontimeout: () => reject(new Error("Timeout")),
            });
        });
    }

    // ── Shared CSS base — all three modules use .t-wrap, .t-sec, .t-row etc ──
    // Module-specific styles are injected separately per module.
    document.head.insertAdjacentHTML('beforeend', `<style>
.t-wrap{margin:8px 0 12px;background:#181818;border:1px solid #333;border-radius:6px;font-family:Arial,sans-serif;font-size:14px;color:#ccc;overflow:hidden}
.t-hdr{display:flex;align-items:center;justify-content:space-between;padding:10px 14px;background:linear-gradient(135deg,#222,#1a1a1a);border-bottom:1px solid #2a2a2a;cursor:pointer;user-select:none;-webkit-user-select:none;-webkit-tap-highlight-color:transparent}
.t-hdr:active{background:#2a2a2a}
.t-title{font-size:15px;font-weight:bold;color:#e0e0e0}
.t-tog{font-size:16px;color:#555;transition:transform .2s}
.t-wrap.open .t-tog{transform:rotate(180deg)}
.t-body{display:none;padding:12px}
.t-wrap.open .t-body{display:block}
.t-sec{font-size:10px;font-weight:bold;color:#555;letter-spacing:.08em;text-transform:uppercase;margin:14px 0 6px;padding-bottom:4px;border-bottom:1px solid #252525}
.t-sec:first-child{margin-top:0}
.t-row{display:flex;justify-content:space-between;align-items:baseline;gap:8px;padding:6px 10px;margin-top:3px;border-radius:4px;background:#1e1e1e}
.t-rl{color:#778;font-size:12px;flex-shrink:0}
.t-rv{color:#dde;font-size:12px;text-align:right}
.t-row.g .t-rv{color:#7abf7a}.t-row.b .t-rv{color:#7a9acc}.t-row.r .t-rv{color:#bf7a7a}.t-row.a .t-rv{color:#bf9f5a}
.t-field{margin-bottom:8px}
.t-field label{display:block;font-size:12px;color:#888;margin-bottom:3px}
.t-field select,.t-field input[type=number],.t-field input[type=password],.t-field input[type=text]{width:100%;padding:8px 10px;background:#222;border:1px solid #383838;border-radius:4px;color:#e0e0e0;font-size:14px;box-sizing:border-box;-webkit-appearance:none;appearance:none}
.t-field select:focus,.t-field input:focus{outline:none;border-color:#555;background:#282828}
.t-sg{display:grid;grid-template-columns:repeat(3,1fr);gap:8px}
.t-sg .t-field{margin-bottom:0}
.t-sg .t-field label{font-size:11px}
.t-btns{display:flex;gap:8px;margin-top:12px;flex-wrap:wrap}
.t-btn{flex:1;padding:10px 8px;border-radius:4px;border:1px solid #383838;background:#222;color:#ddd;font-size:13px;font-weight:bold;cursor:pointer;text-align:center;-webkit-tap-highlight-color:transparent;transition:background .15s;min-width:60px}
.t-btn:active{background:#2a2a3a}
.t-btn-g{background:#1a2518;border-color:#3a5030;color:#7abf7a}
.t-btn-b{background:#18182a;border-color:#3a3a60;color:#8a8aee}
.t-btn-c{background:#182028;border-color:#304060;color:#6aaade}
.t-status{display:none;margin-top:8px;padding:8px 10px;border-radius:4px;font-size:12px;line-height:1.5;word-break:break-word}
.t-status.ok{display:block;background:#182018;border:1px solid #2a4a2a;color:#7abf7a}
.t-status.err{display:block;background:#201818;border:1px solid #4a2828;color:#bf7a7a}
.t-status.warn{display:block;background:#1e1a10;border:1px solid #4a3a18;color:#bf9f5a}
.t-coll-hdr{display:flex;align-items:center;justify-content:space-between;padding:8px 10px;cursor:pointer;-webkit-tap-highlight-color:transparent;font-size:11px;font-weight:bold;color:#555;letter-spacing:.06em;text-transform:uppercase}
.t-coll-tog{font-size:12px;color:#444;transition:transform .2s}
.t-coll.open .t-coll-tog{transform:rotate(180deg)}
.t-coll-body{display:none;padding:8px 10px 10px}
.t-coll.open .t-coll-body{display:block}
.t-bar{height:6px;background:#1e1e2a;border-radius:3px;margin-top:8px;overflow:hidden}
.t-bar-fill{height:100%;border-radius:3px;background:linear-gradient(90deg,#3a5aa0,#6a8acc)}
</style>`);



    // ─────────────────────────────────────────────────────────────────────────
    // JOB DATA
    // ─────────────────────────────────────────────────────────────────────────
    const JOBS = {
        army: {
            label:"Army", topRankPerk:"Spy battle stats once/day (10 pts + $5,000)",
            benefits:[],
            ranks:[
                {name:"Private",         req:{man:2,    int:2,    end:2    },gain:{man:3, int:1, end:2 },jpPerDay:1, jpNeeded:5,   perks:[]},
                {name:"Corporal",        req:{man:50,   int:15,   end:20   },gain:{man:5, int:2, end:3 },jpPerDay:2, jpNeeded:10,  perks:[]},
                {name:"Sergeant",        req:{man:120,  int:35,   end:50   },gain:{man:8, int:3, end:5 },jpPerDay:3, jpNeeded:15,  perks:[]},
                {name:"Master Sergeant", req:{man:325,  int:60,   end:115  },gain:{man:12,int:4, end:7 },jpPerDay:4, jpNeeded:20,  perks:[]},
                {name:"Warrant Officer", req:{man:700,  int:160,  end:300  },gain:{man:17,int:7, end:10},jpPerDay:5, jpNeeded:25,  perks:[]},
                {name:"Lieutenant",      req:{man:1300, int:360,  end:595  },gain:{man:20,int:9, end:11},jpPerDay:6, jpNeeded:30,  perks:[]},
                {name:"Major",           req:{man:2550, int:490,  end:900  },gain:{man:24,int:10,end:13},jpPerDay:7, jpNeeded:35,  perks:[]},
                {name:"Colonel",         req:{man:4150, int:600,  end:1100 },gain:{man:28,int:12,end:15},jpPerDay:8, jpNeeded:40,  perks:[]},
                {name:"Brigadier",       req:{man:7500, int:1350, end:2530 },gain:{man:33,int:18,end:15},jpPerDay:9, jpNeeded:45,  perks:[]},
                {name:"General",         req:{man:10000,int:2000, end:4000 },gain:{man:40,int:25,end:20},jpPerDay:10,jpNeeded:null,perks:["⭐ Spy battle stats once/day — active while in Army"]},
            ]
        },
        grocer: {
            label:"Grocer", topRankPerk:"Steal Energy Drink (25 pts — worth ~$3–5M each)",
            benefits:[],
            ranks:[
                {name:"Bag Boy",       req:{man:2,  int:2,  end:2  },gain:{man:2, int:1, end:3 },jpPerDay:1,jpNeeded:5,  perks:[]},
                {name:"Price Labeler",req:{man:30, int:15, end:50 },gain:{man:3, int:2, end:5 },jpPerDay:2,jpNeeded:10, perks:[]},
                {name:"Cashier",       req:{man:50, int:35, end:120},gain:{man:5, int:3, end:8 },jpPerDay:3,jpNeeded:15, perks:[]},
                {name:"Food Delivery", req:{man:120,int:60, end:225},gain:{man:10,int:5, end:15},jpPerDay:4,jpNeeded:20, perks:[]},
                {name:"Manager",       req:{man:250,int:200,end:500},gain:{man:15,int:10,end:20},jpPerDay:5,jpNeeded:null,perks:["⭐ Steal Energy Drink (~$3–5M each, 25 pts) — active while in Grocer"]},
            ]
        },
        casino: {
            label:"Casino", topRankPerk:"Money payout ~$120–160k per use (10 pts + $100k base)",
            benefits:[],
            ranks:[
                {name:"Dealer",           req:{man:2,  int:2,   end:2   },gain:{man:1, int:2, end:3 },jpPerDay:1,jpNeeded:5,  perks:[]},
                {name:"Gaming Consultant",req:{man:35, int:50,  end:120 },gain:{man:2, int:3, end:5 },jpPerDay:2,jpNeeded:10, perks:[]},
                {name:"Marketing Manager",req:{man:60, int:115, end:325 },gain:{man:4, int:7, end:12},jpPerDay:3,jpNeeded:15, perks:[]},
                {name:"Revenue Manager",  req:{man:360,int:595, end:1300},gain:{man:9, int:11,end:20},jpPerDay:4,jpNeeded:20, perks:[]},
                {name:"Casino Manager",   req:{man:490,int:900, end:2550},gain:{man:10,int:13,end:24},jpPerDay:5,jpNeeded:25, perks:[]},
                {name:"Casino President", req:{man:755,int:1100,end:4150},gain:{man:12,int:15,end:28},jpPerDay:6,jpNeeded:null,perks:["⭐ Count Cards ~$120–160k payout — active while in Casino"]},
            ]
        },
        medical: {
            label:"Medical", topRankPerk:"Revive players for 75 energy — permanent passive, earn $500k–$1M+ per revive",
            benefits:[],
            ranks:[
                {name:"Medical Student",req:{man:0,   int:300,  end:0   },gain:{man:4, int:12,end:7 },jpPerDay:1,jpNeeded:5,  perks:[]},
                {name:"Houseman",       req:{man:100, int:600,  end:150 },gain:{man:7, int:17,end:10},jpPerDay:2,jpNeeded:10, perks:[]},
                {name:"Senior Houseman",req:{man:175, int:1000, end:275 },gain:{man:9, int:20,end:11},jpPerDay:3,jpNeeded:15, perks:[]},
                {name:"GP",             req:{man:300, int:1500, end:500 },gain:{man:10,int:24,end:13},jpPerDay:4,jpNeeded:20, perks:[]},
                {name:"Consultant",     req:{man:600, int:2500, end:1000},gain:{man:12,int:28,end:15},jpPerDay:5,jpNeeded:25, perks:[]},
                {name:"Surgeon",        req:{man:1300,int:5000, end:2000},gain:{man:18,int:33,end:15},jpPerDay:6,jpNeeded:30, perks:[]},
                {name:"Brain Surgeon",  req:{man:2600,int:10000,end:4000},gain:{man:20,int:40,end:25},jpPerDay:7,jpNeeded:null,perks:["⭐ Revive players for 75 energy — permanent passive, earns $500k–$1M+ per revive"]},
            ]
        },
        education: {
            label:"Education", topRankPerk:"10% reduction in all education course completion times — permanent passive",
            benefits:[
                {statKey:"man",unlockedAtRank:0},
                {statKey:"end",unlockedAtRank:2},
                {statKey:"int",unlockedAtRank:4},
            ],
            ranks:[
                {name:"Recess Supervisor", req:{man:0,   int:500, end:0   },gain:{man:8, int:10,end:9 },jpPerDay:1,jpNeeded:5,  perks:[]},
                {name:"Substitute Teacher",req:{man:300, int:750, end:500 },gain:{man:13,int:15,end:14},jpPerDay:2,jpNeeded:10, perks:[]},
                {name:"Elementary Teacher",req:{man:600, int:1000,end:700 },gain:{man:15,int:20,end:17},jpPerDay:3,jpNeeded:15, perks:[]},
                {name:"Secondary Teacher", req:{man:1000,int:1300,end:1000},gain:{man:20,int:25,end:20},jpPerDay:4,jpNeeded:20, perks:[]},
                {name:"Professor",         req:{man:1500,int:2000,end:1500},gain:{man:25,int:30,end:25},jpPerDay:5,jpNeeded:25, perks:[]},
                {name:"Vice Principal",    req:{man:1500,int:3000,end:1500},gain:{man:30,int:35,end:30},jpPerDay:6,jpNeeded:30, perks:[]},
                {name:"Principal",         req:{man:1500,int:5000,end:1500},gain:{man:30,int:40,end:30},jpPerDay:7,jpNeeded:null,perks:["⭐ 10% reduction in all education course times — permanent passive"]},
            ]
        },
        law: {
            label:"Law", topRankPerk:"+5% crime experience & skill progression — permanent passive",
            benefits:[],
            ranks:[
                {name:"Law Student",        req:{man:0,   int:0,   end:1500 },gain:{man:15,int:15,end:20},jpPerDay:1,jpNeeded:5,  perks:[]},
                {name:"Paralegal",          req:{man:1750,int:2500,end:5000 },gain:{man:17,int:20,end:23},jpPerDay:2,jpNeeded:10, perks:[]},
                {name:"Probate Lawyer",     req:{man:2500,int:5000,end:7500 },gain:{man:19,int:23,end:30},jpPerDay:3,jpNeeded:15, perks:[]},
                {name:"Trial Lawyer",       req:{man:3500,int:6500,end:7750 },gain:{man:25,int:27,end:35},jpPerDay:4,jpNeeded:20, perks:[]},
                {name:"Circuit Court Judge",req:{man:4000,int:7250,end:10000},gain:{man:27,int:30,end:38},jpPerDay:5,jpNeeded:25, perks:[]},
                {name:"Federal Judge",      req:{man:6000,int:9000,end:15000},gain:{man:30,int:33,end:45},jpPerDay:6,jpNeeded:null,perks:["⭐ +5% crime experience & skill progression — permanent passive"]},
            ]
        }
    };

    // ─────────────────────────────────────────────────────────────────────────
    // JOB DETECTION
    // ─────────────────────────────────────────────────────────────────────────
    const JOB_KEYWORDS = {
        education:"education",educational:"education","education system":"education",
        army:"army",grocer:"grocer",grocery:"grocer","grocery store":"grocer",
        casino:"casino",medical:"medical","medical system":"medical",law:"law",
    };

    function detectJob() {
        const text = sel => document.querySelector(sel)?.textContent.toLowerCase() ?? "";
        const match = t => {
            for (const [k,v] of Object.entries(JOB_KEYWORDS))
                if (k.length >= 3 && t.includes(k)) return v;
            return null;
        };
        const ptWord = text('.points-text').trim().split(/\s+/)[0];
        if (JOB_KEYWORDS[ptWord]) return JOB_KEYWORDS[ptWord];
        const msgT = text('.info-msg-cont:not(.red) .msg');
        const m1 = msgT.match(/\b(\w+)\s+points\b/);
        if (m1 && JOB_KEYWORDS[m1[1]]) return JOB_KEYWORDS[m1[1]];
        const m2 = msgT.match(/work in the\s+([\w ]+?)(?:\s+system|[.,\n]|$)/);
        if (m2) { const k=m2[1].trim(); if (JOB_KEYWORDS[k]) return JOB_KEYWORDS[k]; if (JOB_KEYWORDS[k.split(' ')[0]]) return JOB_KEYWORDS[k.split(' ')[0]]; }
        for (const e of document.querySelectorAll('.msg')) { const r=match(e.textContent.toLowerCase()); if (r) return r; }
        const bm = document.body.innerText.toLowerCase().match(/work in the\s+([\w ]+?)(?:\s+system|\n|\.)/);
        if (bm) { const k=bm[1].trim(); return JOB_KEYWORDS[k]||JOB_KEYWORDS[k.split(' ')[0]]||null; }
        return null;
    }

    // ─────────────────────────────────────────────────────────────────────────
    // PERSISTENCE
    // ─────────────────────────────────────────────────────────────────────────
    const JT_DEFAULTS = {selectedJob:"education",currentRank:"0",currentJP:"5",manStat:"0",intStat:"0",endStat:"0",collapsed:"no"};
    const jt_load = k => _load(k, JT_DEFAULTS[k]);
    const jt_save = (k, v) => _save(k, v);

    // ─────────────────────────────────────────────────────────────────────────
    // CSS
    // ─────────────────────────────────────────────────────────────────────────
    const STYLES = `
.jt-wrap{margin:8px 0 12px;background:#181818;border:1px solid #333;border-radius:6px;font-family:Arial,sans-serif;font-size:14px;color:#ccc;overflow:hidden}
.jt-header{display:flex;align-items:center;justify-content:space-between;padding:10px 14px;background:linear-gradient(135deg,#242424,#1c1c1c);border-bottom:1px solid #2a2a2a;cursor:pointer;user-select:none;-webkit-user-select:none;-webkit-tap-highlight-color:transparent}
.jt-header:hover{background:linear-gradient(135deg,#2c2c2c,#222)}
.jt-title{font-size:15px;font-weight:bold;color:#e0e0e0}
.jt-toggle{font-size:16px;color:#555;transition:transform .2s}
.jt-wrap.open .jt-toggle{transform:rotate(180deg)}
.jt-body{display:none;padding:12px}
.jt-wrap.open .jt-body{display:block}
.jt-sec{font-size:10px;font-weight:bold;color:#555;letter-spacing:.08em;text-transform:uppercase;margin:14px 0 6px;padding-bottom:4px;border-bottom:1px solid #252525}
.jt-sec:first-child{margin-top:0}
.jt-field{margin-bottom:8px}
.jt-field label{display:block;font-size:12px;color:#888;margin-bottom:3px}
.jt-field select,.jt-field input[type=number]{width:100%;padding:8px 10px;background:#222;border:1px solid #383838;border-radius:4px;color:#e0e0e0;font-size:14px;box-sizing:border-box;-webkit-appearance:none;appearance:none}
.jt-field select:focus,.jt-field input:focus{outline:none;border-color:#555;background:#282828}
.jt-sg{display:grid;grid-template-columns:repeat(3,1fr);gap:8px}
.jt-sg .jt-field{margin-bottom:0}
.jt-sg .jt-field label{font-size:11px}
.jt-btn-row{display:flex;gap:8px;margin-top:12px}
.jt-btn{flex:1;padding:10px 8px;border-radius:4px;border:1px solid #383838;background:#222;color:#ddd;font-size:13px;font-weight:bold;cursor:pointer;text-align:center;-webkit-tap-highlight-color:transparent;transition:background .15s}
.jt-btn:hover,.jt-btn:active{background:#2a2a2a}
.jt-btn-fill{background:#1a2518;border-color:#3a5030;color:#7abf7a}
.jt-btn-fill:hover,.jt-btn-fill:active{background:#20301e}
.jt-btn-calc{background:#18182a;border-color:#303058;color:#7a7acc}
.jt-btn-calc:hover,.jt-btn-calc:active{background:#20203a}
.jt-status{display:none;margin-top:8px;padding:8px 10px;border-radius:4px;font-size:12px;line-height:1.5;word-break:break-word}
.jt-status.ok{display:block;background:#182018;border:1px solid #2a4a2a;color:#7abf7a}
.jt-status.err{display:block;background:#201818;border:1px solid #4a2828;color:#bf7a7a}
.jt-status.warn{display:block;background:#201e10;border:1px solid #4a3a18;color:#bf9f5a}
.jt-row{display:flex;justify-content:space-between;align-items:baseline;gap:8px;padding:6px 10px;margin-top:3px;border-radius:4px;background:#1e1e1e}
.jt-row .jt-rl{color:#888;font-size:13px;flex-shrink:0}
.jt-row .jt-rv{color:#ddd;font-size:13px;text-align:right;word-break:break-word}
.jt-row.g{background:#18201a}.jt-row.g .jt-rv{color:#7abf7a}
.jt-row.r{background:#201818}.jt-row.r .jt-rv{color:#bf7a7a}
.jt-row.a{background:#201e10}.jt-row.a .jt-rv{color:#bf9f5a}
.jt-row.b{background:#181828}.jt-row.b .jt-rv{color:#7a7acc}
.jt-perk{margin-top:8px;padding:10px 12px;background:#182018;border:1px solid #2a4a2a;border-radius:4px;font-size:13px;color:#7abf7a;line-height:1.5}
.jt-perk.locked{background:#201818;border-color:#4a2828;color:#bf7a7a}
.jt-rec{display:flex;gap:8px;padding:8px 10px;margin-top:4px;background:#1c1a14;border:1px solid #302810;border-radius:4px;font-size:12px;color:#c8b060;line-height:1.5}
.jt-rec-num{flex-shrink:0;font-weight:bold;color:#a08040;font-size:13px;min-width:16px}
.jt-rec.urgent{background:#201818;border-color:#503020;color:#d08050}
.jt-rec.urgent .jt-rec-num{color:#b06030}
/* Action cards v3.5 */
.jt-action{display:flex;flex-direction:column;gap:3px;padding:10px 12px;margin-top:5px;border-radius:5px;border-left:3px solid transparent}
.jt-action.now{background:#211208;border-color:#cc5500;border:1px solid #5a2a10;border-left:3px solid #cc5500}
.jt-action.soon{background:#1e1a08;border-color:#a07820;border:1px solid #4a3a10;border-left:3px solid #c09030}
.jt-action.info{background:#141420;border-color:#303050;border:1px solid #2a2a40;border-left:3px solid #4a4a70}
.jt-action.good{background:#0e1e10;border-color:#2a5a2a;border:1px solid #2a4a2a;border-left:3px solid #3a8a3a}
.jt-action-badge{font-size:10px;font-weight:bold;letter-spacing:.06em;text-transform:uppercase;margin-bottom:1px}
.jt-action.now  .jt-action-badge{color:#e06020}
.jt-action.soon .jt-action-badge{color:#c0a030}
.jt-action.info .jt-action-badge{color:#6666aa}
.jt-action.good .jt-action-badge{color:#4aaa4a}
.jt-action-head{font-size:13px;font-weight:bold;line-height:1.4}
.jt-action.now  .jt-action-head{color:#f08040}
.jt-action.soon .jt-action-head{color:#d4b050}
.jt-action.info .jt-action-head{color:#8888cc}
.jt-action.good .jt-action-head{color:#6acc6a}
.jt-action-detail{font-size:12px;color:#778;line-height:1.5;margin-top:1px}
.jt-action-when{font-size:11px;font-weight:bold;margin-top:4px;padding:3px 8px;border-radius:3px;display:inline-block;width:fit-content}
.jt-action.now  .jt-action-when{background:#301808;color:#e07030}
.jt-action.soon .jt-action-when{background:#282010;color:#c0a030}
.jt-action.info .jt-action-when{background:#1a1a2a;color:#7878aa}
.jt-action.good .jt-action-when{background:#102010;color:#4aaa4a}
/* Switch planner hero card */
.jt-switch-hero{padding:12px 14px;border-radius:6px;margin-bottom:8px;border:1px solid #2a4a6a;background:linear-gradient(135deg,#0d1a28,#111820)}
.jt-switch-hero.now{border-color:#2a5a2a;background:linear-gradient(135deg,#0d200f,#0f1a12)}
.jt-switch-hero-label{font-size:10px;font-weight:bold;letter-spacing:.08em;text-transform:uppercase;color:#5588aa;margin-bottom:4px}
.jt-switch-hero.now .jt-switch-hero-label{color:#4a8a4a}
.jt-switch-hero-date{font-size:20px;font-weight:bold;color:#aad4ff;line-height:1.2}
.jt-switch-hero.now .jt-switch-hero-date{color:#6acc6a}
.jt-switch-hero-sub{font-size:12px;color:#667;margin-top:3px}
.jt-switch-hero-save{font-size:12px;color:#5aaa5a;margin-top:6px;font-weight:bold}
.pk{display:inline-block;border-radius:3px;padding:1px 5px;font-size:10px;margin-left:3px}
.pk.ok{background:#1a3a1a;color:#7abf7a}
.pk.no{background:#3a1a1a;color:#bf6060}
.jt-summary{display:flex;align-items:center;justify-content:space-between;gap:8px;padding:8px 10px;background:#1e1e1e;border:1px solid #2e2e2e;border-radius:4px;margin-bottom:8px}
.jt-summary-text{font-size:12px;color:#aaa;line-height:1.5;flex:1}
.jt-summary-text strong{color:#ddd}
.jt-edit-btn{flex-shrink:0;padding:4px 10px;border-radius:3px;border:1px solid #383838;background:#252525;color:#888;font-size:11px;cursor:pointer;-webkit-tap-highlight-color:transparent}
.jt-edit-btn:hover,.jt-edit-btn:active{background:#2e2e2e;color:#bbb}
.jt-inputs{display:none}
.jt-inputs.expanded{display:block}
.jt-planner{margin-top:14px;padding:10px 12px;background:#14181e;border:1px solid #2a3040;border-radius:4px}
.jt-planner-header{display:flex;align-items:center;justify-content:space-between;cursor:pointer;-webkit-tap-highlight-color:transparent}
.jt-planner-title{font-size:12px;font-weight:bold;color:#6a8aaa;letter-spacing:.04em;text-transform:uppercase}
.jt-planner-toggle{font-size:13px;color:#445;transition:transform .2s}
.jt-planner.open .jt-planner-toggle{transform:rotate(180deg)}
.jt-planner-body{display:none;margin-top:10px}
.jt-planner.open .jt-planner-body{display:block}
.jt-prow{display:flex;justify-content:space-between;align-items:baseline;gap:8px;padding:5px 8px;margin-top:3px;border-radius:3px;background:#181c24}
.jt-prow .jt-rl{color:#667;font-size:12px;flex-shrink:0}
.jt-prow .jt-rv{color:#aac;font-size:12px;text-align:right}
.jt-prow.pg .jt-rv{color:#7abf7a}
.jt-prow.pr .jt-rv{color:#bf7a7a}
.jt-prow.pa .jt-rv{color:#bf9f5a}
.jt-prow.pb .jt-rv{color:#7a7acc}
.jt-pstat-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:6px;margin-top:6px}
.jt-pstat{background:#181c24;border:1px solid #222a38;border-radius:3px;padding:6px 8px;font-size:12px}
.jt-pstat-label{color:#556;font-size:10px;text-transform:uppercase;letter-spacing:.05em}
.jt-pstat-val{color:#aac;font-size:13px;font-weight:bold;margin-top:1px}
.jt-pstat-val.ok{color:#7abf7a}
.jt-pstat-days{font-size:10px;color:#445;margin-top:1px}
.jt-plan-summary{background:#0e1a28;border:1px solid #2a4a6a;border-radius:5px;padding:12px 14px;margin-top:10px;margin-bottom:4px}
.jt-plan-summary-title{font-size:10px;font-weight:bold;color:#4a7aaa;letter-spacing:.08em;text-transform:uppercase;margin-bottom:8px}
.jt-plan-step{display:flex;gap:10px;padding:6px 0;border-bottom:1px solid #1a2a3a;align-items:flex-start}
.jt-plan-step:last-child{border-bottom:none;padding-bottom:0}
.jt-plan-num{flex-shrink:0;width:20px;height:20px;background:#1a3a5a;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:11px;font-weight:bold;color:#5a9acc;margin-top:1px}
.jt-plan-step-text{font-size:13px;color:#bbd;line-height:1.5}
.jt-plan-step-text strong{color:#ddeeff}
.jt-plan-step-text .stat-pill{display:inline-block;background:#1a2a3a;border:1px solid #2a4a6a;border-radius:3px;padding:1px 6px;font-size:11px;font-weight:bold;color:#88bbdd;margin:0 2px}
.jt-perk-line{padding:2px 8px 5px;font-size:11px;color:#5a7a3a}`;

    // ─────────────────────────────────────────────────────────────────────────
    // HTML
    // ─────────────────────────────────────────────────────────────────────────
    function buildHTML() {
        const opts = (filter=null) => Object.entries(JOBS)
            .filter(([k]) => !filter || k !== filter)
            .map(([k,v]) => `<option value="${k}">${v.label}</option>`).join('');
        return `
<div class="jt-header"><span class="jt-title">📋 Job Planner</span><span class="jt-toggle">▼</span></div>
<div class="jt-body">
<div class="jt-summary" id="jt-summary" style="display:none">
  <span class="jt-summary-text" id="jt-summary-text">—</span>
  <button class="jt-edit-btn" id="jt-edit-btn">✏ Edit</button>
</div>
<div class="jt-inputs expanded" id="jt-inputs">
  <div class="jt-sec">Job &amp; Rank</div>
  <div class="jt-field"><label>Job</label><select id="jt-job">${opts()}</select></div>
  <div class="jt-field"><label>Current Rank</label><select id="jt-rank"></select></div>
  <div class="jt-field"><label>Job Points (accumulated, not spent)</label><input type="number" id="jt-jp" min="0"></div>
  <div class="jt-sec">Working Stats</div>
  <div class="jt-sg">
    <div class="jt-field"><label>Manual</label><input type="number" id="jt-man" min="0"></div>
    <div class="jt-field"><label>Intelligence</label><input type="number" id="jt-int" min="0"></div>
    <div class="jt-field"><label>Endurance</label><input type="number" id="jt-end" min="0"></div>
  </div>
</div>
<div class="jt-btn-row">
  <button class="jt-btn jt-btn-fill" id="jt-autofill">⟳ Auto-fill</button>
  <button class="jt-btn jt-btn-calc" id="jt-calc">Calculate →</button>
</div>
<div class="jt-status" id="jt-status"></div>
<div id="jt-results"></div>
<div class="jt-planner" id="jt-planner">
  <div class="jt-planner-header" id="jt-planner-header">
    <span class="jt-planner-title">🎯 Switch Job Planner</span>
    <span class="jt-planner-toggle">▼</span>
  </div>
  <div class="jt-planner-body">
    <div class="jt-field"><label>Target Job</label><select id="jt-plan-job">${opts("education")}</select></div>
    <div class="jt-field"><label>Target Rank</label><select id="jt-plan-rank"></select></div>
    <button class="jt-btn jt-btn-calc" id="jt-plan-calc" style="margin-top:8px">Calculate Plan →</button>
    <div id="jt-plan-results"></div>
  </div>
</div>
</div>`;
    }

    // ─────────────────────────────────────────────────────────────────────────
    // MOUNT
    // ─────────────────────────────────────────────────────────────────────────
    const styleEl = document.createElement("style");
    styleEl.textContent = STYLES;
    document.head.appendChild(styleEl);

    const wrap = document.createElement("div");
    wrap.className = "jt-wrap";
    wrap.innerHTML = buildHTML();
    const mountTarget = document.querySelector('.content-wrapper') || document.body;
    mountTarget.insertBefore(wrap, mountTarget.firstChild);

    const q = sel => wrap.querySelector(sel);
    const el = {
        header:     q(".jt-header"),   summary:    q("#jt-summary"),
        sumText:    q("#jt-summary-text"), editBtn: q("#jt-edit-btn"),
        inputs:     q("#jt-inputs"),   job:        q("#jt-job"),
        rank:       q("#jt-rank"),     jp:         q("#jt-jp"),
        man:        q("#jt-man"),      int:        q("#jt-int"),
        end:        q("#jt-end"),      calcBtn:    q("#jt-calc"),
        fillBtn:    q("#jt-autofill"), status:     q("#jt-status"),
        results:    q("#jt-results"),  planner:    q("#jt-planner"),
        planHdr:    q("#jt-planner-header"), planJob: q("#jt-plan-job"),
        planRank:   q("#jt-plan-rank"), planCalc:  q("#jt-plan-calc"),
        planOut:    q("#jt-plan-results"),
    };

    // ─────────────────────────────────────────────────────────────────────────
    // HELPERS
    // ─────────────────────────────────────────────────────────────────────────
    const iV      = e => parseInt(e.value) || 0;
    const fmt     = n => Math.round(n).toLocaleString();
    const met     = (req,m,i,e) => m>=req.man && i>=req.int && e>=req.end;
    const dateIn  = n => { const d=new Date(); d.setDate(d.getDate()+n); return d.toLocaleDateString('en-GB',{day:'numeric',month:'short',year:'numeric'}); };
    const row     = (l,v,c="") => `<div class="jt-row ${c}"><span class="jt-rl">${l}</span><span class="jt-rv">${v}</span></div>`;
    const sec     = l => `<div class="jt-sec" style="margin-top:14px">${l}</div>`;
    const recEl   = (n,t,u=false) => `<div class="jt-rec${u?" urgent":""}"><span class="jt-rec-num">${n}</span><span>${t}</span></div>`;
    // actionCard(type, badge, headline, detail, when)
    // type: 'now' | 'soon' | 'info' | 'good'
    const actionCard = (type, badge, head, detail='', when='') =>
        `<div class="jt-action ${type}">` +
        (badge ? `<div class="jt-action-badge">${badge}</div>` : '') +
        `<div class="jt-action-head">${head}</div>` +
        (detail ? `<div class="jt-action-detail">${detail}</div>` : '') +
        (when   ? `<div class="jt-action-when">${when}</div>` : '') +
        `</div>`;
    const pill    = (h,n) => h>=n ? `<span class="pk ok">${fmt(h)}</span>` : `${fmt(h)}<span class="pk no">-${fmt(n-h)}</span>`;
    const rq      = v => v > 0 ? fmt(v) : "—";
    const prow    = (l,v,c="") => `<div class="jt-prow ${c}"><span class="jt-rl">${l}</span><span class="jt-rv">${v}</span></div>`;
    const psec    = l => `<div class="jt-sec" style="margin-top:10px;font-size:10px">${l}</div>`;

    // ─────────────────────────────────────────────────────────────────────────
    // RANK DROPDOWNS
    // ─────────────────────────────────────────────────────────────────────────
    function populateRanks(jobKey, idx) {
        el.rank.innerHTML = '';
        JOBS[jobKey].ranks.forEach((r,i) => {
            const o = document.createElement("option");
            o.value = i; o.textContent = `${i+1}. ${r.name}`;
            el.rank.appendChild(o);
        });
        el.rank.value = String(idx ?? 0);
    }

    function populatePlanRanks(jobKey) {
        el.planRank.innerHTML = '';
        JOBS[jobKey].ranks.forEach((r,i) => {
            const o = document.createElement("option");
            o.value = i; o.textContent = `${i+1}. ${r.name}`;
            el.planRank.appendChild(o);
        });
        el.planRank.value = String(JOBS[jobKey].ranks.length - 1);
    }

    // ─────────────────────────────────────────────────────────────────────────
    // SUMMARY BAR & PLANNER VISIBILITY
    // ─────────────────────────────────────────────────────────────────────────
    function showSummary(jobKey, ri, jp, man, int_, end) {
        const rank = JOBS[jobKey].ranks[ri];
        el.sumText.innerHTML = `<strong>${JOBS[jobKey].label}</strong> · ${rank.name} · <strong>${jp} JP</strong><br>MAN <strong>${fmt(man)}</strong> · INT <strong>${fmt(int_)}</strong> · END <strong>${fmt(end)}</strong>`;
        el.summary.style.display = "";
        el.inputs.classList.remove("expanded");
    }
    const hideSummary = () => { el.summary.style.display="none"; el.inputs.classList.add("expanded"); };
    const updatePlanner = () => { el.planner.style.display = el.job.value==="education" ? "" : "none"; };

    // ─────────────────────────────────────────────────────────────────────────
    // RESTORE & WIRE
    // ─────────────────────────────────────────────────────────────────────────
    el.job.value = jt_load("selectedJob");
    populateRanks(el.job.value, jt_load("currentRank"));
    el.jp.value  = jt_load("currentJP");
    el.man.value = jt_load("manStat");
    el.int.value = jt_load("intStat");
    el.end.value = jt_load("endStat");
    populatePlanRanks(el.planJob.value || "army");
    updatePlanner();
    if (jt_load("collapsed") !== "yes") wrap.classList.add("open");

    el.editBtn.addEventListener("click", hideSummary);
    el.header.addEventListener("click", () => { const o=wrap.classList.toggle("open"); jt_save("collapsed", o?"no":"yes"); });
    el.job.addEventListener("change", () => {
        jt_save("selectedJob", el.job.value);
        populateRanks(el.job.value, 0); jt_save("currentRank","0");
        updatePlanner(); hideSummary();
    });
    el.rank.addEventListener("change", () => jt_save("currentRank", el.rank.value));
    el.jp.addEventListener("input",  () => jt_save("currentJP",  el.jp.value));
    el.man.addEventListener("input", () => jt_save("manStat",    el.man.value));
    el.int.addEventListener("input", () => jt_save("intStat",    el.int.value));
    el.end.addEventListener("input", () => jt_save("endStat",    el.end.value));
    el.planJob.addEventListener("change", () => populatePlanRanks(el.planJob.value));
    el.planHdr.addEventListener("click", () => el.planner.classList.toggle("open"));
    el.fillBtn.addEventListener("click", autofill);
    el.calcBtn.addEventListener("click", calculate);
    el.planCalc.addEventListener("click", calculatePlan);

    // ─────────────────────────────────────────────────────────────────────────
    // AUTO-FILL
    // ─────────────────────────────────────────────────────────────────────────
    function autofill() {
        const num  = s => s ? parseInt(s.replace(/,/g,''),10)||0 : null;
        const text = sel => { const e=document.querySelector(sel); return e ? e.textContent.trim() : null; };
        const errs=[], filled=[];

        const job = detectJob();
        if (job) { el.job.value=job; jt_save("selectedJob",job); filled.push(JOBS[job].label); }
        else errs.push("Job not detected");

        let ri = 0;
        const rankName = text('.jrank');
        if (rankName && job) {
            // Normalize: collapse hyphens/dashes to spaces for fuzzy matching
            const norm = s => s.toLowerCase().replace(/[-–—]/g,' ').replace(/\s+/g,' ').replace(/ll(er|or)\b/g,'l$1').trim();
            ri = JOBS[job].ranks.findIndex(r => norm(r.name) === norm(rankName));
            if (ri >= 0) { populateRanks(job,ri); jt_save("currentRank",String(ri)); filled.push(rankName); }
            else { errs.push(`Rank "${rankName}" not recognised`); ri=0; populateRanks(job,0); }
        } else if (!rankName) { errs.push("Rank not found"); if (job) populateRanks(job,0); }

        const jpRaw = text('.jpoints');
        if (jpRaw !== null) { const v=num(jpRaw); el.jp.value=v; jt_save("currentJP",String(v)); filled.push(`${v} JP`); }
        else errs.push("JP not found");

        [['.jmanLabor',el.man,"manStat","MAN"],['.jintelligence',el.int,"intStat","INT"],['.jendurance',el.end,"endStat","END"]]
        .forEach(([sel,inp,key,lbl]) => {
            const raw=text(sel);
            if (raw!==null) { const v=num(raw); inp.value=v; jt_save(key,String(v)); filled.push(`${lbl} ${fmt(v)}`); }
            else errs.push(`${lbl} not found`);
        });

        updatePlanner();
        if (!errs.length) {
            cache.set('workstats', {
                man: parseInt(jt_load('manStat')) || 0,
                int: parseInt(jt_load('intStat')) || 0,
                end: parseInt(jt_load('endStat')) || 0,
            });
            el.status.className = "jt-status ok";
            el.status.textContent = "✓ " + filled.join(" · ");
            showSummary(el.job.value, parseInt(el.rank.value)||0, iV(el.jp), iV(el.man), iV(el.int), iV(el.end));
        } else if (filled.length) {
            el.status.className="jt-status warn"; el.status.textContent="⚠ Partial: "+filled.join(", ")+" — "+errs.join("; ");
        } else {
            el.status.className="jt-status err"; el.status.textContent="✗ "+errs.join(" | ");
        }
    }

    // ─────────────────────────────────────────────────────────────────────────
    // SIMULATION  — Education auto-uses all unlocked JP specials each rank,
    // spending surplus on the biggest bottleneck for the next rank.
    // ─────────────────────────────────────────────────────────────────────────
    function simulate(jobKey, startRi, startJP, startMan, startInt, startEnd) {
        const ranks=JOBS[jobKey].ranks, isEdu=jobKey==="education";
        let man=startMan, int_=startInt, end=startEnd, jp=startJP, ri=startRi;
        const results=[{rankIdx:ri, rankName:ranks[ri].name, dayReached:0}];

        for (let day=1; day<=3650; day++) {
            const r=ranks[ri];
            jp+=r.jpPerDay; man+=r.gain.man; int_+=r.gain.int; end+=r.gain.end;

            if (isEdu) {
                const boostable=new Set(JOBS.education.benefits.filter(b=>ri>=b.unlockedAtRank).map(b=>b.statKey));
                if (boostable.size) {
                    const nReq=r.jpNeeded!==null?ranks[ri+1].req:{man:0,int:0,end:0};
                    let surplus=Math.max(0,jp-(r.jpNeeded??0));
                    while (surplus>=10) {
                        let best=null, bs=-1;
                        for (const sk of boostable) {
                            const have=sk==="man"?man:sk==="int"?int_:end, need=sk==="man"?nReq.man:sk==="int"?nReq.int:nReq.end;
                            const gain=sk==="man"?r.gain.man:sk==="int"?r.gain.int:r.gain.end;
                            const score=gain>0?Math.max(0,need-have)/gain:(need>have?9999:0);
                            if (score>bs){bs=score;best=sk;}
                        }
                        if (!best||bs===0) break;
                        jp-=10; surplus-=10;
                        if (best==="man") man+=100; else if (best==="int") int_+=100; else end+=100;
                    }
                }
            }

            if (r.jpNeeded!==null && jp>=r.jpNeeded) {
                const next=ranks[ri+1];
                if (met(next.req,man,int_,end)) {
                    jp-=r.jpNeeded; ri++;
                    results.push({rankIdx:ri, rankName:ranks[ri].name, dayReached:day});
                    if (ri===ranks.length-1) break;
                }
            }
        }
        return results;
    }

    // ─────────────────────────────────────────────────────────────────────────
    // CALCULATE
    // ─────────────────────────────────────────────────────────────────────────
    function calculate() {
        const jobKey=el.job.value, ri=parseInt(el.rank.value)||0;
        const jp=iV(el.jp), man=iV(el.man), int_=iV(el.int), end=iV(el.end);
        const job=JOBS[jobKey], ranks=job.ranks, cur=ranks[ri], total=ranks.length;
        const sim=simulate(jobKey,ri,jp,man,int_,end);
        let html="";

        // ── Stats vs next rank ────────────────────────────────────────────────
        if (ri<total-1) {
            const nx=ranks[ri+1];
            const jpNote=cur.jpNeeded!==null
                ?` <span style="font-size:10px;color:#556;font-weight:normal">· ${fmt(jp)} / ${cur.jpNeeded} JP</span>`
                :'';
            html+=sec(`Stats vs ${nx.name}${jpNote}`);
            html+=row("Manual",      pill(man,nx.req.man)+` / ${rq(nx.req.man)}`);
            html+=row("Intelligence",pill(int_,nx.req.int)+` / ${rq(nx.req.int)}`);
            html+=row("Endurance",   pill(end,nx.req.end)+` / ${rq(nx.req.end)}`);
        } else {
            html+=sec(`${cur.name} — Top Rank`);
            html+=`<div class="jt-perk">${job.topRankPerk}</div>`;
        }

        // ── Timeline ──────────────────────────────────────────────────────────
        if (sim.length>1) {
            html+=sec("Timeline");
            sim.forEach((r,i)=>{
                if(!i)return;
                html+=row(`→ ${r.rankName}`,r.dayReached===0?"✓ Now":`Day ${r.dayReached} (${dateIn(r.dayReached)})`,r.rankIdx===total-1?"g":"");
            });
        }

        // ── What To Do ────────────────────────────────────────────────────────
        html+=sec("What To Do");
        html+=buildRecs(jobKey,ri,jp,man,int_,end,sim).join('');
        el.results.innerHTML=html;
    }


    // ─────────────────────────────────────────────────────────────────────────
    // WHAT TO DO — action cards (v3.8)
    // Returns an array of HTML strings, ordered by priority.
    //
    // FIX (v3.8): Previous versions calculated stat days using only passive
    // daily gains (gap / gain_per_day), completely ignoring Education JP
    // boosts (+100 stat per 10 JP). This caused the "What To Do" cards to
    // show wildly different (higher) day counts than the Timeline, which
    // correctly used simulate(). Now we pull the next-rank promotion day
    // directly from the simulation results so both sections always agree.
    // ─────────────────────────────────────────────────────────────────────────
    function buildRecs(jobKey,ri,jp,man,int_,end,sim) {
        const job=JOBS[jobKey], ranks=job.ranks, cur=ranks[ri], total=ranks.length;
        const cards=[];

        // ── Top rank ──────────────────────────────────────────────────────────
        if (ri===total-1) {
            cards.push(actionCard('good','✓ Done',
                job.topRankPerk,
                jobKey==='medical'?'Revive passive is permanent — you can leave now if you want.'
                :jobKey==='law'?'Crime exp passive is permanent — consider switching for better stat gains.'
                :''));
            return cards;
        }

        const nx=ranks[ri+1];

        // ── Use simulation to get the ACTUAL promotion day ────────────────────
        // This accounts for JP boosts, passive gains, and JP accumulation.
        const simNext = sim.find(r => r.rankIdx === ri + 1);
        const promotionDay = simNext ? simNext.dayReached : null;

        // Current stat gaps (for display purposes only — days come from sim)
        const gaps={man:Math.max(0,nx.req.man-man),int:Math.max(0,nx.req.int-int_),end:Math.max(0,nx.req.end-end)};
        const hasStatGap = gaps.man > 0 || gaps.int > 0 || gaps.end > 0;

        // JP info for display
        const jpLeft=Math.max(0,(cur.jpNeeded??0)-jp);

        // ── CARD 1: What are you waiting on, and when ─────────────────────────
        if (promotionDay === 0) {
            // Can promote right now
            cards.push(actionCard('now','⚡ Promote Now',
                `All requirements met for ${nx.name}`,
                '',
                '→ Go to the Jobs page and promote'));
        } else if (promotionDay !== null) {
            // Build detail lines showing what's currently short
            const lines = [];
            if (gaps.man > 0) {
                const passiveDays = Math.ceil(gaps.man / cur.gain.man);
                lines.push(`MAN: need ${fmt(gaps.man)} more (+${cur.gain.man}/day passive${jobKey==='education'?' + JP boosts':''})`);
            }
            if (gaps.int > 0) {
                const passiveDays = Math.ceil(gaps.int / cur.gain.int);
                lines.push(`INT: need ${fmt(gaps.int)} more (+${cur.gain.int}/day passive${jobKey==='education'?' + JP boosts':''})`);
            }
            if (gaps.end > 0) {
                const passiveDays = Math.ceil(gaps.end / cur.gain.end);
                lines.push(`END: need ${fmt(gaps.end)} more (+${cur.gain.end}/day passive${jobKey==='education'?' + JP boosts':''})`);
            }
            if (jpLeft > 0) {
                lines.push(`JP: need ${fmt(jpLeft)} more at ${cur.jpPerDay}/day`);
            }

            // Determine primary blocker for badge text
            const badge = (hasStatGap && jpLeft <= 0) ? '⏳ Waiting on Stats'
                        : (!hasStatGap && jpLeft > 0)  ? '⏳ Waiting on JP'
                        : '⏳ Waiting on Stats + JP';

            cards.push(actionCard('now', badge,
                `Promote to ${nx.name} on ~${dateIn(promotionDay)}`,
                lines.join('<br>'),
                `in ${promotionDay}d · ${dateIn(promotionDay)}`));
        } else {
            // Simulation didn't reach next rank (shouldn't happen within 10 years, but handle gracefully)
            cards.push(actionCard('info','⏳ Long Wait',
                `${nx.name} is far away`,
                'Stat and JP requirements are very high for your current gains.', ''));
        }

        // ── CARD 2: One secondary insight (top rank date, or boost action) ────
        let secondaryDone = false;

        // Education boost — only show if there's something concrete to do
        if (jobKey==='education' && !secondaryDone) {
            const boostable=JOBS.education.benefits.filter(b=>ri>=b.unlockedAtRank);
            if (boostable.length) {
                const statLabel={man:'MAN',int:'INT',end:'END'};
                const surplusNow=Math.max(0,jp-(cur.jpNeeded??0));
                const boostsNow=Math.floor(surplusNow/10);

                // Which stats need boosts?
                const needBoost=boostable
                    .map(b=>b.statKey).filter((s,i,a)=>a.indexOf(s)===i)
                    .map(sk=>{
                        const gap=sk==='man'?gaps.man:sk==='int'?gaps.int:gaps.end;
                        if (!gap) return null;
                        return {sk,gap,label:statLabel[sk],boostsNeeded:Math.ceil(gap/100)};
                    }).filter(Boolean);

                if (boostsNow>0 && needBoost.length>0) {
                    // Have boosts AND they're needed
                    const target=needBoost.sort((a,b)=>b.boostsNeeded-a.boostsNeeded)[0];
                    cards.push(actionCard('soon','📈 Spend JP Boosts',
                        `${boostsNow} boost${boostsNow!==1?'s':''} ready · put into ${target.label}`,
                        needBoost.map(x=>`${x.label}: ${fmt(x.gap)} gap · ${x.boostsNeeded} boost${x.boostsNeeded!==1?'s':''} needed`).join('<br>'),
                        '→ Education → Job Specials'));
                    secondaryDone=true;
                } else if (boostsNow>0) {
                    // Have boosts but not urgently needed — suggest forward-looking spend
                    const nextNextRank=ranks[Math.min(ri+2,total-1)];
                    let bestStat=null,bestScore=-1;
                    for (const b of boostable) {
                        const sk=b.statKey;
                        const have=sk==='man'?man:sk==='int'?int_:end;
                        const need=sk==='man'?nextNextRank.req.man:sk==='int'?nextNextRank.req.int:nextNextRank.req.end;
                        const gain=sk==='man'?cur.gain.man:sk==='int'?cur.gain.int:cur.gain.end;
                        const score=gain>0?Math.max(0,need-have)/gain:0;
                        if(score>bestScore){bestScore=score;bestStat=sk;}
                    }
                    if (bestStat && bestScore>0) {
                        cards.push(actionCard('info','📈 JP Boosts',
                            `${boostsNow} boost${boostsNow!==1?'s':''} available · spend on ${statLabel[bestStat]}`,
                            `Passive gains + boosts already cover ${nx.name} stats — put surplus into ${statLabel[bestStat]} for ${nextNextRank.name}`,
                            '→ Education → Job Specials'));
                        secondaryDone=true;
                    }
                } else if (needBoost.length>0) {
                    // Boosts needed but not yet affordable
                    const daysToFirstBoost=Math.ceil(Math.max(0,(cur.jpNeeded??0)+10-jp)/cur.jpPerDay);
                    cards.push(actionCard('info','📈 JP Boosts Needed',
                        `Spend on ${needBoost.map(x=>x.label).join(' & ')} before promoting`,
                        needBoost.map(x=>`${x.label}: ${fmt(x.gap)} gap · ${x.boostsNeeded} boost${x.boostsNeeded!==1?'s':''} needed`).join('<br>'),
                        `First boost available in ~${daysToFirstBoost}d`));
                    secondaryDone=true;
                }
                // If passive + boosts cover everything and no boosts available — no card needed
            }
        }

        // Top rank ETA — simple, correct
        if (!secondaryDone && ri<total-1) {
            const topR=sim.find(r=>r.rankIdx===total-1);
            if (topR?.dayReached>0) {
                cards.push(actionCard('info','🏁 Top Rank',
                    `${ranks[total-1].name} on ${dateIn(topR.dayReached)}`,
                    job.topRankPerk,
                    `in ~${topR.dayReached} days`));
            }
        }

        return cards;
    }


    // ─────────────────────────────────────────────────────────────────────────
    // CALCULATE PLAN
    // Tests every subset of unlocked Education JP specials and picks the
    // combination that minimises total days (Education + target job).
    // ─────────────────────────────────────────────────────────────────────────

    function calculatePlan() {
        if (el.job.value !== 'education') return; // planner only valid for Education job
        const targetJobKey = el.planJob?.value;
        if (!targetJobKey || !JOBS[targetJobKey]) return;

        const jp0  = iV(el.jp),  man0 = iV(el.man);
        const int0 = iV(el.int), end0 = iV(el.end);
        if (!man0 && !int0 && !end0) {
            if (el.planOut) el.planOut.innerHTML = '<div style="font-size:12px;color:#557;padding:8px 0">Fill in your current stats first.</div>';
            return;
        }

        const eduRi0  = iV(el.rank);
        const eduJobs = JOBS.education;
        const eduRanks= eduJobs.ranks;
        const tJob    = JOBS[targetJobKey];
        const tRanks  = tJob.ranks;
        const tTotal  = tRanks.length;
        // Target = top rank requirements of the target job
        const topReq  = tRanks[tTotal - 1].req;

        // Helper: days to reach top of target job from given stats/JP
        function daysToTop(man, int_, end, jp) {
            const sim = simulate(targetJobKey, 0, jp, man, int_, end);
            const top = sim.find(r => r.rankIdx === tTotal - 1);
            return top ? top.dayReached : null;
        }

        // Option A: switch now
        const daysNow  = daysToTop(man0, int0, end0, jp0);
        const totalNow = daysNow;

        // Option B: stay in Education, boost toward target job's top rank requirements
        // Run our own sim that spends JP on whichever stat has the biggest gap to topReq
        let eduMan = man0, eduInt = int0, eduEnd = end0, eduJP = jp0;
        let eduRi  = eduRi0;
        let best   = { totalDays: totalNow ?? 99999, switchDay: 0,
                       man: man0, int: int0, end: end0, jp: jp0,
                       eduRankAtSwitch: eduRi0, targetDays: daysNow ?? 99999 };

        for (let day = 1; day <= 1095; day++) {
            const r = eduRanks[Math.min(eduRi, eduRanks.length - 1)];
            eduJP  += r.jpPerDay;
            eduMan += r.gain.man;
            eduInt += r.gain.int;
            eduEnd += r.gain.end;

            // Spend JP boosting toward the TARGET JOB'S top rank requirements
            const bSet = new Set(eduJobs.benefits
                .filter(b => eduRi >= b.unlockedAtRank)
                .map(b => b.statKey));

            if (bSet.size) {
                // Use jpNeeded for edu rank-up reserve (if not at top rank)
                const jpReserve = (r.jpNeeded !== null) ? r.jpNeeded : 0;
                let surplus = Math.max(0, eduJP - jpReserve);

                while (surplus >= 10) {
                    // Boost toward TARGET JOB top rank requirements
                    let bestStat = null, bestScore = -1;
                    for (const sk of bSet) {
                        const have  = sk==="man"?eduMan : sk==="int"?eduInt : eduEnd;
                        const need  = sk==="man"?topReq.man : sk==="int"?topReq.int : topReq.end;
                        const gain  = sk==="man"?r.gain.man : sk==="int"?r.gain.int : r.gain.end;
                        const gap   = Math.max(0, need - have);
                        const score = gain > 0 ? gap / gain : (gap > 0 ? 9999 : 0);
                        if (score > bestScore) { bestScore = score; bestStat = sk; }
                    }
                    if (!bestStat || bestScore === 0) break;
                    eduJP -= 10; surplus -= 10;
                    if (bestStat==="man") eduMan += 100;
                    else if (bestStat==="int") eduInt += 100;
                    else eduEnd += 100;
                }
            }

            // Education rank-up check
            if (eduRi < eduRanks.length - 1 && r.jpNeeded !== null && eduJP >= r.jpNeeded) {
                const next = eduRanks[eduRi + 1];
                if (met(next.req, eduMan, eduInt, eduEnd)) {
                    eduJP -= r.jpNeeded;
                    eduRi++;
                }
            }

            // How long to top rank if we switch today?
            const targetDays = daysToTop(eduMan, eduInt, eduEnd, 0);
            if (targetDays === null) continue;

            const total = day + targetDays;
            if (total < best.totalDays) {
                best = { totalDays: total, switchDay: day,
                         man: Math.round(eduMan), int: Math.round(eduInt),
                         end: Math.round(eduEnd), jp: Math.round(eduJP),
                         eduRankAtSwitch: eduRi, targetDays };
            }

            // Early exit: once we've clearly passed the optimal point
            // (total has been rising for 30+ days), stop
            if (day > best.switchDay + 60) break;
        }

        // ── Build output ──────────────────────────────────────────────────────
        let html = '';
        const saving     = best.switchDay > 0 ? ((totalNow ?? best.totalDays + 1) - best.totalDays) : 0;
        const topRankName = tRanks[tTotal-1].name;
        const finalSim   = simulate(targetJobKey, 0, 0, best.man, best.int, best.end);
        const topRankDay = best.switchDay + (finalSim.find(r=>r.rankIdx===tTotal-1)?.dayReached ?? 0);

        // ── 1: Hero — when to switch and the end date ─────────────────────────
        if (best.switchDay === 0 && totalNow !== null) {
            html += `<div class="jt-switch-hero now">
                <div class="jt-switch-hero-label">⚡ Switch Now</div>
                <div class="jt-switch-hero-date">${tJob.label}</div>
                <div class="jt-switch-hero-sub">${topRankName} by <strong style="color:#8de88d">${dateIn(totalNow)}</strong> (~${totalNow}d)</div>
                <div class="jt-switch-hero-save">${tJob.topRankPerk}</div>
            </div>`;
        } else if (best.switchDay > 0) {
            html += `<div class="jt-switch-hero">
                <div class="jt-switch-hero-label">📅 Switch on</div>
                <div class="jt-switch-hero-date">${dateIn(best.switchDay)}</div>
                <div class="jt-switch-hero-sub">in ${best.switchDay}d · then switch to ${tJob.label}${saving>0?` · saves ~${saving}d`:''}</div>
                <div class="jt-switch-hero-save">${topRankName} by <strong style="color:#8de88d">${dateIn(topRankDay)}</strong> (~${topRankDay}d)</div>
            </div>`;
        }

        // ── 2: JP action — what to spend JP on while waiting ─────────────────
        if (best.switchDay > 0) {
            const eduRi0     = iV(el.rank);
            const curEduRank = JOBS.education.ranks[Math.min(eduRi0, JOBS.education.ranks.length-1)];
            const boostable  = JOBS.education.benefits.filter(b=>eduRi0>=b.unlockedAtRank);
            const slabel     = {man:'MAN',int:'INT',end:'END'};

            if (boostable.length) {
                const surplusNow = Math.max(0, jp0 - (curEduRank.jpNeeded??0));
                const boostsNow  = Math.floor(surplusNow / 10);

                // Pick the stat with the biggest time-weighted gap to target top req
                let bestStat=null, bestScore=-1;
                for (const b of boostable) {
                    const sk   = b.statKey;
                    const have = sk==='man'?man0:sk==='int'?int0:end0;
                    const need = sk==='man'?topReq.man:sk==='int'?topReq.int:topReq.end;
                    const gain = sk==='man'?curEduRank.gain.man:sk==='int'?curEduRank.gain.int:curEduRank.gain.end;
                    const gap  = Math.max(0, need - have);
                    const score = gain>0 ? gap/gain : (gap>0?9999:0);
                    if (score>bestScore){bestScore=score;bestStat=sk;}
                }

                if (bestStat && bestScore > 0) {
                    const have         = bestStat==='man'?man0:bestStat==='int'?int0:end0;
                    const need         = bestStat==='man'?topReq.man:bestStat==='int'?topReq.int:topReq.end;
                    const gap          = Math.max(0, need - have);
                    const boostsNeeded = Math.ceil(gap/100);
                    const availableStr = boostsNow >= boostsNeeded
                        ? `${boostsNow} boost${boostsNow!==1?'s':''} ready now — use them`
                        : boostsNow > 0
                            ? `Use ${boostsNow} now · ${boostsNeeded-boostsNow} more by Day ${best.switchDay}`
                            : `~${boostsNeeded} boost${boostsNeeded!==1?'s':''} will accumulate by Day ${best.switchDay}`;
                    html += actionCard('soon', '📈 JP — spend on',
                        `${slabel[bestStat]} (10 JP → +100 ${slabel[bestStat]})`,
                        `${fmt(gap)} gap to ${topRankName} · needs ${boostsNeeded} boost${boostsNeeded!==1?'s':''}`,
                        availableStr);
                } else {
                    html += actionCard('info', '✓ JP',
                        `Save JP for Education promotions`,
                        `${topRankName} stat requirements already met — no boosts needed`, '');
                }
            } else {
                html += actionCard('info','JP','Save all JP for Education promotions',
                    'No boost specials unlocked yet', '');
            }
        }

        // ── 3: Timeline — condensed, just rank names + absolute dates ────────
        if (finalSim.length > 1) {
            const rows = finalSim.slice(1).map(r => {
                const absDay = best.switchDay + r.dayReached;
                const isTop  = r.rankIdx === tTotal - 1;
                const name   = isTop ? `<strong style="color:#6acc6a">🏁 ${r.rankName}</strong>` : r.rankName;
                return `<span style="color:${isTop?'#6acc6a':'#667'}">${name} — Day ${absDay} · ${dateIn(absDay)}</span>`;
            });
            html += actionCard('info','After switching', tJob.label+' ranks', rows.join('<br>'), '');
        }

        if (el.planOut) el.planOut.innerHTML = html;
    }


    function tryInit() {
        if(document.querySelector('.jrank')&&document.querySelector('.jmanLabor')){
            autofill(); calculate();
            wrap.classList.add("open"); jt_save("collapsed","no");
            updatePlanner(); return true;
        }
        return false;
    }
    // Read battle stats from cross-page cache (written by gym module)
    (function() {
        const ws = cache.get('workstats');
        if (ws) {
            if (ws.man > 0 && !parseInt(jt_load('manStat'))) { jt_save('manStat', String(ws.man)); }
            if (ws.int > 0 && !parseInt(jt_load('intStat'))) { jt_save('intStat', String(ws.int)); }
            if (ws.end > 0 && !parseInt(jt_load('endStat'))) { jt_save('endStat', String(ws.end)); }
        }
    })();

    if(!tryInit()){
        const obs=new MutationObserver(()=>{if(tryInit())obs.disconnect();});
        obs.observe(document.body,{childList:true,subtree:true});
        setTimeout(()=>obs.disconnect(),10000);
    }


    // =========================================================================
    // INTERVIEW ANSWER HELPER  (v3.3)
    // Based on real DOM analysis of joblist.php.
    //
    // DOM structure (confirmed from live page capture):
    //
    //   URL: joblist.php#!p=interview&job=<jobname>&rv=...&q=<num>
    //   job=  : army | grocer | casino | medical | education | law
    //   q=999 : this is a trivia question screen (the ones we care about)
    //   q=1/2 : intro dialogue screens (no trivia — we skip these)
    //
    //   The QUESTION is in the last:
    //     div.interviewer-wrap.dialog.speech-right > .speech-wrap
    //     (strip the <span class="author">Name</span>: prefix and curly quotes)
    //
    //   The ANSWER OPTIONS are all:
    //     div.interviewer-wrap.answer > .speech-wrap > .link.t-blue-cont > a
    //     (there are always 3 options, one per .interviewer-wrap.answer div)
    //
    //   We highlight the correct <a> element with green and dim the others,
    //   AND show a floating panel listing Q→A so the answer is always visible.
    // =========================================================================

    const INTERVIEW_QA = {
        army: [
            { q: "brain bucket",                                    a: "A helmet",                                          ap: "helmet" },
            { q: "when did world war 2 start",                      a: "1939",                                              ap: "^1939$|\\b1939\\b" },
            { q: "this is my rifle",                                a: "Fighting, Fun",                                     ap: "fighting" },
            { q: "jarhead",                                         a: "United States Marine",                              ap: "united states marine" },
            { q: "usmc.*stand for",                                 a: "United States Marine Corps",                        ap: "marine corps" },
            { q: "higher rank in the army",                         a: "General",                                           ap: "^general$|\\bgeneral\\b" },
            { q: "acronym.*rpg|rpg.*stand",                         a: "Rocket propelled grenade",                          ap: "rocket propelled grenade" },
            { q: "not a rank in the us army",                       a: "Admiral",                                           ap: "admiral" },
            { q: "never been at war with",                          a: "France",                                            ap: "^france$|\\bfrance\\b" },
        ],
        grocer: [
            { q: "does not grow on.*tree",                          a: "Strawberries",                                      ap: "strawberr" },
            { q: "not a stone fruit",                               a: "Pear",                                              ap: "^pear$|\\bpear\\b" },
            { q: "cannot find what they.*re looking",               a: "Leave their name and number",                       ap: "name and number" },
            { q: "cannot be rhymed",                                a: "Orange",                                            ap: "^orange$|\\borange\\b" },
            { q: "when is the customer right",                      a: "The customer is always right",                      ap: "always right" },
            { q: "stolen some produce",                             a: "Call the police",                                   ap: "call the police" },
            { q: "hands you the money",                             a: "Place money in the till and give change",           ap: "till|change" },
            { q: "produce is starting to go.*rotten",               a: "Immediately dispose of the rotting produce",        ap: "dispos" },
            { q: "mistaken as a vegetable",                         a: "Tomato",                                            ap: "^tomato$|\\btomato\\b" },
        ],
        casino: [
            { q: "common currency used in casinos",                 a: "Chips",                                             ap: "^chips$|\\bchips\\b" },
            { q: "overhead camera",                                 a: "Eye in the Sky",                                    ap: "eye in the sky" },
            { q: "type of poker game",                              a: "Texas Hold'em",                                     ap: "texas hold" },
            { q: "blackjack.*lowest number.*does not hit",          a: "17",                                                ap: "^17$|\\b17\\b" },
            { q: "better hand",                                     a: "Royal Flush",                                       ap: "royal flush" },
            { q: "would not beat a pair of fives",                  a: "A Pair of twos",                                    ap: "pair of twos" },
            { q: "not usually found in a casino",                   a: "A clock",                                           ap: "clock" },
            { q: "bigger payout",                                   a: "50 / 1",                                            ap: "50" },
            { q: "cash out",                                        a: "Exchange their credit back to cash",                ap: "exchange.*credit|credit.*cash" },
        ],
        medical: [
            { q: "embedded object.*arm.*artery",                    a: "Apply a dressing around the object",                ap: "dressing around the object" },
            { q: "how much blood",                                  a: "5 liters",                                          ap: "5 liters" },
            { q: "tachycardia",                                     a: "An accelerated heart rate",                         ap: "accelerated heart rate" },
            { q: "femur",                                           a: "Thigh bone",                                        ap: "thigh bone" },
            { q: "clutching their throat choking",                  a: "Perform the Heimlich Maneuver",                     ap: "heimlich" },
            { q: "how many bones",                                  a: "206",                                               ap: "^206$|\\b206\\b" },
            { q: "dre is an examination",                           a: "Rectum",                                            ap: "^rectum$|\\brectum\\b" },
            { q: "normal core body temperature",                    a: "37.0 degrees celsius",                              ap: "37" },
            { q: "soldiers disease",                                a: "Morphine addiction",                                ap: "morphine" },
        ],
        education: [
            { q: "illiterate",                                      a: "They cannot read or write",                         ap: "cannot read or write" },
            { q: "fill the gap.*loud noise",                        a: "There",                                             ap: "^there\\." },
            { q: "bullying another pupil",                          a: "Put bully in detention and send letter home",       ap: "detention" },
            { q: "adverb is what",                                  a: "A word describing an action",                       ap: "beautifully|describing an action" },
            { q: "x.*15.*27|27.*x.*15",                             a: "12",                                                ap: "^12$|\\b12\\b" },
            { q: "quoth the raven",                                 a: "Never more",                                        ap: "never more" },
            { q: "correct spelling",                                a: "Miscellaneous",                                     ap: "miscellaneous" },
            { q: "fallen over.*cut her knee",                       a: "Send her to the school nurse",                      ap: "school nurse" },
            { q: "sat.*college admissions",                         a: "Scholastic Aptitude Test",                          ap: "scholastic aptitude" },
        ],
        law: [
            { q: "purpose of the jury",                             a: "To give a true verdict based on evidence",          ap: "true verdict" },
            { q: "highest court",                                   a: "Supreme Court",                                     ap: "supreme court" },
            { q: "intentionally cause damage to property",          a: "Vandalism",                                         ap: "^vandalism$|\\bvandalism\\b" },
            { q: "how do you start a civil action",                 a: "File a complaint with the court",                   ap: "file a complaint" },
            { q: "plea bargain",                                    a: "Incentive for defendant to plead guilty",           ap: "plead guilty" },
            { q: "hiding from the law",                             a: "Fugitive from Justice",                             ap: "fugitive" },
            { q: "civil wrong",                                     a: "Tort",                                              ap: "^tort$|\\btort\\b" },
            { q: "usual basis for filing a civil lawsuit",          a: "Negligence, intentional acts or breach of contract", ap: "negligence" },
            { q: "only crime that doesn.*t occur during full moon", a: "Murder",                                            ap: "^murder$|\\bmurder\\b" },
        ],
    };

    // ── Styles ─────────────────────────────────────────────────────────────────
    document.head.insertAdjacentHTML('beforeend', `<style>
/* Interview Answer Helper v3.3 */
.ih-correct > .speech-wrap,
.ih-correct > .speech-wrap > .link {
    outline: 3px solid #5abf5a !important;
    box-shadow: 0 0 10px #3a9a3a55 !important;
    background: #0d2a0d !important;
    border-radius: 4px;
    position: relative;
}
.ih-correct > .speech-wrap > .link::after {
    content: "✓ Correct";
    position: absolute;
    top: 50%; right: 8px;
    transform: translateY(-50%);
    background: #2a6a2a; color: #9fe89f;
    font-size: 11px; font-weight: bold;
    padding: 2px 7px; border-radius: 3px;
    pointer-events: none; z-index: 10000;
    white-space: nowrap;
}
.ih-wrong > .speech-wrap {
    opacity: 0.35 !important;
}
/* Floating answer panel */
#ih-panel {
    position: fixed; bottom: 70px; right: 14px;
    width: 290px;
    background: #141414; border: 1px solid #2a5a2a;
    border-radius: 7px; box-shadow: 0 4px 18px #0009;
    font-family: Arial, sans-serif; font-size: 13px; color: #ccc;
    z-index: 99999;
}
#ih-panel-hdr {
    display: flex; align-items: center; justify-content: space-between;
    padding: 8px 12px;
    background: linear-gradient(135deg, #1a2e1a, #141414);
    border-bottom: 1px solid #2a4a2a; border-radius: 7px 7px 0 0;
    cursor: pointer; user-select: none; -webkit-user-select: none;
}
#ih-panel-title { font-size: 13px; font-weight: bold; color: #7abf7a; }
#ih-panel-tog { font-size: 12px; color: #456; transition: transform .2s; }
#ih-panel.ih-collapsed #ih-panel-tog { transform: rotate(180deg); }
#ih-panel-body { padding: 10px 12px; }
#ih-panel.ih-collapsed #ih-panel-body { display: none; }
.ih-qa-q {
    font-size: 11px; color: #778; line-height: 1.4; margin-bottom: 4px;
}
.ih-qa-a {
    font-size: 13px; font-weight: bold; color: #7abf7a;
    background: #0e2010; border: 1px solid #2a4a2a;
    border-radius: 4px; padding: 5px 10px; line-height: 1.5;
}
.ih-qa-a.ih-unknown {
    color: #bf9f5a; background: #201a0e; border-color: #4a3a18;
}
.ih-empty { font-size: 12px; color: #446; }
</style>`);

    // ── Helpers ────────────────────────────────────────────────────────────────
    const ihNorm = s =>
        s.toLowerCase()
         .replace(/[\u201c\u201d\u2018\u2019'"]/g, "'")
         .replace(/[^a-z0-9 ']/g, ' ')
         .replace(/\s+/g, ' ')
         .trim();

    const ihMatchEntry = (jobKey, normQ) => {
        const pool = INTERVIEW_QA[jobKey];
        if (!pool) return null;
        for (const e of pool)
            if (new RegExp(e.q, 'i').test(normQ)) return e;
        return null;
    };

    const ihFindOpt = (els, ap) => {
        const rx = new RegExp(ap, 'i');
        return els.find(e => rx.test(ihNorm(e.textContent))) || null;
    };

    // ── URL helpers ────────────────────────────────────────────────────────────

    // Returns true only when on an active interview page:
    //   joblist.php#!p=interview&job=<name>&...
    function ihIsInterview() {
        return location.href.includes('joblist.php') && location.href.includes('p=interview');
    }

    // Extract job key from URL param `job=`.
    // Torn uses: army, grocer (or grocery), casino, medical, education, law
    function ihJobFromURL() {
        const m = location.href.match(/[?&#]job=([a-zA-Z]+)/);
        if (!m) return null;
        const raw = m[1].toLowerCase();
        if (raw === 'grocery') return 'grocer';   // alias just in case
        return INTERVIEW_QA[raw] ? raw : null;
    }

    // ── Floating panel ─────────────────────────────────────────────────────────
    let ihPanel = null;

    function ihEnsurePanel() {
        if (ihPanel) return;
        ihPanel = document.createElement('div');
        ihPanel.id = 'ih-panel';
        ihPanel.innerHTML = `
            <div id="ih-panel-hdr">
                <span id="ih-panel-title">📋 Interview Helper</span>
                <span id="ih-panel-tog">▲</span>
            </div>
            <div id="ih-panel-body">
                <div class="ih-empty">Waiting for question…</div>
            </div>`;
        document.body.appendChild(ihPanel);
        ihPanel.querySelector('#ih-panel-hdr').addEventListener('click', () => {
            ihPanel.classList.toggle('ih-collapsed');
            _save('ih_collapsed', ihPanel.classList.contains('ih-collapsed') ? 'yes' : 'no');
        });
        if (_load('ih_collapsed', 'no') === 'yes') ihPanel.classList.add('ih-collapsed');
    }

    function ihRemovePanel() {
        if (!ihPanel) return;
        ihPanel.remove();
        ihPanel = null;
    }

    function ihSetPanel(qText, entry) {
        if (!ihPanel) return;
        const body = ihPanel.querySelector('#ih-panel-body');
        if (!qText) {
            body.innerHTML = '<div class="ih-empty">Waiting for question…</div>';
        } else if (!entry) {
            body.innerHTML = `
                <div class="ih-qa-q">${qText}</div>
                <div class="ih-qa-a ih-unknown">⚠ Answer not in database</div>`;
        } else {
            body.innerHTML = `
                <div class="ih-qa-q">${qText}</div>
                <div class="ih-qa-a">✓ ${entry.a}</div>`;
        }
    }

    // ── Core: process one page state ───────────────────────────────────────────
    let ihLastQ = '';

    function ihProcess() {
        // Show or hide panel based on whether we're on the interview page
        if (!ihIsInterview()) {
            ihRemovePanel();
            ihLastQ = '';
            return;
        }
        ihEnsurePanel();

        const jobKey = ihJobFromURL();
        if (!jobKey) {
            ihSetPanel('', null);
            return;
        }

        // Find the last interviewer dialog block (the current question)
        const dialogs = document.querySelectorAll(
            '.interviewer-wrap.dialog.speech-right, .interviewer-wrap.dialog'
        );
        if (!dialogs.length) { ihSetPanel('', null); return; }

        const lastDialog = dialogs[dialogs.length - 1];
        const speechWrap = lastDialog.querySelector('.speech-wrap');
        if (!speechWrap) { ihSetPanel('', null); return; }

        // Strip the "Mary:" / "Smith:" author prefix, then curly quotes
        const authorEl = speechWrap.querySelector('.author');
        let rawQ = speechWrap.textContent || '';
        if (authorEl) rawQ = rawQ.replace(authorEl.textContent, '').replace(/^\s*:\s*/, '');
        rawQ = rawQ.replace(/[\u201c\u201d\u201e\u2033""]/g, '').trim();

        // Collect answer option divs
        const answerDivs = Array.from(document.querySelectorAll('.interviewer-wrap.answer'));
        // Trivia screens always have exactly 3 options; skip intro screens (2 options)
        if (answerDivs.length < 3) { ihSetPanel('', null); return; }

        // Dedup — don't re-highlight if nothing changed
        if (rawQ === ihLastQ) return;
        ihLastQ = rawQ;

        const normQ = ihNorm(rawQ);

        // Get the <a> link from each answer div
        const links = answerDivs.map(
            div => div.querySelector('.link.t-blue-cont a') || div.querySelector('a')
        ).filter(Boolean);
        if (links.length < 3) return;

        // Match question → answer
        const entry = ihMatchEntry(jobKey, normQ);

        // Apply highlight classes to the answer divs
        answerDivs.forEach(div => div.classList.remove('ih-correct', 'ih-wrong'));
        if (entry) {
            const correctLink = ihFindOpt(links, entry.ap);
            answerDivs.forEach((div, i) => {
                div.classList.add(links[i] === correctLink ? 'ih-correct' : 'ih-wrong');
            });
        }

        ihSetPanel(rawQ, entry || null);
    }

    // ── Init ───────────────────────────────────────────────────────────────────
    (function ihInit() {
        if (!location.href.includes('joblist.php')) return;

        ihProcess();

        let rafId = null;
        const obs = new MutationObserver(() => {
            if (rafId) return;
            rafId = requestAnimationFrame(() => { rafId = null; ihProcess(); });
        });
        obs.observe(document.body, { childList: true, subtree: true });

        window.addEventListener('hashchange', () => { ihLastQ = ''; ihProcess(); });
        window.addEventListener('popstate',   () => { ihLastQ = ''; ihProcess(); });
    })();

})();