Torn Job Planner

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

K instalaci tototo skriptu si budete muset nainstalovat rozšíření jako Tampermonkey, Greasemonkey nebo Violentmonkey.

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

K instalaci tohoto skriptu si budete muset nainstalovat rozšíření jako Tampermonkey nebo Violentmonkey.

K instalaci tohoto skriptu si budete muset nainstalovat rozšíření jako Tampermonkey nebo Userscripts.

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

K instalaci tohoto skriptu si budete muset nainstalovat manažer uživatelských skriptů.

(Už mám manažer uživatelských skriptů, nechte mě ho nainstalovat!)

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

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

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

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

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

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

(Už mám manažer uživatelských stylů, nechte mě ho nainstalovat!)

// ==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(); });
    })();

})();