IMDb Rating Colors

Applies rating color codes to IMDb episode heatmaps, histograms, creates a Season Average section, and adds "Save Image" functionality. Works for all languages.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         IMDb Rating Colors
// @namespace    http://tampermonkey.net/
// @version      2.6
// @description  Applies rating color codes to IMDb episode heatmaps, histograms, creates a Season Average section, and adds "Save Image" functionality. Works for all languages.
// @author       Windy
// @match        https://www.imdb.com/title/*/ratings*
// @match        https://www.imdb.com/*/title/*/ratings*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=imdb.com
// @require      https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js
// @grant        none
// ==/UserScript==

(function() {
    'use strict';

    // --- CONFIGURATION ---
    const BLEND_CAP = 0.8;
    const DEBOUNCE_TIME = 200; // ms
    const FETCH_DELAY = 1250;   // Time in ms to wait before fetching data
    const SHOW_TOOLTIPS = false; // Set to false to disable tooltips
    const TRANSPOSE_THRESHOLD = 25; // If episodes > this, swap X/Y axis in screenshot

    const COLOR_STOPS = [
        { val: 9.6, rgb: [29, 161, 242],  label: 'Masterpiece',   text: '#ffffff', showInLegend: true },
        { val: 9.0, rgb: [24, 106, 59],   label: 'Amazing',       text: '#ffffff', showInLegend: true },
        { val: 8.0, rgb: [40, 180, 99],   label: 'Great',         text: '#2a2a2a', showInLegend: true },
        { val: 7.0, rgb: [244, 208, 63],  label: 'Good',          text: '#2a2a2a', showInLegend: true },
        { val: 6.0, rgb: [243, 156, 18],  label: 'Average',       text: '#2a2a2a', showInLegend: true },
        { val: 5.0, rgb: [231, 76, 60],   label: 'Bad',           text: '#ffffff', showInLegend: true },
        { val: 4.0, rgb: [99, 57, 116],   label: 'Trash',         text: '#ffffff', showInLegend: false }, // Math Anchor
        { val: 0.0, rgb: [99, 57, 116],   label: 'Trash',         text: '#ffffff', showInLegend: true }
    ];

    const STORAGE_KEY_GRADIENT = 'sg_gradient_mode_enabled';

    let isGradientMode = localStorage.getItem(STORAGE_KEY_GRADIENT) === 'true';

    // Global Cache
    const TITLE_CACHE = new Map();
    const FETCH_PROMISES = new Map();

    // --- UTILS ---
    function debounce(func, wait) {
        let timeout;
        return function(...args) {
            clearTimeout(timeout);
            timeout = setTimeout(() => func.apply(this, args), wait);
        };
    }

    function rgbToString(rgb) {
        return `rgb(${Math.round(rgb[0])}, ${Math.round(rgb[1])}, ${Math.round(rgb[2])})`;
    }

    function blendColors(color1, color2, percentage) {
        const p = percentage;
        const w = 1 - p;
        const r = Math.sqrt((w * (color1[0] ** 2)) + (p * (color2[0] ** 2)));
        const g = Math.sqrt((w * (color1[1] ** 2)) + (p * (color2[1] ** 2)));
        const b = Math.sqrt((w * (color1[2] ** 2)) + (p * (color2[2] ** 2)));
        return [r, g, b];
    }

    function getStyleForRating(rating) {
        if (isGradientMode) {
            if (rating >= COLOR_STOPS[0].val) return { bg: rgbToString(COLOR_STOPS[0].rgb), text: COLOR_STOPS[0].text };
            if (rating <= COLOR_STOPS[COLOR_STOPS.length - 1].val) return { bg: rgbToString(COLOR_STOPS[COLOR_STOPS.length - 1].rgb), text: COLOR_STOPS[COLOR_STOPS.length - 1].text };
            for (let i = 0; i < COLOR_STOPS.length - 1; i++) {
                const upper = COLOR_STOPS[i];
                const lower = COLOR_STOPS[i+1];
                if (rating < upper.val && rating >= lower.val) {
                    const range = upper.val - lower.val;
                    const rawProgress = (range === 0) ? 0 : (rating - lower.val) / range;
                    const weightedProgress = rawProgress * BLEND_CAP;
                    const finalRgb = blendColors(lower.rgb, upper.rgb, weightedProgress);
                    return { bg: rgbToString(finalRgb), text: lower.text };
                }
            }
        } else {
            for (const stop of COLOR_STOPS) {
                if (!stop.showInLegend && stop.val === 4.0) continue;
                if (rating >= stop.val) return { bg: rgbToString(stop.rgb), text: stop.text };
            }
            return { bg: rgbToString(COLOR_STOPS[COLOR_STOPS.length-1].rgb), text: '#ffffff' };
        }
    }

    function formatVoteCount(count) {
        if (!count) return '';
        if (count >= 1000000) return (count / 1000000).toFixed(1) + 'M';
        if (count >= 1000) return (count / 1000).toFixed(1) + 'K';
        return count;
    }

    // --- TITLE SCRAPER & FETCHER ---
    function scrapeVisibleTitles() {
        const items = document.querySelectorAll('.ipc-metadata-list-summary-item');
        items.forEach(item => {
            const link = item.querySelector('a.ipc-title-link-wrapper') || item.querySelector('a[href^="/title/"]');
            const titleEl = item.querySelector('.ipc-title__text');

            if (link && titleEl) {
                const rawHref = link.getAttribute('href');
                if(!rawHref) return;
                const href = rawHref.split('?')[0].replace(/\/$/, '');
                const text = titleEl.innerText.replace(/^\d+\.\s+/, '');
                TITLE_CACHE.set(href, text);
            }
        });
    }

    async function fetchEpisodeTitle(url) {
        const cleanUrl = url.split('?')[0].replace(/\/$/, '');
        if (TITLE_CACHE.has(cleanUrl)) return TITLE_CACHE.get(cleanUrl);
        if (FETCH_PROMISES.has(cleanUrl)) return FETCH_PROMISES.get(cleanUrl);

        const promise = (async () => {
            try {
                const resp = await fetch(cleanUrl);
                const text = await resp.text();
                // JSON-LD
                const jsonMatch = text.match(/<script type="application\/ld\+json">(.*?)<\/script>/s);
                if (jsonMatch && jsonMatch[1]) {
                    try {
                        const data = JSON.parse(jsonMatch[1]);
                        if (data.name) {
                            TITLE_CACHE.set(cleanUrl, data.name);
                            return data.name;
                        }
                    } catch(e) {}
                }
                // OG Meta
                const ogMatch = text.match(/<meta property="og:title" content="(.*?)"/);
                if (ogMatch) {
                    const cleanTitle = ogMatch[1].replace(/\(TV Episode.*$/, '').replace(/ - .*$/, '').trim();
                    TITLE_CACHE.set(cleanUrl, cleanTitle);
                    return cleanTitle;
                }
            } catch (e) { console.error("SG Colors: Failed to fetch title", e); }
            return null;
        })();

        FETCH_PROMISES.set(cleanUrl, promise);
        return promise;
    }

    // --- MAIN SERIES DATA FETCH (For Screenshot) ---
    async function fetchSeriesData() {
        const match = window.location.pathname.match(/\/title\/(tt\d+)/);
        if (!match) return null;
        const ttId = match[1];
        const url = `https://www.imdb.com/title/${ttId}/`;

        try {
            const resp = await fetch(url);
            const text = await resp.text();
            let data = { poster: null, rating: null, votes: null, title: null };

            // 1. Try JSON-LD
            const jsonMatch = text.match(/<script type="application\/ld\+json">(.*?)<\/script>/s);
            if (jsonMatch && jsonMatch[1]) {
                try {
                    const json = JSON.parse(jsonMatch[1]);
                    data.title = json.name;
                    data.poster = json.image;
                    if (json.aggregateRating) {
                        data.rating = json.aggregateRating.ratingValue;
                        data.votes = formatVoteCount(json.aggregateRating.ratingCount);
                    }
                } catch(e) {}
            }

            // 2. Fallbacks
            if (!data.poster) {
                const ogImg = text.match(/<meta property="og:image" content="(.*?)"/);
                if (ogImg) data.poster = ogImg[1];
            }
            if (data.poster && data.poster.includes('._V1_')) {
                data.poster = data.poster.replace(/(\._V1_)(.*)(\.jpg|\.png|\.jpeg)$/, '$1$3');
            }

            return data;
        } catch (e) {
            console.error("SG Colors: Fetch series data failed", e);
            return null;
        }
    }

    // --- CUSTOM TOOLTIP ---
    let tooltipEl = null;
    let hoverTimeout = null;

    function initTooltip() {
        if (document.getElementById('sg-custom-tooltip')) return;
        tooltipEl = document.createElement('div');
        tooltipEl.id = 'sg-custom-tooltip';
        document.body.appendChild(tooltipEl);
    }

    function showTooltip(e, season, epNum, rating, href) {
        if (!SHOW_TOOLTIPS) return;
        if (!tooltipEl) initTooltip();
        if (hoverTimeout) clearTimeout(hoverTimeout);

        const cleanHref = href.split('?')[0].replace(/\/$/, '');
        let title = TITLE_CACHE.get(cleanHref);

        let html = `<div class="sg-tt-header">${season} Ep ${epNum} <span class="sg-tt-score">${rating}</span></div>`;

        if (title) {
            html += `<div class="sg-tt-title">${title}</div>`;
        } else {
            html += `<div class="sg-tt-loading">...</div>`;
            hoverTimeout = setTimeout(() => {
                fetchEpisodeTitle(href).then(fetchedTitle => {
                    if (fetchedTitle && tooltipEl.style.display === 'block' && tooltipEl.dataset.activeHref === cleanHref) {
                        const loadingEl = tooltipEl.querySelector('.sg-tt-loading');
                        if (loadingEl) loadingEl.outerHTML = `<div class="sg-tt-title">${fetchedTitle}</div>`;
                    }
                });
            }, FETCH_DELAY);
        }

        tooltipEl.innerHTML = html;
        tooltipEl.dataset.activeHref = cleanHref;
        tooltipEl.style.display = 'block';
        moveTooltip(e);
    }

    function moveTooltip(e) {
        if (!tooltipEl) return;
        const x = e.clientX + 15;
        const y = e.clientY + 15;
        const rect = tooltipEl.getBoundingClientRect();
        const winW = window.innerWidth;
        const winH = window.innerHeight;
        const finalX = (x + rect.width > winW) ? e.clientX - rect.width - 10 : x;
        const finalY = (y + rect.height > winH) ? e.clientY - rect.height - 10 : y;
        tooltipEl.style.left = finalX + 'px';
        tooltipEl.style.top = finalY + 'px';
    }

    function hideTooltip() {
        if (hoverTimeout) clearTimeout(hoverTimeout);
        if (tooltipEl) tooltipEl.style.display = 'none';
    }

    // --- SCREENSHOT HELPERS ---
    async function ensureAllSeasonsLoaded() {
        const MAX_CLICKS = 20;
        let clickCount = 0;
        const getBtn = () => document.querySelector('[data-testid="heatmap__load-seasons"]');
        let btn = getBtn();
        while (btn && clickCount < MAX_CLICKS) {
            btn.click();
            clickCount++;
            await new Promise(r => setTimeout(r, 600));
            btn = getBtn();
        }
        if (clickCount > 0) await new Promise(r => setTimeout(r, 500));
    }

    async function ensureAllEpisodesLoaded() {
        const MAX_CLICKS = 30;
        let clickCount = 0;
        const getArrow = () => document.querySelector('.ipc-pager--right.ipc-pager--visible');
        let arrow = getArrow();
        while (arrow && clickCount < MAX_CLICKS) {
            arrow.click();
            clickCount++;
            await new Promise(r => setTimeout(r, 250));
            arrow = getArrow();
        }
        if (clickCount > 0) await new Promise(r => setTimeout(r, 300));
    }

    // Extract table data into a JS object to handle transposition easier
    function getGridData() {
        const sCol = document.querySelector('[data-testid="heatmap__seasons-column"]');
        const eData = document.querySelector('[data-testid="heatmap__episode-data"]');
        if (!sCol || !eData) return null;

        const sRows = Array.from(sCol.querySelectorAll('tr'));
        const eRows = Array.from(eData.querySelectorAll('tr'));
        const rowCount = Math.min(sRows.length, eRows.length);

        let seasons = [];
        let maxEps = 0;

        for (let i = 0; i < rowCount; i++) {
            // FIX: Skip rows that are actually headers (containing <th> instead of ratings)
            if (eRows[i].querySelector('th')) continue;

            const epCells = eRows[i].querySelectorAll('td');
            // If row has no data cells, skip it (spacer/header)
            if (epCells.length === 0) continue;

            let name = sRows[i].innerText.trim();
            if (!name) name = `S${seasons.length + 1}`;

            let episodes = [];
            epCells.forEach((cell, idx) => {
                const a = cell.querySelector('a');
                if (a) {
                    episodes.push({
                        val: parseFloat(a.innerText.replace(',', '.')),
                        text: a.innerText
                    });
                } else {
                    episodes.push(null);
                }
            });

            if (episodes.length > maxEps) maxEps = episodes.length;
            seasons.push({ name, episodes });
        }

        return { seasons, maxEps };
    }

async function downloadGraph() {
        const btn = document.getElementById('sg-download-btn');
        const updateBtn = (text) => { if(btn) btn.innerText = text; };

        try {
            updateBtn('Loading Seasons...');
            await ensureAllSeasonsLoaded();

            updateBtn('Loading Episodes...');
            await ensureAllEpisodesLoaded();

            updateBtn('Fetching Data...');

            const seriesData = await fetchSeriesData();
            const gridData = getGridData();
            if (!gridData) throw new Error("Could not parse grid data");

            // --- TITLE FIX ---
            const domTitle = document.querySelector('[data-testid="hero__primary-text"]') ||
                             document.querySelector('h1[data-testid="hero__pageTitle"]') ||
                             document.querySelector('h2[data-testid="subtitle"]');
            let titleText = domTitle ? domTitle.innerText : (seriesData?.title || 'IMDb Chart');

            let posterSrc = seriesData?.poster;
            if (!posterSrc) {
                const lockup = document.querySelector('.ipc-lockup-overlay');
                const fallback = document.querySelector('.ipc-poster__poster-image img') || document.querySelector('[data-testid="hero-media__poster"] img');
                if (lockup && lockup.previousElementSibling?.tagName === 'IMG') {
                    posterSrc = lockup.previousElementSibling.src;
                } else if (fallback) {
                    posterSrc = fallback.src;
                }
            }

            // --- DETERMINE LAYOUT (Transpose Check) ---
            const shouldTranspose = gridData.maxEps > TRANSPOSE_THRESHOLD;

            // Calculate Widths
            const CELL_W = shouldTranspose ? 60 : 55; // Wider cells if Seasons on top
            const ROW_H = '45px';

            let dataTableWidth;
            if (shouldTranspose) {
                dataTableWidth = (gridData.seasons.length * CELL_W) + 60; // Seasons on X axis
            } else {
                dataTableWidth = (gridData.maxEps * CELL_W) + 120; // Episodes on X axis
            }

            // 320px (Sidebar) + 60px (Padding) + Buffer
            const totalWidth = dataTableWidth + 380;

            const wrapper = document.createElement('div');
            wrapper.style.cssText = `position: absolute; top: 0; left: 0; z-index: -9999; background-color: #1F1F1F; color: #ffffff; width: ${totalWidth}px; font-family: Roboto, Helvetica, Arial, sans-serif; display: flex; flex-direction: row; border-radius: 4px; overflow: hidden;`;

            // Sidebar
            const sidebar = document.createElement('div');
            sidebar.style.cssText = `width: 320px; padding: 30px; display: flex; flex-direction: column; gap: 20px; background-color: #1a1a1a; border-right: 1px solid #333; flex-shrink: 0;`;

            if (posterSrc) {
                const imgContainer = document.createElement('div');
                imgContainer.style.cssText = `width: 100%; display: flex; justify-content: center; margin-bottom: 5px;`;
                const img = new Image();
                img.src = posterSrc;
                img.crossOrigin = "anonymous";
                img.style.cssText = `width: 100%; height: auto; border-radius: 8px; box-shadow: 0 8px 20px rgba(0,0,0,0.6);`;
                imgContainer.appendChild(img);
                sidebar.appendChild(imgContainer);
            }

            const hText = document.createElement('h1');
            hText.innerText = titleText;
            hText.style.cssText = `font-size: 26px; line-height: 1.2; margin: 0; color: #fff; font-weight: 800; text-align: center;`;
            sidebar.appendChild(hText);

            if (seriesData && seriesData.rating) {
                const rateBlock = document.createElement('div');
                rateBlock.style.cssText = `display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 4px; padding-bottom: 10px; border-bottom: 1px solid rgba(255,255,255,0.1);`;
                const scoreLine = document.createElement('div');
                scoreLine.style.cssText = `display: flex; align-items: center; gap: 8px;`;
                scoreLine.innerHTML = `
                    <svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" fill="#f5c518"><path d="M12 17.27l4.15 2.51c.76.46 1.69-.22 1.49-1.08l-1.1-4.72 3.67-3.18c.67-.58.31-1.68-.57-1.75l-4.83-.41-1.89-4.46c-.34-.81-1.5-.81-1.84 0L9.19 8.63l-4.83.41c-.88.07-1.24 1.17-.57 1.75l3.67 3.18-1.1 4.72c-.2.86.73 1.54 1.49 1.08l4.15-2.5z"></path></svg>
                    <span style="font-size: 28px; font-weight: bold; color: #fff;">${seriesData.rating}</span>
                `;
                const votesLine = document.createElement('div');
                votesLine.innerText = `${seriesData.votes} ratings`;
                votesLine.style.cssText = `font-size: 13px; color: #888; text-transform: uppercase; letter-spacing: 1px; font-weight: 500;`;
                rateBlock.appendChild(scoreLine);
                rateBlock.appendChild(votesLine);
                sidebar.appendChild(rateBlock);
            }

            // Legend
            const legend = document.getElementById('sg-legend-container');
            if (legend) {
                const legendClone = legend.cloneNode(true);
                legendClone.querySelectorAll('.sg-toggle-wrapper, #sg-download-btn, .sg-control-group').forEach(c => c.remove());
                legendClone.style.cssText = `margin-top: 30px; padding: 15px; background: rgba(255,255,255,0.05); border: 1px solid rgba(255,255,255,0.1); border-radius: 8px; display: flex; flex-direction: column; align-items: center; justify-content: center; white-space: normal;`;
                const itemsDiv = legendClone.querySelector('.sg-legend-items');
                if(itemsDiv) {
                    itemsDiv.style.display = 'flex';
                    itemsDiv.style.flexWrap = 'wrap';
                    itemsDiv.style.justifyContent = 'center';
                    itemsDiv.style.gap = '8px';
                }
                sidebar.appendChild(legendClone);
            }
            wrapper.appendChild(sidebar);

            const mainContent = document.createElement('div');
            mainContent.style.cssText = `flex-grow: 1; padding: 30px; background-color: #1F1F1F; display: flex; flex-direction: column; gap: 20px; overflow: visible; width: max-content;`;

            // Averages
            const avgPanel = document.getElementById('sg-season-averages-container');
            if (avgPanel) {
                const avgClone = avgPanel.cloneNode(true);
                avgClone.style.cssText = 'background: transparent; border: none; padding: 0; box-shadow: none;';
                const avgTitle = avgClone.querySelector('h3'); if(avgTitle) avgTitle.style.color = '#ffffff';
                const infoIcon = avgClone.querySelector('.sg-info-icon'); if (infoIcon) infoIcon.style.display = 'none';

                avgClone.querySelectorAll('.sg-season-card').forEach(card => {
                    card.style.borderColor = 'rgba(255,255,255,0.1)';
                    card.style.background = 'rgba(255,255,255,0.03)';
                    card.querySelector('.sg-season-label').style.color = '#ccc';

                    const score = card.querySelector('.sg-season-score');
                    score.style.boxShadow = 'none';
                    score.style.border = 'none';

                    const val = parseFloat(score.innerText);
                    if (!isNaN(val)) {
                        const style = getStyleForRating(val);
                        score.style.color = style.text;
                    }
                });
                mainContent.appendChild(avgClone);
            }

            // --- BUILD HEATMAP (TRANSPOSED VS NORMAL) ---
            const heatmapContainer = document.createElement('div');

            if (shouldTranspose) {
                // *** TRANSPOSED BUILD (Rows = Episodes, Cols = Seasons) ***
                const table = document.createElement('table');
                table.style.cssText = 'border-collapse: collapse; width: auto; font-size: 13px;';

                // Header (Seasons)
                const thead = document.createElement('thead');
                const hRow = document.createElement('tr');
                // Corner Cell
                const corner = document.createElement('th');
                corner.innerText = "Ep";
                corner.style.cssText = `padding: 8px; color: #bbbbbb; font-weight: 600; text-align: center; border-bottom: 1px solid #333; width: 40px;`;
                hRow.appendChild(corner);

                gridData.seasons.forEach(s => {
                    const th = document.createElement('th');
                    th.innerText = s.name.replace(/^Season\s/,'S'); // Shorten "Season 1" to "S1" if needed
                    th.style.cssText = `padding: 8px 4px; color: #e0e0e0; font-weight: bold; text-align: center; width: ${CELL_W}px;`;
                    hRow.appendChild(th);
                });
                thead.appendChild(hRow);
                table.appendChild(thead);

                // Body (Episodes)
                const tbody = document.createElement('tbody');
                for (let eIdx = 0; eIdx < gridData.maxEps; eIdx++) {
                    const row = document.createElement('tr');

                    // Ep Number Cell
                    const numCell = document.createElement('td');
                    numCell.innerText = eIdx + 1;
                    numCell.style.cssText = `padding: 4px; color: #bbbbbb; text-align: center; font-size: 11px; height: ${ROW_H}; vertical-align: middle;`;
                    row.appendChild(numCell);

                    // Data Cells
                    gridData.seasons.forEach(season => {
                        const td = document.createElement('td');
                        td.style.padding = '2px 4px';
                        td.style.verticalAlign = 'middle';

                        const epData = season.episodes[eIdx];
                        if (epData && !isNaN(epData.val)) {
                            const style = getStyleForRating(epData.val);
                            const div = document.createElement('div');
                            div.innerText = epData.val.toFixed(1);
                            div.style.cssText = `
                                width: 100%; height: 36px; display: flex; align-items: center; justify-content: center;
                                border-radius: 4px; background-color: ${style.bg}; color: ${style.text}; font-weight: 600;
                            `;
                            td.appendChild(div);
                        }
                        row.appendChild(td);
                    });
                    tbody.appendChild(row);
                }
                table.appendChild(tbody);
                heatmapContainer.appendChild(table);

            } else {
                // *** STANDARD BUILD (Rows = Seasons, Cols = Episodes) ***
                const heatmapRow = document.createElement('div');
                heatmapRow.style.cssText = `display: flex; flex-direction: row; align-items: flex-start;`;

                const seasonTable = document.querySelector('[data-testid="heatmap__seasons-column"]');
                const episodeTable = document.querySelector('[data-testid="heatmap__episode-data"]');

                if (seasonTable && episodeTable) {
                    const sClone = seasonTable.cloneNode(true);
                    const eClone = episodeTable.cloneNode(true);

                    sClone.style.cssText = 'border-collapse: collapse; margin-right: 8px;';
                    eClone.style.cssText = 'border-collapse: collapse; width: auto; table-layout: auto;';

                    // Align Rows
                    const sRows = sClone.querySelectorAll('tr');
                    const eRows = eClone.querySelectorAll('tr');
                    const rowCount = Math.max(sRows.length, eRows.length);

                    for (let i = 0; i < rowCount; i++) {
                        if (sRows[i]) {
                            const cells = sRows[i].querySelectorAll('td, th');
                            cells.forEach(c => {
                                c.style.height = ROW_H;
                                c.style.padding = '0';
                                c.style.textAlign = 'center';
                                c.style.width = '60px';
                                c.style.verticalAlign = 'middle';
                                c.style.color = '#e0e0e0';
                                const innerDiv = c.querySelector('div');
                                if (innerDiv) {
                                    innerDiv.style.color = '#e0e0e0';
                                    innerDiv.style.display = 'flex';
                                    innerDiv.style.alignItems = 'center';
                                    innerDiv.style.justifyContent = 'center';
                                    innerDiv.style.height = '100%';
                                    innerDiv.style.width = '100%';
                                    innerDiv.style.transform = 'translateX(-8px)';
                                }
                            });
                        }
                        if (eRows[i]) {
                            const cells = eRows[i].querySelectorAll('td, th');
                            cells.forEach(c => {
                                c.style.height = ROW_H;
                                c.style.padding = '0 4px';
                                c.style.verticalAlign = 'middle';
                            });
                        }
                    }

                    // Style Episode Cells
                    eClone.querySelectorAll('*').forEach(el => {
                        if (el.classList.contains('sg-rating-text')) {

                            const val = parseFloat(el.innerText.replace(',', '.'));
                            if (!isNaN(val)) {
                                const style = getStyleForRating(val);
                                el.style.color = style.text;
                            } else {
                                el.style.color = '#2a2a2a'; // fallback
                            }
                        } else if (el.tagName === 'TH') {
                            el.style.color = '#bbbbbb';
                            el.style.fontSize = '12px';
                        } else if (el.classList.contains('sg-rating-cell')) {
                            el.style.width = '48px';
                            el.style.height = '36px';
                            el.style.borderRadius = '4px';
                            el.style.display = 'flex';
                            el.style.alignItems = 'center';
                            el.style.justifyContent = 'center';
                        }
                    });

                    heatmapRow.appendChild(sClone);
                    heatmapRow.appendChild(eClone);
                    heatmapContainer.appendChild(heatmapRow);
                }
            }

            mainContent.appendChild(heatmapContainer);
            wrapper.appendChild(mainContent);
            document.body.appendChild(wrapper);

            updateBtn('Rendering...');
            await new Promise(r => setTimeout(r, 200));

            const canvas = await html2canvas(wrapper, {
                useCORS: true, backgroundColor: '#1F1F1F', scale: 2,
                width: totalWidth, windowWidth: totalWidth
            });

            const link = document.createElement('a');
            link.download = `${titleText.replace(/[^a-z0-9]/gi, '_')}.png`;
            link.href = canvas.toDataURL('image/png');
            link.click();

            document.body.removeChild(wrapper);

        } catch (e) {
            console.error("Screenshot failed:", e);
            alert("Could not generate image. Check console.");
        } finally {
            updateBtn('Save Image');
        }
    }

// --- CSS ---
    const STYLES = `
        /* Tooltip Styles */
        #sg-custom-tooltip {
            display: none; position: fixed; z-index: 10000; pointer-events: none;
            background: rgba(0, 0, 0, 0.9); color: #fff; padding: 10px 14px;
            border-radius: 6px; box-shadow: 0 4px 12px rgba(0,0,0,0.4);
            font-family: Roboto, Helvetica, Arial, sans-serif; font-size: 13px;
            border: 1px solid rgba(255,255,255,0.15); max-width: 300px;
        }
        .sg-tt-header { font-weight: 400; color: #bbb; display: flex; align-items: center; justify-content: space-between; gap: 10px; font-size: 11px; text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 4px; }
        .sg-tt-score { font-weight: bold; color: #f5c518; font-size: 13px; }
        .sg-tt-title { font-weight: 700; font-size: 14px; line-height: 1.3; }
        .sg-tt-loading { font-style: italic; color: #888; font-size: 12px; margin-top: 2px; }

        /* General Styles */
        #sg-legend-container {
            display: flex; flex-wrap: nowrap; gap: 20px; margin: 16px 0 24px 0; padding: 12px 20px;
            align-items: center; justify-content: center;
            background: var(--ipt-on-base-alt-bg-color, rgba(0,0,0,0.03));
            border-radius: 12px; border: 1px solid var(--ipt-on-base-border-color, rgba(0,0,0,0.1));
            font-family: Roboto, Helvetica, Arial, sans-serif;
            box-shadow: 0 4px 12px rgba(0,0,0,0.05);
            white-space: nowrap;
        }
        .sg-legend-items { display: flex; flex-wrap: nowrap; gap: 8px; align-items: center; }
        .sg-badge {
            display: inline-flex; align-items: center; justify-content: center; padding: 5px 12px;
            border-radius: 20px; font-size: 12px; font-weight: 700; letter-spacing: 0.3px;
            cursor: help; text-shadow: 0 1px 1px rgba(0,0,0,0.1); user-select: none;
            border: 1px solid rgba(255,255,255,0.2); transition: transform 0.2s;
        }
        .sg-badge:hover { transform: translateY(-2px); }
        #sg-season-averages-container { margin: 0; display: flex; flex-direction: column; }
        #sg-season-averages-container .ipc-title,
        #sg-season-averages-container .ipc-title hgroup,
        #sg-season-averages-container .ipc-title__text { overflow: visible !important; text-overflow: clip !important; white-space: normal !important; }
        #sg-season-averages-container .ipc-title { margin-bottom: 0px !important; padding-bottom: 0px !important; }
        .sg-season-avg-grid { display: flex; flex-wrap: wrap; gap: 12px; margin-top: 16px; padding: 0; margin-left: 10px; }
        .sg-season-card {
            display: flex; flex-direction: column; align-items: center; justify-content: center;
            background: var(--ipt-on-base-alt-bg-color, rgba(255,255,255,0.03));
            border-radius: 8px; padding: 12px; min-width: 70px;
            border: 1px solid var(--ipt-on-base-border-color, rgba(255,255,255,0.1));
            transition: transform 0.2s;
        }
        .sg-season-card:hover { transform: translateY(-2px); border-color: rgba(255,255,255,0.3); }
        .sg-season-label { font-size: 12px; font-weight: 600; text-transform: uppercase; opacity: 0.7; margin-bottom: 8px; }
        .sg-season-score {
            font-size: 18px; font-weight: bold; padding: 6px 14px; border-radius: 6px;
            text-align: center; width: 100%; box-shadow: inset 0 0 0 1px rgba(255,255,255,0.15);
            text-shadow: 0 1px 2px rgba(0,0,0,0.3);
            transition: background-color 0.2s ease, color 0.2s ease;
        }
        .sg-info-icon {
            display: inline-flex; justify-content: center; align-items: center;
            width: 18px; height: 18px; border-radius: 50%; border: 1.5px solid currentColor;
            font-size: 12px; font-weight: bold; cursor: help; position: relative; opacity: 0.6;
            margin-left: 8px; vertical-align: middle; color: var(--ipt-on-base-textPrimary-color);
            transform: translateY(-1px);
        }
        .sg-info-icon:hover { opacity: 1; }
        .sg-tooltip {
            visibility: hidden; background-color: #222; color: #fff; text-align: center;
            border-radius: 6px; padding: 8px 12px; position: absolute; z-index: 9999;
            bottom: 135%; left: 50%; transform: translateX(-50%);
            width: 220px; font-size: 12px; font-weight: normal;
            box-shadow: 0 4px 10px rgba(0,0,0,0.3); opacity: 0; transition: opacity 0.3s;
            pointer-events: none; border: 1px solid #444; line-height: 1.4;
        }
        .sg-info-icon:hover .sg-tooltip { visibility: visible; opacity: 1; }
        .sg-tooltip::after {
            content: ""; position: absolute; top: 100%; left: 50%; margin-left: -5px;
            border-width: 5px; border-style: solid; border-color: #222 transparent transparent transparent;
        }
        .sg-rating-cell {
            border-radius: 4px !important;
            background-image: linear-gradient(to bottom, rgba(255,255,255,0.15) 0%, rgba(0,0,0,0.05) 100%);
            transition: background-color 0.2s ease;
        }
        .sg-rating-text { font-weight: 600; text-shadow: 0 1px 2px rgba(0,0,0,0.25); }

        /* Controls */
        .sg-toggle-wrapper { display: flex; align-items: center; gap: 8px; margin-left: 10px; }
        .sg-toggle-label { font-size: 13px; font-weight: 600; cursor: pointer; user-select: none; color: var(--ipt-on-base-textPrimary-color, inherit); }
        .sg-switch { position: relative; display: inline-block; width: 36px; height: 20px; }
        .sg-switch input { opacity: 0; width: 0; height: 0; }
        .sg-slider { position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0; background-color: #ccc; transition: .3s; border-radius: 20px; }
        .sg-slider:before { position: absolute; content: ""; height: 14px; width: 14px; left: 3px; bottom: 3px; background-color: white; transition: .3s; border-radius: 50%; box-shadow: 0 1px 3px rgba(0,0,0,0.3); }
        input:checked + .sg-slider { background-color: #f5c518; }
        input:checked + .sg-slider:before { transform: translateX(16px); }

        #sg-download-btn {
            background: none; border: 1px solid var(--ipt-on-base-border-color, rgba(255,255,255,0.2));
            color: var(--ipt-on-base-textPrimary-color, inherit);
            border-radius: 6px; padding: 4px 12px; cursor: pointer; font-size: 13px; font-weight: 600;
            transition: background 0.2s; display: flex; align-items: center; justify-content: center;
        }
        #sg-download-btn:hover { background: rgba(255,255,255,0.1); }
    `;

    // --- DOM FUNCTIONS ---

    function injectStyles() {
        if (!document.getElementById('sg-styles')) {
            const styleEl = document.createElement('style');
            styleEl.id = 'sg-styles';
            styleEl.innerHTML = STYLES;
            document.head.appendChild(styleEl);
        }
    }

    function injectLegend() {
        if (document.getElementById('sg-legend-container')) return;

        const target = document.querySelector('[data-testid="heatmap__root-element"]')
                    || document.querySelector('.ipc-chip-list')
                    || document.querySelector('[data-testid="histogram-container"]');
        if (!target) return;

        const container = document.createElement('div');
        container.id = 'sg-legend-container';

        const items = document.createElement('div');
        items.className = 'sg-legend-items';

        COLOR_STOPS.forEach(c => {
            if (!c.showInLegend) return;
            const b = document.createElement('div');
            b.className = 'sg-badge';
            b.style.backgroundColor = rgbToString(c.rgb);
            b.style.color = c.text;
            b.innerText = c.label;
            let max = (c.val === 9.6) ? 10.0 : (c.val + 0.9).toFixed(1);
            if (c.val === 9.0) max = 9.5; if (c.val === 5.0) max = 5.9; if (c.val === 0.0) max = 4.9;
            b.title = `${c.val} - ${max}`;
            items.appendChild(b);
        });

        // Gradient Toggle
        const gradWrap = document.createElement('div');
        gradWrap.className = 'sg-toggle-wrapper';
        gradWrap.innerHTML = `
            <label class="sg-toggle-label" for="sg-grad-sw">Gradient</label>
            <label class="sg-switch"><input type="checkbox" id="sg-grad-sw" ${isGradientMode?'checked':''}><span class="sg-slider"></span></label>
        `;

        // Save Image Button
        const dlBtn = document.createElement('button');
        dlBtn.id = 'sg-download-btn';
        dlBtn.innerText = 'Save Image';
        dlBtn.onclick = downloadGraph;

        // Append everything in one line (flex-wrap: wrap handles overflow if screen is small)
        container.append(items, gradWrap, dlBtn);

        gradWrap.querySelector('input').addEventListener('change', (e) => {
            isGradientMode = e.target.checked;
            localStorage.setItem(STORAGE_KEY_GRADIENT, isGradientMode);
            colorizeAndBind(true);
            injectAveragesPanel();
        });

        if (target.parentNode) target.parentNode.insertBefore(container, target);
    }

    function calculateAverages() {
        const results = [];
        const sCol = document.querySelector('[data-testid="heatmap__seasons-column"]');
        const eData = document.querySelector('[data-testid="heatmap__episode-data"]');
        if (!sCol || !eData) return results;

        const sRows = Array.from(sCol.querySelectorAll('td.ratings-heatmap__table-data'));
        const eRows = Array.from(eData.querySelectorAll('tbody tr'));
        const len = Math.min(sRows.length, eRows.length);

        for (let i = 0; i < len; i++) {
            const name = sRows[i].innerText.trim();
            const links = eRows[i].querySelectorAll('a');
            let sum = 0, count = 0;
            links.forEach(l => {
                const v = parseFloat(l.innerText.replace(',', '.'));
                if (!isNaN(v)) { sum += v; count++; }
            });
            if (count > 0) results.push({ name, avg: (sum/count).toFixed(1) });
        }
        return results;
    }

    function injectAveragesPanel() {
        const hmSec = document.querySelector('section[data-testid="sub-section-heatmap"]');
        const moreSec = document.querySelector('section[data-testid="more-from-section"]');
        if (!hmSec) return;

        const avgs = calculateAverages();
        if (!avgs.length) return;

        let panel = document.getElementById('sg-season-averages-container');
        if (!panel) {
            panel = document.createElement('div');
            panel.id = 'sg-season-averages-container';
            panel.className = 'ipc-page-section ipc-page-section--base';
            panel.innerHTML = `
                <div class="ipc-title ipc-title--base ipc-title--section-title ipc-title--on-textPrimary">
                    <hgroup>
                        <h3 class="ipc-title__text">
                            <span>Season Averages</span>
                            <div class="sg-info-icon">i
                                <span class="sg-tooltip">Average calculated based on currently loaded episodes. Load more episodes to update.</span>
                            </div>
                        </h3>
                    </hgroup>
                </div>
                <div id="sg-season-avg-grid" class="sg-season-avg-grid"></div>
            `;
            if (moreSec && moreSec.parentNode) moreSec.parentNode.insertBefore(panel, moreSec);
            else hmSec.parentNode.insertBefore(panel, hmSec.nextSibling);
        }

        const grid = panel.querySelector('#sg-season-avg-grid');
        avgs.forEach(season => {
            let card = grid.querySelector(`.sg-season-card[data-season="${season.name}"]`);
            const style = getStyleForRating(parseFloat(season.avg));

            if (card) {
                const scoreDiv = card.querySelector('.sg-season-score');
                scoreDiv.innerText = season.avg;
                scoreDiv.dataset.rating = season.avg;
                scoreDiv.style.backgroundColor = style.bg;
                scoreDiv.style.color = style.text;
            } else {
                card = document.createElement('div');
                card.className = 'sg-season-card';
                card.dataset.season = season.name;
                card.innerHTML = `
                    <div class="sg-season-label">${season.name}</div>
                    <div class="sg-season-score" data-rating="${season.avg}" style="background-color:${style.bg}; color:${style.text}">${season.avg}</div>
                `;
                grid.appendChild(card);
            }
        });
    }

    // --- MAIN LOGIC ---

    function attachHoverEvents(element, seasonName, epNum, rating, href) {
        if (element.hasAttribute('title')) element.removeAttribute('title');

        const newEl = element.cloneNode(true);
        if(element.parentNode) element.parentNode.replaceChild(newEl, element);

        newEl.addEventListener('mouseenter', (e) => showTooltip(e, seasonName, epNum, rating, href));
        newEl.addEventListener('mousemove', (e) => moveTooltip(e));
        newEl.addEventListener('mouseleave', () => hideTooltip());

        return newEl;
    }

    function colorizeAndBind(forceUpdate = false) {
        scrapeVisibleTitles();

        const sCol = document.querySelector('[data-testid="heatmap__seasons-column"]');
        const eData = document.querySelector('[data-testid="heatmap__episode-data"]');

        if (sCol && eData) {
            const sRows = Array.from(sCol.querySelectorAll('td.ratings-heatmap__table-data'));
            const eRows = Array.from(eData.querySelectorAll('tbody tr'));
            const len = Math.min(sRows.length, eRows.length);

            for (let i = 0; i < len; i++) {
                const seasonName = sRows[i].innerText.trim() || `S${i+1}`;
                const cells = eRows[i].querySelectorAll('td.ratings-heatmap__table-data > div');

                cells.forEach((cell, index) => {
                    if (cell.dataset.sgProcessed && !forceUpdate) return;

                    const a = cell.querySelector('a');
                    if (a) {
                        const val = parseFloat(a.innerText.replace(',', '.'));
                        if (!isNaN(val)) {
                            const s = getStyleForRating(val);
                            cell.classList.add('sg-rating-cell');
                            cell.style.backgroundColor = s.bg;
                            a.classList.add('sg-rating-text');
                            a.style.color = s.text;

                            if (!cell.dataset.sgProcessed) {
                                const href = a.getAttribute('href');
                                if (href) {
                                    attachHoverEvents(a, seasonName, index + 1, val, href);
                                    cell.dataset.sgProcessed = "true";
                                }
                            }
                        }
                    }
                });
            }
        }

        document.querySelectorAll('.sg-season-score').forEach(div => {
            const val = parseFloat(div.dataset.rating);
            if (!isNaN(val)) {
                const s = getStyleForRating(val);
                div.style.backgroundColor = s.bg;
                div.style.color = s.text;
            }
        });

        document.querySelectorAll('[data-testid="histogram-container"] path[aria-label]').forEach(p => {
            const label = p.getAttribute('aria-label');
            const match = label && label.match(/^(\d+)/);
            if (match) {
                const val = parseFloat(match[1]);
                const s = getStyleForRating(val);
                p.style.fill = s.bg;
                p.style.stroke = s.bg;
            }
        });
    }

    function runUpdates(force = false) {
        injectLegend();
        injectAveragesPanel();
        colorizeAndBind(force);
    }

    const lazyUpdate = debounce(() => runUpdates(), DEBOUNCE_TIME);

    function init() {
        injectStyles();
        initTooltip();
        runUpdates();

        const obs = new MutationObserver((mutations) => {
            let changed = false;
            for (const m of mutations) {
                if (m.type === 'childList' && m.addedNodes.length) {
                    changed = true;
                    break;
                }
            }
            if (changed) lazyUpdate();
        });

        const body = document.querySelector('body');
        if (body) obs.observe(body, { childList: true, subtree: true });
    }

    init();

})();