TM Player Enhanced

Player page overhaul — redesigned card, live transfer tracker, R5/REC/TI charts, skill graphs, compare tool, squad scout & more

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         TM Player Enhanced
// @namespace    https://trophymanager.com
// @version      1.5.0
// @description  Player page overhaul — redesigned card, live transfer tracker, R5/REC/TI charts, skill graphs, compare tool, squad scout & more
// @match        https://trophymanager.com/players/*
// @grant        none
// @run-at       document-idle
// ==/UserScript==

(function () {
    'use strict';

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

    const urlMatch = location.pathname.match(/\/players\/(\d+)/);
    const IS_SQUAD_PAGE = !urlMatch && /\/players\/?$/.test(location.pathname);
    if (!urlMatch && !IS_SQUAD_PAGE) return;
    const PLAYER_ID = urlMatch ? urlMatch[1] : null;

    /* ═══════════════════════════════════════════════════════════
       IndexedDB Storage — replaces localStorage for player data
       (localStorage has 5 MB limit; IndexedDB has hundreds of MB)
       Provides sync reads via in-memory cache + async writes.
       ═══════════════════════════════════════════════════════════ */
    const PlayerDB = (() => {
        const DB_NAME = 'TMPlayerData';
        const STORE_NAME = 'players';
        const DB_VERSION = 1;
        let db = null;
        const cache = {};

        const open = () => new Promise((resolve, reject) => {
            const req = indexedDB.open(DB_NAME, DB_VERSION);
            req.onupgradeneeded = (e) => {
                const d = e.target.result;
                if (!d.objectStoreNames.contains(STORE_NAME))
                    d.createObjectStore(STORE_NAME);
            };
            req.onsuccess = (e) => { db = e.target.result; resolve(db); };
            req.onerror = (e) => reject(e.target.error);
        });

        /** Sync read from cache (call after init) */
        const get = (pid) => cache[pid] || null;

        /** Async write: updates cache immediately + persists to IndexedDB */
        const set = (pid, value) => {
            cache[pid] = value;
            if (!db) return Promise.resolve();
            return new Promise((resolve, reject) => {
                const tx = db.transaction(STORE_NAME, 'readwrite');
                tx.objectStore(STORE_NAME).put(value, pid);
                tx.oncomplete = () => resolve();
                tx.onerror = (e) => reject(e.target.error);
            }).catch(e => console.warn('[DB] write failed:', e));
        };

        /** Async delete: removes from cache + IndexedDB */
        const remove = (pid) => {
            delete cache[pid];
            if (!db) return Promise.resolve();
            return new Promise((resolve, reject) => {
                const tx = db.transaction(STORE_NAME, 'readwrite');
                tx.objectStore(STORE_NAME).delete(pid);
                tx.oncomplete = () => resolve();
                tx.onerror = (e) => reject(e.target.error);
            }).catch(e => console.warn('[DB] delete failed:', e));
        };

        /** Get all pids (from cache, sync) */
        const allPids = () => Object.keys(cache);

        /** Init: open DB → migrate localStorage → preload cache */
        const init = async () => {
            await open();

            /* Migrate existing localStorage _data keys to IndexedDB */
            const toMigrate = [];
            const keysToRemove = [];
            for (let i = 0; i < localStorage.length; i++) {
                const k = localStorage.key(i);
                if (!k || !k.endsWith('_data')) continue;
                const pid = k.replace('_data', '');
                if (!/^\d+$/.test(pid)) continue;
                try {
                    const data = JSON.parse(localStorage.getItem(k));
                    if (data) toMigrate.push({ pid, data });
                    keysToRemove.push(k);
                } catch (e) { keysToRemove.push(k); }
            }
            if (toMigrate.length > 0) {
                const tx = db.transaction(STORE_NAME, 'readwrite');
                const store = tx.objectStore(STORE_NAME);
                for (const item of toMigrate) store.put(item.data, item.pid);
                await new Promise((res, rej) => { tx.oncomplete = res; tx.onerror = rej; });
                for (const k of keysToRemove) localStorage.removeItem(k);
                console.log(`%c[DB] Migrated ${toMigrate.length} player(s) from localStorage → IndexedDB`,
                    'font-weight:bold;color:#6cc040');
            }

            /* Preload ALL records into sync cache */
            const tx = db.transaction(STORE_NAME, 'readonly');
            const store = tx.objectStore(STORE_NAME);
            const reqAll = store.getAll();
            const reqKeys = store.getAllKeys();
            await new Promise((res, rej) => { tx.oncomplete = res; tx.onerror = rej; });
            for (let i = 0; i < reqKeys.result.length; i++)
                cache[reqKeys.result[i]] = reqAll.result[i];

            console.log(`[DB] Loaded ${Object.keys(cache).length} player(s) from IndexedDB`);

            /* Request persistent storage so Chrome won't auto-evict */
            if (navigator.storage && navigator.storage.persist) {
                navigator.storage.persist().then(granted => {
                    console.log(`[DB] Persistent storage: ${granted ? '✓ granted' : '✗ denied'}`);
                });
            }
        };

        return { init, get, set, remove, allPids };
    })();

    /* ── Migrate R6 old format + scan unmigrated ── */
    const scanAndMigrateR6 = () => {
        /* Phase 1: Convert R6 4-key format ({pid}_SI etc.) → PlayerDB */
        const seenPids = new Set();
        for (let i = 0; i < localStorage.length; i++) {
            const k = localStorage.key(i);
            if (!k) continue;
            const m = k.match(/^(\d+)_SI$/);
            if (!m) continue;
            const pid = m[1];
            if (seenPids.has(pid)) continue;
            seenPids.add(pid);
            const existing = PlayerDB.get(pid);
            if (existing && (existing._v === 1 || existing._v === 2 || existing._v === 3)) continue;
            try {
                const siObj = JSON.parse(localStorage.getItem(`${pid}_SI`) || '{}');
                const rerecObj = JSON.parse(localStorage.getItem(`${pid}_REREC`) || '{}');
                const r5Obj = JSON.parse(localStorage.getItem(`${pid}_R5`) || '{}');
                const skillsObj = JSON.parse(localStorage.getItem(`${pid}_skills`) || '{}');
                const ages = Object.keys(siObj);
                if (ages.length === 0) continue;
                const store = { _v: 1, lastSeen: Date.now(), records: {} };
                for (const ageKey of ages) {
                    store.records[ageKey] = {
                        SI: parseInt(siObj[ageKey]) || 0,
                        REREC: rerecObj[ageKey] ?? null,
                        R5: r5Obj[ageKey] ?? null,
                        skills: skillsObj[ageKey] || []
                    };
                }
                PlayerDB.set(pid, store);
                localStorage.removeItem(`${pid}_SI`);
                localStorage.removeItem(`${pid}_REREC`);
                localStorage.removeItem(`${pid}_R5`);
                localStorage.removeItem(`${pid}_skills`);
                console.log(`%c[Migration] Converted R6 player ${pid} (${ages.length} records) → PlayerDB v1`,
                    'color:#6cc040');
            } catch (e) {
                console.warn(`[Migration] Failed R6→PlayerDB for ${pid}:`, e.message);
            }
        }

        /* Phase 2: Log players still needing v3 migration */
        const unmigrated = [];
        for (const pid of PlayerDB.allPids()) {
            const s = PlayerDB.get(pid);
            if (s && s._v < 3 && s.records && Object.keys(s.records).length > 3) {
                const firstKey = Object.keys(s.records)[0];
                const sk = s.records[firstKey]?.skills;
                const type = Array.isArray(sk) && sk.length === 11 ? 'GK' : 'OUT';
                unmigrated.push({
                    Player: pid, Type: type, Records: Object.keys(s.records).length, Version: s._v,
                    Link: `https://trophymanager.com/players/${pid}/`
                });
            }
        }
        if (unmigrated.length > 0) {
            console.log(`%c[Migration] ${unmigrated.length} player(s) need v3 sync (visit each):`,
                'font-weight:bold;color:#fbbf24');
            unmigrated.sort((a, b) => b.Records - a.Records);
            console.table(unmigrated);
        } else {
            console.log('[Migration] All players migrated ✓');
        }
    };

    /* ═══════════════════════════════════════════════════════════
       SQUAD PAGE — Parse players table from /players/ list page
       Extracts skill values, training progress (part_up, one_up),
       TI and TI change for each player in the squad table.
       ═══════════════════════════════════════════════════════════ */
    const SKILL_NAMES_OUT_SHORT = ['Str','Sta','Pac','Mar','Tac','Wor','Pos','Pas','Cro','Tec','Hea','Fin','Lon','Set'];
    const SKILL_NAMES_GK_SHORT  = ['Str','Sta','Pac','Han','One','Ref','Aer','Jum','Com','Kic','Thr'];
    const SKILL_NAMES_OUT_FULL  = ['Strength','Stamina','Pace','Marking','Tackling','Workrate','Positioning','Passing','Crossing','Technique','Heading','Finishing','Longshots','Set Pieces'];
    const SKILL_NAMES_GK_FULL   = ['Strength','Stamina','Pace','Handling','One on ones','Reflexes','Aerial Ability','Jumping','Communication','Kicking','Throwing'];

    /* ═══════════════════════════════════════════════════════════
       SQUAD PAGE — Ensure all players (main + reserves) are visible
       Hash format: #/a/{true|false}/b/{true|false}/
       ═══════════════════════════════════════════════════════════ */
    const parseSquadHash = () => {
        const h = location.hash || '';
        const aMatch = h.match(/\/a\/(true|false)/i);
        const bMatch = h.match(/\/b\/(true|false)/i);
        return {
            a: aMatch ? aMatch[1] === 'true' : true,   /* default: main squad visible */
            b: bMatch ? bMatch[1] === 'true' : false    /* default: reserves hidden */
        };
    };

    const ensureAllPlayersVisible = () => new Promise((resolve) => {
        const sqDiv = document.getElementById('sq');
        if (!sqDiv) { resolve(); return; }

        const vis = parseSquadHash();
        const needA = !vis.a;
        const needB = !vis.b;

        if (!needA && !needB) { resolve(); return; } /* Both already visible */

        /* Set hash to show both squads */
        const newHash = '#/a/true/b/true/';
        const onHashChange = () => {
            window.removeEventListener('hashchange', onHashChange);
            /* Wait for DOM to update after hash-triggered toggle */
            setTimeout(resolve, 500);
        };
        window.addEventListener('hashchange', onHashChange);
        location.hash = newHash;

        /* If hash was already the same (no event fires), or toggles need clicking */
        setTimeout(() => {
            window.removeEventListener('hashchange', onHashChange);
            /* Fallback: click toggles directly if hash didn't work */
            if (needA) { const aBtn = document.getElementById('toggle_a_team'); if (aBtn) aBtn.click(); }
            if (needB) { const bBtn = document.getElementById('toggle_b_team'); if (bBtn) bBtn.click(); }
            setTimeout(resolve, 500);
        }, 1500);
    });

    /* ═══════════════════════════════════════════════════════════
       SQUAD PAGE — Loader/Progress overlay
       ═══════════════════════════════════════════════════════════ */
    const createSquadLoader = () => {
        const overlay = document.createElement('div');
        overlay.id = 'tmrc-squad-loader';
        overlay.innerHTML = `
            <div style="position:fixed;top:0;left:0;right:0;z-index:99999;
                         background:rgba(20,30,15,0.95);border-bottom:2px solid #6cc040;
                         padding:10px 20px;font-family:Arial,sans-serif;color:#e8f5d8;">
                <div style="display:flex;align-items:center;gap:12px;max-width:900px;margin:0 auto;">
                    <div style="font-size:14px;font-weight:700;color:#6cc040;">⚽ Squad Sync</div>
                    <div style="flex:1;background:rgba(108,192,64,0.15);border-radius:8px;height:18px;
                                overflow:hidden;border:1px solid rgba(108,192,64,0.3);">
                        <div id="tmrc-loader-bar" style="height:100%;width:0%;background:linear-gradient(90deg,#3d6828,#6cc040);
                                  border-radius:8px;transition:width 0.3s;"></div>
                    </div>
                    <div id="tmrc-loader-text" style="font-size:12px;min-width:180px;text-align:right;">Initializing...</div>
                </div>
            </div>`;
        document.body.appendChild(overlay);
        return {
            update: (current, total, name) => {
                const pct = Math.round((current / total) * 100);
                const bar = document.getElementById('tmrc-loader-bar');
                const txt = document.getElementById('tmrc-loader-text');
                if (bar) bar.style.width = pct + '%';
                if (txt) txt.textContent = `${current}/${total} — ${name}`;
            },
            done: (count) => {
                const bar = document.getElementById('tmrc-loader-bar');
                const txt = document.getElementById('tmrc-loader-text');
                if (bar) bar.style.width = '100%';
                if (txt) { txt.style.color = '#6cc040'; txt.textContent = `✓ ${count} players processed`; }
                setTimeout(() => {
                    const el = document.getElementById('tmrc-squad-loader');
                    if (el) { el.style.transition = 'opacity 0.5s'; el.style.opacity = '0'; setTimeout(() => el.remove(), 600); }
                }, 2500);
            },
            error: (msg) => {
                const txt = document.getElementById('tmrc-loader-text');
                if (txt) { txt.style.color = '#f87171'; txt.textContent = msg; }
                setTimeout(() => { const el = document.getElementById('tmrc-squad-loader'); if (el) el.remove(); }, 4000);
            }
        };
    };

    const parseSquadPage = () => {
        const sqDiv = document.getElementById('sq');
        if (!sqDiv) { console.warn('[Squad] No #sq div found'); return; }

        const rows = sqDiv.querySelectorAll('table tbody tr:not(.header):not(.splitter)');
        if (!rows.length) { console.warn('[Squad] No player rows found'); return; }

        /* Detect if we've hit the GK section */
        let isGKSection = false;
        const allRows = sqDiv.querySelectorAll('table tbody tr');
        const splitterIndices = new Set();
        allRows.forEach((r, i) => { if (r.classList.contains('splitter')) splitterIndices.add(i); });

        const players = [];

        allRows.forEach((row, rowIdx) => {
            if (row.classList.contains('splitter')) {
                isGKSection = true; // After "Goalkeepers" splitter
                return;
            }
            if (row.classList.contains('header')) return;

            const cells = row.querySelectorAll('td');
            if (cells.length < 10) return; // Skip malformed rows

            /* ── Player ID from link ── */
            const link = row.querySelector('a[player_link]');
            if (!link) return;
            const pid = link.getAttribute('player_link');
            const name = link.textContent.trim();

            /* ── Squad number ── */
            const numEl = cells[0]?.querySelector('span.faux_link');
            const number = numEl ? parseInt(numEl.textContent) || 0 : 0;

            /* ── Age ── */
            const ageText = cells[2]?.textContent?.trim() || '0.0';
            const [ageYears, ageMonths] = ageText.split('.').map(s => parseInt(s) || 0);

            /* ── Position ── */
            const posEl = cells[3]?.querySelector('.favposition');
            const posText = posEl ? posEl.textContent.trim() : '';
            const isGK = isGKSection;

            /* ── Skills: parse values + training status ── */
            const skillCount = isGK ? 11 : 14;
            const skillStartIdx = 4; // Skills start at column index 4
            const skills = [];
            const improved = []; // Array of { index, type: 'part_up'|'one_up' }

            for (let i = 0; i < skillCount; i++) {
                const cell = cells[skillStartIdx + i];
                if (!cell) { skills.push(0); continue; }

                const innerDiv = cell.querySelector('div.skill');
                if (!innerDiv) { skills.push(0); continue; }

                /* Check training status */
                const hasPartUp = innerDiv.classList.contains('part_up');
                const hasOneUp = innerDiv.classList.contains('one_up');

                /* Parse skill value: could be a number or a star image */
                const starImg = innerDiv.querySelector('img');
                let skillVal = 0;
                if (starImg) {
                    const src = starImg.getAttribute('src') || '';
                    if (src.includes('star_silver')) skillVal = 19;
                    else if (src.includes('star')) skillVal = 20;
                } else {
                    skillVal = parseInt(innerDiv.textContent.trim()) || 0;
                }

                skills.push(skillVal);

                if (hasPartUp) {
                    improved.push({ index: i, type: 'part_up', skillName: isGK ? SKILL_NAMES_GK_SHORT[i] : SKILL_NAMES_OUT_SHORT[i] });
                } else if (hasOneUp) {
                    improved.push({ index: i, type: 'one_up', skillName: isGK ? SKILL_NAMES_GK_SHORT[i] : SKILL_NAMES_OUT_SHORT[i] });
                }
            }

            /* ── TI and TI change: last 2 cells (before any dashes for GK) ── */
            // For outfield: columns 4..17 = 14 skills, then col 18 = TI, col 19 = +/-
            // For GK: columns 4..14 = 11 skills, then 3 dash columns (15,16,17), then col 18 = TI, col 19 = +/-
            const tiIdx = skillStartIdx + skillCount + (isGK ? 3 : 0); // Skip 3 dash cols for GK
            const tiCell = cells[tiIdx];
            const tiChangeCell = cells[tiIdx + 1];
            const TI = tiCell ? parseInt(tiCell.textContent.trim()) || 0 : 0;
            const tiChangeText = tiChangeCell ? tiChangeCell.textContent.trim() : '0';
            const TI_change = parseInt(tiChangeText.replace('+', '')) || 0;

            /* ── Build player summary ── */
            const totalSkill = skills.reduce((s, v) => s + v, 0);
            const partUpCount = improved.filter(x => x.type === 'part_up').length;
            const oneUpCount = improved.filter(x => x.type === 'one_up').length;

            players.push({
                pid,
                name,
                number,
                ageYears,
                ageMonths,
                position: posText,
                isGK,
                skills,
                improved,
                partUpCount,
                oneUpCount,
                totalImproved: partUpCount + oneUpCount,
                TI,
                TI_change,
                totalSkill
            });
        });

        return players;
    };

    /* ═══════════════════════════════════════════════════════════
       SQUAD PAGE — Process: fetch tooltips, distribute decimals,
       log results with +/- compared to previous week.
       ═══════════════════════════════════════════════════════════ */
    const processSquadPage = async (players) => {
        if (!players || !players.length) return;

        const NAMES_OUT = ['Strength','Stamina','Pace','Marking','Tackling','Workrate','Positioning','Passing','Crossing','Technique','Heading','Finishing','Longshots','Set Pieces'];
        const NAMES_GK  = ['Strength','Stamina','Pace','Handling','One on ones','Reflexes','Aerial Ability','Jumping','Communication','Kicking','Throwing'];

        /* TI efficiency by skill level — same as analyzeGrowth */
        const eff = (lvl) => {
            if (lvl >= 20) return 0;
            if (lvl >= 18) return 0.04;
            if (lvl >= 15) return 0.05;
            if (lvl >= 5)  return 0.10;
            return 0.15;
        };

        /* ASI → total skill points */
        const totalPts = (asi, isGK) => {
            const w = isGK ? 48717927500 : 263533760000;
            return Math.pow(2, Math.log(w * asi) / Math.log(Math.pow(2, 7)));
        };

        /* Extract integer skills from tooltip [{name, value}] */
        const extractSkills = (skillsArr, isGK) => {
            const names = isGK ? NAMES_GK : NAMES_OUT;
            const sv = (name) => {
                const sk = skillsArr.find(s => s.name === name);
                if (!sk) return 0;
                const v = sk.value;
                if (typeof v === 'string') {
                    if (v.includes('star_silver')) return 19;
                    if (v.includes('star')) return 20;
                    return parseInt(v) || 0;
                }
                return parseInt(v) || 0;
            };
            return names.map(sv);
        };

        /* Fetch tooltip for a single player */
        const fetchTip = (pid) => new Promise((resolve) => {
            $.post('/ajax/tooltip.ajax.php', { player_id: pid }, (res) => {
                try {
                    const data = typeof res === 'object' ? res : JSON.parse(res);
                    resolve(data && data.player ? data.player : null);
                } catch (e) { resolve(null); }
            }).fail(() => resolve(null));
        });

        /* Delay helper */
        const delay = (ms) => new Promise(r => setTimeout(r, ms));

        console.log(`%c[Squad] Fetching tooltips for ${players.length} players...`, 'font-weight:bold;color:#38bdf8');

        /* ── Loader UI ── */
        const loader = createSquadLoader();

        const results = [];

        for (let pi = 0; pi < players.length; pi++) {
            const p = players[pi];
            loader.update(pi + 1, players.length, p.name);

            /* Skip players whose current-week record is already locked */
            const curAgeKeyCheck = `${p.ageYears}.${p.ageMonths}`;
            const existingStore = PlayerDB.get(p.pid);
            if (existingStore && existingStore.records && existingStore.records[curAgeKeyCheck]?.locked) {
                console.log(`[Squad] ${p.name} — already locked for ${curAgeKeyCheck}, skipping`);
                continue;
            }

            const tip = await fetchTip(p.pid);
            await delay(100); // avoid hammering server

            if (!tip) {
                console.warn(`[Squad] Could not fetch tooltip for ${p.name} (${p.pid})`);
                continue;
            }

            /* ── Extract data from tooltip ── */
            const asi = parseInt((tip.asi || tip.skill_index || '').toString().replace(/[^0-9]/g, '')) || 0;
            const routine = parseFloat(tip.routine) || 0;
            const wage = parseInt((tip.wage || '').toString().replace(/[^0-9]/g, '')) || 0;
            const favpos = tip.favposition || '';
            const isGK = favpos.split(',')[0].toLowerCase() === 'gk';
            const N = isGK ? 11 : 14;
            const NAMES = isGK ? NAMES_GK : NAMES_OUT;
            const SHORT = isGK ? SKILL_NAMES_GK_SHORT : SKILL_NAMES_OUT_SHORT;

            /* Integer skills from tooltip (ground truth) */
            const intSkills = tip.skills ? extractSkills(tip.skills, isGK) : p.skills;

            /* ── Get previous decimals from IndexedDB ── */
            const dbRecord = PlayerDB.get(p.pid);
            let prevDecimals = null;
            let prevAgeKey = null;
            let prevSkillsFull = null;
            let curDbSkillsFull = null;

            if (dbRecord && dbRecord.records) {
                /* Find records sorted chronologically */
                const keys = Object.keys(dbRecord.records).sort((a, b) => {
                    const [ay, am] = a.split('.').map(Number);
                    const [by, bm] = b.split('.').map(Number);
                    return (ay * 12 + am) - (by * 12 + bm);
                });

                /* Current age key from squad page */
                const curAgeKey = `${p.ageYears}.${p.ageMonths}`;

                /* Capture current week's existing DB record (if player page was visited this week) */
                const curDbRec = dbRecord.records[curAgeKey];
                if (curDbRec && curDbRec.skills && curDbRec.skills.length === N) {
                    curDbSkillsFull = curDbRec.skills.map(v => {
                        const n = typeof v === 'string' ? parseFloat(v) : v;
                        return n >= 20 ? 20 : n;
                    });
                }

                /* Use PREVIOUS week for comparison:
                   - If latest key == current age → that's THIS week, use second-to-last
                   - If latest key != current age → latest IS the previous week */
                let prevIdx = keys.length - 1;
                if (keys.length > 1 && keys[prevIdx] === curAgeKey) {
                    prevIdx = keys.length - 2;
                }

                if (prevIdx >= 0) {
                    prevAgeKey = keys[prevIdx];
                    const prevRec = dbRecord.records[prevAgeKey];
                    if (prevRec && prevRec.skills && prevRec.skills.length === N) {
                        prevSkillsFull = prevRec.skills.map(v => {
                            const n = typeof v === 'string' ? parseFloat(v) : v;
                            return n >= 20 ? 20 : n;
                        });
                        prevDecimals = prevSkillsFull.map(v => v >= 20 ? 0 : v - Math.floor(v));
                    }
                }
            }

            /* ── Compute ASI remainder ── */
            const asiTotalPts = asi > 0 ? totalPts(asi, isGK) : 0;
            const intSum = intSkills.reduce((s, v) => s + v, 0);
            const asiRemainder = asi > 0 ? Math.round((asiTotalPts - intSum) * 100) / 100 : 0;

            /* ── Build improvement map (index → type) ── */
            const improvementMap = {};
            p.improved.forEach(imp => {
                improvementMap[imp.index] = imp.type;
            });

            /* ── Distribute TI gain across improved skills ── */
            const totalGain = p.TI / 10;  // total skill points this week

            let newDecimals;

            if (prevDecimals && asi > 0) {
                /* === We have previous data — smart distribution === */
                newDecimals = [...prevDecimals];

                /* Step 1: Calculate raw gains per improved skill using eff() weights */
                const improvedIndices = p.improved.map(imp => imp.index);
                if (improvedIndices.length > 0 && totalGain > 0) {
                    /* Compute eff-weighted shares for improved skills only */
                    const effWeights = improvedIndices.map(i => eff(intSkills[i]));
                    const effTotal = effWeights.reduce((a, b) => a + b, 0);
                    const shares = effTotal > 0
                        ? effWeights.map(w => w / effTotal)
                        : effWeights.map(() => 1 / improvedIndices.length);

                    /* Distribute gain */
                    improvedIndices.forEach((idx, j) => {
                        newDecimals[idx] += totalGain * shares[j];
                    });
                }

                /* Step 2: Handle one_up → snap to .00 */
                for (const imp of p.improved) {
                    if (imp.type === 'one_up') {
                        newDecimals[imp.index] = 0.00;
                    }
                }

                /* Step 3: Handle part_up cap → if decimal ≥ 1.0, cap at .99, send overflow to pool */
                let overflow = 0;
                let passes = 0;
                do {
                    overflow = 0;
                    let freeCount = 0;
                    for (let i = 0; i < N; i++) {
                        if (intSkills[i] >= 20) { newDecimals[i] = 0; continue; }
                        if (newDecimals[i] >= 1.0) {
                            overflow += newDecimals[i] - 0.99;
                            newDecimals[i] = 0.99;
                        } else if (newDecimals[i] < 0.99) {
                            freeCount++;
                        }
                    }
                    if (overflow > 0.0001 && freeCount > 0) {
                        const add = overflow / freeCount;
                        for (let i = 0; i < N; i++) {
                            if (intSkills[i] < 20 && newDecimals[i] < 0.99) {
                                newDecimals[i] += add;
                            }
                        }
                    }
                } while (overflow > 0.0001 && ++passes < 20);

                /* Step 4: Also check subtle skills — they must NOT have crossed an integer boundary.
                   A subtle skill at previous 16.98 that got redistributed overflow to 17.01 is impossible
                   (it would show as one_up if it crossed). Cap and re-distribute. */
                let subtleOverflow = 0;
                passes = 0;
                do {
                    subtleOverflow = 0;
                    let freeCount2 = 0;
                    for (let i = 0; i < N; i++) {
                        if (intSkills[i] >= 20) continue;
                        const prevInt = prevSkillsFull ? Math.floor(prevSkillsFull[i]) : intSkills[i];
                        const curInt = intSkills[i];
                        if (!improvementMap[i] && curInt === prevInt) {
                            /* Subtle: decimal must stay < 1.0 (same integer) */
                            if (newDecimals[i] >= 1.0) {
                                subtleOverflow += newDecimals[i] - 0.99;
                                newDecimals[i] = 0.99;
                            }
                        }
                    }
                    if (subtleOverflow > 0.0001) {
                        let freeSlots = 0;
                        for (let i = 0; i < N; i++) {
                            if (intSkills[i] < 20 && newDecimals[i] < 0.99) freeSlots++;
                        }
                        if (freeSlots > 0) {
                            const add2 = subtleOverflow / freeSlots;
                            for (let i = 0; i < N; i++) {
                                if (intSkills[i] < 20 && newDecimals[i] < 0.99) {
                                    newDecimals[i] += add2;
                                }
                            }
                        }
                    }
                } while (subtleOverflow > 0.0001 && ++passes < 20);

                /* Step 5: Normalize so Σ decimals = ASI remainder */
                const decSum = newDecimals.reduce((a, b) => a + b, 0);
                if (decSum > 0.001 && asiRemainder > 0) {
                    const scale = asiRemainder / decSum;
                    newDecimals = newDecimals.map((d, i) => intSkills[i] >= 20 ? 0 : d * scale);
                } else if (asiRemainder > 0) {
                    /* All decimals near zero — seed evenly */
                    const nonMax = intSkills.filter(v => v < 20).length;
                    newDecimals = intSkills.map(v => v >= 20 ? 0 : asiRemainder / nonMax);
                }

                /* Step 6: Final cap pass (normalization could push above .99 again) */
                passes = 0;
                do {
                    overflow = 0;
                    let freeCount = 0;
                    for (let i = 0; i < N; i++) {
                        if (intSkills[i] >= 20) { newDecimals[i] = 0; continue; }
                        if (newDecimals[i] > 0.99) { overflow += newDecimals[i] - 0.99; newDecimals[i] = 0.99; }
                        else if (newDecimals[i] < 0) { newDecimals[i] = 0; }
                        else if (newDecimals[i] < 0.99) freeCount++;
                    }
                    if (overflow > 0.0001 && freeCount > 0) {
                        const add = overflow / freeCount;
                        for (let i = 0; i < N; i++) {
                            if (intSkills[i] < 20 && newDecimals[i] < 0.99) newDecimals[i] += add;
                        }
                    }
                } while (overflow > 0.0001 && ++passes < 20);
            } else if (asi > 0) {
                /* === No previous data — seed from ASI remainder evenly === */
                const nonMax = intSkills.filter(v => v < 20).length;
                newDecimals = intSkills.map(v => v >= 20 ? 0 : (nonMax > 0 ? asiRemainder / nonMax : 0));
            } else {
                /* No ASI available — can't compute */
                newDecimals = new Array(N).fill(0);
            }

            /* ── Build full skill values ── */
            const newSkillsFull = intSkills.map((v, i) => v >= 20 ? 20 : v + (newDecimals[i] || 0));

            /* ── Compute diff: New vs DB (current week record) ── */
            const diffSkills = curDbSkillsFull
                ? newSkillsFull.map((v, i) => v - curDbSkillsFull[i])
                : null;

            /* ── Compute R5 and REC for ALL positions, keep best ── */
            const allPositions = favpos.split(',').map(s => s.trim()).filter(Boolean);
            let R5 = null, REC = null, R5_DB = null;
            let bestPos = allPositions[0] || '';
            const r5ByPos = {};

            if (asi > 0) {
                let bestR5 = -Infinity;
                for (const pos of allPositions) {
                    const pIdx = pos.toLowerCase() === 'gk' ? 9 : getPositionIndex(pos);
                    const r5val = Number(calculateR5F(pIdx, newSkillsFull, asi, routine));
                    const recval = Number(calculateRemaindersF(pIdx, newSkillsFull, asi).rec);
                    r5ByPos[pos] = { R5: r5val, REC: recval };
                    if (r5val > bestR5) {
                        bestR5 = r5val;
                        R5 = r5val;
                        REC = recval;
                        bestPos = pos;
                    }
                }
                if (curDbSkillsFull) {
                    const bestPosIdx = bestPos.toLowerCase() === 'gk' ? 9 : getPositionIndex(bestPos);
                    R5_DB = Number(calculateR5F(bestPosIdx, curDbSkillsFull, asi, routine));
                }
            }

            /* ── Store result ── */
            results.push({
                pid: p.pid,
                name: p.name,
                number: p.number,
                ageYears: p.ageYears,
                ageMonths: p.ageMonths,
                position: favpos,
                isGK,
                asi,
                routine,
                TI: p.TI,
                TI_change: p.TI_change,
                intSkills,
                newDecimals,
                newSkillsFull,
                prevSkillsFull,
                curDbSkillsFull,
                diffSkills,
                improved: p.improved,
                R5,
                R5_DB,
                REC,
                r5ByPos,
                bestPos,
                asiRemainder,
                hadPrevData: !!prevDecimals
            });
        }

        /* ═══ Console output — one table per player ═══ */
        console.log(`%c[Squad] ═══ Processed ${results.length} players ═══`, 'font-weight:bold;color:#6cc040');

        const fv = (v) => v >= 20 ? '★' : v.toFixed(2);

        for (const r of results) {
            const SHORT = r.isGK ? SKILL_NAMES_GK_SHORT : SKILL_NAMES_OUT_SHORT;
            const FULL  = r.isGK ? SKILL_NAMES_GK_FULL  : SKILL_NAMES_OUT_FULL;
            const N = r.isGK ? 11 : 14;

            const rows = [];
            for (let i = 0; i < N; i++) {
                const imp = r.improved.find(x => x.index === i);
                const marker = imp ? (imp.type === 'one_up' ? '⬆+1' : '↑') : '';
                const db   = r.curDbSkillsFull ? fv(r.curDbSkillsFull[i]) : '-';
                const curr = fv(r.newSkillsFull[i]);
                const diff = r.diffSkills
                    ? (Math.abs(r.diffSkills[i]) < 0.005 ? '' : (r.diffSkills[i] >= 0 ? '+' : '') + r.diffSkills[i].toFixed(2))
                    : '';
                rows.push({
                    Skill: SHORT[i],
                    DB: db,
                    New: curr,
                    Diff: diff,
                    Train: marker
                });
            }

            /* Totals row */
            const totalNew = r.newSkillsFull.reduce((s, v) => s + v, 0);
            const totalDb   = r.curDbSkillsFull ? r.curDbSkillsFull.reduce((s, v) => s + v, 0) : null;
            rows.push({
                Skill: 'TOTAL',
                DB: totalDb != null ? totalDb.toFixed(2) : '-',
                New: totalNew.toFixed(2),
                Diff: totalDb != null ? ((totalNew - totalDb) >= 0 ? '+' : '') + (totalNew - totalDb).toFixed(2) : '',
                Train: ''
            });

            const posR5Str = Object.entries(r.r5ByPos).map(([pos, v]) =>
                `${pos}:${v.R5.toFixed(1)}`).join(' ');
            console.log(
                `%c── ${r.name} (#${r.number}) ── Age:${r.ageYears}.${String(r.ageMonths).padStart(2,'0')} | ${r.position} | ASI:${r.asi} | Rtn:${r.routine} | TI:${r.TI}(${r.TI_change>=0?'+':''}${r.TI_change}) | Best:${r.bestPos} R5_DB:${r.R5_DB!=null?r.R5_DB.toFixed(2):'?'} R5_New:${r.R5!=null?r.R5.toFixed(2):'?'} | R5[${posR5Str}] | REC:${r.REC!=null?r.REC.toFixed(2):'?'} | Rem:${r.asiRemainder.toFixed(2)} | DB:${r.curDbSkillsFull?'✓':'✗'}`,
                'font-weight:bold;color:#fbbf24'
            );
            console.table(rows);
        }

        /* ═══ Sync results to IndexedDB ═══ */
        loader.update(0, results.length, 'Syncing to DB...');
        const bar = document.getElementById('tmrc-loader-bar');
        if (bar) bar.style.width = '0%';

        let syncCount = 0;
        for (const r of results) {
            if (r.asi <= 0) { syncCount++; continue; }

            const ageKey = `${r.ageYears}.${r.ageMonths}`;
            let store = PlayerDB.get(r.pid);
            if (!store || !store._v) store = { _v: 1, lastSeen: Date.now(), records: {} };

            store.records[ageKey] = {
                SI: r.asi,
                REREC: r.REC,
                R5: r.R5,
                skills: r.newSkillsFull.map(v => Math.round(v * 100) / 100),
                routine: r.routine,
                locked: true
            };
            store.lastSeen = Date.now();
            if (!store.meta) {
                store.meta = { name: r.name, pos: r.position, isGK: r.isGK };
            }

            await PlayerDB.set(r.pid, store);
            syncCount++;

            const pct = Math.round((syncCount / results.length) * 100);
            if (bar) bar.style.width = pct + '%';
            const txt = document.getElementById('tmrc-loader-text');
            if (txt) txt.textContent = `Syncing ${syncCount}/${results.length} — ${r.name}`;
        }

        console.log(`%c[Squad] ✓ Synced ${syncCount} players to IndexedDB`, 'font-weight:bold;color:#6cc040');
        loader.done(syncCount);

        return results;
    };

    /* ═══════════════════════════════════════════════════════════
       SHARED STATE (tooltip-derived)
       ═══════════════════════════════════════════════════════════ */
    let isGoalkeeper = false;
    let playerRecSort = null;
    let playerRoutine = null;
    let playerAge = null;
    let playerASI = null;
    let playerMonths = null;
    let playerTI = null;
    let playerPosition = null;
    let playerSkillSums = null;
    let tooltipSkills = null;
    let tooltipPlayer = null;

    /* Check if player belongs to the logged-in user's club */
    const getOwnClubIds = () => {
        const s = window.SESSION;
        if (!s) return [];
        const ids = [];
        if (s.main_id) ids.push(String(s.main_id));
        if (s.b_team) ids.push(String(s.b_team));
        return ids;
    };

    /* ── per-tab cache ── */
    const dataLoaded = {};          // key → bool
    let activeMainTab = null;
    let cssInjected = false;

    /* ═══════════════════════════════════════════════════════════
       CSS — Tab bar + History + Scout + Graphs
       (Training CSS lives inside Shadow DOM)
       ═══════════════════════════════════════════════════════════ */
    const CSS = `
/* ── Layout widths ── */
.main_center { width: 1200px !important; }
.column1 { width: 300px !important; margin-right: 8px !important; margin-left: 4px !important; }
.column2_a { width: 550px !important; margin-left: 0 !important; margin-right: 8px !important; }
.column3_a { margin-left: 0 !important; margin-right: 4px !important; }

/* ── Hide native TM tabs ── */
.tabs_outer { display: none !important; }
.tabs_content { display: none !important; }

/* ═══════════════════════════════════════
   SKILLS GRID (tmps-*)
   ═══════════════════════════════════════ */
.tmps-wrap {
    background: #1c3410; border: 1px solid #3d6828; border-radius: 8px;
    overflow: hidden; padding: 0; margin-bottom: 4px;
}
.tmps-grid {
    display: grid; grid-template-columns: 1fr 1fr; gap: 0;
}
.tmps-row {
    display: flex; justify-content: space-between; align-items: center;
    padding: 6px 14px; border-bottom: 1px solid rgba(42,74,28,.35);
}
.tmps-row:last-child { border-bottom: none; }
.tmps-row:hover { background: rgba(255,255,255,.03); }
.tmps-name {
    color: #6a9a58; font-size: 11px; font-weight: 600;
}
.tmps-val {
    font-size: 13px; font-weight: 700; font-variant-numeric: tabular-nums;
}
.tmps-star { font-size: 15px; line-height: 1; }
.tmps-dec  { font-size: 9px; opacity: .75; vertical-align: super; letter-spacing: 0; }
.tmps-divider {
    grid-column: 1 / -1; height: 1px; background: #3d6828; margin: 0;
}
.tmps-hidden {
    display: grid; grid-template-columns: 1fr 1fr; gap: 0;
}
.tmps-hidden .tmps-row {
    padding: 5px 14px;
}
.tmps-hidden .tmps-name { color: #5a7a48; font-size: 10px; }
.tmps-hidden .tmps-val { font-size: 11px; color: #6a9a58; }
.tmps-unlock {
    grid-column: 1 / -1; text-align: center; padding: 10px 14px;
}
.tmps-unlock-btn {
    display: inline-block; padding: 5px 16px;
    background: rgba(42,74,28,.4); border: 1px solid #2a4a1c; border-radius: 6px;
    color: #8aac72; font-size: 11px; font-weight: 600; cursor: pointer;
    transition: all 0.15s; font-family: inherit;
}
.tmps-unlock-btn:hover { background: rgba(42,74,28,.7); color: #c8e0b4; }
.tmps-unlock-btn img { height: 12px; vertical-align: middle; margin-left: 4px; position: relative; top: -1px; }

/* ═══════════════════════════════════════
   PLAYER CARD (tmpc-*)
   ═══════════════════════════════════════ */
.tmpc-card {
    background: #1c3410; border: 1px solid #3d6828; border-radius: 8px;
    overflow: hidden; margin-bottom: 4px;
    font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
.tmpc-header {
    display: flex; gap: 16px; padding: 14px; align-items: flex-start;
}
.tmpc-photo {
    width: 110px; min-width: 110px; border-radius: 6px;
    border: 3px solid #3d6828; display: block;
}
.tmpc-info { flex: 1; min-width: 0; }
.tmpc-top-grid {
    display: grid; grid-template-columns: 1fr auto;
    gap: 2px 8px; align-items: center; margin-bottom: 10px;
}
.tmpc-name {
    font-size: 16px; font-weight: 800; color: #e8f5d8;
    line-height: 1.2;
}
.tmpc-badge-chip {
    font-size: 12px; font-weight: 800; letter-spacing: -0.3px;
    line-height: 16px;
    font-variant-numeric: tabular-nums;
    display: inline-flex; align-items: baseline; gap: 4px;
    padding: 1px 8px; border-radius: 4px;
    background: rgba(232,245,216,0.08); border: 1px solid rgba(232,245,216,0.15);
    justify-self: end;
}
.tmpc-badge-lbl {
    color: #6a9a58; font-size: 9px; font-weight: 600;
    text-transform: uppercase;
}
.tmpc-pos-row {
    display: flex; align-items: center; gap: 6px;
    flex-wrap: wrap;
}
.tmpc-pos {
    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;
}
.tmpc-details {
    display: grid; grid-template-columns: 1fr 1fr; gap: 4px 16px;
}
.tmpc-detail {
    display: flex; justify-content: space-between; align-items: center;
    padding: 3px 0;
}
.tmpc-lbl {
    color: #6a9a58; font-size: 10px; font-weight: 600;
    text-transform: uppercase; letter-spacing: 0.3px;
}
.tmpc-val {
    color: #c8e0b4; font-size: 12px; font-weight: 700;
    font-variant-numeric: tabular-nums;
}
.tmpc-pos-ratings {
    border-top: 1px solid #3d6828; padding: 6px 14px;
}
.tmpc-rating-row {
    display: flex; align-items: center; gap: 10px;
    padding: 5px 0;
}
.tmpc-rating-row + .tmpc-rating-row { border-top: 1px solid rgba(61,104,40,.2); }
.tmpc-pos-bar {
    width: 4px; height: 22px; border-radius: 2px; flex-shrink: 0;
}
.tmpc-pos-name {
    font-size: 11px; font-weight: 700; min-width: 32px;
    letter-spacing: 0.3px;
}
.tmpc-pos-stat {
    display: flex; align-items: baseline; gap: 4px; margin-left: auto;
}
.tmpc-pos-stat + .tmpc-pos-stat { margin-left: 16px; }
.tmpc-pos-stat-lbl {
    color: #6a9a58; font-size: 9px; font-weight: 600;
    text-transform: uppercase; letter-spacing: 0.3px;
}
.tmpc-pos-stat-val {
    font-size: 14px; font-weight: 800; letter-spacing: -0.3px;
    font-variant-numeric: tabular-nums;
}
.tmpc-expand-toggle {
    display: flex; align-items: center; justify-content: center;
    gap: 6px; padding: 4px 0; cursor: pointer;
    border-top: 1px solid rgba(61,104,40,.25);
    color: #6a9a58; font-size: 10px; font-weight: 600;
    letter-spacing: 0.4px; text-transform: uppercase;
    transition: color .15s;
}
.tmpc-expand-toggle:hover { color: #80e048; }
.tmpc-expand-chevron {
    display: inline-block; font-size: 10px; transition: transform .2s;
}
.tmpc-expand-toggle.tmpc-expanded .tmpc-expand-chevron { transform: rotate(180deg); }
.tmpc-all-positions {
    max-height: 0; overflow: hidden; transition: max-height .3s ease;
}
.tmpc-all-positions.tmpc-expanded {
    max-height: 600px;
}
.tmpc-all-positions .tmpc-rating-row.tmpc-is-player-pos {
    background: rgba(61,104,40,.15);
}
.tmpc-rec-stars { font-size: 14px; letter-spacing: 1px; margin-top: 2px; line-height: 1; }
.tmpc-star-full { color: #fbbf24; }
.tmpc-star-half {
    background: linear-gradient(90deg, #fbbf24 50%, #3d6828 50%);
    -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text;
}
.tmpc-star-empty { color: #3d6828; }
.tmpc-flag { vertical-align: middle; margin-left: 4px; }
.tmpc-nt {
    display: inline-flex; align-items: center; gap: 3px;
    font-size: 9px; font-weight: 700; color: #fbbf24;
    background: rgba(251,191,36,.12); border: 1px solid rgba(251,191,36,.25);
    padding: 1px 6px; border-radius: 4px; margin-left: 6px;
    vertical-align: middle; letter-spacing: 0.3px; line-height: 14px;
}

/* ── Column1 Nav (tmcn-*) ── */
.tmcn-nav {
    background: #1c3410; border: 1px solid #3d6828; border-radius: 8px;
    overflow: hidden; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
    margin-bottom: 10px;
}
.tmcn-nav a {
    display: flex; align-items: center; gap: 8px;
    padding: 10px 14px; color: #90b878; font-size: 12px; font-weight: 600;
    text-decoration: none; border-bottom: 1px solid rgba(42,74,28,.5);
    transition: all 0.15s;
}
.tmcn-nav a:last-child { border-bottom: none; }
.tmcn-nav a:hover { background: rgba(42,74,28,.4); color: #e8f5d8; }
.tmcn-nav a .tmcn-icon { font-size: 14px; width: 20px; text-align: center; flex-shrink: 0; }
.tmcn-nav a .tmcn-lbl { flex: 1; }
.column1 > .box { display: none !important; }

/* ── Strip TM box chrome in column2_a ── */
.column2_a > .box,
.column2_a > .box > .box_body { background: none !important; border: none !important; padding: 0 !important; box-shadow: none !important; }
.column2_a > .box > .box_head,
.column2_a .box_shadow,
.column2_a .box_footer,
.column2_a > h3 { display: none !important; }

/* ── Sidebar (column3_a) ── */
.tmps-sidebar {
    font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
.tmps-section {
    background: #1c3410; border: 1px solid #3d6828; border-radius: 8px;
    overflow: hidden; margin-bottom: 8px;
}
.tmps-section-head {
    font-size: 10px; font-weight: 700; color: #6a9a58;
    text-transform: uppercase; letter-spacing: 0.5px;
    padding: 8px 12px 4px; border-bottom: 1px solid rgba(61,104,40,.3);
}
.tmps-btn-list {
    display: flex; flex-direction: column; gap: 2px; padding: 6px;
}
.tmps-btn {
    display: flex; align-items: center; gap: 8px;
    padding: 7px 10px; border-radius: 5px; cursor: pointer;
    background: transparent; border: none; width: 100%;
    font-size: 11px; font-weight: 600; color: #c8e0b4;
    font-family: inherit; text-align: left;
    transition: background 0.12s;
}
.tmps-btn:hover { background: rgba(255,255,255,0.06); }
.tmps-btn-icon {
    width: 16px; height: 16px; display: flex; align-items: center;
    justify-content: center; font-size: 13px; flex-shrink: 0;
}
.tmps-btn.yellow { color: #fbbf24; }
.tmps-btn.red { color: #f87171; }
.tmps-btn.green { color: #4ade80; }
.tmps-btn.blue { color: #60a5fa; }
.tmps-btn.muted { color: #8aac72; }
.tmps-note {
    margin: 0 6px 6px; padding: 6px 10px; border-radius: 5px;
    background: rgba(42,74,28,0.5); border: 1px solid rgba(61,104,40,.3);
    font-size: 11px; color: #8aac72; line-height: 1.4;
}
.tmps-award-list {
    display: flex; flex-direction: column; gap: 0;
}
.tmps-award {
    display: flex; align-items: center; gap: 10px;
    padding: 8px 12px;
}
.tmps-award + .tmps-award { border-top: 1px solid rgba(61,104,40,.2); }
.tmps-award-icon {
    width: 28px; height: 28px; border-radius: 6px; flex-shrink: 0;
    display: flex; align-items: center; justify-content: center;
    font-size: 16px;
}
.tmps-award-icon.gold { background: rgba(212,175,55,0.15); }
.tmps-award-icon.silver { background: rgba(96,165,250,0.15); }
.tmps-award-body { flex: 1; min-width: 0; }
.tmps-award-title {
    font-size: 11px; font-weight: 700; color: #e8f5d8; line-height: 1.2;
}
.tmps-award-sub {
    font-size: 10px; color: #8aac72; line-height: 1.3; margin-top: 1px;
}
.tmps-award-sub a { color: #80e048; text-decoration: none; }
.tmps-award-sub a:hover { text-decoration: underline; }
.tmps-award-season {
    font-size: 11px; font-weight: 800; color: #fbbf24;
    flex-shrink: 0; font-variant-numeric: tabular-nums;
}

/* ── Transfer Live Card (tmtf-*) ── */
.tmtf-card {
    background: #1c3410; border: 1px solid #3d6828; border-radius: 8px;
    overflow: hidden; margin-bottom: 8px;
    font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
.tmtf-head {
    font-size: 10px; font-weight: 700; color: #6a9a58;
    text-transform: uppercase; letter-spacing: 0.5px;
    padding: 8px 12px 4px; border-bottom: 1px solid rgba(61,104,40,.3);
    display: flex; align-items: center; justify-content: space-between;
}
.tmtf-reload {
    background: none; border: none; color: #6a9a58; cursor: pointer;
    font-size: 13px; padding: 0 2px; line-height: 1;
    transition: color .15s;
}
.tmtf-reload:hover { color: #80e048; }
.tmtf-body { padding: 10px 12px; }
.tmtf-row {
    display: flex; justify-content: space-between; align-items: center;
    padding: 4px 0; font-size: 11px; color: #c8e0b4;
}
.tmtf-row + .tmtf-row { border-top: 1px solid rgba(61,104,40,.15); }
.tmtf-lbl { color: #6a9a58; font-weight: 600; font-size: 10px; text-transform: uppercase; }
.tmtf-val { font-weight: 700; font-variant-numeric: tabular-nums; }
.tmtf-val.expiry { color: #fbbf24; }
.tmtf-val.bid { color: #80e048; }
.tmtf-val.buyer { color: #60a5fa; }
.tmtf-val.agent { color: #c084fc; }
.tmtf-val.expired { color: #f87171; }
.tmtf-val.sold { color: #4ade80; }
.tmtf-bid-btn {
    display: flex; align-items: center; justify-content: center; gap: 6px;
    width: 100%; margin-top: 8px; padding: 8px 0;
    background: rgba(108,192,64,0.12); border: 1px solid rgba(108,192,64,0.3);
    border-radius: 6px; color: #80e048; font-size: 11px; font-weight: 700;
    cursor: pointer; transition: background .15s;
    font-family: inherit;
}
.tmtf-bid-btn:hover { background: rgba(108,192,64,0.22); }
.tmtf-spinner { display: inline-block; width: 10px; height: 10px; border: 2px solid #6a9a58; border-top-color: transparent; border-radius: 50%; animation: tmtf-spin 0.6s linear infinite; margin-left: 6px; vertical-align: middle; }
@keyframes tmtf-spin { to { transform: rotate(360deg); } }

/* ── ASI Calculator (tmac-*) ── */
.tmac-card {
    background: #1c3410; border: 1px solid #3d6828; border-radius: 8px;
    overflow: hidden; margin-bottom: 8px; padding: 12px;
    font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
.tmac-head {
    font-size: 10px; font-weight: 700; color: #6a9a58;
    text-transform: uppercase; letter-spacing: 0.5px;
    margin-bottom: 10px; display: flex; align-items: center; gap: 6px;
}
.tmac-head::before { content: '📊'; font-size: 12px; }
.tmac-form { display: flex; flex-direction: column; gap: 8px; }
.tmac-field {
    display: flex; align-items: center; justify-content: space-between; gap: 8px;
}
.tmac-label {
    font-size: 10px; font-weight: 600; color: #90b878;
    text-transform: uppercase; letter-spacing: 0.3px; white-space: nowrap;
}
.tmac-input {
    width: 70px; padding: 5px 8px; border-radius: 4px;
    background: rgba(0,0,0,.25); border: 1px solid rgba(42,74,28,.6);
    color: #e8f5d8; font-size: 12px; font-weight: 600;
    font-family: inherit; text-align: right; outline: none;
    transition: border-color 0.15s;
}
.tmac-input:focus { border-color: #6cc040; }
.tmac-input::placeholder { color: #5a7a48; }
.tmac-result {
    margin-top: 10px; padding: 8px 10px; border-radius: 5px;
    background: rgba(42,74,28,.3); border: 1px solid rgba(42,74,28,.5);
    display: none;
}
.tmac-result.show { display: block; }
.tmac-result-row {
    display: flex; justify-content: space-between; align-items: center;
    padding: 3px 0;
}
.tmac-result-row + .tmac-result-row { border-top: 1px solid rgba(42,74,28,.3); padding-top: 5px; margin-top: 2px; }
.tmac-result-lbl { font-size: 10px; font-weight: 600; color: #6a9a58; text-transform: uppercase; letter-spacing: 0.3px; }
.tmac-result-val { font-size: 13px; font-weight: 700; color: #e8f5d8; font-variant-numeric: tabular-nums; }
.tmac-diff { font-size: 10px; font-weight: 700; color: #6cc040; margin-left: 4px; }

/* ── Main Tab Bar ── */
#tmpe-container {
    margin: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
.tmpe-tabs-bar {
    display: flex; background: #274a18;
    border: 1px solid #3d6828; border-bottom: none;
    border-radius: 8px 8px 0 0; overflow: hidden;
}
.tmpe-main-tab {
    flex: 1; padding: 8px 12px; text-align: center; font-size: 12px; font-weight: 600;
    text-transform: uppercase; letter-spacing: 0.5px; color: #90b878; cursor: pointer;
    border: none; border-bottom: 2px solid transparent; transition: all 0.15s;
    background: transparent; font-family: inherit;
    -webkit-appearance: none; appearance: none;
    display: flex; align-items: center; justify-content: center; gap: 6px;
}
.tmpe-main-tab .tmpe-icon { font-size: 14px; line-height: 1; }
.tmpe-main-tab:hover { color: #c8e0b4; background: #305820; }
.tmpe-main-tab.active { color: #e8f5d8; border-bottom-color: #6cc040; background: #305820; }
.tmpe-panels {
    border: 1px solid #3d6828; border-top: none;
    border-radius: 0 0 8px 8px;
    padding: 0; min-height: 120px;
    background: #1c3410;
}
.tmpe-panel {
    animation: tmpe-fadeIn 0.25s ease-out;
    padding: 8px;
}
@keyframes tmpe-fadeIn {
    from { opacity: 0; transform: translateY(4px); }
    to { opacity: 1; transform: translateY(0); }
}
/* ── Loading spinner ── */
.tmpe-loading {
    display: flex; flex-direction: column; align-items: center; justify-content: center;
    padding: 50px 20px; gap: 14px;
}
.tmpe-spinner {
    width: 28px; height: 28px; border: 3px solid #274a18;
    border-top-color: #6cc040; border-radius: 50%;
    animation: tmpe-spin 0.8s linear infinite;
}
@keyframes tmpe-spin { to { transform: rotate(360deg); } }
.tmpe-loading-text { color: #6a9a58; font-size: 12px; font-weight: 600; letter-spacing: 0.5px; }

/* ═══════════════════════════════════════
   HISTORY (tmph-*)
   ═══════════════════════════════════════ */
#tmph-root {
    display: block; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
    color: #c8e0b4; line-height: 1.4;
}
.tmph-wrap {
    background: transparent; border-radius: 0; border: none; overflow: hidden;
    font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
    color: #c8e0b4; font-size: 13px;
}
.tmph-tabs { display: flex; gap: 6px; padding: 10px 14px 6px; flex-wrap: wrap; }
.tmph-tab {
    padding: 4px 12px; font-size: 11px; font-weight: 600;
    text-transform: uppercase; letter-spacing: 0.4px; color: #90b878; cursor: pointer;
    border-radius: 4px; background: rgba(42,74,28,.3); border: 1px solid rgba(42,74,28,.6);
    transition: all 0.15s; font-family: inherit; -webkit-appearance: none; appearance: none;
}
.tmph-tab:hover { color: #c8e0b4; background: rgba(42,74,28,.5); border-color: #3d6828; }
.tmph-tab.active { color: #e8f5d8; background: #305820; border-color: #3d6828; }
.tmph-body { padding: 6px 14px 16px; font-size: 13px; min-height: 120px; }
.tmph-tbl { width: 100%; border-collapse: collapse; font-size: 11px; margin-bottom: 4px; }
.tmph-tbl th {
    padding: 6px; font-size: 10px; font-weight: 700; color: #6a9a58;
    text-transform: uppercase; letter-spacing: 0.4px; border-bottom: 1px solid #2a4a1c;
    text-align: left; white-space: nowrap;
}
.tmph-tbl th.c { text-align: center; }
.tmph-tbl th.r { text-align: right; }
.tmph-tbl td {
    padding: 5px 6px; border-bottom: 1px solid rgba(42,74,28,.4);
    color: #c8e0b4; font-variant-numeric: tabular-nums; vertical-align: middle;
}
.tmph-tbl td.c { text-align: center; }
.tmph-tbl td.r { text-align: right; }
.tmph-tbl tr:hover { background: rgba(255,255,255,.03); }
.tmph-tbl a { color: #80e048; text-decoration: none; font-weight: 600; }
.tmph-tbl a:hover { color: #c8e0b4; text-decoration: underline; }
.tmph-tbl .tmph-tot td { border-top: 2px solid #3d6828; color: #e0f0cc; font-weight: 800; }
.tmph-transfer td {
    background: rgba(42,74,28,.2); color: #6a9a58; font-size: 10px;
    padding: 4px 6px; border-bottom: 1px solid rgba(42,74,28,.3);
}
.tmph-xfer { display: flex; align-items: center; justify-content: center; gap: 8px; }
.tmph-xfer-arrow { color: #5b9bff; font-size: 13px; line-height: 1; }
.tmph-xfer-label { color: #6a9a58; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px; font-size: 9px; }
.tmph-xfer-sum {
    color: #fbbf24; font-weight: 700; font-size: 11px;
    background: rgba(251,191,36,.08); padding: 1px 8px; border-radius: 3px;
    border: 1px solid rgba(251,191,36,.2);
}
.tmph-div { white-space: nowrap; font-size: 11px; }
.tmph-club { display: flex; align-items: center; gap: 6px; white-space: nowrap; max-width: 200px; overflow: hidden; text-overflow: ellipsis; }
.tmph-r-good { color: #6cc040; }
.tmph-r-avg { color: #c8e0b4; }
.tmph-r-low { color: #f87171; }
.tmph-empty { text-align: center; color: #5a7a48; padding: 40px; font-size: 13px; font-style: italic; }

/* ═══════════════════════════════════════
   SCOUT (tmsc-*)
   ═══════════════════════════════════════ */
#tmsc-root {
    display: block; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
    color: #c8e0b4; line-height: 1.4;
}
.tmsc-wrap {
    background: transparent; border-radius: 0; border: none; overflow: hidden;
    font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
    color: #c8e0b4; font-size: 13px;
}
.tmsc-tabs { display: flex; gap: 6px; padding: 10px 14px 6px; flex-wrap: wrap; }
.tmsc-tab {
    padding: 4px 12px; font-size: 11px; font-weight: 600;
    text-transform: uppercase; letter-spacing: 0.4px; color: #90b878; cursor: pointer;
    border-radius: 4px; background: rgba(42,74,28,.3); border: 1px solid rgba(42,74,28,.6);
    transition: all 0.15s; font-family: inherit; -webkit-appearance: none; appearance: none;
}
.tmsc-tab:hover { color: #c8e0b4; background: rgba(42,74,28,.5); border-color: #3d6828; }
.tmsc-tab.active { color: #e8f5d8; background: #305820; border-color: #3d6828; }
.tmsc-body { padding: 6px 14px 16px; font-size: 13px; min-height: 120px; }
.tmsc-tbl { width: 100%; border-collapse: collapse; font-size: 11px; margin-bottom: 4px; }
.tmsc-tbl th {
    padding: 6px; font-size: 10px; font-weight: 700; color: #6a9a58;
    text-transform: uppercase; letter-spacing: 0.4px; border-bottom: 1px solid #2a4a1c;
    text-align: left; white-space: nowrap;
}
.tmsc-tbl th.c { text-align: center; }
.tmsc-tbl td {
    padding: 5px 6px; border-bottom: 1px solid rgba(42,74,28,.4);
    color: #c8e0b4; font-variant-numeric: tabular-nums; vertical-align: middle;
}
.tmsc-tbl td.c { text-align: center; }
.tmsc-tbl tr:hover { background: rgba(255,255,255,.03); }
.tmsc-tbl a { color: #80e048; text-decoration: none; font-weight: 600; }
.tmsc-tbl a:hover { color: #c8e0b4; text-decoration: underline; }
.tmsc-empty { text-align: center; color: #5a7a48; padding: 40px; font-size: 13px; font-style: italic; }
.tmsc-stars { font-size: 20px; letter-spacing: 2px; line-height: 1; }
.tmsc-star-full { color: #fbbf24; }
.tmsc-star-half {
    background: linear-gradient(90deg, #fbbf24 50%, #3d6828 50%);
    -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text;
}
.tmsc-star-empty { color: #3d6828; }
.tmsc-report { display: flex; flex-direction: column; gap: 14px; }
.tmsc-report-header {
    display: flex; justify-content: space-between; align-items: flex-start;
    padding-bottom: 10px; border-bottom: 1px solid #2a4a1c;
}
.tmsc-report-scout { color: #e8f5d8; font-weight: 700; font-size: 14px; margin-bottom: 4px; }
.tmsc-report-date {
    color: #6a9a58; font-size: 11px; font-weight: 600;
    background: rgba(42,74,28,.4); padding: 3px 10px; border-radius: 4px; white-space: nowrap;
}
.tmsc-report-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
.tmsc-report-item {
    display: flex; justify-content: space-between; align-items: center;
    padding: 5px 10px; background: rgba(42,74,28,.25); border-radius: 4px;
    border: 1px solid rgba(42,74,28,.4);
}
.tmsc-report-label { color: #6a9a58; font-size: 10px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.4px; }
.tmsc-report-value { color: #e8f5d8; font-weight: 700; font-size: 12px; }
.tmsc-report-item.wide { grid-column: 1 / -1; }
.tmsc-section-title {
    color: #6a9a58; font-size: 10px; font-weight: 700; text-transform: uppercase;
    letter-spacing: 0.6px; padding-bottom: 6px; border-bottom: 1px solid #2a4a1c; margin-bottom: 8px;
}
.tmsc-bar-row { display: flex; align-items: center; gap: 10px; padding: 4px 0; }
.tmsc-bar-label { color: #90b878; font-size: 11px; font-weight: 600; width: 100px; flex-shrink: 0; }
.tmsc-bar-track {
    flex: 1; height: 6px; background: #1a2e10; border-radius: 3px;
    overflow: hidden; max-width: 120px; position: relative;
}
.tmsc-bar-fill { height: 100%; border-radius: 3px; transition: width 0.3s; }
.tmsc-bar-fill-reach {
    position: absolute; top: 0; left: 0; height: 100%;
    border-radius: 3px; transition: width 0.3s;
}
.tmsc-bar-text { font-size: 11px; font-weight: 600; min-width: 60px; }
.tmsc-league-cell { white-space: nowrap; font-size: 11px; }
.tmsc-league-cell a { color: #80e048; text-decoration: none; font-weight: 600; }
.tmsc-league-cell a:hover { color: #c8e0b4; text-decoration: underline; }
.tmsc-club-cell a { color: #80e048; text-decoration: none; font-weight: 600; }
.tmsc-club-cell a:hover { color: #c8e0b4; text-decoration: underline; }
.tmsc-send-btn {
    background: rgba(42,74,28,.4); color: #8aac72;
    border: 1px solid #2a4a1c; border-radius: 6px;
    padding: 4px 14px; font-size: 10px; font-weight: 600; cursor: pointer;
    text-transform: uppercase; letter-spacing: 0.4px; transition: all 0.15s; font-family: inherit;
}
.tmsc-send-btn:hover { background: rgba(42,74,28,.7); color: #c8e0b4; }
.tmsc-send-btn:disabled { opacity: 0.4; cursor: not-allowed; }
.tmsc-send-btn.tmsc-away {
    background: transparent; border-color: rgba(61,104,40,.4); color: #5a7a48; font-size: 9px;
}
.tmsc-online { display: inline-block; width: 7px; height: 7px; border-radius: 50%; margin-left: 4px; vertical-align: middle; }
.tmsc-online.on { background: #6cc040; box-shadow: 0 0 4px rgba(108,192,64,.5); }
.tmsc-online.off { background: #3d3d3d; }
.tmsc-yd-badge {
    display: inline-block; background: #274a18; color: #6cc040; font-size: 9px;
    font-weight: 700; padding: 1px 6px; border-radius: 3px; border: 1px solid #3d6828;
    margin-left: 6px; letter-spacing: 0.5px; vertical-align: middle;
}
.tmsc-error {
    text-align: center; color: #f87171; padding: 10px; font-size: 12px; font-weight: 600;
    background: rgba(248,113,113,.06); border: 1px solid rgba(248,113,113,.15);
    border-radius: 4px; margin-bottom: 10px;
}
.tmsc-report-divider { border: none; border-top: 1px dashed #3d6828; margin: 16px 0; }
.tmsc-report-count {
    color: #6a9a58; font-size: 10px; text-align: center; padding: 4px 0;
    font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px;
}
.tmsc-star-green { color: #6cc040; }
.tmsc-star-green-half {
    background: linear-gradient(90deg, #6cc040 50%, #3d6828 50%);
    -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text;
}
.tmsc-star-split {
    background: linear-gradient(90deg, #fbbf24 50%, #6cc040 50%);
    -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text;
}
.tmsc-conf {
    display: inline-block; font-size: 9px; font-weight: 700; padding: 1px 5px;
    border-radius: 3px; margin-left: 6px; letter-spacing: 0.3px;
    vertical-align: middle; white-space: nowrap;
}
.tmsc-best-wrap {
    background: rgba(42,74,28,.3); border: 1px solid #2a4a1c;
    border-radius: 6px; padding: 12px; margin-bottom: 6px;
}
.tmsc-best-title {
    color: #6cc040; font-size: 10px; font-weight: 700; text-transform: uppercase;
    letter-spacing: 0.6px; margin-bottom: 10px; display: flex; align-items: center; gap: 6px;
}
.tmsc-best-title::before { content: '★'; font-size: 13px; }

/* ═══════════════════════════════════════
   GRAPHS (tmg-*)
   ═══════════════════════════════════════ */
.tmg-chart-wrap {
    position: relative; background: rgba(0,0,0,0.18);
    border: 1px solid rgba(120,180,80,0.25); border-radius: 6px;
    padding: 6px 4px 4px; margin: 6px 0 10px;
}
.tmg-chart-title { font-size: 13px; font-weight: 700; color: #e8f5d8; padding: 2px 8px 4px; letter-spacing: 0.3px; }
.tmg-canvas { display: block; cursor: crosshair; }
.tmg-tooltip {
    position: absolute; background: rgba(0,0,0,0.88); color: #fff;
    padding: 5px 10px; border-radius: 4px; font-size: 11px; pointer-events: none;
    z-index: 1000; white-space: nowrap; display: none;
    border: 1px solid rgba(255,255,255,0.15); box-shadow: 0 2px 8px rgba(0,0,0,0.4);
}
.tmg-legend {
    display: grid; grid-template-columns: 1fr 1fr; gap: 1px 12px;
    padding: 8px 12px 4px; max-width: 450px; margin: 0 auto;
}
.tmg-legend.tmg-legend-inline {
    grid-template-columns: repeat(3, auto); justify-content: center; gap: 1px 18px;
}
.tmg-legend-item {
    display: flex; align-items: center; gap: 3px; font-size: 11px;
    color: #ccc; cursor: pointer; user-select: none; padding: 1px 0;
}
.tmg-legend-item input[type="checkbox"] {
    appearance: none; -webkit-appearance: none; width: 13px; height: 13px; min-width: 13px;
    border: 1px solid rgba(255,255,255,0.25); border-radius: 2px; cursor: pointer; margin: 0;
}
.tmg-legend-dot { font-size: 9px; line-height: 1; }
.tmg-legend-toggle { display: flex; gap: 6px; justify-content: center; padding: 4px 0 6px; }
.tmg-export-btn {
    background: rgba(42,74,28,.5); color: #8aac72; border: 1px solid #3d6828;
    border-radius: 5px; padding: 2px 10px; font-size: 10px; cursor: pointer;
    font-family: inherit; letter-spacing: 0.3px;
}
.tmg-export-btn:hover { background: #305820; color: #c8e0b4; }
.tmg-btn {
    background: rgba(42,74,28,.4); color: #8aac72; border: 1px solid #2a4a1c;
    border-radius: 6px; padding: 4px 14px; font-size: 11px; cursor: pointer; font-family: inherit;
    font-weight: 600; transition: all 0.15s; text-transform: uppercase; letter-spacing: 0.4px;
}
.tmg-btn:hover { background: rgba(42,74,28,.7); color: #c8e0b4; }
.tmg-enable-card {
    background: rgba(0,0,0,0.18); border: 1px solid rgba(120,180,80,0.25);
    border-radius: 6px; padding: 14px 16px; margin: 6px 0 10px;
    display: flex; align-items: center; justify-content: space-between; gap: 12px;
}
.tmg-enable-title { font-size: 13px; font-weight: 700; color: #6a9a58; letter-spacing: 0.3px; }
.tmg-enable-desc { font-size: 11px; color: #5a7a48; margin-top: 2px; }
.tmg-enable-btn {
    display: inline-flex; align-items: center; gap: 4px;
    padding: 6px 16px; background: rgba(42,74,28,.5); border: 1px solid #3d6828;
    border-radius: 6px; color: #80e048; font-size: 11px; font-weight: 700;
    cursor: pointer; transition: all 0.15s; font-family: inherit;
    text-transform: uppercase; letter-spacing: 0.4px; white-space: nowrap;
    -webkit-appearance: none; appearance: none;
}
.tmg-enable-btn:hover { background: #305820; color: #e8f5d8; border-color: #4a8030; }
.tmg-enable-btn .pro_icon { height: 12px; vertical-align: middle; position: relative; top: -1px; }
.tmg-enable-all {
    display: flex; justify-content: center; padding: 4px 0 8px;
}
.tmg-skill-arrow { font-size: 9px; margin-left: 1px; }

/* ═══════════════════════════════════════
   BEST ESTIMATE CARD (tmbe-*)
   ═══════════════════════════════════════ */
.tmbe-card {
    background: #1c3410; border: 1px solid #3d6828; border-radius: 8px;
    overflow: hidden; margin-bottom: 4px; padding: 14px;
    font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
.tmbe-title {
    color: #6a9a58; font-size: 10px; font-weight: 700; text-transform: uppercase;
    letter-spacing: 0.6px; margin-bottom: 12px; display: flex; align-items: center; gap: 8px;
}
.tmbe-title::before { content: '★'; font-size: 13px; color: #fbbf24; }
.tmbe-title-stars { font-size: 18px; letter-spacing: 1px; line-height: 1; margin-left: auto; }
.tmbe-grid {
    display: grid; grid-template-columns: 1fr; gap: 6px; margin-bottom: 14px;
}
.tmbe-item {
    display: flex; justify-content: space-between; align-items: center;
    padding: 6px 10px; background: rgba(42,74,28,.25); border-radius: 4px;
    border: 1px solid rgba(42,74,28,.4);
}
.tmbe-lbl { color: #6a9a58; font-size: 10px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.4px; }
.tmbe-val { color: #e8f5d8; font-weight: 700; font-size: 12px; }
.tmbe-divider {
    color: #6a9a58; font-size: 9px; font-weight: 700; text-transform: uppercase;
    letter-spacing: 0.5px; padding: 8px 0 2px; margin-top: 2px;
    border-top: 1px solid rgba(42,74,28,.5);
}
.tmbe-peak-item {
    flex-direction: column !important; align-items: stretch !important; gap: 6px; padding: 8px 10px !important;
}
.tmbe-peak-header {
    display: flex; justify-content: space-between; align-items: center;
}
.tmbe-peak-reach {
    font-size: 10px; font-weight: 700; line-height: 1;
    display: flex; align-items: center; gap: 12px;justify-content: space-between;
}
.tmbe-reach-item {
    display: flex; align-items: center; gap: 4px;
}
.tmbe-reach-tag {
    font-size: 8px; font-weight: 600; text-transform: uppercase;
    letter-spacing: 0.3px; color: #5a7a48;
}
.tmbe-bar-row {
    display: flex; flex-direction: column; gap: 3px; padding: 6px 0;
    border-bottom: 1px solid rgba(42,74,28,.3);
}
.tmbe-bar-row:last-child { border-bottom: none; }
.tmbe-bar-header {
    display: flex; align-items: center; justify-content: space-between;
}
.tmbe-bar-label { color: #90b878; font-size: 11px; font-weight: 600; }
.tmbe-bar-right { display: flex; align-items: center; gap: 8px; }
.tmbe-bar-track {
    width: 100%; height: 6px; background: rgba(0,0,0,.3); border-radius: 3px;
    overflow: hidden; position: relative;
}
.tmbe-bar-fill { height: 100%; border-radius: 3px; transition: width 0.3s; }
.tmbe-bar-fill-reach {
    position: absolute; top: 0; left: 0; height: 100%;
    border-radius: 3px; transition: width 0.3s;
}
.tmbe-bar-val { font-size: 12px; font-weight: 700; font-variant-numeric: tabular-nums; }
.tmbe-conf {
    display: inline-block; font-size: 9px; font-weight: 700; padding: 1px 5px;
    border-radius: 3px; margin-left: 6px; letter-spacing: 0.3px;
    vertical-align: middle; white-space: nowrap;
}
/* ═══════════════════════════════════════
   COMPARE MODAL (tmc-*)
   ═══════════════════════════════════════ */
.tmc-overlay {
    position: fixed; inset: 0; background: rgba(0,0,0,0.75); z-index: 99999;
    display: flex; align-items: center; justify-content: center;
}
.tmc-modal {
    background: #1a3311; border: 1px solid #3d6828; border-radius: 10px;
    width: 500px; max-width: 96vw; max-height: 90vh; display: flex; flex-direction: column;
    box-shadow: 0 8px 40px rgba(0,0,0,0.7); overflow: hidden;
}
.tmc-modal-header {
    display: flex; align-items: center; justify-content: space-between;
    padding: 12px 16px; background: #274a18; border-bottom: 1px solid #3d6828;
    font-weight: 700; color: #e8f5d8; font-size: 14px; flex-shrink: 0;
}
.tmc-close-btn { background: none; border: none; color: #90b878; cursor: pointer; font-size: 16px; padding: 0 4px; line-height: 1; }
.tmc-close-btn:hover { color: #e8f5d8; }
.tmc-modal-body { flex: 1; overflow-y: auto; padding: 12px 14px; min-height: 0; }
.tmc-input-row { display: flex; align-items: center; gap: 8px; margin-bottom: 8px; }
.tmc-input-icon { font-size: 14px; flex-shrink: 0; }
.tmc-input {
    flex: 1; background: rgba(0,0,0,0.3); border: 1px solid #3d6828; border-radius: 5px;
    color: #e8f5d8; padding: 6px 10px; font-size: 12px; font-family: inherit; outline: none;
}
.tmc-input:focus { border-color: #6cc040; }
.tmc-player-list { margin-top: 8px; max-height: 340px; overflow-y: auto; border: 1px solid rgba(61,104,40,0.4); border-radius: 6px; }
.tmc-player-row { display: flex; align-items: center; gap: 10px; padding: 8px 12px; cursor: pointer; border-bottom: 1px solid rgba(61,104,40,0.25); transition: background 0.1s; }
.tmc-player-row:last-child { border-bottom: none; }
.tmc-player-row:hover { background: rgba(108,192,64,0.12); }
.tmc-row-name { flex: 1; color: #e8f5d8; font-size: 12px; font-weight: 600; }
.tmc-row-sub { font-size: 10px; color: #8aac72; }
.tmc-row-count { font-size: 10px; color: #5a7a48; font-weight: 700; }
.tmc-list-header { padding: 5px 12px 3px; font-size: 10px; font-weight: 700; text-transform: uppercase; letter-spacing: .06em; color: #5a7a48; background: rgba(61,104,40,0.18); border-bottom: 1px solid rgba(61,104,40,0.25); }
.tmc-squad-badge { display: inline-block; font-size: 9px; font-weight: 700; line-height: 1; padding: 1px 4px; border-radius: 3px; background: #2d5a1a; color: #a8d888; margin-left: 4px; vertical-align: middle; }
.tmc-empty-list, .tmc-loading-msg { padding: 24px; text-align: center; color: #5a7a48; font-size: 12px; font-style: italic; }
.tmc-error-msg { padding: 24px; text-align: center; color: #f87171; font-size: 12px; }
.tmc-back-btn { background: rgba(42,74,28,.5); color: #8aac72; border: 1px solid #3d6828; border-radius: 5px; padding: 4px 12px; font-size: 11px; cursor: pointer; font-family: inherit; margin-bottom: 12px; display: block; }
.tmc-back-btn:hover { background: #305820; color: #c8e0b4; }
.tmc-compare-wrap { font-size: 12px; }
.tmc-compare-header { display: flex; align-items: center; gap: 0; margin-bottom: 14px; padding-bottom: 12px; border-bottom: 1px solid rgba(61,104,40,0.4); }
.tmc-compare-col { flex: 1; text-align: center; }
.tmc-compare-vs { width: 32px; height: 32px; border-radius: 50%; background: rgba(61,104,40,0.4); color: #5a7a48; font-weight: 800; font-size: 10px; display: flex; align-items: center; justify-content: center; flex-shrink: 0; }
.tmc-player-name { color: #e8f5d8; font-weight: 700; font-size: 13px; }
.tmc-player-sub { color: #8aac72; font-size: 10px; margin-top: 2px; }
.tmc-section-title { font-size: 10px; font-weight: 700; color: #5a7a48; text-transform: uppercase; letter-spacing: 0.8px; margin: 12px 0 6px; padding-bottom: 3px; border-bottom: 1px solid rgba(61,104,40,0.3); }
.tmc-stat-grid { display: flex; gap: 6px; margin-bottom: 4px; }
.tmc-stat-card { flex: 1; background: rgba(42,74,28,0.35); border: 1px solid rgba(61,104,40,0.3); border-radius: 6px; padding: 8px 4px; text-align: center; }
.tmc-stat-card-label { font-size: 9px; font-weight: 700; color: #5a7a48; text-transform: uppercase; letter-spacing: .5px; margin-bottom: 6px; }
.tmc-stat-card-vals { display: flex; align-items: center; justify-content: center; gap: 6px; }
.tmc-stat-card-v { font-weight: 700; font-size: 14px; }
.tmc-stat-card-sep { color: #3d6828; font-size: 10px; }
.tmc-skill-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 0; }
.tmc-skill-cell { display: flex; align-items: center; padding: 5px 8px; border-bottom: 1px solid rgba(61,104,40,0.15); gap: 6px; }
.tmc-skill-cell:nth-last-child(-n+2) { border-bottom: none; }
.tmc-skill-name { color: #8aac72; font-size: 11px; white-space: nowrap; flex: 1; }
.tmc-skill-vals { display: flex; align-items: baseline; gap: 1px; font-size: 12px; white-space: nowrap; }
.tmc-skill-v { font-weight: 400; font-size: 11px; }
.tmc-skill-v.win { font-weight: 800; font-size: 13px; }
.tmc-skill-sep { color: #3d6828; font-size: 10px; margin: 0 1px; }
`;

    const injectCSS = () => {
        if (cssInjected) return;
        const s = document.createElement('style');
        s.textContent = CSS;
        document.head.appendChild(s);
        cssInjected = true;
    };

    /* ═══════════════════════════════════════════════════════════
       COMPARE MODULE
       ═══════════════════════════════════════════════════════════ */
    const CompareMod = (() => {
        const NAMES_OUT = ['Strength', 'Stamina', 'Pace', 'Marking', 'Tackling', 'Workrate', 'Positioning', 'Passing', 'Crossing', 'Technique', 'Heading', 'Finishing', 'Longshots', 'Set Pieces'];
        const NAMES_GK = ['Strength', 'Stamina', 'Pace', 'Handling', 'One on ones', 'Reflexes', 'Aerial Ability', 'Jumping', 'Communication', 'Kicking', 'Throwing'];

        const fPos = p => p ? p.split(',').map(s => s.trim().toUpperCase()).join('/') : '–';
        const fAge = (y, m) => `${y}y ${m}m`;
        const fv = (v, d = 2) => v == null ? '–' : Number(v).toFixed(d);

        const latestKey = store => {
            const keys = Object.keys(store.records || {});
            if (!keys.length) return null;
            return keys.sort((a, b) => {
                const [ay, am] = a.split('.').map(Number), [by, bm] = b.split('.').map(Number);
                return (ay * 12 + am) - (by * 12 + bm);
            }).at(-1);
        };

        /* Extract integer skill array from tooltip-style skills [{name,value}] */
        const extractSkills = (skillsArr, isGK) => {
            if (!skillsArr || !skillsArr.length) return null;
            const names = isGK ? NAMES_GK : NAMES_OUT;
            const sv = (name) => {
                const sk = skillsArr.find(s => s.name === name);
                if (!sk) return 0;
                const v = sk.value;
                if (typeof v === 'string') {
                    if (v.includes('star_silver')) return 19;
                    if (v.includes('star')) return 20;
                    return parseInt(v) || 0;
                }
                return parseInt(v) || 0;
            };
            return names.map(sv);
        };

        /* Compute R5 & REC for first favposition */
        const computeR5REC = (favpos, skills, asi, routine) => {
            if (!skills || !skills.length || !asi) return { R5: null, REREC: null };
            const firstPos = (favpos || '').split(',')[0].trim();
            const posIdx = getPositionIndex(firstPos);
            const rou = routine || 0;
            const r5 = Number(calculateR5F(posIdx, skills, asi, rou));
            const rec = Number(calculateRemaindersF(posIdx, skills, asi).rec);
            return { R5: r5, REREC: rec };
        };

        const currentData = () => {
            const store = PlayerDB.get(PLAYER_ID);
            const lk = store ? latestKey(store) : null;
            const rec = lk && store ? store.records[lk] : null;
            let R5 = rec?.R5 ?? null;
            let REREC = rec?.REREC ?? null;
            let skills = rec?.skills ?? null;
            const asi = rec ? parseInt(rec.SI) || 0 : (playerASI || 0);
            /* Compute R5/REC from tooltip skills if store doesn't have them */
            if ((R5 == null || REREC == null) && tooltipSkills && asi > 0) {
                const intSkills = extractSkills(tooltipSkills, isGoalkeeper);
                if (intSkills && intSkills.some(s => s > 0)) {
                    const computed = computeR5REC(playerPosition, intSkills, asi, playerRoutine || 0);
                    if (R5 == null) R5 = computed.R5;
                    if (REREC == null) REREC = computed.REREC;
                    if (!skills) skills = intSkills;
                }
            }
            return {
                id: PLAYER_ID,
                name: store?.meta?.name || tooltipPlayer?.name || `Player #${PLAYER_ID}`,
                pos: fPos(store?.meta?.pos || playerPosition || ''),
                years: tooltipPlayer ? parseInt(tooltipPlayer.age) || 0 : null,
                months: tooltipPlayer ? parseInt(tooltipPlayer.months) || 0 : null,
                isGK: isGoalkeeper,
                SI: asi,
                R5,
                REREC,
                skills,
                records: store ? Object.keys(store.records || {}).length : 0
            };
        };

        const buildCompareHtml = (a, b) => {
            const skillNames = (a.isGK || b.isGK) ? NAMES_GK : NAMES_OUT;
            const clr = (av, bv) => {
                if (av == null || bv == null) return '#8aac72';
                if (av === bv) return '#ccc';
                return av > bv ? '#80e048' : '#f87171';
            };
            let h = `<div class="tmc-compare-wrap">`;
            h += `<div class="tmc-compare-header">
                <div class="tmc-compare-col">
                    <div class="tmc-player-name">${a.name}</div>
                    <div class="tmc-player-sub">${a.pos}${a.years != null ? ' · ' + fAge(a.years, a.months) : ''}</div>
                </div>
                <div class="tmc-compare-vs">VS</div>
                <div class="tmc-compare-col">
                    <div class="tmc-player-name">${b.name}</div>
                    <div class="tmc-player-sub">${b.pos}${b.years != null ? ' · ' + fAge(b.years, b.months) : ''}</div>
                </div>
            </div>`;
            h += `<div class="tmc-stat-grid">`;
            for (const [lbl, ak, bk, dec] of [['ASI', 'SI', 'SI', 0], ['R5', 'R5', 'R5', 2], ['REC', 'REREC', 'REREC', 2]]) {
                const av = a[ak], bv = b[bk];
                h += `<div class="tmc-stat-card">
                    <div class="tmc-stat-card-label">${lbl}</div>
                    <div class="tmc-stat-card-vals">
                        <span class="tmc-stat-card-v" style="color:${clr(av, bv)}">${fv(av, dec)}</span>
                        <span class="tmc-stat-card-sep">:</span>
                        <span class="tmc-stat-card-v" style="color:${clr(bv, av)}">${fv(bv, dec)}</span>
                    </div>
                </div>`;
            }
            h += `</div>`;
            if (a.skills || b.skills) {
                const clrA = '#6cc040', clrB = '#4a9fd6';
                h += `<div class="tmc-section-title">Skills <span style="font-weight:400;letter-spacing:0">(</span><span style="color:${clrA};font-weight:400;letter-spacing:0">${a.name.split(' ').pop()}</span> <span style="font-weight:400;letter-spacing:0">vs</span> <span style="color:${clrB};font-weight:400;letter-spacing:0">${b.name.split(' ').pop()}</span><span style="font-weight:400;letter-spacing:0">)</span></div><div class="tmc-skill-grid">`;
                const half = Math.ceil(skillNames.length / 2);
                const fmtVal = (v, isWin, clr) => {
                    if (v == null) return `<span class="tmc-skill-v" style="color:#555">–</span>`;
                    const fl = Math.floor(v);
                    if (fl >= 20) return `<span class="tmc-skill-v${isWin ? ' win' : ''}" style="color:gold">★</span>`;
                    if (fl >= 19) return `<span class="tmc-skill-v${isWin ? ' win' : ''}" style="color:silver">★</span>`;
                    return `<span class="tmc-skill-v${isWin ? ' win' : ''}" style="color:${clr}">${fl}</span>`;
                };
                for (let r = 0; r < half; r++) {
                    for (const ci of [r, r + half]) {
                        if (ci >= skillNames.length) { h += `<div class="tmc-skill-cell"></div>`; continue; }
                        const name = skillNames[ci];
                        const av = a.skills?.[ci] ?? null, bv = b.skills?.[ci] ?? null;
                        const aWin = av != null && bv != null && av > bv;
                        const bWin = av != null && bv != null && bv > av;
                        h += `<div class="tmc-skill-cell">
                            <div class="tmc-skill-name">${name}</div>
                            <div class="tmc-skill-vals">${fmtVal(av, aWin, clrA)}<span class="tmc-skill-sep">/</span>${fmtVal(bv, bWin, clrB)}</div>
                        </div>`;
                    }
                }
                h += `</div>`;
            }
            h += `</div>`;
            return h;
        };

        let overlay = null;

        const showView = (html, backFn) => {
            const body = overlay.querySelector('.tmc-modal-body');
            body.innerHTML = `<button class="tmc-back-btn">← Back</button>${html}`;
            body.querySelector('.tmc-back-btn').addEventListener('click', backFn || renderSelection);
        };

        let squadCache = null; /* { list: [{pid, name, fp, team}], post: {pid: playerObj} } */

        const fetchSquad = (cb) => {
            if (squadCache) { cb(squadCache); return; }
            const s = window.SESSION;
            const mainId = s?.main_id ? String(s.main_id) : null;
            const bId    = s?.b_team  ? String(s.b_team)  : null;
            if (!mainId) { squadCache = { list: [], post: {} }; cb(squadCache); return; }
            const parse = (res, team) => {
                try {
                    const data = typeof res === 'object' ? res : JSON.parse(res);
                    const list = (data?.squad || []).map(p => ({
                        pid: String(p.player_id),
                        name: p.name || `#${p.player_id}`,
                        fp: p.fp || '',
                        team
                    }));
                    const post = {};
                    if (data?.post) { for (const [id, p] of Object.entries(data.post)) post[String(id)] = p; }
                    return { list, post };
                } catch(e) { return { list: [], post: {} }; }
            };
            $.post('/ajax/players_get_select.ajax.php', { type: 'change', club_id: mainId }, res => {
                const main = parse(res, 'A');
                if (!bId) { squadCache = main; cb(squadCache); return; }
                $.post('/ajax/players_get_select.ajax.php', { type: 'change', club_id: bId }, res2 => {
                    const b = parse(res2, 'B');
                    squadCache = { list: [...main.list, ...b.list], post: { ...main.post, ...b.post } };
                    cb(squadCache);
                }).fail(() => { squadCache = main; cb(squadCache); });
            }).fail(() => { squadCache = { list: [], post: {} }; cb(squadCache); });
        };

        const renderSelection = () => {
            const body = overlay.querySelector('.tmc-modal-body');
            body.innerHTML = `
                <div class="tmc-input-row"><span class="tmc-input-icon">🔗</span><input class="tmc-input" id="tmc-url-input" placeholder="Paste player URL (e.g. /players/12345678/)…"></div>
                <div class="tmc-input-row"><span class="tmc-input-icon">🔍</span><input class="tmc-input" id="tmc-search-input" placeholder="Search players…"></div>
                <div id="tmc-player-list" class="tmc-player-list"><div class="tmc-empty-list">Loading squad…</div></div>`;
            const urlIn = body.querySelector('#tmc-url-input');
            urlIn.addEventListener('input', () => {
                const m = urlIn.value.match(/\/players\/(\d+)/);
                if (m) { urlIn.style.borderColor = '#6cc040'; startCompare(m[1]); }
                else urlIn.style.borderColor = '';
            });
            body.querySelector('#tmc-search-input').addEventListener('input', e => renderList(e.target.value.toLowerCase()));
            fetchSquad(() => renderList(''));
        };

        const renderList = (filter) => {
            const list = overlay.querySelector('#tmc-player-list');
            if (!list) return;
            if (!squadCache || !squadCache.list.length) { list.innerHTML = '<div class="tmc-empty-list">No players found</div>'; return; }
            const rows = squadCache.list
                .filter(p => String(p.pid) !== String(PLAYER_ID) && (!filter || p.name.toLowerCase().includes(filter) || p.pid.includes(filter)))
                .map(p =>
                    `<div class="tmc-player-row" data-pid="${p.pid}">
                        <div><div class="tmc-row-name">${p.name}${p.team === 'B' ? ' <span class="tmc-squad-badge">B</span>' : ''}</div><div class="tmc-row-sub">${p.fp}</div></div>
                    </div>`
                );
            if (!rows.length) { list.innerHTML = '<div class="tmc-empty-list">No players found</div>'; return; }
            list.innerHTML = rows.join('');
            list.querySelectorAll('.tmc-player-row').forEach(row => row.addEventListener('click', () => startCompare(row.dataset.pid)));
        };

        const startCompare = (oppId) => {
            oppId = String(oppId);
            showView('<div class="tmc-loading-msg">⏳ Loading player data…</div>');
            const a = currentData();

            const buildOpp = (p, oppStore) => {
                const lk = oppStore ? latestKey(oppStore) : null;
                const rec = lk ? oppStore.records[lk] : null;
                const fp = p.favposition || '';
                const oppIsGK = fp.split(',')[0].trim().toLowerCase() === 'gk';
                const asi = rec ? parseInt(rec.SI) || 0 : (parseInt((p.asi || p.skill_index || '').toString().replace(/[^0-9]/g, '')) || 0);
                let R5 = rec?.R5 ?? null;
                let REREC = rec?.REREC ?? null;
                let skills = rec?.skills ?? null;
                /* Compute R5/REC from skills if store doesn't have them */
                if ((R5 == null || REREC == null) && p.skills && asi > 0) {
                    const intSkills = extractSkills(p.skills, oppIsGK);
                    if (intSkills && intSkills.some(s => s > 0)) {
                        const rou = rec?.routine || parseFloat(p.routine) || 0;
                        const computed = computeR5REC(fp, intSkills, asi, rou);
                        if (R5 == null) R5 = computed.R5;
                        if (REREC == null) REREC = computed.REREC;
                        if (!skills) skills = intSkills;
                    }
                }
                return {
                    id: oppId,
                    name: p.name || p.player_name || p.player_name_long || `Player #${oppId}`,
                    pos: p.fp ? p.fp : fPos(fp),
                    years: parseInt(p.age) || 0,
                    months: parseInt(p.months || p.month) || 0,
                    isGK: oppIsGK,
                    SI: asi,
                    R5,
                    REREC,
                    skills,
                    records: oppStore ? Object.keys(oppStore.records || {}).length : 0
                };
            };

            /* Priority 1: cached post data from squad endpoint (own team) */
            const cachedPost = squadCache?.post?.[oppId];
            if (cachedPost) {
                const oppStore = PlayerDB.get(oppId);
                showView(buildCompareHtml(a, buildOpp(cachedPost, oppStore)));
                return;
            }

            /* Priority 2: tooltip endpoint (external players) */
            $.post('/ajax/tooltip.ajax.php', { player_id: oppId }, res => {
                try {
                    const data = typeof res === 'object' ? res : JSON.parse(res);
                    const p = data?.player;
                    if (!p) { showView('<div class="tmc-error-msg">⚠ Player not found</div>'); return; }
                    const oppStore = PlayerDB.get(oppId);
                    showView(buildCompareHtml(a, buildOpp(p, oppStore)));
                } catch (e) { showView('<div class="tmc-error-msg">⚠ Failed to load data</div>'); }
            }).fail(() => showView('<div class="tmc-error-msg">⚠ Network error</div>'));
        };

        const openDialog = () => {
            if (!overlay) {
                overlay = document.createElement('div');
                overlay.className = 'tmc-overlay';
                overlay.innerHTML = `<div class="tmc-modal"><div class="tmc-modal-header"><span>⚖️ Compare Player</span><button class="tmc-close-btn">✕</button></div><div class="tmc-modal-body"></div></div>`;
                document.body.appendChild(overlay);
                overlay.querySelector('.tmc-close-btn').addEventListener('click', () => overlay.style.display = 'none');
                overlay.addEventListener('click', e => { if (e.target === overlay) overlay.style.display = 'none'; });
            }
            overlay.style.display = 'flex';
            renderSelection();
        };

        window.tmCompareOpen = openDialog;
        return { openDialog };
    })();

    /* ═══════════════════════════════════════════════════════════
       PARSE NATIONAL TEAM STATS (before DOM is modified)
       ═══════════════════════════════════════════════════════════ */
    let parsedNTData = null;
    const parseNTData = () => {
        const h3s = document.querySelectorAll('h3.dark');
        for (const h3 of h3s) {
            const txt = h3.textContent;
            if (!txt.includes('Called up for') && !txt.includes('Previously played for')) continue;
            const countryLink = h3.querySelector('a.country_link');
            const countryName = countryLink ? countryLink.textContent.trim() : '';
            const flagLinks = h3.querySelectorAll('.country_link');
            const flagEl = flagLinks.length > 1 ? flagLinks[flagLinks.length - 1] : flagLinks[0];
            const flagHtml = flagEl ? flagEl.outerHTML : '';
            const nextDiv = h3.nextElementSibling;
            const table = nextDiv && nextDiv.querySelector('table');
            if (!table) continue;
            const tds = table.querySelectorAll('tr:not(:first-child) td, tr.odd td');
            if (tds.length >= 6) {
                /* Hide TM's original NT section */
                h3.style.display = 'none';
                if (nextDiv) nextDiv.style.display = 'none';
                return {
                    country: countryName,
                    flagHtml: flagHtml,
                    matches: parseInt(tds[0].textContent) || 0,
                    goals: parseInt(tds[1].textContent) || 0,
                    assists: parseInt(tds[2].textContent) || 0,
                    cards: parseInt(tds[3].textContent) || 0,
                    rating: parseFloat(tds[4].textContent) || 0,
                    mom: parseInt(tds[5].textContent) || 0
                };
            }
        }
        return null;
    };

    /* ═══════════════════════════════════════════════════════════
       SHARED TOOLTIP FETCH
       ═══════════════════════════════════════════════════════════ */
    const fetchTooltip = () => {
        $.post('/ajax/tooltip.ajax.php', { player_id: PLAYER_ID }, (res) => {
            try {
                const data = typeof res === 'object' ? res : JSON.parse(res);
                if (!data || !data.player) return;
                const p = data.player;

                /* Retired / deleted player — club is null → clean up storage */
                if (p.club_id === null || data.club === null) {
                    if (PlayerDB.get(PLAYER_ID)) {
                        PlayerDB.remove(PLAYER_ID);
                        console.log(`%c[Cleanup] Removed retired/deleted player ${PLAYER_ID} from DB`, 'font-weight:bold;color:#f87171');
                    }
                    return;
                }
                const fp = p.favposition || '';
                isGoalkeeper = fp.split(',')[0].toLowerCase() === 'gk';
                if (p.rec_sort !== undefined) playerRecSort = parseFloat(p.rec_sort) || 0;
                if (p.age !== undefined) {
                    playerAge = (parseInt(p.age) || 0) + (parseInt(p.months) || 0) / 12;
                    playerMonths = parseInt(p.months) || 0;
                }
                if (p.favposition !== undefined) playerPosition = p.favposition;
                tooltipPlayer = p;

                /* Migrate meta.pos if missing for existing store */
                try {
                    const existingStore = PlayerDB.get(PLAYER_ID);
                    if (existingStore && p.favposition) {
                        const fp = p.favposition || '';
                        const pGK = fp.split(',')[0].toLowerCase() === 'gk';
                        if (!existingStore.meta) {
                            existingStore.meta = { name: p.name || '', pos: fp, isGK: pGK };
                            PlayerDB.set(PLAYER_ID, existingStore);
                            console.log(`[TmPlayer] Migrated meta for player ${PLAYER_ID}: pos=${fp}`);
                        } else if (!existingStore.meta.pos) {
                            existingStore.meta.pos = fp;
                            existingStore.meta.isGK = pGK;
                            if (!existingStore.meta.name && p.name) existingStore.meta.name = p.name;
                            PlayerDB.set(PLAYER_ID, existingStore);
                            console.log(`[TmPlayer] Migrated meta.pos for player ${PLAYER_ID}: pos=${fp}`);
                        }
                    }
                } catch (e) { /* non-critical */ }

                if (p.skills) {
                    tooltipSkills = p.skills;
                    const sv = (name) => {
                        const sk = p.skills.find(s => s.name === name);
                        if (!sk) return 0;
                        const v = sk.value;
                        if (typeof v === 'string' && v.includes('star')) return v.includes('silver') ? 19 : 20;
                        return parseInt(v) || 0;
                    };
                    if (isGoalkeeper) {
                        playerSkillSums = {
                            phy: sv('Strength') + sv('Stamina') + sv('Pace') + sv('Jumping'),
                            tac: sv('One on ones') + sv('Aerial Ability') + sv('Communication'),
                            tec: sv('Handling') + sv('Reflexes') + sv('Kicking') + sv('Throwing')
                        };
                    } else {
                        playerSkillSums = {
                            phy: sv('Strength') + sv('Stamina') + sv('Pace') + sv('Heading'),
                            tac: sv('Marking') + sv('Tackling') + sv('Workrate') + sv('Positioning'),
                            tec: sv('Passing') + sv('Crossing') + sv('Technique') + sv('Finishing') + sv('Longshots') + sv('Set Pieces')
                        };
                    }
                }
                /* re-render scout if already shown */
                ScoutMod.reRender();
                /* parse NT data before buildPlayerCard modifies the DOM */
                parsedNTData = parseNTData();
                /* build player card after tooltip data arrives */
                buildPlayerCard();
                /* build ASI calculator with defaults after TI is computed */
                buildASICalculator();
                /* re-render history if already loaded, so NT tab appears */
                if (parsedNTData && dataLoaded['history']) HistoryMod.reRender();
                /* fetch scout data for Best Estimate card */
                fetchBestEstimate();
                /* save current visit and then patch decimal values into already-rendered skill grid */
                if (p.age !== undefined && p.months !== undefined && p.skills) {
                    const _yr = parseInt(p.age);
                    const _mo = parseInt(p.months) || 0;
                    const _NAMES_OUT = ['Strength', 'Stamina', 'Pace', 'Marking', 'Tackling', 'Workrate', 'Positioning', 'Passing', 'Crossing', 'Technique', 'Heading', 'Finishing', 'Longshots', 'Set Pieces'];
                    const _NAMES_GK = ['Strength', 'Stamina', 'Pace', 'Handling', 'One on ones', 'Reflexes', 'Aerial Ability', 'Jumping', 'Communication', 'Kicking', 'Throwing'];
                    const _names = isGoalkeeper ? _NAMES_GK : _NAMES_OUT;
                    const _skillsArr = _names.map(name => {
                        const sk = p.skills.find(s => s.name === name);
                        if (!sk) return 0;
                        const v = sk.value;
                        if (typeof v === 'string' && v.includes('star')) return v.includes('silver') ? 19 : 20;
                        return parseInt(v) || 0;
                    });
                    setTimeout(() => syncFromGraphs(_yr, _mo, _skillsArr, playerASI, isGoalkeeper), 500);
                    setTimeout(() => {
                        /* Patch the already-rendered skill grid with decimal values */
                        const ageKey = `${_yr}.${_mo}`;
                        let skillsC = null;
                        try {
                            const store = PlayerDB.get(PLAYER_ID);
                            if (store && store._v >= 1 && store.records && store.records[ageKey])
                                skillsC = store.records[ageKey].skills;
                        } catch (e) { }
                        if (!skillsC) {
                            /* Fallback: compute from ASI */
                            if (playerASI && playerASI > 0) {
                                const w = isGoalkeeper ? 48717927500 : 263533760000;
                                const log27 = Math.log(Math.pow(2, 7));
                                const allSum = _skillsArr.reduce((s, v) => s + v, 0);
                                const rem = Math.round((Math.pow(2, Math.log(w * playerASI) / log27) - allSum) * 10) / 10;
                                const gs = _skillsArr.filter(v => v === 20).length;
                                const ns = _skillsArr.length - gs;
                                skillsC = _skillsArr.map(v => v === 20 ? 20 : v + (ns > 0 ? rem / ns : 0));
                            }
                        }
                        if (!skillsC) return;
                        const _NAMES = isGoalkeeper
                            ? ['Strength', 'Stamina', 'Pace', 'Handling', 'One on ones', 'Reflexes', 'Aerial Ability', 'Jumping', 'Communication', 'Kicking', 'Throwing']
                            : ['Strength', 'Stamina', 'Pace', 'Marking', 'Tackling', 'Workrate', 'Positioning', 'Passing', 'Crossing', 'Technique', 'Heading', 'Finishing', 'Longshots', 'Set Pieces'];
                        const decMap = {};
                        _NAMES.forEach((name, i) => { decMap[name] = skillsC[i]; });
                        const renderDec = (v) => {
                            const floor = Math.floor(v);
                            const frac = v - floor;
                            if (floor >= 20) return `<span class="tmps-star" style="color:gold">★</span>`;
                            if (floor >= 19) {
                                const fracStr = frac > 0.005 ? `<span class="tmps-dec">.${Math.round(frac * 100).toString().padStart(2, '0')}</span>` : '';
                                return `<span class="tmps-star" style="color:silver">★${fracStr}</span>`;
                            }
                            const dispVal = frac > 0.005 ? `${floor}<span class="tmps-dec">.${Math.round(frac * 100).toString().padStart(2, '0')}</span>` : floor;
                            return `<span style="color:${skillColor(floor)}">${dispVal}</span>`;
                        };
                        document.querySelectorAll('.tmps-grid .tmps-row').forEach(row => {
                            const nameEl = row.querySelector('.tmps-name');
                            const valEl = row.querySelector('.tmps-val');
                            if (!nameEl || !valEl) return;
                            const name = nameEl.textContent.trim();
                            if (decMap[name] !== undefined) valEl.innerHTML = renderDec(decMap[name]);
                        });
                    }, 600);
                }
            } catch (e) { /* ignore */ }
        });
    };

    /* ═══════════════════════════════════════════════════════════
       ███  MODULE: HISTORY
       ═══════════════════════════════════════════════════════════ */
    const HistoryMod = (() => {
        let historyData = null;
        let activeTab = 'nat';
        let root = null;

        const q = (sel) => root ? root.querySelector(sel) : null;
        const qa = (sel) => root ? root.querySelectorAll(sel) : [];

        const extractClubName = (html) => { if (!html) return '-'; const m = html.match(/>([^<]+)<\/a>/); return m ? m[1] : (html === '-' ? '-' : html.replace(/<[^>]+>/g, '').trim() || '-'); };
        const extractClubLink = (html) => { if (!html) return ''; const m = html.match(/href="([^"]+)"/); return m ? m[1] : ''; };
        const fixDivFlags = (s) => s ? s.replace(/class='flag-img-([^']+)'/g, "class='flag-img-$1 tmsq-flag'") : '';
        const ratingClass = (r) => { const v = parseFloat(r); if (isNaN(v) || v === 0) return 'tmph-r-avg'; if (v >= 6.0) return 'tmph-r-good'; if (v < 4.5) return 'tmph-r-low'; return 'tmph-r-avg'; };
        const calcRating = (rating, games) => { const r = parseFloat(rating), g = parseInt(games); if (!r || !g || g === 0) return '-'; return (r / g).toFixed(2); };
        const fmtNum = (n) => (n == null || n === '' || n === 0) ? '0' : Number(n).toLocaleString();

        const buildNTTable = (nt) => {
            if (!nt) return '<div class="tmph-empty">Not called up for any national team</div>';
            const avgR = nt.matches > 0 ? nt.rating.toFixed(1) : '-';
            const rc = ratingClass(avgR);
            return `<table class="tmph-tbl"><thead><tr><th>Country</th><th></th><th class="c">Gp</th><th class="c">${isGoalkeeper ? 'Con' : 'G'}</th><th class="c">A</th><th class="c">Cards</th><th class="c">Rating</th><th class="c" style="color:#e8a832">Mom</th></tr></thead>`
                + `<tbody><tr><td><div class="tmph-club">${nt.country}</div></td><td class="tmph-div">${nt.flagHtml}</td><td class="c">${nt.matches}</td><td class="c" style="color:#6cc040;font-weight:600">${nt.goals}</td><td class="c" style="color:#5b9bff">${nt.assists}</td><td class="c" style="color:#fbbf24">${nt.cards}</td><td class="c ${rc}" style="font-weight:700">${avgR}</td><td class="c" style="color:#e8a832;font-weight:700">${nt.mom}</td></tr></tbody></table>`;
        };

        const buildTable = (rows) => {
            if (!rows || !rows.length) return '<div class="tmph-empty">No history data available</div>';
            const totalRow = rows.find(r => r.season === 'total');
            const dataRows = rows.filter(r => r.season !== 'total');
            let tb = '';
            for (const row of dataRows) {
                if (row.season === 'transfer') {
                    tb += `<tr class="tmph-transfer"><td colspan="8"><div class="tmph-xfer"><span class="tmph-xfer-arrow">⇄</span><span class="tmph-xfer-label">Transfer</span><span class="tmph-xfer-sum">${row.transfer}</span></div></td></tr>`;
                    continue;
                }
                const cn = extractClubName(row.klubnavn), cl = extractClubLink(row.klubnavn);
                const cnH = cl ? `<a href="${cl}" target="_blank">${cn}</a>` : cn;
                const divH = fixDivFlags(row.division_string);
                const avgR = calcRating(row.rating, row.games);
                tb += `<tr><td class="c" style="font-weight:700;color:#e8f5d8">${row.season}</td><td><div class="tmph-club">${cnH}</div></td><td class="tmph-div">${divH}</td><td class="c">${row.games || 0}</td><td class="c" style="color:#6cc040;font-weight:600">${isGoalkeeper ? (row.conceded || 0) : (row.goals || 0)}</td><td class="c" style="color:#5b9bff">${row.assists || 0}</td><td class="c" style="color:#fbbf24">${row.cards || 0}</td><td class="r ${ratingClass(avgR)}" style="font-weight:700">${avgR}</td></tr>`;
            }
            if (totalRow) {
                const tr = calcRating(totalRow.rating, totalRow.games);
                tb += `<tr class="tmph-tot"><td class="c" colspan="2" style="font-weight:800">Career Total</td><td></td><td class="c">${fmtNum(totalRow.games)}</td><td class="c" style="color:#6cc040">${fmtNum(isGoalkeeper ? totalRow.conceded : totalRow.goals)}</td><td class="c" style="color:#5b9bff">${fmtNum(totalRow.assists)}</td><td class="c" style="color:#fbbf24">${fmtNum(totalRow.cards)}</td><td class="r" style="color:#e0f0cc">${tr}</td></tr>`;
            }
            return `<table class="tmph-tbl"><thead><tr><th class="c" style="width:36px">S</th><th>Club</th><th>Division</th><th class="c">Gp</th><th class="c">${isGoalkeeper ? 'Con' : 'G'}</th><th class="c">A</th><th class="c">Cards</th><th class="r">Rating</th></tr></thead><tbody>${tb}</tbody></table>`;
        };

        const render = (container, data) => {
            historyData = data.table;
            activeTab = 'nat';
            container.innerHTML = '';
            const wrapper = document.createElement('div');
            wrapper.id = 'tmph-root';
            container.appendChild(wrapper);
            root = wrapper;
            const TAB_LABELS = { nat: 'League', cup: 'Cup', int: 'International', total: 'Total' };
            if (parsedNTData) TAB_LABELS.nt = 'National Team';
            let tabsH = '';
            for (const [key, label] of Object.entries(TAB_LABELS)) {
                if (key === 'nt') {
                    tabsH += `<button class="tmph-tab ${key === activeTab ? 'active' : ''}" data-tab="${key}">${label}</button>`;
                } else {
                    const rows = historyData[key] || [];
                    tabsH += `<button class="tmph-tab ${key === activeTab ? 'active' : ''}" data-tab="${key}" ${!rows.length ? 'style="opacity:0.4"' : ''}>${label}</button>`;
                }
            }
            root.innerHTML = `<div class="tmph-wrap"><div class="tmph-tabs">${tabsH}</div><div class="tmph-body" id="tmph-tab-content">${buildTable(historyData[activeTab])}</div></div>`;
            qa('.tmph-tab').forEach(tab => {
                tab.addEventListener('click', () => {
                    const key = tab.dataset.tab;
                    if (key === 'nt') {
                        if (!parsedNTData) return;
                    } else {
                        if (!(historyData[key] || []).length) return;
                    }
                    activeTab = key;
                    qa('.tmph-tab').forEach(t => t.classList.remove('active'));
                    tab.classList.add('active');
                    const c = q('#tmph-tab-content');
                    if (c) c.innerHTML = key === 'nt' ? buildNTTable(parsedNTData) : buildTable(historyData[key]);
                });
            });
        };

        const reRender = () => {
            if (!root || !historyData) return;
            const panel = root.closest('.tmpe-panel') || root.parentNode;
            if (panel) render(panel, { table: historyData });
        };

        return { render, reRender };
    })();

    /* ═══════════════════════════════════════════════════════════
       ███  MODULE: SCOUT
       ═══════════════════════════════════════════════════════════ */
    const ScoutMod = (() => {
        let scoutData = null;
        let root = null;
        let activeTab = 'report';
        let containerRef = null;

        const q = (sel) => root ? root.querySelector(sel) : null;
        const qa = (sel) => root ? root.querySelectorAll(sel) : [];

        const POT_LABELS = ['', 'Queasy', 'Despicable', 'Miserable', 'Horrible', 'Wretched', 'Inadequate', 'Unimpressive', 'Mediocre', 'Standard', 'Modest', 'Okay', 'Decent', 'Fine', 'Good', 'Great', 'Excellent', 'Superb', 'Outstanding', 'Extraordinary', 'Wonderkid'];
        const SPECIALTIES = ['None', 'Strength', 'Stamina', 'Pace', 'Marking', 'Tackling', 'Workrate', 'Positioning', 'Passing', 'Crossing', 'Technique', 'Heading', 'Finishing', 'Longshots', 'Set Pieces'];
        const PEAK_SUMS = {
            outfield: { phy: [64, 70, 74, 80], tac: [64, 70, 74, 80], tec: [96, 105, 111, 120] },
            gk: { phy: [64, 70, 74, 80], tac: [50, 55, 60], tec: [68, 74, 80] }
        };

        const skillColor = (v) => { v = parseInt(v); if (v >= 19) return '#6cc040'; if (v >= 16) return '#80e048'; if (v >= 13) return '#c8e0b4'; if (v >= 10) return '#fbbf24'; if (v >= 7) return '#f97316'; return '#f87171'; };
        const potColor = (pot) => { pot = parseInt(pot); if (pot >= 18) return '#6cc040'; if (pot >= 15) return '#5b9bff'; if (pot >= 12) return '#c8e0b4'; if (pot >= 9) return '#fbbf24'; return '#f87171'; };
        const extractTier = (txt) => { if (!txt) return null; const m = txt.match(/\((\d)\/(\d)\)/); return m ? { val: parseInt(m[1]), max: parseInt(m[2]) } : null; };
        const barColor = (val, max) => { const r = val / max; if (r >= 0.75) return '#6cc040'; if (r >= 0.5) return '#80e048'; if (r >= 0.25) return '#fbbf24'; return '#f87171'; };
        const reachColor = (pct) => { if (pct >= 90) return '#6cc040'; if (pct >= 80) return '#80e048'; if (pct >= 70) return '#fbbf24'; if (pct >= 60) return '#f97316'; return '#f87171'; };
        const fixFlags = (html) => html ? html.replace(/class='flag-img-([^']+)'/g, "class='flag-img-$1 tmsq-flag'").replace(/class="flag-img-([^"]+)"/g, 'class="flag-img-$1 tmsq-flag"') : '';
        const bloomColor = (txt) => { if (!txt) return '#c8e0b4'; const t = txt.toLowerCase(); if (t === 'bloomed') return '#6cc040'; if (t.includes('late bloom')) return '#80e048'; if (t.includes('middle')) return '#fbbf24'; if (t.includes('starting')) return '#f97316'; if (t.includes('not bloomed')) return '#f87171'; return '#c8e0b4'; };
        const cashColor = (c) => { if (!c) return '#c8e0b4'; if (c.includes('Astonishingly')) return '#6cc040'; if (c.includes('Incredibly')) return '#80e048'; if (c.includes('Very rich')) return '#a0d880'; if (c.includes('Rich')) return '#c8e0b4'; if (c.includes('Terrible')) return '#f87171'; if (c.includes('Poor')) return '#f97316'; return '#c8e0b4'; };
        const cleanPeakText = (txt) => txt ? txt.replace(/^\s*-\s*/, '').replace(/\s*(physique|tactical ability|technical ability)\s*$/i, '').trim() : '';
        const confPct = (skill) => Math.round((parseInt(skill) || 0) / 20 * 100);
        const confBadge = (pct) => { const c = pct >= 90 ? '#6cc040' : pct >= 70 ? '#80e048' : pct >= 50 ? '#fbbf24' : '#f87171'; const bg = pct >= 90 ? 'rgba(108,192,64,.12)' : pct >= 70 ? 'rgba(128,224,72,.1)' : pct >= 50 ? 'rgba(251,191,36,.1)' : 'rgba(248,113,113,.1)'; return `<span class="tmsc-conf" style="color:${c};background:${bg}">${pct}%</span>`; };
        const onlineDot = (on) => `<span class="tmsc-online ${on ? 'on' : 'off'}"></span>`;
        const getScoutForReport = (r) => { if (!scoutData || !scoutData.scouts || !r.scoutid) return null; return Object.values(scoutData.scouts).find(s => String(s.id) === String(r.scoutid)) || null; };
        const personalityTier = (key, value) => { value = parseInt(value) || 0; if (key === 'aggression') { if (value >= 17) return 'Alarmingly (5/5)'; if (value >= 13) return 'Somewhat (4/5)'; if (value >= 9) return 'Slightly (3/5)'; if (value >= 5) return 'Not particularly (2/5)'; return 'Not (1/5)'; } if (value >= 17) return 'Superb (5/5)'; if (value >= 13) return 'Good (4/5)'; if (value >= 9) return 'OK (3/5)'; if (value >= 5) return 'No special (2/5)'; return 'Bad (1/5)'; };

        const getCurrentBloomStatus = (allReports, scouts) => {
            if (!allReports || !allReports.length || playerAge === null) return { text: '-', certain: false, range: null };
            const getDevSkill = (r) => {
                if (!scouts) return 0;
                const s = Object.values(scouts).find(sc => String(sc.id) === String(r.scoutid));
                return s ? (parseInt(s.development) || 0) : 0;
            };
            const phaseFor = (start) => {
                if (playerAge < start) return 'not';
                if (playerAge >= start + 3) return 'done';
                const y = playerAge - start;
                return y < 1 ? 'starting' : y < 2 ? 'middle' : 'late';
            };
            const PHASE_LABEL = { not: 'Not bloomed', starting: 'Starting', middle: 'Middle', late: 'Late bloom', done: 'Bloomed' };
            const statusFrom = (start) => {
                const range = `${start}.0\u2013${start + 2}.11`;
                const p = phaseFor(start);
                if (p === 'done') return { text: 'Bloomed', certain: true, range: null };
                const notBloomedTxt = bloomType ? `Not bloomed (${bloomType})` : 'Not bloomed';
                const text = p === 'not' ? notBloomedTxt : p === 'starting' ? 'Starting to bloom' : p === 'middle' ? 'In the middle of his bloom' : 'In his late bloom';
                return { text, certain: true, range };
            };

            /* --- STEP 1: Determine bloom TYPE from the most credible "Not bloomed" scout ---
               Among all "Not bloomed" reports that include a type (Early/Normal/Late), the scout
               with the highest dev skill wins. Ties broken by most-recent report date.
               Weaker scouts' conflicting type claims are ignored for possibleStarts. */
            let seenBloomed = false;
            let bloomType = null, possibleStarts = null;
            let bloomTypeBestDevSk = -1, bloomTypeBestDate = '';
            for (const r of allReports) {
                const bt = r.bloom_status_txt || '';
                if (!bt || bt === '-') continue;
                if (bt === 'Bloomed') { seenBloomed = true; continue; }
                if (!bt.includes('Not bloomed')) continue;
                const hasType = bt.includes('Early') || bt.includes('Normal') || bt.includes('Late');
                if (!hasType) continue;
                const devSk = getDevSkill(r);
                const rDate = r.done || '';
                if (devSk > bloomTypeBestDevSk || (devSk === bloomTypeBestDevSk && rDate > bloomTypeBestDate)) {
                    bloomTypeBestDevSk = devSk;
                    bloomTypeBestDate = rDate;
                    if (bt.includes('Early')) { bloomType = 'Early'; possibleStarts = [16, 17]; }
                    else if (bt.includes('Normal')) { bloomType = 'Normal'; possibleStarts = [18, 19]; }
                    else { bloomType = 'Late'; possibleStarts = [20, 21, 22]; }
                }
            }

            /* --- STEP 2: 75% threshold for phase reports within the bloom window ---
               Phase reports are only trusted within the possible bloom window if the scout
               has dev skill >= 15 (75%). If no scout reaches 15, the best available is used.
               Outside the bloom window, phases are accepted normally (inconsistent ones rejected). */
            const MIN_PHASE_DEV = 15;
            const bloomWinMin = possibleStarts ? possibleStarts[0] : Infinity;
            const bloomWinMax = possibleStarts ? possibleStarts[possibleStarts.length - 1] + 3 : -Infinity;
            let maxPhaseDevSkInWindow = 0;
            for (const r of allReports) {
                const bt = r.bloom_status_txt || '';
                if (!bt || bt.includes('Not bloomed') || bt === 'Bloomed' || bt === '-') continue;
                const rAge = parseFloat(r.report_age) || 0;
                if (rAge < bloomWinMin || rAge >= bloomWinMax) continue;
                const devSk = getDevSkill(r);
                if (devSk > maxPhaseDevSkInWindow) maxPhaseDevSkInWindow = devSk;
            }
            const phaseThreshold = maxPhaseDevSkInWindow >= MIN_PHASE_DEV ? MIN_PHASE_DEV : maxPhaseDevSkInWindow;

            /* --- STEP 3: Find best phase report ---
               Must satisfy the threshold (when inside bloom window) and must be consistent
               with the determined bloom type (implied start must be in possibleStarts). */
            let bestPhase = null;
            for (const r of allReports) {
                const bt = r.bloom_status_txt || '';
                if (!bt || bt.includes('Not bloomed') || bt === 'Bloomed' || bt === '-') continue;
                const rAge = parseFloat(r.report_age) || 0;
                const rFloor = Math.floor(rAge);
                const devSk = getDevSkill(r);
                let candidateStart = null;
                if (bt.includes('Starting') && !bt.includes('Not')) candidateStart = rFloor;
                else if (bt.toLowerCase().includes('middle')) candidateStart = rFloor - 1;
                else if (bt.toLowerCase().includes('late bloom')) candidateStart = rFloor - 2;
                if (candidateStart === null) continue;
                const inWindow = possibleStarts && rAge >= bloomWinMin && rAge < bloomWinMax;
                /* Reject if inside window but below quality threshold */
                if (inWindow && devSk < phaseThreshold) continue;
                /* Reject if implied start conflicts with determined bloom type */
                if (possibleStarts && !possibleStarts.includes(candidateStart)) continue;
                if (!bestPhase || devSk > bestPhase.devSkill) {
                    bestPhase = { knownStart: candidateStart, devSkill: devSk };
                }
            }

            /* --- STEP 4: Dominated check ---
               If a more credible scout said "Not bloomed" at an age >= the phase's implied start,
               the phase report is contradicted and discarded. */
            if (bestPhase) {
                let dominated = false;
                for (const r of allReports) {
                    const bt = r.bloom_status_txt || '';
                    if (!bt.includes('Not bloomed')) continue;
                    const rAge = parseFloat(r.report_age) || 0;
                    const devSk = getDevSkill(r);
                    if (rAge >= bestPhase.knownStart && devSk > bestPhase.devSkill) {
                        dominated = true;
                        break;
                    }
                }
                if (!dominated) return statusFrom(bestPhase.knownStart);
            }

            if (seenBloomed) return { text: 'Bloomed', certain: true, range: null };
            if (!possibleStarts) return { text: '-', certain: false, range: null };

            /* --- STEP 5: Narrow possibleStarts using "Not bloomed" age observations ---
               All "Not bloomed" reports contribute their age for narrowing (type claim ignored
               here — we already determined the type; only the age observation matters). */
            for (const r of allReports) {
                const bt = r.bloom_status_txt || '';
                if (!bt.includes('Not bloomed')) continue;
                const rAge = parseFloat(r.report_age) || 0;
                possibleStarts = possibleStarts.filter(s => s > rAge);
            }
            if (possibleStarts.length === 0) return { text: '-', certain: false, range: null };
            if (possibleStarts.length === 1) return statusFrom(possibleStarts[0]);

            /* Multiple possible starts — determine phase for each */
            const rangeStr = possibleStarts.map(s => `${s}.0\u2013${s + 2}.11`).join(' or ');
            const phases = possibleStarts.map(s => phaseFor(s));
            const unique = [...new Set(phases)];
            const notBloomedLabel = bloomType ? `Not bloomed (${bloomType})` : 'Not bloomed';
            if (unique.length === 1) {
                if (unique[0] === 'not') return { text: notBloomedLabel, certain: true, range: rangeStr };
                if (unique[0] === 'done') return { text: 'Bloomed', certain: true, range: null };
                return { text: PHASE_LABEL[unique[0]], certain: true, range: rangeStr };
            }
            const allBlooming = phases.every(p => p !== 'not' && p !== 'done');
            if (allBlooming) {
                const labels = unique.map(p => PHASE_LABEL[p]).join(' or ');
                return { text: 'Blooming', certain: true, phases: labels, range: rangeStr };
            }
            let parts = [];
            if (phases.includes('not')) parts.push(notBloomedLabel);
            const bloomPhases = unique.filter(p => p !== 'not' && p !== 'done');
            if (bloomPhases.length) parts.push('Blooming (' + bloomPhases.map(p => PHASE_LABEL[p]).join('/') + ')');
            if (phases.includes('done')) parts.push('Bloomed');
            return { text: parts.join(' or '), certain: false, range: rangeStr };
        };

        const greenStarsHtml = (rec) => { rec = parseFloat(rec) || 0; const full = Math.floor(rec); const half = (rec % 1) >= 0.25; let h = ''; for (let i = 0; i < full; i++)h += '<span class="tmsc-star-green">★</span>'; if (half) h += '<span class="tmsc-star-green-half">★</span>'; const e = 5 - full - (half ? 1 : 0); for (let i = 0; i < e; i++)h += '<span class="tmsc-star-empty">★</span>'; return h; };

        const combinedStarsHtml = (current, potMax) => {
            current = parseFloat(current) || 0; potMax = parseFloat(potMax) || 0;
            if (potMax < current) potMax = current;
            let h = '';
            for (let i = 1; i <= 5; i++) {
                if (i <= current) h += '<span class="tmsc-star-full">★</span>';
                else if (i - 0.5 <= current && current < i) {
                    /* Half gold — check if potMax fills the other half */
                    if (potMax >= i) h += '<span class="tmsc-star-split">★</span>';
                    else h += '<span class="tmsc-star-half">★</span>';
                }
                else if (i <= potMax) h += '<span class="tmsc-star-green">★</span>';
                else if (i - 0.5 <= potMax && potMax < i) h += '<span class="tmsc-star-green-half">★</span>';
                else h += '<span class="tmsc-star-empty">★</span>';
            }
            return h;
        };

        /* ── Build Scouts Table ── */
        const buildScoutsTable = (scouts) => {
            if (!scouts || !Object.keys(scouts).length) return '<div class="tmsc-empty">No scouts hired</div>';
            const skills = ['seniors', 'youths', 'physical', 'tactical', 'technical', 'development', 'psychology'];
            let rows = '';
            for (const s of Object.values(scouts)) {
                let sc = ''; for (const sk of skills) { const v = parseInt(s[sk]) || 0; sc += `<td class="c" style="color:${skillColor(v)};font-weight:600">${v}</td>`; }
                const bc = s.away ? 'tmsc-send-btn tmsc-away' : 'tmsc-send-btn';
                const bl = s.away ? (s.returns || 'Away') : 'Send';
                rows += `<tr><td style="font-weight:600;color:#e8f5d8;white-space:nowrap">${s.name} ${s.surname}</td>${sc}<td class="c"><button class="${bc}" data-scout-id="${s.id}" ${s.away ? 'disabled title="' + (s.returns || '') + '"' : ''}>${bl}</button></td></tr>`;
            }
            return `<table class="tmsc-tbl"><thead><tr><th>Name</th><th class="c">Sen</th><th class="c">Yth</th><th class="c">Phy</th><th class="c">Tac</th><th class="c">Tec</th><th class="c">Dev</th><th class="c">Psy</th><th class="c"></th></tr></thead><tbody>${rows}</tbody></table>`;
        };

        /* ── Build Report Card ── */
        const buildReportCard = (r) => {
            const pot = parseInt(r.old_pot) || 0;
            const potStarsVal = (parseFloat(r.potential) || 0) / 2;
            if (r.scout_name === 'YD' || r.scoutid === '0') {
                return `<div class="tmsc-report"><div class="tmsc-report-header"><div><div class="tmsc-stars">${greenStarsHtml(potStarsVal)}</div><div class="tmsc-report-scout">Youth Development<span class="tmsc-yd-badge">YD</span></div></div><div class="tmsc-report-date">${r.done || '-'}</div></div><div class="tmsc-report-grid"><div class="tmsc-report-item wide"><span class="tmsc-report-label">Potential</span><span class="tmsc-report-value" style="color:${potColor(pot)}">${pot}</span></div><div class="tmsc-report-item wide"><span class="tmsc-report-label">Age at report</span><span class="tmsc-report-value">${r.report_age || '-'}</span></div></div></div>`;
            }
            const spec = parseInt(r.specialist) || 0; const specLabel = SPECIALTIES[spec] || 'None';
            const scout = getScoutForReport(r);
            let potConf = null, bloomConf = null, phyConf = null, tacConf = null, tecConf = null, psyConf = null, specConf = null;
            if (scout) { const age = parseInt(r.report_age) || 0; const senYth = age < 20 ? (parseInt(scout.youths) || 0) : (parseInt(scout.seniors) || 0); const dev = parseInt(scout.development) || 0; potConf = confPct(Math.min(senYth, dev)); bloomConf = confPct(dev); phyConf = confPct(parseInt(scout.physical) || 0); tacConf = confPct(parseInt(scout.tactical) || 0); tecConf = confPct(parseInt(scout.technical) || 0); psyConf = confPct(parseInt(scout.psychology) || 0); if (spec > 0) { const phyS = [1, 2, 3, 11]; const tacS = [4, 5, 6, 7]; if (phyS.includes(spec)) specConf = phyConf; else if (tacS.includes(spec)) specConf = tacConf; else specConf = tecConf; } }
            const peaks = [{ label: 'Physique', text: cleanPeakText(r.peak_phy_txt), conf: phyConf }, { label: 'Tactical', text: cleanPeakText(r.peak_tac_txt), conf: tacConf }, { label: 'Technical', text: cleanPeakText(r.peak_tec_txt), conf: tecConf }];
            let peaksH = '';
            for (const p of peaks) { const tier = extractTier(p.text); if (tier) { const pct = (tier.val / tier.max) * 100; const c = barColor(tier.val, tier.max); peaksH += `<div class="tmsc-bar-row"><span class="tmsc-bar-label">${p.label}</span><div class="tmsc-bar-track"><div class="tmsc-bar-fill" style="width:${pct}%;background:${c}"></div></div><span class="tmsc-bar-text" style="color:${c}">${tier.val}/${tier.max}</span>${p.conf !== null ? confBadge(p.conf) : ''}</div>`; } }
            const charisma = parseInt(r.charisma) || 0; const professionalism = parseInt(r.professionalism) || 0; const aggression = parseInt(r.aggression) || 0;
            const pers = [{ label: 'Leadership', key: 'leadership', value: charisma }, { label: 'Professionalism', key: 'professionalism', value: professionalism }, { label: 'Aggression', key: 'aggression', value: aggression }];
            let persH = '';
            for (const p of pers) { const pct = (p.value / 20) * 100; const c = skillColor(p.value); persH += `<div class="tmsc-bar-row"><span class="tmsc-bar-label">${p.label}</span><div class="tmsc-bar-track"><div class="tmsc-bar-fill" style="width:${pct}%;background:${c}"></div></div><span class="tmsc-bar-text" style="color:${c}">${p.value}</span>${psyConf !== null ? confBadge(psyConf) : ''}</div>`; }
            return `<div class="tmsc-report"><div class="tmsc-report-header"><div><div class="tmsc-stars">${combinedStarsHtml(r.rec, potStarsVal)}</div><div class="tmsc-report-scout">${r.scout_name || 'Unknown'}</div></div><div class="tmsc-report-date">${r.done || '-'}</div></div><div class="tmsc-report-grid"><div class="tmsc-report-item"><span class="tmsc-report-label">Potential</span><span class="tmsc-report-value" style="color:${potColor(pot)}">${pot}${potConf !== null ? confBadge(potConf) : ''}</span></div><div class="tmsc-report-item"><span class="tmsc-report-label">Age</span><span class="tmsc-report-value">${r.report_age || '-'}</span></div><div class="tmsc-report-item"><span class="tmsc-report-label">Bloom</span><span class="tmsc-report-value" style="color:${bloomColor(r.bloom_status_txt)}">${r.bloom_status_txt || '-'}${bloomConf !== null ? confBadge(bloomConf) : ''}</span></div><div class="tmsc-report-item"><span class="tmsc-report-label">Development</span><span class="tmsc-report-value">${r.dev_status || '-'}${bloomConf !== null ? confBadge(bloomConf) : ''}</span></div><div class="tmsc-report-item wide"><span class="tmsc-report-label">Specialty</span><span class="tmsc-report-value" style="color:${spec > 0 ? '#fbbf24' : '#5a7a48'}">${specLabel}${specConf !== null ? confBadge(specConf) : ''}</span></div></div><div><div class="tmsc-section-title">Peak Development</div>${peaksH}</div><div><div class="tmsc-section-title">Personality</div>${persH}</div></div>`;
        };

        /* ── Build Report Tab ── */
        const buildReport = (reports, error) => {
            let h = '';
            if (error) { const msg = error === 'multi_scout' ? 'This scout is already on a mission' : error === 'multi_bid' ? 'Scout already scouting this player' : error; h += `<div class="tmsc-error">${msg}</div>`; }
            if (!reports || !reports.length) return h + '<div class="tmsc-empty">No scout reports available</div>';
            if (reports.length > 1) h += `<div class="tmsc-report-count">${reports.length} Reports</div>`;
            for (let i = 0; i < reports.length; i++) { if (i > 0) h += '<hr class="tmsc-report-divider">'; h += buildReportCard(reports[i]); }
            return h;
        };

        /* ── Build Interested ── */
        const buildInterested = (interested) => {
            if (!interested || !interested.length) return '<div class="tmsc-empty">No interested clubs</div>';
            let rows = '';
            for (const c of interested) { const ch = fixFlags(c.club_link || ''); const lh = fixFlags(c.league_link || ''); const cc = cashColor(c.cash); rows += `<tr><td class="tmsc-club-cell">${ch} ${onlineDot(c.online)}</td><td class="tmsc-league-cell">${lh}</td><td style="color:${cc};font-weight:600;font-size:11px">${c.cash}</td></tr>`; }
            return `<table class="tmsc-tbl"><thead><tr><th>Club</th><th>League</th><th>Financial</th></tr></thead><tbody>${rows}</tbody></table>`;
        };

        /* ── Render ── */
        const render = (container, data) => {
            containerRef = container;
            scoutData = data;
            activeTab = (data.reports && data.reports.length) ? 'report' : 'scouts';
            container.innerHTML = '';
            const wrapper = document.createElement('div');
            wrapper.id = 'tmsc-root';
            container.appendChild(wrapper);
            root = wrapper;
            const TAB_LABELS = { report: 'Report', scouts: 'Scouts', interested: 'Interested' };
            let tabsH = '';
            for (const [key, label] of Object.entries(TAB_LABELS)) {
                let hasData = true;
                if (key === 'report') hasData = data.reports && data.reports.length > 0;
                if (key === 'interested') hasData = data.interested && data.interested.length > 0;
                if (key === 'scouts') hasData = data.scouts && Object.keys(data.scouts).length > 0;
                tabsH += `<button class="tmsc-tab ${key === activeTab ? 'active' : ''}" data-tab="${key}" ${!hasData ? 'style="opacity:0.4"' : ''}>${label}</button>`;
            }
            const getContent = (tab) => { switch (tab) { case 'report': return buildReport(scoutData.reports, scoutData.error); case 'scouts': return buildScoutsTable(scoutData.scouts); case 'interested': return buildInterested(scoutData.interested); default: return ''; } };
            root.innerHTML = `<div class="tmsc-wrap"><div class="tmsc-tabs">${tabsH}</div><div class="tmsc-body" id="tmsc-tab-content">${getContent(activeTab)}</div></div>`;
            bindTabs();
            bindSendButtons();
        };

        const bindTabs = () => {
            qa('.tmsc-tab').forEach(tab => {
                tab.addEventListener('click', () => {
                    const key = tab.dataset.tab; activeTab = key;
                    qa('.tmsc-tab').forEach(t => t.classList.remove('active')); tab.classList.add('active');
                    const c = q('#tmsc-tab-content'); if (!c) return;
                    switch (key) { case 'report': c.innerHTML = buildReport(scoutData.reports, scoutData.error); break; case 'scouts': c.innerHTML = buildScoutsTable(scoutData.scouts); bindSendButtons(); break; case 'interested': c.innerHTML = buildInterested(scoutData.interested); break; }
                });
            });
        };

        const bindSendButtons = () => {
            qa('.tmsc-send-btn').forEach(btn => {
                if (btn.disabled) return;
                btn.addEventListener('click', () => {
                    const scoutId = btn.dataset.scoutId; btn.disabled = true; btn.textContent = '...';
                    $.post('/ajax/players_get_info.ajax.php', { player_id: PLAYER_ID, type: 'scout', scout_id: scoutId, show_non_pro_graphs: true }, 'json')
                        .done((res) => { try { const d = typeof res === 'object' ? res : JSON.parse(res); if (d && (d.scouts || d.reports)) { render(containerRef, d); } else { btn.textContent = 'Sent'; btn.style.background = '#274a18'; btn.style.color = '#6cc040'; } } catch (e) { btn.textContent = 'Sent'; btn.style.background = '#274a18'; btn.style.color = '#6cc040'; } })
                        .fail(() => { btn.textContent = 'Error'; btn.style.color = '#f87171'; setTimeout(() => { btn.textContent = 'Send'; btn.disabled = false; btn.style.color = ''; }, 2000); });
                });
            });
        };

        const reRender = () => { if (containerRef && scoutData) render(containerRef, scoutData); };

        const getEstimateHtml = (data) => {
            const hasScouts = data && data.reports && data.reports.length && data.scouts;
            let scouts = {}, regular = [];
            let potPick = null, bloomPick = null, phyPick = null, tacPick = null, tecPick = null, psyPick = null;
            if (hasScouts) {
                scouts = data.scouts;
                regular = data.reports.filter(r => r.scout_name !== 'YD' && r.scoutid !== '0');
                if (regular.length) {
                    const scoutSkill = (r, field) => { const s = Object.values(scouts).find(s => String(s.id) === String(r.scoutid)); return s ? (parseInt(s[field]) || 0) : 0; };
                    const pickBest = (field) => { let best = null, bs = -1, bd = ''; for (const r of regular) { const sk = scoutSkill(r, field); const d = r.done || ''; if (sk > bs || (sk === bs && d > bd)) { best = r; bs = sk; bd = d; } } return best ? { report: best, conf: confPct(bs) } : null; };
                    const pickBestPot = () => { let best = null, bs = -1, bd = ''; for (const r of regular) { const s = Object.values(scouts).find(s => String(s.id) === String(r.scoutid)); const age = parseInt(r.report_age) || 0; let sk = 0; if (s) { const senYth = age < 20 ? (parseInt(s.youths) || 0) : (parseInt(s.seniors) || 0); const dev = parseInt(s.development) || 0; sk = Math.min(senYth, dev); } const d = r.done || ''; if (sk > bs || (sk === bs && d > bd)) { best = r; bs = sk; bd = d; } } return best ? { report: best, conf: confPct(bs) } : null; };
                    potPick = pickBestPot(); bloomPick = pickBest('development'); phyPick = pickBest('physical'); tacPick = pickBest('tactical'); tecPick = pickBest('technical'); psyPick = pickBest('psychology');
                }
            }
            /* If no scouts AND no skill sums, nothing to show */
            if (!regular.length && !playerSkillSums) return '';

            const pot = potPick ? parseInt(potPick.report.old_pot) || 0 : 0;
            const potStarsVal = potPick ? (parseFloat(potPick.report.potential) || 0) / 2 : 0;
            const rec = potPick ? parseFloat(potPick.report.rec) || 0 : 0;
            const bloomResult = getCurrentBloomStatus(regular, scouts);
            const bloomTxt = bloomResult.text || '-';
            const devTxt = bloomPick ? bloomPick.report.dev_status : '-';
            let specVal = 0, specLabel = 'None', specConf = null;
            for (const pick of [phyPick, tacPick, tecPick]) { if (pick) { const s = parseInt(pick.report.specialist) || 0; if (s > 0) { specVal = s; specLabel = SPECIALTIES[s] || 'None'; specConf = pick.conf; break; } } }

            const cb = (conf) => {
                if (conf === null) return '';
                if (conf === 0) return '<span class="tmbe-conf" style="background:rgba(90,122,72,.15);color:#5a7a48">?</span>';
                let bg, clr;
                if (conf >= 90) { bg = 'rgba(108,192,64,.15)'; clr = '#6cc040'; }
                else if (conf >= 70) { bg = 'rgba(251,191,36,.12)'; clr = '#fbbf24'; }
                else { bg = 'rgba(248,113,113,.1)'; clr = '#f87171'; }
                return `<span class="tmbe-conf" style="background:${bg};color:${clr}">${conf}%</span>`;
            };

            /* Peak bars with reach */
            const peaks = [
                { label: 'Physique', text: phyPick ? cleanPeakText(phyPick.report.peak_phy_txt) : '', conf: phyPick ? phyPick.conf : null, cat: 'phy' },
                { label: 'Tactical', text: tacPick ? cleanPeakText(tacPick.report.peak_tac_txt) : '', conf: tacPick ? tacPick.conf : null, cat: 'tac' },
                { label: 'Technical', text: tecPick ? cleanPeakText(tecPick.report.peak_tec_txt) : '', conf: tecPick ? tecPick.conf : null, cat: 'tec' }
            ];
            let peaksH = '';
            for (const p of peaks) {
                const isGK = isGoalkeeper;
                const peakArr = (isGK ? PEAK_SUMS.gk : PEAK_SUMS.outfield)[p.cat];
                if (!peakArr) continue;
                const maxPeakSum = peakArr[peakArr.length - 1];
                const tier = extractTier(p.text);
                const curSum = playerSkillSums ? playerSkillSums[p.cat] : null;

                if (tier && curSum !== null) {
                    /* Have both scout data and skill sums */
                    const peakSum = peakArr[tier.val - 1];
                    const peakPct = (peakSum / maxPeakSum) * 100;
                    const curPct = (curSum / maxPeakSum) * 100;
                    const c = barColor(tier.val, tier.max);
                    const rPct = Math.round(curSum / peakSum * 100); const rC = reachColor(rPct);
                    const mPct = Math.round(curSum / maxPeakSum * 100); const mC = reachColor(mPct);
                    const reachLbl = `<div class="tmbe-peak-reach"><span class="tmbe-reach-item"><span class="tmbe-reach-tag">Peak</span><span style="color:${rC}">${rPct}%</span><span style="color:#90b878;font-weight:400;font-size:9px">(${curSum}/${peakSum})</span></span><span class="tmbe-reach-item"><span class="tmbe-reach-tag">Max</span><span style="color:${mC}">${mPct}%</span><span style="color:#90b878;font-weight:400;font-size:9px">(${curSum}/${maxPeakSum})</span></span></div>`;
                    peaksH += `<div class="tmbe-item tmbe-peak-item"><div class="tmbe-peak-header"><span class="tmbe-lbl">${p.label}</span><span class="tmbe-val" style="color:${c}">${tier.val}/${tier.max}${p.conf !== null ? cb(p.conf) : ''}</span></div>${reachLbl}<div class="tmbe-bar-track"><div class="tmbe-bar-fill" style="width:${peakPct}%;background:${c};opacity:0.35"></div><div class="tmbe-bar-fill-reach" style="width:${curPct}%;background:${rC}"></div></div></div>`;
                } else if (curSum !== null) {
                    /* No scout but have skill sums — show current only with ? */
                    const mPct = Math.round(curSum / maxPeakSum * 100);
                    const curPct = (curSum / maxPeakSum) * 100;
                    const mC = reachColor(mPct);
                    const reachLbl = `<div class="tmbe-peak-reach"><span class="tmbe-reach-item"><span class="tmbe-reach-tag">Max</span><span style="color:${mC}">${mPct}%</span><span style="color:#90b878;font-weight:400;font-size:9px">(${curSum}/${maxPeakSum})</span></span></div>`;
                    peaksH += `<div class="tmbe-item tmbe-peak-item"><div class="tmbe-peak-header"><span class="tmbe-lbl">${p.label}</span><span class="tmbe-val" style="color:#5a7a48">?</span></div>${reachLbl}<div class="tmbe-bar-track"><div class="tmbe-bar-fill" style="width:${curPct}%;background:${mC}"></div></div></div>`;
                } else if (tier) {
                    /* Scout data but no skill sums */
                    const peakSum = peakArr[tier.val - 1];
                    const peakPct = (peakSum / maxPeakSum) * 100;
                    const c = barColor(tier.val, tier.max);
                    peaksH += `<div class="tmbe-item tmbe-peak-item"><div class="tmbe-peak-header"><span class="tmbe-lbl">${p.label}</span><span class="tmbe-val" style="color:${c}">${tier.val}/${tier.max}${p.conf !== null ? cb(p.conf) : ''}</span></div><div class="tmbe-bar-track"><div class="tmbe-bar-fill" style="width:${peakPct}%;background:${c}"></div></div></div>`;
                }
            }

            /* Personality */
            let persH = '';
            if (psyPick) {
                const pers = [{ label: 'Leadership', value: parseInt(psyPick.report.charisma) || 0 }, { label: 'Professionalism', value: parseInt(psyPick.report.professionalism) || 0 }, { label: 'Aggression', value: parseInt(psyPick.report.aggression) || 0 }];
                for (const p of pers) { const pct = (p.value / 20) * 100; const c = skillColor(p.value); persH += `<div class="tmbe-bar-row"><div class="tmbe-bar-header"><span class="tmbe-bar-label">${p.label}</span><div class="tmbe-bar-right"><span class="tmbe-bar-val" style="color:${c}">${p.value}</span>${cb(psyPick.conf)}</div></div><div class="tmbe-bar-track"><div class="tmbe-bar-fill" style="width:${pct}%;background:${c}"></div></div></div>`; }
            } else if (!hasScouts) {
                const persLabels = ['Leadership', 'Professionalism', 'Aggression'];
                for (const lbl of persLabels) { persH += `<div class="tmbe-bar-row"><div class="tmbe-bar-header"><span class="tmbe-bar-label">${lbl}</span><div class="tmbe-bar-right"><span class="tmbe-bar-val" style="color:#5a7a48">?</span></div></div></div>`; }
            }

            const currentRating = playerRecSort !== null ? playerRecSort : rec;
            const hasData = regular.length > 0;
            let h = `<div class="tmbe-card"><div class="tmbe-title">Best Estimate<span class="tmbe-title-stars">${combinedStarsHtml(currentRating, potStarsVal)}</span></div>`;
            h += `<div class="tmbe-grid">`;
            h += `<div class="tmbe-item"><span class="tmbe-lbl">Potential</span><span class="tmbe-val" style="color:${hasData ? potColor(pot) : '#5a7a48'}">${hasData ? pot : '?'}${potPick ? cb(potPick.conf) : ''}</span></div>`;
            const beBloomClr = hasData ? (bloomResult.certain ? (bloomResult.phases ? '#80e048' : bloomColor(bloomTxt)) : '#fbbf24') : '#5a7a48';
            const beBloomTxt = hasData ? (!bloomResult.certain && !bloomResult.phases ? (bloomResult.text || bloomResult.range || '-') : bloomTxt) : '?';
            let beBloomSub = '';
            if (hasData && bloomResult.phases) beBloomSub += `<span style="display:block;font-size:9px;color:#90b878;font-weight:600;margin-top:1px">${bloomResult.phases}</span>`;
            if (hasData && bloomResult.range && bloomTxt !== 'Bloomed') beBloomSub += `<span style="display:block;font-size:9px;color:#6a9a58;font-weight:600;margin-top:1px">${bloomResult.range}</span>`;
            h += `<div class="tmbe-item"><span class="tmbe-lbl">Bloom</span><span class="tmbe-val" style="color:${beBloomClr}">${beBloomTxt}${bloomPick ? cb(bloomPick.conf) : ''}${beBloomSub}</span></div>`;
            h += `<div class="tmbe-item"><span class="tmbe-lbl">Development</span><span class="tmbe-val" style="color:${hasData ? '#e8f5d8' : '#5a7a48'}">${hasData ? devTxt : '?'}${bloomPick ? cb(bloomPick.conf) : ''}</span></div>`;
            h += `<div class="tmbe-item"><span class="tmbe-lbl">Specialty</span><span class="tmbe-val" style="color:${hasData ? (specVal > 0 ? '#fbbf24' : '#5a7a48') : '#5a7a48'}">${hasData ? specLabel : '?'}${specConf !== null ? cb(specConf) : ''}</span></div>`;
            if (peaksH) h += `<div class="tmbe-divider">Peak Development</div>${peaksH}`;
            h += `</div>`;
            if (persH) h += `<div class="tmbe-divider">Personality</div>${persH}`;
            h += `</div>`;
            return h;
        };

        return { render, reRender, getEstimateHtml };
    })();

    /* ═══════════════════════════════════════════════════════════
       ███  MODULE: TRAINING (Shadow DOM)
       ═══════════════════════════════════════════════════════════ */
    const TrainingMod = (() => {
        const TRAINING_TYPES = { '1': 'Technical', '2': 'Fitness', '3': 'Tactical', '4': 'Finishing', '5': 'Defending', '6': 'Wings' };
        const MAX_PTS = 4;
        const SKILL_NAMES = { strength: 'Strength', stamina: 'Stamina', pace: 'Pace', marking: 'Marking', tackling: 'Tackling', workrate: 'Workrate', positioning: 'Positioning', passing: 'Passing', crossing: 'Crossing', technique: 'Technique', heading: 'Heading', finishing: 'Finishing', longshots: 'Longshots', set_pieces: 'Set Pieces' };
        const COLORS = ['#6cc040', '#5b9bff', '#fbbf24', '#f97316', '#a78bfa', '#f87171'];

        const TMT_CSS = `*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
:host{display:block;all:initial;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;color:#c8e0b4;line-height:1.4}
.tmt-wrap{background:transparent;border-radius:0;border:none;overflow:hidden;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;color:#c8e0b4;font-size:13px}
.tmt-tabs{display:flex;gap:6px;padding:10px 14px 6px;flex-wrap:wrap}
.tmt-tab{padding:4px 12px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.4px;color:#90b878;cursor:pointer;border-radius:4px;background:rgba(42,74,28,.3);border:1px solid rgba(42,74,28,.6);transition:all 0.15s;font-family:inherit;-webkit-appearance:none;appearance:none}
.tmt-tab:hover{color:#c8e0b4;background:rgba(42,74,28,.5);border-color:#3d6828}.tmt-tab.active{color:#e8f5d8;background:#305820;border-color:#3d6828}
.tmt-pro{display:inline-block;background:rgba(108,192,64,.2);color:#6cc040;padding:1px 5px;border-radius:3px;font-size:9px;font-weight:800;letter-spacing:0.5px;margin-left:4px;vertical-align:middle}
.tmt-body{padding:10px 14px 16px;font-size:13px}
.tmt-sbar{display:flex;align-items:center;gap:8px;padding:6px 10px;background:rgba(42,74,28,.35);border:1px solid #2a4a1c;border-radius:6px;margin-bottom:10px;flex-wrap:wrap}
.tmt-sbar-label{color:#6a9a58;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.4px}
.tmt-sbar select{background:rgba(42,74,28,.4);color:#c8e0b4;border:1px solid #2a4a1c;padding:4px 8px;border-radius:6px;font-size:11px;cursor:pointer;font-weight:600;font-family:inherit}
.tmt-sbar select:focus{border-color:#6cc040;outline:none}
.tmt-cards{display:flex;gap:14px;margin-bottom:12px;padding:12px 14px;background:rgba(42,74,28,.3);border:1px solid #2a4a1c;border-radius:8px;flex-wrap:wrap}
.tmt-cards>div{min-width:80px}.tmt-cards .lbl{color:#6a9a58;font-size:9px;text-transform:uppercase;letter-spacing:0.5px;font-weight:700}.tmt-cards .val{font-size:16px;font-weight:800;margin-top:3px}
.tmt-pool-bar{height:6px;background:rgba(0,0,0,.2);border-radius:3px;overflow:hidden;display:flex;gap:1px;margin-top:8px}
.tmt-pool-seg{height:100%;border-radius:3px;transition:width 0.3s ease;min-width:0}.tmt-pool-rem{flex:1;height:100%}
.tmt-tbl{width:100%;border-collapse:collapse;font-size:11px;margin-bottom:8px}
.tmt-tbl th{padding:6px;font-size:10px;font-weight:700;color:#6a9a58;text-transform:uppercase;letter-spacing:0.4px;border-bottom:1px solid #2a4a1c;text-align:left;white-space:nowrap}.tmt-tbl th.c{text-align:center}
.tmt-tbl td{padding:5px 6px;border-bottom:1px solid rgba(42,74,28,.4);color:#c8e0b4;font-variant-numeric:tabular-nums;vertical-align:middle}.tmt-tbl td.c{text-align:center}
.tmt-tbl tr:hover{background:rgba(255,255,255,.03)}
.tmt-clr-bar{width:3px;padding:0;border-radius:2px}
.tmt-dots{display:inline-flex;gap:3px;align-items:center}
.tmt-dot{width:18px;height:18px;border-radius:50%;transition:all 0.15s;cursor:pointer;display:inline-block}
.tmt-dot-empty{background:rgba(255,255,255,.06);border:1px solid rgba(42,74,28,.6)}.tmt-dot-empty:hover{background:rgba(255,255,255,.12);border-color:rgba(42,74,28,.9)}
.tmt-dot-filled{box-shadow:0 0 6px rgba(0,0,0,.25),inset 0 1px 0 rgba(255,255,255,.2);border:1px solid rgba(255,255,255,.15)}
.tmt-btn{display:inline-flex;align-items:center;justify-content:center;width:24px;height:24px;background:rgba(42,74,28,.4);border:1px solid #2a4a1c;border-radius:6px;color:#8aac72;font-size:14px;font-weight:700;cursor:pointer;transition:all 0.15s;padding:0;line-height:1;font-family:inherit;-webkit-appearance:none;appearance:none}
.tmt-btn:hover:not(:disabled){background:rgba(42,74,28,.7);color:#c8e0b4}.tmt-btn:active:not(:disabled){background:rgba(74,144,48,.3)}.tmt-btn:disabled{opacity:0.2;cursor:not-allowed}
.tmt-pts{font-size:13px;font-weight:800;color:#e8f5d8;min-width:14px;text-align:center}
.tmt-footer{display:flex;align-items:center;justify-content:space-between;padding:12px 14px;background:rgba(42,74,28,.3);border:1px solid #2a4a1c;border-radius:8px;gap:10px;flex-wrap:wrap}
.tmt-footer-total .lbl{color:#6a9a58;font-size:9px;text-transform:uppercase;letter-spacing:0.5px;font-weight:700}
.tmt-footer-total .val{font-size:18px;font-weight:900;color:#e8f5d8;letter-spacing:-0.5px}.tmt-footer-total .dim{color:#6a9a58;font-weight:600}
.tmt-footer-acts{display:flex;gap:6px}
.tmt-act{display:inline-block;padding:4px 14px;background:rgba(42,74,28,.4);border:1px solid #2a4a1c;border-radius:6px;color:#8aac72;font-size:11px;font-weight:600;cursor:pointer;transition:all 0.15s;text-transform:uppercase;letter-spacing:0.4px;font-family:inherit;-webkit-appearance:none;appearance:none}
.tmt-act:hover{background:rgba(42,74,28,.7);color:#c8e0b4}
.tmt-act.dng:hover{border-color:rgba(248,113,113,.3);color:#f87171;background:rgba(248,113,113,.08)}
.tmt-saved{display:inline-block;font-size:10px;font-weight:700;color:#6cc040;background:rgba(108,192,64,.12);border:1px solid rgba(108,192,64,.25);border-radius:4px;padding:2px 8px;margin-left:8px;opacity:0;transition:opacity 0.3s;vertical-align:middle}.tmt-saved.vis{opacity:1}
.tmt-custom-off .tmt-cards{display:none}.tmt-custom-off .tmt-tbl{display:none}.tmt-custom-off .tmt-footer{display:none}
.tmt-wrap:not(.tmt-custom-off) .tmt-sbar{display:none}`;

        let trainingData = null, teamPoints = [0, 0, 0, 0, 0, 0], originalPoints = [0, 0, 0, 0, 0, 0], maxPool = 0, customOn = false, currentType = '3', shadow = null, customDataRef = null;
        const q = (sel) => shadow ? shadow.querySelector(sel) : null;
        const qa = (sel) => shadow ? shadow.querySelectorAll(sel) : [];

        const renderPoolBar = () => { const tot = teamPoints.reduce((a, b) => a + b, 0); let s = ''; for (let i = 0; i < 6; i++) { if (teamPoints[i] > 0) { s += `<div class="tmt-pool-seg" style="width:${(teamPoints[i] / maxPool * 100).toFixed(2)}%;background:${COLORS[i]};opacity:0.7"></div>`; } } const rem = ((maxPool - tot) / maxPool * 100).toFixed(2); if (rem > 0) s += `<div class="tmt-pool-rem" style="width:${rem}%"></div>`; return s; };
        const renderDots = (idx) => { const pts = teamPoints[idx]; const c = COLORS[idx]; let h = ''; for (let i = 0; i < MAX_PTS; i++) { h += i < pts ? `<span class="tmt-dot tmt-dot-filled" data-team="${idx}" data-seg="${i}" style="background:${c}"></span>` : `<span class="tmt-dot tmt-dot-empty" data-team="${idx}" data-seg="${i}"></span>`; } return h; };

        let saveDebounce = null, saveTimer = null;
        const flashSaved = () => { const el = q('#saved'); if (!el) return; el.classList.add('vis'); clearTimeout(saveTimer); saveTimer = setTimeout(() => el.classList.remove('vis'), 1800); };
        const saveCustomTraining = () => { const tot = teamPoints.reduce((a, b) => a + b, 0); if (tot !== maxPool || !customDataRef) return; clearTimeout(saveDebounce); saveDebounce = setTimeout(() => { const d = { type: 'custom', on: 1, player_id: PLAYER_ID, 'custom[points_spend]': 0, 'custom[player_id]': PLAYER_ID, 'custom[saved]': '' }; for (let i = 0; i < 6; i++) { const t = customDataRef['team' + (i + 1)]; const p = `custom[team${i + 1}]`; d[`${p}[num]`] = i + 1; d[`${p}[label]`] = t.label || `Team ${i + 1}`; d[`${p}[points]`] = teamPoints[i]; d[`${p}[skills][]`] = t.skills; } $.post('/ajax/training_post.ajax.php', d).done(() => flashSaved()); }, 300); };
        const saveTrainingType = (type) => { $.post('/ajax/training_post.ajax.php', { type: 'player_pos', player_id: PLAYER_ID, team_id: type }).done(() => flashSaved()); };

        const updateUI = () => {
            const tot = teamPoints.reduce((a, b) => a + b, 0); const rem = maxPool - tot;
            const barEl = q('#pool-bar'); if (barEl) barEl.innerHTML = renderPoolBar();
            const uEl = q('#card-used'); if (uEl) uEl.textContent = tot;
            const fEl = q('#card-free'); if (fEl) { fEl.textContent = rem; fEl.style.color = rem > 0 ? '#fbbf24' : '#6a9a58'; }
            for (let i = 0; i < 6; i++) { const dEl = q(`#dots-${i}`); if (dEl) dEl.innerHTML = renderDots(i); const pEl = q(`#pts-${i}`); if (pEl) pEl.textContent = teamPoints[i]; }
            const tEl = q('#total'); if (tEl) tEl.innerHTML = `${tot}<span class="dim">/${maxPool}</span>`;
            qa('.tmt-minus').forEach(b => { b.disabled = teamPoints[parseInt(b.dataset.team)] <= 0; });
            qa('.tmt-plus').forEach(b => { b.disabled = teamPoints[parseInt(b.dataset.team)] >= MAX_PTS || rem <= 0; });
            bindDotClicks();
        };

        const bindDotClicks = () => { qa('.tmt-dot').forEach(dot => { dot.onclick = () => { const ti = parseInt(dot.dataset.team); const si = parseInt(dot.dataset.seg); const tp = si + 1; const tot = teamPoints.reduce((a, b) => a + b, 0); const cur = teamPoints[ti]; if (tp === cur) teamPoints[ti] = si; else if (tp > cur) { const need = tp - cur; const avail = maxPool - tot; teamPoints[ti] = need <= avail ? tp : cur + avail; } else teamPoints[ti] = tp; updateUI(); saveCustomTraining(); }; }); };

        const bindEvents = () => {
            qa('.tmt-plus').forEach(b => { b.addEventListener('click', () => { const i = parseInt(b.dataset.team); if (teamPoints[i] < MAX_PTS && teamPoints.reduce((a, b) => a + b, 0) < maxPool) { teamPoints[i]++; updateUI(); saveCustomTraining(); } }); });
            qa('.tmt-minus').forEach(b => { b.addEventListener('click', () => { const i = parseInt(b.dataset.team); if (teamPoints[i] > 0) { teamPoints[i]--; updateUI(); saveCustomTraining(); } }); });
            bindDotClicks();
            q('#btn-clear')?.addEventListener('click', () => { teamPoints.fill(0); updateUI(); saveCustomTraining(); });
            q('#btn-reset')?.addEventListener('click', () => { teamPoints = [...originalPoints]; updateUI(); saveCustomTraining(); });
            const tS = q('#tab-std'), tC = q('#tab-cus'), w = q('.tmt-wrap');
            tS?.addEventListener('click', () => { if (customOn) { customOn = false; tS.classList.add('active'); tC.classList.remove('active'); w.classList.add('tmt-custom-off'); saveTrainingType(currentType); } });
            tC?.addEventListener('click', () => { if (!customOn) { customOn = true; tC.classList.add('active'); tS.classList.remove('active'); w.classList.remove('tmt-custom-off'); saveCustomTraining(); } });
            q('#type-select')?.addEventListener('change', (e) => { const v = e.target.value; if (v !== currentType) { currentType = v; saveTrainingType(v); } });
            updateUI();
        };

        const render = (container, data) => {
            trainingData = data;
            const custom = data.custom;
            const customData = custom.custom;
            customOn = !!custom.custom_on;
            currentType = String(custom.team || '3');
            customDataRef = customData;

            if (data.custom?.gk) {
                container.innerHTML = '';
                const host = document.createElement('div');
                container.appendChild(host);
                shadow = host.attachShadow({ mode: 'open' });
                shadow.innerHTML = `<style>${TMT_CSS}</style><div class="tmt-wrap"><div class="tmt-body" style="text-align:center;padding:20px 14px"><div style="font-size:22px;margin-bottom:6px">🧤</div><div style="color:#e8f5d8;font-weight:700;font-size:14px;margin-bottom:4px">Goalkeeper Training</div><div style="color:#6a9a58;font-size:11px">Training is automatically set and cannot be changed for goalkeepers.</div></div></div>`;
                return;
            }

            for (let i = 0; i < 6; i++) { const t = customData['team' + (i + 1)]; teamPoints[i] = parseInt(t.points) || 0; originalPoints[i] = teamPoints[i]; }
            const totalAlloc = teamPoints.reduce((a, b) => a + b, 0);
            maxPool = totalAlloc + (parseInt(customData.points_spend) || 0); if (maxPool < 1) maxPool = 10;
            const rem = maxPool - totalAlloc;

            container.innerHTML = ''; const host = document.createElement('div'); container.appendChild(host);
            shadow = host.attachShadow({ mode: 'open' });

            let typeOpts = customOn ? '<option value="" selected>— Select —</option>' : '';
            Object.entries(TRAINING_TYPES).forEach(([id, name]) => { typeOpts += `<option value="${id}" ${!customOn && id === currentType ? 'selected' : ''}>${name}</option>`; });

            let teamRows = '';
            for (let i = 0; i < 6; i++) { const t = customData['team' + (i + 1)]; const skills = t.skills.map(s => SKILL_NAMES[s] || s).join(', '); teamRows += `<tr data-team="${i}"><td class="tmt-clr-bar" style="background:${COLORS[i]}"></td><td style="font-weight:700;color:#e8f5d8;white-space:nowrap">T${i + 1}</td><td style="color:#8aac72;font-size:11px">${skills}</td><td class="c"><div style="display:flex;align-items:center;gap:6px;justify-content:center"><button class="tmt-btn tmt-minus" data-team="${i}">−</button><span class="tmt-dots" id="dots-${i}">${renderDots(i)}</span><span class="tmt-pts" id="pts-${i}">${teamPoints[i]}</span><button class="tmt-btn tmt-plus" data-team="${i}">+</button></div></td></tr>`; }

            shadow.innerHTML = `<style>${TMT_CSS}</style>
<div class="tmt-wrap ${customOn ? '' : 'tmt-custom-off'}">
<div class="tmt-tabs"><button class="tmt-tab ${!customOn ? 'active' : ''}" id="tab-std">Standard</button><button class="tmt-tab ${customOn ? 'active' : ''}" id="tab-cus">Custom <span class="tmt-pro">PRO</span></button></div>
<div class="tmt-body">
<div class="tmt-sbar" id="type-bar"><span class="tmt-sbar-label">Training Type</span><select id="type-select">${typeOpts}</select></div>
<div class="tmt-cards"><div><div class="lbl">Allocated</div><div class="val" style="color:#6cc040" id="card-used">${totalAlloc}</div></div><div><div class="lbl">Remaining</div><div class="val" style="color:${rem > 0 ? '#fbbf24' : '#6a9a58'}" id="card-free">${rem}</div></div><div><div class="lbl">Total Pool</div><div class="val" style="color:#e8f5d8">${maxPool}</div></div><div style="flex:1;display:flex;align-items:flex-end"><div class="tmt-pool-bar" id="pool-bar" style="width:100%">${renderPoolBar()}</div></div></div>
<table class="tmt-tbl" id="teams-tbl"><thead><tr><th style="width:3px;padding:0"></th><th style="width:30px">Team</th><th>Skills</th><th class="c">Points</th></tr></thead><tbody id="teams-body">${teamRows}</tbody></table>
<div class="tmt-footer"><div class="tmt-footer-total"><div class="lbl">Total Training</div><div class="val" id="total">${totalAlloc}<span class="dim">/${maxPool}</span></div></div><div class="tmt-footer-acts"><button class="tmt-act dng" id="btn-clear">Clear All</button><button class="tmt-act" id="btn-reset">Reset</button></div></div>
</div></div>`;
            bindEvents();
        };

        return { render };
    })();

    /* ═══════════════════════════════════════════════════════════
       ███  MODULE: GRAPHS
       ═══════════════════════════════════════════════════════════ */
    const GraphsMod = (() => {
        let lastData = null;
        let containerRef = null;

        const SKILL_META = [
            { key: 'strength', label: 'Strength', color: '#22cc22' }, { key: 'stamina', label: 'Stamina', color: '#00bcd4' },
            { key: 'pace', label: 'Pace', color: '#8bc34a' }, { key: 'marking', label: 'Marking', color: '#f44336' },
            { key: 'tackling', label: 'Tackling', color: '#26a69a' }, { key: 'workrate', label: 'Workrate', color: '#3f51b5' },
            { key: 'positioning', label: 'Positioning', color: '#9c27b0' }, { key: 'passing', label: 'Passing', color: '#e91e63' },
            { key: 'crossing', label: 'Crossing', color: '#2196f3' }, { key: 'technique', label: 'Technique', color: '#ff4081' },
            { key: 'heading', label: 'Heading', color: '#757575' }, { key: 'finishing', label: 'Finishing', color: '#4caf50' },
            { key: 'longshots', label: 'Longshots', color: '#00e5ff' }, { key: 'set_pieces', label: 'Set Pieces', color: '#607d8b' }
        ];
        const SKILL_META_GK = [
            { key: 'strength', label: 'Strength', color: '#22cc22' }, { key: 'stamina', label: 'Stamina', color: '#00bcd4' },
            { key: 'pace', label: 'Pace', color: '#8bc34a' }, { key: 'handling', label: 'Handling', color: '#f44336' },
            { key: 'one_on_ones', label: 'One on ones', color: '#26a69a' }, { key: 'reflexes', label: 'Reflexes', color: '#3f51b5' },
            { key: 'aerial_ability', label: 'Aerial Ability', color: '#9c27b0' }, { key: 'jumping', label: 'Jumping', color: '#e91e63' },
            { key: 'communication', label: 'Communication', color: '#2196f3' }, { key: 'kicking', label: 'Kicking', color: '#ff4081' },
            { key: 'throwing', label: 'Throwing', color: '#757575' }
        ];
        const getSkillMeta = () => isGoalkeeper ? SKILL_META_GK : SKILL_META;
        const PEAK_META = [
            { key: 'physical', label: 'Physical', color: '#ffeb3b' },
            { key: 'tactical', label: 'Tactical', color: '#00e5ff' },
            { key: 'technical', label: 'Technical', color: '#ff4081' }
        ];

        const calcTicks = (min, max, n) => {
            if (max === min) return [min]; const range = max - min; const raw = range / n; const mag = Math.pow(10, Math.floor(Math.log10(raw)));
            const res = raw / mag; let step; if (res <= 1.5) step = mag; else if (res <= 3) step = 2 * mag; else if (res <= 7) step = 5 * mag; else step = 10 * mag;
            const ticks = []; let t = Math.ceil(min / step) * step; while (t <= max + step * 0.01) { ticks.push(Math.round(t * 10000) / 10000); t += step; } return ticks;
        };

        const buildAges = (n, years, months) => { const cur = years + months / 12; const ages = []; for (let i = 0; i < n; i++)ages.push(cur - (n - 1 - i) / 12); return ages; };

        const drawChart = (canvas, ages, values, opts = {}) => {
            const { lineColor = '#fff', fillColor = 'rgba(255,255,255,0.06)', gridColor = 'rgba(255,255,255,0.10)', axisColor = '#9ab889', dotRadius = 2.5, yMinOverride, yMaxOverride, formatY = v => String(Math.round(v)) } = opts;
            const ctx = canvas.getContext('2d'); const dpr = window.devicePixelRatio || 1;
            const cssW = canvas.clientWidth, cssH = canvas.clientHeight;
            canvas.width = cssW * dpr; canvas.height = cssH * dpr; ctx.scale(dpr, dpr);
            const pL = 50, pR = 10, pT = 12, pB = 30, cW = cssW - pL - pR, cH = cssH - pT - pB;
            const minA = Math.floor(Math.min(...ages)), maxA = Math.ceil(Math.max(...ages));
            const rMin = Math.min(...values), rMax = Math.max(...values), m = (rMax - rMin) * 0.06 || 1;
            const yMin = yMinOverride !== undefined ? yMinOverride : Math.floor(rMin - m);
            const yMax = yMaxOverride !== undefined ? yMaxOverride : Math.ceil(rMax + m);
            const xS = v => pL + ((v - minA) / (maxA - minA)) * cW; const yS = v => pT + cH - ((v - yMin) / (yMax - yMin)) * cH;
            ctx.clearRect(0, 0, cssW, cssH); ctx.fillStyle = 'rgba(0,0,0,0.08)'; ctx.fillRect(pL, pT, cW, cH);
            const yTicks = calcTicks(yMin, yMax, 6);
            ctx.font = '11px -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif'; ctx.textAlign = 'right';
            yTicks.forEach(tick => { const y = yS(tick); if (y < pT || y > pT + cH) return; ctx.strokeStyle = gridColor; ctx.lineWidth = 1; ctx.beginPath(); ctx.moveTo(pL, y); ctx.lineTo(pL + cW, y); ctx.stroke(); ctx.fillStyle = axisColor; ctx.fillText(formatY(tick), pL - 7, y + 4); });
            ctx.textAlign = 'center';
            for (let a = minA; a <= maxA; a++) { const x = xS(a); ctx.strokeStyle = gridColor; ctx.lineWidth = 1; ctx.beginPath(); ctx.moveTo(x, pT); ctx.lineTo(x, pT + cH); ctx.stroke(); ctx.fillStyle = axisColor; ctx.fillText(String(a), x, pT + cH + 16); }
            ctx.fillStyle = axisColor; ctx.font = '12px -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif'; ctx.textAlign = 'center'; ctx.fillText('Age', cssW / 2, cssH - 2);
            ctx.beginPath(); ctx.moveTo(xS(ages[0]), yS(values[0])); for (let i = 1; i < values.length; i++)ctx.lineTo(xS(ages[i]), yS(values[i]));
            ctx.lineTo(xS(ages[ages.length - 1]), pT + cH); ctx.lineTo(xS(ages[0]), pT + cH); ctx.closePath(); ctx.fillStyle = fillColor; ctx.fill();
            ctx.beginPath(); ctx.strokeStyle = lineColor; ctx.lineWidth = 1.8; ctx.lineJoin = 'round'; ctx.lineCap = 'round';
            ctx.moveTo(xS(ages[0]), yS(values[0])); for (let i = 1; i < values.length; i++)ctx.lineTo(xS(ages[i]), yS(values[i])); ctx.stroke();
            for (let i = 0; i < values.length; i++) { ctx.beginPath(); ctx.arc(xS(ages[i]), yS(values[i]), dotRadius, 0, Math.PI * 2); ctx.fillStyle = lineColor; ctx.fill(); ctx.strokeStyle = 'rgba(0,0,0,0.25)'; ctx.lineWidth = 0.8; ctx.stroke(); }
            ctx.strokeStyle = 'rgba(120,180,80,0.3)'; ctx.lineWidth = 1; ctx.strokeRect(pL, pT, cW, cH);
            return { xS, yS, ages, values, formatY };
        };

        const drawMultiLine = (canvas, ages, seriesData, opts = {}) => {
            const { gridColor = 'rgba(255,255,255,0.10)', axisColor = '#9ab889', yMinOverride, yMaxOverride, formatY = v => String(Math.round(v)), dotRadius = 1.5, yTickCount = 6 } = opts;
            const ctx = canvas.getContext('2d'); const dpr = window.devicePixelRatio || 1;
            const cssW = canvas.clientWidth, cssH = canvas.clientHeight; canvas.width = cssW * dpr; canvas.height = cssH * dpr; ctx.scale(dpr, dpr);
            const pL = 50, pR = 10, pT = 12, pB = 30, cW = cssW - pL - pR, cH = cssH - pT - pB;
            const vis = seriesData.filter(s => s.visible); let all = []; vis.forEach(s => all.push(...s.values)); if (!all.length) all = [0, 1];
            const rMin = Math.min(...all), rMax = Math.max(...all), m = (rMax - rMin) * 0.06 || 1;
            const yMin = yMinOverride !== undefined ? yMinOverride : Math.floor(rMin - m);
            const yMax = yMaxOverride !== undefined ? yMaxOverride : Math.ceil(rMax + m);
            const minA = Math.floor(Math.min(...ages)), maxA = Math.ceil(Math.max(...ages));
            const xS = v => pL + ((v - minA) / (maxA - minA)) * cW; const yS = v => pT + cH - ((v - yMin) / (yMax - yMin)) * cH;
            ctx.clearRect(0, 0, cssW, cssH); ctx.fillStyle = 'rgba(0,0,0,0.08)'; ctx.fillRect(pL, pT, cW, cH);
            const yTicks = calcTicks(yMin, yMax, yTickCount);
            ctx.font = '11px -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif'; ctx.textAlign = 'right';
            yTicks.forEach(tick => { const y = yS(tick); if (y < pT || y > pT + cH) return; ctx.strokeStyle = gridColor; ctx.lineWidth = 1; ctx.beginPath(); ctx.moveTo(pL, y); ctx.lineTo(pL + cW, y); ctx.stroke(); ctx.fillStyle = axisColor; ctx.fillText(formatY(tick), pL - 7, y + 4); });
            ctx.textAlign = 'center';
            for (let a = minA; a <= maxA; a++) { const x = xS(a); ctx.strokeStyle = gridColor; ctx.lineWidth = 1; ctx.beginPath(); ctx.moveTo(x, pT); ctx.lineTo(x, pT + cH); ctx.stroke(); ctx.fillStyle = axisColor; ctx.fillText(String(a), x, pT + cH + 16); }
            ctx.fillStyle = axisColor; ctx.font = '12px -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif'; ctx.textAlign = 'center'; ctx.fillText('Age', cssW / 2, cssH - 2);
            vis.forEach(s => { ctx.beginPath(); ctx.strokeStyle = s.color; ctx.lineWidth = 1.5; ctx.lineJoin = 'round'; ctx.lineCap = 'round'; ctx.moveTo(xS(ages[0]), yS(s.values[0])); for (let i = 1; i < s.values.length; i++)ctx.lineTo(xS(ages[i]), yS(s.values[i])); ctx.stroke(); for (let i = 0; i < s.values.length; i++) { ctx.beginPath(); ctx.arc(xS(ages[i]), yS(s.values[i]), dotRadius, 0, Math.PI * 2); ctx.fillStyle = s.color; ctx.fill(); } });
            ctx.strokeStyle = 'rgba(120,180,80,0.3)'; ctx.lineWidth = 1; ctx.strokeRect(pL, pT, cW, cH);
            return { xS, yS, ages, seriesData, formatY };
        };

        const attachTooltip = (wrap, canvas, info) => {
            const tip = wrap.querySelector('.tmg-tooltip'); if (!tip) return;
            canvas.addEventListener('mousemove', e => { const r = canvas.getBoundingClientRect(); const mx = e.clientX - r.left, my = e.clientY - r.top; let best = -1, bd = Infinity; for (let i = 0; i < info.ages.length; i++) { const d = Math.hypot(mx - info.xS(info.ages[i]), my - info.yS(info.values[i])); if (d < bd && d < 25) { bd = d; best = i; } } if (best >= 0) { const a = info.ages[best], v = info.values[best]; const ay = Math.floor(a), am = Math.round((a - ay) * 12); tip.innerHTML = `<b>Age:</b> ${ay}y ${am}m &nbsp; <b>Value:</b> ${info.formatY(v)}`; tip.style.display = 'block'; const px = info.xS(a), py = info.yS(v); let tx = px - tip.offsetWidth / 2; if (tx < 0) tx = 0; if (tx + tip.offsetWidth > canvas.clientWidth) tx = canvas.clientWidth - tip.offsetWidth; tip.style.left = tx + 'px'; tip.style.top = (py - 32) + 'px'; } else tip.style.display = 'none'; });
            canvas.addEventListener('mouseleave', () => { tip.style.display = 'none'; });
        };

        const attachMultiTooltip = (wrap, canvas, infoGetter) => {
            const tip = wrap.querySelector('.tmg-tooltip'); if (!tip) return;
            canvas.addEventListener('mousemove', e => { const r = canvas.getBoundingClientRect(); const mx = e.clientX - r.left, my = e.clientY - r.top; const info = infoGetter(); if (!info) return; let best = null, bd = Infinity; info.seriesData.filter(s => s.visible).forEach(s => { for (let i = 0; i < s.values.length; i++) { const d = Math.hypot(mx - info.xS(info.ages[i]), my - info.yS(s.values[i])); if (d < bd && d < 25) { bd = d; best = { series: s, idx: i }; } } }); if (best) { const a = info.ages[best.idx], v = best.series.values[best.idx]; const ay = Math.floor(a), am = Math.round((a - ay) * 12); tip.innerHTML = `<span style="color:${best.series.color}">●</span> <b>${best.series.label}:</b> ${info.formatY(v)} &nbsp; <b>Age:</b> ${ay}y ${am}m`; tip.style.display = 'block'; const px = info.xS(a), py = info.yS(v); let tx = px - tip.offsetWidth / 2; if (tx < 0) tx = 0; if (tx + tip.offsetWidth > canvas.clientWidth) tx = canvas.clientWidth - tip.offsetWidth; tip.style.left = tx + 'px'; tip.style.top = (py - 32) + 'px'; } else tip.style.display = 'none'; });
            canvas.addEventListener('mouseleave', () => { tip.style.display = 'none'; });
        };

        const CHART_DEFS = [
            { key: 'ti', title: 'Training Intensity', opts: { lineColor: '#fff', fillColor: 'rgba(255,255,255,0.05)' }, prepareData: raw => { const v = []; for (let i = 0; i < raw.length; i++) { if (i === 0 && typeof raw[i] === 'number' && Number(raw[i]) === 0) continue; v.push(Number(raw[i])); } return v; } },
            { key: 'skill_index', title: 'ASI', opts: { lineColor: '#fff', fillColor: 'rgba(255,255,255,0.05)', formatY: v => v >= 1000 ? Math.round(v).toLocaleString() : String(Math.round(v)) }, prepareData: raw => raw.map(Number) },
            { key: 'recommendation', title: 'REC', opts: { lineColor: '#fff', fillColor: 'rgba(255,255,255,0.05)', yMinOverride: 0, formatY: v => v.toFixed(1) }, prepareData: raw => { const v = raw.map(Number); return v; }, yMaxFn: vals => Math.max(6, Math.ceil(Math.max(...vals) * 10) / 10) }
        ];

        const MULTI_DEFS = [
            { title: 'Skills', get meta() { return getSkillMeta(); }, showToggle: true, enableKey: 'skills', getSeriesData: g => { const sm = getSkillMeta(); return sm.map(m => ({ key: m.key, label: m.label, color: m.color, values: (g[m.key] || []).map(Number), visible: true })); }, opts: { yMinOverride: 0, yMaxOverride: 20, dotRadius: 1.5, yTickCount: 11 } },
            {
                title: 'Peaks', meta: PEAK_META, showToggle: false, enableKey: 'peaks', getSeriesData: g => {
                    const pk = g.peaks || {};
                    console.log('[Graphs] Raw peaks data', { pk });
                    /* If TM peaks exist, use them */
                    // if (PEAK_META.some(m => pk[m.key] && pk[m.key].length > 0)) {
                    //     return PEAK_META.map(m => ({ key: m.key, label: m.label, color: m.color, values: (pk[m.key] || []).map(Number), visible: true }));
                    // }
                    /* Compute peaks from skills */
                    if (isGoalkeeper) {
                        /* GK: Physical: Str+Sta+Pac+Jum (4×20=80), Tactical: 1v1+Aer+Com (3×20=60), Technical: Han+Ref+Kic+Thr (4×20=80) */
                        const PHYS = ['strength', 'stamina', 'pace', 'jumping'];
                        const TACT = ['one_on_ones', 'aerial_ability', 'communication'];
                        const TECH = ['handling', 'reflexes', 'kicking', 'throwing'];
                        const L = (g[PHYS[0]] || []).length;
                        if (L < 2) return PEAK_META.map(m => ({ key: m.key, label: m.label, color: m.color, values: [], visible: true }));
                        const sumAt = (keys, i) => keys.reduce((s, k) => s + ((g[k] || [])[i] || 0), 0);
                        const phys = [], tact = [], tech = [];
                        for (let i = 0; i < L; i++) {
                            phys.push(Math.round(sumAt(PHYS, i) / 80 * 1000) / 10);
                            tact.push(Math.round(sumAt(TACT, i) / 60 * 1000) / 10);
                            tech.push(Math.round(sumAt(TECH, i) / 80 * 1000) / 10);
                        }
                        return [
                            { key: 'physical', label: 'Physical', color: '#ffeb3b', values: phys, visible: true },
                            { key: 'tactical', label: 'Tactical', color: '#00e5ff', values: tact, visible: true },
                            { key: 'technical', label: 'Technical', color: '#ff4081', values: tech, visible: true }
                        ];
                    }
                    /* Outfield: Physical: Str+Sta+Pac+Hea (4×20=80), Tactical: Mar+Tac+Wor+Pos (4×20=80), Technical: Pas+Cro+Tec+Fin+Lon+Set (6×20=120) */
                    const PHYS = ['strength', 'stamina', 'pace', 'heading'];
                    const TACT = ['marking', 'tackling', 'workrate', 'positioning'];
                    const TECH = ['passing', 'crossing', 'technique', 'finishing', 'longshots', 'set_pieces'];
                    const L = (g[PHYS[0]] || []).length;
                    if (L < 2) return PEAK_META.map(m => ({ key: m.key, label: m.label, color: m.color, values: [], visible: true }));
                    const sumAt = (keys, i) => keys.reduce((s, k) => s + ((g[k] || [])[i] || 0), 0);
                    const phys = [], tact = [], tech = [];
                    for (let i = 0; i < L; i++) {
                        phys.push(Math.round(sumAt(PHYS, i) / 80 * 1000) / 10);
                        tact.push(Math.round(sumAt(TACT, i) / 80 * 1000) / 10);
                        tech.push(Math.round(sumAt(TECH, i) / 120 * 1000) / 10);
                    }
                    console.log('[Graphs] Peaks computed from skills', { g });
                    return [
                        { key: 'physical', label: 'Physical', color: '#ffeb3b', values: phys, visible: true },
                        { key: 'tactical', label: 'Tactical', color: '#00e5ff', values: tact, visible: true },
                        { key: 'technical', label: 'Technical', color: '#ff4081', values: tech, visible: true }
                    ];
                }, opts: { dotRadius: 1.5, yMinOverride: 0, yMaxOverride: 100, formatY: v => v.toFixed(1) + '%' }, legendInline: true
            }
        ];

        const buildSingleChart = (el, def, graphData, player) => {
            let values, ages;
            let enhanced = false;

            /* ASI fallback: if TM's skill_index is missing, reconstruct from TI or store */
            if (def.key === 'skill_index' && (!graphData[def.key] || graphData[def.key].length < 2)) {
                /* Priority 1: reconstruct ASI from TI array + current playerASI */
                if (playerASI > 0 && graphData.ti && graphData.ti.length >= 2) {
                    try {
                        const tiRaw = graphData.ti;
                        /* TI array usually has a dummy 0 at index 0; skip it */
                        const tiStart = (typeof tiRaw[0] === 'number' && tiRaw[0] === 0) || tiRaw[0] === '0' || tiRaw[0] === 0 ? 1 : 0;
                        const tiVals = tiRaw.slice(tiStart).map(v => parseInt(v) || 0);
                        const L = tiVals.length;
                        if (L >= 2) {
                            const K = isGoalkeeper ? 48717927500 : (Math.pow(2, 9) * Math.pow(5, 4) * Math.pow(7, 7));
                            const asiArr = new Array(L);
                            asiArr[L - 1] = playerASI;
                            for (let j = L - 2; j >= 0; j--) {
                                const ti = tiVals[j + 1];
                                const base = Math.pow(asiArr[j + 1] * K, 1 / 7);
                                asiArr[j] = Math.max(0, Math.round(Math.pow(base - ti / 10, 7) / K));
                            }
                            values = asiArr;
                            ages = buildAges(L, player.years, player.months);
                            enhanced = true;
                            console.log(`[Graphs] ASI reconstructed from TI (${L} points)`);
                        }
                    } catch (e) { console.warn('[Graphs] ASI from TI failed', e); }
                }
                /* Priority 2: fall back to store SI records */
                if (!values) {
                    try {
                        const store = PlayerDB.get(PLAYER_ID);
                        if (store && store.records) {
                            const keys = Object.keys(store.records).sort((a, b) => {
                                const [ay, am] = a.split('.').map(Number);
                                const [by, bm] = b.split('.').map(Number);
                                return (ay * 12 + am) - (by * 12 + bm);
                            });
                            const tmpAges = [], tmpVals = [];
                            keys.forEach(k => {
                                const si = parseInt(store.records[k].SI) || 0;
                                if (si <= 0) return;
                                const [y, m] = k.split('.').map(Number);
                                tmpAges.push(y + m / 12);
                                tmpVals.push(si);
                            });
                            /* Extend to current age using live playerASI from page */
                            if (tmpVals.length > 0 && playerASI > 0) {
                                const curAge = player.years + player.months / 12;
                                const lastAge = tmpAges[tmpAges.length - 1];
                                if (curAge > lastAge + 0.001) {
                                    tmpAges.push(curAge);
                                    tmpVals.push(playerASI);
                                }
                            }
                            if (tmpVals.length >= 2) {
                                values = tmpVals;
                                ages = tmpAges;
                                enhanced = true;
                            }
                        }
                    } catch (e) { }
                }
                if (!values) return;
                /* REC fallback: if TM's recommendation is missing, use our store REREC */
            } else if (def.key === 'recommendation' && (!graphData[def.key] || graphData[def.key].length < 2)) {
                try {
                    const store = PlayerDB.get(PLAYER_ID);
                    if (store && store._v >= 3 && store.records) {
                        const keys = Object.keys(store.records).sort((a, b) => {
                            const [ay, am] = a.split('.').map(Number);
                            const [by, bm] = b.split('.').map(Number);
                            return (ay * 12 + am) - (by * 12 + bm);
                        });
                        const tmpAges = [], tmpVals = [];
                        keys.forEach(k => {
                            const rec = store.records[k];
                            if (rec.REREC == null) return;
                            const [y, m] = k.split('.').map(Number);
                            tmpAges.push(y + m / 12);
                            tmpVals.push(rec.REREC);
                        });
                        if (tmpVals.length >= 2) {
                            values = tmpVals;
                            ages = tmpAges;
                            enhanced = true;
                        }
                    }
                } catch (e) { }
                if (!values) return;
            /* TI fallback: compute from ASI differences when TM's TI graph is missing */
            } else if (def.key === 'ti' && (!graphData[def.key] || graphData[def.key].length < 2)) {
                const K = isGoalkeeper ? 48717927500 : (Math.pow(2, 9) * Math.pow(5, 4) * Math.pow(7, 7));
                /* Priority 1: compute TI from ASI graph data */
                if (graphData.skill_index && graphData.skill_index.length >= 2) {
                    try {
                        const asiRaw = graphData.skill_index.map(Number);
                        const tiVals = [];
                        for (let i = 1; i < asiRaw.length; i++) {
                            const prev = Math.pow(asiRaw[i - 1] * K, 1 / 7);
                            const cur = Math.pow(asiRaw[i] * K, 1 / 7);
                            tiVals.push(Math.round((cur - prev) * 10));
                        }
                        if (tiVals.length >= 2) {
                            values = tiVals;
                            /* TI[i] corresponds to training from age[i] to age[i+1], so ages start one later */
                            ages = buildAges(tiVals.length, player.years, player.months);
                            enhanced = true;
                            console.log(`[Graphs] TI computed from ASI graph (${tiVals.length} points)`);
                        }
                    } catch (e) { console.warn('[Graphs] TI from ASI graph failed', e); }
                }
                /* Priority 2: compute TI from IndexedDB SI records */
                if (!values) {
                    try {
                        const store = PlayerDB.get(PLAYER_ID);
                        if (store && store.records) {
                            const keys = Object.keys(store.records).sort((a, b) => {
                                const [ay, am] = a.split('.').map(Number);
                                const [by, bm] = b.split('.').map(Number);
                                return (ay * 12 + am) - (by * 12 + bm);
                            });
                            const tmpAges = [], tmpASI = [];
                            keys.forEach(k => {
                                const si = parseInt(store.records[k].SI) || 0;
                                if (si <= 0) return;
                                const [y, m] = k.split('.').map(Number);
                                tmpAges.push(y + m / 12);
                                tmpASI.push(si);
                            });
                            if (tmpASI.length >= 2) {
                                const tiVals = [], tiAges = [];
                                for (let i = 1; i < tmpASI.length; i++) {
                                    const prev = Math.pow(tmpASI[i - 1] * K, 1 / 7);
                                    const cur = Math.pow(tmpASI[i] * K, 1 / 7);
                                    tiVals.push(Math.round((cur - prev) * 10));
                                    tiAges.push(tmpAges[i]);
                                }
                                if (tiVals.length >= 2) {
                                    values = tiVals;
                                    ages = tiAges;
                                    enhanced = true;
                                    console.log(`[Graphs] TI computed from store SI (${tiVals.length} points)`);
                                }
                            }
                        }
                    } catch (e) { }
                }
                if (!values) return;
            } else {
                const raw = graphData[def.key]; if (!raw) return;
                values = def.prepareData(raw); if (!values.length) return;
                ages = buildAges(values.length, player.years, player.months);
            }

            /* REC hybrid: splice our v3 REREC (0.01 precision) over TM's (0.10) */
            let recSpliceIdx = -1;
            if (def.key === 'recommendation') {
                try {
                    const store = PlayerDB.get(PLAYER_ID);
                    if (store && store._v >= 3 && store.records) {
                        const curAgeMonths = player.years * 12 + player.months;
                        const L = values.length;
                        for (let i = 0; i < L; i++) {
                            const am = curAgeMonths - (L - 1 - i);
                            const key = `${Math.floor(am / 12)}.${am % 12}`;
                            const rec = store.records[key];
                            if (rec && rec.REREC != null) {
                                if (recSpliceIdx < 0) recSpliceIdx = i;
                                values[i] = rec.REREC;
                            }
                        }
                        if (recSpliceIdx >= 0) console.log(`[Graphs] REC hybrid: TM data 0..${recSpliceIdx - 1}, our data ${recSpliceIdx}..${L - 1}`);
                    }
                } catch (e) { }
            }

            /* Dynamic yMax: use yMaxFn if defined (e.g. REC → min 6.0) */
            const chartOpts = { ...def.opts };
            if (def.yMaxFn) chartOpts.yMaxOverride = def.yMaxFn(values);
            /* When we have enhanced REC data, show 2 decimals in tooltip */
            if (recSpliceIdx >= 0 || (enhanced && def.key === 'recommendation')) {
                chartOpts.formatY = v => v % 1 === 0 ? v.toFixed(1) : v.toFixed(2);
            }

            const wrap = document.createElement('div'); wrap.className = 'tmg-chart-wrap';
            let enhLabel = '';
            if (enhanced && def.key === 'skill_index') enhLabel = ' <span style="font-size:10px;color:#f0c040;font-weight:400">(from TI)</span>';
            else if (enhanced && def.key === 'ti') enhLabel = ' <span style="font-size:10px;color:#f0c040;font-weight:400">(from ASI)</span>';
            else if (enhanced && def.key === 'recommendation') enhLabel = ' <span style="font-size:10px;color:#5b9bff;font-weight:400">(computed)</span>';
            else if (recSpliceIdx >= 0) enhLabel = ' <span style="font-size:10px;color:#38bdf8;font-weight:400">(enhanced)</span>';
            wrap.innerHTML = `<div class="tmg-chart-title">${def.title}${enhLabel}</div><canvas class="tmg-canvas" style="width:100%;height:260px;"></canvas><div class="tmg-tooltip"></div>`;
            el.appendChild(wrap);
            const canvas = wrap.querySelector('canvas');
            requestAnimationFrame(() => { const info = drawChart(canvas, ages, values, chartOpts); attachTooltip(wrap, canvas, info); });
        };

        /* Build per-skill arrays from v3 store records — fallback when TM skills unavailable */
        const buildStoreSkillGraphData = (player) => {
            try {
                const store = PlayerDB.get(PLAYER_ID);
                if (!store || !store.records) { console.log('[Skills] No store or no records'); return null; }
                const sm = getSkillMeta();
                const expectedLen = sm.length; /* 14 for outfield, 11 for GK */
                const sortedKeys = Object.keys(store.records).sort((a, b) => {
                    const [ay, am] = a.split('.').map(Number);
                    const [by, bm] = b.split('.').map(Number);
                    return (ay * 12 + am) - (by * 12 + bm);
                });
                console.log('[Skills] store._v:', store._v, 'total records:', sortedKeys.length, 'isGK:', isGoalkeeper);
                const skillArrays = {};
                sm.forEach(m => { skillArrays[m.key] = []; });
                let count = 0;
                sortedKeys.forEach(k => {
                    const rec = store.records[k];
                    const hasSkills = rec.skills && rec.skills.length >= expectedLen;
                    const nonZero = hasSkills && rec.skills.some(v => v !== 0);
                    if (!hasSkills || !nonZero) {
                        console.log(`[Skills] skip ${k}: hasSkills=${hasSkills}, nonZero=${nonZero}`, rec.skills?.slice(0,3));
                        return;
                    }
                    sm.forEach((m, i) => { skillArrays[m.key].push(rec.skills[i]); });
                    count++;
                });
                console.log('[Skills] usable records with skills:', count);
                if (count < 2) return null;
                skillArrays._ages = sortedKeys.filter(k => {
                    const r = store.records[k];
                    return r.skills && r.skills.length >= expectedLen && r.skills.some(v => v !== 0);
                }).map(k => { const [y, m] = k.split('.').map(Number); return y + m / 12; });
                return skillArrays;
            } catch (e) { console.log('[Skills] error:', e); return null; }
        };

        const buildMultiChart = (el, def, graphData, player, skillpoints, isOwnPlayer) => {
            let seriesData = def.getSeriesData(graphData);
            let fromStore = false;
            let storeAges = null;
            if (!seriesData.length || !seriesData[0].values.length) {
                /* Try store fallback */
                const storeGD = buildStoreSkillGraphData(player);
                if (storeGD) {
                    storeAges = storeGD._ages;
                    seriesData = def.getSeriesData(storeGD);
                }
                if (!seriesData.length || !seriesData[0].values.length) {
                    /* No data at all — show enable card if own player, else info msg */
                    if (isOwnPlayer && def.enableKey) {
                        buildEnableCard(el, def.enableKey);
                    } else if (def.enableKey) {
                        const msg = document.createElement('div');
                        msg.style.cssText = 'background:rgba(0,0,0,0.15);border:1px solid rgba(120,180,80,0.2);border-radius:6px;padding:10px 14px;margin:4px 0 8px;color:#5a7a48;font-size:11px;';
                        msg.textContent = `${def.title}: No data available (graph not enabled)`;
                        el.appendChild(msg);
                    }
                    return;
                }
                fromStore = true;
            }
            const ages = storeAges || buildAges(seriesData[0].values.length, player.years, player.months);
            const wrap = document.createElement('div'); wrap.className = 'tmg-chart-wrap';
            const upSet = new Set((skillpoints?.up) || []); const downSet = new Set((skillpoints?.down) || []);
            const legendCls = def.legendInline ? 'tmg-legend tmg-legend-inline' : 'tmg-legend';
            let legendH = `<div class="${legendCls}">`;
            seriesData.forEach((s, i) => { let arr = ''; if (upSet.has(s.key)) arr = '<span class="tmg-skill-arrow" style="color:#4caf50">▲</span>'; else if (downSet.has(s.key)) arr = '<span class="tmg-skill-arrow" style="color:#f44336">▼</span>'; legendH += `<label class="tmg-legend-item"><input type="checkbox" data-idx="${i}" checked style="background:${s.color}"><span class="tmg-legend-dot" style="color:${s.color}">●</span>${s.label}${arr}</label>`; });
            legendH += '</div>';
            let toggleH = def.showToggle ? '<div class="tmg-legend-toggle"><button class="tmg-btn" data-action="all">All</button><button class="tmg-btn" data-action="none">None</button></div>' : '';
            const computedLabel = fromStore ? ' <span style="font-size:10px;color:#5b9bff;font-weight:400">(computed)</span>' : '';
            const enableBtn = (fromStore && isOwnPlayer && def.enableKey)
                ? `<button class="tmg-enable-btn" data-enable-key="${def.enableKey}" style="font-size:10px;padding:3px 10px;margin-left:auto;">Enable <img src="/pics/pro_icon.png" class="pro_icon"></button>`
                : '';
            wrap.innerHTML = `<div class="tmg-chart-title" style="display:flex;align-items:center;gap:8px;">${def.title}${computedLabel}${enableBtn}</div><canvas class="tmg-canvas" style="width:100%;height:280px;"></canvas><div class="tmg-tooltip"></div>${legendH}${toggleH}`;
            el.appendChild(wrap);
            if (enableBtn) {
                wrap.querySelector('.tmg-enable-btn').addEventListener('click', () => {
                    if (typeof window.graph_enable === 'function') window.graph_enable(PLAYER_ID, def.enableKey);
                });
            }
            const canvas = wrap.querySelector('canvas'); let curInfo = null;
            const redraw = () => { curInfo = drawMultiLine(canvas, ages, seriesData, def.opts); };
            wrap.querySelectorAll('.tmg-legend input[type="checkbox"]').forEach(cb => { cb.addEventListener('change', () => { const i = parseInt(cb.dataset.idx); seriesData[i].visible = cb.checked; cb.style.background = cb.checked ? seriesData[i].color : 'rgba(255,255,255,0.08)'; redraw(); }); });
            if (def.showToggle) { wrap.querySelectorAll('.tmg-btn').forEach(btn => { btn.addEventListener('click', () => { const v = btn.dataset.action === 'all'; seriesData.forEach(s => s.visible = v); wrap.querySelectorAll('.tmg-legend input[type="checkbox"]').forEach((cb, i) => { cb.checked = v; cb.style.background = v ? seriesData[i].color : 'rgba(255,255,255,0.08)'; }); redraw(); }); }); }
            attachMultiTooltip(wrap, canvas, () => curInfo);
            requestAnimationFrame(() => redraw());
        };

        /* Enable button descriptions */
        const ENABLE_INFO = {
            skill_index: { title: 'Skill Index', desc: 'Monitor your player\'s ASI increase each training.', enableKey: 'skill_index' },
            recommendation: { title: 'Recommendation', desc: 'See when your player gained new recommendation stars.', enableKey: 'recommendation' },
            skills: { title: 'Skills', desc: 'Monitor when a player gained a point in a certain skill.', enableKey: 'skills' },
            peaks: { title: 'Peaks', desc: 'See what % of weekly training went into each peak area.', enableKey: 'peaks' }
        };

        const hasGraphData = (graphData, key) => {
            if (key === 'skills') return getSkillMeta().some(m => graphData[m.key] && graphData[m.key].length > 0);
            if (key === 'peaks') return graphData.peaks && PEAK_META.some(m => graphData.peaks[m.key] && graphData.peaks[m.key].length > 0);
            return graphData[key] && graphData[key].length > 0;
        };

        /* R5 chart — reads R5 values from our v3 store (not from TM endpoint) */
        const buildR5Chart = (el, player) => {
            try {
                const store = PlayerDB.get(PLAYER_ID);
                if (!store || store._v < 3 || !store.records) return;
                const keys = Object.keys(store.records).sort((a, b) => {
                    const [ay, am] = a.split('.').map(Number);
                    const [by, bm] = b.split('.').map(Number);
                    return (ay * 12 + am) - (by * 12 + bm);
                });
                const ages = [], values = [];
                keys.forEach(k => {
                    const rec = store.records[k];
                    if (rec.R5 == null) return;
                    const [y, m] = k.split('.').map(Number);
                    ages.push(y + m / 12);
                    values.push(rec.R5);
                });
                if (values.length < 2) return;

                const rawMin = Math.min(...values), rawMax = Math.max(...values);
                const yMin = rawMin < 30 ? Math.floor(rawMin) : 30;
                const yMax = rawMax > 120 ? Math.ceil(rawMax) : 120;
                const opts = {
                    lineColor: '#5b9bff', fillColor: 'rgba(91,155,255,0.06)',
                    yMinOverride: yMin, yMaxOverride: yMax,
                    formatY: v => v % 1 === 0 ? v.toFixed(1) : v.toFixed(2)
                };

                const wrap = document.createElement('div'); wrap.className = 'tmg-chart-wrap';
                wrap.innerHTML = `<div class="tmg-chart-title" style="display:flex;align-items:center;justify-content:space-between">
                    <span>R5 <span style="font-size:10px;color:#5b9bff;font-weight:400">(computed)</span></span>
                    <button class="tmg-export-btn" title="Export to Excel">⬇ Excel</button>
                </div><canvas class="tmg-canvas" style="width:100%;height:260px;"></canvas><div class="tmg-tooltip"></div>`;
                el.appendChild(wrap);
                wrap.querySelector('.tmg-export-btn').addEventListener('click', () => {
                    const row = values.map(v => v.toFixed(2).replace('.', ',')).join(';');
                    const csv = 'sep=;\r\n' + row + '\r\n';
                    const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
                    const url = URL.createObjectURL(blob);
                    const a = document.createElement('a');
                    a.href = url; a.download = `R5_player_${PLAYER_ID}.csv`;
                    document.body.appendChild(a); a.click();
                    setTimeout(() => { document.body.removeChild(a); URL.revokeObjectURL(url); }, 500);
                });
                const canvas = wrap.querySelector('canvas');
                requestAnimationFrame(() => { const info = drawChart(canvas, ages, values, opts); attachTooltip(wrap, canvas, info); });
            } catch (e) { }
        };


        const buildEnableCard = (container, key) => {
            const info = ENABLE_INFO[key];
            if (!info) return;
            const card = document.createElement('div');
            card.className = 'tmg-enable-card';
            card.innerHTML = `<div><div class="tmg-enable-title">${info.title}</div><div class="tmg-enable-desc">${info.desc}</div></div><button class="tmg-enable-btn" data-enable-key="${info.enableKey}">Enable <img src="/pics/pro_icon.png" class="pro_icon"></button>`;
            card.querySelector('.tmg-enable-btn').addEventListener('click', () => {
                if (typeof window.graph_enable === 'function') window.graph_enable(PLAYER_ID, info.enableKey);
            });
            container.appendChild(card);
        };

        const render = (container, data) => {
            containerRef = container;
            lastData = data;
            container.innerHTML = '';
            const graphData = data.graphs;
            const player = data.player;
            const skillpoints = data.skillpoints;
            console.log('[Graphs] Rendering with data:', { graphData, player, skillpoints });
            if (!graphData || !player) { container.innerHTML = '<div style="text-align:center;padding:40px;color:#5a7a48;font-style:italic">No graph data available</div>'; return; }

            /* Determine if this is the user's own player (for enable buttons) */
            const clubAnchor = document.querySelector('a[club_link]');
            const clubHrefRaw = clubAnchor ? (clubAnchor.getAttribute('href') || '') : '';
            const clubLinkAttr = clubAnchor ? clubAnchor.getAttribute('club_link') : null;
            const clubIdMatch = clubHrefRaw.match(/\/club\/(\d+)/i) || clubHrefRaw.match(/club_link[=\/]?(\d+)/i);
            const playerClubId = clubIdMatch ? clubIdMatch[1] : (clubLinkAttr || '');
            const isOwnPlayer = getOwnClubIds().includes(String(playerClubId));

            /* TI chart first */
            buildSingleChart(container, CHART_DEFS[0], graphData, player);

            /* R5 chart — built entirely from our v3 store */
            buildR5Chart(container, player);

            /* Remaining charts (ASI, REC) */
            for (let i = 1; i < CHART_DEFS.length; i++) buildSingleChart(container, CHART_DEFS[i], graphData, player);

            MULTI_DEFS.forEach(def => buildMultiChart(container, def, graphData, player, skillpoints, isOwnPlayer));
        };

        const reRender = () => { if (containerRef && lastData) render(containerRef, lastData); };

        return { render, reRender };
    })();

    /* ═══════════════════════════════════════════════════════════
       MAIN UI — Tab bar + panels + data fetching
       ═══════════════════════════════════════════════════════════ */
    const TABS = [
        { key: 'history', label: 'History', mod: HistoryMod },
        { key: 'scout', label: 'Scout', mod: ScoutMod },
        { key: 'training', label: 'Training', mod: TrainingMod },
        { key: 'graphs', label: 'Graphs', mod: GraphsMod }
    ];

    const switchTab = (key) => {
        activeMainTab = key;
        /* highlight active button */
        document.querySelectorAll('.tmpe-main-tab').forEach(b =>
            b.classList.toggle('active', b.dataset.tab === key));
        /* show / hide panels */
        document.querySelectorAll('.tmpe-panel').forEach(p =>
            p.style.display = p.dataset.tab === key ? '' : 'none');

        if (dataLoaded[key]) return; /* already rendered, keep DOM intact */

        const panel = document.querySelector(`.tmpe-panel[data-tab="${key}"]`);
        if (!panel) return;

        panel.innerHTML = '<div class="tmpe-loading"><div class="tmpe-spinner"></div><div class="tmpe-loading-text">Loading…</div></div>';

        $.post('/ajax/players_get_info.ajax.php', {
            player_id: PLAYER_ID,
            type: key,
            show_non_pro_graphs: true
        }).done(res => {
            try {
                const data = typeof res === 'object' ? res : JSON.parse(res);
                dataLoaded[key] = true;
                const tab = TABS.find(t => t.key === key);
                if (tab) tab.mod.render(panel, data);
            } catch (e) {
                panel.innerHTML = '<div class="tmpe-loading"><div style="font-size:20px">⚠</div><div class="tmpe-loading-text" style="color:#f87171">Failed to load data</div></div>';
            }
        }).fail(() => {
            panel.innerHTML = '<div class="tmpe-loading"><div style="font-size:20px">⚠</div><div class="tmpe-loading-text" style="color:#f87171">Failed to load data</div></div>';
        });
    };

    let initRetries = 0;
    const initUI = () => {
        const tabsContent = document.querySelector('.tabs_content');
        if (!tabsContent) {
            if (initRetries++ < 50) setTimeout(initUI, 200);
            return;
        }

        injectCSS();

        /* Build container */
        const container = document.createElement('div');
        container.id = 'tmpe-container';

        /* Tab bar */
        const bar = document.createElement('div');
        bar.className = 'tmpe-tabs-bar';
        const TAB_ICONS = { history: '📋', scout: '🔍', training: '⚙', graphs: '📊' };
        TABS.forEach(t => {
            const btn = document.createElement('button');
            btn.className = 'tmpe-main-tab';
            btn.dataset.tab = t.key;
            btn.innerHTML = `<span class="tmpe-icon">${TAB_ICONS[t.key] || ''}</span>${t.label}`;
            btn.addEventListener('click', () => switchTab(t.key));
            bar.appendChild(btn);
        });
        container.appendChild(bar);

        /* Panels */
        const panels = document.createElement('div');
        panels.className = 'tmpe-panels';
        TABS.forEach(t => {
            const p = document.createElement('div');
            p.className = 'tmpe-panel';
            p.dataset.tab = t.key;
            p.style.display = 'none';
            panels.appendChild(p);
        });
        container.appendChild(panels);

        /* Insert before native .tabs_content */
        tabsContent.parentNode.insertBefore(container, tabsContent);

        /* Load default tab */
        switchTab('history');
    };

    /* ═══════════════════════════════════════════════════════════
       WINDOW RESIZE — redraw graphs
       ═══════════════════════════════════════════════════════════ */
    let resizeTimer = null;
    window.addEventListener('resize', () => {
        clearTimeout(resizeTimer);
        resizeTimer = setTimeout(() => GraphsMod.reRender(), 300);
    });

    /* ═══════════════════════════════════════════════════════════
       INIT — wait for IndexedDB, then start everything
       ═══════════════════════════════════════════════════════════ */
    PlayerDB.init().then(() => {
        scanAndMigrateR6();

        if (IS_SQUAD_PAGE) {
            /* ── Squad page: ensure reserves visible, parse, process ── */
            const runSquadSync = async () => {
                await ensureAllPlayersVisible();
                const parsed = parseSquadPage();
                if (parsed && parsed.length) {
                    await processSquadPage(parsed);
                } else {
                    console.warn('[Squad] No players parsed from table');
                }

                /* ── Watch for hash changes (toggle clicks) to re-process newly visible players ── */
                const processedPids = new Set((parsed || []).map(p => p.pid));
                let hashProcessing = false;
                window.addEventListener('hashchange', async () => {
                    if (hashProcessing) return;
                    hashProcessing = true;
                    try {
                        await new Promise(r => setTimeout(r, 600)); /* Wait for DOM update */
                        const reParsed = parseSquadPage();
                        if (!reParsed) { hashProcessing = false; return; }
                        const newPlayers = reParsed.filter(p => !processedPids.has(p.pid));
                        if (newPlayers.length > 0) {
                            console.log(`%c[Squad] Detected ${newPlayers.length} new players after toggle`, 'font-weight:bold;color:#38bdf8');
                            newPlayers.forEach(p => processedPids.add(p.pid));
                            await processSquadPage(newPlayers);
                        }
                    } catch (e) { console.error('[Squad] Re-process error:', e); }
                    hashProcessing = false;
                });
            };
            runSquadSync().catch(e => console.error('[Squad] Squad sync error:', e));
            return; /* Don't run player-specific code on squad page */
        }

        fetchTooltip();
        initUI();
    }).catch(e => {
        console.warn('[DB] IndexedDB init failed, falling back:', e);
        if (IS_SQUAD_PAGE) {
            const parsed = parseSquadPage();
            if (parsed && parsed.length) processSquadPage(parsed);
            return;
        }
        fetchTooltip();
        initUI();
    });

    /* ═══════════════════════════════════════════════════════════
       R5 / TI CALCULATION
       ═══════════════════════════════════════════════════════════ */
    const POS_MULTIPLIERS = [0.3, 0.3, 0.9, 0.6, 1.5, 0.9, 0.9, 0.6, 0.3];
    const COLOR_LEVELS = [
        { color: '#ff4c4c' }, { color: '#ff8c00' }, { color: '#ffd700' },
        { color: '#90ee90' }, { color: '#00cfcf' }, { color: '#5b9bff' }, { color: '#cc88ff' }
    ];
    const R5_THRESHOLDS = [110, 100, 90, 80, 70, 60, 0];
    const TI_THRESHOLDS = [12, 9, 6, 4, 2, 1, -Infinity];
    const REC_THRESHOLDS = [5.5, 5, 4, 3, 2, 1, 0];
    const RTN_THRESHOLDS = [90, 60, 40, 30, 20, 10, 0];

    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],
        [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],
        [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],
        [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],
        [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],
        [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],
        [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],
        [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],
        [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],
        [0.45462811, 0.30278232, 0.45462811, 0.90925623, 0.45462811, 0.90925623, 0.45462811, 0.45462811, 0.30278232, 0.15139116, 0.15139116]
    ];
    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],
        [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],
        [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],
        [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],
        [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],
        [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],
        [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],
        [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],
        [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],
        [0.07466384, 0.07466384, 0.07466384, 0.14932769, 0.10452938, 0.14932769, 0.10452938, 0.10344411, 0.07512610, 0.04492581, 0.04479831]
    ];

    const WAGE_RATE = 15.8079;
    const TRAINING1 = new Date('2023-01-16T23:00:00Z');
    const SEASON_DAYS = 84;
    const 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 currentSession = getCurrentSession();

    const posGroupColor = posIdx => {
        if (posIdx === 9) return '#4ade80';
        if (posIdx <= 1) return '#60a5fa';
        if (posIdx <= 7) return '#fbbf24';
        return '#f87171';
    };

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

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

    const getPositionIndex = pos => {
        switch ((pos || '').toLowerCase()) {
            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;
            case 'fc': case 'fcl': case 'fcr': case 'f': return 8;
            default: return 0;
        }
    };

    const calculateRemainders = (posIdx, skills, asi) => {
        const weight = posIdx === 9 ? 48717927500 : 263533760000;
        const skillSum = skills.reduce((sum, s) => sum + parseInt(s), 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 = fix2((rec + remainder * remainderW1 / not20 - 2) / 3);
        return { remainder, remainderW2, not20, ratingR, rec };
    };

    /* Float-aware version: uses parseFloat for skillSum so remainder ≈ 0 with decimal skills */
    const calculateRemaindersF = (posIdx, skills, asi) => {
        const weight = posIdx === 9 ? 48717927500 : 263533760000;
        const skillSum = skills.reduce((sum, s) => sum + parseFloat(s), 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 (!not20) { not20 = posIdx === 9 ? 11 : 14; remainderW1 = 1; remainderW2 = 5; }
        rec = fix2((rec + remainder * remainderW1 / not20 - 2) / 3);
        return { remainder, remainderW2, not20, ratingR, rec };
    };

    const calculateR5 = (posIdx, skills, asi, rou) => {
        const r = calculateRemainders(posIdx, skills, asi);
        const routineBonus = (3 / 100) * (100 - 100 * Math.pow(Math.E, -rou * 0.035));
        let rating = Number(fix2(r.ratingR + (r.remainder * r.remainderW2 / r.not20) + routineBonus * 5));
        const rou2 = routineBonus;
        const goldstar = skills.filter(s => s == 20).length;
        const skillsB = skills.map(s => s == 20 ? 20 : s * 1 + r.remainder / (skills.length - goldstar));
        const sr = skillsB.map((s, i) => i === 1 ? s : s + rou2);
        if (skills.length !== 11) {
            const { pow, E } = Math;
            const hb = sr[10] > 12 ? 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 fk = fix2(pow(E, (sr[13] + sr[12] + sr[9] * 0.5) ** 2 * 0.002) / 327.92526);
            const ck = fix2(pow(E, (sr[13] + sr[8] + sr[9] * 0.5) ** 2 * 0.002) / 983.65770);
            const pk = fix2(pow(E, (sr[13] + sr[11] + sr[9] * 0.5) ** 2 * 0.002) / 1967.31409);
            const ds = 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 os = 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 m = POS_MULTIPLIERS[posIdx];
            return fix2(rating + hb * 1 + fk * 1 + ck * 1 + pk * 1 + fix2(ds / 6 / 22.9 ** 2) * m + fix2(os / 6 / 22.9 ** 2) * m);
        }
        return fix2(rating);
    };

    /* Float-aware R5: uses calculateRemaindersF so remainder ≈ 0 with decimal skills */
    const calculateR5F = (posIdx, skills, asi, rou) => {
        const r = calculateRemaindersF(posIdx, skills, asi);
        const routineBonus = (3 / 100) * (100 - 100 * Math.pow(Math.E, -rou * 0.035));
        let rating = Number(fix2(r.ratingR + (r.remainder * r.remainderW2 / r.not20) + routineBonus * 5));
        const rou2 = routineBonus;
        const goldstar = skills.filter(s => s == 20).length;
        const skillsB = skills.map(s => s == 20 ? 20 : s * 1 + r.remainder / (skills.length - goldstar));
        const sr = skillsB.map((s, i) => i === 1 ? s : s + rou2);
        if (skills.length !== 11) {
            const { pow, E } = Math;
            const hb = sr[10] > 12 ? 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 fk = fix2(pow(E, (sr[13] + sr[12] + sr[9] * 0.5) ** 2 * 0.002) / 327.92526);
            const ck = fix2(pow(E, (sr[13] + sr[8] + sr[9] * 0.5) ** 2 * 0.002) / 983.65770);
            const pk = fix2(pow(E, (sr[13] + sr[11] + sr[9] * 0.5) ** 2 * 0.002) / 1967.31409);
            const ds = 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 os = 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 m = POS_MULTIPLIERS[posIdx];
            return fix2(rating + hb * 1 + fk * 1 + ck * 1 + pk * 1 + fix2(ds / 6 / 22.9 ** 2) * m + fix2(os / 6 / 22.9 ** 2) * m);
        }
        return fix2(rating);
    };

    const calculateTI = (asi, wage, isGK) => {
        if (!asi || !wage || wage <= 30000) return null;
        const w = isGK ? 48717927500 : 263533760000;
        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);
    };

    /* ═══════════════════════════════════════════════════════════
       PLAYER CARD — replace native info_table
       ═══════════════════════════════════════════════════════════ */
    const buildPlayerCard = () => {
        const infoTable = document.querySelector('table.info_table.zebra');
        if (!infoTable || !tooltipPlayer) return;

        /* Extract data from DOM (before any DOM changes) */
        const imgEl = infoTable.querySelector('img[src*="player_pic"]');
        const photoSrc = imgEl ? imgEl.getAttribute('src') : '/pics/player_pic2.php';
        const infoWrapper = infoTable.closest('div.std') || infoTable.parentElement;

        const rowData = {};
        infoTable.querySelectorAll('tr').forEach(tr => {
            const th = tr.querySelector('th');
            const td = tr.querySelector('td');
            if (th && td) rowData[th.textContent.trim()] = td;
        });

        const clubTd = rowData['Club'];
        const clubLink = clubTd ? clubTd.querySelector('a[club_link]') : null;
        const clubName = clubLink ? clubLink.textContent.trim() : '-';
        const clubHref = clubLink ? clubLink.getAttribute('href') : '';
        const clubFlag = clubTd ? (clubTd.querySelector('.country_link') || { outerHTML: '' }).outerHTML : '';

        const ageTxt = rowData['Age'] ? rowData['Age'].textContent.trim() : '-';
        const hwRaw = rowData['Height / Weight'] ? rowData['Height / Weight'].textContent.trim() : '';
        const hwParts = hwRaw.split('/').map(s => s.trim());
        const heightTxt = hwParts[0] || '-';
        const weightTxt = hwParts[1] || '-';

        const wageTd = rowData['Wage'];
        const wageTxt = wageTd ? wageTd.textContent.trim().replace(/[^0-9]/g, '') : '0';
        const wageDisplay = wageTd ? wageTd.textContent.trim() : '-';
        const wageNum = parseInt(wageTxt) || 0;

        const asiTd = rowData['Skill Index'];
        const asiTxt = asiTd ? asiTd.textContent.trim().replace(/[^0-9]/g, '') : '0';
        const asiNum = parseInt(asiTxt) || 0;
        const asiDisplay = asiTd ? asiTd.textContent.trim() : '-';
        if (asiNum > 0) playerASI = asiNum;

        const routineTd = rowData['Routine'];
        const routineVal = routineTd ? parseFloat(routineTd.textContent.trim()) || 0 : 0;
        playerRoutine = routineVal;

        const statusTd = rowData['Status'];
        const statusHtml = statusTd ? statusTd.innerHTML : '';

        /* Player name and position from page header */
        const headerEl = document.querySelector('.box_sub_header .large strong');
        const playerName = headerEl ? headerEl.textContent.trim() : 'Player';
        const posEl = document.querySelector('.favposition.long');
        const posText = posEl ? posEl.textContent.trim() : '';
        const flagEl = document.querySelector('.box_sub_header .country_link');
        const flagHtml = flagEl ? flagEl.outerHTML : '';
        const hasNT = !!document.querySelector('.nt_icon');

        /* Parse positions (comma-separated from tooltip) */
        const positions = playerPosition ? playerPosition.split(',').map(s => s.trim()) : [];
        positions.sort((a, b) => getPositionIndex(a) - getPositionIndex(b));
        const posList = positions.map(s => ({ name: s.toUpperCase(), idx: getPositionIndex(s) }));
        const posIdx = posList.length > 0 ? posList[0].idx : 0;

        /* Recommendation stars from DOM */
        const recTd = rowData['Recommendation'];
        let recStarsHtml = '';
        if (recTd) {
            const halfStars = (recTd.innerHTML.match(/half_star\.png/g) || []).length;
            const darkStars = (recTd.innerHTML.match(/dark_star\.png/g) || []).length;
            const allStarMatches = (recTd.innerHTML.match(/star\.png/g) || []).length;
            const fullStars = allStarMatches - halfStars - darkStars;
            for (let i = 0; i < fullStars; i++) recStarsHtml += '<span class="tmpc-star-full">★</span>';
            if (halfStars) recStarsHtml += '<span class="tmpc-star-half">★</span>';
            const empty = 5 - fullStars - (halfStars ? 1 : 0);
            for (let i = 0; i < empty; i++) recStarsHtml += '<span class="tmpc-star-empty">★</span>';
        }

        /* R5 / REC / TI calculation — per position */
        const posRatings = [];
        const allPosRatings = [];
        const ALL_OUTFIELD_POS = [
            { name: 'DC', idx: 0 }, { name: 'DL/DR', idx: 1 },
            { name: 'DMC', idx: 2 }, { name: 'DML/DMR', idx: 3 },
            { name: 'MC', idx: 4 }, { name: 'ML/MR', idx: 5 },
            { name: 'OMC', idx: 6 }, { name: 'OML/OMR', idx: 7 },
            { name: 'FC', idx: 8 }
        ];
        let tiVal = null;
        if (tooltipSkills && posList.length > 0) {
            const sv = (name) => {
                const sk = tooltipSkills.find(s => s.name === name);
                if (!sk) return 0;
                const v = sk.value;
                if (typeof v === 'string' && v.includes('star')) return v.includes('silver') ? 19 : 20;
                return parseInt(v) || 0;
            };
            let skills;
            if (posIdx === 9) {
                skills = [sv('Strength'), sv('Pace'), sv('Jumping'), sv('Stamina'), sv('One on ones'), sv('Reflexes'), sv('Aerial Ability'), sv('Communication'), sv('Kicking'), sv('Throwing'), sv('Handling')];
            } else {
                skills = [sv('Strength'), sv('Stamina'), sv('Pace'), sv('Marking'), sv('Tackling'), sv('Workrate'), sv('Positioning'), sv('Passing'), sv('Crossing'), sv('Technique'), sv('Heading'), sv('Finishing'), sv('Longshots'), sv('Set Pieces')];
            }
            if (asiNum > 0 && skills.some(s => s > 0)) {
                /* Try to load decimal skills from v3 store for more precise R5/REC */
                let decSkills = skills; // fallback: integer skills
                let decRoutine = routineVal;
                try {
                    const v3Store = PlayerDB.get(PLAYER_ID);
                    if (v3Store && v3Store._v >= 3 && v3Store.records) {
                        const recKeys = Object.keys(v3Store.records).sort((a, b) => {
                            const [ay, am] = a.split('.').map(Number);
                            const [by, bm] = b.split('.').map(Number);
                            return (ay * 12 + am) - (by * 12 + bm);
                        });
                        if (recKeys.length > 0) {
                            const lastRec = v3Store.records[recKeys[recKeys.length - 1]];
                            if (lastRec && lastRec.skills && lastRec.skills.length === skills.length) {
                                decSkills = lastRec.skills;
                                if (lastRec.routine != null) decRoutine = lastRec.routine;
                                console.log('[Card] Using v3 decimal skills for R5/REC');
                            }
                        }
                    }
                } catch (e) { }

                for (const pp of posList) {
                    const r5 = Number(calculateR5F(pp.idx, decSkills, asiNum, decRoutine));
                    const rec = Number(calculateRemaindersF(pp.idx, decSkills, asiNum).rec);
                    posRatings.push({ name: pp.name, idx: pp.idx, r5, rec });
                }
                /* Calculate R5/REC for ALL outfield positions */
                if (posIdx !== 9) {
                    const playerIdxSet = new Set(posList.map(p => p.idx));
                    for (const ap of ALL_OUTFIELD_POS) {
                        const r5 = Number(calculateR5F(ap.idx, decSkills, asiNum, decRoutine));
                        const rec = Number(calculateRemaindersF(ap.idx, decSkills, asiNum).rec);
                        allPosRatings.push({ name: ap.name, idx: ap.idx, r5, rec, isPlayerPos: playerIdxSet.has(ap.idx) });
                    }
                }
            }
            if (asiNum > 0 && wageNum > 0) {
                const tiRaw = calculateTI(asiNum, wageNum, posIdx === 9);
                tiVal = tiRaw !== null && currentSession > 0
                    ? Number((tiRaw / currentSession).toFixed(1)) : null;
                if (tiVal !== null) playerTI = tiVal;
            }
        }

        /* Build HTML */
        let html = `<div class="tmpc-card">`;
        html += `<div class="tmpc-header">`;
        html += `<img class="tmpc-photo" src="${photoSrc}">`;
        html += `<div class="tmpc-info">`;
        html += `<div class="tmpc-top-grid">`;
        const ntBadge = hasNT ? `<span class="tmpc-nt">🏆 NT</span>` : '';
        html += `<div class="tmpc-name">${playerName} ${flagHtml}</div>`;
        html += `<span class="tmpc-badge-chip"><span class="tmpc-badge-lbl">ASI</span><span style="color:${asiNum > 0 ? '#e8f5d8' : '#5a7a48'}">${asiDisplay}</span></span>`;
        const posChips = posList.map(pp => {
            const clr = posGroupColor(pp.idx);
            return `<span class="tmpc-pos" style="background:${clr}22;border:1px solid ${clr}44;color:${clr}">${pp.name}</span>`;
        }).join('');
        html += `<div class="tmpc-pos-row">${posChips || posText}${ntBadge}</div>`;
        html += `<span class="tmpc-badge-chip"><span class="tmpc-badge-lbl">TI</span><span style="color:${tiVal !== null ? getColor(tiVal, TI_THRESHOLDS) : '#5a7a48'}">${tiVal !== null ? tiVal.toFixed(1) : '—'}</span></span>`;
        html += `</div>`;
        html += `<div class="tmpc-details">`;
        html += `<div class="tmpc-detail"><span class="tmpc-lbl">Club</span><span class="tmpc-val"><a href="${clubHref}" style="color:#80e048;text-decoration:none;font-weight:600">${clubName}</a> ${clubFlag}</span></div>`;
        html += `<div class="tmpc-detail"><span class="tmpc-lbl">Age</span><span class="tmpc-val">${ageTxt}</span></div>`;
        html += `<div class="tmpc-detail"><span class="tmpc-lbl">Height</span><span class="tmpc-val">${heightTxt}</span></div>`;
        html += `<div class="tmpc-detail"><span class="tmpc-lbl">Weight</span><span class="tmpc-val">${weightTxt}</span></div>`;
        html += `<div class="tmpc-detail"><span class="tmpc-lbl">Wage</span><span class="tmpc-val" style="color:#fbbf24">${wageDisplay}</span></div>`;
        html += `<div class="tmpc-detail"><span class="tmpc-lbl">Status</span><span class="tmpc-val">${statusHtml}</span></div>`;
        html += `<div class="tmpc-detail"><span class="tmpc-lbl">REC</span><span class="tmpc-rec-stars">${recStarsHtml}</span></div>`;
        html += `<div class="tmpc-detail"><span class="tmpc-lbl">Routine</span><span class="tmpc-val" style="color:${getColor(routineVal, RTN_THRESHOLDS)}">${routineVal.toFixed(1)}</span></div>`;
        html += `</div>`; /* details */
        html += `</div>`; /* info */
        html += `</div>`; /* header */

        /* Position ratings — R5 & REC per position */
        if (posRatings.length > 0) {
            html += `<div class="tmpc-pos-ratings">`;
            for (const pr of posRatings) {
                const clr = posGroupColor(pr.idx);
                html += `<div class="tmpc-rating-row">`;
                html += `<div class="tmpc-pos-bar" style="background:${clr}"></div>`;
                html += `<span class="tmpc-pos-name" style="color:${clr}">${pr.name}</span>`;
                html += `<span class="tmpc-pos-stat"><span class="tmpc-pos-stat-lbl">R5</span><span class="tmpc-pos-stat-val" style="color:${getColor(pr.r5, R5_THRESHOLDS)}">${pr.r5.toFixed(2)}</span></span>`;
                html += `<span class="tmpc-pos-stat"><span class="tmpc-pos-stat-lbl">REC</span><span class="tmpc-pos-stat-val" style="color:${getColor(pr.rec, REC_THRESHOLDS)}">${pr.rec.toFixed(2)}</span></span>`;
                html += `</div>`;
            }
            /* Expand chevron for all positions (non-GK only) */
            if (allPosRatings.length > 0) {
                html += `<div class="tmpc-expand-toggle" onclick="this.classList.toggle('tmpc-expanded');this.nextElementSibling.classList.toggle('tmpc-expanded')">`;
                html += `<span>All Positions</span><span class="tmpc-expand-chevron">▼</span>`;
                html += `</div>`;
                html += `<div class="tmpc-all-positions">`;
                for (const ap of allPosRatings) {
                    const clr = posGroupColor(ap.idx);
                    const playerCls = ap.isPlayerPos ? ' tmpc-is-player-pos' : '';
                    html += `<div class="tmpc-rating-row${playerCls}">`;
                    html += `<div class="tmpc-pos-bar" style="background:${clr}"></div>`;
                    html += `<span class="tmpc-pos-name" style="color:${clr}">${ap.name}</span>`;
                    html += `<span class="tmpc-pos-stat"><span class="tmpc-pos-stat-lbl">R5</span><span class="tmpc-pos-stat-val" style="color:${getColor(ap.r5, R5_THRESHOLDS)}">${ap.r5.toFixed(2)}</span></span>`;
                    html += `<span class="tmpc-pos-stat"><span class="tmpc-pos-stat-lbl">REC</span><span class="tmpc-pos-stat-val" style="color:${getColor(ap.rec, REC_THRESHOLDS)}">${ap.rec.toFixed(2)}</span></span>`;
                    html += `</div>`;
                }
                html += `</div>`;
            }
            html += `</div>`;
        }

        html += `</div>`; /* card */

        /* ── Clean column2_a: strip TM box chrome ── */
        const col = document.querySelector('.column2_a');
        if (!col) return;
        const box = col.querySelector(':scope > .box');
        const boxBody = box ? box.querySelector(':scope > .box_body') : null;
        if (box && boxBody) {
            [...boxBody.children].forEach(el => {
                if (!el.classList.contains('box_shadow')) col.appendChild(el);
            });
            box.remove();
        }
        /* Remove h3 headers (Skills, Player Info) */
        col.querySelectorAll(':scope > h3').forEach(h => h.remove());
        /* Remove box_sub_header */
        const subHeader = document.querySelector('.box_sub_header.align_center');
        if (subHeader) subHeader.remove();

        /* Replace info_table wrapper with our card */
        const cardEl = document.createElement('div');
        cardEl.innerHTML = html;
        if (infoWrapper && infoWrapper.parentNode === col) {
            col.replaceChild(cardEl.firstChild, infoWrapper);
        } else {
            col.prepend(cardEl.firstChild);
        }
    };

    /* ═══════════════════════════════════════════════════════════
       SKILLS GRID — replace native skill table
       ═══════════════════════════════════════════════════════════ */
    const skillColor = v => {
        if (v >= 20) return 'gold';
        if (v >= 19) return 'silver';
        if (v >= 16) return '#66dd44';
        if (v >= 12) return '#cccc00';
        if (v >= 8) return '#ee9900';
        return '#ee6633';
    };

    const buildSkillsGrid = () => {
        const skillTable = document.querySelector('table.skill_table.zebra');
        if (!skillTable) return;

        /* Parse skills from native table */
        const SKILL_ORDER = [
            ['Strength', 'Passing'],
            ['Stamina', 'Crossing'],
            ['Pace', 'Technique'],
            ['Marking', 'Heading'],
            ['Tackling', 'Finishing'],
            ['Workrate', 'Longshots'],
            ['Positioning', 'Set Pieces']
        ];

        const GK_SKILL_ORDER = [
            ['Strength', 'Handling'],
            ['Stamina', 'One on ones'],
            ['Pace', 'Reflexes'],
            [null, 'Aerial Ability'],
            [null, 'Jumping'],
            [null, 'Communication'],
            [null, 'Kicking'],
            [null, 'Throwing']
        ];

        const parseSkillVal = (td) => {
            if (!td) return 0;
            const img = td.querySelector('img');
            if (img) {
                const alt = img.getAttribute('alt');
                if (alt) return parseInt(alt) || 0;
                const src = img.getAttribute('src') || '';
                if (src.includes('star_silver')) return 19;
                if (src.includes('star.png')) return 20;
            }
            const txt = td.textContent.trim();
            return parseInt(txt) || 0;
        };

        /* Build skill map from existing rows */
        const skillMap = {};
        const rows = skillTable.querySelectorAll('tr');
        rows.forEach(row => {
            const ths = row.querySelectorAll('th');
            const tds = row.querySelectorAll('td');
            ths.forEach((th, i) => {
                const name = th.textContent.trim();
                if (name && tds[i]) {
                    skillMap[name] = parseSkillVal(tds[i]);
                }
            });
        });

        /* Detect GK from DOM skill names (fallback for async tooltip timing) */
        const isGK = isGoalkeeper || 'Handling' in skillMap;

        /* Parse hidden skills from DOM */
        const hiddenTable = document.querySelector('#hidden_skill_table');
        const hiddenSkills = [];
        let hasHiddenValues = false;
        if (hiddenTable) {
            const hRows = hiddenTable.querySelectorAll('tr');
            hRows.forEach(row => {
                const ths = row.querySelectorAll('th');
                const tds = row.querySelectorAll('td');
                ths.forEach((th, i) => {
                    const name = th.textContent.trim();
                    const td = tds[i];
                    let val = '';
                    let numVal = 0;
                    if (td) {
                        /* Check tooltip for numeric value */
                        const tip = td.getAttribute('tooltip') || '';
                        const tipMatch = tip.match(/(\d+)\/20/);
                        if (tipMatch) numVal = parseInt(tipMatch[1]) || 0;
                        val = td.textContent.trim();
                    }
                    if (name) {
                        hiddenSkills.push({ name, val, numVal });
                        if (val) hasHiddenValues = true;
                    }
                });
            });
        }

        /* Compute decimal skill values — prefer stored localStorage record for current age */
        const NAMES_OUT_R5 = ['Strength', 'Stamina', 'Pace', 'Marking', 'Tackling', 'Workrate', 'Positioning', 'Passing', 'Crossing', 'Technique', 'Heading', 'Finishing', 'Longshots', 'Set Pieces'];
        const NAMES_GK_R5 = ['Strength', 'Stamina', 'Pace', 'Handling', 'One on ones', 'Reflexes', 'Aerial Ability', 'Jumping', 'Communication', 'Kicking', 'Throwing'];
        const skillNames = isGK ? NAMES_GK_R5 : NAMES_OUT_R5;
        const decimalSkillMap = { ...skillMap };
        let usedStorage = false;
        if (playerAge !== null && playerMonths !== null) {
            const ageKey = `${parseInt(playerAge)}.${playerMonths}`;
            try {
                const store = PlayerDB.get(PLAYER_ID);
                if (store && store._v >= 1 && store.records && store.records[ageKey] && Array.isArray(store.records[ageKey].skills)) {
                    const stored = store.records[ageKey].skills;
                    skillNames.forEach((name, i) => {
                        if (stored[i] !== undefined) decimalSkillMap[name] = stored[i];
                    });
                    usedStorage = true;
                }
            } catch (e) { }
        }
        if (!usedStorage && playerASI && playerASI > 0) {
            const skillsArr = skillNames.map(n => skillMap[n] || 0);
            const w = isGK ? 48717927500 : 263533760000;
            const log27 = Math.log(Math.pow(2, 7));
            const skillSum = skillsArr.reduce((a, b) => a + b, 0);
            const remainder = Math.round((Math.pow(2, Math.log(w * playerASI) / log27) - skillSum) * 10) / 10;
            const goldstar = skillsArr.filter(s => s === 20).length;
            const nonStar = skillsArr.length - goldstar;
            if (remainder > 0 && nonStar > 0) {
                const dec = remainder / nonStar;
                skillNames.forEach(name => {
                    if ((skillMap[name] || 0) !== 20) decimalSkillMap[name] = (skillMap[name] || 0) + dec;
                });
            }
        }

        /* Build display */
        const renderVal = (v) => {
            const floor = Math.floor(v);
            const frac = v - floor;
            if (floor >= 20) return `<span class="tmps-star" style="color:gold">★</span>`;
            if (floor >= 19) {
                const fracStr = frac > 0.005 ? `<span class="tmps-dec">.${Math.round(frac * 100).toString().padStart(2, '0')}</span>` : '';
                return `<span class="tmps-star" style="color:silver">★${fracStr}</span>`;
            }
            const dispVal = frac > 0.005 ? `${floor}<span class="tmps-dec">.${Math.round(frac * 100).toString().padStart(2, '0')}</span>` : floor;
            return `<span style="color:${skillColor(floor)}">${dispVal}</span>`;
        };

        let leftCol = '', rightCol = '';
        const activeOrder = isGK ? GK_SKILL_ORDER : SKILL_ORDER;
        activeOrder.forEach(([left, right]) => {
            if (left) {
                const lv = decimalSkillMap[left] || 0;
                leftCol += `<div class="tmps-row"><span class="tmps-name">${left}</span><span class="tmps-val">${renderVal(lv)}</span></div>`;
            } else {
                leftCol += `<div class="tmps-row" style="visibility:hidden"><span class="tmps-name">&nbsp;</span><span class="tmps-val">&nbsp;</span></div>`;
            }
            if (right) {
                const rv = decimalSkillMap[right] || 0;
                rightCol += `<div class="tmps-row"><span class="tmps-name">${right}</span><span class="tmps-val">${renderVal(rv)}</span></div>`;
            }
        });

        let hiddenH = '';
        if (hasHiddenValues) {
            let hLeft = '', hRight = '';
            for (let i = 0; i < hiddenSkills.length; i++) {
                const hs = hiddenSkills[i];
                const color = hs.numVal ? skillColor(hs.numVal) : '#6a9a58';
                const row = `<div class="tmps-row"><span class="tmps-name">${hs.name}</span><span class="tmps-val" style="color:${color}">${hs.val || '-'}</span></div>`;
                if (i % 2 === 0) hLeft += row; else hRight += row;
            }
            hiddenH = `<div class="tmps-divider"></div><div class="tmps-hidden"><div>${hLeft}</div><div>${hRight}</div></div>`;
        } else {
            /* Get unlock button onclick from original */
            const unlockBtn = document.querySelector('.hidden_skills_text .button');
            const onclick = unlockBtn ? unlockBtn.getAttribute('onclick') || '' : '';
            hiddenH = `<div class="tmps-divider"></div><div class="tmps-unlock"><span class="tmps-unlock-btn" onclick="${onclick}">Assess Hidden Skills <img src="/pics/pro_icon.png" class="pro_icon"></span></div>`;
        }

        const html = `<div class="tmps-wrap"><div class="tmps-grid"><div>${leftCol}</div><div>${rightCol}</div></div>${hiddenH}</div>`;

        /* Replace the native div.std that contains skill_table */
        const parentDiv = skillTable.closest('div.std');
        if (parentDiv) {
            const newDiv = document.createElement('div');
            newDiv.innerHTML = html;
            parentDiv.parentNode.replaceChild(newDiv, parentDiv);
        }
    };

    /* Wait for DOM then replace */
    let skillRetries = 0;
    const tryBuildSkills = () => {
        if (document.querySelector('table.skill_table.zebra')) {
            buildSkillsGrid();
        } else if (skillRetries++ < 30) {
            setTimeout(tryBuildSkills, 200);
        }
    };
    tryBuildSkills();

    /* ═══════════════════════════════════════════════════════════
       BEST ESTIMATE — fetch scout data, render card in column1
       ═══════════════════════════════════════════════════════════ */
    const fetchBestEstimate = () => {
        const renderCard = (data) => {
            const html = ScoutMod.getEstimateHtml(data || {});
            if (!html) return;
            const col1 = document.querySelector('.column1');
            if (!col1) return;
            const existing = col1.querySelector('#tmbe-standalone');
            if (existing) existing.remove();
            const el = document.createElement('div');
            el.id = 'tmbe-standalone';
            el.innerHTML = html;
            const nav = col1.querySelector('.tmcn-nav');
            if (nav && nav.nextSibling) {
                col1.insertBefore(el, nav.nextSibling);
            } else {
                col1.appendChild(el);
            }
        };
        $.post('/ajax/players_get_info.ajax.php', {
            player_id: PLAYER_ID, type: 'scout', show_non_pro_graphs: true
        }).done(res => {
            try {
                const data = typeof res === 'object' ? res : JSON.parse(res);
                renderCard(data);
            } catch (e) { renderCard({}); }
        }).fail(() => { renderCard({}); });
    };

    /* ═══════════════════════════════════════════════════════════
       SIDEBAR — restyle column3_a
       ═══════════════════════════════════════════════════════════ */
    const buildSidebar = () => {
        const col3 = document.querySelector('.column3_a');
        if (!col3) return;

        /* ── Extract data before destroying DOM ── */
        /* Transfer buttons */
        const transferBox = col3.querySelector('.transfer_box');
        const btnData = [];
        let transferListed = null; /* { playerId, playerName, minBid } if external player is listed */
        if (transferBox) {
            /* Check if this is an external player on the transfer list */
            const tbText = transferBox.textContent || '';
            const bidBtn = transferBox.querySelector('[onclick*="tlpop_pop_transfer_bid"]');
            if (bidBtn && tbText.includes('transferlisted')) {
                const bidMatch = bidBtn.getAttribute('onclick').match(/tlpop_pop_transfer_bid\(['"]([^'"]*)['"]\s*,\s*\d+\s*,\s*(\d+)\s*,\s*['"]([^'"]*)['"]/);
                if (bidMatch) {
                    transferListed = { minBid: bidMatch[1], playerId: bidMatch[2], playerName: bidMatch[3] };
                }
            }
            if (!transferListed) {
                transferBox.querySelectorAll('span.button').forEach(btn => {
                    const onclick = btn.getAttribute('onclick') || '';
                    const label = btn.textContent.trim();
                    const imgSrc = btn.querySelector('img');
                    let icon = '⚡', cls = 'muted';
                    if (/set_asking/i.test(onclick)) { icon = '💰'; cls = 'yellow'; }
                    else if (/reject/i.test(onclick)) { icon = '🚫'; cls = 'red'; }
                    else if (/transferlist/i.test(onclick)) { icon = '📋'; cls = 'green'; }
                    else if (/fire/i.test(onclick)) { icon = '🗑️'; cls = 'red'; }
                    btnData.push({ onclick, label, icon, cls });
                });
            }
        }

        /* Other options buttons */
        const otherBtns = [];
        const otherSection = col3.querySelectorAll('.box_body .std.align_center');
        const otherDiv = otherSection.length > 1 ? otherSection[1] : (otherSection[0] && !otherSection[0].classList.contains('transfer_box') ? otherSection[0] : null);
        /* Note text */
        let noteText = '';
        const notePar = col3.querySelector('p.dark.rounded');
        if (notePar) {
            noteText = notePar.innerHTML.replace(/<span[^>]*>Note:\s*<\/span>/i, '').replace(/<br\s*\/?>/gi, ' ').trim();
        }
        if (otherDiv) {
            otherDiv.querySelectorAll('span.button').forEach(btn => {
                const onclick = btn.getAttribute('onclick') || '';
                const label = btn.textContent.trim();
                let icon = '⚙️', cls = 'muted';
                if (/note/i.test(label)) { icon = '📝'; cls = 'blue'; }
                else if (/nickname/i.test(label)) { icon = '🏷️'; cls = 'muted'; }
                else if (/favorite.*pos/i.test(label)) { icon = '🔄'; cls = 'muted'; }
                else if (/compare/i.test(label)) { icon = '⚖️'; cls = 'blue'; }
                else if (/demote/i.test(label)) { icon = '⬇️'; cls = 'red'; }
                else if (/promote/i.test(label)) { icon = '⬆️'; cls = 'green'; }
                otherBtns.push({ onclick, label, icon, cls });
            });
        }

        /* Awards — parse structured data from each award_row */
        const awardRows = [];
        col3.querySelectorAll('.award_row').forEach(li => {
            const img = li.querySelector('img');
            const imgSrc = img ? img.getAttribute('src') : '';
            const rawText = li.textContent.trim();

            /* Determine award type from image */
            let awardType = '', awardIcon = '🏆', iconCls = 'gold';
            if (/award_year_u21/.test(imgSrc)) { awardType = 'U21 Player of the Year'; awardIcon = '🌟'; iconCls = 'silver'; }
            else if (/award_year/.test(imgSrc)) { awardType = 'Player of the Year'; awardIcon = '🏆'; iconCls = 'gold'; }
            else if (/award_goal_u21/.test(imgSrc)) { awardType = 'U21 Top Scorer'; awardIcon = '⚽'; iconCls = 'silver'; }
            else if (/award_goal/.test(imgSrc)) { awardType = 'Top Scorer'; awardIcon = '⚽'; iconCls = 'gold'; }

            /* Extract season number */
            const seasonMatch = rawText.match(/season\s+(\d+)/i);
            const season = seasonMatch ? seasonMatch[1] : '';

            /* Extract league link + flag */
            const leagueLink = li.querySelector('a[league_link]');
            const leagueName = leagueLink ? leagueLink.textContent.trim() : '';
            const leagueHref = leagueLink ? leagueLink.getAttribute('href') : '';
            const flagEl = li.querySelector('.country_link');
            const flagHtml = flagEl ? flagEl.outerHTML : '';

            /* Extract stats: goals or rating */
            let statText = '';
            const goalMatch = rawText.match(/(\d+)\s+goals?\s+in\s+(\d+)\s+match/i);
            const ratingMatch = rawText.match(/rating\s+of\s+([\d.]+)\s+in\s+(\d+)\s+match/i);
            if (goalMatch) statText = `${goalMatch[1]} goals / ${goalMatch[2]} games`;
            else if (ratingMatch) statText = `${ratingMatch[1]} avg / ${ratingMatch[2]} games`;

            awardRows.push({ awardType, awardIcon, iconCls, season, leagueName, leagueHref, flagHtml, statText });
        });

        /* ── Build new sidebar HTML ── */
        let h = '<div class="tmps-sidebar">';

        /* Transfer Options (own player) */
        if (btnData.length > 0) {
            h += '<div class="tmps-section">';
            h += '<div class="tmps-section-head">Transfer Options</div>';
            h += '<div class="tmps-btn-list">';
            for (const b of btnData) {
                h += `<button class="tmps-btn ${b.cls}" onclick="${b.onclick.replace(/"/g, '&quot;')}">`;
                h += `<span class="tmps-btn-icon">${b.icon}</span>${b.label}`;
                h += '</button>';
            }
            h += '</div></div>';
        }

        /* Transfer Listed (external player) — live card */
        if (transferListed) {
            h += '<div class="tmtf-card" id="tmtf-live"></div>';
        }

        /* Other Options */
        if (noteText || otherBtns.length > 0) {
            h += '<div class="tmps-section">';
            h += '<div class="tmps-section-head">Options</div>';
            if (noteText) {
                h += `<div class="tmps-note">${noteText}</div>`;
            }
            if (otherBtns.length > 0) {
                h += '<div class="tmps-btn-list">';
                for (const b of otherBtns) {
                    const isCompare = /compare/i.test(b.label);
                    const oc = isCompare ? 'window.tmCompareOpen()' : b.onclick.replace(/"/g, '&quot;');
                    h += `<button class="tmps-btn ${b.cls}" onclick="${oc}">`;
                    h += `<span class="tmps-btn-icon">${b.icon}</span>${b.label}`;
                    h += '</button>';
                }
                h += '</div>';
            }
            h += '</div>';
        }

        /* Awards */
        if (awardRows.length > 0) {
            h += '<div class="tmps-section">';
            h += '<div class="tmps-section-head">Awards</div>';
            h += '<div class="tmps-award-list">';
            for (const a of awardRows) {
                h += `<div class="tmps-award">`;
                h += `<div class="tmps-award-icon ${a.iconCls}">${a.awardIcon}</div>`;
                h += `<div class="tmps-award-body">`;
                h += `<div class="tmps-award-title">${a.awardType}</div>`;
                let sub = '';
                if (a.flagHtml) sub += a.flagHtml + ' ';
                if (a.leagueName) sub += a.leagueHref ? `<a href="${a.leagueHref}">${a.leagueName}</a>` : a.leagueName;
                if (a.statText) sub += (sub ? ' · ' : '') + a.statText;
                if (sub) h += `<div class="tmps-award-sub">${sub}</div>`;
                h += `</div>`;
                if (a.season) h += `<span class="tmps-award-season">S${a.season}</span>`;
                h += `</div>`;
            }
            h += '</div></div>';
        }

        h += '</div>';

        /* ── Replace column3_a contents ── */
        col3.innerHTML = h;

        /* ── Live Transfer Polling ── */
        if (transferListed) {
            const tfCard = document.getElementById('tmtf-live');
            if (tfCard) {
                let tfInterval = null;
                const fmtCoin = (v) => {
                    const n = typeof v === 'string' ? parseInt(v.replace(/[^0-9]/g, '')) : v;
                    return n ? n.toLocaleString('en-US') : '0';
                };
                const fmtBidArg = (v) => {
                    const n = typeof v === 'string' ? parseInt(v.replace(/[^0-9]/g, '')) : v;
                    return n ? n.toLocaleString('en-US') : '0';
                };
                const renderTransfer = (d) => {
                    const isExpired = d.expiry === 'expired';
                    const hasBuyer = d.buyer_id && d.buyer_id !== '0' && d.buyer_name;
                    const isAgent = !hasBuyer && parseInt((d.current_bid || '0').toString().replace(/[^0-9]/g, '')) > 0;
                    let html = `<div class="tmtf-head"><span>🔄 Transfer</span>`;
                    html += `<button class="tmtf-reload" title="Refresh" id="tmtf-reload-btn">↻</button>`;
                    html += `</div><div class="tmtf-body">`;
                    /* Expiry */
                    html += `<div class="tmtf-row"><span class="tmtf-lbl">Expiry</span>`;
                    if (isExpired) {
                        html += `<span class="tmtf-val expired">Expired</span>`;
                    } else {
                        html += `<span class="tmtf-val expiry">${d.expiry}</span>`;
                    }
                    html += `</div>`;
                    /* Current Bid */
                    const curBid = parseInt((d.current_bid || '0').toString().replace(/[^0-9]/g, ''));
                    if (curBid > 0) {
                        html += `<div class="tmtf-row"><span class="tmtf-lbl">Current Bid</span>`;
                        html += `<span class="tmtf-val bid"><span class="coin">${fmtCoin(curBid)}</span></span></div>`;
                    }
                    /* Bidder */
                    if (hasBuyer) {
                        html += `<div class="tmtf-row"><span class="tmtf-lbl">Bidder</span>`;
                        html += `<span class="tmtf-val buyer"><a href="/club/${d.buyer_id}" style="color:#60a5fa;text-decoration:none">${d.buyer_name}</a></span></div>`;
                    } else if (isAgent && !isExpired) {
                        html += `<div class="tmtf-row"><span class="tmtf-lbl">Bidder</span>`;
                        html += `<span class="tmtf-val agent">Agent</span></div>`;
                    }
                    /* Next bid / min bid */
                    if (!isExpired && d.next_bid) {
                        const nextVal = typeof d.next_bid === 'number' ? d.next_bid : parseInt((d.next_bid || '0').toString().replace(/[^0-9]/g, ''));
                        html += `<div class="tmtf-row"><span class="tmtf-lbl">${curBid > 0 ? 'Next Bid' : 'Min Bid'}</span>`;
                        html += `<span class="tmtf-val bid"><span class="coin">${fmtCoin(nextVal)}</span></span></div>`;
                    }
                    /* Expired result */
                    if (isExpired) {
                        if (hasBuyer) {
                            html += `<div class="tmtf-row"><span class="tmtf-lbl">Sold To</span>`;
                            html += `<span class="tmtf-val sold"><a href="/club/${d.buyer_id}" style="color:#4ade80;text-decoration:none">${d.buyer_name}</a></span></div>`;
                            html += `<div class="tmtf-row"><span class="tmtf-lbl">Price</span>`;
                            html += `<span class="tmtf-val sold"><span class="coin">${fmtCoin(d.current_bid)}</span></span></div>`;
                        } else if (curBid > 0) {
                            html += `<div class="tmtf-row"><span class="tmtf-lbl">Result</span>`;
                            html += `<span class="tmtf-val agent">Sold to Agent</span></div>`;
                            html += `<div class="tmtf-row"><span class="tmtf-lbl">Price</span>`;
                            html += `<span class="tmtf-val sold"><span class="coin">${fmtCoin(d.current_bid)}</span></span></div>`;
                        } else {
                            html += `<div class="tmtf-row"><span class="tmtf-lbl">Result</span>`;
                            html += `<span class="tmtf-val expired">Not Sold</span></div>`;
                        }
                    }
                    /* Bid button */
                    if (!isExpired) {
                        const nb = d.next_bid ? fmtBidArg(d.next_bid) : transferListed.minBid;
                        html += `<button class="tmtf-bid-btn" onclick="tlpop_pop_transfer_bid('${nb}',1,${transferListed.playerId},'${transferListed.playerName.replace(/'/g, "\\'")}')">🔨 Make Bid / Agent</button>`;
                    }
                    html += `</div>`;
                    tfCard.innerHTML = html;
                    /* Attach reload */
                    const reloadBtn = document.getElementById('tmtf-reload-btn');
                    if (reloadBtn) reloadBtn.addEventListener('click', () => fetchTransfer());
                    /* Stop polling on expired */
                    if (isExpired && tfInterval) {
                        clearInterval(tfInterval);
                        tfInterval = null;
                    }
                };
                const fetchTransfer = () => {
                    const reloadBtn = document.getElementById('tmtf-reload-btn');
                    if (reloadBtn) { reloadBtn.innerHTML = '<span class="tmtf-spinner"></span>'; reloadBtn.disabled = true; }
                    $.post('/ajax/transfer_get.ajax.php', {
                        type: 'transfer_reload',
                        player_id: transferListed.playerId
                    }, res => {
                        try {
                            const d = typeof res === 'object' ? res : JSON.parse(res);
                            if (d.success) renderTransfer(d);
                        } catch (e) {}
                    }).fail(() => {
                        if (reloadBtn) { reloadBtn.innerHTML = '↻'; reloadBtn.disabled = false; }
                    });
                };
                /* Initial fetch + start interval */
                fetchTransfer();
                tfInterval = setInterval(fetchTransfer, 60000);
            }
        }
    };

    buildSidebar();

    /* ═══════════════════════════════════════════════════════════
       ASI CALCULATOR — widget in column3_a
       ═══════════════════════════════════════════════════════════ */
    const buildASICalculator = () => {
        const col3 = document.querySelector('.column3_a');
        if (!col3) return;

        const K = Math.pow(2, 9) * Math.pow(5, 4) * Math.pow(7, 7); // 263534560000

        const calcASI = (currentASI, trainings, avgTI) => {
            const base = Math.pow(currentASI * K, 1 / 7);
            const added = (avgTI * trainings) / 10;
            if (isGoalkeeper) {
                const ss11 = base / 14 * 11;
                const fs11 = ss11 + added;
                return Math.round(Math.pow(fs11 / 11 * 14, 7) / K);
            }
            return Math.round(Math.pow(base + added, 7) / K);
        };

        const agentVal = (si, ageM, gk) => {
            const a = ageM / 12;
            if (a < 18) return 0;
            let p = Math.round(si * 500 * Math.pow(25 / a, 2.5));
            if (gk) p = Math.round(p * 0.75);
            return p;
        };

        /* Defaults: trainings = months until .11, TI = player average TI */
        const defaultTrainings = playerMonths !== null ? (playerMonths >= 11 ? 12 : 11 - playerMonths) : '';
        const defaultTI = playerTI !== null ? playerTI : '';

        let h = '<div class="tmac-card">';
        h += '<div class="tmac-head">ASI Calculator</div>';
        h += '<div class="tmac-form">';
        h += `<div class="tmac-field"><span class="tmac-label">Trainings</span><input type="number" id="tmac-trainings" class="tmac-input" value="${defaultTrainings}" placeholder="12" min="1" max="500"></div>`;
        h += `<div class="tmac-field"><span class="tmac-label">Avg TI</span><input type="number" id="tmac-ti" class="tmac-input" value="${defaultTI}" placeholder="8" min="0.1" max="10" step="0.1"></div>`;
        h += '<button id="tmac-calc" class="tmsc-send-btn">Calculate</button>';
        h += '</div>';
        h += '<div class="tmac-result" id="tmac-result">';
        h += '<div class="tmac-result-row"><span class="tmac-result-lbl">Age</span><span class="tmac-result-val" id="tmac-age">-</span></div>';
        h += '<div class="tmac-result-row"><span class="tmac-result-lbl">New ASI</span><span class="tmac-result-val" id="tmac-asi">-</span></div>';
        h += '<div class="tmac-result-row"><span class="tmac-result-lbl">Skill Sum</span><span class="tmac-result-val" id="tmac-skillsum">-</span></div>';
        h += '<div class="tmac-result-row"><span class="tmac-result-lbl">Sell To Agent</span><span class="tmac-result-val" id="tmac-sta">-</span></div>';
        h += '</div>';
        h += '</div>';

        const el = document.createElement('div');
        el.innerHTML = h;
        col3.appendChild(el);

        document.getElementById('tmac-calc').addEventListener('click', () => {
            const trainings = parseInt(document.getElementById('tmac-trainings').value) || 0;
            const avgTI = parseFloat(document.getElementById('tmac-ti').value) || 0;
            if (trainings <= 0 || avgTI <= 0 || !playerASI) return;

            const newASI = calcASI(playerASI, trainings, avgTI);
            const asiDiff = newASI - playerASI;

            /* 1 training = 1 month */
            const resultEl = document.getElementById('tmac-result');
            resultEl.classList.add('show');

            const ageEl = document.getElementById('tmac-age');
            if (playerAge !== null && playerMonths !== null) {
                const curYears = Math.floor(playerAge);
                const totalMonths = curYears * 12 + playerMonths + trainings;
                const newYrs = Math.floor(totalMonths / 12);
                const newMos = totalMonths % 12;
                ageEl.innerHTML = `${newYrs}.${newMos}`;
            } else {
                ageEl.textContent = '-';
            }

            const asiEl = document.getElementById('tmac-asi');
            asiEl.innerHTML = `${newASI.toLocaleString()}<span class="tmac-diff">+${asiDiff.toLocaleString()}</span>`;

            /* Skill Sum: current → future */
            const rawBase = Math.pow(playerASI * K, 1 / 7);
            const curSS = isGoalkeeper ? rawBase / 14 * 11 : rawBase;
            const futSS = curSS + (avgTI * trainings) / 10;
            document.getElementById('tmac-skillsum').innerHTML =
                `${curSS.toFixed(1)} → ${futSS.toFixed(1)}`;

            /* Sell To Agent: future price with diff */
            if (playerAge !== null && playerMonths !== null) {
                const curTotMo = Math.floor(playerAge) * 12 + playerMonths;
                const futTotMo = curTotMo + trainings;
                const curSTA = agentVal(playerASI, curTotMo, isGoalkeeper);
                const futSTA = agentVal(newASI, futTotMo, isGoalkeeper);
                const staDiff = futSTA - curSTA;
                const staSign = staDiff >= 0 ? '+' : '';
                document.getElementById('tmac-sta').innerHTML =
                    `${futSTA.toLocaleString()}<span class="tmac-diff">${staSign}${staDiff.toLocaleString()}</span>`;
            } else {
                document.getElementById('tmac-sta').textContent = '-';
            }
        });
    };
    // buildASICalculator called from fetchTooltip callback

    /* ═══════════════════════════════════════════════════════════
       COLUMN1 NAV — replace native menu
       ═══════════════════════════════════════════════════════════ */
    const buildColumn1Nav = () => {
        const col1 = document.querySelector('.column1');
        if (!col1) return;
        const links = col1.querySelectorAll('.content_menu a');
        if (!links.length) return;

        const ICONS = {
            'Squad Overview': '👥',
            'Statistics': '📊',
            'History': '📜',
            'Fixtures': '📅'
        };

        let navH = '<div class="tmcn-nav">';
        links.forEach(a => {
            const label = a.textContent.trim();
            const href = a.getAttribute('href') || '#';
            const icon = ICONS[label] || '📋';
            navH += `<a href="${href}"><span class="tmcn-icon">${icon}</span><span class="tmcn-lbl">${label}</span></a>`;
        });
        navH += '</div>';

        const nav = document.createElement('div');
        nav.innerHTML = navH;
        col1.prepend(nav.firstChild);
    };

    buildColumn1Nav();

    /* ═══════════════════════════════════════════════════════════
       GRAPH SYNC — Build full skill history from graphs endpoint
       when player has unlocked skills. This gives us weekly integer
       skills + ASI from the player's debut, far more data than
       manual visits. Decision matrix:
         No store at all         → try graphs
         Store + graphSync + cur → skip (already done)
         Store + graphSync - cur → regular save (add current week)
         Store - graphSync       → try graphs (overwrite with full history)
       ═══════════════════════════════════════════════════════════ */
    const GRAPH_KEYS_OUT = ['strength', 'stamina', 'pace', 'marking', 'tackling', 'workrate', 'positioning', 'passing', 'crossing', 'technique', 'heading', 'finishing', 'longshots', 'set_pieces'];
    const GRAPH_KEYS_GK = ['strength', 'pace', 'jumping', 'stamina', 'one_on_ones', 'reflexes', 'aerial_ability', 'communication', 'kicking', 'throwing', 'handling'];

    const syncFromGraphs = (year, month, skills, SI, gk) => {
        const ageKey = `${year}.${month}`;
        const store = PlayerDB.get(PLAYER_ID);
        const hasStore = store && store.records;
        const hasGraphSync = hasStore && store.graphSync === true;
        const hasCurWeek = hasStore && store.records[ageKey];

        /* graphSync + current week exists → nothing to do (unless still v1/v2 or has null values) */
        if (hasGraphSync && hasCurWeek) {
            if (store._v < 3) {
                console.log('[GraphSync] graphSync present but store still v' + store._v + ' — running analyzeGrowth');
                setTimeout(analyzeGrowth, 500);
            } else if (!store._nullResynced &&
                       Object.values(store.records).some(r =>
                           r.REREC == null || r.R5 == null || r.routine == null)) {
                console.log('[GraphSync] v3 store has null REREC/R5/routine — re-running analyzeGrowth');
                setTimeout(analyzeGrowth, 500);
            } else {
                console.log('[GraphSync] Already synced from graphs, current week exists — skipping');
            }
            return;
        }

        /* graphSync + current week missing → just do regular save + analyzeGrowth */
        if (hasGraphSync && !hasCurWeek) {
            console.log('[GraphSync] Has graphSync but missing current week — regular save');
            saveCurrentVisit(year, month, skills, SI, gk);
            setTimeout(analyzeGrowth, 800);
            return;
        }

        /* No graphSync → try graphs endpoint */
        console.log('[GraphSync] Trying graphs endpoint...');
        $.post('/ajax/players_get_info.ajax.php', {
            player_id: PLAYER_ID, type: 'graphs', show_non_pro_graphs: true
        }, (res) => {
            try {
                const data = typeof res === 'object' ? res : JSON.parse(res);
                const g = data && data.graphs;
                const graphKeys = gk ? GRAPH_KEYS_GK : GRAPH_KEYS_OUT;
                console.log('[GraphSync] Graphs data received:', g ? Object.keys(g) : 'no graphs');
                /* Check if graphs has skill data (first skill key must exist with data) */
                const firstSkillArr = g && g[graphKeys[0]];
                if (!g || !firstSkillArr || firstSkillArr.length < 2) {
                    console.log('[GraphSync] No graph skill data available — falling back to regular');
                    saveCurrentVisit(year, month, skills, SI, gk);
                    setTimeout(analyzeGrowth, 800);
                    return;
                }

                /* Build v1 store from graph data */
                const L = firstSkillArr.length;

                /* Reconstruct ASI array: prefer skill_index, else derive from TI backwards
                   using the same formula as the ASI Calculator widget:
                   base = (ASI * K)^(1/7), then ASI = (base ± ti/10)^7 / K
                   NOTE: TI (and skill_index) arrays may be 1 longer than skill arrays
                   because they include a pre-pro dummy at index 0. We align using an offset. */
                let asiArr;
                if (g.skill_index && g.skill_index.length >= L) {
                    /* skill_index may have extra pre-pro entry; take the last L entries */
                    const off = g.skill_index.length - L;
                    asiArr = g.skill_index.slice(off).map(v => parseInt(v) || 0);
                } else if (g.ti && g.ti.length >= L) {
                    const K = gk ? 48717927500 : (Math.pow(2, 9) * Math.pow(5, 4) * Math.pow(7, 7));
                    const tiOff = g.ti.length - L; /* usually 1: TI has extra pre-pro entry */
                    asiArr = new Array(L);
                    asiArr[L - 1] = SI; /* current ASI from tooltip */
                    for (let j = L - 2; j >= 0; j--) {
                        const ti = parseInt(g.ti[j + 1 + tiOff]) || 0;
                        const base = Math.pow(asiArr[j + 1] * K, 1 / 7);
                        asiArr[j] = Math.max(0, Math.round(Math.pow(base - ti / 10, 7) / K));
                    }
                    console.log(`[GraphSync] ASI reconstructed from TI (offset=${tiOff}, skill_index unavailable)`);
                } else {
                    asiArr = new Array(L).fill(0);
                    console.log('[GraphSync] No skill_index or TI — ASI set to 0');
                }

                const curAgeMonths = year * 12 + month;
                const _prevStore = PlayerDB.get(PLAYER_ID);
                const newStore = { _v: 1, graphSync: true, lastSeen: Date.now(), records: {} };
                if (_prevStore?.meta) newStore.meta = _prevStore.meta;

                for (let i = 0; i < L; i++) {
                    const ageMonths = curAgeMonths - (L - 1 - i);
                    const yr = Math.floor(ageMonths / 12);
                    const mo = ageMonths % 12;
                    const key = `${yr}.${mo}`;
                    const si = parseInt(asiArr[i]) || 0;
                    const sk = graphKeys.map(k => parseInt(g[k]?.[i]) || 0);

                    newStore.records[key] = {
                        SI: si,
                        REREC: null,
                        R5: null,
                        skills: sk,
                        routine: null
                    };
                }

                PlayerDB.set(PLAYER_ID, newStore);
                const recCount = Object.keys(newStore.records).length;
                console.log(`%c[GraphSync] ✓ Synced player ${PLAYER_ID} from graphs: ${recCount} weeks (full career)`,
                    'font-weight:bold;color:#38bdf8');

                /* Now run analyzeGrowth to convert v1 → v3 */
                setTimeout(analyzeGrowth, 500);

            } catch (e) {
                console.warn('[GraphSync] Parse error, falling back to regular:', e.message);
                saveCurrentVisit(year, month, skills, SI, gk);
                setTimeout(analyzeGrowth, 800);
            }
        }).fail(() => {
            console.warn('[GraphSync] Request failed — falling back to regular');
            saveCurrentVisit(year, month, skills, SI, gk);
            setTimeout(analyzeGrowth, 800);
        });
    };

    /* ═══════════════════════════════════════════════════════════
       SAVE CURRENT VISIT TO GROWTH RECORD
       Writes {pid}_data["year.month"] = { SI, skills } on every visit.
       REREC and R5 are left as stored (filled in by RatingR6 script).
       ═══════════════════════════════════════════════════════════ */
    const saveCurrentVisit = (year, month, skills, SI, gk) => {
        if (!SI || SI <= 0 || !year || !skills || !skills.length) return;
        const ageKey = `${year}.${month}`;
        try {
            /* Compute decimal skill values (skillsC) — same as RatingR6 setJSON */
            const weight = gk ? 48717927500 : 263533760000;
            const log27 = Math.log(Math.pow(2, 7));
            const allSum = skills.reduce((s, v) => s + v, 0);
            const remainder = Math.round((Math.pow(2, Math.log(weight * SI) / log27) - allSum) * 10) / 10;
            const goldstar = skills.filter(v => v === 20).length;
            const nonStar = skills.length - goldstar;
            const skillsC = skills.map(v => v === 20 ? 20 : v + (nonStar > 0 ? remainder / nonStar : 0));

            let store = PlayerDB.get(PLAYER_ID);
            if (!store || !store._v) store = { _v: 1, lastSeen: Date.now(), records: {} };
            const prev = store.records[ageKey] || {};
            if (prev.locked) {
                console.log(`[TmPlayer] Record ${ageKey} is locked (squad sync) — skipping overwrite`);
                return;
            }
            store.records[ageKey] = {
                SI,
                REREC: prev.REREC ?? null,
                R5: prev.R5 ?? null,
                skills: store._v >= 2 && prev.skills ? prev.skills : skillsC,
                routine: prev.routine ?? null
            };
            store.lastSeen = Date.now();
            /* Save player meta (name, pos) for compare dialog */
            if (tooltipPlayer) {
                store.meta = { name: tooltipPlayer.name || '', pos: tooltipPlayer.favposition || '', isGK: gk };
            }
            PlayerDB.set(PLAYER_ID, store);
            console.log(store.records);
            console.log(`[TmPlayer] Saved visit: player ${PLAYER_ID}, age ${ageKey}, SI ${SI}`);
        } catch (e) {
            console.warn('[TmPlayer] saveCurrentVisit failed:', e.message);
        }
    };
    /* ═══════════════════════════════════════════════════════════
       GROWTH ANALYSIS — Week-by-week decimal estimation using
       training weights × TI efficiency curves.
       Goes through stored records chronologically, computes the
       total-skill-point delta each month from the ASI formula,
       distributes it across skills weighted by:
         W[i]  = training allocation for skill i
         eff() = TI-to-skill-point efficiency at that skill level
       Handles gold-star overflow (maxed skill's share goes to ALL
       other non-maxed skills randomly, not to its group mates).
       ═══════════════════════════════════════════════════════════ */
    const analyzeGrowth = () => {
        let store;
        try { store = PlayerDB.get(PLAYER_ID); } catch (e) { return; }
        if (!store || !store.records) return;
        if (store._v >= 3) {
            /* Check if any records have null REREC/R5/routine needing re-sync */
            if (store._nullResynced) return; /* already retried once */
            const hasNulls = Object.values(store.records).some(r =>
                r.REREC == null || r.R5 == null || r.routine == null);
            if (!hasNulls) return;
            console.log('%c[Growth] v3 store has null values — re-processing', 'color:#e8a838;font-weight:bold');
            store._v = 2; /* downgrade to allow full re-processing */
            store._resyncingNulls = true;
        }

        /* Sort records chronologically by age key (year.month) */
        const rawKeys = Object.keys(store.records).sort((a, b) => {
            const [ay, am] = a.split('.').map(Number);
            const [by, bm] = b.split('.').map(Number);
            return (ay * 12 + am) - (by * 12 + bm);
        });
        if (rawKeys.length < 2) {
            /* Single record — can't do delta analysis but can still compute R5/REREC/routine */
            const onlyKey = rawKeys[0];
            const rec = store.records[onlyKey];
            const skills = rec.skills || [];
            const isGKs = skills.length === 11;
            const posIdxS = isGKs ? 9 : (playerPosition ? getPositionIndex(playerPosition.split(',')[0].trim()) : 0);
            const si = parseInt(rec.SI) || 0;
            const rtn = playerRoutine ?? 0;
            const skillsF = skills.map(v => {
                const n = typeof v === 'string' ? parseFloat(v) : v;
                return n >= 20 ? 20 : n;
            });
            const singleStore = {
                _v: 3, lastSeen: Date.now(),
                records: {
                    [onlyKey]: {
                        SI: rec.SI,
                        REREC: Number(calculateRemaindersF(posIdxS, skillsF, si).rec),
                        R5: Number(calculateR5F(posIdxS, skillsF, si, rtn)),
                        skills: skillsF,
                        routine: rtn
                    }
                }
            };
            if (store.graphSync) singleStore.graphSync = true;
            if (store.meta) singleStore.meta = store.meta;
            if (store._nullResynced || store._resyncingNulls) singleStore._nullResynced = true;
            PlayerDB.set(PLAYER_ID, singleStore);
            console.log(`%c[Growth] ✓ Single-record player ${PLAYER_ID} upgraded to v3`,
                'font-weight:bold;color:#6cc040');
            try { GraphsMod.reRender(); } catch (e) { }
            return;
        }

        /* ── Fill gaps: interpolate missing months between recorded ones ── */
        const ageToMonths = (k) => { const [y, m] = k.split('.').map(Number); return y * 12 + m; };
        const monthsToAge = (m) => `${Math.floor(m / 12)}.${m % 12}`;
        const intSkills = (rec) => rec.skills.map(v => {
            const n = typeof v === 'string' ? parseFloat(v) : v;
            return Math.floor(n);
        });
        for (let idx = 0; idx < rawKeys.length - 1; idx++) {
            const aM = ageToMonths(rawKeys[idx]);
            const bM = ageToMonths(rawKeys[idx + 1]);
            const gap = bM - aM;
            if (gap <= 1) continue;
            const rA = store.records[rawKeys[idx]];
            const rB = store.records[rawKeys[idx + 1]];
            const siA = parseInt(rA.SI) || 0, siB = parseInt(rB.SI) || 0;
            const skA = intSkills(rA), skB = intSkills(rB);
            for (let step = 1; step < gap; step++) {
                const t = step / gap;
                const interpKey = monthsToAge(aM + step);
                if (store.records[interpKey]) continue; // already exists
                const interpSI = Math.round(siA + (siB - siA) * t);
                /* For skills: gradually increase — floored integer ramps */
                const interpSk = skA.map((sa, i) => {
                    const sb = skB[i];
                    const diff = sb - sa;
                    return sa + Math.floor(diff * t);
                });
                store.records[interpKey] = {
                    SI: interpSI,
                    REREC: null,
                    R5: null,
                    skills: interpSk,
                    _interpolated: true
                };
            }
        }
        /* Re-sort with interpolated records included */
        const ageKeys = Object.keys(store.records).sort((a, b) =>
            ageToMonths(a) - ageToMonths(b)
        );

        /* Detect GK by skill count from first record */
        const firstSkills = store.records[ageKeys[0]].skills || [];
        const isGK = firstSkills.length === 11;

        /* ── Constants (outfield vs GK) ── */
        const SK = isGK
            ? ['Strength', 'Pace', 'Jumping', 'Stamina', 'One on ones', 'Reflexes', 'Aerial Ability', 'Communication', 'Kicking', 'Throwing', 'Handling']
            : ['Strength', 'Stamina', 'Pace', 'Marking', 'Tackling', 'Workrate',
                'Positioning', 'Passing', 'Crossing', 'Technique', 'Heading',
                'Finishing', 'Longshots', 'Set Pieces'];
        const N = isGK ? 11 : 14;

        /* Training groups (indices into SK array)
           Outfield: T1=Str/Wor/Sta  T2=Mar/Tac  T3=Cro/Pac  T4=Pas/Tec/Set  T5=Hea/Pos  T6=Fin/Lon
           GK: single group — all skills get equal weight (automatic training) */
        const GRP = isGK
            ? [[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]]  /* GK: one group with all 11 skills */
            : [[0, 5, 1], [3, 4], [8, 2], [7, 9, 13], [10, 6], [11, 12]];
        const GRP_COUNT = isGK ? 1 : 6;
        const GRP_NAMES = isGK
            ? ['GK (all)']
            : ['Str/Wor/Sta', 'Mar/Tac', 'Cro/Pac', 'Pas/Tec/Set', 'Hea/Pos', 'Fin/Lon'];
        const s2g = new Array(N);
        GRP.forEach((g, gi) => g.forEach(si => { s2g[si] = gi; }));

        /* Standard training type → focus group index (outfield only) */
        const STD_FOCUS = { '1': 3, '2': 0, '3': 1, '4': 5, '5': 4, '6': 2 };
        const STD_NAMES = {
            '1': 'Technical', '2': 'Fitness', '3': 'Tactical',
            '4': 'Finishing', '5': 'Defending', '6': 'Wings'
        };

        /* TI efficiency by current skill level (skill points gained per 1 TI) */
        const eff = (lvl) => {
            if (lvl >= 20) return 0;        // gold star – maxed
            if (lvl >= 18) return 0.04;     // 40% chance → ~0.04
            if (lvl >= 15) return 0.05;     // 50% chance → ~0.05
            if (lvl >= 5) return 0.10;     // normal
            return 0.15;                    // 100% + 50% bonus
        };

        /* ASI → total skill points: totalPts = (WEIGHT × SI)^(1/7) */
        const ASI_WEIGHT = isGK ? 48717927500 : 263533760000;
        const LOG128 = Math.log(Math.pow(2, 7));
        const totalPts = (si) => Math.pow(2, Math.log(ASI_WEIGHT * si) / LOG128);

        /* Parse integer skills from a record (handles string / float / number) */
        const intOf = (rec) => rec.skills.map(v => {
            const n = typeof v === 'string' ? parseFloat(v) : v;
            return Math.floor(n);
        });

        /* ── Compute per-skill share of total delta ──
           share[i] = w[i]·eff(level[i]) / Σ(w·eff)
           where w[i] comes from training group allocation.
           If a skill is at 20 its share goes to ALL non-maxed skills
           (random overflow), NOT to its group mates. */
        const calcShares = (intS, gw) => {
            const base = new Array(N).fill(0);
            let overflow = 0;
            for (let gi = 0; gi < GRP_COUNT; gi++) {
                const grp = GRP[gi];
                const perSk = gw[gi] / grp.length;
                for (const si of grp) {
                    if (intS[si] >= 20) overflow += perSk;
                    else base[si] = perSk;
                }
            }
            const nonMax = intS.filter(v => v < 20).length;
            const ovfEach = nonMax > 0 ? overflow / nonMax : 0;
            const w = base.map((b, i) => intS[i] >= 20 ? 0 : b + ovfEach);
            const wE = w.map((wi, i) => wi * eff(intS[i]));
            const tot = wE.reduce((a, b) => a + b, 0);
            return tot > 0 ? wE.map(x => x / tot) : new Array(N).fill(0);
        };

        /* Cap decimals at 0.99 per skill; redistribute overflow to uncapped non-maxed skills */
        const capDecimals = (decArr, intArr) => {
            const CAP = 0.99;
            const d = [...decArr];
            let overflow = 0, passes = 0;
            do {
                overflow = 0;
                let freeCount = 0;
                for (let i = 0; i < N; i++) {
                    if (intArr[i] >= 20) { d[i] = 0; continue; }
                    if (d[i] > CAP) { overflow += d[i] - CAP; d[i] = CAP; }
                    else if (d[i] < CAP) freeCount++;
                }
                if (overflow > 0.0001 && freeCount > 0) {
                    const add = overflow / freeCount;
                    for (let i = 0; i < N; i++) {
                        if (intArr[i] < 20 && d[i] < CAP) d[i] += add;
                    }
                }
            } while (overflow > 0.0001 && ++passes < 20);
            return d;
        };

        /* ── Main analysis runner ── */
        const run = (trainingInfo, historyInfo) => {
            /* Determine group weights from training data */
            let gw = new Array(GRP_COUNT).fill(1 / GRP_COUNT);
            let desc = isGK ? 'GK (balanced)' : 'Balanced (no data)';
            if (!isGK && trainingInfo && trainingInfo.custom) {
                const c = trainingInfo.custom;
                const cd = c.custom;
                if (c.custom_on && cd) {
                    /* Custom training: read dots per group */
                    const dots = [];
                    let dtot = 0;
                    for (let i = 0; i < 6; i++) {
                        const d = parseInt(cd['team' + (i + 1)]?.points) || 0;
                        dots.push(d); dtot += d;
                    }
                    /* Laplace smoothing: 0-dot groups still get a small chance */
                    const sm = 0.5;
                    const den = dtot + 6 * sm;
                    gw = dots.map(d => (d + sm) / den);
                    desc = `Custom dots=[${dots.join(',')}]`;
                } else {
                    /* Standard training type */
                    const t = String(c.team || '3');
                    const fg = STD_FOCUS[t] ?? 1;
                    /* Focus group: 25% targeted + 75%/6 random = 37.5%
                       Other groups: 75%/6 = 12.5% each */
                    gw = new Array(6).fill(0.125);
                    gw[fg] = 0.375;
                    desc = `Standard: ${STD_NAMES[t] || t} → focus ${GRP_NAMES[fg]}`;
                }
            }

            /* ── Compute routine history from history GP data ── */
            const routineMap = {};
            if (playerRoutine !== null && playerAge !== null && historyInfo && historyInfo.table && historyInfo.table.total) {
                const totalRows = historyInfo.table.total.filter(r => typeof r.season === 'number');
                const gpBySeason = {};
                totalRows.forEach(r => { gpBySeason[r.season] = (gpBySeason[r.season] || 0) + (parseInt(r.games) || 0); });
                /* Current season = max season in history (it has games this season) */
                const curSeason = totalRows.length > 0 ? Math.max(...totalRows.map(r => r.season)) : 0;
                const curWeek = currentSession;
                const curAgeMonths = Math.floor(playerAge) * 12 + playerMonths;
                const curRoutine = playerRoutine;
                console.log('[Growth] Routine:', { curSeason, curWeek, curRoutine, gpBySeason });
                for (const ageKey of ageKeys) {
                    const recAgeMonths = ageToMonths(ageKey);
                    const weeksBack = curAgeMonths - recAgeMonths;
                    if (weeksBack <= 0) { routineMap[ageKey] = curRoutine; continue; }
                    let gamesAfter = 0;
                    for (let w = 0; w < weeksBack; w++) {
                        const absWeek = (curSeason - 65) * 12 + (curWeek - 1) - w;
                        const season = 65 + Math.floor(absWeek / 12);
                        const gp = gpBySeason[season] || 0;
                        gamesAfter += (season === curSeason) ? (curWeek > 0 ? gp / curWeek : 0) : gp / 12;
                    }
                    routineMap[ageKey] = Math.max(0, Math.round((curRoutine - gamesAfter * 0.1) * 10) / 10);
                }
                console.log(`%c[Growth] Routine history: ${Object.keys(routineMap).length} records`, 'color:#8abc78');
            }

            /* ── Initial record ── */
            const r0 = store.records[ageKeys[0]];
            const i0 = intOf(r0);
            const t0 = totalPts(r0.SI);
            const iSum0 = i0.reduce((a, b) => a + b, 0);
            const rem0 = t0 - iSum0;
            const sh0 = calcShares(i0, gw);
            let dec = capDecimals(sh0.map(s => Math.max(0, rem0 * s)), i0);

            /* Format helpers */
            const fmtDec = (intV, decV) => {
                if (intV >= 20) return '★';
                const d = Math.max(0, decV);
                return `${intV}.${Math.round(d * 100).toString().padStart(2, '0')}`;
            };

            /* Collect summary rows */
            const summary = [];
            const posIdx = isGK ? 9 : (playerPosition ? getPositionIndex(playerPosition.split(',')[0].trim()) : 0);
            const makeRow = (key, si, intArr, decArr, interp, routine) => {
                const row = { Age: interp ? `${key} ≈` : key, ASI: si };
                SK.forEach((n, i) => { row[n.substring(0, 3)] = fmtDec(intArr[i], decArr[i]); });
                const rtn = routine ?? routineMap[key];
                row.Rtn = rtn != null ? rtn : '-';
                if (si > 0) {
                    const skillsF = intArr.map((v, i) => v >= 20 ? 20 : v + decArr[i]);
                    const r = rtn != null ? rtn : 0;
                    /* Old: integer skills + flat remainder */
                    const oldRem = calculateRemainders(posIdx, intArr, si);
                    row.oREC = Number(oldRem.rec);
                    row.oR5 = Number(calculateR5(posIdx, intArr, si, r));
                    /* New: decimal skills (float-aware, no double-counting) */
                    const newRem = calculateRemaindersF(posIdx, skillsF, si);
                    row.nREC = Number(newRem.rec);
                    row.nR5 = Number(calculateR5F(posIdx, skillsF, si, r));
                } else { row.oREC = '-'; row.oR5 = '-'; row.nREC = '-'; row.nR5 = '-'; }
                return row;
            };
            summary.push(makeRow(ageKeys[0], parseInt(r0.SI) || 0, i0, dec, !!store.records[ageKeys[0]]._interpolated, routineMap[ageKeys[0]]));



            /* ── Process each subsequent record ── */
            for (let m = 1; m < ageKeys.length; m++) {
                const prevKey = ageKeys[m - 1], currKey = ageKeys[m];
                const prevRec = store.records[prevKey], currRec = store.records[currKey];
                const pi = intOf(prevRec), ci = intOf(currRec);
                const pt = totalPts(prevRec.SI), ct = totalPts(currRec.SI);
                const delta = ct - pt;
                const ciSum = ci.reduce((a, b) => a + b, 0);
                const cRem = ct - ciSum;

                /* Distribute delta by shares (prev skills determine weights) */
                const sh = calcShares(pi, gw);
                const gains = sh.map(s => delta * s);

                /* Add gains to previous decimals */
                let newDec = dec.map((d, i) => d + gains[i]);

                /* Handle level-ups: subtract integer gain, clamp to 0 */
                for (let i = 0; i < N; i++) {
                    const chg = ci[i] - pi[i];
                    if (chg > 0) {
                        newDec[i] -= chg;
                        if (newDec[i] < 0) newDec[i] = 0;
                    }
                    if (ci[i] >= 20) newDec[i] = 0;
                }

                /* Normalize: scale so Σdecimals = actual remainder from ASI */
                const ndSum = newDec.reduce((a, b) => a + b, 0);
                if (ndSum > 0.001) {
                    const scale = cRem / ndSum;
                    dec = capDecimals(newDec.map((d, i) => ci[i] >= 20 ? 0 : d * scale), ci);
                } else {
                    /* All near-zero → re-seed from shares */
                    const csh = calcShares(ci, gw);
                    dec = capDecimals(csh.map(s => Math.max(0, cRem * s)), ci);
                }

                summary.push(makeRow(currKey, parseInt(currRec.SI) || 0, ci, dec, !!currRec._interpolated, routineMap[currKey]));
            }

            /* ── Week-by-week summary table ── */
            console.log(`%c[Growth] ═══ Player ${PLAYER_ID} — ${ageKeys.length} weeks (≈ = interpolated) ═══`,
                'font-weight:bold;color:#6cc040');
            // console.table(summary);
            console.log(summary.slice(-1)[0].nR5)

            /* ── Migrate: overwrite {pid}_data with weighted decimals + routine (_v:3) ── */
            const growthStore = { _v: 3, lastSeen: Date.now(), records: {} };
            if (store.graphSync) growthStore.graphSync = true;
            if (store.meta) growthStore.meta = store.meta;
            /* Mark _nullResynced if this was a re-sync of a previously v3 store */
            if (store._nullResynced || store._resyncingNulls) growthStore._nullResynced = true;
            /* Re-iterate to build records with computed skillsC */
            /* We already have the summary, but we need the raw dec arrays.
               Re-run quickly just to collect the float arrays. */
            {
                const r0g = store.records[ageKeys[0]];
                const i0g = intOf(r0g);
                const t0g = totalPts(r0g.SI);
                const iSum0g = i0g.reduce((a, b) => a + b, 0);
                const rem0g = t0g - iSum0g;
                const sh0g = calcShares(i0g, gw);
                let decG = capDecimals(sh0g.map(s => Math.max(0, rem0g * s)), i0g);
                const skillsC0 = i0g.map((v, i) => v >= 20 ? 20 : v + decG[i]);
                const prev0 = store.records[ageKeys[0]];
                growthStore.records[ageKeys[0]] = {
                    SI: prev0.SI,
                    REREC: Number(calculateRemaindersF(posIdx, skillsC0, parseInt(prev0.SI) || 0).rec),
                    R5: Number(calculateR5F(posIdx, skillsC0, parseInt(prev0.SI) || 0, routineMap[ageKeys[0]] || 0)),
                    skills: skillsC0,
                    routine: routineMap[ageKeys[0]] ?? null
                };

                for (let m = 1; m < ageKeys.length; m++) {
                    const pKey = ageKeys[m - 1], cKey = ageKeys[m];
                    const pRec = store.records[pKey], cRec = store.records[cKey];
                    const pig = intOf(pRec), cig = intOf(cRec);
                    const ptg = totalPts(pRec.SI), ctg = totalPts(cRec.SI);
                    const deltaG = ctg - ptg;
                    const ciSumG = cig.reduce((a, b) => a + b, 0);
                    const cRemG = ctg - ciSumG;

                    const shG = calcShares(pig, gw);
                    const gainsG = shG.map(s => deltaG * s);
                    let newDecG = decG.map((d, i) => d + gainsG[i]);

                    for (let i = 0; i < N; i++) {
                        const chg = cig[i] - pig[i];
                        if (chg > 0) { newDecG[i] -= chg; if (newDecG[i] < 0) newDecG[i] = 0; }
                        if (cig[i] >= 20) newDecG[i] = 0;
                    }

                    const ndSumG = newDecG.reduce((a, b) => a + b, 0);
                    if (ndSumG > 0.001) {
                        const scaleG = cRemG / ndSumG;
                        decG = capDecimals(newDecG.map((d, i) => cig[i] >= 20 ? 0 : d * scaleG), cig);
                    } else {
                        const cshG = calcShares(cig, gw);
                        decG = capDecimals(cshG.map(s => Math.max(0, cRemG * s)), cig);
                    }

                    const skillsCm = cig.map((v, i) => v >= 20 ? 20 : v + decG[i]);
                    growthStore.records[cKey] = {
                        SI: cRec.SI,
                        REREC: Number(calculateRemaindersF(posIdx, skillsCm, parseInt(cRec.SI) || 0).rec),
                        R5: Number(calculateR5F(posIdx, skillsCm, parseInt(cRec.SI) || 0, routineMap[cKey] || 0)),
                        skills: skillsCm,
                        routine: routineMap[cKey] ?? null
                    };
                }
            }
            PlayerDB.set(PLAYER_ID, growthStore);
            console.log(`%c[Growth] ✓ Migrated player ${PLAYER_ID} to v3 (weighted decimals + routine)`,
                'font-weight:bold;color:#6cc040');

            /* Re-render graphs if already displayed, so REC chart picks up REREC */
            try { GraphsMod.reRender(); } catch (e) { }

            /* Log comparison for last record */
            const lastKey = ageKeys[ageKeys.length - 1];
            const oldRec = store.records[lastKey];
            const newRec = growthStore.records[lastKey];
            if (oldRec && newRec) {
                const cmpRows = SK.map((name, i) => {
                    const oldV = typeof oldRec.skills[i] === 'string' ? parseFloat(oldRec.skills[i]) : oldRec.skills[i];
                    const newV = newRec.skills[i];
                    const diff = newV - oldV;
                    return {
                        Skill: name,
                        Old: oldV >= 20 ? '★' : oldV.toFixed(2),
                        New: newV >= 20 ? '★' : newV.toFixed(2),
                        Diff: oldV >= 20 && newV >= 20 ? '-' : (diff >= 0 ? '+' : '') + diff.toFixed(2)
                    };
                });
                const totalOld = cmpRows.reduce((s, r) => s + (r.Old === '★' ? 20 : parseFloat(r.Old)), 0);
                const totalNew = cmpRows.reduce((s, r) => s + (r.New === '★' ? 20 : parseFloat(r.New)), 0);
                cmpRows.push({
                    Skill: '── TOTAL ──',
                    Old: totalOld.toFixed(2),
                    New: totalNew.toFixed(2),
                    Diff: ((totalNew - totalOld) >= 0 ? '+' : '') + (totalNew - totalOld).toFixed(2)
                });
                // console.table(cmpRows);

                /* Log old vs new REC and R5 for last record */
                const lastSI = parseInt(newRec.SI) || 0;
                const lastRtn = newRec.routine || 0;
                const oldSkills = oldRec.skills.map(v => typeof v === 'string' ? parseFloat(v) : v);
                const newSkills = newRec.skills;
                const oREC = Number(calculateRemainders(posIdx, oldSkills.map(Math.floor), lastSI).rec);
                const nREC = Number(calculateRemaindersF(posIdx, newSkills, lastSI).rec);
                const oR5 = Number(calculateR5(posIdx, oldSkills.map(Math.floor), lastSI, lastRtn));
                const nR5 = Number(calculateR5F(posIdx, newSkills, lastSI, lastRtn));
                console.log(`%c[Growth] REC: ${oREC} → ${nREC} (${(nREC - oREC) >= 0 ? '+' : ''}${(nREC - oREC).toFixed(2)})  |  R5: ${oR5} → ${nR5} (${(nR5 - oR5) >= 0 ? '+' : ''}${(nR5 - oR5).toFixed(2)})`,
                    'font-weight:bold;color:#5b9bff');
            }


        };

        /* Fetch training + history data in parallel */
        const _parse = r => { try { return typeof r === 'object' ? r : JSON.parse(r); } catch (e) { return null; } };
        const trainReq = $.post('/ajax/players_get_info.ajax.php', {
            player_id: PLAYER_ID, type: 'training', show_non_pro_graphs: true
        }).then(r => _parse(r), () => null);
        const histReq = $.post('/ajax/players_get_info.ajax.php', {
            player_id: PLAYER_ID, type: 'history', show_non_pro_graphs: true
        }).then(r => _parse(r), () => null);
        Promise.all([trainReq, histReq]).then(([t, h]) => run(t, h));
    };

    /* analyzeGrowth is now triggered by syncFromGraphs after data sync */

})();