TM Transfer Scanner

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

})();