(function () {
    'use strict';

    const loading = document.getElementById('loading');
    const error = document.getElementById('error');
    const slidesContainer = document.getElementById('slidesContainer');
    const slideSelect = document.getElementById('slideSelect');
    const slideCount = document.getElementById('slideCount');
    const zoomInBtn = document.getElementById('zoomIn');
    const zoomOutBtn = document.getElementById('zoomOut');
    const zoomResetBtn = document.getElementById('zoomReset');
    const zoomLevel = document.getElementById('zoomLevel');

    const dataTag = document.getElementById('presentation-data');
    let presentation = {};
    let parseError = null;
    try {
        const raw = (dataTag && dataTag.textContent) ? dataTag.textContent : '{}';
        if (raw.length > 60 * 1024 * 1024) {
            throw new Error('Presentation payload is too large to render safely.');
        }
        const parsed = JSON.parse(raw);
        presentation = normalizePresentation(parsed);
    } catch (err) {
        parseError = err instanceof Error ? err : new Error(String(err));
    }

    let currentScale = 1;
    const MIN_SCALE = 0.5;
    const MAX_SCALE = 2.5;
    const SCALE_STEP = 0.1;

    let pdfDoc = null;
    let renderToken = 0;

    const RENDER_TIMEOUT_MS = 30000;
    const PDFJS_LOAD_TIMEOUT_MS = 12000;

    function normalizePresentation(parsed) {
        if (!parsed || typeof parsed !== 'object') {
            return { mode: 'xml', slides: [], totalSlides: 0 };
        }
        const normalized = { ...parsed };
        if (normalized.mode !== 'pdf' && normalized.mode !== 'xml') {
            normalized.mode = 'xml';
        }
        if (!Array.isArray(normalized.slides)) {
            normalized.slides = [];
        }
        if (!Number.isFinite(normalized.totalSlides)) {
            normalized.totalSlides = normalized.mode === 'pdf'
                ? Number(normalized.totalSlides || 0)
                : normalized.slides.length;
        }
        return normalized;
    }

    function nextFrame() {
        return new Promise((resolve) => {
            requestAnimationFrame(() => resolve());
        });
    }

    async function withTimeout(promise, timeoutMs, message) {
        let timer = null;
        try {
            return await Promise.race([
                promise,
                new Promise((_, reject) => {
                    timer = setTimeout(() => reject(new Error(message)), timeoutMs);
                })
            ]);
        } finally {
            if (timer) clearTimeout(timer);
        }
    }

    function setError(message) {
        if (loading) loading.style.display = 'none';
        if (error) {
            error.textContent = message;
            error.style.display = 'block';
        }
    }

    function setEmptyState(message) {
        if (error) {
            error.style.display = 'none';
            error.textContent = '';
        }
        if (loading) {
            loading.textContent = message;
            loading.style.display = 'block';
        }
    }

    function updateZoomText() {
        if (zoomLevel) {
            zoomLevel.textContent = `${Math.round(currentScale * 100)}%`;
        }
    }

    function base64ToUint8Array(base64) {
        const binary = atob(base64);
        const bytes = new Uint8Array(binary.length);
        for (let i = 0; i < binary.length; i++) {
            bytes[i] = binary.charCodeAt(i);
        }
        return bytes;
    }

    function mapAlign(align) {
        if (align === 'ctr') return 'center';
        if (align === 'r') return 'right';
        if (align === 'just') return 'justify';
        return 'left';
    }

    function safeDimension(value, fallback) {
        const n = Number(value);
        return Number.isFinite(n) && n > 0 ? n : fallback;
    }

    function hexToRgb(hex) {
        const raw = (hex || '').replace('#', '').trim();
        if (raw.length === 3) {
            const r = parseInt(raw[0] + raw[0], 16);
            const g = parseInt(raw[1] + raw[1], 16);
            const b = parseInt(raw[2] + raw[2], 16);
            return Number.isNaN(r) ? null : { r, g, b };
        }
        if (raw.length === 6) {
            const r = parseInt(raw.slice(0, 2), 16);
            const g = parseInt(raw.slice(2, 4), 16);
            const b = parseInt(raw.slice(4, 6), 16);
            return Number.isNaN(r) ? null : { r, g, b };
        }
        return null;
    }

    function getReadableTextColor(bgColor) {
        const rgb = hexToRgb(bgColor);
        if (!rgb) return '#111111';
        const luminance = (0.299 * rgb.r + 0.587 * rgb.g + 0.114 * rgb.b) / 255;
        return luminance < 0.58 ? '#ffffff' : '#111111';
    }

    function createTextShape(element, fallbackColor) {
        const box = document.createElement('div');
        box.className = `shape-box text-shape ${element.isTitle ? 'title-shape' : ''}`.trim();
        if (element.textStylePreset) {
            box.dataset.textPreset = element.textStylePreset;
        }
        box.style.left = `${element.x}px`;
        box.style.top = `${element.y}px`;
        box.style.width = `${element.width}px`;
        box.style.height = `${element.height}px`;
        box.style.zIndex = String(element.zIndex || 0);
        if (element.rotateDeg) {
            box.style.transform = `rotate(${element.rotateDeg}deg)`;
            box.style.transformOrigin = 'top left';
        }
        if (element.fillColor) box.style.backgroundColor = element.fillColor;
        if (element.borderColor) box.style.border = `${Math.max(1, Number(element.borderWidthPx || 1))}px solid ${element.borderColor}`;

        const paragraphs = Array.isArray(element.paragraphs) ? element.paragraphs : [];
        const isLikelyList = !element.isTitle && paragraphs.length >= 3;
        paragraphs.forEach((paragraph) => {
            const p = document.createElement('p');
            p.className = 'text-line';
            p.style.marginLeft = `${Math.max(0, Number(paragraph.level || 0)) * 16}px`;
            p.style.textAlign = mapAlign(paragraph.align);
            const level = Math.max(0, Number(paragraph.level || 0));
            const inferredSize = element.isTitle ? 52 : Math.max(16, 24 - level * 2);
            const fontSize = paragraph.fontSizePx || inferredSize;
            p.style.fontSize = `${fontSize}px`;
            p.style.lineHeight = `${Math.max(18, Math.round(fontSize * 1.18))}px`;
            if (element.textStylePreset === 'cover-subtitle') {
                p.style.fontFamily = "'Malgun Gothic', 'Apple SD Gothic Neo', 'Noto Sans KR', sans-serif";
                p.style.fontWeight = '300';
                p.style.letterSpacing = '-1px';
                p.style.lineHeight = `${Math.max(20, Math.round(fontSize * 1.02))}px`;
            }
            if (element.textStylePreset === 'cover-title') {
                p.style.fontFamily = "'Malgun Gothic', 'Apple SD Gothic Neo', 'Noto Sans KR', sans-serif";
                p.style.fontWeight = '400';
                p.style.letterSpacing = '-2px';
                p.style.lineHeight = `${Math.max(24, Math.round(fontSize * 0.98))}px`;
            }

            if (paragraph.bold) p.style.fontWeight = '700';
            else if (element.textStylePreset === 'cover-subtitle') p.style.fontWeight = '300';
            else p.style.fontWeight = '400';
            if (paragraph.italic) p.style.fontStyle = 'italic';
            const paragraphColor = paragraph.color || fallbackColor || '#111111';
            p.style.color = paragraphColor;

            const showBullet = !!paragraph.bullet || (isLikelyList && !!(paragraph.text || '').trim());
            if (showBullet) {
                const bullet = document.createElement('span');
                bullet.textContent = '•';
                bullet.style.display = 'inline-block';
                bullet.style.width = '26px';
                bullet.style.marginRight = '6px';
                bullet.style.textAlign = 'center';
                bullet.style.color = paragraphColor;
                p.appendChild(bullet);
            }

            const runs = Array.isArray(paragraph.runs) ? paragraph.runs : [];
            if (runs.length > 0) {
                runs.forEach((run) => {
                    const span = document.createElement('span');
                    span.textContent = run.text || '';
                    if (run.fontSizePx) span.style.fontSize = `${run.fontSizePx}px`;
                    if (run.bold) span.style.fontWeight = '700';
                    if (run.italic) span.style.fontStyle = 'italic';
                    span.style.color = run.color || paragraphColor;
                    p.appendChild(span);
                });
            } else {
                p.appendChild(document.createTextNode(paragraph.text || ''));
            }
            box.appendChild(p);
        });

        box.style.overflow = 'hidden';

        requestAnimationFrame(() => {
            if (box.scrollHeight > box.clientHeight + 2) {
                const ratio = box.clientHeight / box.scrollHeight;
                const ps = box.querySelectorAll('p.text-line');
                ps.forEach((p) => {
                    const cur = parseFloat(p.style.fontSize) || 24;
                    const next = Math.max(9, Math.floor(cur * ratio));
                    p.style.fontSize = `${next}px`;
                    p.style.lineHeight = `${Math.max(12, Math.round(next * 1.18))}px`;
                });
            }
        });

        return box;
    }

    function createImageShape(element) {
        const box = document.createElement('div');
        box.className = 'shape-box image-shape';
        box.style.left = `${element.x}px`;
        box.style.top = `${element.y}px`;
        box.style.width = `${element.width}px`;
        box.style.height = `${element.height}px`;
        box.style.zIndex = String(element.zIndex || 0);
        if (element.rotateDeg) {
            box.style.transform = `rotate(${element.rotateDeg}deg)`;
            box.style.transformOrigin = 'top left';
        }

        const img = document.createElement('img');
        img.className = 'slide-image';
        img.src = element.src || '';
        img.alt = '';
        const srcRect = element.srcRect;
        if (srcRect && (srcRect.l || srcRect.t || srcRect.r || srcRect.b)) {
            const visibleW = Math.max(1, 100 - (srcRect.l || 0) - (srcRect.r || 0));
            const visibleH = Math.max(1, 100 - (srcRect.t || 0) - (srcRect.b || 0));
            const scaleW = 100 / visibleW;
            const scaleH = 100 / visibleH;
            img.style.position = 'absolute';
            img.style.width = `${100 * scaleW}%`;
            img.style.height = `${100 * scaleH}%`;
            img.style.left = `${-(srcRect.l || 0) * scaleW}%`;
            img.style.top = `${-(srcRect.t || 0) * scaleH}%`;
            img.style.objectFit = 'fill';
            img.style.maxWidth = 'none';
            img.style.maxHeight = 'none';
            box.style.overflow = 'hidden';
        }
        if (element.vectorFallback) {
            img.addEventListener('load', () => {
                try {
                    const normalized = normalizeVectorFallbackLogo(img);
                    if (normalized) {
                        img.src = normalized;
                    }
                } catch {
                    // Keep original image when post-processing fails.
                }
            }, { once: true });
        }
        box.appendChild(img);
        return box;
    }

    function normalizeVectorFallbackLogo(img) {
        const w = img.naturalWidth || img.width;
        const h = img.naturalHeight || img.height;
        if (!w || !h) return null;

        const canvas = document.createElement('canvas');
        canvas.width = w;
        canvas.height = h;
        const ctx = canvas.getContext('2d');
        if (!ctx) return null;

        ctx.drawImage(img, 0, 0, w, h);
        const imageData = ctx.getImageData(0, 0, w, h);
        const data = imageData.data;
        const target = { r: 237, g: 30, b: 68 };

        for (let i = 0; i < data.length; i += 4) {
            const r = data[i];
            const g = data[i + 1];
            const b = data[i + 2];
            const a = data[i + 3];
            if (a === 0) continue;

            const redDominant = r > 130 && r > g * 1.25 && r > b * 1.25;
            if (redDominant) {
                data[i + 3] = 0;
                continue;
            }

            const brightness = (r + g + b) / 3;
            const alphaRatio = Math.max(0, Math.min(1, brightness / 255));
            data[i] = target.r;
            data[i + 1] = target.g;
            data[i + 2] = target.b;
            data[i + 3] = Math.round(a * alphaRatio);
        }

        ctx.putImageData(imageData, 0, 0);
        return canvas.toDataURL('image/png');
    }

    function createTableShape(element) {
        const box = document.createElement('div');
        box.className = 'shape-box table-shape';
        box.style.left = `${element.x}px`;
        box.style.top = `${element.y}px`;
        box.style.width = `${element.width}px`;
        box.style.height = `${element.height}px`;
        box.style.zIndex = String(element.zIndex || 0);
        if (element.rotateDeg) {
            box.style.transform = `rotate(${element.rotateDeg}deg)`;
            box.style.transformOrigin = 'top left';
        }

        const table = document.createElement('table');
        const rows = Array.isArray(element.tableRows) ? element.tableRows : [];
        rows.forEach((row, rowIndex) => {
            const tr = document.createElement('tr');
            row.forEach((cell) => {
                const td = document.createElement(rowIndex === 0 ? 'th' : 'td');
                td.textContent = cell || '';
                tr.appendChild(td);
            });
            table.appendChild(tr);
        });
        box.appendChild(table);
        return box;
    }

    function createChartShape(element) {
        const box = document.createElement('div');
        box.className = `shape-box chart-shape ${element.chartData ? 'chart-shape-rendered' : ''}`.trim();
        box.style.left = `${element.x}px`;
        box.style.top = `${element.y}px`;
        box.style.width = `${element.width}px`;
        box.style.height = `${element.height}px`;
        box.style.zIndex = String(element.zIndex || 0);
        if (element.rotateDeg) {
            box.style.transform = `rotate(${element.rotateDeg}deg)`;
            box.style.transformOrigin = 'top left';
        }

        if (element.chartData && element.chartData.kind === 'stackedColumn') {
            const canvas = document.createElement('canvas');
            canvas.width = Math.max(300, Math.floor(element.width));
            canvas.height = Math.max(180, Math.floor(element.height));
            canvas.style.width = '100%';
            canvas.style.height = '100%';
            drawStackedColumnChart(canvas, element.chartData);
            box.appendChild(canvas);
        } else if (element.chartData && element.chartData.kind === 'line') {
            const canvas = document.createElement('canvas');
            canvas.width = Math.max(300, Math.floor(element.width));
            canvas.height = Math.max(180, Math.floor(element.height));
            canvas.style.width = '100%';
            canvas.style.height = '100%';
            drawLineChart(canvas, element.chartData);
            box.appendChild(canvas);
        } else {
            const title = document.createElement('div');
            title.className = 'chart-title';
            title.textContent = element.chartTitle || (element.chartKind === 'smartart' ? 'SmartArt' : 'Chart');
            const subtitle = document.createElement('div');
            subtitle.className = 'chart-subtitle';
            subtitle.textContent = element.chartKind === 'smartart' ? 'SmartArt placeholder' : 'Chart placeholder';
            box.appendChild(title);
            box.appendChild(subtitle);
        }
        return box;
    }

    function getDecimalPlaces(formatCode) {
        if (!formatCode) return 0;
        const match = formatCode.match(/0\.([0#]+)/);
        return match ? match[1].length : 0;
    }

    function formatChartValue(value, formatCode) {
        const decimals = getDecimalPlaces(formatCode);
        const isCurrency = typeof formatCode === 'string' && formatCode.includes('$');
        const useParensForNegative = typeof formatCode === 'string' && /;\s*\\?\(.*0/.test(formatCode);
        const abs = Math.abs(value);
        const numeric = abs.toLocaleString('en-US', {
            minimumFractionDigits: decimals,
            maximumFractionDigits: decimals
        });
        const prefixed = isCurrency ? `$${numeric}` : numeric;
        if (value < 0) {
            return useParensForNegative ? `(${prefixed})` : `-${prefixed}`;
        }
        return prefixed;
    }

    function getNiceStep(span, targetTicks) {
        const safeSpan = Math.max(0.000001, span);
        const rough = safeSpan / Math.max(1, targetTicks);
        const power = Math.pow(10, Math.floor(Math.log10(rough)));
        const normalized = rough / power;
        let unit = 1;
        if (normalized > 5) unit = 10;
        else if (normalized > 2) unit = 5;
        else if (normalized > 1) unit = 2;
        return unit * power;
    }

    function drawStackedColumnChart(canvas, chartData) {
        const ctx = canvas.getContext('2d');
        if (!ctx) return;

        const w = canvas.width;
        const h = canvas.height;
        const margin = { left: 74, right: 20, top: 20, bottom: 88 };
        const plotW = w - margin.left - margin.right;
        const plotH = h - margin.top - margin.bottom;
        if (plotW <= 20 || plotH <= 20) return;

        const categories = Array.isArray(chartData.categories) ? chartData.categories : [];
        const series = Array.isArray(chartData.series) ? chartData.series : [];
        if (categories.length === 0 || series.length === 0) return;

        const valueAxis = chartData.valueAxis || {};
        const categoryAxis = chartData.categoryAxis || {};
        const legend = chartData.legend || {};
        const axisFormat = valueAxis.numFmt || '$#,##0.0';
        const axisTextColor = valueAxis.color || '#222222';
        const axisFontSize = Math.max(10, Number(valueAxis.fontSizePx || 11));
        const categoryTextColor = categoryAxis.color || '#222222';
        const categoryFontSize = Math.max(10, Number(categoryAxis.fontSizePx || 11));

        const sums = categories.map((_, idx) =>
            series.reduce((acc, s) => acc + Math.max(0, Number((s.values || [])[idx] || 0)), 0)
        );
        const mins = categories.map((_, idx) =>
            series.reduce((acc, s) => acc + Math.min(0, Number((s.values || [])[idx] || 0)), 0)
        );

        const axisMin = Number(valueAxis.min);
        const axisMax = Number(valueAxis.max);
        let minValue = Number.isFinite(axisMin) ? axisMin : Math.min(0, ...mins);
        let maxValue = Number.isFinite(axisMax) ? axisMax : Math.max(1, ...sums);
        if (minValue >= maxValue) {
            maxValue = minValue + 1;
        }
        const span = maxValue - minValue;
        const yFor = (v) => margin.top + ((maxValue - v) / span) * plotH;
        const zeroY = yFor(Number.isFinite(valueAxis.crossesAt) ? valueAxis.crossesAt : 0);
        const yStep = Number.isFinite(valueAxis.majorUnit) && valueAxis.majorUnit > 0
            ? valueAxis.majorUnit
            : getNiceStep(span, 8);

        ctx.clearRect(0, 0, w, h);

        ctx.strokeStyle = valueAxis.lineColor || '#4f81bd';
        ctx.lineWidth = 1;
        ctx.strokeRect(margin.left, margin.top, plotW, plotH);

        ctx.strokeStyle = valueAxis.gridColor || 'rgba(0,0,0,0.22)';
        ctx.lineWidth = 1;
        let tick = Math.floor(minValue / yStep) * yStep;
        let safeTickCount = 0;
        while (tick <= maxValue + yStep * 0.5 && safeTickCount < 250) {
            const y = yFor(tick);
            ctx.beginPath();
            ctx.moveTo(margin.left, y);
            ctx.lineTo(margin.left + plotW, y);
            ctx.stroke();

            ctx.fillStyle = axisTextColor;
            ctx.font = `400 ${axisFontSize}px Arial, sans-serif`;
            ctx.textAlign = 'right';
            ctx.textBaseline = 'middle';
            ctx.fillText(formatChartValue(tick, axisFormat), margin.left - 12, y);

            tick += yStep;
            safeTickCount += 1;
        }

        ctx.strokeStyle = '#111111';
        ctx.lineWidth = 1.5;
        ctx.beginPath();
        ctx.moveTo(margin.left, zeroY);
        ctx.lineTo(margin.left + plotW, zeroY);
        ctx.stroke();

        const groupW = plotW / categories.length;
        const gapWidth = Number(chartData.gapWidth);
        const gapScale = Number.isFinite(gapWidth) ? Math.max(0.2, Math.min(0.9, 1 - gapWidth / 300)) : 0.58;
        const barW = Math.max(8, groupW * gapScale);

        categories.forEach((cat, idx) => {
            const cx = margin.left + groupW * idx + groupW / 2;
            let posBase = 0;
            let negBase = 0;

            series.forEach((s) => {
                const raw = Number((s.values || [])[idx] || 0);
                if (!Number.isFinite(raw) || raw === 0) return;

                let from;
                let to;
                if (raw >= 0) {
                    from = yFor(posBase);
                    posBase += raw;
                    to = yFor(posBase);
                } else {
                    from = yFor(negBase);
                    negBase += raw;
                    to = yFor(negBase);
                }
                const top = Math.min(from, to);
                const height = Math.max(1, Math.abs(from - to));

                ctx.fillStyle = s.color || '#d10000';
                ctx.fillRect(cx - barW / 2, top, barW, height);

                const dataLabel = s.dataLabel || {};
                const showValue = dataLabel.showValue !== false;
                if (showValue && Math.abs(raw) >= 0.1 && height > 12) {
                    const labelFmt = dataLabel.numFmt || s.valueFormat || axisFormat;
                    const labelColor = dataLabel.color || '#ffffff';
                    const labelSize = Math.max(9, Number(dataLabel.fontSizePx || 11));
                    ctx.fillStyle = labelColor;
                    ctx.font = `400 ${labelSize}px Arial, sans-serif`;
                    ctx.textAlign = 'center';
                    ctx.textBaseline = 'middle';
                    ctx.fillText(formatChartValue(raw, labelFmt), cx, top + height / 2);
                }
            });

            ctx.fillStyle = categoryTextColor;
            ctx.font = `400 ${categoryFontSize}px Arial, sans-serif`;
            ctx.textAlign = 'center';
            ctx.textBaseline = 'alphabetic';
            const parts = String(cat).split(' ');
            if (parts.length > 1) {
                ctx.fillText(parts[0], cx, h - 30);
                ctx.fillText(parts.slice(1).join(' '), cx, h - 14);
            } else {
                ctx.fillText(cat, cx, h - 16);
            }
        });

        const legendFontSize = Math.max(10, Number(legend.fontSizePx || 12));
        const legendTextColor = legend.color || '#222222';
        const legendItems = series.map((s, i) => ({
            color: s.color || '#999999',
            label: s.name || `Series ${i + 1}`
        }));
        const legendY = h - 6;
        const itemGap = 22;
        const swatchW = 10;
        const contentWidth = legendItems.reduce((acc, item) => acc + swatchW + 6 + item.label.length * (legendFontSize * 0.55) + itemGap, 0);
        let cursorX = Math.max(8, (w - contentWidth) / 2);

        ctx.textAlign = 'left';
        ctx.textBaseline = 'alphabetic';
        ctx.font = `400 ${legendFontSize}px Arial, sans-serif`;
        legendItems.forEach((item) => {
            ctx.fillStyle = item.color;
            ctx.fillRect(cursorX, legendY - 10, swatchW, swatchW);
            cursorX += swatchW + 6;
            ctx.fillStyle = legendTextColor;
            ctx.fillText(item.label, cursorX, legendY - 1);
            cursorX += item.label.length * (legendFontSize * 0.55) + itemGap;
        });
    }

    function drawLineChart(canvas, chartData) {
        const ctx = canvas.getContext('2d');
        if (!ctx) return;

        const w = canvas.width;
        const h = canvas.height;
        const margin = { left: 74, right: 20, top: 20, bottom: 88 };
        const plotW = w - margin.left - margin.right;
        const plotH = h - margin.top - margin.bottom;
        if (plotW <= 20 || plotH <= 20) return;

        const categories = Array.isArray(chartData.categories) ? chartData.categories : [];
        const series = Array.isArray(chartData.series) ? chartData.series : [];
        if (categories.length === 0 || series.length === 0) return;

        const valueAxis = chartData.valueAxis || {};
        const categoryAxis = chartData.categoryAxis || {};
        const legend = chartData.legend || {};
        const axisFormat = valueAxis.numFmt || '0.0';
        const axisTextColor = valueAxis.color || '#222222';
        const axisFontSize = Math.max(10, Number(valueAxis.fontSizePx || 11));
        const categoryTextColor = categoryAxis.color || '#222222';
        const categoryFontSize = Math.max(10, Number(categoryAxis.fontSizePx || 11));
        const numericValues = series.flatMap((s) => Array.isArray(s.values) ? s.values : []).filter((v) => Number.isFinite(v));
        if (numericValues.length === 0) return;

        const axisMin = Number(valueAxis.min);
        const axisMax = Number(valueAxis.max);
        let minValue = Number.isFinite(axisMin) ? axisMin : Math.min(0, ...numericValues);
        let maxValue = Number.isFinite(axisMax) ? axisMax : Math.max(...numericValues);
        if (minValue >= maxValue) {
            const pad = Math.max(1, Math.abs(maxValue || 1) * 0.1);
            minValue -= pad;
            maxValue += pad;
        }

        const span = maxValue - minValue;
        const yFor = (v) => margin.top + ((maxValue - v) / span) * plotH;
        const zeroY = yFor(Number.isFinite(valueAxis.crossesAt) ? valueAxis.crossesAt : 0);
        const yStep = Number.isFinite(valueAxis.majorUnit) && valueAxis.majorUnit > 0
            ? valueAxis.majorUnit
            : getNiceStep(span, 8);
        const xForIndex = (idx) => (
            categories.length === 1
                ? margin.left + plotW / 2
                : margin.left + (plotW / Math.max(1, categories.length - 1)) * idx
        );

        ctx.clearRect(0, 0, w, h);

        ctx.strokeStyle = valueAxis.gridColor || 'rgba(0,0,0,0.18)';
        ctx.lineWidth = 1;
        let tick = Math.floor(minValue / yStep) * yStep;
        let safeTickCount = 0;
        while (tick <= maxValue + yStep * 0.5 && safeTickCount < 250) {
            const y = yFor(tick);
            ctx.beginPath();
            ctx.moveTo(margin.left, y);
            ctx.lineTo(margin.left + plotW, y);
            ctx.stroke();

            ctx.fillStyle = axisTextColor;
            ctx.font = `400 ${axisFontSize}px Arial, sans-serif`;
            ctx.textAlign = 'right';
            ctx.textBaseline = 'middle';
            ctx.fillText(formatChartValue(tick, axisFormat), margin.left - 12, y);

            tick += yStep;
            safeTickCount += 1;
        }

        ctx.strokeStyle = valueAxis.lineColor || '#111111';
        ctx.lineWidth = 1.5;
        ctx.beginPath();
        ctx.moveTo(margin.left, zeroY);
        ctx.lineTo(margin.left + plotW, zeroY);
        ctx.stroke();

        ctx.beginPath();
        ctx.moveTo(margin.left, margin.top);
        ctx.lineTo(margin.left, margin.top + plotH);
        ctx.lineTo(margin.left + plotW, margin.top + plotH);
        ctx.stroke();

        categories.forEach((cat, idx) => {
            const x = xForIndex(idx);
            ctx.fillStyle = categoryTextColor;
            ctx.font = `400 ${categoryFontSize}px Arial, sans-serif`;
            ctx.textAlign = 'center';
            ctx.textBaseline = 'alphabetic';
            const parts = String(cat).split(' ');
            if (parts.length > 1) {
                ctx.fillText(parts[0], x, h - 30);
                ctx.fillText(parts.slice(1).join(' '), x, h - 14);
            } else {
                ctx.fillText(cat, x, h - 16);
            }
        });

        series.forEach((s, seriesIndex) => {
            const values = Array.isArray(s.values) ? s.values : [];
            const points = values
                .map((raw, idx) => ({ raw: Number(raw), idx }))
                .filter((point) => Number.isFinite(point.raw));
            if (points.length === 0) return;

            ctx.strokeStyle = s.color || '#4472c4';
            ctx.lineWidth = 3;
            ctx.beginPath();
            points.forEach((point, idx) => {
                const x = xForIndex(point.idx);
                const y = yFor(point.raw);
                if (idx === 0) ctx.moveTo(x, y);
                else ctx.lineTo(x, y);
            });
            ctx.stroke();

            points.forEach((point) => {
                const x = xForIndex(point.idx);
                const y = yFor(point.raw);

                ctx.fillStyle = '#ffffff';
                ctx.beginPath();
                ctx.arc(x, y, 4.5, 0, Math.PI * 2);
                ctx.fill();
                ctx.strokeStyle = s.color || '#4472c4';
                ctx.lineWidth = 2;
                ctx.beginPath();
                ctx.arc(x, y, 4.5, 0, Math.PI * 2);
                ctx.stroke();

                const dataLabel = s.dataLabel || {};
                const showValue = dataLabel.showValue !== false;
                if (!showValue) return;

                const labelFmt = dataLabel.numFmt || s.valueFormat || axisFormat;
                const labelColor = dataLabel.color || s.color || '#222222';
                const labelSize = Math.max(9, Number(dataLabel.fontSizePx || 11));
                const labelY = y < margin.top + 20 ? y + 18 + (seriesIndex * 2) : y - 10 - (seriesIndex * 2);
                ctx.fillStyle = labelColor;
                ctx.font = `600 ${labelSize}px Arial, sans-serif`;
                ctx.textAlign = 'center';
                ctx.textBaseline = y < margin.top + 20 ? 'top' : 'bottom';
                ctx.fillText(formatChartValue(point.raw, labelFmt), x, labelY);
            });
        });

        const legendFontSize = Math.max(10, Number(legend.fontSizePx || 12));
        const legendTextColor = legend.color || '#222222';
        const legendItems = series.map((s, i) => ({
            color: s.color || '#999999',
            label: s.name || `Series ${i + 1}`
        }));
        const legendY = h - 6;
        const itemGap = 22;
        const swatchW = 18;
        const contentWidth = legendItems.reduce((acc, item) => acc + swatchW + 6 + item.label.length * (legendFontSize * 0.55) + itemGap, 0);
        let cursorX = Math.max(8, (w - contentWidth) / 2);

        ctx.textAlign = 'left';
        ctx.textBaseline = 'alphabetic';
        ctx.font = `400 ${legendFontSize}px Arial, sans-serif`;
        legendItems.forEach((item) => {
            ctx.strokeStyle = item.color;
            ctx.lineWidth = 2.5;
            ctx.beginPath();
            ctx.moveTo(cursorX, legendY - 5);
            ctx.lineTo(cursorX + swatchW, legendY - 5);
            ctx.stroke();
            ctx.fillStyle = '#ffffff';
            ctx.beginPath();
            ctx.arc(cursorX + swatchW / 2, legendY - 5, 3.5, 0, Math.PI * 2);
            ctx.fill();
            ctx.strokeStyle = item.color;
            ctx.stroke();

            cursorX += swatchW + 6;
            ctx.fillStyle = legendTextColor;
            ctx.fillText(item.label, cursorX, legendY - 1);
            cursorX += item.label.length * (legendFontSize * 0.55) + itemGap;
        });
    }

    function createGenericShape(element) {
        const box = document.createElement('div');
        box.className = 'shape-box generic-shape';
        box.style.left = `${element.x}px`;
        box.style.top = `${element.y}px`;
        box.style.width = `${element.width}px`;
        box.style.height = `${element.height}px`;
        box.style.zIndex = String(element.zIndex || 0);
        if (element.rotateDeg) {
            box.style.transform = `rotate(${element.rotateDeg}deg)`;
            box.style.transformOrigin = 'top left';
        }

        const preset = element.presetGeom;
        // Render line connectors as SVG with optional arrowheads/circles.
        if (preset === 'line' || preset === 'straightConnector1') {
            const w = element.width || 0;
            const h = element.height || 0;
            const svgW = Math.max(1, Math.abs(w));
            const svgH = Math.max(1, Math.abs(h));
            const svgNS = 'http://www.w3.org/2000/svg';
            const svg = document.createElementNS(svgNS, 'svg');
            svg.setAttribute('width', String(svgW));
            svg.setAttribute('height', String(svgH));
            svg.setAttribute('viewBox', `0 0 ${svgW} ${svgH}`);
            svg.style.display = 'block';
            svg.style.overflow = 'visible';
            svg.style.position = 'absolute';
            svg.style.left = '0';
            svg.style.top = '0';

            const defs = document.createElementNS(svgNS, 'defs');
            const strokeColor = element.borderColor || '#000000';
            const strokeW = Math.max(1, Number(element.borderWidthPx || 1));

            // Arrow marker
            const arrowMarker = document.createElementNS(svgNS, 'marker');
            arrowMarker.setAttribute('id', `arrow-${element.zIndex}`);
            arrowMarker.setAttribute('markerWidth', '8');
            arrowMarker.setAttribute('markerHeight', '6');
            arrowMarker.setAttribute('refX', '8');
            arrowMarker.setAttribute('refY', '3');
            arrowMarker.setAttribute('orient', 'auto');
            arrowMarker.setAttribute('markerUnits', 'strokeWidth');
            const arrowPath = document.createElementNS(svgNS, 'path');
            arrowPath.setAttribute('d', 'M 0 0 L 8 3 L 0 6 Z');
            arrowPath.setAttribute('fill', strokeColor);
            arrowMarker.appendChild(arrowPath);
            defs.appendChild(arrowMarker);

            // Circle marker
            const circleMarker = document.createElementNS(svgNS, 'marker');
            circleMarker.setAttribute('id', `circle-${element.zIndex}`);
            circleMarker.setAttribute('markerWidth', '6');
            circleMarker.setAttribute('markerHeight', '6');
            circleMarker.setAttribute('refX', '3');
            circleMarker.setAttribute('refY', '3');
            circleMarker.setAttribute('orient', 'auto');
            circleMarker.setAttribute('markerUnits', 'strokeWidth');
            const circle = document.createElementNS(svgNS, 'circle');
            circle.setAttribute('cx', '3');
            circle.setAttribute('cy', '3');
            circle.setAttribute('r', '2.5');
            circle.setAttribute('fill', strokeColor);
            circleMarker.appendChild(circle);
            defs.appendChild(circleMarker);

            svg.appendChild(defs);

            const line = document.createElementNS(svgNS, 'line');
            // flipH/flipV reverse the line direction within its bounding box.
            const fH = !!element.flipH;
            const fV = !!element.flipV;
            line.setAttribute('x1', String(fH ? svgW : 0));
            line.setAttribute('y1', String(fV ? svgH : 0));
            line.setAttribute('x2', String(fH ? 0 : svgW));
            line.setAttribute('y2', String(fV ? 0 : svgH));
            line.setAttribute('stroke', strokeColor);
            line.setAttribute('stroke-width', String(strokeW));

            function markerUrl(type, prefix) {
                if (type === 'arrow' || type === 'triangle' || type === 'stealth') return `url(#arrow-${element.zIndex})`;
                if (type === 'oval' || type === 'diamond') return `url(#circle-${element.zIndex})`;
                return '';
            }
            if (element.headEnd) {
                const url = markerUrl(element.headEnd);
                if (url) line.setAttribute('marker-start', url);
            }
            if (element.tailEnd) {
                const url = markerUrl(element.tailEnd);
                if (url) line.setAttribute('marker-end', url);
            }
            // If no explicit markers, default to arrow at tail end for non-decoration lines.
            if (!element.headEnd && !element.tailEnd && svgW + svgH > 20) {
                line.setAttribute('marker-end', `url(#arrow-${element.zIndex})`);
            }

            svg.appendChild(line);
            box.appendChild(svg);
            box.style.overflow = 'visible';
            return box;
        }

        // Render known preset shapes as SVG.
        const svgPath = preset ? presetGeomToSvgPath(preset, element.width, element.height) : null;
        const resolvedPath = element.customSvgPath || svgPath;
        if (resolvedPath) {
            const svgNS = 'http://www.w3.org/2000/svg';
            const svg = document.createElementNS(svgNS, 'svg');
            svg.setAttribute('width', String(element.width));
            svg.setAttribute('height', String(element.height));
            svg.setAttribute('viewBox', `0 0 ${element.width} ${element.height}`);
            svg.style.display = 'block';
            svg.style.overflow = 'visible';
            const path = document.createElementNS(svgNS, 'path');
            path.setAttribute('d', resolvedPath);
            path.setAttribute('fill', element.fillColor || 'none');
            path.setAttribute('stroke', element.borderColor || 'none');
            path.setAttribute('stroke-width', String(Math.max(1, Number(element.borderWidthPx || 1))));
            svg.appendChild(path);
            box.appendChild(svg);
            return box;
        }

        if (element.fillColor) box.style.backgroundColor = element.fillColor;
        if (element.borderColor) box.style.border = `${Math.max(1, Number(element.borderWidthPx || 1))}px solid ${element.borderColor}`;
        return box;
    }

    function presetGeomToSvgPath(preset, w, h) {
        if (!w || w < 0 || !h || h < 0) return null;
        switch (preset) {
            case 'rightArrow': {
                const bodyH = h * 0.6;
                const bodyTop = (h - bodyH) / 2;
                const headW = Math.min(w * 0.35, h * 0.9);
                const bodyRight = w - headW;
                return `M 0 ${bodyTop} L ${bodyRight} ${bodyTop} L ${bodyRight} 0 L ${w} ${h / 2} L ${bodyRight} ${h} L ${bodyRight} ${bodyTop + bodyH} L 0 ${bodyTop + bodyH} Z`;
            }
            case 'leftArrow': {
                const bodyH = h * 0.6;
                const bodyTop = (h - bodyH) / 2;
                const headW = Math.min(w * 0.35, h * 0.9);
                return `M ${w} ${bodyTop} L ${headW} ${bodyTop} L ${headW} 0 L 0 ${h / 2} L ${headW} ${h} L ${headW} ${bodyTop + bodyH} L ${w} ${bodyTop + bodyH} Z`;
            }
            case 'upArrow': {
                const bodyW = w * 0.6;
                const bodyLeft = (w - bodyW) / 2;
                const headH = Math.min(h * 0.35, w * 0.9);
                return `M ${bodyLeft} ${h} L ${bodyLeft} ${headH} L 0 ${headH} L ${w / 2} 0 L ${w} ${headH} L ${bodyLeft + bodyW} ${headH} L ${bodyLeft + bodyW} ${h} Z`;
            }
            case 'downArrow': {
                const bodyW = w * 0.6;
                const bodyLeft = (w - bodyW) / 2;
                const headH = Math.min(h * 0.35, w * 0.9);
                const bodyBottom = h - headH;
                return `M ${bodyLeft} 0 L ${bodyLeft + bodyW} 0 L ${bodyLeft + bodyW} ${bodyBottom} L ${w} ${bodyBottom} L ${w / 2} ${h} L 0 ${bodyBottom} L ${bodyLeft} ${bodyBottom} Z`;
            }
            case 'roundRect': {
                const r = Math.min(w, h) * 0.12;
                return `M ${r} 0 L ${w - r} 0 Q ${w} 0 ${w} ${r} L ${w} ${h - r} Q ${w} ${h} ${w - r} ${h} L ${r} ${h} Q 0 ${h} 0 ${h - r} L 0 ${r} Q 0 0 ${r} 0 Z`;
            }
            case 'ellipse':
            case 'oval':
                return `M ${w / 2} 0 A ${w / 2} ${h / 2} 0 1 0 ${w / 2} ${h} A ${w / 2} ${h / 2} 0 1 0 ${w / 2} 0 Z`;
            case 'triangle':
                return `M ${w / 2} 0 L ${w} ${h} L 0 ${h} Z`;
            default:
                return null;
        }
    }

    function createXmlSlideElement(slide, index) {
        const slideEl = document.createElement('article');
        slideEl.className = 'slide';
        slideEl.dataset.page = String(index + 1);

        const header = document.createElement('div');
        header.className = 'slide-header';
        const stageWidth = Math.max(320, safeDimension(slide.widthPx, 1280));
        const stageHeight = Math.max(240, safeDimension(slide.heightPx, 720));
        header.textContent = `Slide ${slide.slideNumber || index + 1}`;

        const stage = document.createElement('div');
        stage.className = 'slide-stage';
        stage.style.width = `${stageWidth}px`;
        stage.style.setProperty('height', `${stageHeight}px`, 'important');
        stage.style.setProperty('min-height', `${stageHeight}px`, 'important');
        stage.style.setProperty('display', 'block', 'important');
        stage.style.setProperty('position', 'relative', 'important');
        slideEl.style.setProperty('min-height', `${stageHeight + 56}px`, 'important');
        stage.style.backgroundColor = slide.backgroundColor || '#ffffff';
        stage.style.border = '1px solid rgba(255,255,255,0.15)';
        const fallbackTextColor = getReadableTextColor(slide.backgroundColor || '#ffffff');

        const elements = Array.isArray(slide.elements) ? [...slide.elements] : [];

        // Promote full-size background images from negative z-index to 0 so
        // they render as visible child elements above the parent background.
        elements.forEach((el) => {
            if (
                el.type === 'image' && el.src
                && el.width >= stageWidth * 0.8 && el.height >= stageHeight * 0.8
                && (el.zIndex || 0) < 0
            ) {
                el.zIndex = 0;
            }
        });

        elements.sort((a, b) => (a.zIndex || 0) - (b.zIndex || 0));

        elements.forEach((element) => {
            if (element.type === 'image') stage.appendChild(createImageShape(element));
            else if (element.type === 'text') stage.appendChild(createTextShape(element, fallbackTextColor));
            else if (element.type === 'table') stage.appendChild(createTableShape(element));
            else if (element.type === 'chart') stage.appendChild(createChartShape(element));
            else stage.appendChild(createGenericShape(element));
        });

        if (elements.length === 0) {
            const fallback = document.createElement('div');
            fallback.className = 'no-content';
            fallback.textContent = '(No visible slide elements)';
            stage.appendChild(fallback);
        }

        stage.style.transform = `scale(${currentScale})`;
        stage.style.transformOrigin = 'top left';

        slideEl.appendChild(header);
        slideEl.appendChild(stage);
        return slideEl;
    }

    async function renderXmlSlides(token) {
        const slides = Array.isArray(presentation.slides) ? presentation.slides : [];
        slidesContainer.querySelectorAll('.slide').forEach((el) => el.remove());
        for (let index = 0; index < slides.length; index++) {
            if (token !== renderToken) {
                return false;
            }
            const slide = slides[index];
            slidesContainer.appendChild(createXmlSlideElement(slide, index));
            if (loading && slides.length > 0) {
                loading.textContent = `Rendering slides... (${index + 1}/${slides.length})`;
            }
            if ((index + 1) % 2 === 0) {
                await nextFrame();
            }
        }
        return true;
    }

    async function renderPdfSlides(token) {
        if (!pdfDoc) return false;
        slidesContainer.querySelectorAll('.slide').forEach((el) => el.remove());

        for (let pageNum = 1; pageNum <= pdfDoc.numPages; pageNum++) {
            if (token !== renderToken) {
                return false;
            }
            const page = await pdfDoc.getPage(pageNum);
            const viewport = page.getViewport({ scale: currentScale });

            const slide = document.createElement('article');
            slide.className = 'slide';
            slide.dataset.page = String(pageNum);

            const header = document.createElement('div');
            header.className = 'slide-header';
            header.textContent = `Slide ${pageNum}`;

            const canvas = document.createElement('canvas');
            canvas.className = 'slide-canvas';
            canvas.width = Math.floor(viewport.width);
            canvas.height = Math.floor(viewport.height);

            const context = canvas.getContext('2d');
            if (!context) continue;
            await page.render({ canvasContext: context, viewport }).promise;

            slide.appendChild(header);
            slide.appendChild(canvas);
            slidesContainer.appendChild(slide);
            if (loading && pdfDoc.numPages > 0) {
                loading.textContent = `Rendering slides... (${pageNum}/${pdfDoc.numPages})`;
            }
            if (pageNum % 2 === 0) {
                await nextFrame();
            }
        }
        return true;
    }

    async function renderSlides() {
        const token = ++renderToken;
        const totalPages = presentation.mode === 'pdf'
            ? Number(presentation.totalSlides || 0)
            : (Array.isArray(presentation.slides) ? presentation.slides.length : 0);

        if (totalPages === 0) {
            slidesContainer.querySelectorAll('.slide').forEach((el) => el.remove());
            setEmptyState('This PowerPoint file contains 0 pages. It appears to be a template or a master/layout-only file.');
            return;
        }

        const renderStartedAt = performance.now();
        if (loading) {
            loading.style.display = 'block';
            loading.textContent = 'Rendering slides...';
        }
        console.log(`[PPT] render start token=${token} mode=${presentation.mode} scale=${currentScale.toFixed(2)}`);
        const finished = await withTimeout(
            presentation.mode === 'pdf' ? renderPdfSlides(token) : renderXmlSlides(token),
            RENDER_TIMEOUT_MS,
            'Slide rendering timed out. Please reopen the file.'
        );
        const elapsed = Math.round(performance.now() - renderStartedAt);
        console.log(`[PPT] render end token=${token} finished=${Boolean(finished)} elapsed=${elapsed}ms`);
        if (token === renderToken && finished && loading) {
            loading.style.display = 'none';
        }
    }

    function populateSlideSelect(totalPages) {
        if (!slideSelect) return;
        slideSelect.innerHTML = '';
        slideSelect.disabled = totalPages === 0;

        for (let i = 1; i <= totalPages; i++) {
            const option = document.createElement('option');
            option.value = String(i);
            option.textContent = `Slide ${i}`;
            slideSelect.appendChild(option);
        }

        if (totalPages === 0) {
            const option = document.createElement('option');
            option.value = '';
            option.textContent = '0 pages';
            slideSelect.appendChild(option);
        }

        if (slideCount) {
            slideCount.textContent = totalPages === 0 ? '0 pages' : `${totalPages} slides`;
        }
    }

    function jumpToSlide(pageNum) {
        const target = document.querySelector(`.slide[data-page="${pageNum}"]`);
        if (target) {
            target.scrollIntoView({ behavior: 'smooth', block: 'start' });
        }
    }

    async function changeZoom(nextScale) {
        currentScale = Math.min(MAX_SCALE, Math.max(MIN_SCALE, nextScale));
        updateZoomText();
        await renderSlides();
    }

    function bindEvents() {
        if (zoomInBtn) zoomInBtn.addEventListener('click', () => void changeZoom(currentScale + SCALE_STEP));
        if (zoomOutBtn) zoomOutBtn.addEventListener('click', () => void changeZoom(currentScale - SCALE_STEP));
        if (zoomResetBtn) zoomResetBtn.addEventListener('click', () => void changeZoom(1));

        if (slideSelect) {
            slideSelect.addEventListener('change', (event) => {
                const target = event.target;
                if (target && target.value) jumpToSlide(Number(target.value));
            });
        }

        document.addEventListener('keydown', (event) => {
            if (event.ctrlKey && (event.key === '+' || event.key === '=')) {
                event.preventDefault();
                void changeZoom(currentScale + SCALE_STEP);
            } else if (event.ctrlKey && event.key === '-') {
                event.preventDefault();
                void changeZoom(currentScale - SCALE_STEP);
            } else if (event.ctrlKey && event.key === '0') {
                event.preventDefault();
                void changeZoom(1);
            }
        });
    }

    async function initPdfMode() {
        if (!presentation.pdfBase64) {
            throw new Error('No converted PDF data found for this .ppt file.');
        }
        const pdfjsLib = await getPdfJsLib();
        const bytes = base64ToUint8Array(presentation.pdfBase64);
        const loadingTask = pdfjsLib.getDocument({ data: bytes, isEvalSupported: false });
        pdfDoc = await loadingTask.promise;
        populateSlideSelect(pdfDoc.numPages);
    }

    async function getPdfJsLib() {
        const ready = window.__OMNI_PDFJS_READY__;
        if (!ready || typeof ready.then !== 'function') {
            throw new Error('pdf.js loader was not initialized.');
        }

        const pdfjsLib = await withTimeout(
            ready,
            PDFJS_LOAD_TIMEOUT_MS,
            'Timed out while loading pdf.js from the packaged extension assets.'
        );
        if (!pdfjsLib) {
            throw new Error('pdf.js failed to initialize.');
        }
        return pdfjsLib;
    }

    function initXmlMode() {
        const slides = Array.isArray(presentation.slides) ? presentation.slides : [];
        populateSlideSelect(slides.length);
    }

    async function init() {
        try {
            if (parseError) {
                throw parseError;
            }
            if (presentation.mode === 'pdf') {
                await initPdfMode();
            } else {
                initXmlMode();
            }

            bindEvents();
            await renderSlides();
            updateZoomText();
        } catch (err) {
            const message = err instanceof Error ? err.message : String(err);
            setError(message);
        }
    }

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