TM Transfer Scanner

Enhanced transfer market with smart filters, TI calculation and skill analysis

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         TM Transfer Scanner
// @namespace    https://trophymanager.com
// @version      1.0.0
// @description  Enhanced transfer market with smart filters, TI calculation and skill analysis
// @match        https://trophymanager.com/transfer*
// @grant        none
// @run-at       document-end
// ==/UserScript==

(function () {
    'use strict';

    const $ = window.jQuery;
    if (!$) return;

    // Only match the main transfer page, not /transfer/bids etc.
    if (!/^\/transfer\/?$/.test(location.pathname)) return;

    // ═══════════════════════════════════════════════════════════════════
    //  CONSTANTS
    // ═══════════════════════════════════════════════════════════════════

    const K_ASI     = Math.pow(2, 9) * Math.pow(5, 4) * Math.pow(7, 7); // 263,533,760,000
    const K_ASI_GK  = 48717927500;
    const WAGE_RATE = 15.8079;
    const TRAINING1 = new Date('2023-01-16T23:00:00Z');
    const SEASON_DAYS = 84;

    // Color scale (same as tm-player.user.js)
    const COLOR_LEVELS = [
        '#ff4c4c', '#ff8c00', '#ffd700', '#90ee90', '#00cfcf', '#5b9bff', '#cc88ff'
    ];
    const TI_THRESHOLDS  = [12, 9, 6, 4, 2, 1, -Infinity];
    const REC_THRESHOLDS = [5.5, 5, 4, 3, 2, 1, 0];
    const R5_THRESHOLDS  = [110, 100, 90, 80, 70, 60, -Infinity];

    // GK skill order for R5/Rec weight tables (after tooltip reorder)
    const GK_WEIGHT_ORDER = ['str','pac','jum','sta','one','ref','ari','com','kic','thr','han'];
    // Position multipliers for R5 bonuses (outfield posIdx 0-8)
    const R5_POS_MULT = [0.3, 0.3, 0.9, 0.6, 1.5, 0.9, 0.9, 0.6, 0.3];

    const SAVED_FILTERS_KEY = 'tms_saved_filters';

    //              Str         Sta         Pac         Mar         Tac         Wor         Pos         Pas         Cro         Tec         Hea         Fin         Lon         Set
    const WEIGHT_R5 = [
        [0.41029304, 0.18048062, 0.56730138, 1.06344654, 1.02312672, 0.40831256, 0.58235457, 0.12717479, 0.05454137, 0.09089830, 0.42381693, 0.04626272, 0.02199046, 0], // DC
        [0.42126371, 0.18293193, 0.60567629, 0.91904794, 0.89070915, 0.40038476, 0.56146633, 0.15053902, 0.15955429, 0.15682932, 0.42109742, 0.09460329, 0.03589655, 0], // DL/R
        [0.23412419, 0.32032289, 0.62194779, 0.63162534, 0.63143081, 0.45218831, 0.47370658, 0.55054737, 0.17744915, 0.39932519, 0.26915814, 0.16413124, 0.07404301, 0], // DMC
        [0.27276905, 0.26814289, 0.61104798, 0.39865092, 0.42862643, 0.43582015, 0.46617076, 0.44931076, 0.25175412, 0.46446692, 0.29986350, 0.43843061, 0.21494592, 0], // DML/R
        [0.25219260, 0.25112993, 0.56090649, 0.18230261, 0.18376490, 0.45928749, 0.53498118, 0.59461481, 0.09851189, 0.61601950, 0.31243959, 0.65402884, 0.29982016, 0], // MC
        [0.28155678, 0.24090675, 0.60680245, 0.19068879, 0.20018012, 0.45148647, 0.48230007, 0.42982389, 0.26268609, 0.57933805, 0.31712419, 0.65824985, 0.29885649, 0], // ML/R
        [0.22029884, 0.29229690, 0.63248227, 0.09904394, 0.10043602, 0.47469498, 0.52919791, 0.77555880, 0.10531819, 0.71048302, 0.27667115, 0.56813972, 0.21537826, 0], // OMC
        [0.21151292, 0.35804710, 0.88688492, 0.14391236, 0.13769621, 0.46586605, 0.34446036, 0.51377701, 0.59723919, 0.75126119, 0.16550722, 0.29966502, 0.12417045, 0], // OML/R
        [0.35479780, 0.14887553, 0.43273380, 0.00023928, 0.00021111, 0.46931131, 0.57731335, 0.41686333, 0.05607604, 0.62121195, 0.45370457, 1.03660702, 0.43205492, 0], // F
        [0.45462811, 0.30278232, 0.45462811, 0.90925623, 0.45462811, 0.90925623, 0.45462811, 0.45462811, 0.30278232, 0.15139116, 0.15139116],                            // GK
    ];

    //              Str         Sta         Pac         Mar         Tac         Wor         Pos         Pas         Cro         Tec         Hea         Fin         Lon         Set
    const WEIGHT_RB = [
        [0.10493615, 0.05208547, 0.07934211, 0.14448971, 0.13159554, 0.06553072, 0.07778375, 0.06669303, 0.05158306, 0.02753168, 0.12055170, 0.01350989, 0.02549169, 0.03887550], // DC
        [0.07715535, 0.04943315, 0.11627229, 0.11638685, 0.12893778, 0.07747251, 0.06370799, 0.03830611, 0.10361093, 0.06253997, 0.09128094, 0.01314110, 0.02449199, 0.03726305], // DL/R
        [0.08219824, 0.08668831, 0.07434242, 0.09661001, 0.08894242, 0.08998026, 0.09281287, 0.08868309, 0.04753574, 0.06042619, 0.05396986, 0.05059984, 0.05660203, 0.03060871], // DMC
        [0.06744248, 0.06641401, 0.09977251, 0.08253749, 0.09709316, 0.09241026, 0.08513703, 0.06127851, 0.10275520, 0.07985941, 0.04618960, 0.03927270, 0.05285911, 0.02697852], // DML/R
        [0.07304213, 0.08174111, 0.07248656, 0.08482334, 0.07078726, 0.09568392, 0.09464529, 0.09580381, 0.04746231, 0.07093008, 0.04595281, 0.05955544, 0.07161249, 0.03547345], // MC
        [0.06527363, 0.06410270, 0.09701305, 0.07406706, 0.08563595, 0.09648566, 0.08651209, 0.06357183, 0.10819222, 0.07386495, 0.03245554, 0.05430668, 0.06572005, 0.03279859], // ML/R
        [0.07842736, 0.07744888, 0.07201150, 0.06734457, 0.05002348, 0.08350204, 0.08207655, 0.11181914, 0.03756112, 0.07486004, 0.06533972, 0.07457344, 0.09781475, 0.02719742], // OMC
        [0.06545375, 0.06145378, 0.10503536, 0.06421508, 0.07627526, 0.09232981, 0.07763931, 0.07001035, 0.11307331, 0.07298351, 0.04248486, 0.06462713, 0.07038293, 0.02403557], // OML/R
        [0.07738289, 0.05022488, 0.07790481, 0.01356516, 0.01038191, 0.06495444, 0.07721954, 0.07701905, 0.02680715, 0.07759692, 0.12701687, 0.15378395, 0.12808992, 0.03805251], // F
        [0.07466384, 0.07466384, 0.07466384, 0.14932769, 0.10452938, 0.14932769, 0.10452938, 0.10344411, 0.07512610, 0.04492581, 0.04479831],                                    // GK
    ];

    // Tooltip skill name → transfer key
    const SKILL_NAME_TO_KEY = {
        'Strength':'str','Stamina':'sta','Pace':'pac','Marking':'mar','Tackling':'tac',
        'Workrate':'wor','Positioning':'pos','Passing':'pas','Crossing':'cro',
        'Technique':'tec','Heading':'hea','Finishing':'fin','Longshots':'lon','Set Pieces':'set',
        'Handling':'han','One on ones':'one','Reflexes':'ref','Aerial Ability':'ari',
        'Jumping':'jum','Communication':'com','Kicking':'kic','Throwing':'thr',
    };

    const OUTFIELD_SKILLS = ['str','sta','pac','mar','tac','wor','pos','pas','cro','tec','hea','fin','lon','set'];
    const GK_SKILLS       = ['str','sta','pac','han','one','ref','ari','jum','com','kic','thr'];
    const ALL_SKILLS      = ['str','sta','pac','mar','tac','wor','pos','pas','cro','tec','hea','fin','lon','set','han','one','ref','ari','jum','com','kic','thr'];

    const SKILL_NAMES = {
        str:'Str',sta:'Sta',pac:'Pac',mar:'Mar',tac:'Tac',wor:'Wor',
        pos:'Pos',pas:'Pas',cro:'Cro',tec:'Tec',hea:'Hea',fin:'Fin',
        lon:'Lon',set:'Set',han:'Han',one:'One',ref:'Ref',ari:'Aer',
        jum:'Jum',com:'Com',kic:'Kic',thr:'Thr',
    };

    // Position colors matching tm-player.user.js posGroupColor palette
    const POS_COLOR = { g:'#4ade80', d:'#60a5fa', dm:'#fbbf24', m:'#fbbf24', mf:'#fbbf24', om:'#fbbf24', f:'#f87171' };

    // Individual formation position → TM API group + side
    const FP_MAP = {
        gk:  { group: 'gk', side: null },
        dl:  { group: 'de', side: 'le' },
        dc:  { group: 'de', side: 'ce' },
        dr:  { group: 'de', side: 'ri' },
        dml: { group: 'dm', side: 'le' },
        dmc: { group: 'dm', side: 'ce' },
        dmr: { group: 'dm', side: 'ri' },
        ml:  { group: 'mf', side: 'le' },
        mc:  { group: 'mf', side: 'ce' },
        mr:  { group: 'mf', side: 'ri' },
        oml: { group: 'om', side: 'le' },
        omc: { group: 'om', side: 'ce' },
        omr: { group: 'om', side: 'ri' },
        fc:  { group: 'fw', side: null },
    };

    // ═══════════════════════════════════════════════════════════════════
    //  STATE
    // ═══════════════════════════════════════════════════════════════════

    let allPlayers        = [];
    let sortKey           = 'time';
    let sortAsc           = true;
    let expandedId        = null;
    let playerTipEl       = null;
    let playerTipTimer    = null;
    let isLoading         = false;
    let skillsMode        = false;
    let tooltipCache      = {};   // pid → { recSort, ti, skills }
    let tooltipFetchAbort = false;
    let findAllRunning    = false;
    let findAllAbort      = false;

    // ═══════════════════════════════════════════════════════════════════
    //  CALCULATION HELPERS
    // ═══════════════════════════════════════════════════════════════════

    function parseAge(ageFloat) {
        const years  = Math.floor(ageFloat);
        const months = Math.round((ageFloat - years) * 100);
        return { years, months, totalMonths: years * 12 + months, decimal: years + months / 12 };
    }

    function playerIsGK(p) {
        return p.fp && p.fp[0] === 'gk';
    }

    function starSum(p, gk) {
        const skills = gk ? GK_SKILLS : OUTFIELD_SKILLS;
        let sum = 0, count = 0;
        for (const s of skills) {
            if (p[s] > 0) { sum += p[s]; count++; }
        }
        return { sum, count, total: skills.length, max: skills.length * 20 };
    }

    function fmtNum(n) {
        if (n == null || isNaN(n) || n === 0) return '-';
        if (n >= 1e9) return (n / 1e9).toFixed(2) + 'B';
        if (n >= 1e6) return (n / 1e6).toFixed(1) + 'M';
        if (n >= 1e3) return (n / 1e3).toFixed(0) + 'k';
        return String(Math.round(n));
    }

    function getCurrentSession() {
        const now = new Date();
        let day = (now.getTime() - TRAINING1.getTime()) / 1000 / 3600 / 24;
        while (day > SEASON_DAYS - 16 / 24) day -= SEASON_DAYS;
        const s = Math.floor(day / 7) + 1;
        return s <= 0 ? 12 : s;
    }
    const CURRENT_SESSION = getCurrentSession();

    function calculateTI(asi, wage, isGK) {
        if (!asi || !wage || wage <= 30000) return null;
        const w = isGK ? K_ASI_GK : K_ASI;
        const { pow, log, round } = Math;
        const log27 = log(pow(2, 7));
        return round((pow(2, log(w * asi) / log27) - pow(2, log(w * wage / WAGE_RATE) / log27)) * 10);
    }

    function getColor(value, thresholds) {
        for (let i = 0; i < thresholds.length; i++) {
            if (value >= thresholds[i]) return COLOR_LEVELS[i];
        }
        return COLOR_LEVELS[COLOR_LEVELS.length - 1];
    }

    function fmtRec(val) {
        if (val == null || val === '') return '<span style="color:#4a5a40">—</span>';
        const num  = parseFloat(val);
        const disp = Number.isInteger(num) ? String(num) : num.toFixed(2);
        const clr  = getColor(num, REC_THRESHOLDS);
        return `<span class="tms-rec" style="background:rgba(0,0,0,0.25);border:1px solid ${clr}44;color:${clr}">${disp}</span>`;
    }

    function tiHtml(ti) {
        if (ti === null || ti === undefined) return '<span style="color:#4a5a40">—</span>';
        const clr = getColor(ti, TI_THRESHOLDS);
        return `<span style="color:${clr};font-weight:700">${ti.toFixed(1)}</span>`;
    }

    function fmtR5(r5) {
        if (r5 == null) return '<span class="tms-tip-pending">…</span>';
        const clr = getColor(r5, R5_THRESHOLDS);
        return `<span style="color:${clr};font-weight:700">${r5.toFixed(1)}</span>`;
    }

    function fmtAge(ageFloat) {
        const { years, months } = parseAge(ageFloat);
        return `<span class="tms-age-y">${years}.${months}</span>`;
    }

    function fmtPos(fp) {
        if (!fp || !fp.length) return '-';
        // Sort positions same as squad: DC before DMC etc.
        const sorted = [...fp].sort((a, b) => getPosIndex(a) - getPosIndex(b));
        const labelOf = str => {
            if (!str) return { label: '', color: '#aaa' };
            const side = str.slice(-1);
            const pos  = str.slice(0, str.length - 1);
            const color = POS_COLOR[pos] || POS_COLOR[str] || '#aaa';
            const sideLabel = { l:'L', c:'C', r:'R', k:'' }[side] || '';
            const label = (str === 'gk') ? 'GK' : (pos.toUpperCase() + sideLabel);
            return { label, color };
        };
        if (sorted.length === 1) {
            const { label, color } = labelOf(sorted[0]);
            return `<span style="color:${color};font-weight:700">${label}</span>`;
        }
        const firstColor = (() => {
            const str = sorted[0];
            if (str === 'gk') return '#4ade80';
            const pos = str.replace(/[lcrk]$/, '');
            return POS_COLOR[pos] || POS_COLOR[str] || '#fbbf24';
        })();
        const inner = sorted.map(str => {
            const { label, color } = labelOf(str);
            if(!label.trim()) return '';
            return ` <span style="color:${color}">${label}</span>`;
        }).filter(Boolean);
        return `<span class="tms-pos-chip" style="background:${firstColor}22;border:1px solid ${firstColor}44">${inner}</span>`;
    }

    function skillColor(val) {
        if (!val || val <= 0) return '#2a3a28';
        if (val >= 20) return '#d4af37';
        if (val >= 19) return '#c0c0c0';
        if (val >= 16) return '#66dd44';
        if (val >= 12) return '#cccc00';
        if (val >= 8)  return '#ee9900';
        return '#ee6633';
    }

    function skillCell(val) {
        if (!val || val <= 0) return `<td class="tms-skill tms-skill0">-</td>`;
        const pct = (val / 20) * 100;
        const clr = skillColor(val);
        return `<td class="tms-skill"><div class="tms-bar-wrap"><div class="tms-bar" style="width:${pct}%;background:${clr}"></div><span>${val}</span></div></td>`;
    }

    function processPlayer(p) {
        const gk   = playerIsGK(p);
        const ss   = starSum(p, gk);
        const ageP = parseAge(p.age);
        return { ...p, _gk: gk, _ss: ss, _ageP: ageP };
    }

    const fix2 = v => (Math.round(v * 100) / 100).toFixed(2);

    function getPosIndex(favposition) {
        if (!favposition) return 0;
        const pos = favposition.split(',')[0].toLowerCase().trim();
        switch (pos) {
            case 'gk':                           return 9;
            case 'dc': case 'dcl': case 'dcr':  return 0;
            case 'dr': case 'dl':               return 1;
            case 'dmc': case 'dmcl': case 'dmcr': return 2;
            case 'dmr': case 'dml':             return 3;
            case 'mc': case 'mcl': case 'mcr':  return 4;
            case 'mr': case 'ml':               return 5;
            case 'omc': case 'omcl': case 'omcr': return 6;
            case 'omr': case 'oml':             return 7;
            default:                            return 8; // fc / f
        }
    }

    function skillsToArray(skillsObj, posIdx) {
        const order = posIdx === 9 ? GK_WEIGHT_ORDER : OUTFIELD_SKILLS;
        return order.map(k => skillsObj[k] || 0);
    }

    function computeRemainders(posIdx, skills, asi) {
        const weight = posIdx === 9 ? K_ASI_GK : K_ASI;
        const skillSum = skills.reduce((s, v) => s + v, 0);
        const remainder = Math.round((Math.pow(2, Math.log(weight * asi) / Math.log(Math.pow(2, 7))) - skillSum) * 10) / 10;
        let rec = 0, ratingR = 0, remainderW1 = 0, remainderW2 = 0, not20 = 0;
        for (let i = 0; i < WEIGHT_RB[posIdx].length; i++) {
            rec     += skills[i] * WEIGHT_RB[posIdx][i];
            ratingR += skills[i] * WEIGHT_R5[posIdx][i];
            if (skills[i] !== 20) {
                remainderW1 += WEIGHT_RB[posIdx][i];
                remainderW2 += WEIGHT_R5[posIdx][i];
                not20++;
            }
        }
        if (remainder / not20 > 0.9 || !not20) {
            not20 = posIdx === 9 ? 11 : 14;
            remainderW1 = 1;
            remainderW2 = 5;
        }
        rec = parseFloat(fix2((rec + remainder * remainderW1 / not20 - 2) / 3));
        return { remainder, remainderW2, not20, ratingR, rec };
    }

    function computeR5(posIdx, skills, asi, routine) {
        const r = computeRemainders(posIdx, skills, asi);
        const { pow, E } = Math;
        const rou = routine || 0;
        const routineBonus = (3 / 100) * (100 - 100 * pow(E, -rou * 0.035));
        let rating = parseFloat(fix2(r.ratingR + (r.remainder * r.remainderW2 / r.not20) + routineBonus * 5));
        const rou2 = routineBonus;
        const goldstar = skills.filter(s => s === 20).length;
        const denom = skills.length - goldstar || 1;
        const skillsB = skills.map(s => s === 20 ? 20 : s + r.remainder / denom);
        const sr = skillsB.map((s, i) => i === 1 ? s : s + rou2);
        if (skills.length !== 11) { // outfield (14 skills)
            const headerBonus = sr[10] > 12
                ? parseFloat(fix2((pow(E, (sr[10] - 10) ** 3 / 1584.77) - 1) * 0.8 +
                    pow(E, sr[0] ** 2 * 0.007 / 8.73021) * 0.15 +
                    pow(E, sr[6] ** 2 * 0.007 / 8.73021) * 0.05))
                : 0;
            const fkBonus = parseFloat(fix2(pow(E, (sr[13] + sr[12] + sr[9] * 0.5) ** 2 * 0.002) / 327.92526));
            const ckBonus = parseFloat(fix2(pow(E, (sr[13] + sr[8] + sr[9] * 0.5) ** 2 * 0.002) / 983.65770));
            const pkBonus = parseFloat(fix2(pow(E, (sr[13] + sr[11] + sr[9] * 0.5) ** 2 * 0.002) / 1967.31409));
            const allBonus = headerBonus + fkBonus + ckBonus + pkBonus;
            const defSkillsSq = sr[0]**2 + sr[1]**2*0.5 + sr[2]**2*0.5 + sr[3]**2 + sr[4]**2 + sr[5]**2 + sr[6]**2;
            const gainBase = parseFloat(fix2(defSkillsSq / 6 / 22.9**2));
            const offSkillsSq = sr[0]**2*0.5 + sr[1]**2*0.5 + sr[2]**2 + sr[3]**2 + sr[4]**2 + sr[5]**2 + sr[6]**2;
            const keepBase = parseFloat(fix2(offSkillsSq / 6 / 22.9**2));
            const m = R5_POS_MULT[posIdx];
            return parseFloat(fix2(rating + allBonus + gainBase * m + keepBase * m));
        }
        return parseFloat(fix2(rating));
    }

    // Estimate R5 range from transfer-data skills with assumed routine [0 … 4.2*(age-15)]
    function estimatePlayerR5(p) {
        const asi = p.asi || 0;
        if (!asi) return null;
        const gk = p._gk;
        const skillKeys = gk ? GK_WEIGHT_ORDER : OUTFIELD_SKILLS;
        const skills = skillKeys.map(k => p[k] || 0);
        if (skills.every(s => s === 0)) return null;
        const positions = [...(p.fp || [])].sort((a, b) => getPosIndex(a) - getPosIndex(b));
        if (!positions.length) return null;
        const ageYears = p._ageP ? p._ageP.years : Math.floor(parseFloat(p.age) || 20);
        const routineMax = Math.max(0, 4.2 * (ageYears - 15));
        let r5Lo = null, r5Hi = null, recCalc = null;
        for (const pos of positions) {
            const pi = getPosIndex(pos);
            const lo = computeR5(pi, skills, asi, 0);
            const hi = computeR5(pi, skills, asi, routineMax);
            const rm = computeRemainders(pi, skills, asi);
            if (r5Lo === null || lo > r5Lo) r5Lo = lo;
            if (r5Hi === null || hi > r5Hi) r5Hi = hi;
            if (recCalc === null || rm.rec > recCalc) recCalc = rm.rec;
        }
        return { r5Lo, r5Hi, recCalc, routineMax };
    }

    function fmtR5Range(lo, hi) {
        if (lo == null || hi == null) return '<span class="tms-tip-pending">…</span>';
        const loFixed = lo.toFixed(1), hiFixed = hi.toFixed(1);
        const clrLo = getColor(lo, R5_THRESHOLDS);
        const clrHi = getColor(hi, R5_THRESHOLDS);
        if (loFixed === hiFixed)
            return `<span style="color:${clrHi};font-weight:700;opacity:0.75">${hiFixed}</span>`;
        return `<span style="opacity:0.75">` +
               `<span style="color:${clrLo};font-weight:700;font-size:10px">${loFixed}</span>` +
               `<span style="color:#4a6a38;font-size:9px">–</span>` +
               `<span style="color:${clrHi};font-weight:700;font-size:10px">${hiFixed}</span></span>`;
    }

    // Pre-populate tooltipCache with R5 estimates for players not yet fully fetched
    function computeAllEstimates(players) {
        for (const p of players) {
            if (tooltipCache[p.id] && !tooltipCache[p.id].estimated) continue;
            const est = estimatePlayerR5(p);
            if (est) {
                console.log(`[TMS] ${p.name_js || p.name} | age ${p.age} | routineMax ${est.routineMax.toFixed(1)} | R5: ${est.r5Lo != null ? est.r5Lo.toFixed(1) : '?'}-${est.r5Hi != null ? est.r5Hi.toFixed(1) : '?'} | Rec: ${est.recCalc != null ? est.recCalc.toFixed(2) : '?'}`);
                tooltipCache[p.id] = {
                    estimated: true,
                    r5Lo: est.r5Lo, r5Hi: est.r5Hi,
                    recCalc: est.recCalc,
                    r5: null, recSort: null, ti: null, skills: null,
                };
            }
        }
    }

    // ═══════════════════════════════════════════════════════════════════
    //  CSS
    // ═══════════════════════════════════════════════════════════════════

    function injectStyles() {
        if (document.getElementById('tms-style')) return;
        const css = `
/* ─── Root layout ─── */
#tms-root {
    display: flex;
    gap: 0;
    align-items: flex-start;
    font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
    color: #c8e0b4;
}

/* ─── Sidebar ─── */
#tms-sidebar {
    width: 250px;
    min-width: 250px;
    background: transparent;
    box-sizing: border-box;
    position: sticky;
    top: 8px;
    max-height: calc(100vh - 20px);
    overflow-y: auto;
}
#tms-sidebar::-webkit-scrollbar { width: 4px; }
#tms-sidebar::-webkit-scrollbar-track { background: #111; }
#tms-sidebar::-webkit-scrollbar-thumb { background: #3d6828; border-radius: 2px; }

/* Card-style sections (matching tm-player widget style) */
.tms-sb-section {
    background: #1c3410;
    border: 1px solid #3d6828;
    border-radius: 8px;
    overflow: hidden;
    margin-bottom: 8px;
}
.tms-sb-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    font-size: 10px;
    font-weight: 700;
    color: #6a9a58;
    text-transform: uppercase;
    letter-spacing: 0.5px;
    padding: 8px 12px 6px;
    border-bottom: 1px solid rgba(61,104,40,0.3);
}
.tms-for-inline {
    display: flex; align-items: center; gap: 4px;
    font-size: 10px; font-weight: 600; color: #90b878;
    text-transform: none; letter-spacing: 0; cursor: pointer;
}
.tms-for-inline input[type=checkbox] { accent-color: #6cc040; cursor: pointer; margin: 0; }
.tms-sb-body {
    padding: 8px 10px;
}

.tms-pos-formation { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 3px; }
.tms-pos-formation-empty { pointer-events: none; }
.tms-more-toggle {
    display: flex; align-items: center; justify-content: space-between;
    width: 100%; padding: 6px 10px; margin: 16px 0;
    background: rgba(42,74,28,0.25); border: 1px solid #2a4a1c;
    border-radius: 6px; color: #6a9a58; font-size: 10px; font-weight: 700;
    text-transform: uppercase; letter-spacing: 0.5px;
    cursor: pointer; user-select: none;
}
.tms-more-toggle:hover { background: rgba(42,74,28,0.5); color: #c8e0b4; }
.tms-more-toggle .tms-more-arrow { font-size: 9px; transition: transform .2s; }
.tms-more-toggle.open .tms-more-arrow { transform: rotate(180deg); }
.tms-more-body { display: none; }
.tms-more-body.open { display: block; }

.tms-filter-btn {
    padding: 5px 5px;
    border-radius: 5px;
    font-size: 11px;
    font-weight: 700;
    border: 1px solid rgba(61,104,40,0.45);
    background: rgba(0,0,0,0.15);
    color: #90b878;
    cursor: pointer;
    text-align: center;
    transition: all 0.12s;
    user-select: none;
}
.tms-filter-btn.active  { background: #3d6828; color: #e8f5d8; border-color: #6cc040; }
.tms-filter-btn:hover   { background: #2a4a1c; }
.tms-filter-btn.tms-gk  { color: #4ade80; }
.tms-filter-btn.tms-de  { color: #60a5fa; }
.tms-filter-btn.tms-dm  { color: #fbbf24; }
.tms-filter-btn.tms-mf  { color: #fbbf24; }
.tms-filter-btn.tms-om  { color: #fb923c; }
.tms-filter-btn.tms-fw  { color: #f87171; }

.tms-row { display: flex; align-items: center; gap: 6px; margin-bottom: 5px; }
.tms-row:last-child { margin-bottom: 0; }
.tms-range-row { display: flex; align-items: center; gap: 4px; }
.tms-range-row .tms-num { flex: 1; min-width: 0; }
.tms-range-sep { font-size: 10px; color: #5a7a48; flex-shrink: 0; }
.tms-lbl { font-size: 10px; color: #8aac72; font-weight: 600; min-width: 30px; letter-spacing: 0.3px; text-transform: uppercase; }
.tms-sel {
    flex: 1;
    background: rgba(0,0,0,0.25);
    border: 1px solid rgba(42,74,28,0.6);
    border-radius: 4px;
    color: #e8f5d8;
    font-size: 12px;
    font-weight: 600;
    padding: 5px 8px;
    outline: none;
    cursor: pointer;
    font-family: inherit;
    transition: border-color 0.15s;
}
.tms-sel:focus { border-color: #6cc040; }
.tms-num { -moz-appearance: textfield; }
.tms-num::-webkit-inner-spin-button,
.tms-num::-webkit-outer-spin-button { opacity: 1; filter: invert(0.6); }
.tms-num::placeholder { color: #5a7a48; }

.tms-check-row { display: flex; align-items: center; gap: 6px; }
.tms-check-row label { font-size: 11px; color: #90b878; cursor: pointer; }
.tms-check-row input[type=checkbox] { accent-color: #6cc040; cursor: pointer; }

.tms-skill-row { display: grid; grid-template-columns: 1fr auto; gap: 4px; margin-bottom: 4px; }
.tms-skill-row:last-child { margin-bottom: 0; }
.tms-skill-row .tms-sel { font-size: 10px; }

.tms-post-note {
    font-size: 9px;
    font-weight: 400;
    color: #4a7a38;
    text-transform: none;
    letter-spacing: 0;
    margin-left: 4px;
}

#tms-search-btn {
    width: 100%;
    padding: 9px;
    border-radius: 7px;
    border: none;
    background: #3d6828;
    color: #e8f5d8;
    font-size: 12px;
    font-weight: 700;
    cursor: pointer;
    transition: background 0.15s;
    letter-spacing: 0.3px;
    font-family: inherit;
    margin-bottom: 6px;
}
#tms-search-btn:hover { background: #4d8030; }
#tms-findall-btn {
    width: 100%;
    padding: 8px;
    border-radius: 7px;
    border: 1px solid #3d6828;
    background: rgba(61,104,40,0.12);
    color: #6cc040;
    font-size: 11px;
    font-weight: 700;
    cursor: pointer;
    transition: background 0.15s;
    letter-spacing: 0.3px;
    font-family: inherit;
}
#tms-findall-btn:hover { background: rgba(61,104,40,0.3); }

#tms-filter-box {
    background: #162e0e;
    border: 1px solid #3d6828;
    border-radius: 8px;
    padding: 8px;
    margin-bottom: 8px;
}
#tms-filter-box .tms-sb-section { margin-bottom: 6px; }
#tms-filter-box .tms-sb-section:last-of-type { margin-bottom: 8px; }
#tms-filter-box #tms-search-btn { margin-bottom: 5px; }
#tms-filter-box #tms-findall-btn { margin-bottom: 0; }

/* ─── Main content ─── */
#tms-main { flex: 1; min-width: 0; padding-left: 12px; position: relative; }
.tms-spacer { flex: 1; }
#tms-toolbar {
    position: absolute;
    top: 4px; right: 4px;
    z-index: 5;
    display: flex;
    align-items: center;
    gap: 4px;
    font-size: 11px;
    background: rgba(22,46,14,0.92);
    padding: 2px 8px;
    border-radius: 4px;
    pointer-events: none;
}
#tms-hits {
    font-size: 12px;
    font-weight: 800;
    color: #80e048;
    font-variant-numeric: tabular-nums;
}
#tms-toolbar .tms-toolbar-label {
    font-size: 11px;
    color: #6a9a58;
}

/* ─── Table ─── */
.tms-table-wrap { overflow-x: auto; border-radius: 8px; border: 1px solid #2a4a1c; }
.tms-table-wrap::-webkit-scrollbar { height: 4px; }
.tms-table-wrap::-webkit-scrollbar-track { background: #111; }
.tms-table-wrap::-webkit-scrollbar-thumb { background: #3d6828; border-radius: 2px; }

#tms-table {
    width: 100%;
    border-collapse: collapse;
    font-size: 11px;
    color: #c8e0b4;
}
#tms-table thead tr { border-bottom: 1px solid #2a4a1c;background: rgba(0,0,0,0.2); }
#tms-table th {
    background: #162e0e;
    color: #6a9a58;
    font-size: 10px;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.4px;
    padding: 6px 8px;
    white-space: nowrap;
    cursor: pointer;
    user-select: none;
    position: sticky;
    top: 0;
    z-index: 2;
    background: #162e0e;
}
#tms-table th:hover { color: #c8e0b4; background: #243d18; }
#tms-table th.sort-asc::after  { content: ' ▲'; color: #6cc040; }
#tms-table th.sort-desc::after { content: ' ▼'; color: #6cc040; }
#tms-table td {
    padding: 4px 7px;
    border-bottom: 1px solid rgba(42,74,28,.4);
    vertical-align: middle;
    white-space: nowrap;
}
#tms-table .tms-player-row { background: #1c3410; }
#tms-table tbody .tms-player-row:nth-child(odd)  { background: #1c3410; }
#tms-table tbody .tms-player-row:nth-child(even) { background: #162e0e; }
#tms-table .tms-player-row:hover { background: #243d18 !important; cursor: pointer; }
#tms-table .tms-player-row.tms-expanded { background: rgba(255,255,255,.07); }

/* Column-specific */
.tms-col-flag { width: 24px; text-align: center; }
.tms-col-name { max-width: 220px; overflow: hidden; text-overflow: ellipsis; }
.tms-col-name a { color: #80e048; text-decoration: none; font-weight: 600; }
.tms-col-name a:hover { color: #c8e0b4; text-decoration: underline; }
.tms-note-icon {
    display: inline-block;
    margin-left: 5px;
    font-size: 11px;
    cursor: default;
    opacity: 0.75;
    vertical-align: middle;
    position: relative;
}
.tms-note-icon:hover { opacity: 1; }
.tms-note-icon::after {
    content: attr(data-note);
    display: none;
    position: absolute;
    left: 50%;
    transform: translateX(-50%);
    top: calc(100% + 5px);
    background: #1a2e14;
    border: 1px solid #4a9030;
    border-radius: 5px;
    padding: 5px 8px;
    font-size: 11px;
    color: #c8e0b4;
    white-space: pre-wrap;
    max-width: 260px;
    min-width: 100px;
    word-break: break-word;
    z-index: 100002;
    box-shadow: 0 4px 14px rgba(0,0,0,0.6);
    pointer-events: none;
    line-height: 1.5;
}
.tms-note-icon:hover::after { display: block; }
.tms-col-age  { text-align: center; white-space: nowrap; }
.tms-col-r    { text-align: right; font-variant-numeric: tabular-nums; }
.tms-col-c    { text-align: center; }
.tms-age-y  { font-size: 13px; font-weight: 700; color: #e8f5d8; }
.tms-age-mo { font-size: 10px; color: #8aac72; margin-left: 1px; }
.tms-pos {
    font-size: 10px;
    font-weight: 700;
    padding: 1px 3px;
    border-radius: 3px;
    display: inline-block;
}
.tms-pos-chip {
    display: inline-block; padding: 1px 6px; border-radius: 4px;
    font-size: 10px; font-weight: 700; letter-spacing: 0.3px;
    line-height: 16px; text-align: center; min-width: 28px;
}
.tms-pos-bar { width: 3px; padding: 0 !important; border-radius: 2px; }
.tms-col-posbar { width: 4px; padding: 0 !important; }
.tms-rec {
    display: inline-block;
    padding: 1px 6px;
    border-radius: 8px;
    font-size: 10px;
    font-weight: 700;
}
.tms-bid-btn {
    padding: 3px 8px;
    border-radius: 3px;
    border: 1px solid #3d6828;
    background: rgba(61,104,40,0.25);
    color: #6cc040;
    font-size: 10px;
    font-weight: 700;
    cursor: pointer;
    transition: all 0.12s;
}
.tms-bid-btn:hover { background: #3d6828; color: #e8f5d8; }
.tms-reload-btn {
    padding: 2px 6px;
    border-radius: 3px;
    border: 1px solid #2a4a1c;
    background: transparent;
    color: #4a7a38;
    font-size: 13px;
    line-height: 1;
    cursor: pointer;
    transition: color 0.12s, border-color 0.12s;
    margin-right: 3px;
    vertical-align: middle;
}
.tms-reload-btn:hover { color: #6cc040; border-color: #4a8030; }
.tms-reload-btn.tms-reloading { animation: tms-spin 0.7s linear infinite; pointer-events: none; color: #6cc040; }

/* Pending tooltip indicator */
.tms-tip-pending {
    color: #4a5a40;
    font-size: 10px;
    animation: tms-pending-blink 1.2s ease-in-out infinite;
}
@keyframes tms-pending-blink { 0%,100% { opacity: 0.3; } 50% { opacity: 1; } }

/* Skill columns (skills mode) */
.tms-skill { text-align: center; padding: 4px 2px !important; }
.tms-skill0 { color: #4a5a40; font-size: 10px; }
.tms-bar-wrap { display: flex; align-items: center; gap: 3px; min-width: 38px; }
.tms-bar { height: 8px; border-radius: 2px; min-width: 2px; flex-shrink: 0; }
.tms-bar-wrap span { font-size: 10px; min-width: 12px; }

/* ─── Expanded row ─── */
tr.tms-expand-row td { padding: 12px 10px !important; background: #1c3410 !important; cursor: default; }
.tms-expand-inner { display: flex; gap: 20px; flex-wrap: wrap; }
.tms-expand-skills { flex: 1; min-width: 240px; }
.tms-expand-analysis { width: 215px; min-width: 190px; }
.tms-exp-head {
    font-size: 9px;
    font-weight: 700;
    color: #6a9a58;
    text-transform: uppercase;
    letter-spacing: 0.6px;
    margin-bottom: 8px;
}
.tms-skill-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 4px; }
.tms-skill-cell {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 2px;
    padding: 5px 2px;
    background: rgba(0,0,0,0.25);
    border-radius: 4px;
    border: 1px solid rgba(61,104,40,0.3);
}
.tms-sk-name { font-size: 9px; color: #6a9a58; text-transform: uppercase; }
.tms-sk-bar  { width: 100%; height: 5px; background: rgba(0,0,0,0.3); border-radius: 2px; overflow: hidden; }
.tms-sk-fill { height: 100%; border-radius: 2px; }
.tms-sk-val  { font-size: 12px; font-weight: 700; }
.tms-an-row {
    display: flex;
    justify-content: space-between;
    padding: 4px 0;
    border-bottom: 1px solid rgba(61,104,40,0.2);
    font-size: 11px;
}
.tms-an-row:last-child { border-bottom: none; }
.tms-an-lbl { color: #6a9a58; font-weight: 600; }
.tms-an-val { color: #c8e0b4; font-weight: 700; font-variant-numeric: tabular-nums; }

/* ─── Loading / empty ─── */
#tms-loading { text-align: center; padding: 50px 20px; color: #6a9a58; font-size: 13px; }
.tms-spinner {
    display: inline-block;
    width: 16px;
    height: 16px;
    border: 2px solid #3d6828;
    border-top-color: #6cc040;
    border-radius: 50%;
    animation: tms-spin 0.7s linear infinite;
    margin-right: 8px;
    vertical-align: middle;
}
@keyframes tms-spin { to { transform: rotate(360deg); } }

/* ─── Player row tooltip ─── */
.tms-player-tip {
    position: fixed; z-index: 100001;
    background: linear-gradient(135deg, #1a2e14 0%, #243a1a 100%);
    border: 1px solid #4a9030; border-radius: 8px;
    padding: 10px 12px; min-width: 220px; max-width: 280px;
    box-shadow: 0 6px 24px rgba(0,0,0,0.6);
    pointer-events: none; font-size: 11px; color: #c8e0b4;
    opacity: 0; transition: opacity .15s ease;
}
.tms-player-tip.visible { opacity: 1; }
.tms-player-tip-header {
    display: flex; align-items: flex-start; gap: 8px;
    margin-bottom: 8px; padding-bottom: 6px;
    border-bottom: 1px solid rgba(74,144,48,0.3);
}
.tms-player-tip-name { font-size: 13px; font-weight: 700; color: #e0f0cc; }
.tms-player-tip-pos { font-size: 10px; color: #8abc78; font-weight: 600; margin-top: 2px; }
.tms-player-tip-badges { display: flex; flex-direction: column; gap: 3px; margin-left: auto; align-items: flex-end; }
.tms-player-tip-badge { font-size: 10px; font-weight: 700; padding: 1px 5px; border-radius: 4px; background: rgba(0,0,0,0.3); }
.tms-player-tip-skills { display: flex; gap: 12px; margin-bottom: 6px; }
.tms-player-tip-skills-col { flex: 1; min-width: 0; }
.tms-player-tip-skill {
    display: flex; justify-content: space-between;
    padding: 1px 0; border-bottom: 1px solid rgba(74,144,48,0.12);
}
.tms-player-tip-skill-name { color: #8abc78; font-size: 10px; }
.tms-player-tip-skill-val { font-weight: 700; font-size: 11px; }
.tms-player-tip-footer {
    display: flex; gap: 6px; justify-content: center;
    padding-top: 6px; border-top: 1px solid rgba(74,144,48,0.3);
}
.tms-player-tip-stat { text-align: center; }
.tms-player-tip-stat-val { font-size: 13px; font-weight: 800; }
.tms-player-tip-stat-lbl { font-size: 9px; color: #6a9a58; text-transform: uppercase; letter-spacing: 0.3px; }

/* ─── Websocket-compatible watched rows ─── */
#tms-table tr.tms-bump td             { background: rgba(255,200,40,0.10) !important; }
#tms-table tr.tms-bump a              { color: #ffe680; }
#tms-table tr.watched-player td           { background: rgba(108,192,64,0.18) !important; }
#tms-table tr.watched-player-currentbid td{ background: rgba(0,220,110,0.25) !important; box-shadow: inset 0 0 0 1px #00e676; }
#tms-table tr.watched-player-outbid td   { background: rgba(255,60,40,0.2) !important; box-shadow: inset 0 0 0 1px #ff4c4c; }
#tms-table tr.watched-player a           { color: #e8f5d8; }

/* ─── Time cell ─── */
.tms-time-cell { position: relative; text-align: right; }
.tms-time-cell::after {
    content: '';
    background: url(/pics/ultra2/clock2.png) no-repeat center;
    background-size: contain;
    display: inline-block;
    width: 13px; height: 13px;
    vertical-align: text-bottom;
    margin-left: 2px;
}
.tms-time-cell .countdown-split-seconds,
.tms-time-cell .countdown-split-minutes,
.tms-time-cell .countdown-split-hours {
    width: 18px; text-align: left; padding-left: 2px;
}

/* ─── Hide TM's page content, our UI lives directly on body ─── */
#right_col, .column3_a, .column3_b, .column2_a { display: none !important; }
.column1_d{display: none !important;}
.main_center{padding-top: 0!important;padding-bottom: 0 !important;} 
/* ─── Our outer wrapper, full-width, directly on body ─── */
#tms-outer {
    display: block;
    width: calc(100% - 20px);
    max-width: 1400px;
    margin: 10px auto 0;
    font-family: Arial, sans-serif;
    font-size: 12px;
    color: #c8ddb8;
    box-sizing: border-box;
}
#tms-root { width: 100%; }

/* ─── Custom modal ─── */
#tms-modal-overlay {
    position: fixed; inset: 0; z-index: 200000;
    background: rgba(0,0,0,0.78);
    display: flex; align-items: center; justify-content: center;
    backdrop-filter: blur(3px);
}
.tms-modal {
    background: linear-gradient(160deg, #1a2e14 0%, #0e1e0a 100%);
    border: 1px solid #4a9030;
    border-radius: 12px;
    padding: 28px 24px 20px;
    max-width: 440px;
    width: calc(100% - 40px);
    box-shadow: 0 20px 60px rgba(0,0,0,0.9), 0 0 0 1px rgba(74,144,48,0.15);
    color: #c8e0b4;
    font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
}
.tms-modal-icon { font-size: 30px; margin-bottom: 10px; line-height: 1; }
.tms-modal-title { font-size: 15px; font-weight: 800; color: #e0f0cc; margin-bottom: 8px; }
.tms-modal-msg { font-size: 12px; color: #90b878; line-height: 1.65; margin-bottom: 22px; }
.tms-modal-btns { display: flex; flex-direction: column; gap: 8px; }
.tms-modal-btn {
    padding: 10px 16px; border-radius: 7px;
    font-size: 12px; font-weight: 700;
    cursor: pointer; border: none;
    transition: all 0.14s; font-family: inherit;
    text-align: left;
}
.tms-modal-btn-primary   { background: #3d6828; color: #e8f5d8; border: 1px solid #6cc040; }
.tms-modal-btn-primary:hover { background: #4d8030; }
.tms-modal-btn-secondary { background: rgba(61,104,40,0.15); color: #80c050; border: 1px solid #3d6828; }
.tms-modal-btn-secondary:hover { background: rgba(61,104,40,0.3); }
.tms-modal-btn-danger    { background: rgba(60,15,5,0.3); color: #a05040; border: 1px solid #5a2a1a; }
.tms-modal-btn-danger:hover { background: rgba(80,20,5,0.5); color: #c06050; }
.tms-modal-btn-sub { font-size: 10px; font-weight: 400; opacity: 0.7; display: block; margin-top: 2px; }

/* ─── Saved filters ─── */
.tms-filter-action-btn {
    flex: 1;
    padding: 5px 6px;
    border-radius: 5px;
    font-size: 10px;
    font-weight: 700;
    border: 1px solid rgba(61,104,40,0.45);
    background: rgba(0,0,0,0.15);
    color: #90b878;
    cursor: pointer;
    transition: all 0.12s;
    font-family: inherit;
}
.tms-filter-action-btn:hover { background: #2a4a1c; color: #c8e0b4; }
.tms-filter-action-btn.tms-filter-del { color: #a05040; border-color: rgba(90,42,26,0.45); }
.tms-filter-action-btn.tms-filter-del:hover { background: rgba(80,20,5,0.4); color: #c06050; }
        `;
        const el = document.createElement('style');
        el.id = 'tms-style';
        el.textContent = css;
        document.head.appendChild(el);
    }

    // ═══════════════════════════════════════════════════════════════════
    //  SIDEBAR
    // ═══════════════════════════════════════════════════════════════════

    function optRange(from, to, def) {
        let s = '';
        for (let i = from; i <= to; i++) s += `<option value="${i}"${i === def ? ' selected' : ''}>${i}</option>`;
        return s;
    }

    function optList(arr, def) {
        return arr.map(([v, l]) => `<option value="${v}"${v === def ? ' selected' : ''}>${l}</option>`).join('');
    }

    function skillSelectOpts(withNone = true) {
        const combined = [...OUTFIELD_SKILLS, ...GK_SKILLS.filter(s => !OUTFIELD_SKILLS.includes(s))];
        let s = withNone ? '<option value="0">—</option>' : '';
        for (const sk of combined) s += `<option value="${sk}">${SKILL_NAMES[sk]}</option>`;
        return s;
    }

    function decRecToTM(val) {
        return Math.min(10, Math.max(0, Math.floor(parseFloat(val) * 2)));
    }

    function buildSidebar() {
        const valOpts = `<option value="0">≥</option>${[...Array(20)].map((_,i)=>`<option value="${i+1}">${i+1}</option>`).join('')}`;

        return `
<div id="tms-sidebar">
  <div id="tms-filter-box">
  <div class="tms-sb-section">
    <div class="tms-sb-head">Age Range
      <label class="tms-for-inline"><input type="checkbox" id="tms-for" checked /> Foreigners</label>
    </div>
    <div class="tms-sb-body">
      <div class="tms-range-row">
        <input type="number" id="tms-amin" class="tms-sel tms-num" min="18" max="37" value="18" placeholder="Min" />
        <span class="tms-range-sep">–</span>
        <input type="number" id="tms-amax" class="tms-sel tms-num" min="18" max="37" value="37" placeholder="Max" />
      </div>
    </div>
  </div>

  <div class="tms-sb-section">
    <div class="tms-sb-head">Recommendation</div>
    <div class="tms-sb-body">
      <div class="tms-range-row">
        <input type="number" id="tms-rmin" class="tms-sel tms-num" min="0" max="5" step="0.01" value="0" placeholder="Min" />
        <span class="tms-range-sep">–</span>
        <input type="number" id="tms-rmax" class="tms-sel tms-num" min="0" max="5" step="0.01" value="5" placeholder="Max" />
      </div>
    </div>
  </div>

  <div class="tms-sb-section">
    <div class="tms-sb-head">R5 <span class="tms-post-note">post-filter</span></div>
    <div class="tms-sb-body">
      <div class="tms-range-row">
        <input type="number" id="tms-r5min" class="tms-sel tms-num" min="0" max="200" step="0.1" placeholder="Min" />
        <span class="tms-range-sep">–</span>
        <input type="number" id="tms-r5max" class="tms-sel tms-num" min="0" max="200" step="0.1" placeholder="Max" />
      </div>
    </div>
  </div>

  <div class="tms-sb-section">
    <div class="tms-sb-head">TI <span class="tms-post-note">post-filter</span></div>
    <div class="tms-sb-body">
      <div class="tms-range-row">
        <input type="number" id="tms-timin" class="tms-sel tms-num" min="-100" max="200" step="0.1" placeholder="Min" />
        <span class="tms-range-sep">–</span>
        <input type="number" id="tms-timax" class="tms-sel tms-num" min="-100" max="200" step="0.1" placeholder="Max" />
      </div>
    </div>
  </div>

  <div class="tms-sb-section">
    <div class="tms-sb-body">
      <div class="tms-pos-formation">
        <div class="tms-pos-formation-empty"></div>
        <div class="tms-filter-btn tms-gk" data-fp="gk">GK</div>
        <div class="tms-pos-formation-empty"></div>
        <div class="tms-filter-btn tms-de" data-fp="dl">DL</div>
        <div class="tms-filter-btn tms-de" data-fp="dc">DC</div>
        <div class="tms-filter-btn tms-de" data-fp="dr">DR</div>
        <div class="tms-filter-btn tms-dm" data-fp="dml">DML</div>
        <div class="tms-filter-btn tms-dm" data-fp="dmc">DMC</div>
        <div class="tms-filter-btn tms-dm" data-fp="dmr">DMR</div>
        <div class="tms-filter-btn tms-mf" data-fp="ml">ML</div>
        <div class="tms-filter-btn tms-mf" data-fp="mc">MC</div>
        <div class="tms-filter-btn tms-mf" data-fp="mr">MR</div>
        <div class="tms-filter-btn tms-om" data-fp="oml">OML</div>
        <div class="tms-filter-btn tms-om" data-fp="omc">OMC</div>
        <div class="tms-filter-btn tms-om" data-fp="omr">OMR</div>
        <div class="tms-pos-formation-empty"></div>
        <div class="tms-filter-btn tms-fw" data-fp="fc">FC</div>
        <div class="tms-pos-formation-empty"></div>
      </div>
    </div>
  </div>

  <button id="tms-search-btn">🔍 Search 100</button>
  <button id="tms-findall-btn">⬇️ Find All</button>
  <div class="tms-sb-section" style="margin-top:6px">
    <div class="tms-sb-head">Saved Filters</div>
    <div class="tms-sb-body">
      <select id="tms-saved-filters-sel" class="tms-sel" style="width:100%;margin-bottom:6px"><option value="">— no saved filters —</option></select>
      <div style="display:flex;gap:4px">
        <button id="tms-filter-load-btn" class="tms-filter-action-btn">📂 Load</button>
        <button id="tms-filter-save-btn" class="tms-filter-action-btn" style="flex:2">💾 Save Current</button>
        <button id="tms-filter-del-btn" class="tms-filter-action-btn tms-filter-del">🗑</button>
      </div>
    </div>
  </div>
  <button class="tms-more-toggle" id="tms-more-toggle"><span>More Filters</span><span class="tms-more-arrow">▼</span></button>
  <div class="tms-more-body" id="tms-more-body">
    <div class="tms-sb-section">
      <div class="tms-sb-head">Max Price</div>
      <div class="tms-sb-body">
        <div class="tms-row">
          <select id="tms-cost" class="tms-sel">
            <option value="0" selected>Any</option>
            <option value="aff">Affordable</option>
            <option value="5">5 Mil</option>
            <option value="25">25 Mil</option>
            <option value="50">50 Mil</option>
            <option value="100">100 Mil</option>
            <option value="250">250 Mil</option>
            <option value="500">500 Mil</option>
          </select>
        </div>
      </div>
    </div>

    <div class="tms-sb-section">
      <div class="tms-sb-head">Time Left</div>
      <div class="tms-sb-body">
        <div class="tms-row">
          <select id="tms-time" class="tms-sel">
            <option value="0" selected>Any</option>
            <option value="1">15 Minutes</option>
            <option value="2">1 Hour</option>
            <option value="3">6 Hours</option>
            <option value="4">1 Day</option>
            <option value="5">2 Days</option>
            <option value="6">4 Days</option>
          </select>
        </div>
      </div>
    </div>

    <div class="tms-sb-section">
      <div class="tms-sb-head">Skill Filters</div>
      <div class="tms-sb-body">
        <div class="tms-skill-row">
          <select class="tms-sel" id="tms-sf-s0">${skillSelectOpts()}</select>
          <select class="tms-sel" id="tms-sf-v0" style="width:46px">${valOpts}</select>
        </div>
        <div class="tms-skill-row">
          <select class="tms-sel" id="tms-sf-s1">${skillSelectOpts()}</select>
          <select class="tms-sel" id="tms-sf-v1" style="width:46px">${valOpts}</select>
        </div>
        <div class="tms-skill-row">
          <select class="tms-sel" id="tms-sf-s2">${skillSelectOpts()}</select>
          <select class="tms-sel" id="tms-sf-v2" style="width:46px">${valOpts}</select>
        </div>
      </div>
    </div>
  </div>
  </div>

</div>`;
    }

    // ═══════════════════════════════════════════════════════════════════
    //  TABLE RENDER
    // ═══════════════════════════════════════════════════════════════════

    const BREAKDOWN_COLS = [
        { key: 'posbar', label: '',     sort: false, cls: 'tms-col-posbar' },
        { key: 'flag',   label: '',     sort: false, cls: 'tms-col-flag' },
        { key: 'name',   label: 'Name', sort: true,  cls: 'tms-col-name' },
        { key: 'age',    label: 'Age',  sort: true,  cls: 'tms-col-age' },
        { key: 'fp',     label: 'Pos',  sort: false, cls: 'tms-col-c' },
        { key: 'r5',     label: 'R5',   sort: true,  cls: 'tms-col-r' },
        { key: 'rec',    label: 'Rec',  sort: true,  cls: 'tms-col-c' },
        { key: 'ti',     label: 'TI',   sort: true,  cls: 'tms-col-r' },
        { key: 'asi',    label: 'ASI',  sort: true,  cls: 'tms-col-r' },
        { key: 'bid',    label: 'Bid',  sort: true,  cls: 'tms-col-r' },
        { key: 'time',   label: 'Time', sort: true,  cls: 'tms-col-r' },
        { key: 'act',    label: '',     sort: false, cls: '' },
    ];

    function thSortClass(key) {
        if (sortKey !== key) return '';
        return sortAsc ? ' sort-asc' : ' sort-desc';
    }

    function buildBidBtn(p) {
        const nameJs = (p.name_js || p.name || '').replace(/\\/g, '\\\\').replace(/'/g, "\\'");
        const fetched = tooltipCache[p.id] && !tooltipCache[p.id].estimated;
        const reloadBtn = fetched ? '' : `<button class="tms-reload-btn" data-pid="${p.id}" title="Fetch stats">↻</button>`;
        return `${reloadBtn}<button class="tms-bid-btn" onclick="event.stopPropagation();tlpop_pop_transfer_bid('${p.next_bid||0}',${p.pro||0},'${p.id}','${nameJs}')" title="Place Bid">Bid</button>`;
    }

    function buildPlayerRow(p) {
        const nameLink = `<a href="/players/${p.id}/" target="_blank" onclick="event.stopPropagation()">${p.name || p.id}</a>`;

        const timeId  = `tms-td-${p.id}`;
        const timeTd  = p.time > 0
            ? `<span id="${timeId}" class="tms-time-cell"></span>`
            : '—';

        const bidCls  = `bid_${p.id}`;

        const cachedTip = tooltipCache[p.id];
        const recHtml = cachedTip
            ? fmtRec(cachedTip.recCalc != null ? cachedTip.recCalc : cachedTip.recSort)
            : fmtRec(p.rec);

        const barClr = p.fp && p.fp.length
            ? (() => {
                const str = p.fp[0];
                if (str === 'gk') return '#4ade80';
                const pos = str.replace(/[lcrk]$/, '');
                return POS_COLOR[pos] || POS_COLOR[str] || '#4a5a40';
              })()
            : '#4a5a40';

        const noteIcon = p.txt ? `<span class="tms-note-icon" data-note="${p.txt.replace(/"/g, '&quot;')}">📋</span>` : '';

        return `<tr class="tms-player-row${p.bump ? ' tms-bump' : ''}" id="player_row_${p.id}" data-pid="${p.id}">
  <td class="tms-pos-bar" style="background:${barClr}"></td>
  <td class="tms-col-flag">${p.flag || ''}</td>
  <td class="tms-col-name">${nameLink}</td>
  <td class="tms-col-age">${fmtAge(p.age)}</td>
  <td class="tms-col-c">${fmtPos(p.fp)}</td>
  <td class="tms-col-r" id="tms-r5-${p.id}">${
    cachedTip && cachedTip.r5 != null ? fmtR5(cachedTip.r5)
    : cachedTip && (cachedTip.r5Lo != null || cachedTip.r5Hi != null) ? fmtR5Range(cachedTip.r5Lo, cachedTip.r5Hi)
    : '<span class="tms-tip-pending">…</span>'
  }</td>
  <td class="tms-col-c" id="tms-rec-${p.id}">${recHtml}</td>
  <td class="tms-col-r" id="tms-ti-${p.id}">${cachedTip ? tiHtml(cachedTip.ti) : '<span class="tms-tip-pending">…</span>'}</td>
  <td class="tms-col-r" style="color:#e0f0cc">${p.asi ? fmtNum(p.asi) : '—'}</td>
  <td class="tms-col-r ${bidCls}">${fmtNum(p.bid) || '—'}</td>
  <td class="tms-col-r">${timeTd}</td>
  <td>${buildBidBtn(p)}${noteIcon}</td>
</tr>`;
    }

    function buildExpandRow(p, colCount) {
        const gk     = p._gk;
        const skills = gk ? GK_SKILLS : OUTFIELD_SKILLS;
        const ss     = p._ss;
        const ageP   = p._ageP;
        const tip    = tooltipCache[p.id];

        const skillCells = skills.map(s => {
            // Prefer tooltip integer skills over star ratings from transfer list
            const val = (tip && tip.skills && tip.skills[s] != null) ? tip.skills[s] : (p[s] || 0);
            const pct = (val / 20) * 100;
            const clr = skillColor(val);
            return `<div class="tms-skill-cell">
  <span class="tms-sk-name">${SKILL_NAMES[s]}</span>
  <div class="tms-sk-bar"><div class="tms-sk-fill" style="width:${pct}%;background:${clr}"></div></div>
  <span class="tms-sk-val" style="color:${clr}">${val || '—'}</span>
</div>`;
        }).join('');

        const bidN    = fmtNum(p.bid);
        const recDisp = tip
            ? fmtRec(tip.recCalc != null ? tip.recCalc : tip.recSort)
            : fmtRec(p.rec);
        const r5Disp  = tip
            ? (tip.r5 != null ? fmtR5(tip.r5) : fmtR5Range(tip.r5Lo, tip.r5Hi))
            : '<span style="color:#4a5a40">Loading…</span>';
        const tiDisp  = tip ? tiHtml(tip.ti) : '<span style="color:#4a5a40">Loading…</span>';

        const skillNote = tip ? `(from tooltip)` : `(transfer list stars)`;

        return `<tr class="tms-expand-row">
  <td colspan="${colCount}">
    <div class="tms-expand-inner">
      <div class="tms-expand-skills">
        <div class="tms-exp-head">Skills — ${ss.count}/${ss.total} scouted &nbsp;<span style="font-weight:400;color:#4a5a40">${skillNote}</span></div>
        <div class="tms-skill-grid">${skillCells}</div>
      </div>
      <div class="tms-expand-analysis">
        <div class="tms-exp-head">Analysis</div>
        <div class="tms-an-row"><span class="tms-an-lbl">Age</span><span class="tms-an-val">${ageP.years}.${ageP.months}</span></div>
        <div class="tms-an-row"><span class="tms-an-lbl">ASI</span><span class="tms-an-val">${p.asi ? fmtNum(p.asi) : '—'}</span></div>
        <div class="tms-an-row"><span class="tms-an-lbl">Rec</span><span class="tms-an-val">${recDisp}</span></div>
        <div class="tms-an-row"><span class="tms-an-lbl">R5</span><span class="tms-an-val">${r5Disp}</span></div>
        <div class="tms-an-row"><span class="tms-an-lbl">TI / session</span><span class="tms-an-val">${tiDisp}</span></div>
        <div class="tms-an-row"><span class="tms-an-lbl">Current Bid</span><span class="tms-an-val">${bidN}</span></div>
        <div class="tms-an-row"><span class="tms-an-lbl">Position</span><span class="tms-an-val">${(p.fp || []).join(', ')}</span></div>
        <div class="tms-an-row"><span class="tms-an-lbl">Type</span><span class="tms-an-val">${gk ? 'Goalkeeper' : 'Outfield'}</span></div>
      </div>
    </div>
  </td>
</tr>`;
    }

    // ═══════════════════════════════════════════════════════════════════
    //  FILTER + SORT + REFRESH
    // ═══════════════════════════════════════════════════════════════════

    function getPostFilters() {
        const r5min = $('#tms-r5min').val(); const r5max = $('#tms-r5max').val();
        const timin = $('#tms-timin').val(); const timax = $('#tms-timax').val();
        return {
            r5min: r5min !== '' ? parseFloat(r5min) : null,
            r5max: r5max !== '' ? parseFloat(r5max) : null,
            timin: timin !== '' ? parseFloat(timin) : null,
            timax: timax !== '' ? parseFloat(timax) : null,
        };
    }

    function passesPostFilters(p, pf) {
        const tip = tooltipCache[p.id];
        if (!tip) return true; // no tooltip yet — show until we know
        if (tip.r5 != null) {
            if (pf.r5min !== null && tip.r5 < pf.r5min) return false;
            if (pf.r5max !== null && tip.r5 > pf.r5max) return false;
        } else {
            if (pf.r5min !== null && tip.r5Hi != null && tip.r5Hi < pf.r5min) return false;
            if (pf.r5max !== null && tip.r5Lo != null && tip.r5Lo > pf.r5max) return false;
        }
        if (pf.timin !== null && tip.ti != null && tip.ti < pf.timin) return false;
        if (pf.timax !== null && tip.ti != null && tip.ti > pf.timax) return false;
        return true;
    }

    function getVisible() {
        const pf = getPostFilters();
        let arr = allPlayers.filter(p => passesPostFilters(p, pf));
        arr.sort((a, b) => {
            // Bump players always float to the top
            if (a.bump && !b.bump) return -1;
            if (!a.bump && b.bump) return 1;
            let av, bv;
            switch (sortKey) {
                case 'name':  av = (a.name||'').toLowerCase(); bv = (b.name||'').toLowerCase(); break;
                case 'age':   av = a.age||0;    bv = b.age||0;    break;
                case 'rec': {
                    const ta = tooltipCache[a.id]; const tb = tooltipCache[b.id];
                    av = ta && ta.recCalc != null ? ta.recCalc : (ta && ta.recSort != null ? ta.recSort : (a.rec||0));
                    bv = tb && tb.recCalc != null ? tb.recCalc : (tb && tb.recSort != null ? tb.recSort : (b.rec||0));
                    break;
                }
                case 'r5': {
                    const ta = tooltipCache[a.id]; const tb = tooltipCache[b.id];
                    av = ta ? (ta.r5 != null ? ta.r5 : (ta.r5Hi != null ? ta.r5Hi : -9999)) : -9999;
                    bv = tb ? (tb.r5 != null ? tb.r5 : (tb.r5Hi != null ? tb.r5Hi : -9999)) : -9999;
                    break;
                }
                case 'ti': {
                    const ta = tooltipCache[a.id]; const tb = tooltipCache[b.id];
                    av = ta && ta.ti != null ? ta.ti : -9999;
                    bv = tb && tb.ti != null ? tb.ti : -9999;
                    break;
                }
                case 'asi':   av = a.asi||0;    bv = b.asi||0;    break;
                case 'bid':   av = a.bid||0;    bv = b.bid||0;    break;
                default:
                case 'time':  av = a.time||0;   bv = b.time||0;   break;
            }
            if (typeof av === 'string') return sortAsc ? av.localeCompare(bv) : bv.localeCompare(av);
            return sortAsc ? av - bv : bv - av;
        });

        return arr;
    }

    function startCountdowns(arr) {
        arr.forEach(p => {
            if (!p.time || p.time <= 0) return;
            const $span = $(`#tms-td-${p.id}`);
            if (!$span.length) return;

            // Reuse existing countdown if alive
            if (window.countDowns && window.countDowns[p.id]) {
                try { $span.html(window.countDowns[p.id].getJQ()); return; } catch(e) {}
            }

            if (!window.Countdown) { $span.text(p.time + 's'); return; }

            const cd = new window.Countdown(p.time, '', 'highest', true, (cntdwn) => {
                if (!cntdwn || !cntdwn.getJQ) return;
                const $row = cntdwn.getJQ().closest('tr');
                if (!$row.length) return;
                if ($row.hasClass('watched-player-currentbid')) {
                    $row.find('.tms-time-cell').text('Won').removeClass('tms-time-cell');
                } else if ($row.hasClass('watched-player-outbid')) {
                    $row.find('.tms-time-cell').text('Lost').removeClass('tms-time-cell');
                } else {
                    $row.find('td').css({ color: '#ff3636' });
                    $row.find('.tms-bid-btn').remove();
                    cntdwn.getJQ().closest('td').html('—');
                    setTimeout(() => $row.fadeOut(800, () => $row.remove()), 3000);
                }
            });

            if (window.countDowns) window.countDowns[p.id] = cd;
            try { $span.html(cd.getJQ()); } catch(e) { $span.text(p.time + 's'); }
        });
    }

    function removePlayerTip() {
        clearTimeout(playerTipTimer);
        if (playerTipEl) { playerTipEl.remove(); playerTipEl = null; }
    }

    function buildPlayerTip(p) {
        const tip = tooltipCache[p.id];
        const gk = p._gk;
        const skillList = gk ? GK_SKILLS : OUTFIELD_SKILLS;
        const SKILL_LONG = {
            str:'Strength', sta:'Stamina',   pac:'Pace',      mar:'Marking',    tac:'Tackling',
            wor:'Workrate', pos:'Positioning',pas:'Passing',   cro:'Crossing',   tec:'Technique',
            hea:'Heading',  fin:'Finishing',  lon:'Longshots', set:'Set Pieces',
            han:'Handling', one:'One on ones',ref:'Reflexes',  ari:'Aerial',
            jum:'Jumping',  com:'Communication', kic:'Kicking', thr:'Throwing',
        };
        const ageP   = p._ageP;
        const recVal = tip ? (tip.recCalc != null ? tip.recCalc : tip.recSort) : null;
        const r5     = tip ? tip.r5   : null;
        const r5Lo   = tip ? tip.r5Lo : null;
        const r5Hi   = tip ? tip.r5Hi : null;
        const ti     = tip ? tip.ti   : null;

        const posStr = (p.fp && p.fp.length)
            ? p.fp.map(s => {
                const side = s.slice(-1);
                const base = s.slice(0, s.length - 1);
                const sl   = { l:'L', c:'C', r:'R', k:'' }[side] || '';
                return s === 'gk' ? 'GK' : (base.toUpperCase() + sl);
              }).join(', ')
            : '—';

        let h = '<div class="tms-player-tip-header">';
        h += `<div><div class="tms-player-tip-name">${p.name || p.id}</div>`;
        h += `<div class="tms-player-tip-pos">${posStr} · Age ${ageP.years}.${String(ageP.months).padStart(2,'0')}</div></div>`;
        h += '<div class="tms-player-tip-badges">';
        if (r5 != null)     h += `<span class="tms-player-tip-badge" style="color:${getColor(r5, R5_THRESHOLDS)}">R5 ${r5.toFixed(1)}</span>`;
        else if (r5Hi != null) h += `<span class="tms-player-tip-badge" style="color:${getColor(r5Hi, R5_THRESHOLDS)}">R5 ${r5Lo != null && r5Lo.toFixed(1) !== r5Hi.toFixed(1) ? r5Lo.toFixed(1) + '–' : ''}${r5Hi.toFixed(1)}</span>`;
        if (recVal != null) h += `<span class="tms-player-tip-badge" style="color:${getColor(recVal, REC_THRESHOLDS)}">Rec ${recVal.toFixed(2)}</span>`;
        h += '</div></div>';

        const leftIdx  = gk ? [0,1,2,7]        : [0,1,2,3,4,5,6];
        const rightIdx = gk ? [3,4,5,6,8,9,10] : [7,8,9,10,11,12,13];
        const renderCol = (indices) => {
            let c = '<div class="tms-player-tip-skills-col">';
            indices.forEach(i => {
                const key = skillList[i];
                if (!key) return;
                const val = (tip && tip.skills && tip.skills[key] != null) ? tip.skills[key] : null;
                const clr  = val != null ? skillColor(val) : '#4a5a40';
                const disp = val == null ? '—' : (val >= 20 ? '★' : String(val));
                c += '<div class="tms-player-tip-skill">';
                c += `<span class="tms-player-tip-skill-name">${SKILL_LONG[key] || key}</span>`;
                c += `<span class="tms-player-tip-skill-val" style="color:${clr}">${disp}</span>`;
                c += '</div>';
            });
            c += '</div>';
            return c;
        };
        h += '<div class="tms-player-tip-skills">';
        h += renderCol(leftIdx);
        h += renderCol(rightIdx);
        h += '</div>';

        h += '<div class="tms-player-tip-footer">';
        const r5FooterVal = r5 != null ? r5 : r5Hi;
        const r5FooterDisp = r5 != null ? r5.toFixed(1)
            : (r5Hi != null ? (r5Lo != null && r5Lo.toFixed(1) !== r5Hi.toFixed(1) ? r5Lo.toFixed(1) + '–' + r5Hi.toFixed(1) : r5Hi.toFixed(1)) : '…');
        h += `<div class="tms-player-tip-stat"><div class="tms-player-tip-stat-val" style="color:${r5FooterVal != null ? getColor(r5FooterVal, R5_THRESHOLDS) : '#6a9a58'}">${r5FooterDisp}</div><div class="tms-player-tip-stat-lbl">R5</div></div>`;
        h += `<div class="tms-player-tip-stat"><div class="tms-player-tip-stat-val" style="color:${recVal != null ? getColor(recVal, REC_THRESHOLDS) : '#6a9a58'}">${recVal != null ? recVal.toFixed(2) : '…'}</div><div class="tms-player-tip-stat-lbl">Rec</div></div>`;
        h += `<div class="tms-player-tip-stat"><div class="tms-player-tip-stat-val" style="color:${ti != null ? getColor(ti, TI_THRESHOLDS) : '#6a9a58'}">${ti != null ? ti.toFixed(1) : '…'}</div><div class="tms-player-tip-stat-lbl">TI</div></div>`;
        h += `<div class="tms-player-tip-stat"><div class="tms-player-tip-stat-val" style="color:#e0f0cc">${p.asi ? fmtNum(p.asi) : '—'}</div><div class="tms-player-tip-stat-lbl">ASI</div></div>`;
        h += `<div class="tms-player-tip-stat"><div class="tms-player-tip-stat-val" style="color:#c8e0b4">${fmtNum(p.bid) || '—'}</div><div class="tms-player-tip-stat-lbl">Bid</div></div>`;
        h += '</div>';
        if (p.txt) h += `<div style="margin-top:7px;padding-top:6px;border-top:1px solid rgba(74,144,48,0.25);font-size:10px;color:#90b878;line-height:1.5">📋 ${p.txt}</div>`;
        return h;
    }

    function refreshDisplay() {
        const $wrap = $('#tms-table-wrap');
        if (!$wrap.length) return;

        const arr = getVisible();
        $('#tms-hits').text(arr.length);

        if (!arr.length) {
            $wrap.html('<div id="tms-loading">No players found. Try adjusting your filters.</div>');
            return;
        }

        let html;
        if (skillsMode) {
            const colCount = 5 + ALL_SKILLS.length + 2;
            const thSkills = ALL_SKILLS.map(s => `<th>${SKILL_NAMES[s]}</th>`).join('');
            html = `<div class="tms-table-wrap"><table id="tms-table">
<thead><tr>
  <th class="tms-col-posbar"></th>
  <th class="tms-col-flag"></th>
  <th data-sort="name" class="${thSortClass('name')}">Name</th>
  <th data-sort="age"  class="${thSortClass('age')}">Age</th>
  <th>Pos</th>
  ${thSkills}
  <th data-sort="time" class="${thSortClass('time')}">Time</th>
  <th></th>
</tr></thead>
</table></div>`;
            $wrap.html(html);
            startCountdowns(arr);
        } else {
            const thCols = BREAKDOWN_COLS.map(c => {
                if (!c.sort) return `<th class="${c.cls||''}">${c.label}</th>`;
                return `<th data-sort="${c.key}" class="${c.cls||''} ${thSortClass(c.key)}">${c.label}</th>`;
            }).join('');
            html = `<div class="tms-table-wrap"><table id="tms-table">
<thead><tr>${thCols}</tr></thead>
<tbody>${arr.map(p => buildPlayerRow(p)).join('')}</tbody>
</table></div>`;
            $wrap.html(html);
            startCountdowns(arr);
        }

        // Sort headers
        $('#tms-table th[data-sort]').on('click', function () {
            const k = $(this).data('sort');
            if (sortKey === k) sortAsc = !sortAsc;
            else { sortKey = k; sortAsc = (k === 'time'); }
            expandedId = null;
            refreshDisplay();
        });

        // Row hover tooltip
        removePlayerTip();
        $('#tms-table tbody')
            .on('mouseenter', '.tms-player-row', function () {
                const pid = $(this).data('pid');
                const player = allPlayers.find(x => x.id == pid);
                if (!player) return;
                removePlayerTip();
                playerTipEl = $('<div class="tms-player-tip"></div>').appendTo('body');
                const $tip = playerTipEl;
                $tip.html(buildPlayerTip(player));
                const nameCell = this.querySelector('.tms-col-name') || this;
                const rect = nameCell.getBoundingClientRect();
                $tip.css({ left: rect.left + 'px', top: (rect.bottom + 6) + 'px' });
                playerTipTimer = setTimeout(() => $tip.addClass('visible'), 50);
                requestAnimationFrame(() => {
                    if (!playerTipEl) return;
                    const tr = $tip[0].getBoundingClientRect();
                    if (tr.right > window.innerWidth - 10)
                        $tip.css('left', Math.max(4, window.innerWidth - tr.width - 10) + 'px');
                    if (tr.bottom > window.innerHeight - 10)
                        $tip.css('top', Math.max(4, rect.top - tr.height - 6) + 'px');
                });
                // Fetch tooltip on hover if not yet loaded
                const cachedTip = tooltipCache[pid];
                if (!cachedTip || cachedTip.estimated) {
                    fetchOnePlayer(player).then(() => {
                        if (playerTipEl === $tip) $tip.html(buildPlayerTip(player));
                    });
                }
            })
            .on('mouseleave', '.tms-player-row', removePlayerTip);

        // Compatibility hooks
        if (typeof window.make_highlighted_rows === 'function') {
            try { window.make_highlighted_rows(); } catch(e) {}
        }
    }

    // ═══════════════════════════════════════════════════════════════════
    //  TOOLTIP BACKGROUND FETCHER
    // ═══════════════════════════════════════════════════════════════════

    function updateTooltipCells(pid, tip) {
        const recVal = tip.recCalc != null ? tip.recCalc : tip.recSort;

        // Post-filters: remove rows that no longer pass after tooltip data arrives
        const pf = getPostFilters();
        const failsRec = (() => {
            if (recVal == null) return false;
            const decMin = parseFloat($('#tms-rmin').val()) || 0;
            const decMax = parseFloat($('#tms-rmax').val());
            const maxVal = isNaN(decMax) ? 5 : decMax;
            return (decMin > 0 || maxVal < 5) && (recVal < decMin || recVal > maxVal);
        })();
        const failsR5 = tip.r5 != null
            ? ((pf.r5min !== null && tip.r5 < pf.r5min) || (pf.r5max !== null && tip.r5 > pf.r5max))
            : ((pf.r5min !== null && tip.r5Hi != null && tip.r5Hi < pf.r5min) ||
               (pf.r5max !== null && tip.r5Lo != null && tip.r5Lo > pf.r5max));
        const failsTI = (pf.timin !== null && tip.ti != null && tip.ti < pf.timin) ||
                        (pf.timax !== null && tip.ti != null && tip.ti > pf.timax);
        if (failsRec || failsR5 || failsTI) {
            const $row = $(`#player_row_${pid}`);
            $row.next('.tms-expand-row').remove();
            $row.remove();
            allPlayers = allPlayers.filter(p => String(p.id) !== String(pid));
            const parts = ($('#tms-hits').text() || '').split('/');
            const shown = parseInt(parts[0]) || 0;
            const total = parseInt(parts[1]) || 0;
            $('#tms-hits').text((shown > 0 ? shown - 1 : 0) + ' / ' + (total > 0 ? total - 1 : 0));
            return;
        }

        // Remove reload button now that we have full data
        $(`#player_row_${pid} .tms-reload-btn`).remove();

        const $rec = $(`#tms-rec-${pid}`);
        if ($rec.length && recVal != null) $rec.html(fmtRec(recVal));

        const $r5 = $(`#tms-r5-${pid}`);
        if ($r5.length) {
            if (tip.r5 != null) $r5.html(fmtR5(tip.r5));
            else if (tip.r5Lo != null || tip.r5Hi != null) $r5.html(fmtR5Range(tip.r5Lo, tip.r5Hi));
        }

        const $ti = $(`#tms-ti-${pid}`);
        if ($ti.length) $ti.html(tiHtml(tip.ti));

        // Refresh open expand row for this player (legacy — no-op when no expand rows)
        const $expRow = $(`#player_row_${pid}`).next('.tms-expand-row');
        if ($expRow.length) {
            const player = allPlayers.find(x => x.id == pid);
            if (player) {
                const colCount = skillsMode ? (5 + ALL_SKILLS.length + 2) : BREAKDOWN_COLS.length;
                $expRow.replaceWith(buildExpandRow(player, colCount));
            }
        }

        // Refresh hover tooltip if it's showing this player
        if (playerTipEl) {
            const $hovRow = $(`#player_row_${pid}`);
            if ($hovRow.length && $hovRow.is(':hover')) {
                const player = allPlayers.find(x => x.id == pid);
                if (player) playerTipEl.html(buildPlayerTip(player));
            }
        }
    }

    async function fetchOnePlayer(p) {
            const data = await new Promise(resolve => {
                $.post('/ajax/tooltip.ajax.php', { player_id: p.id }, res => {
                    try { resolve(typeof res === 'object' ? res : JSON.parse(res)); }
                    catch(e) { resolve(null); }
                }).fail(() => resolve(null));
            });
            if (!data || !data.player) return;

            const tp = data.player;

            // rec_sort (decimal recommendation)
            const recSort = tp.rec_sort !== undefined ? parseFloat(tp.rec_sort) : null;

            // TI from ASI + wage
            const wageNum = parseInt((tp.wage || '').toString().replace(/[^0-9]/g, '')) || 0;
            const asiNum  = p.asi || parseInt((tp.asi || tp.skill_index || '').toString().replace(/[^0-9]/g, '')) || 0;
            const favpos  = tp.favposition || '';
            const isGK    = favpos.split(',')[0].toLowerCase() === 'gk';
            let ti = null;
            if (asiNum && wageNum) {
                const tiRaw = calculateTI(asiNum, wageNum, isGK);
                if (tiRaw !== null && CURRENT_SESSION > 0) {
                    ti = Number((tiRaw / CURRENT_SESSION).toFixed(1));
                }
            }

            // Integer skills from tooltip
            let skills = null;
            if (tp.skills && Array.isArray(tp.skills)) {
                skills = {};
                for (const sk of tp.skills) {
                    const key = SKILL_NAME_TO_KEY[sk.name];
                    if (!key) continue;
                    const v = sk.value;
                    if (typeof v === 'string') {
                        if (v.includes('star_silver')) skills[key] = 19;
                        else if (v.includes('star'))   skills[key] = 20;
                        else skills[key] = parseInt(v) || 0;
                    } else {
                        skills[key] = parseInt(v) || 0;
                    }
                }
            }

            // Exact routine from tooltip (null if not scouted)
            const tooltipRoutine = tp.routine != null ? parseFloat(tp.routine) : null;

            // Compute R5 with exact skills + real routine; keep lo-hi range as fallback
            let recCalc = null, r5 = null, r5Lo = null, r5Hi = null;
            if (skills && asiNum) {
                const positions = favpos.split(',').map(s => s.trim()).filter(Boolean);
                if (positions.length) {
                    const ageYears = p._ageP ? p._ageP.years : Math.floor(parseFloat(p.age) || 20);
                    const routineMax = Math.max(0, 4.2 * (ageYears - 15));
                    const exactRou = tooltipRoutine !== null ? tooltipRoutine : null;
                    for (const pos of positions) {
                        const pix = getPosIndex(pos);
                        const sax = skillsToArray(skills, pix);
                        const rmx = computeRemainders(pix, sax, asiNum);
                        const lo = computeR5(pix, sax, asiNum, 0);
                        const hi = computeR5(pix, sax, asiNum, routineMax);
                        if (recCalc === null || rmx.rec > recCalc) recCalc = rmx.rec;
                        if (r5Lo === null || lo > r5Lo) r5Lo = lo;
                        if (r5Hi === null || hi > r5Hi) r5Hi = hi;
                        if (exactRou !== null) {
                            const exact = computeR5(pix, sax, asiNum, exactRou);
                            if (r5 === null || exact > r5) r5 = exact;
                        }
                    }
                }
            }

            const tip = { recSort, recCalc, r5, r5Lo, r5Hi, ti, skills };
            tooltipCache[p.id] = tip;
            updateTooltipCells(p.id, tip);
    }

    async function startTooltipFetch(players) {
        tooltipFetchAbort = false;
        const uncached = players.filter(p => !tooltipCache[p.id] || tooltipCache[p.id].estimated);
        // Fire all in parallel — browser caps at ~6 concurrent naturally
        await Promise.all(uncached.map(async p => {
            if (!tooltipFetchAbort) await fetchOnePlayer(p);
        }));
    }

    // ═══════════════════════════════════════════════════════════════════
    //  SEARCH
    // ═══════════════════════════════════════════════════════════════════

    function buildHash() {
        let h = '/';
        const activeFps = $('[data-fp].active').map(function () { return $(this).data('fp'); }).get();
        if (activeFps.length) {
            const groups = new Set(), sides = new Set();
            for (const fp of activeFps) {
                const m = FP_MAP[fp]; if (!m) continue;
                groups.add(m.group);
                if (m.side) sides.add(m.side);
            }
            for (const g of groups) h += g + '/';
            for (const s of sides)  h += s + '/';
        }
        if ($('#tms-for').is(':checked')) h += 'for/';

        const amin = $('#tms-amin').val(), amax = $('#tms-amax').val();
        if (amin !== '18') h += `amin/${amin}/`;
        if (amax !== '37') h += `amax/${amax}/`;

        const recMin = parseFloat($('#tms-rmin').val()) || 0;
        const recMax = parseFloat($('#tms-rmax').val());
        const tmRmin = decRecToTM(recMin);
        const tmRmax = decRecToTM(isNaN(recMax) ? 5 : recMax);
        if (tmRmin > 0)  h += `rmin/${tmRmin}/`;
        if (tmRmax < 10) h += `rmax/${tmRmax}/`;

        const cost = $('#tms-cost').val();
        if (cost && cost !== '0') h += `cost/${cost}/`;

        const time = $('#tms-time').val();
        if (time && time !== '0') h += `time/${time}/`;

        for (let i = 0; i < 3; i++) {
            const sk = $(`#tms-sf-s${i}`).val();
            const sv = $(`#tms-sf-v${i}`).val();
            if (sk && sk !== '0' && sv && sv !== '0') h += `${sk}/${sv}/`;
        }
        return h;
    }

    function doSearch() {
        if (isLoading) return;
        isLoading = true;
        expandedId = null;
        tooltipFetchAbort = true; // cancel in-flight tooltip fetches
        findAllAbort = true;      // cancel in-flight findAll scan
        tooltipCache = {}; // fresh cache for new search results
        $('#tms-table-wrap').html('<div id="tms-loading"><span class="tms-spinner"></span> Searching transfer market…</div>');

        // Clear old countdowns
        if (window.countDowns) {
            for (const id in window.countDowns) { window.countDowns[id] = null; }
            window.countDowns = {};
        }

        const hash   = buildHash();
        const clubId = window.SESSION ? window.SESSION.id : 0;

        $.post('/ajax/transfer.ajax.php', { search: hash, club_id: clubId }, function (data) {
            isLoading = false;

            if (!data) {
                $('#tms-table-wrap').html('<div id="tms-loading" style="color:#ff7373">No data received. Please try again.</div>');
                return;
            }
            if (data.refresh) { location.reload(); return; }

            const raw = Array.isArray(data.list) ? data.list : [];
            window.transfer_info_ar = raw;
            allPlayers = raw.map(processPlayer);
            computeAllEstimates(allPlayers);
            refreshDisplay();
            // Start background tooltip enrichment
            tooltipFetchAbort = true; // abort any previous fetch
            setTimeout(() => startTooltipFetch(allPlayers), 300);
        }, 'json').fail(function () {
            isLoading = false;
            $('#tms-table-wrap').html('<div id="tms-loading" style="color:#ff7373">Network error. Please try again.</div>');
        });
    }
    function resetFilters(silent) {
        $('[data-fp]').removeClass('active');
        $('#tms-for').prop('checked', false);
        $('#tms-amin').val('18');
        $('#tms-amax').val('37');
        $('#tms-rmin').val('0');
        $('#tms-rmax').val('5');
        $('#tms-cost').val('0');
        $('#tms-time').val('0');
        for (let i = 0; i < 3; i++) {
            $(`#tms-sf-s${i}`).val('0');
            $(`#tms-sf-v${i}`).val('0');
        }
        $('#tms-r5min').val('');
        $('#tms-r5max').val('');
        $('#tms-timin').val('');
        $('#tms-timax').val('');
        if (!silent) doSearch();
    }

    // ═══════════════════════════════════════════════════════════════════
    //  SAVED FILTERS
    // ═══════════════════════════════════════════════════════════════════

    function readCurrentFilterState() {
        const positions = $('[data-fp].active').map(function () { return $(this).data('fp'); }).get();
        const skills = [];
        for (let i = 0; i < 3; i++) {
            skills.push([$(`#tms-sf-s${i}`).val() || '0', $(`#tms-sf-v${i}`).val() || '0']);
        }
        return {
            positions,
            foreigners: $('#tms-for').is(':checked'),
            amin: $('#tms-amin').val(),
            amax: $('#tms-amax').val(),
            rmin: $('#tms-rmin').val(),
            rmax: $('#tms-rmax').val(),
            cost: $('#tms-cost').val(),
            time: $('#tms-time').val(),
            skills,
            r5min: $('#tms-r5min').val(),
            r5max: $('#tms-r5max').val(),
            timin: $('#tms-timin').val(),
            timax: $('#tms-timax').val(),
        };
    }

    function applyFilterState(state) {
        if (!state) return;
        $('[data-fp]').removeClass('active');
        (state.positions || []).forEach(fp => $(`[data-fp="${fp}"]`).addClass('active'));
        $('#tms-for').prop('checked', !!state.foreigners);
        $('#tms-amin').val(state.amin != null ? state.amin : '18');
        $('#tms-amax').val(state.amax != null ? state.amax : '37');
        $('#tms-rmin').val(state.rmin != null ? state.rmin : '0');
        $('#tms-rmax').val(state.rmax != null ? state.rmax : '5');
        $('#tms-cost').val(state.cost || '0');
        $('#tms-time').val(state.time || '0');
        const skills = state.skills || [];
        for (let i = 0; i < 3; i++) {
            const [sk, sv] = skills[i] || ['0', '0'];
            $(`#tms-sf-s${i}`).val(sk);
            $(`#tms-sf-v${i}`).val(sv);
        }
        $('#tms-r5min').val(state.r5min || '');
        $('#tms-r5max').val(state.r5max || '');
        $('#tms-timin').val(state.timin || '');
        $('#tms-timax').val(state.timax || '');
        // Open "More Filters" if any of those fields are active
        const hasMore = (state.cost && state.cost !== '0') ||
                        (state.time && state.time !== '0') ||
                        (skills.some(([sk]) => sk && sk !== '0'));
        if (hasMore) {
            $('#tms-more-toggle').addClass('open');
            $('#tms-more-body').addClass('open');
        }
    }

    function getSavedFilters() {
        try {
            return JSON.parse(localStorage.getItem(SAVED_FILTERS_KEY) || '{}');
        } catch (e) { return {}; }
    }

    function saveNamedFilter(name, state) {
        const filters = getSavedFilters();
        filters[name] = state;
        localStorage.setItem(SAVED_FILTERS_KEY, JSON.stringify(filters));
    }

    function deleteNamedFilter(name) {
        const filters = getSavedFilters();
        delete filters[name];
        localStorage.setItem(SAVED_FILTERS_KEY, JSON.stringify(filters));
    }

    function populateSavedFiltersDropdown() {
        const filters = getSavedFilters();
        const names = Object.keys(filters);
        const $sel = $('#tms-saved-filters-sel');
        if (!$sel.length) return;
        const current = $sel.val();
        $sel.empty();
        if (names.length === 0) {
            $sel.append('<option value="">— no saved filters —</option>');
        } else {
            $sel.append('<option value="">— select filter —</option>');
            for (const name of names) {
                $sel.append(`<option value="${name}"${name === current ? ' selected' : ''}>${name}</option>`);
            }
        }
    }

    // ═══════════════════════════════════════════════════════════════════
    //  FIND ALL PLAYERS (exhaustive cartesian scan: pos × age × rec)
    // ═══════════════════════════════════════════════════════════════════

    function readBaseFilters() {
        return {
            foreigners: $('#tms-for').is(':checked'),
            amin:   $('#tms-amin').val() || '18',
            amax:   $('#tms-amax').val() || '37',
            rmin:   $('#tms-rmin').val() || '0',
            rmax:   $('#tms-rmax').val() || '5',
            cost:   $('#tms-cost').val() || '0',
            time:   $('#tms-time').val() || '0',
            skills: [0, 1, 2].map(i => [$(`#tms-sf-s${i}`).val() || '0', $(`#tms-sf-v${i}`).val() || '0']),
        };
    }

    function buildHashRaw({ positions = [], sides = [], foreigners, amin, amax, rmin, rmax, cost, time, skills = [] }) {
        let h = '/';
        for (const p of positions) h += p + '/';
        for (const s of sides)     h += s + '/';
        if (foreigners)              h += 'for/';
        if (amin && amin !== '18')   h += `amin/${amin}/`;
        if (amax && amax !== '37')   h += `amax/${amax}/`;
        if (rmin && rmin !== '0')    h += `rmin/${rmin}/`;
        if (rmax && rmax !== '10')   h += `rmax/${rmax}/`;
        if (cost && cost !== '0')    h += `cost/${cost}/`;
        if (time && time !== '0')    h += `time/${time}/`;
        for (const [sk, sv] of skills) {
            if (sk && sk !== '0' && sv && sv !== '0') h += `${sk}/${sv}/`;
        }
        return h;
    }

    function fetchWithHash(hash) {
        return new Promise(resolve => {
            const clubId = window.SESSION ? window.SESSION.id : 0;
            $.post('/ajax/transfer.ajax.php', { search: hash, club_id: clubId }, function (data) {
                resolve(Array.isArray(data && data.list) ? data.list : []);
            }, 'json').fail(() => resolve([]));
        });
    }

    function showModal({ icon, title, message, buttons }) {
        return new Promise(resolve => {
            const overlay = document.createElement('div');
            overlay.id = 'tms-modal-overlay';
            overlay.innerHTML =
                `<div class="tms-modal">` +
                `<div class="tms-modal-icon">${icon || ''}</div>` +
                `<div class="tms-modal-title">${title}</div>` +
                `<div class="tms-modal-msg">${message}</div>` +
                `<div class="tms-modal-btns">${buttons.map(b =>
                    `<button class="tms-modal-btn tms-modal-btn-${b.style || 'secondary'}" data-val="${b.value}">` +
                    `${b.label}${b.sub ? `<span class="tms-modal-btn-sub">${b.sub}</span>` : ''}` +
                    `</button>`
                ).join('')}</div></div>`;
            const closeWith = val => { overlay.remove(); resolve(val); };
            const onKey = e => {
                if (e.key === 'Escape') { document.removeEventListener('keydown', onKey); closeWith('cancel'); }
            };
            overlay.addEventListener('click', e => {
                if (e.target === overlay) { document.removeEventListener('keydown', onKey); closeWith('cancel'); }
            });
            document.addEventListener('keydown', onKey);
            overlay.querySelectorAll('.tms-modal-btn').forEach(btn =>
                btn.addEventListener('click', () => {
                    document.removeEventListener('keydown', onKey);
                    closeWith(btn.dataset.val);
                })
            );
            document.body.appendChild(overlay);
        });
    }

    function promptModal({ icon, title, placeholder, defaultValue }) {
        return new Promise(resolve => {
            const overlay = document.createElement('div');
            overlay.id = 'tms-modal-overlay';
            const esc = s => (s || '').replace(/"/g, '&quot;');
            overlay.innerHTML =
                `<div class="tms-modal">` +
                `<div class="tms-modal-icon">${icon || ''}</div>` +
                `<div class="tms-modal-title">${title}</div>` +
                `<input type="text" id="tms-prompt-input" class="tms-sel" style="width:100%;box-sizing:border-box;margin-bottom:14px" placeholder="${esc(placeholder)}" value="${esc(defaultValue)}" />` +
                `<div class="tms-modal-btns">` +
                `<button class="tms-modal-btn tms-modal-btn-primary" data-val="ok">💾 Save</button>` +
                `<button class="tms-modal-btn tms-modal-btn-danger" data-val="cancel">Cancel</button>` +
                `</div></div>`;
            const getVal = () => overlay.querySelector('#tms-prompt-input').value.trim();
            const closeWith = val => { overlay.remove(); resolve(val); };
            const onKey = e => {
                if (e.key === 'Escape') { document.removeEventListener('keydown', onKey); closeWith(null); }
                if (e.key === 'Enter')  { document.removeEventListener('keydown', onKey); closeWith(getVal() || null); }
            };
            overlay.addEventListener('click', e => {
                if (e.target === overlay) { document.removeEventListener('keydown', onKey); closeWith(null); }
            });
            document.addEventListener('keydown', onKey);
            overlay.querySelector('[data-val="ok"]').addEventListener('click', () => {
                document.removeEventListener('keydown', onKey);
                closeWith(getVal() || null);
            });
            overlay.querySelector('[data-val="cancel"]').addEventListener('click', () => {
                document.removeEventListener('keydown', onKey);
                closeWith(null);
            });
            document.body.appendChild(overlay);
            setTimeout(() => overlay.querySelector('#tms-prompt-input').focus(), 50);
        });
    }

    async function findAllPlayers() {
        if (isLoading || findAllRunning) return;

        // Warn if age range > 3 years and no position/rec filter is set
        const _amin = Math.max(18, parseInt($('#tms-amin').val()) || 18);
        const _amax = Math.min(37, parseInt($('#tms-amax').val()) || 37);
        const _hasPos = $('[data-fp].active').length > 0;
        const _rmin   = parseFloat($('#tms-rmin').val()) || 0;
        const _rmax   = parseFloat($('#tms-rmax').val());
        const _hasRec = _rmin > 0 || (!isNaN(_rmax) && _rmax < 5);
        if ((_amax - _amin) > 3 && !_hasPos && !_hasRec) {
            const choice = await showModal({
                icon: '⚠️',
                title: 'This scan may take a long time',
                message: 'A wide age range is selected and no <strong style="color:#c8e0b4">position</strong> or ' +
                         '<strong style="color:#c8e0b4">recommendation</strong> filter is active.<br><br>' +
                         'Consider adding one to speed things up significantly.',
                buttons: [
                    { label: 'Proceed Anyway', value: 'ok',     style: 'secondary' },
                    { label: 'Cancel',         value: 'cancel', style: 'danger' },
                ],
            });
            if (choice !== 'ok') return;
        }

        findAllRunning    = true;
        findAllAbort      = false;
        tooltipFetchAbort = true;
        tooltipCache      = {};
        expandedId        = null;

        if (window.countDowns) {
            for (const id in window.countDowns) window.countDowns[id] = null;
            window.countDowns = {};
        }

        const base    = readBaseFilters();
        const rminNum = Math.max(0,  decRecToTM(base.rmin));
        const rmaxNum = Math.min(10, decRecToTM(parseFloat(base.rmax) || 5));
        const aminNum = Math.max(18, parseInt(base.amin) || 18);
        const amaxNum = Math.min(37, parseInt(base.amax) || 37);

        // ── 1. Position combos: respect user's sidebar selection ──────────
        const activeFps = $('[data-fp].active').map(function () { return $(this).data('fp'); }).get();
        const fpKeys    = activeFps.length ? activeFps : Object.keys(FP_MAP);
        const posCombos = fpKeys.map(fp => {
            const m = FP_MAP[fp];
            return { positions: [m.group], sides: m.side ? [m.side] : [] };
        });

        // ── 2. Age: one step per year ─────────────────────────────────────
        const ages = [];
        for (let a = aminNum; a <= amaxNum; a++) ages.push(a);

        // ── 3. Rec: one step per unit ─────────────────────────────────────
        const recRanges = [];
        for (let r = rminNum; r < rmaxNum; r++) recRanges.push([r, r + 1]);
        if (recRanges.length === 0) recRanges.push([rminNum, rmaxNum]);

        // ── 4. Full cartesian product ─────────────────────────────────────
        const tasks = [];
        for (const pos of posCombos) {
            for (const age of ages) {
                for (const [lo, hi] of recRanges) {
                    tasks.push({ pos, age, recLo: lo, recHi: hi });
                }
            }
        }

        const collected = new Map();
        const total     = tasks.length;
        let done        = 0;

        const updateProgress = () => {
            const pct = total > 0 ? Math.round((done / total) * 100) : 0;
            $('#tms-table-wrap').html(
                `<div id="tms-loading"><span class="tms-spinner"></span>` +
                `Scanning… <strong style="color:#c8e0b4">${done}/${total}</strong> (${pct}%) &mdash; ` +
                `<span style="color:#80e048">${collected.size}</span> players found&hellip;</div>`
            );
        };
        updateProgress();

        // Fire all tasks in parallel; browser naturally caps at ~6 concurrent
        await Promise.all(tasks.map(async (task) => {
            if (findAllAbort) return;
            const hash = buildHashRaw({
                ...base,
                positions: task.pos.positions,
                sides:     task.pos.sides,
                amin:      String(task.age),
                amax:      String(task.age),
                rmin:      String(task.recLo),
                rmax:      String(task.recHi),
            });
            const result = await fetchWithHash(hash);
            if (!findAllAbort) {
                for (const p of result) collected.set(p.id, p);
            }
            done++;
            updateProgress();
        }));

        findAllRunning = false;
        if (findAllAbort) return;

        const foundCount = collected.size;
        let fetchTooltips = true;
        if (foundCount > 600) {
            const choice = await showModal({
                icon: '📊',
                title: `${foundCount} players found`,
                message: 'Fetching full stats (R5, Rec, TI) for this many players may take several minutes.' +
                         '<br><br>Or get an <strong style="color:#c8e0b4">instant R5 range estimate</strong> ' +
                         'based on transfer-data skills and assumed routine ' +
                         '<span style="color:#80e048">0 – 4.2 × (age − 15)</span>, with no API calls.',
                buttons: [
                    { label: 'Full Analysis',  value: 'full',     style: 'primary',   sub: 'Fetches R5 · Rec · TI via tooltip API — slower' },
                    { label: 'Quick Estimate', value: 'estimate', style: 'secondary', sub: 'Shows R5 range instantly, no extra API calls' },
                    { label: 'Cancel',         value: 'cancel',   style: 'danger' },
                ],
            });
            if (choice === 'cancel') return;
            fetchTooltips = (choice === 'full');
        }

        allPlayers = [...collected.values()].map(processPlayer);
        computeAllEstimates(allPlayers);
        refreshDisplay();
        if (fetchTooltips) setTimeout(() => startTooltipFetch(allPlayers), 300);
    }

    // ═══════════════════════════════════════════════════════════════════
    //  LAYOUT
    // ═══════════════════════════════════════════════════════════════════

    function buildLayout() {
        console.log('Building layout');
        if ($('#tms-outer').length) return;
        const $outer = $('<div id="tms-outer"></div>');
        $outer.html(`
<div id="tms-root">
  ${buildSidebar()}
  <div id="tms-main">
    <div id="tms-toolbar">
      <span id="tms-hits">0</span>
      <span class="tms-toolbar-label"> players</span>
    </div>
    <div id="tms-table-wrap">
      <div id="tms-loading"><span class="tms-spinner"></span> Loading transfer market…</div>
    </div>
  </div>
</div>
<div id="transfer_list" style="display:none"></div>
        `);
        $('.main_center').last().after($outer);
    }

    // ═══════════════════════════════════════════════════════════════════
    //  EVENT BINDING
    // ═══════════════════════════════════════════════════════════════════

    function bindEvents() {
        $(document).on('click', '#tms-search-btn', doSearch);
        $(document).on('keydown', '#tms-sidebar', function (e) {
            if (e.key === 'Enter') doSearch();
        });

        $(document).on('click', '.tms-reload-btn', function (e) {
            e.stopPropagation();
            const pid = $(this).data('pid');
            const player = allPlayers.find(x => x.id == pid);
            if (!player) return;
            $(this).addClass('tms-reloading');
            fetchOnePlayer(player);
        });

        $(document).on('click', '[data-fp]', function () { $(this).toggleClass('active'); });
        $(document).on('click', '#tms-findall-btn', findAllPlayers);

        $(document).on('click', '#tms-more-toggle', function () {
            $(this).toggleClass('open');
            $('#tms-more-body').toggleClass('open');
        });

        $(document).on('click', '#tms-filter-save-btn', async function () {
            const currentSel = $('#tms-saved-filters-sel').val();
            const name = await promptModal({
                icon: '💾',
                title: 'Save Current Filter',
                placeholder: 'Enter filter name…',
                defaultValue: currentSel || '',
            });
            if (!name) return;
            saveNamedFilter(name, readCurrentFilterState());
            populateSavedFiltersDropdown();
            $('#tms-saved-filters-sel').val(name);
        });

        $(document).on('click', '#tms-filter-load-btn', function () {
            const name = $('#tms-saved-filters-sel').val();
            if (!name) return;
            const state = getSavedFilters()[name];
            if (!state) return;
            applyFilterState(state);
            doSearch();
        });

        $(document).on('click', '#tms-filter-del-btn', async function () {
            const name = $('#tms-saved-filters-sel').val();
            if (!name) return;
            const confirmed = await showModal({
                icon: '🗑️',
                title: 'Delete saved filter',
                message: `Delete "<strong style="color:#c8e0b4">${name}</strong>"?`,
                buttons: [
                    { label: 'Delete', value: 'ok',     style: 'danger' },
                    { label: 'Cancel', value: 'cancel', style: 'secondary' },
                ],
            });
            if (confirmed !== 'ok') return;
            deleteNamedFilter(name);
            populateSavedFiltersDropdown();
        });

        $(document).on('input', '#tms-r5min, #tms-r5max, #tms-timin, #tms-timax', function () {
            expandedId = null;
            refreshDisplay();
        });


        $(document).on('click', '#tms-mode-bd', function () {
            skillsMode = false;
            $(this).addClass('active');
            $('#tms-mode-sk').removeClass('active');
            expandedId = null;
            refreshDisplay();
        });

        $(document).on('click', '#tms-mode-sk', function () {
            skillsMode = true;
            $(this).addClass('active');
            $('#tms-mode-bd').removeClass('active');
            expandedId = null;
            refreshDisplay();
        });
    }

    // ═══════════════════════════════════════════════════════════════════
    //  NEUTRALIZE TM'S OWN RENDER PIPELINE
    // ═══════════════════════════════════════════════════════════════════

    function neutralizeTM() {
        console.log('Neutralizing TM rendering');
        // Stop the hash-check interval TM registered on document.ready
        if (window.hashCheck) {
            clearInterval(window.hashCheck);
            window.hashCheck = null;
        }
        // Make TM's rendering functions no-ops so their AJAX callback
        // doesn't overwrite our UI (they still populate transfer_info_ar)
        window.makeTable  = function (arr) { if (arr) window.transfer_info_ar = arr; };
        window.sort_it    = function () {};
        window.startSearch = function () {};
        window.check_hash  = function () {};
        window.popFilterImages = function () {};
    }

    // ═══════════════════════════════════════════════════════════════════
    //  INIT
    // ═══════════════════════════════════════════════════════════════════

    function init() {
        neutralizeTM();
        injectStyles();
        buildLayout();
        bindEvents();
        populateSavedFiltersDropdown();
        // Tiny delay gives TM's own DOM-ready callbacks time to clear,
        // then we kick off our first search
        setTimeout(doSearch, 150);
        console.log('Transfer Market Scanner initialized');
    }

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

})();