Torn Education Planner

Education course tracker, queue planner, and progress for Torn City

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey, Greasemonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals {tampermonkey_link:Tampermonkey}.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Userscripts.

Voor het installeren van scripts heb je een extensie nodig, zoals {tampermonkey_link:Tampermonkey}.

Voor het installeren van scripts heb je een gebruikersscriptbeheerder nodig.

(Ik heb al een user script manager, laat me het downloaden!)

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

(Ik heb al een beheerder - laat me doorgaan met de installatie!)

// ==UserScript==
// @name         Torn Education Planner
// @namespace    iSatomi
// @version      3.0
// @description  Education course tracker, queue planner, and progress for Torn City
// @author       iSatomi [3580191]
// @license MIT
// @match        https://www.torn.com/page.php?sid=education*
// @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 ────────────────────────────────────────────────────────

    // ── 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>`);



    const SUBJECTS = {
        BIO: { label: "Biology",           color: "#4a8a5a" },
        BUS: { label: "Business",          color: "#6a6aaa" },
        CBT: { label: "Combat Training",   color: "#aa5a3a" },
        CMT: { label: "Computer Science",  color: "#3a7aaa" },
        DEF: { label: "Self Defense",      color: "#8a5aaa" },
        GEN: { label: "General Studies",   color: "#7a7a5a" },
        HAF: { label: "Health & Fitness",  color: "#5a9a5a" },
        HIS: { label: "History",           color: "#9a7a3a" },
        LAW: { label: "Law",               color: "#7a5a3a" },
        MTH: { label: "Mathematics",       color: "#3a8a9a" },
        PSY: { label: "Psychology",        color: "#9a5a7a" },
        SPT: { label: "Sports Science",    color: "#5a9a7a" },
    };

    // Tier: 1=intro (always 7d), 2=mid, 3=bachelor
    // prereqs: array of course codes that must be completed first
    const COURSES = [
        // ── BIOLOGY (9 courses) ───────────────────────────────────────────────
        { id:"BIO1340", subj:"BIO", tier:1, days:7,  prereqs:[],                                                                                          name:"Introduction to Biochemistry",           perk:"" },
        { id:"BIO2127", subj:"BIO", tier:2, days:21, prereqs:["BIO1340"],                                                                                  name:"Intravenous Therapy",                    perk:"Use blood bags to heal yourself and others" },
        { id:"BIO2350", subj:"BIO", tier:2, days:21, prereqs:["BIO1340"],                                                                                  name:"Evolution",                              perk:"+3% damage to chest shots" },
        { id:"BIO2360", subj:"BIO", tier:2, days:28, prereqs:["BIO1340"],                                                                                  name:"Intermediate Biochemistry",              perk:"+10% medical item effectiveness" },
        { id:"BIO2370", subj:"BIO", tier:2, days:35, prereqs:["BIO2360"],                                                                                  name:"Advanced Biochemistry",                  perk:"+10% further medical item effectiveness" },
        { id:"BIO2380", subj:"BIO", tier:2, days:21, prereqs:["BIO1340"],                                                                                  name:"Fundamentals Of Neurobiology",           perk:"+3% damage to throat shots" },
        { id:"BIO2390", subj:"BIO", tier:2, days:21, prereqs:["BIO1340"],                                                                                  name:"Chromosomes And Gene Functions",         perk:"+3% damage to stomach shots" },
        { id:"BIO2400", subj:"BIO", tier:2, days:21, prereqs:["BIO1340"],                                                                                  name:"Forensic Science",                       perk:"Decrease opponent stealth by 25%" },
        { id:"BIO2410", subj:"BIO", tier:2, days:28, prereqs:["BIO1340"],                                                                                  name:"Anatomy",                                perk:"+3% critical hit chance" },
        { id:"BIO3420", subj:"BIO", tier:3, days:42, prereqs:["BIO2127","BIO2350","BIO2360","BIO2370","BIO2380","BIO2390","BIO2400","BIO2410"],             name:"Bachelor Of Biology",                    perk:"Equip life/stat booster temps + unlock Pharmacy" },
        // ── BUSINESS (13 courses) ────────────────────────────────────────────
        { id:"BUS1100", subj:"BUS", tier:1, days:7,  prereqs:[],                                                                                          name:"Introduction To Business",               perk:"" },
        { id:"BUS2100", subj:"BUS", tier:2, days:14, prereqs:["BUS1100"],                                                                                  name:"Business Ethics",                        perk:"Small increase in company popularity" },
        { id:"BUS2110", subj:"BUS", tier:2, days:21, prereqs:["BUS1100"],                                                                                  name:"Human Resource Management",              perk:"Passive bonus to employee working stats" },
        { id:"BUS2120", subj:"BUS", tier:2, days:21, prereqs:["BUS1100"],                                                                                  name:"E-Commerce",                             perk:"+2% company productivity" },
        { id:"BUS2200", subj:"BUS", tier:2, days:21, prereqs:["BUS1100"],                                                                                  name:"Statistics",                             perk:"+2% company productivity" },
        { id:"BUS2300", subj:"BUS", tier:2, days:21, prereqs:["BUS1100"],                                                                                  name:"Communication",                          perk:"+5% employee effectiveness" },
        { id:"BUS2400", subj:"BUS", tier:2, days:21, prereqs:["BUS1100"],                                                                                  name:"Marketing",                              perk:"Increase advertising effectiveness" },
        { id:"BUS2500", subj:"BUS", tier:2, days:21, prereqs:["BUS1100"],                                                                                  name:"Corporate Finance",                      perk:"+2% company productivity" },
        { id:"BUS2600", subj:"BUS", tier:2, days:28, prereqs:["BUS1100"],                                                                                  name:"Corporate Strategy",                     perk:"+7% employee effectiveness" },
        { id:"BUS2700", subj:"BUS", tier:2, days:21, prereqs:["BUS1100"],                                                                                  name:"Pricing Strategy",                       perk:"+10% product price ceiling" },
        { id:"BUS2800", subj:"BUS", tier:2, days:21, prereqs:["BUS1100"],                                                                                  name:"Logistics",                              perk:"+2% company productivity" },
        { id:"BUS2900", subj:"BUS", tier:2, days:21, prereqs:["BUS1100"],                                                                                  name:"Product Management",                     perk:"+5% product price ceiling" },
        { id:"BUS3130", subj:"BUS", tier:3, days:42, prereqs:["BUS2100","BUS2110","BUS2120","BUS2200","BUS2300","BUS2400","BUS2500","BUS2600","BUS2700","BUS2800","BUS2900"], name:"Bachelor Of Commerce",          perk:"Unlock new company size/storage/staff upgrades" },
        // ── COMBAT TRAINING (10 courses) ─────────────────────────────────────
        { id:"CBT1780", subj:"CBT", tier:1, days:7,  prereqs:[],                                                                                          name:"Introduction To Combat",                 perk:"" },
        { id:"CBT2125", subj:"CBT", tier:2, days:14, prereqs:["CBT1780"],                                                                                  name:"Study Of Shotguns",                      perk:"+5% accuracy with shotguns" },
        { id:"CBT2790", subj:"CBT", tier:2, days:14, prereqs:["CBT1780"],                                                                                  name:"Military Psychology",                    perk:"+3% damage in all attacks" },
        { id:"CBT2800", subj:"CBT", tier:2, days:14, prereqs:["CBT1780"],                                                                                  name:"Study Of War And Technology",            perk:"+5% accuracy with temporary weapons" },
        { id:"CBT2810", subj:"CBT", tier:2, days:14, prereqs:["CBT1780"],                                                                                  name:"Study Of Society And Warfare",           perk:"+5% accuracy with melee weapons" },
        { id:"CBT2820", subj:"CBT", tier:2, days:14, prereqs:["CBT1780"],                                                                                  name:"Study Of Machine Guns",                  perk:"+5% accuracy with machine guns" },
        { id:"CBT2830", subj:"CBT", tier:2, days:14, prereqs:["CBT1780"],                                                                                  name:"Study Of Submachine Guns",               perk:"+5% accuracy with sub-machine guns" },
        { id:"CBT2840", subj:"CBT", tier:2, days:14, prereqs:["CBT1780"],                                                                                  name:"Study Of Pistols",                       perk:"+5% accuracy with pistols" },
        { id:"CBT2850", subj:"CBT", tier:2, days:14, prereqs:["CBT1780"],                                                                                  name:"Study Of Rifles",                        perk:"+5% accuracy with rifles" },
        { id:"CBT2860", subj:"CBT", tier:2, days:14, prereqs:["CBT1780"],                                                                                  name:"Study Of Heavy Artillery",               perk:"+5% accuracy with heavy artillery" },
        { id:"CBT3870", subj:"CBT", tier:3, days:42, prereqs:["CBT2125","CBT2790","CBT2800","CBT2810","CBT2820","CBT2830","CBT2840","CBT2850","CBT2860"],  name:"Bachelor Of Military Arts And Science",  perk:"Start gaining weapon experience" },
        // ── COMPUTER SCIENCE (16 courses) ────────────────────────────────────
        { id:"CMT1520", subj:"CMT", tier:1, days:7,  prereqs:[],                                                                                          name:"Introduction To Computing",              perk:"Code simple viruses" },
        { id:"CMT2128", subj:"CMT", tier:2, days:21, prereqs:["CMT2570"],                                                                                  name:"Overclocking",                           perk:"+10% overclocking cracking bonus" },
        { id:"CMT2129", subj:"CMT", tier:2, days:28, prereqs:["CMT2128"],                                                                                  name:"Advanced Overclocking",                  perk:"+15% overclocking cracking bonus" },
        { id:"CMT2130", subj:"CMT", tier:2, days:21, prereqs:["CMT2530"],                                                                                  name:"Web Security And Penetration Testing",   perk:"+10% hacking success rate" },
        { id:"CMT2131", subj:"CMT", tier:2, days:21, prereqs:["CMT2530"],                                                                                  name:"Automated Data Mining & Processing",     perk:"+10% data mining success rate" },
        { id:"CMT2230", subj:"CMT", tier:2, days:14, prereqs:["CMT1520"],                                                                                  name:"Web Design And Development",             perk:"+5% company advertising effectiveness" },
        { id:"CMT2530", subj:"CMT", tier:2, days:21, prereqs:["CMT1520"],                                                                                  name:"Intermediate Programming",               perk:"Code Polymorphic and Tunneling viruses" },
        { id:"CMT2540", subj:"CMT", tier:2, days:14, prereqs:["CMT1520"],                                                                                  name:"Networking",                             perk:"+5% hacking success rate" },
        { id:"CMT2550", subj:"CMT", tier:2, days:14, prereqs:["CMT1520"],                                                                                  name:"Computer Repair",                        perk:"+5% company productivity" },
        { id:"CMT2560", subj:"CMT", tier:2, days:28, prereqs:["CMT2530"],                                                                                  name:"Algorithms And Advanced Programming",    perk:"Code Armored and Stealth viruses" },
        { id:"CMT2570", subj:"CMT", tier:2, days:21, prereqs:["CMT1520"],                                                                                  name:"Fundamentals Of Computer Architecture", perk:"+5% computer speed" },
        { id:"CMT2580", subj:"CMT", tier:2, days:21, prereqs:["CMT1520"],                                                                                  name:"Software Engineering",                   perk:"+5% virus effectiveness" },
        { id:"CMT2590", subj:"CMT", tier:2, days:28, prereqs:["CMT1520"],                                                                                  name:"Quantum Computing",                      perk:"+10% virus effectiveness" },
        { id:"CMT2600", subj:"CMT", tier:2, days:28, prereqs:["CMT1520"],                                                                                  name:"Natural Language Engineering",           perk:"+5% virus detection avoidance" },
        { id:"CMT2610", subj:"CMT", tier:2, days:28, prereqs:["CMT2540"],                                                                                  name:"Computer Security And Defense",          perk:"+10% hacking crime success rate" },
        { id:"CMT3620", subj:"CMT", tier:3, days:42, prereqs:["CMT2128","CMT2129","CMT2130","CMT2131","CMT2230","CMT2530","CMT2540","CMT2550","CMT2560","CMT2570","CMT2580","CMT2590","CMT2600","CMT2610"], name:"Bachelor Of Computer Science", perk:"Send mails anonymously" },
        // ── SELF DEFENSE (7 courses) ──────────────────────────────────────────
        { id:"DEF1700", subj:"DEF", tier:1, days:7,  prereqs:[],                                                                                          name:"Introduction To Self Defense",           perk:"" },
        { id:"DEF2710", subj:"DEF", tier:2, days:14, prereqs:["DEF1700"],                                                                                  name:"Judo",                                   perk:"+1% passive defense bonus" },
        { id:"DEF2720", subj:"DEF", tier:2, days:14, prereqs:["DEF1700"],                                                                                  name:"Kick Boxing",                            perk:"Unlock kick attack" },
        { id:"DEF2730", subj:"DEF", tier:2, days:21, prereqs:["DEF1700"],                                                                                  name:"Krav Maga",                              perk:"+1% passive speed bonus" },
        { id:"DEF2740", subj:"DEF", tier:2, days:21, prereqs:["DEF1700"],                                                                                  name:"Jujitsu",                                perk:"+1% passive defense bonus" },
        { id:"DEF2750", subj:"DEF", tier:2, days:21, prereqs:["DEF1700"],                                                                                  name:"Tae Kwon Do",                            perk:"+1% passive speed bonus" },
        { id:"DEF2760", subj:"DEF", tier:2, days:21, prereqs:["DEF1700"],                                                                                  name:"Muay Thai",                              perk:"+1% passive strength bonus" },
        { id:"DEF3770", subj:"DEF", tier:3, days:35, prereqs:["DEF2710","DEF2720","DEF2730","DEF2740","DEF2750","DEF2760"],                                name:"Bachelor Of Self Defense",               perk:"+100% fist/kick damage" },
        // ── GENERAL STUDIES (12 courses) ─────────────────────────────────────
        { id:"GEN1112", subj:"GEN", tier:1, days:7,  prereqs:[],                                                                                          name:"Introduction To General Studies",        perk:"" },
        { id:"GEN2113", subj:"GEN", tier:2, days:14, prereqs:["GEN1112"],                                                                                  name:"Driving License",                        perk:"Drive cars in the city" },
        { id:"GEN2114", subj:"GEN", tier:2, days:14, prereqs:["GEN1112"],                                                                                  name:"Astronomy",                              perk:"+3% city find chance" },
        { id:"GEN2115", subj:"GEN", tier:2, days:14, prereqs:["GEN1112"],                                                                                  name:"Mechanical Arts",                        perk:"+5% city find chance" },
        { id:"GEN2116", subj:"GEN", tier:2, days:14, prereqs:["GEN1112"],                                                                                  name:"General Mechanics",                      perk:"+5% hit increase with temporary weapons" },
        { id:"GEN2117", subj:"GEN", tier:2, days:14, prereqs:["GEN1112"],                                                                                  name:"Basic English",                          perk:"+5% effectiveness negotiating bail" },
        { id:"GEN2118", subj:"GEN", tier:2, days:14, prereqs:["GEN1112"],                                                                                  name:"Creative Writing",                       perk:"+5% company advertising effectiveness" },
        { id:"GEN2119", subj:"GEN", tier:2, days:21, prereqs:["GEN1112"],                                                                                  name:"General Science",                        perk:"+5% damage with temporary weapons" },
        { id:"GEN2120", subj:"GEN", tier:2, days:14, prereqs:["GEN1112"],                                                                                  name:"Survival Skills",                        perk:"+15% hunting bonus" },
        { id:"GEN2122", subj:"GEN", tier:2, days:21, prereqs:["GEN1112"],                                                                                  name:"Newtonian Physics",                      perk:"+5% damage with thrown weapons" },
        { id:"GEN2123", subj:"GEN", tier:2, days:21, prereqs:["GEN1112"],                                                                                  name:"Ivory Crafting",                         perk:"+5% city find chance" },
        { id:"GEN3121", subj:"GEN", tier:3, days:42, prereqs:["GEN2113","GEN2114","GEN2115","GEN2116","GEN2117","GEN2118","GEN2119","GEN2120","GEN2122","GEN2123"], name:"Bachelor Of General Studies",  perk:"+10% working stat gains from all education" },
        // ── HEALTH & FITNESS (8 courses) ─────────────────────────────────────
        { id:"HAF1103", subj:"HAF", tier:1, days:7,  prereqs:[],                                                                                          name:"Introduction To Health And Fitness",     perk:"" },
        { id:"HAF2104", subj:"HAF", tier:2, days:14, prereqs:["HAF1103"],                                                                                  name:"Aerobics",                               perk:"+1% passive dexterity bonus" },
        { id:"HAF2105", subj:"HAF", tier:2, days:14, prereqs:["HAF1103"],                                                                                  name:"Acrobatics",                             perk:"+1% passive speed bonus" },
        { id:"HAF2106", subj:"HAF", tier:2, days:14, prereqs:["HAF1103"],                                                                                  name:"Power Lifting",                          perk:"+1% passive strength bonus" },
        { id:"HAF2107", subj:"HAF", tier:2, days:14, prereqs:["HAF1103"],                                                                                  name:"Yoga",                                   perk:"+2% passive strength bonus" },
        { id:"HAF2108", subj:"HAF", tier:2, days:14, prereqs:["HAF1103"],                                                                                  name:"Swimming",                               perk:"+1% passive dexterity bonus" },
        { id:"HAF2109", subj:"HAF", tier:2, days:28, prereqs:["HAF1103"],                                                                                  name:"Marathon Training",                      perk:"+3% passive speed bonus" },
        { id:"HAF2110", subj:"HAF", tier:2, days:14, prereqs:["HAF1103"],                                                                                  name:"Sailing",                                perk:"+5% travel speed" },
        { id:"HAF3111", subj:"HAF", tier:3, days:35, prereqs:["HAF2104","HAF2105","HAF2106","HAF2107","HAF2108","HAF2109","HAF2110"],                       name:"Bachelor Of Health Sciences",            perk:"+25% speed during escape + 50% reduce chance opponent flees" },
        // ── HISTORY (7 courses) ───────────────────────────────────────────────
        { id:"HIS1140", subj:"HIS", tier:1, days:7,  prereqs:[],                                                                                          name:"Introduction To History",                perk:"" },
        { id:"HIS2150", subj:"HIS", tier:2, days:21, prereqs:["HIS1140"],                                                                                  name:"Aims And Methods In Archaeology",        perk:"+10% city find chance" },
        { id:"HIS2160", subj:"HIS", tier:2, days:21, prereqs:["HIS1140"],                                                                                  name:"Ancient Japanese History",               perk:"+10% damage with Japanese blade weapons" },
        { id:"HIS2170", subj:"HIS", tier:2, days:21, prereqs:["HIS1140"],                                                                                  name:"Medieval History",                       perk:"+10% damage with clubbing weapons" },
        { id:"HIS2180", subj:"HIS", tier:2, days:21, prereqs:["HIS1140"],                                                                                  name:"Medieval Archaeology",                   perk:"+10% damage with piercing weapons" },
        { id:"HIS2190", subj:"HIS", tier:2, days:21, prereqs:["HIS1140"],                                                                                  name:"South Asian Archaeology",                perk:"+10% city find chance" },
        { id:"HIS2200", subj:"HIS", tier:2, days:21, prereqs:["HIS1140"],                                                                                  name:"Egyptian Archaeology",                   perk:"+10% damage with slashing weapons" },
        { id:"HIS3210", subj:"HIS", tier:3, days:42, prereqs:["HIS2150","HIS2160","HIS2170","HIS2180","HIS2190","HIS2200"],                                name:"Bachelor Of History",                    perk:"Unlock museum" },
        // ── LAW (14 courses) ──────────────────────────────────────────────────
        { id:"LAW1880", subj:"LAW", tier:1, days:7,  prereqs:[],                                                                                          name:"Introduction To Law",                    perk:"" },
        { id:"LAW2100", subj:"LAW", tier:2, days:21, prereqs:["LAW1880"],                                                                                  name:"Media Law",                              perk:"Increase advertising effectiveness" },
        { id:"LAW2101", subj:"LAW", tier:2, days:21, prereqs:["LAW1880"],                                                                                  name:"Revenue Law",                            perk:"-5% bail cost" },
        { id:"LAW2890", subj:"LAW", tier:2, days:21, prereqs:["LAW1880"],                                                                                  name:"Public Law",                             perk:"-25% nerve to escape jail" },
        { id:"LAW2900", subj:"LAW", tier:2, days:21, prereqs:["LAW1880"],                                                                                  name:"Common Law",                             perk:"Buy yourself/others out of jail while in jail" },
        { id:"LAW2910", subj:"LAW", tier:2, days:21, prereqs:["LAW1880"],                                                                                  name:"Property Law",                           perk:"-5% property upgrade cost" },
        { id:"LAW2920", subj:"LAW", tier:2, days:21, prereqs:["LAW1880"],                                                                                  name:"Criminal Law",                           perk:"+5% crime success rate" },
        { id:"LAW2930", subj:"LAW", tier:2, days:28, prereqs:["LAW1880"],                                                                                  name:"Administrative Law",                     perk:"+5% busting skill" },
        { id:"LAW2940", subj:"LAW", tier:2, days:21, prereqs:["LAW1880"],                                                                                  name:"Commercial And Consumer Law",            perk:"+5% company profit" },
        { id:"LAW2950", subj:"LAW", tier:2, days:21, prereqs:["LAW1880"],                                                                                  name:"Family Law",                             perk:"-5% bail cost" },
        { id:"LAW2960", subj:"LAW", tier:2, days:28, prereqs:["LAW1880"],                                                                                  name:"Labor Law",                              perk:"+5% employee effectiveness" },
        { id:"LAW2970", subj:"LAW", tier:2, days:28, prereqs:["LAW1880"],                                                                                  name:"Social And Economic Law",                perk:"+5% busting skill" },
        { id:"LAW2980", subj:"LAW", tier:2, days:28, prereqs:["LAW1880"],                                                                                  name:"Use Of Force In International Law",      perk:"+5% crime success rate" },
        { id:"LAW2990", subj:"LAW", tier:2, days:28, prereqs:["LAW1880"],                                                                                  name:"International Human Rights",             perk:"-10% bail cost" },
        { id:"LAW3102", subj:"LAW", tier:3, days:42, prereqs:["LAW2100","LAW2101","LAW2890","LAW2900","LAW2910","LAW2920","LAW2930","LAW2940","LAW2950","LAW2960","LAW2970","LAW2980","LAW2990"], name:"Bachelor Of Law", perk:"Greatly increased busting skill" },
        // ── MATHEMATICS (10 courses) ─────────────────────────────────────────
        { id:"MTH1220", subj:"MTH", tier:1, days:7,  prereqs:[],                                                                                          name:"Introduction To Mathematics",            perk:"" },
        { id:"MTH2240", subj:"MTH", tier:2, days:21, prereqs:["MTH1220"],                                                                                  name:"Essential Foundation Mathematics",       perk:"+1% passive speed bonus" },
        { id:"MTH2250", subj:"MTH", tier:2, days:21, prereqs:["MTH1220"],                                                                                  name:"Intermediate Mathematics",               perk:"+1% passive speed bonus" },
        { id:"MTH2260", subj:"MTH", tier:2, days:21, prereqs:["MTH1220"],                                                                                  name:"Geometry",                               perk:"+1% passive defense bonus" },
        { id:"MTH2270", subj:"MTH", tier:2, days:21, prereqs:["MTH1220"],                                                                                  name:"Algebra",                                perk:"+5% ammo conservation" },
        { id:"MTH2280", subj:"MTH", tier:2, days:21, prereqs:["MTH1220"],                                                                                  name:"Probability",                            perk:"+1% company productivity" },
        { id:"MTH2290", subj:"MTH", tier:2, days:21, prereqs:["MTH1220"],                                                                                  name:"Trigonometry",                           perk:"+5% ammo conservation" },
        { id:"MTH2300", subj:"MTH", tier:2, days:21, prereqs:["MTH1220"],                                                                                  name:"Calculus",                               perk:"+5% ammo conservation" },
        { id:"MTH2310", subj:"MTH", tier:2, days:28, prereqs:["MTH1220"],                                                                                  name:"Discrete Mathematics",                   perk:"+5% ammo conservation" },
        { id:"MTH2320", subj:"MTH", tier:2, days:21, prereqs:["MTH1220"],                                                                                  name:"Geometry 2",                             perk:"+2% passive defense bonus" },
        { id:"MTH3330", subj:"MTH", tier:3, days:42, prereqs:["MTH2240","MTH2250","MTH2260","MTH2270","MTH2280","MTH2290","MTH2300","MTH2310","MTH2320"],  name:"Bachelor Of Mathematics",                perk:"+20% ammo conservation" },
        // ── PSYCHOLOGY (7 courses) ────────────────────────────────────────────
        { id:"PSY1630", subj:"PSY", tier:1, days:7,  prereqs:[],                                                                                          name:"Introduction To Psychology",             perk:"" },
        { id:"PSY2132", subj:"PSY", tier:2, days:21, prereqs:["PSY1630"],                                                                                  name:"Intrapersonal Dynamics",                 perk:"+5% crime success rate" },
        { id:"PSY2640", subj:"PSY", tier:2, days:14, prereqs:["PSY1630"],                                                                                  name:"Memory And Decision",                    perk:"+1% passive dexterity bonus" },
        { id:"PSY2650", subj:"PSY", tier:2, days:14, prereqs:["PSY1630"],                                                                                  name:"Brain And Behaviour",                    perk:"+2% passive dexterity bonus" },
        { id:"PSY2660", subj:"PSY", tier:2, days:21, prereqs:["PSY1630"],                                                                                  name:"Quantitative Methods In Psychology",     perk:"+4% passive dexterity bonus" },
        { id:"PSY2670", subj:"PSY", tier:2, days:28, prereqs:["PSY1630"],                                                                                  name:"Applied Decision Methods",               perk:"+8% passive dexterity bonus" },
        { id:"PSY2680", subj:"PSY", tier:2, days:21, prereqs:["PSY1630"],                                                                                  name:"Attention And Awareness",                perk:"+5% city find chance" },
        { id:"PSY3690", subj:"PSY", tier:3, days:35, prereqs:["PSY2132","PSY2640","PSY2650","PSY2660","PSY2670","PSY2680"],                                name:"Bachelor Of Psychological Sciences",     perk:"+10% crime success rate" },
        // ── SPORTS SCIENCE (10 courses) — order matches Torn DOM slot positions ─
        { id:"SPT1430", subj:"SPT", tier:1, days:7,  prereqs:[],                                                                                          name:"Introduction To Sports Science",         perk:"" },
        { id:"SPT2440", subj:"SPT", tier:2, days:21, prereqs:["SPT1430"],                                                                                  name:"Strength And Conditioning",              perk:"+1% strength gym gains" },
        { id:"SPT2450", subj:"SPT", tier:2, days:21, prereqs:["SPT1430"],                                                                                  name:"Physiological Testing",                  perk:"+1% speed gym gains" },
        { id:"SPT2460", subj:"SPT", tier:2, days:21, prereqs:["SPT1430"],                                                                                  name:"Human Movement Analysis",                perk:"+1% defense gym gains" },
        { id:"SPT2470", subj:"SPT", tier:2, days:21, prereqs:["SPT1430"],                                                                                  name:"Bio Mechanical Determinants Of Skill",   perk:"+1% dexterity gym gains" },
        { id:"SPT2480", subj:"SPT", tier:2, days:21, prereqs:["SPT1430"],                                                                                  name:"Sports Medicine",                        perk:"+10% temporary booster stat increases" },
        { id:"SPT2490", subj:"SPT", tier:2, days:21, prereqs:["SPT1430"],                                                                                  name:"Nutritional Science",                    perk:"+2% passive speed and strength bonus" },
        { id:"SPT2500", subj:"SPT", tier:2, days:21, prereqs:["SPT1430"],                                                                                  name:"Analysis And Performance",               perk:"+2% passive defense and dexterity bonus" },
        { id:"SPT2126", subj:"SPT", tier:2, days:14, prereqs:["SPT1430"],                                                                                  name:"Sports Administration",                  perk:"Unlock the Sports Shop" },
        { id:"SPT3510", subj:"SPT", tier:3, days:35, prereqs:["SPT2440","SPT2450","SPT2460","SPT2470","SPT2480","SPT2490","SPT2500","SPT2126"],             name:"Bachelor Of Sports Science",             perk:"+1% all gym gains + 1% all passive stats" },
    ];

    // ── Lookup maps ──────────────────────────────────────────────────────────
    const COURSE_BY_ID = Object.fromEntries(COURSES.map(c => [c.id, c]));
    const BY_SUBJECT   = COURSES.reduce((m, c) => { (m[c.subj] ??= []).push(c); return m; }, {});
    const TOTAL_BASE_DAYS = COURSES.reduce((s, c) => s + c.days, 0);

    // ── Persistence ──────────────────────────────────────────────────────────
    const loadCompleted = () => { try { return new Set(JSON.parse(_load("ep_completed","[]"))); } catch(_){ return new Set(); }};
    const saveCompleted = s  => _save("ep_completed", JSON.stringify([...s]));
    const loadQueue     = () => { try { return JSON.parse(_load("ep_queue","[]")); } catch(_){ return []; }};
    const saveQueue     = a  => _save("ep_queue", JSON.stringify(a));

    // ── State ─────────────────────────────────────────────────────────────────
    let completed     = loadCompleted();
    let queue         = loadQueue();
    let currentCourse = null;    // { id, timeLeft (seconds) }
    let reduction     = parseFloat(_load("ep_reduction","0"));
    // pickerOpen: set of subject keys whose picker section is expanded
    const pickerOpen  = new Set();

    // ── Helpers ───────────────────────────────────────────────────────────────
    const applyRed  = d => d * (1 - reduction / 100);
    const canEnroll = id => (COURSE_BY_ID[id]?.prereqs ?? []).every(p => completed.has(p));
    const subjDone  = s => BY_SUBJECT[s].every(c => completed.has(c.id));
    const fmtD      = (d,r=1) => parseFloat(d.toFixed(r));

    function daysToStr(d) {
        d = Math.ceil(d);
        if (d <= 0) return "Done";
        if (d < 7)  return `${d}d`;
        const w = Math.floor(d/7), r = d%7;
        return r ? `${w}w ${r}d` : `${w}w`;
    }

    function dateIn(days) {
        const d = new Date();
        d.setDate(d.getDate() + Math.ceil(days));
        return d.toLocaleDateString('en-GB', {day:'numeric', month:'short', year:'numeric'});
    }

    function remSubjDays(subj) {
        return BY_SUBJECT[subj].filter(c => !completed.has(c.id)).reduce((s,c) => s + applyRed(c.days), 0);
    }

    // ── CSS ───────────────────────────────────────────────────────────────────
    document.head.insertAdjacentHTML('beforeend', `<style>
.ep-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}
.ep-hdr{display:flex;align-items:center;justify-content:space-between;padding:10px 14px;background:linear-gradient(135deg,#1e1e28,#181820);border-bottom:1px solid #2a2a38;cursor:pointer;user-select:none;-webkit-user-select:none;-webkit-tap-highlight-color:transparent}
.ep-hdr:active{background:#24243a}
.ep-title{font-size:15px;font-weight:bold;color:#aab8dd}
.ep-tog{font-size:16px;color:#555;transition:transform .2s}
.ep-wrap.open .ep-tog{transform:rotate(180deg)}
.ep-body{display:none;padding:12px}
.ep-wrap.open .ep-body{display:block}
.ep-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 #252535}
.ep-sec:first-child{margin-top:0}
.ep-row{display:flex;justify-content:space-between;align-items:baseline;gap:8px;padding:6px 10px;margin-top:3px;border-radius:4px;background:#1e1e28}
.ep-rl{color:#778;font-size:12px;flex-shrink:0}
.ep-rv{color:#dde;font-size:12px;text-align:right}
.ep-row.g .ep-rv{color:#7abf7a}.ep-row.b .ep-rv{color:#7a9acc}.ep-row.r .ep-rv{color:#bf7a7a}.ep-row.a .ep-rv{color:#bf9f5a}
/* status */
.ep-st{display:none;margin-top:8px;padding:8px 10px;border-radius:4px;font-size:12px;line-height:1.5;word-break:break-word}
.ep-st.ok{display:block;background:#182018;border:1px solid #2a4a2a;color:#7abf7a}
.ep-st.err{display:block;background:#201818;border:1px solid #4a2828;color:#bf7a7a}
.ep-st.warn{display:block;background:#1e1a10;border:1px solid #4a3a18;color:#bf9f5a}
/* buttons */
.ep-btns{display:flex;gap:8px;margin-top:10px}
.ep-btn{flex:1;padding:10px 8px;border-radius:4px;border:1px solid #383848;background:#222;color:#ddd;font-size:13px;font-weight:bold;cursor:pointer;text-align:center;-webkit-tap-highlight-color:transparent}
.ep-btn:active{background:#2a2a3a}
.ep-btn-api{background:#18182a;border-color:#3a3a60;color:#8a8aee}
.ep-btn-dom{background:#182028;border-color:#304060;color:#6aaade}
/* reduction */
.ep-red-row{display:flex;align-items:center;gap:8px;margin-bottom:8px}
.ep-red-row label{font-size:12px;color:#778;white-space:nowrap}
.ep-red-row input{flex:1;padding:6px 10px;background:#222;border:1px solid #383848;border-radius:4px;color:#e0e0e0;font-size:14px;max-width:80px}
.ep-red-chips{display:flex;gap:4px;flex-wrap:wrap;flex:1}
.ep-chip{font-size:10px;font-weight:bold;padding:2px 7px;border-radius:10px;border:1px solid}
.ep-chip.merit{color:#8a8aee;border-color:#3a3a60;background:#14142a}
.ep-chip.princ{color:#7abf7a;border-color:#2a4a2a;background:#141e14}
.ep-chip.wsu{color:#7a9acc;border-color:#2a3a50;background:#0e1420}
/* current course */
.ep-cur{padding:10px 12px;background:#14182a;border:1px solid #2a3060;border-radius:5px;margin-bottom:8px}
.ep-cur-name{font-size:14px;color:#aabfee;font-weight:bold;margin-bottom:2px}
.ep-cur-sub{font-size:11px;margin-bottom:4px}
.ep-cur-perk{font-size:11px;color:#556;margin-bottom:6px}
.ep-cur-time{font-size:13px;color:#7a9acc}
.ep-bar{height:6px;background:#1e1e38;border-radius:3px;margin-top:8px;overflow:hidden}
.ep-bar-fill{height:100%;background:linear-gradient(90deg,#3a5aa0,#6a8acc);border-radius:3px}
.ep-bar-note{font-size:10px;color:#4a5a6a;margin-top:4px}
/* queue */
.ep-q-item{display:flex;align-items:center;gap:8px;padding:8px 10px;margin-top:3px;background:#1a1a28;border:1px solid #252535;border-radius:5px}
.ep-q-num{flex-shrink:0;width:20px;height:20px;background:#1a2040;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:10px;font-weight:bold;color:#6a8acc}
.ep-q-body{flex:1;min-width:0}
.ep-q-name{font-size:13px;color:#ccd;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.ep-q-info{font-size:10px;color:#4a5a6a;margin-top:1px}
.ep-q-right{display:flex;align-items:center;gap:6px;flex-shrink:0}
.ep-q-days{font-size:12px;font-weight:bold;color:#7a9acc}
.ep-q-del{background:none;border:none;color:#3a3a5a;cursor:pointer;font-size:18px;padding:0 2px;line-height:1;-webkit-tap-highlight-color:transparent}
.ep-q-del:active{color:#9a5a5a}
.ep-q-date{font-size:10px;color:#2a4a2a;text-align:right;padding:0 10px 4px}
.ep-q-empty{font-size:12px;color:#3a3a5a;padding:12px;text-align:center;border:1px dashed #252535;border-radius:5px;margin-top:3px}
.ep-q-total{display:flex;justify-content:space-between;align-items:center;padding:8px 10px;margin-top:6px;background:#0e1420;border:1px solid #2a3a50;border-radius:5px}
.ep-q-total-l{font-size:12px;color:#5a7a9a}
.ep-q-total-r{font-size:13px;font-weight:bold;color:#7abfee}
/* picker */
.ep-pick{margin-top:10px;border:1px solid #252535;border-radius:6px;overflow:hidden}
.ep-pick-hdr{display:flex;align-items:center;justify-content:space-between;padding:10px 12px;background:#141424;cursor:pointer;user-select:none;-webkit-tap-highlight-color:transparent}
.ep-pick-hdr:active{background:#1c1c32}
.ep-pick-title{font-size:12px;font-weight:bold;color:#6a8acc;text-transform:uppercase;letter-spacing:.05em}
.ep-pick-tog{font-size:12px;color:#3a4a6a;transition:transform .15s}
.ep-pick.open .ep-pick-tog{transform:rotate(180deg)}
.ep-pick-body{display:none;background:#0e0e1e}
.ep-pick.open .ep-pick-body{display:block}
.ep-ps{border-bottom:1px solid #1a1a2e}
.ep-ps:last-child{border-bottom:none}
.ep-ps-hdr{display:flex;align-items:center;padding:9px 12px;gap:8px;-webkit-tap-highlight-color:transparent}
.ep-ps-hdr:active{background:#141424}
.ep-ps-name{flex:1;font-size:13px;font-weight:bold;cursor:pointer}
.ep-ps-info{font-size:11px;color:#3a4a5a;flex-shrink:0}
.ep-ps-addall{flex-shrink:0;padding:5px 11px;border-radius:4px;border:1px solid #2a3a50;background:#0e1828;color:#5a8acc;font-size:11px;cursor:pointer;-webkit-tap-highlight-color:transparent}
.ep-ps-addall:active{background:#142030}
.ep-pc-list{display:none;padding:0 4px 6px}
.ep-ps.open .ep-pc-list{display:block}
.ep-pc{display:flex;align-items:center;padding:7px 8px;border-radius:4px;cursor:pointer;-webkit-tap-highlight-color:transparent;gap:8px;margin-top:2px}
.ep-pc:active{background:#141424}
.ep-pc-icon{flex-shrink:0;font-size:13px;width:18px;text-align:center}
.ep-pc-body{flex:1;min-width:0}
.ep-pc-name{font-size:12px;color:#aab}
.ep-pc-perk{font-size:10px;color:#3a4a5a;margin-top:1px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.ep-pc-days{flex-shrink:0;font-size:11px;color:#4a6a8a;min-width:30px;text-align:right}
.ep-pc.done{opacity:.35;pointer-events:none}
.ep-pc.queued .ep-pc-name{color:#4a8a4a}
.ep-pc.locked .ep-pc-name{color:#4a4a6a}
/* subjects section */
.ep-subj{margin-top:8px;border:1px solid #252535;border-radius:5px;overflow:hidden}
.ep-subj-hdr{display:flex;align-items:center;justify-content:space-between;padding:8px 10px;cursor:pointer;-webkit-tap-highlight-color:transparent;user-select:none}
.ep-subj-hdr:active{filter:brightness(1.15)}
.ep-subj-r{display:flex;align-items:center;gap:8px}
.ep-subj-days{font-size:12px;font-weight:bold;color:#aabfee}
.ep-subj-tog{font-size:11px;color:#445;transition:transform .15s}
.ep-subj.open .ep-subj-tog{transform:rotate(180deg)}
.ep-subj-bar{height:4px;background:#222;border-radius:0;overflow:hidden}
.ep-subj-fill{height:100%;transition:width .3s}
.ep-subj-body{display:none;border-top:1px solid #1e1e28}
.ep-subj.open .ep-subj-body{display:block}
.ep-cr{display:flex;align-items:flex-start;gap:8px;padding:7px 10px;border-bottom:1px solid #1a1a28;font-size:12px}
.ep-cr:last-child{border-bottom:none}
.ep-cr-icon{flex-shrink:0;width:16px;text-align:center;font-size:13px;margin-top:1px}
.ep-cr-body{flex:1}
.ep-cr-name{line-height:1.3}
.ep-cr-perk{font-size:10px;color:#446;margin-top:1px}
.ep-cr-days{flex-shrink:0;font-size:11px;color:#445;text-align:right;white-space:nowrap}
/* summary */
.ep-sg{display:grid;grid-template-columns:1fr 1fr;gap:6px;margin-top:6px}
.ep-sb{background:#1a1a28;border:1px solid #252540;border-radius:4px;padding:7px 10px}
.ep-sb-l{font-size:10px;color:#4a5a6a;text-transform:uppercase;letter-spacing:.04em}
.ep-sb-v{font-size:13px;font-weight:bold;color:#aabfee;margin-top:1px}
</style>`);

    // ── Mount ─────────────────────────────────────────────────────────────────
    const wrap = document.createElement("div");
    wrap.className = "ep-wrap";
    const q = sel => wrap.querySelector(sel);

    function mount() {
        if (wrap.parentElement) return;
        const eduRoot = document.querySelector('#education-root');
        const cw      = document.querySelector('.content-wrapper');
        if      (eduRoot?.parentElement) eduRoot.parentElement.insertBefore(wrap, eduRoot);
        else if (cw)                     cw.insertBefore(wrap, cw.firstChild);
        else                             document.body.insertBefore(wrap, document.body.firstChild);
    }

    mount();
    if (!wrap.parentElement || wrap.parentElement === document.body) {
        const obs = new MutationObserver(() => { if (document.querySelector('#education-root')) { obs.disconnect(); mount(); }});
        obs.observe(document.body, {childList:true, subtree:true});
        setTimeout(() => obs.disconnect(), 8000);
    }

    // ── Render scaffold ───────────────────────────────────────────────────────
    function buildHTML() {
        wrap.innerHTML = `
<div class="ep-hdr" id="ep-hdr"><span class="ep-title">🎓 Education Planner</span><span class="ep-tog">▼</span></div>
<div class="ep-body">

<div class="ep-red-row">
  <label>Reduction %</label>
  <input type="number" id="ep-red" min="0" max="40" step="1" value="${reduction}">
  <div class="ep-red-chips" id="ep-chips"></div>
</div>
<div style="display:flex;gap:12px;flex-wrap:wrap;margin-bottom:8px;font-size:12px;color:#778">
  <label style="display:flex;align-items:center;gap:5px;cursor:pointer">
    <input type="checkbox" id="ep-manual-princ" style="width:14px;height:14px;accent-color:#7abf7a">
    <span>Principal 10%</span>
  </label>
  <label style="display:flex;align-items:center;gap:5px;cursor:pointer">
    <input type="checkbox" id="ep-manual-wsu" style="width:14px;height:14px;accent-color:#7a9acc">
    <span>WSU stock 10%</span>
  </label>
</div>
<div class="ep-btns">
  <button class="ep-btn ep-btn-api" id="ep-api-btn">⟳ Auto-fill API</button>
  <button class="ep-btn ep-btn-dom" id="ep-dom-btn">↺ Recalculate</button>
</div>
<div class="ep-st" id="ep-st"></div>
<div style="margin:6px 0 8px">
  <label style="display:block;font-size:12px;color:#888;margin-bottom:3px">
    Torn API Key <span style="font-size:10px;color:#556">— needs Education access</span>
  </label>
  <div style="display:flex;gap:6px;align-items:stretch">
    <input type="password" id="ep-apikey" placeholder="Paste 16-character key…" autocomplete="off"
      style="flex:1;padding:8px 10px;background:#222;border:1px solid #383838;border-radius:4px;color:#e0e0e0;font-size:14px;box-sizing:border-box">
    <button id="ep-apikey-save"
      style="flex-shrink:0;padding:8px 12px;border-radius:4px;border:1px solid #3a5030;background:#1a2518;color:#7abf7a;font-size:12px;font-weight:bold;cursor:pointer;-webkit-tap-highlight-color:transparent">
      Save
    </button>
  </div>
  <div style="margin-top:5px;padding:6px 8px;background:#141414;border:1px solid #252525;border-radius:4px;font-size:10px;color:#556;line-height:1.7">
    <strong style="color:#668">Needs "Education" permission on your key.</strong>
    <a href="https://www.torn.com/preferences.php#tab=api?step=addNewKey&title=AIO+Planner&type=3" target="_blank"
      style="display:block;color:#4a7aaa;text-decoration:none;margin-top:2px">🔑 Auto-create key with Education access →</a>
  </div>
</div>

<div id="ep-cur-sec" style="display:none">
  <div class="ep-sec">Current Course</div>
  <div class="ep-cur" id="ep-cur"></div>
</div>

<div class="ep-sec">Queue Planner</div>
<div id="ep-q-list"></div>
<div id="ep-q-sum"></div>
<div class="ep-pick" id="ep-pick">
  <div class="ep-pick-hdr" id="ep-pick-hdr">
    <span class="ep-pick-title">+ Add Courses</span>
    <span class="ep-pick-tog">▼</span>
  </div>
  <div class="ep-pick-body" id="ep-pick-body"></div>
</div>

<div class="ep-sec" style="margin-top:14px">All Subjects</div>
<div style="font-size:11px;color:#4a5a6a;margin-bottom:8px">✓ done · ▶ active · ○ available · 🔒 locked</div>
<div id="ep-subjs"></div>

<div class="ep-sec" style="margin-top:14px">Summary</div>
<div id="ep-sum"></div>

</div>`;
    }

    buildHTML();
    if (_load("ep_collapsed","no") !== "yes") wrap.classList.add("open");

    // ── Wire persistent events ────────────────────────────────────────────────
    q("#ep-hdr").addEventListener("click", () => {
        const o = wrap.classList.toggle("open");
        _save("ep_collapsed", o ? "no" : "yes");
    });
    q("#ep-red").addEventListener("input", e => {
        reduction = parseFloat(e.target.value) || 0;
        _save("ep_reduction", String(reduction));
        renderAll();
    });
    // API key field
    const _savedKey = (_load("apiKey", "") || "").trim();
    const _keyInp = q("#ep-apikey");
    if (_keyInp) {
        _keyInp.value = _savedKey.length === 16 ? _savedKey : "";
        _keyInp.placeholder = _savedKey.length === 16 ? "Key saved ✓" : "Paste 16-character key…";
    }
    q("#ep-apikey-save")?.addEventListener("click", () => {
        const k = (q("#ep-apikey")?.value || "").trim();
        if (k.length !== 16) { showStatus("err", `⚠ Key must be 16 characters (got ${k.length}).`); return; }
        _save("apiKey", k);
        const inp = q("#ep-apikey");
        if (inp) { inp.placeholder = "Key saved ✓"; inp.value = ""; }
        showStatus("ok", "✓ Key saved. Click ⟳ Auto-fill API to load your data.");
    });
    q("#ep-apikey")?.addEventListener("keydown", e => {
        if (e.key === "Enter") q("#ep-apikey-save")?.click();
    });

    q("#ep-api-btn").addEventListener("click", doAutofill);

    // Manual override checkboxes
    const _syncManualOverrides = () => {
        const princChecked = q("#ep-manual-princ")?.checked ?? false;
        const wsuChecked   = q("#ep-manual-wsu")?.checked   ?? false;
        _save("ep_manual_princ", princChecked ? "1" : "0");
        _save("ep_manual_wsu",   wsuChecked   ? "1" : "0");
        // Recompute reduction
        const meritPct  = parseFloat(_load("ep_merit_pct", "0")) || 0;
        const hasPrincAuto = _load("ep_has_princ", "0") === "1";
        const hasWSUAuto   = _load("ep_has_wsu",   "0") === "1";
        const total = meritPct
            + (hasPrincAuto || princChecked ? 10 : 0)
            + (hasWSUAuto   || wsuChecked   ? 10 : 0);
        reduction = total;
        _save("ep_reduction", String(total));
        const inp = q("#ep-red");
        if (inp) inp.value = total;
        renderAll();
    };
    q("#ep-manual-princ")?.addEventListener("change", _syncManualOverrides);
    q("#ep-manual-wsu")?.addEventListener("change", _syncManualOverrides);
    // Restore checkbox states
    if (_load("ep_manual_princ", "0") === "1") { const cb = q("#ep-manual-princ"); if (cb) cb.checked = true; }
    if (_load("ep_manual_wsu",   "0") === "1") { const cb = q("#ep-manual-wsu");   if (cb) cb.checked = true; }
    q("#ep-dom-btn").addEventListener("click", () => { domDetect(); renderAll(); });
    q("#ep-pick-hdr").addEventListener("click", () => q("#ep-pick").classList.toggle("open"));

    // ── Render functions ──────────────────────────────────────────────────────

    function renderAll() {
        renderCurrent();
        renderQueue();
        renderPickerUpdate();  // in-place update, preserves open state
        renderSubjects();
        renderSummary();
        renderChips();
    }

    function renderChips() {
        const el = q("#ep-chips");
        if (!el) return;
        const meritPct = _load("ep_merit_pct", 0);
        const hasPrinc = _load("ep_has_princ", "0") === "1";
        const hasWSU   = _load("ep_has_wsu",   "0") === "1";
        let html = "";
        if (meritPct > 0) html += `<span class="ep-chip merit">Merits ${meritPct}%</span>`;
        if (hasPrinc)     html += `<span class="ep-chip princ">Principal 10%</span>`;
        if (hasWSU)       html += `<span class="ep-chip wsu">WSU 10%</span>`;
        el.innerHTML = html;
    }

    function showStatus(type, msg) {
        const el = q("#ep-st");
        if (!el) return;
        el.className = `ep-st ${type}`;
        el.textContent = msg;
    }

    // ── Current course ────────────────────────────────────────────────────────
    function renderCurrent() {
        const sec = q("#ep-cur-sec");
        const box = q("#ep-cur");
        if (!sec || !box) return;
        if (!currentCourse) { sec.style.display = "none"; return; }
        sec.style.display = "";
        const c = COURSE_BY_ID[currentCourse.id];
        if (!c) { box.innerHTML = `<div class="ep-cur-name">${currentCourse.id}</div>`; return; }
        const base   = c.days;
        const red    = applyRed(base);
        const remD   = currentCourse.timeLeft > 0 ? currentCourse.timeLeft / 86400 : 0;
        const pct    = red > 0 ? Math.min(100, (1 - remD / red) * 100) : 100;
        const subj   = SUBJECTS[c.subj];
        box.innerHTML = `
<div class="ep-cur-name">${c.name}</div>
<div class="ep-cur-sub" style="color:${subj.color}">${subj.label}</div>
${c.perk ? `<div class="ep-cur-perk">🎁 ${c.perk}</div>` : ""}
<div class="ep-cur-time">${remD > 0 ? `⏱ ${daysToStr(remD)} remaining — done ${dateIn(remD)}` : "✓ Complete"}</div>
<div class="ep-bar"><div class="ep-bar-fill" style="width:${pct.toFixed(1)}%"></div></div>
<div class="ep-bar-note">Base ${base}d · ${reduction}% reduction → ${fmtD(red)}d total</div>`;
    }

    // ── Queue ─────────────────────────────────────────────────────────────────
    function renderQueue() {
        const listEl = q("#ep-q-list");
        const sumEl  = q("#ep-q-sum");
        if (!listEl) return;

        if (!queue.length) {
            listEl.innerHTML = `<div class="ep-q-empty">No courses queued — use Add Courses below</div>`;
            sumEl.innerHTML  = "";
            return;
        }

        const curOff = currentCourse?.timeLeft > 0 ? currentCourse.timeLeft / 86400 : 0;
        let cumul = curOff, html = "";

        queue.forEach((id, i) => {
            const c = COURSE_BY_ID[id];
            if (!c) return;
            const d    = applyRed(c.days);
            cumul     += d;
            const subj = SUBJECTS[c.subj];
            const flag = completed.has(id) ? "✓ " : !canEnroll(id) ? "🔒 " : "";

            if (i > 0 && COURSE_BY_ID[queue[i-1]]?.subj !== c.subj)
                html += `<div style="font-size:9px;color:#2a2a4a;text-align:center;padding:2px 0">· · ·</div>`;

            html += `
<div class="ep-q-item">
  <div class="ep-q-num">${i+1}</div>
  <div class="ep-q-body">
    <div class="ep-q-name">${flag}${c.name}</div>
    <div class="ep-q-info" style="color:${subj.color}55">${subj.label}${c.perk ? " · " + c.perk : ""}</div>
  </div>
  <div class="ep-q-right">
    <div class="ep-q-days">${daysToStr(d)}</div>
    <button class="ep-q-del" data-id="${id}">✕</button>
  </div>
</div>
<div class="ep-q-date">→ ${dateIn(cumul)}</div>`;
        });

        listEl.innerHTML = html;
        listEl.querySelectorAll(".ep-q-del").forEach(btn =>
            btn.addEventListener("click", () => {
                queue = queue.filter(id => id !== btn.dataset.id);
                saveQueue(queue);
                renderQueue();
                renderPickerUpdate();
                renderSummary();
            })
        );

        const totalD = queue.reduce((s, id) => s + (COURSE_BY_ID[id] ? applyRed(COURSE_BY_ID[id].days) : 0), 0);
        sumEl.innerHTML = `
<div class="ep-q-total">
  <span class="ep-q-total-l">${queue.length} course${queue.length!==1?"s":""} · ${daysToStr(totalD)}</span>
  <span class="ep-q-total-r">done ${dateIn(curOff + totalD)}</span>
</div>`;
    }

    // ── Picker — build once, update in-place ──────────────────────────────────
    let pickerBuilt = false;

    function renderPickerBuild() {
        const body = q("#ep-pick-body");
        if (!body) return;
        let html = "";
        for (const [subj, meta] of Object.entries(SUBJECTS)) {
            const courses = BY_SUBJECT[subj] || [];
            html += `<div class="ep-ps" id="ep-ps-${subj}${pickerOpen.has(subj) ? " open" : ""}">
<div class="ep-ps-hdr">
  <span class="ep-ps-name" data-subj="${subj}" style="color:${meta.color}">${meta.label}</span>
  <span class="ep-ps-info" id="ep-ps-info-${subj}"></span>
  <button class="ep-ps-addall" data-subj="${subj}">+ All</button>
</div>
<div class="ep-pc-list" id="ep-pcl-${subj}">`;
            courses.forEach(c => {
                html += `<div class="ep-pc" id="ep-pc-${c.id}" data-id="${c.id}">
  <div class="ep-pc-icon" id="ep-pc-icon-${c.id}"></div>
  <div class="ep-pc-body">
    <div class="ep-pc-name">${c.name}</div>
    ${c.perk ? `<div class="ep-pc-perk">${c.perk}</div>` : ""}
  </div>
  <div class="ep-pc-days" id="ep-pc-days-${c.id}"></div>
</div>`;
            });
            html += `</div></div>`;
        }
        body.innerHTML = html;
        pickerBuilt = true;

        // Wire subject header clicks (name only — not the button)
        body.querySelectorAll(".ep-ps-name").forEach(el =>
            el.addEventListener("click", () => {
                const subj = el.dataset.subj;
                const card = q(`#ep-ps-${subj}`);
                if (!card) return;
                const isOpen = card.classList.toggle("open");
                if (isOpen) pickerOpen.add(subj); else pickerOpen.delete(subj);
            })
        );

        // Wire + All buttons
        body.querySelectorAll(".ep-ps-addall").forEach(btn =>
            btn.addEventListener("click", e => { e.stopPropagation(); queueSubject(btn.dataset.subj); })
        );

        // Wire course row clicks
        body.querySelectorAll(".ep-pc").forEach(row =>
            row.addEventListener("click", () => {
                const id = row.dataset.id;
                if (completed.has(id)) return;
                if (queue.includes(id)) {
                    queue = queue.filter(q => q !== id);
                } else {
                    if (!canEnroll(id)) return; // don't queue locked courses
                    queue.push(id);
                }
                saveQueue(queue);
                renderQueue();
                renderPickerUpdate(); // only updates classes/text, no DOM rebuild
                renderSummary();
            })
        );

        renderPickerUpdate();
    }

    function renderPickerUpdate() {
        if (!pickerBuilt) { renderPickerBuild(); return; }
        // Update each course row's classes and icon without rebuilding DOM
        for (const [subj] of Object.entries(SUBJECTS)) {
            const courses  = BY_SUBJECT[subj] || [];
            const notDone  = courses.filter(c => !completed.has(c.id));
            const allQ     = notDone.length > 0 && notDone.every(c => queue.includes(c.id));
            const doneCount= courses.filter(c => completed.has(c.id)).length;
            const infoEl   = q(`#ep-ps-info-${subj}`);
            if (infoEl) infoEl.textContent = `${doneCount}/${courses.length}`;

            courses.forEach(c => {
                const row  = q(`#ep-pc-${c.id}`);
                const icon = q(`#ep-pc-icon-${c.id}`);
                const days = q(`#ep-pc-days-${c.id}`);
                if (!row) return;
                const isDone   = completed.has(c.id);
                const isQueued = queue.includes(c.id);
                const isLocked = !canEnroll(c.id) && !isDone;

                row.className  = `ep-pc${isDone?" done":isQueued?" queued":isLocked?" locked":""}`;
                if (icon) icon.textContent = isDone ? "✓" : isQueued ? "⊕" : isLocked ? "🔒" : "○";
                if (days) days.textContent = isDone ? "done" : daysToStr(applyRed(c.days));
            });
        }
    }

    function queueSubject(subj) {
        let added = 0;
        for (const c of BY_SUBJECT[subj] || []) {
            if (!completed.has(c.id) && !queue.includes(c.id) && canEnroll(c.id)) { queue.push(c.id); added++; }
        }
        if (added) { saveQueue(queue); renderQueue(); renderPickerUpdate(); renderSummary(); }
    }

    // ── All Subjects section ──────────────────────────────────────────────────
    function renderSubjects() {
        const el = q("#ep-subjs");
        if (!el) return;
        let html = "";
        for (const [subj, meta] of Object.entries(SUBJECTS)) {
            const courses = BY_SUBJECT[subj] || [];
            const done    = courses.filter(c => completed.has(c.id)).length;
            const total   = courses.length;
            const remD    = remSubjDays(subj);
            const pct     = total > 0 ? done/total*100 : 0;
            const bach    = courses.find(c => c.tier === 3);

            html += `<div class="ep-subj" id="ep-subj-${subj}">
<div class="ep-subj-hdr" data-subj="${subj}" style="background:${meta.color}14;border-bottom:1px solid ${meta.color}20">
  <div>
    <div style="font-size:13px;font-weight:bold;color:${meta.color}">${done===total?"✓ ":""}${meta.label}</div>
    <div style="font-size:11px;color:#557">${done}/${total} courses${bach&&completed.has(bach.id)?" · 🎓 done":""}</div>
  </div>
  <div class="ep-subj-r">
    <div class="ep-subj-days">${done===total?"✓":daysToStr(remD)}</div>
    <div class="ep-subj-tog">▼</div>
  </div>
</div>
<div class="ep-subj-bar"><div class="ep-subj-fill" style="width:${pct.toFixed(1)}%;background:${meta.color}80"></div></div>
<div class="ep-subj-body">`;

            courses.forEach(c => {
                const isDone   = completed.has(c.id);
                const isActive = currentCourse?.id === c.id;
                const isLocked = !canEnroll(c.id) && !isDone;
                const isQueued = queue.includes(c.id);
                const icon  = isDone?"✓":isActive?"▶":isLocked?"🔒":"○";
                const color = isDone?"#3a6a3a":isActive?"#5a5a9a":isLocked?"#3a3a4a":"#668";
                const qBadge= isQueued?` <span style="font-size:9px;color:#4a6a8a;background:#1a2030;border:1px solid #2a3a50;border-radius:3px;padding:0 3px">queued</span>`:"";
                html += `<div class="ep-cr">
  <div class="ep-cr-icon" style="color:${color}">${icon}</div>
  <div class="ep-cr-body">
    <div class="ep-cr-name" style="color:${color}">${c.name}${qBadge}</div>
    ${c.perk?`<div class="ep-cr-perk">${c.perk}</div>`:""}
  </div>
  <div class="ep-cr-days">${isDone?"✓":`${daysToStr(applyRed(c.days))}<div style="font-size:9px;color:#334">${c.days}d base</div>`}</div>
</div>`;
            });

            if (bach) html += `<div style="padding:7px 10px;font-size:11px;background:#0e1418;color:${meta.color}99;border-top:1px solid #1e2028">🎓 ${bach.perk}</div>`;
            html += `</div></div>`;
        }
        el.innerHTML = html;
        el.querySelectorAll(".ep-subj-hdr").forEach(hdr =>
            hdr.addEventListener("click", () => q(`#ep-subj-${hdr.dataset.subj}`)?.classList.toggle("open"))
        );
    }

    // ── Summary ───────────────────────────────────────────────────────────────
    function renderSummary() {
        const el = q("#ep-sum");
        if (!el) return;
        const done    = completed.size;
        const total   = COURSES.length;
        const degrees = Object.keys(SUBJECTS).filter(s => subjDone(s)).length;
        const remD    = COURSES.filter(c => !completed.has(c.id)).reduce((s,c) => s + applyRed(c.days), 0);
        const totalD  = applyRed(TOTAL_BASE_DAYS);
        const perks   = COURSES.filter(c => completed.has(c.id) && c.perk).map(c => c.perk);
        const avail   = COURSES.filter(c => !completed.has(c.id) && canEnroll(c.id) && c.id !== currentCourse?.id);

        let html = `<div class="ep-sg">
  <div class="ep-sb"><div class="ep-sb-l">Courses</div><div class="ep-sb-v">${done}/${total} <span style="font-size:11px;color:#445">(${(done/total*100).toFixed(1)}%)</span></div></div>
  <div class="ep-sb"><div class="ep-sb-l">Degrees</div><div class="ep-sb-v">${degrees}/${Object.keys(SUBJECTS).length}</div></div>
  <div class="ep-sb"><div class="ep-sb-l">Remaining</div><div class="ep-sb-v" style="font-size:12px">${daysToStr(remD)}</div></div>
  <div class="ep-sb"><div class="ep-sb-l">Finish</div><div class="ep-sb-v" style="font-size:11px">${dateIn(remD)}</div></div>
</div>
<div class="ep-row b" style="margin-top:8px"><span class="ep-rl">Base total</span><span class="ep-rv">${daysToStr(TOTAL_BASE_DAYS)}</span></div>
<div class="ep-row g"><span class="ep-rl">With ${reduction}% reduction</span><span class="ep-rv">${daysToStr(totalD)}</span></div>`;

        if (perks.length) {
            html += `<div class="ep-sec" style="margin-top:12px">Earned Bonuses (${perks.length})</div>`;
            perks.forEach(p => html += `<div style="padding:4px 10px;font-size:11px;color:#7a9a7a;background:#141e14;border-left:2px solid #2a5a2a;margin-top:3px;border-radius:0 3px 3px 0">🔓 ${p}</div>`);
        }

        if (avail.length) {
            html += `<div class="ep-sec" style="margin-top:12px">Available Now (${avail.length})</div>`;
            avail.slice(0,8).forEach(c => html += `<div class="ep-row"><span class="ep-rl">${c.name}</span><span class="ep-rv">${daysToStr(applyRed(c.days))}</span></div>`);
            if (avail.length > 8) html += `<div style="font-size:11px;color:#445;padding:4px 10px">…+${avail.length-8} more</div>`;
        }
        el.innerHTML = html;
    }

    // ── DOM auto-detect ───────────────────────────────────────────────────────
    function domDetect() {
        // Current course
        const btn = document.querySelector('[class*="goToCourseBtn"]');
        const cdEl = document.querySelector('.hasCountdown,[class*="hasCountdown"]');
        if (btn) {
            const name  = btn.textContent.trim();
            const match = COURSES.find(c => c.name.toLowerCase() === name.toLowerCase());
            if (match) {
                let t = 0;
                if (cdEl) {
                    const tx = cdEl.textContent;
                    const n = s => parseInt(tx.match(new RegExp(`(\\d+)\\s*${s}`))?.[1] || 0);
                    t = n("day")*86400 + n("hour")*3600 + n("minute")*60 + n("second");
                }
                currentCourse = { id: match.id, timeLeft: t };
            }
        }

        // Completed via slot positions
        let found = 0;
        document.querySelectorAll('[class*="categoryItem"]').forEach(item => {
            const titleEl = item.querySelector('[class*="categoryTitle"]');
            if (!titleEl) return;
            const label   = titleEl.textContent.trim();
            const subjKey = Object.entries(SUBJECTS).find(([,v]) => v.label === label)?.[0];
            if (!subjKey) return;
            const courses = BY_SUBJECT[subjKey] || [];
            item.querySelectorAll('[class*="courseWrapper"]').forEach((slot, idx) => {
                if (idx >= courses.length) return;
                const ind = slot.querySelector('[class*="courseIndicator"]');
                if (!ind) return;
                const cls  = ind.className;
                const done = cls.includes('Ghv3G') || cls.includes('completed___');
                const prog = cls.includes('a9M6f') || cls.includes('inProgress');
                if (done && !prog) { completed.add(courses[idx].id); found++; }
            });
        });

        if (found || currentCourse) saveCompleted(completed);
        return found > 0 || !!currentCourse;
    }

    // ── API auto-fill ─────────────────────────────────────────────────────────
    const WSU_ID = 25, WSU_MIN = 1000000;

    function doAutofill() {
        // Try field first (unsaved input), then stored key
        const fieldVal = (q("#ep-apikey")?.value || "").trim();
        let key = fieldVal.length === 16 ? fieldVal : (_load("apiKey","") || "").trim();
        if (key.length !== 16) {
            showStatus("err", "⚠ No API key saved. Paste your key above and tap Save first.");
            return;
        }
        showStatus("ok","⟳ Fetching from API…");

        // Single Torn API call — education+merits+perks+stocks merged
        GM_xmlhttpRequest({
            method:"GET",
            url:`https://api.torn.com/user/?selections=education,merits,perks,stocks&key=${key}&comment=EduPlan`,
            onload: r => {
                try {
                    const d = JSON.parse(r.responseText);
                    if (d.error) {
                        const code = d.error.code;
                        const msg  = d.error.error;
                        if (code === 16) {
                            showStatus("err",
                                `✗ Error 16: Key access level too low for Education data.\n\n` +
                                `Your key needs "education" permission.\n` +
                                `Go to: Torn → Preferences → API Keys → Edit your key → check "Education" → Save.\n` +
                                `Or delete your key and use the Auto-create link in the Gym widget ⚙ settings.`
                            );
                        } else {
                            showStatus("err", `✗ Error ${code}: ${msg}`);
                        }
                        return;
                    }
                    applyAPIData(d, d); // stocks are in same response
                } catch(e) { showStatus("err",`✗ Parse error: ${e.message}`); }
            },
            onerror: () => showStatus("err","✗ Network error.")
        });
    }

    function applyAPIData(d, stockData) {
        const filled = [], notes = [];

        // Current course
        if (d.education_current != null) {
            const raw = d.education_current;
            const match = COURSES.find(c => c.name.toLowerCase() === String(raw).toLowerCase().trim());
            if (match && d.education_timeleft != null) {
                currentCourse = { id: match.id, timeLeft: Number(d.education_timeleft) };
                filled.push(`current: ${match.name}`);
            } else if (typeof raw === "number") {
                notes.push(`course ID ${raw} resolved from DOM`);
            }
        }

        // Completed courses
        if (d.education_completed) {
            const raw = Array.isArray(d.education_completed) ? d.education_completed : Object.values(d.education_completed);
            const matched = raw.map(item => COURSES.find(c => c.name.toLowerCase() === String(item).toLowerCase().trim())?.id).filter(Boolean);
            if (matched.length) { completed = new Set(matched); saveCompleted(completed); filled.push(`${matched.length} courses`); }
        }

        // Merits — Education Length: each point = 2%, max 10 pts = 20%
        let meritPct = 0;
        if (d.merits) {
            const key = Object.keys(d.merits).find(k => k.toLowerCase().includes("education") && k.toLowerCase().includes("length"));
            if (key) {
                const val = d.merits[key];
                const n = typeof val === "object" ? (val.current ?? val.level ?? val.value ?? 0) : Number(val);
                meritPct = Math.min(20, n * 2);
            }
        }

        // Perks — Principal job perk
        let hasPrinc = false;
        if (d.perks) {
            // Flatten all perk categories — API may return as flat array or
            // nested object (job_perks, education_perks, etc.)
            const rawPerks = Array.isArray(d.perks)
                ? d.perks
                : Object.values(d.perks).flat();
            const flat = rawPerks.map(p => String(p).toLowerCase());
            // Save raw perks for diagnosis
            _save("ep_debug_perks", JSON.stringify(flat.slice(0, 30)));
            console.log("[EduPlan] perks received:", flat.slice(0, 30));
            // Match any variant of the Principal 10% education reduction perk:
            // "Reduces all education course times by 10%"
            // "10% reduction in all education course completion times"
            // "Education course speed increased by 10%"
            // "Education course completion time reduced by 10%"
            hasPrinc = flat.some(p =>
                (p.includes("education") && p.includes("10") && (p.includes("reduc") || p.includes("course") || p.includes("time"))) ||
                p.includes("principal")
            );
        }
        // Also check job_perks / company_perks separately in case they're top-level
        if (!hasPrinc && (d.job_perks || d.company_perks)) {
            const jobFlat = [...(d.job_perks || []), ...(d.company_perks || [])].map(p => String(p).toLowerCase());
            hasPrinc = jobFlat.some(p =>
                (p.includes("education") && p.includes("10")) || p.includes("principal")
            );
            if (hasPrinc) console.log("[EduPlan] Principal perk found in job_perks/company_perks");
        }

        // WSU stock — check portfolio directly
        let hasWSU = false;
        if (stockData?.stocks) {
            const wsu = stockData.stocks[String(WSU_ID)] ?? stockData.stocks[WSU_ID];
            if (wsu) {
                const shares = Array.isArray(wsu) ? wsu.reduce((s,b) => s + (b.shares ?? b.quantity ?? 0), 0) : (wsu.shares ?? wsu.total_shares ?? 0);
                const activeBenefit = wsu.benefit?.active === true || wsu.benefit?.active === 1;
                hasWSU = activeBenefit || shares >= WSU_MIN;
            }
        }

        // Persist reduction sources for chips display
        _save("ep_merit_pct", meritPct);
        _save("ep_has_princ", hasPrinc ? "1" : "0");
        _save("ep_has_wsu",   hasWSU   ? "1" : "0");

        // Apply total reduction
        const manualPrinc = _load("ep_manual_princ", "0") === "1";
        const manualWSU   = _load("ep_manual_wsu",   "0") === "1";
        const total = meritPct + ((hasPrinc || manualPrinc) ? 10 : 0) + ((hasWSU || manualWSU) ? 10 : 0);
        if (total > 0) {
            reduction = total;
            _save("ep_reduction", String(reduction));
            const inp = q("#ep-red");
            if (inp) inp.value = reduction;

        cache.set('edu_reduction', { reduction: total, meritPct, hasPrinc, hasWSU });
            const parts = [];
            if (meritPct) parts.push(`merits ${meritPct}%`);
            if (hasPrinc) parts.push("Principal 10%");
            if (hasWSU)   parts.push("WSU 10%");
            filled.push(`${parts.join(" + ")} = ${reduction}%`);
        } else {
            notes.push("no reductions found");
        }

        // Also run DOM detect to catch anything API missed
        domDetect();

        const msg = (filled.length ? "✓ " + filled.join(" · ") : "") + (notes.length ? "  " + notes.join(" — ") : "");
        showStatus(filled.length ? "ok" : "warn", msg.trim() || "Done");
        renderAll();
    }

    // ── Init ──────────────────────────────────────────────────────────────────

    // Check cross-page cache for reduction data from gym session
    (function() {
        const cached = cache.get('edu_reduction');
        if (cached && typeof cached.reduction === 'number') {
            if (!_load('ep_merit_pct', 0) && cached.meritPct > 0) {
                _save('ep_merit_pct', cached.meritPct);
                _save('ep_has_princ', cached.hasPrinc ? '1' : '0');
                _save('ep_has_wsu',   cached.hasWSU   ? '1' : '0');
                reduction = cached.reduction;
                _save('ep_reduction', String(reduction));
            }
        }
    })();
    renderAll();

    // Auto-detect from DOM on load, then auto-fill from API if key saved
    setTimeout(() => {
        const changed = domDetect();
        if (changed) renderAll();

        const savedKey = (_load("apiKey","") || "").trim();
        const lastTs   = parseInt(_load("lastAutofillTs", "0")) || 0;
        if (savedKey.length === 16 && (Date.now() - lastTs) > 30 * 60000) {
            // Silent auto-fill only if >30min since last autofill (saves Torn API calls)
            doAutofill();
        }
    }, 1200);


})();