Pollinations.ai Enhancer

Enhanced markdown formatting for pollinations.ai with better readability, and smoother viewing

目前為 2025-04-27 提交的版本,檢視 最新版本

// ==UserScript==
// @name         Pollinations.ai Enhancer
// @namespace    https://greasyfork.org/en/users/1462897-fisventurous
// @version      1.9.3
// @description  Enhanced markdown formatting for pollinations.ai with better readability, and smoother viewing
// @author       fisven
// @match        *://*.pollinations.ai/*
// @connect      *
// @grant        GM_addStyle
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_xmlhttpRequest
// @grant        unsafeWindow
// @run-at       document-start
// @license MIT
// ==/UserScript==

(function() {
    'use strict';

    const THEME_KEY = 'pollinations_enhancer_theme';
    const FONT_SIZE_KEY = 'pollinations_enhancer_fontsize';

    let observer = null;

    // wait for page load or run immediately
    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', init);
    } else {
        init();
    }

    function init() {
        console.log("Pollinations Enhancer v1.9.2 Initialising...");
        addStyles();
        applyTheme();
        applyFontSize();

        const pageType = detectPageType();
        setupLinkPreviews();

        if (pageType.isText) enhanceTextPage();
        else if (pageType.isImage) enhanceImagePage();
        else if (pageType.isAudio) enhanceAudioPage();
        else createCommonButtons(extractUrlParameters(), 'unknown'); // fallback for unknown

        if (pageType.isText) {
            updateThemeToggleButton(document.body.classList.contains('theme-dark'));
        }

        startObserver(pageType);
    }

    function detectPageType() {
        const url = window.location.href.toLowerCase();
        const urlParams = new URLSearchParams(window.location.search);

        let isImage = false;
        let isAudio = false;
        let isText = false;

        // check URL first
        if (url.includes('image.pollinations.ai') || url.includes('/image/')) {
            isImage = true;
        } else if (url.includes('audio') || url.match(/\.(mp3|wav|ogg|m4a)(\?|$)/i)) {
            isAudio = true;
        } else if (url.includes('text.pollinations.ai')) {
            isText = true;
        }

        // check URL params if type unknown
        if (!isImage && !isAudio && !isText) {
            const model = urlParams.get('model') || '';
            const hasVoice = urlParams.has('voice');

            if (model.includes('audio') || hasVoice) {
                isAudio = true;
            } else if (model.includes('image')) {
                isImage = true;
            }
        }

        // check page content as a last resort
        if (!isImage && !isAudio && !isText) {
            if (document.querySelector('img:not([width="16"][height="16"])')) {
                isImage = true; // found a significant image
            } else if (document.querySelector('audio, video, [type*="audio"]')) {
                isAudio = true; // found audio/video elements
            } else {
                isText = true; // assume text otherwise
            }
        }

        return { isText, isImage, isAudio };
    }

    function startObserver(pageType) {
        if (observer) observer.disconnect();

        observer = new MutationObserver((mutations) => {
            for (const mutation of mutations) {
                if (mutation.type === 'childList' && mutation.addedNodes.length) {
                    // try re-enhancing if media loads dynamically
                    if (pageType.isImage && !document.getElementById('save-image-btn')) {
                        const newImages = Array.from(mutation.addedNodes)
                            .filter(node => node.tagName === 'IMG' || (node.querySelectorAll && node.querySelectorAll('img').length));
                        if (newImages.length) setTimeout(enhanceImagePage, 100);
                    }

                    if (pageType.isAudio && !document.getElementById('save-audio-btn')) {
                        const newAudio = Array.from(mutation.addedNodes)
                            .filter(node =>
                                node.tagName === 'AUDIO' ||
                                node.tagName === 'VIDEO' ||
                                (node.querySelectorAll && (
                                    node.querySelectorAll('audio').length ||
                                    node.querySelectorAll('video').length ||
                                    node.querySelectorAll('source[type*="audio"]').length
                                ))
                            );
                        if (newAudio.length) setTimeout(enhanceAudioPage, 100);
                    }
                }
            }
        });

        observer.observe(document.body, { childList: true, subtree: true });
    }

    function applyTheme() {
        const savedTheme = GM_getValue(THEME_KEY, 'theme-dark'); // default dark
        document.body.classList.remove('theme-dark', 'theme-light');
        document.body.classList.add(savedTheme);
    }

    function toggleTheme() {
        const isDark = document.body.classList.contains('theme-dark');
        const newTheme = isDark ? 'theme-light' : 'theme-dark';
        document.body.classList.remove('theme-dark', 'theme-light');
        document.body.classList.add(newTheme);
        GM_setValue(THEME_KEY, newTheme); // save preference
        updateThemeToggleButton(newTheme === 'theme-dark');
    }

    function updateThemeToggleButton(isDark) {
        const btn = document.getElementById('theme-toggle-btn');
        if (btn) {
            // sun icon for dark theme, moon for light
            btn.innerHTML = isDark
                ? '<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="5"></circle><path d="M12 1v2M12 21v2M4.2 4.2l1.4 1.4M18.4 18.4l1.4 1.4M1 12h2M21 12h2M4.2 19.8l1.4-1.4M18.4 5.6l1.4-1.4"></path></svg>'
                : '<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path></svg>';
        }
    }

    function setupLinkPreviews() {
        const preview = document.createElement('div');
        preview.className = 'preview';
        document.body.appendChild(preview);
        let showTimeout = null, hideTimeout = null, currentLink = null;

        document.body.addEventListener('mouseover', (e) => {
            const link = e.target.closest('a');
            if (link?.href) { // check for link and href
                clearTimeout(hideTimeout);
                if (currentLink !== link) {
                    currentLink = link;
                    clearTimeout(showTimeout);
                    showTimeout = setTimeout(() => showPreview(link, preview), 200); // slight delay
                }
            }
        });

        document.body.addEventListener('mouseout', (e) => {
            if (e.target.closest('a')) {
                clearTimeout(showTimeout);
                hideTimeout = setTimeout(() => {
                    preview.style.opacity = '0';
                    setTimeout(() => {
                        preview.style.display = 'none';
                        currentLink = null;
                    }, 150); // wait for fade out
                }, 200); // delay before hiding
            }
        });

        // keep preview open if mouse moves onto it
        preview.addEventListener('mouseenter', () => clearTimeout(hideTimeout));
        preview.addEventListener('mouseleave', () => {
            hideTimeout = setTimeout(() => {
                preview.style.opacity = '0';
                setTimeout(() => {
                    preview.style.display = 'none';
                    currentLink = null;
                }, 150);
            }, 200);
        });
    }

    function showPreview(link, preview) {
        try {
            const url = link.href;
            const rect = link.getBoundingClientRect();
            let sourceName = 'Source';
            let faviconUrl = '';

            try {
                const urlObj = new URL(url);
                sourceName = urlObj.hostname.replace(/^www\./, '');
                faviconUrl = `https://www.google.com/s2/favicons?domain=${urlObj.hostname}&sz=64`;
            } catch (err) { /* ignore invalid urls */ }

            let contentHTML = link.textContent?.trim() || url;
            if (link.closest('pre, code')) {
                contentHTML = `<code class="inline-code">${contentHTML}</code>`; // preserve code styling
            }

            const faviconHTML = faviconUrl
                ? `<img src="${faviconUrl}" alt="" onerror="this.style.display='none'; this.parentElement.textContent='${sourceName.charAt(0).toUpperCase()}';" />`
                : sourceName.charAt(0).toUpperCase();

            preview.innerHTML = `
                <div class="preview-header">
                    <div class="preview-icon">${faviconHTML}</div>
                    <div>${sourceName}</div>
                </div>
                <div class="preview-content">${contentHTML}</div>
                <div class="preview-url">${url}</div>
            `;

            preview.style.opacity = 0;
            preview.style.display = 'block';

            // position calculation needs to happen after display:block
            requestAnimationFrame(() => {
                const previewRect = preview.getBoundingClientRect();
                const winWidth = window.innerWidth;
                const winHeight = window.innerHeight;
                let top = rect.bottom + window.scrollY + 5;
                let left = rect.left + window.scrollX;

                // adjust position to stay within viewport
                if (left + previewRect.width > winWidth - 10) left = winWidth - previewRect.width - 10;
                if (left < 10) left = 10;
                if (top + previewRect.height > winHeight + window.scrollY - 10) top = rect.top + window.scrollY - previewRect.height - 5;
                if (top < window.scrollY + 10) top = window.scrollY + 10;

                preview.style.top = `${top}px`;
                preview.style.left = `${left}px`;
                preview.classList.add('active');
                preview.style.opacity = 1;
            });
        } catch (e) {
            console.error("Pollinations Enhancer: Error showing preview", e);
            preview.classList.remove('active');
            preview.style.display = 'none';
        }
    }

    function enhanceTextPage() {
        document.body.classList.add('text-enhanced');
        const params = extractUrlParameters();
        let contentContainer = null;
        let originalContent = "";

        try {
            // find the main content area
            contentContainer = document.querySelector(
                'main:not(:empty), article:not(:empty), .content:not(:empty), #content:not(:empty), .main-content:not(:empty), .post-content:not(:empty)'
            );

            // handle pages that might just be a single <pre> tag
            if (!contentContainer && document.body.children.length === 1 && document.body.firstElementChild?.tagName === 'PRE') {
                contentContainer = document.body.firstElementChild;
            }

            // if no container found, wrap bare text nodes (risky but sometimes needed)
            if (!contentContainer && (document.body.innerText || document.body.textContent || '').trim().length > 50) {
                console.log("Pollinations Enhancer: Wrapping bare text content.");
                contentContainer = document.createElement('div');
                contentContainer.className = 'content-container-generated'; // mark as generated
                // move body nodes into the container, careful not to move our script/buttons
                while (document.body.firstChild &&
                       (!document.body.firstChild.matches ||
                        !document.body.firstChild.matches('#pollinations-enhancer-buttons, .preview, script, style'))) {
                    contentContainer.appendChild(document.body.firstChild);
                }
                document.body.appendChild(contentContainer);
            }

            if (contentContainer) {
                contentContainer.classList.add('content-container');
                if (contentContainer.tagName !== 'PRE') {
                    // get original text *before* markdown processing
                    originalContent = contentContainer.innerText || contentContainer.textContent || '';
                    processMarkdown(contentContainer);
                } else {
                    originalContent = contentContainer.textContent || ''; // content is already plain text
                }
            } else {
                 console.warn("Pollinations Enhancer: Could not find a suitable content container.");
            }
        } catch (error) {
            console.error("Pollinations Enhancer: Error enhancing text page:", error);
        }

        createCommonButtons(params, 'text', contentContainer, originalContent);
    }

    function processMarkdown(container) {
        if (!container) return;

        // basic cleanup first
        container.innerHTML = container.innerHTML
            .replace(/<span class="[^"]*">/g, '') // remove potentially interfering spans
            .replace(/<\/span>/g, '');

        const codeBlocks = [];
        let codeBlockCount = 0;

        // isolate code blocks first to prevent markdown processing inside them
        container.innerHTML = container.innerHTML.replace(/```(\w*)\n([\s\S]*?)```/g, (match, lang, code) => {
            const placeholder = `<!--CODEBLOCK_${codeBlockCount}-->`;
            codeBlocks.push({
                placeholder,
                language: lang || 'text',
                code: code.replace(/`/g, '`') // escape backticks inside code
            });
            codeBlockCount++;
            return placeholder;
        });

        const originalHtmlWithPlaceholders = container.innerHTML;
        const textContentForProcessing = container.textContent || '';

        // process blockquotes on the text content
        let processedHtml = processBlockquotes(textContentForProcessing);

        // restore code block placeholders if blockquotes were processed
        if (processedHtml !== textContentForProcessing) {
             processedHtml = restoreCodeBlocks(processedHtml, originalHtmlWithPlaceholders);
        } else {
            processedHtml = originalHtmlWithPlaceholders; // use original if no blockquotes found
        }

        // now apply other markdown rules to the potentially modified html
        processedHtml = processedHtml
            .replace(/^(#{1,6})\s+(.+)$/gm, (match, hashes, content) => `<h${hashes.length}>${content}</h${hashes.length}>`) // Headers
            .replace(/^---+$/gm, '<hr class="markdown-hr">') // Horizontal rule
            .replace(/\*\*(.*?)\*\*|__(.*?)__/g, '<strong>$1$2</strong>') // Bold
            .replace(/\*(.*?)\*|_(.*?)_/g, '<em>$1$2</em>') // Italic
            .replace(/~~(.*?)~~/g, '<del>$1</del>') // Strikethrough
            .replace(/\[([^\]]+?)\]\((https?:\/\/[^)]+)\)/g, '<a href="$2" target="_blank" rel="noopener noreferrer">$1</a>') // Links
            .replace(/`([^`]+?)`/g, '<code class="inline-code">$1</code>'); // Inline code

        // process lists after other formatting
        processedHtml = processLists(processedHtml);

        // put the processed html back into the container
        container.innerHTML = processedHtml;

        // restore the actual code block content
        codeBlocks.forEach(block => {
            const codeHtml = `
                <pre class="code-block-container">
                    <div class="code-header">
                        <span class="code-language">${block.language}</span>
                        <button class="code-copy-btn" title="Copy code">Copy</button>
                    </div>
                    <code class="language-${block.language}">${block.code}</code>
                </pre>`;
            container.innerHTML = container.innerHTML.replace(block.placeholder, codeHtml);
        });

        // add listeners to copy buttons inside code blocks
        container.querySelectorAll('.code-copy-btn').forEach(btn => {
            btn.addEventListener('click', () => {
                const codeElement = btn.closest('.code-block-container')?.querySelector('code');
                if (codeElement) {
                    navigator.clipboard.writeText(codeElement.textContent || '').then(() => {
                        const originalText = btn.textContent;
                        btn.textContent = 'Copied!';
                        setTimeout(() => { btn.textContent = originalText; }, 1500);
                    }).catch(err => console.error('Failed to copy code:', err));
                }
            });
        });
    }

    function processBlockquotes(text) {
        const lines = text.split('\n');
        let result = [];
        let inMultiBlockquote = false;
        let blockquoteContent = [];

        for (let i = 0; i < lines.length; i++) {
            const line = lines[i];
            const trimmedLine = line.trim();

            if (trimmedLine.startsWith('>>>')) {
                // start multi-line blockquote
                if (inMultiBlockquote) { // close previous one if any
                    result.push(`<blockquote class="blockquote-multi">${blockquoteContent.join(' ')}</blockquote>`);
                }
                inMultiBlockquote = true;
                const initial = trimmedLine.substring(3).trim();
                blockquoteContent = initial ? [initial] : [];
            } else if (inMultiBlockquote) {
                // continue or end multi-line blockquote
                if (trimmedLine === '') { // end on empty line
                    result.push(`<blockquote class="blockquote-multi">${blockquoteContent.join(' ')}</blockquote>`);
                    result.push(''); // keep the empty line separator
                    inMultiBlockquote = false;
                    blockquoteContent = [];
                } else {
                    blockquoteContent.push(trimmedLine);
                }
            } else if (trimmedLine.startsWith('> ')) {
                // single line blockquote
                result.push(`<blockquote class="blockquote-single">${trimmedLine.substring(2)}</blockquote>`);
            } else {
                // regular line
                result.push(line);
            }
        }

        // close any open multi-line quote at the end of the text
        if (inMultiBlockquote) {
            result.push(`<blockquote class="blockquote-multi">${blockquoteContent.join(' ')}</blockquote>`);
        }

        return result.join('\n');
    }

    function restoreCodeBlocks(processedHtml, originalHtmlWithPlaceholders) {
        // find all placeholders in the original html
        const placeholders = originalHtmlWithPlaceholders.match(/<!--CODEBLOCK_\d+-->/g) || [];
        if (placeholders.length === 0) return processedHtml;

        let currentHtml = processedHtml;
        // replace placeholder markers in the processed text with the original placeholders
        // this is tricky because blockquote processing might have altered line structure
        // simple strategy: replace markers sequentially
        let placeholderIndex = 0;
        currentHtml = currentHtml.replace(/<!--CODEBLOCK_\d+-->/g, () => {
            if (placeholderIndex < placeholders.length) {
                return placeholders[placeholderIndex++];
            }
            return ''; // should not happen if counts match
        });

        // if counts didn't match, try injecting remaining placeholders (less ideal)
        while(placeholderIndex < placeholders.length) {
             console.warn("Pollinations Enhancer: Mismatched code block count during restoration.");
             currentHtml += placeholders[placeholderIndex++]; // append leftovers
        }

        return currentHtml;
    }


    function processLists(text) {
        const lines = text.split('\n');
        let result = [];
        let lastWasNumbered = false;

        for (let i = 0; i < lines.length; i++) {
            const line = lines[i];
            const numberedMatch = line.match(/^(\s*)(\d+)\.\s+(.*)/); // capture indent
            const dashMatch = line.match(/^(\s*)[-*]\s+(.*)/); // capture indent

            if (numberedMatch) {
                const indent = numberedMatch[1];
                const num = numberedMatch[2];
                const content = numberedMatch[3];
                // basic indent handling - could be more sophisticated
                const indentLevel = indent.length / 2; // rough estimate
                result.push(`<div class="list-item numbered-item" style="padding-left: ${indentLevel * 1.5}em;">${num}. ${content}</div>`);
                lastWasNumbered = true;
            } else if (dashMatch) {
                 const indent = dashMatch[1];
                 const content = dashMatch[2];
                 const indentLevel = indent.length / 2;
                 const bulletClass = lastWasNumbered ? "nested-bullet-item" : "bullet-item";
                 result.push(`<div class="list-item ${bulletClass}" style="padding-left: ${indentLevel * 1.5}em;">${content}</div>`);
                 // lastWasNumbered remains true if nested under numbered
            } else {
                 result.push(line); // pass through non-list lines
                 lastWasNumbered = line.trim() !== ''; // reset if line is not empty
            }
        }
        return result.join('\n');
    }

    function enhanceImagePage() {
        removeExistingButtons(); // clear old buttons first
        let mainImage = null;

        try {
            // prioritise images directly in body
            const directImages = Array.from(document.querySelectorAll('body > img'));
            if (directImages.length > 0) {
                let largestArea = 0;
                directImages.forEach(img => {
                    if (img.complete && img.naturalWidth > 30 && img.naturalHeight > 30) {
                        const area = img.naturalWidth * img.naturalHeight;
                        if (area > largestArea) {
                            largestArea = area;
                            mainImage = img;
                        }
                    } else if (!img.complete) {
                        // if image not loaded yet, re-run enhancement on load
                        img.addEventListener('load', () => setTimeout(enhanceImagePage, 100), { once: true });
                    }
                });
            }

            // fallback to searching all significant images if no direct body image found
            if (!mainImage) {
                const allImages = document.querySelectorAll('img');
                let largestArea = 0;
                allImages.forEach(img => {
                    if (img.width < 30 || img.height < 30) return; // ignore small icons
                    const area = img.naturalWidth * img.naturalHeight || img.width * img.height;
                    if (area > largestArea) {
                        largestArea = area;
                        mainImage = img;
                    }
                });
            }
        } catch (error) {
            console.error("Pollinations Enhancer: Error finding image element:", error);
        }

        const params = extractUrlParameters();
        if (mainImage) {
            // add width/height from image if available
            params.width = mainImage.naturalWidth || mainImage.width || params.width || '';
            params.height = mainImage.naturalHeight || mainImage.height || params.height || '';
            createCommonButtons(params, 'image', mainImage);
        } else {
            // if no image found yet, maybe it loads later? retry once.
            console.log("Pollinations Enhancer: Main image not found, scheduling retry.");
            setTimeout(enhanceImagePage, 600); // longer delay for retry
        }
    }

    function enhanceAudioPage() {
        removeExistingButtons();
        const params = extractUrlParameters();
        const audioSrc = findAudioSource() || window.location.href; // use page url as fallback
        createCommonButtons(params, 'audio', null, '', audioSrc);
    }

    function removeExistingButtons() {
        document.getElementById('pollinations-enhancer-buttons')?.remove();
        document.getElementById('metadata-box')?.remove();
    }

    function findAudioSource() {
        // check common audio/video tags and sources
        const mediaElements = document.querySelectorAll('audio, video');
        for (const el of mediaElements) {
            if (el.src && el.src.length > 10) return el.src;
            for (const source of el.querySelectorAll('source')) {
                if (source.src && source.src.length > 10) return source.src;
            }
        }

        // check links that look like audio files
        const audioExtRegex = /\.(mp3|wav|ogg|m4a|flac|aac)(\?|$)/i;
        for (const link of document.querySelectorAll('a[href]')) {
            try {
                const url = new URL(link.getAttribute('href'), window.location.href);
                if (audioExtRegex.test(url.pathname) || url.pathname.includes('/audio/')) {
                    return url.href;
                }
            } catch (e) { /* ignore invalid href */ }
        }

        // check elements with audio-related attributes
        for (const el of document.querySelectorAll('[type*="audio"], [src*=".mp3"], [src*="/audio/"]')) {
            const src = el.getAttribute('src') || el.getAttribute('data-src');
            if (src && src.length > 10) return new URL(src, window.location.href).href;
        }

        // check meta tags
        const metaOgAudio = document.querySelector('meta[property="og:audio"], meta[property="og:audio:url"]');
        if (metaOgAudio?.content) return metaOgAudio.content;

        // last resort: scan html for direct audio urls (less reliable)
        const htmlContent = document.documentElement.outerHTML;
        const audioUrlMatches = htmlContent.match(/https?:\/\/[^"'\s]+\.(mp3|wav|ogg|m4a)(\?[^"'\s]*)?/gi);
        if (audioUrlMatches?.length) return audioUrlMatches[0];

        return null; // no source found
    }

    // creates the floating button panel
    function createCommonButtons(
        params,
        type,
        targetElement = null,
        originalContent = '',
        resourceUrl = ''
    ) {
        try {
            removeExistingButtons(); // ensure clean state

            const buttonContainer = document.createElement('div');
            buttonContainer.id = 'pollinations-enhancer-buttons';
            buttonContainer.style.cssText = `
                position: fixed; top: 10px; right: 20px; z-index: 9999;
                display: flex; flex-direction: column; gap: 8px;
                align-items: flex-end;
            `; // stack buttons vertically

            // --- Text Page Specific Buttons ---
            if (type === 'text') {
                const themeToggleBtn = createButton('theme-toggle-btn', 'Toggle theme (Light/Dark)', '', toggleTheme);
                buttonContainer.appendChild(themeToggleBtn);
                updateThemeToggleButton(document.body.classList.contains('theme-dark'));

                if (targetElement) {
                    const copyContentBtn = createButton(
                        'copy-content-btn',
                        'Copy text content',
                        '<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path></svg>',
                        () => {
                            const textToCopy = originalContent || (targetElement.innerText || targetElement.textContent || '');
                            navigator.clipboard.writeText(textToCopy).then(
                                () => flashButton(copyContentBtn, true),
                                () => flashButton(copyContentBtn, false)
                            );
                        }
                    );
                    buttonContainer.appendChild(copyContentBtn);
                }

                // --- Font Size Controls ---
                const fontSizeContainer = document.createElement('div');
                fontSizeContainer.className = 'font-size-controls';
                fontSizeContainer.style.cssText = 'display: flex; gap: 5px; justify-content: flex-end;';

                const decreaseFontBtn = createButton(
                    'decrease-font-btn', 'Decrease text size',
                    '<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><line x1="8" y1="12" x2="16" y2="12"></line></svg>',
                     decreaseFontSize
                 );
                const increaseFontBtn = createButton(
                    'increase-font-btn', 'Increase text size',
                    '<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="8" x2="12" y2="16"></line><line x1="8" y1="12" x2="16" y2="12"></line></svg>',
                     increaseFontSize
                 );

                fontSizeContainer.appendChild(decreaseFontBtn);
                fontSizeContainer.appendChild(increaseFontBtn);
                buttonContainer.appendChild(fontSizeContainer);
            }

            // --- Image Download Button ---
            if (type === 'image' && targetElement?.src) {
                const timestamp = getFormattedTimestamp();
                const seedPart = params.seed ? `_${params.seed}` : '_noseed';
                let extension = '.jpg'; // default
                try {
                    const urlPath = new URL(targetElement.src).pathname;
                    const match = urlPath.match(/\.(jpg|jpeg|png|webp|gif)$/i);
                    if (match) {
                        extension = match[0].toLowerCase();
                        if (extension === '.jpeg') extension = '.jpg'; // normalise
                    }
                } catch (e) { console.warn("Enhancer: Couldn't parse img URL for extension", e); }

                const filename = `pollinations.ai${seedPart}_${timestamp}${extension}`;
                const saveImageBtn = createButton(
                    'save-image-btn', `Save image (${filename})`,
                    '<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path><polyline points="7 10 12 15 17 10"></polyline><line x1="12" y1="15" x2="12" y2="3"></line></svg>',
                    () => downloadResource(targetElement.src, filename, saveImageBtn, 'image')
                );
                buttonContainer.appendChild(saveImageBtn);
            }

            // --- Audio Download Button ---
            if (type === 'audio' && resourceUrl) {
                const timestamp = getFormattedTimestamp();
                const seedPart = params.seed ? `_${params.seed}` : '_noseed';
                let extension = '.mp3'; // default
                try {
                    const urlPath = new URL(resourceUrl).pathname;
                    const match = urlPath.match(/\.(mp3|wav|ogg|m4a|flac|aac)$/i);
                    if (match) extension = match[0].toLowerCase();
                } catch (e) { console.warn("Enhancer: Couldn't parse audio URL for extension", e); }

                const filename = `pollinations.ai${seedPart}_${timestamp}${extension}`;
                const saveAudioBtn = createButton(
                    'save-audio-btn', `Save audio (${filename})`,
                    '<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"></path><polyline points="17 21 17 13 7 13 7 21"></polyline><polyline points="7 3 7 8 15 8"></polyline></svg>',
                    () => downloadResource(resourceUrl, filename, saveAudioBtn, 'audio')
                );
                buttonContainer.appendChild(saveAudioBtn);
            }

            // --- Metadata Button & Box ---
            // check if any relevant parameter exists
            const hasMetadata = Object.entries(params).some(([key, value]) =>
                value && ['model', 'prompt', 'seed', 'voice', 'width', 'system', 'private', 'guidance_scale', 'negative_prompt', 'strength', 'steps', 'language'].includes(key)
            );

            if (hasMetadata) {
                const metadataBtn = createButton(
                    'metadata-btn', 'View metadata',
                    '<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="16" x2="12" y2="12"></line><line x1="12" y1="8" x2="12.01" y2="8"></line></svg>',
                    () => { // toggle visibility and collapsed state
                        const box = document.getElementById('metadata-box');
                        if (box) {
                            const becomesVisible = !box.classList.contains('visible');
                            box.classList.toggle('visible');
                            if (becomesVisible) {
                                box.classList.remove('collapsed'); // always expand when showing
                            } else {
                                box.classList.add('collapsed'); // ensure marked collapsed when hiding
                            }
                            updateMetadataToggleIcon(box);
                        }
                    }
                );
                buttonContainer.appendChild(metadataBtn);
                createMetadataBox(params, type); // create the hidden box
            }

            // add the whole container to the page if it has any buttons
            if (buttonContainer.hasChildNodes()) {
                document.body.appendChild(buttonContainer);
            }

        } catch (error) {
            console.error("Pollinations Enhancer: Error creating common buttons:", error);
        }
    }

    function increaseFontSize() {
        const currentScale = parseFloat(GM_getValue(FONT_SIZE_KEY, '1')) || 1;
        const newScale = Math.min(currentScale + 0.1, 2.0).toFixed(1); // cap at 2.0
        GM_setValue(FONT_SIZE_KEY, newScale);
        applyFontSize();
    }

    function decreaseFontSize() {
        const currentScale = parseFloat(GM_getValue(FONT_SIZE_KEY, '1')) || 1;
        const newScale = Math.max(currentScale - 0.1, 0.7).toFixed(1); // cap at 0.7
        GM_setValue(FONT_SIZE_KEY, newScale);
        applyFontSize();
    }

    function applyFontSize() {
        const scale = GM_getValue(FONT_SIZE_KEY, '1') || '1';
        document.getElementById('font-size-style')?.remove(); // remove old style

        const styleEl = document.createElement('style');
        styleEl.id = 'font-size-style';
        // apply scale to font-size only, avoid scaling the whole container
        styleEl.textContent = `
            body.text-enhanced .content-container,
            body.text-enhanced .content-container-generated {
                font-size: calc(17px * ${scale}) !important;
            }
        `;
        document.head.appendChild(styleEl);
    }

    function createButton(id, title, innerHTML, onClick) {
        const btn = document.createElement('div'); // using div styled as button
        btn.id = id;
        btn.className = 'p-btn';
        btn.title = title;
        btn.innerHTML = innerHTML;
        btn.addEventListener('click', onClick);
        btn.style.position = 'relative'; // needed for potential future absolute elements inside
        btn.style.margin = '0'; // remove default margins
        return btn;
    }

    function flashButton(button, success) {
        if (!button) return;
        const originalColor = button.style.backgroundColor; // capture default
        button.style.backgroundColor = success ? 'var(--success-color)' : 'var(--error-color)';
        button.style.transform = 'scale(1.1)';
        setTimeout(() => {
            if (button) { // check if button still exists
                button.style.backgroundColor = ''; // revert to css default
                button.style.transform = 'scale(1)';
            }
        }, 1000);
    }

    // handles downloading, attempts exif stripping for images
    function downloadResource(url, filename, buttonToFlash, resourceType = 'unknown') {
        GM_xmlhttpRequest({
            method: "GET",
            url: url,
            responseType: 'blob',
            onload: function(response) {
                if (response.status === 200 && response.response) {
                    const originalBlob = response.response;

                    // attempt exif strip via canvas only for image types
                    if (resourceType === 'image' && originalBlob.type.startsWith('image/')) {
                        const img = document.createElement('img');
                        const canvas = document.createElement('canvas');
                        const ctx = canvas.getContext('2d');
                        const objectURL = URL.createObjectURL(originalBlob); // temp url for img src

                        img.onload = () => {
                            try {
                                canvas.width = img.naturalWidth;
                                canvas.height = img.naturalHeight;
                                ctx.drawImage(img, 0, 0);

                                let mimeType = originalBlob.type;
                                // ensure canvas export supports the type, default to jpeg
                                if (!['image/jpeg', 'image/png', 'image/webp'].includes(mimeType)) {
                                    console.warn(`Enhancer: Unsupported MIME type for canvas: ${mimeType}. Using image/jpeg.`);
                                    mimeType = 'image/jpeg';
                                    filename = filename.replace(/\.[^.]+$/, '.jpg'); // fix extension if changed
                                }

                                canvas.toBlob((processedBlob) => {
                                    if (processedBlob) {
                                        triggerDownload(processedBlob, filename, buttonToFlash, true);
                                    } else {
                                        console.error('Enhancer: Canvas toBlob failed. Downloading original.');
                                        triggerDownload(originalBlob, filename, buttonToFlash, false); // fallback
                                    }
                                    URL.revokeObjectURL(objectURL); // cleanup img src url
                                }, mimeType, 0.92); // quality for jpeg/webp

                            } catch (e) {
                                console.error("Enhancer: Error during canvas processing:", e);
                                URL.revokeObjectURL(objectURL);
                                triggerDownload(originalBlob, filename, buttonToFlash, false); // fallback
                            }
                        };

                        img.onerror = () => {
                            console.error('Enhancer: Failed to load image for EXIF stripping. Downloading original.');
                            URL.revokeObjectURL(objectURL);
                            triggerDownload(originalBlob, filename, buttonToFlash, false); // fallback
                        };

                        img.src = objectURL; // load the blob into the img element

                    } else {
                        // download non-images directly
                        triggerDownload(originalBlob, filename, buttonToFlash, true);
                    }

                } else {
                    console.error("Enhancer: Download failed:", response.statusText, response.status);
                    if (buttonToFlash) flashButton(buttonToFlash, false);
                    tryDirectDownload(url, filename, buttonToFlash); // fallback attempt
                }
            },
            onerror: function(response) {
                console.error("Enhancer: Download network error:", response.error);
                if (buttonToFlash) flashButton(buttonToFlash, false);
                tryDirectDownload(url, filename, buttonToFlash); // fallback attempt
            }
        });
    }

    // helper to create the download link and click it
    function triggerDownload(blob, filename, buttonToFlash, successStatus) {
        try {
            const link = document.createElement('a');
            link.href = URL.createObjectURL(blob);
            link.download = filename;
            document.body.appendChild(link); // required for firefox
            link.click();
            document.body.removeChild(link);
            URL.revokeObjectURL(link.href); // cleanup blob url
            if (buttonToFlash) flashButton(buttonToFlash, successStatus);
        } catch (e) {
            console.error("Enhancer: Error creating download link:", e);
            if (buttonToFlash) flashButton(buttonToFlash, false);
        }
    }

    // simple fallback using direct link (may not respect filename, no exif strip)
    function tryDirectDownload(url, filename, buttonToFlash) {
        try {
            const link = document.createElement('a');
            link.href = url;
            link.download = filename; // works in chrome, less so elsewhere
            link.target = '_blank';
            link.rel = 'noopener noreferrer';
            document.body.appendChild(link);
            link.click();
            document.body.removeChild(link);
            // cannot reliably flash success here
        } catch (err) {
            console.error("Enhancer: Direct download attempt failed:", err);
            if (buttonToFlash) flashButton(buttonToFlash, false);
        }
    }

    function createMetadataBox(params, type) {
        document.getElementById('metadata-box')?.remove(); // clear existing

        const metadataBox = document.createElement('div');
        metadataBox.id = 'metadata-box';
        metadataBox.className = 'meta-box'; // initially hidden by css (opacity 0)

        let contentHTML = '';
        // common params
        if (params.model) contentHTML += createMetaRow('Model', params.model);
        if (params.prompt) contentHTML += createMetaRow('Prompt', params.prompt, true); // allow copy
        if (params.seed) contentHTML += createMetaRow('Seed', params.seed);
        if (params.private) contentHTML += createMetaRow('Private', params.private);

        // image specific
        if (type === 'image') {
            if (params.width && params.height) contentHTML += createMetaRow('Size', `${params.width} × ${params.height}`);
            if (params.guidance_scale) contentHTML += createMetaRow('Guidance', params.guidance_scale); // shorter label
            if (params.negative_prompt) contentHTML += createMetaRow('Negative', params.negative_prompt, true); // shorter label, allow copy
            if (params.strength) contentHTML += createMetaRow('Strength', params.strength);
            if (params.steps) contentHTML += createMetaRow('Steps', params.steps);
        }

        // audio specific
        if (type === 'audio') {
            if (params.voice) contentHTML += createMetaRow('Voice', params.voice);
            if (params.format) contentHTML += createMetaRow('Format', params.format);
        }

        // text specific
        if (type === 'text') {
            if (params.system) contentHTML += createMetaRow('System', params.system, true); // allow copy
            if (params.language) contentHTML += createMetaRow('Language', params.language);
            if (params.modalities) contentHTML += createMetaRow('Modalities', params.modalities);
        }

        if (!contentHTML.trim()) return; // don't create box if no content

        metadataBox.innerHTML = `
            <div class="meta-header" title="Click to toggle details">
                <span>${type.charAt(0).toUpperCase() + type.slice(1)} Parameters</span>
                <span class="toggle-icon">▼</span>
            </div>
            <div class="meta-content">${contentHTML}</div>
        `;
        document.body.appendChild(metadataBox);

        // click header to toggle visibility/collapse
        metadataBox.querySelector('.meta-header').addEventListener('click', () => {
             const box = metadataBox; // already have ref
             const becomesVisible = !box.classList.contains('visible');
             box.classList.toggle('visible');
             if (becomesVisible) {
                 box.classList.remove('collapsed');
             } else {
                 box.classList.add('collapsed');
             }
             updateMetadataToggleIcon(box);
        });

        // add copy functionality to buttons in rows
        metadataBox.querySelectorAll('.copy-btn[data-copy-target]').forEach(copyBtn => {
            const targetLabel = copyBtn.getAttribute('data-copy-target');
            const textToCopy = params[targetLabel.toLowerCase().replace(' ', '_')]; // handle multi-word labels like 'guidance scale'

            if (textToCopy) {
                copyBtn.addEventListener('click', (e) => {
                    e.stopPropagation(); // prevent header click
                    navigator.clipboard.writeText(textToCopy).then(() => {
                        copyBtn.textContent = '✓'; // success indicator
                        setTimeout(() => { copyBtn.textContent = '📋'; }, 1200); // revert icon
                    }).catch(err => console.error(`Enhancer: Failed to copy ${targetLabel}:`, err));
                });
            } else {
                copyBtn.style.display = 'none'; // hide button if no text found
            }
        });

        updateMetadataToggleIcon(metadataBox); // set initial icon state
    }

    function updateMetadataToggleIcon(box) {
        const icon = box?.querySelector('.toggle-icon');
        if (icon) {
            // show down arrow if collapsed or hidden, up arrow if expanded/visible
            icon.textContent = box.classList.contains('collapsed') || !box.classList.contains('visible') ? '▼' : '▲';
        }
    }

    function createMetaRow(label, value, addCopyButton = false) {
        if (!value) return '';
        // basic html escaping for value display
        const cleanValue = String(value).replace(/</g, "<").replace(/>/g, ">");
        const copyBtnHTML = addCopyButton
            ? `<span class="copy-btn" title="Copy ${label}" data-copy-target="${label}">📋</span>`
            : '';
        return `
            <div class="meta-row">
                <div class="meta-label">${label}:</div>
                <div class="meta-value">${cleanValue}${copyBtnHTML}</div>
            </div>
        `;
    }

    // decode prompts, handle spaces encoded as '+' or '%20'
    function sanitizePrompt(text) {
        if (!text) return '';
        try {
            let decoded = text;
            // attempt decoding multiple times for nested encoding
            for (let i = 0; i < 3; i++) {
                if (decoded.includes('%')) decoded = decodeURIComponent(decoded);
                else break; // stop if no more percent signs
            }
            // replace '+' with space and remove potential replacement characters
            return decoded.replace(/\+/g, ' ').replace(/\uFFFD/gu, '').trim();
        } catch (e) {
            console.warn("Enhancer: Failed to fully decode prompt:", text, e);
            // fallback: basic space replacement
            return text.replace(/\+/g, ' ').replace(/%20/g, ' ').trim();
        }
    }

    // helper for consistent timestamp format
    function getFormattedTimestamp() {
        const now = new Date();
        const year = now.getFullYear();
        const month = String(now.getMonth() + 1).padStart(2, '0');
        const day = String(now.getDate()).padStart(2, '0');
        const hours = String(now.getHours()).padStart(2, '0');
        const minutes = String(now.getMinutes()).padStart(2, '0');
        const seconds = String(now.getSeconds()).padStart(2, '0');
        return `${year}${month}${day}${hours}${minutes}${seconds}`; // YYYYMMDDHHMMSS
    }

    function extractUrlParameters() {
        const urlParams = new URLSearchParams(window.location.search);
        const path = window.location.pathname;
        let rawPrompt = '';

        // prefer 'prompt' param if present
        if (urlParams.has('prompt')) {
            rawPrompt = urlParams.get('prompt');
        } else {
            // fallback: try to get prompt from the last part of the path
            const pathSegments = decodeURIComponent(path).split('/').filter(Boolean);
            if (pathSegments.length > 0) {
                const lastSegment = pathSegments[pathSegments.length - 1];
                // basic check: assume it's a prompt if it's reasonably long and doesn't look like a file/id
                 if (lastSegment.length > 5 && !lastSegment.match(/^[\w-]{8,}$/) && !lastSegment.includes('.')) {
                     rawPrompt = lastSegment;
                 }
            }
        }

        // return object with all relevant params, sanitising prompts
        return {
            prompt: sanitizePrompt(rawPrompt),
            model: urlParams.get('model') || '',
            seed: urlParams.get('seed') || '',
            voice: urlParams.get('voice') || '',
            width: urlParams.get('width') || '',
            height: urlParams.get('height') || '',
            system: sanitizePrompt(urlParams.get('system') || ''),
            private: urlParams.get('private') || '', // boolean-like
            format: urlParams.get('format') || '', // audio format
            modalities: urlParams.get('modalities') || '',
            guidance_scale: urlParams.get('guidance_scale') || '',
            negative_prompt: sanitizePrompt(urlParams.get('negative_prompt') || ''),
            strength: urlParams.get('strength') || '',
            steps: urlParams.get('steps') || '',
            language: urlParams.get('language') || '',
        };
    }

    function addStyles() {
        GM_addStyle(`
            :root {
                --text-color: #f0f0f0;
                --bg-color: #1a1a1a;
                --accent: #3498db;
                --accent-hover: #2980b9;
                --light-bg: #f2f2f2;
                --light-text: #333333;
                --light-content-bg: #ffffff;
                --dark-content-bg: #2d2d2d;
                --border-color-dark: #444;
                --border-color-light: #ddd;
                --button-bg: #333; /* Unused currently */
                --button-hover-bg: #444; /* Unused currently */
                --success-color: #27ae60;
                --error-color: #e74c3c;
                --code-bg-dark: #1e1e1e;
                --code-bg-light: #f8f8f8;
                --code-header-bg-dark: linear-gradient(90deg, rgba(30,30,30,1) 0%, rgba(45,45,45,1) 100%);
                --code-header-bg-light: linear-gradient(90deg, rgba(245,245,245,1) 0%, rgba(235,235,235,1) 100%);
                --blockquote-border: #3498db; /* Same as accent */
                --blockquote-bg-dark: rgba(255,255,255,0.05);
                --blockquote-bg-light: rgba(0,0,0,0.03);
            }
            body {
                transition: background-color 0.3s, color 0.3s;
            }
            body.theme-light {
                background-color: var(--light-bg) !important;
                color: var(--light-text) !important;
            }
            body.theme-dark {
                background-color: var(--bg-color) !important;
                color: var(--text-color) !important;
            }

            /* Buttons */
            .p-btn {
                background-color: var(--accent);
                color: white;
                border: none;
                border-radius: 50%;
                width: 36px;
                height: 36px;
                display: flex;
                align-items: center;
                justify-content: center;
                cursor: pointer;
                /* margin: 5px; Removed, using gap in container */
                transition: all 0.2s ease-in-out;
                box-shadow: 0 2px 5px rgba(0,0,0,0.2);
                flex-shrink: 0; /* Prevent shrinking in flex container */
            }
            .p-btn:hover {
                background-color: var(--accent-hover);
                transform: scale(1.05);
            }
            .p-btn svg {
                pointer-events: none; /* Prevent svg from capturing clicks */
            }
            .font-size-controls {
                display: flex;
                gap: 5px;
                justify-content: flex-end; /* Align to right within its space */
            }
            .font-size-controls .p-btn {
                width: 30px; /* Slightly smaller font buttons */
                height: 30px;
            }

            /* Metadata Box */
            .meta-box {
                position: fixed;
                bottom: 20px;
                right: 20px;
                background-color: rgba(45, 45, 45, 0.9);
                color: var(--text-color);
                border-radius: 8px;
                padding: 12px;
                max-width: 320px;
                width: auto;
                font-size: 13px;
                line-height: 1.5;
                z-index: 9998;
                transition: opacity 0.3s, transform 0.3s, height 0.3s ease-out, padding 0.3s ease-out;
                box-shadow: 0 4px 15px rgba(0,0,0,0.3);
                opacity: 0;
                transform: translateY(10px);
                pointer-events: none;
                overflow: hidden;
                backdrop-filter: blur(3px);
                -webkit-backdrop-filter: blur(3px);
            }
            body.theme-light .meta-box {
                background-color: rgba(255, 255, 255, 0.9);
                color: var(--light-text);
            }
            .meta-box.visible {
                opacity: 1;
                transform: translateY(0);
                pointer-events: auto;
            }
            .meta-box.collapsed {
                 /* Animate height collapse */
                height: 24px; /* Approx height of header */
                padding-top: 5px;
                padding-bottom: 5px;
                min-height: 24px;
                overflow: hidden;
            }
            .meta-box.collapsed .meta-content {
                display: none;
            }
            .meta-header {
                display: flex;
                justify-content: space-between;
                align-items: center;
                margin-bottom: 8px;
                cursor: pointer;
                user-select: none;
                font-weight: bold;
            }
            .meta-box.collapsed .meta-header {
                margin-bottom: 0; /* Remove margin when collapsed */
            }
            .meta-header .toggle-icon {
                font-size: 16px;
                padding: 0 5px;
                transition: transform 0.2s; /* Rotate icon */
            }
            .meta-content {
                margin-top: 5px;
            }
            .meta-row {
                display: flex;
                margin-bottom: 5px;
                align-items: baseline; /* Align label and value nicely */
            }
            .meta-label {
                font-weight: 600;
                width: 75px; /* Slightly wider label */
                color: #aaa;
                flex-shrink: 0;
                padding-right: 5px;
            }
            body.theme-light .meta-label {
                color: #555;
            }
            .meta-value {
                flex: 1;
                word-break: break-word; /* Wrap long values */
                display: inline; /* Helps with copy button alignment */
            }
            .copy-btn {
                display: inline-block; /* Make it inline with text */
                cursor: pointer;
                margin-left: 8px;
                font-size: 14px;
                opacity: 0.6;
                transition: opacity 0.2s;
                vertical-align: middle; /* Align with text */
                user-select: none;
            }
            .copy-btn:hover {
                opacity: 1;
            }

            /* Link Preview */
            .preview {
                position: absolute;
                display: none;
                max-width: 350px;
                background-color: #383838;
                color: #eee;
                border-radius: 6px;
                box-shadow: 0 5px 20px rgba(0, 0, 0, 0.4);
                padding: 10px 12px;
                font-size: 13px;
                line-height: 1.4;
                z-index: 10001;
                pointer-events: none;
                transition: opacity 0.15s ease-in-out;
                opacity: 0;
                border: 1px solid rgba(255, 255, 255, 0.1);
            }
            body.theme-light .preview {
                background-color: white;
                color: #333;
                box-shadow: 0 4px 15px rgba(0, 0, 0, 0.15);
                border: 1px solid #eee;
            }
            .preview.active {
                opacity: 1;
                pointer-events: auto; /* Allow interaction when visible */
                display: block;
            }
            .preview-header {
                display: flex;
                align-items: center;
                margin-bottom: 6px;
                font-weight: 600;
                color: #f5f5f5; /* Slightly off-white */
            }
            body.theme-light .preview-header {
                color: #444;
            }
            .preview-icon {
                width: 16px;
                height: 16px;
                margin-right: 7px;
                border-radius: 3px;
                background-color: var(--accent);
                display: flex;
                align-items: center;
                justify-content: center;
                font-size: 10px;
                font-weight: bold;
                color: white;
                overflow: hidden;
                flex-shrink: 0;
                text-align: center;
            }
            .preview-icon img {
                width: 100%;
                height: 100%;
                object-fit: cover;
            }
            .preview-content {
                max-height: 100px; /* Limit height */
                overflow: hidden;
                text-overflow: ellipsis;
                /* Consider white-space: normal; if needed */
            }
            .preview-url {
                font-size: 11px;
                color: #aaa;
                margin-top: 6px;
                white-space: nowrap;
                overflow: hidden;
                text-overflow: ellipsis;
            }
            body.theme-light .preview-url {
                color: #777;
            }

            /* Text Enhancement Styles */
            body.text-enhanced {
                font-family: Arial, sans-serif !important;
                /* Base font size set by applyFontSize */
                line-height: 1.55 !important; /* Slightly more spacing */
            }
            .content-container, .content-container-generated {
                max-width: 850px !important;
                margin: 25px auto !important; /* More top/bottom margin */
                padding: 25px 35px !important; /* More padding */
                border-radius: 8px !important;
                /* Font size applied via JS */
            }
            body.theme-dark .content-container,
            body.theme-dark .content-container-generated {
                background-color: var(--dark-content-bg) !important;
                box-shadow: 0 3px 10px rgba(0,0,0,0.2);
            }
            body.theme-light .content-container,
            body.theme-light .content-container-generated {
                background-color: var(--light-content-bg) !important;
                box-shadow: 0 2px 10px rgba(0,0,0,0.08) !important;
            }

            /* Headings */
            .content-container h1, .content-container h2, .content-container h3,
            .content-container h4, .content-container h5, .content-container h6 {
                margin-top: 1.2em !important;
                margin-bottom: 0.5em !important;
                font-weight: 600;
                line-height: 1.2;
            }
            body.theme-dark .content-container h1,
            body.theme-dark .content-container h2,
            body.theme-dark .content-container h3 { color: #eee !important; }
            body.theme-light .content-container h1,
            body.theme-light .content-container h2,
            body.theme-light .content-container h3 { color: #222 !important; }

            .content-container h1 {
                font-size: 2.1em !important;
                border-bottom: 1px solid var(--border-color-dark);
                padding-bottom: 0.3em;
            }
            .content-container h2 {
                font-size: 1.7em !important;
                border-bottom: 1px solid var(--border-color-dark);
                padding-bottom: 0.3em;
            }
            .content-container h3 { font-size: 1.4em !important; }
            .content-container h4 { font-size: 1.2em !important; }
            .content-container h5 { font-size: 1.1em !important; }
            .content-container h6 { font-size: 1.0em !important; color: #888; } /* Dim h6 */

            body.theme-light .content-container h1,
            body.theme-light .content-container h2 { border-bottom-color: var(--border-color-light); }

            /* Links */
            .content-container a {
                color: var(--accent) !important;
                text-decoration: none !important;
                transition: color 0.2s;
            }
            .content-container a:hover {
                color: var(--accent-hover) !important;
                text-decoration: underline !important;
            }

            /* Blockquotes */
            .content-container blockquote,
            .content-container .blockquote-single,
            .content-container .blockquote-multi {
                border-left: 4px solid var(--blockquote-border) !important;
                padding: 10px 18px !important; /* More padding */
                margin: 1em 0 !important;
                border-radius: 0 4px 4px 0; /* Rounded right corners */
                font-style: italic; /* Often blockquotes are italicized */
            }
            body.theme-dark .content-container blockquote,
            body.theme-dark .content-container .blockquote-single,
            body.theme-dark .content-container .blockquote-multi {
                color: #ccc !important;
                background-color: var(--blockquote-bg-dark);
            }
            body.theme-light .content-container blockquote,
            body.theme-light .content-container .blockquote-single,
            body.theme-light .content-container .blockquote-multi {
                color: #555 !important;
                background-color: var(--blockquote-bg-light);
            }

            /* Lists - Handled by divs generated in JS */
            .content-container .list-item {
                 margin-bottom: 0.3em;
                 line-height: 1.4;
            }
             .content-container .numbered-item {
                font-weight: normal; /* Less bold than header */
             }
             .content-container .bullet-item::before {
                 content: "•"; /* Standard bullet */
                 margin-right: 8px;
                 color: var(--accent); /* Themed bullet */
             }
             .content-container .nested-bullet-item::before {
                 content: "◦"; /* Open circle for nested */
                 margin-right: 8px;
                 color: var(--accent);
             }
            /* Ensure JS-generated padding is respected */
             .content-container .bullet-item,
             .content-container .nested-bullet-item {
                display: list-item; /* Use list-item display for pseudo-elements */
                list-style-position: inside; /* Keep bullet inside padding */
                list-style-type: none; /* Hide default browser bullet */
             }

             /* Horizontal Rule */
            .content-container hr.markdown-hr {
                border: 0;
                height: 1px;
                background: var(--border-color-dark);
                margin: 1.5em 0; /* More spacing around hr */
            }
            body.theme-light .content-container hr.markdown-hr {
                background: var(--border-color-light);
            }

             /* Bold / Italic / Strikethrough */
            .content-container strong, .content-container b { font-weight: 600 !important; }
            .content-container em, .content-container i { font-style: italic !important; }
            .content-container del { text-decoration: line-through; color: #888; } /* Dim strikethrough */

            /* Code Block Styling */
            .code-block-container {
                position: relative;
                background-color: var(--code-bg-dark) !important;
                border: 1px solid var(--border-color-dark);
                border-radius: 6px !important;
                margin: 1.2em 0 !important; /* Consistent margin */
                padding: 0 !important; /* Remove padding, handled by children */
                overflow: hidden !important;
            }
            body.theme-light .code-block-container {
                background-color: var(--code-bg-light) !important;
                border-color: var(--border-color-light);
            }

            .code-header {
                display: flex;
                justify-content: space-between;
                align-items: center;
                padding: 6px 12px; /* Slightly tighter header */
                background: var(--code-header-bg-dark);
                border-bottom: 1px solid var(--border-color-dark);
                font-family: 'Consolas', 'Monaco', monospace !important;
                font-size: 12px;
                color: #ccc;
            }
            body.theme-light .code-header {
                background: var(--code-header-bg-light);
                border-bottom: 1px solid var(--border-color-light);
                color: #555;
            }

            .code-language {
                color: #aaa;
                text-transform: lowercase;
                font-weight: bold;
            }
            body.theme-light .code-language {
                color: #666;
            }

            .code-copy-btn {
                background: rgba(255,255,255,0.1);
                border: 1px solid rgba(255,255,255,0.2);
                color: #fff;
                font-size: 11px;
                padding: 3px 8px; /* Smaller padding */
                border-radius: 4px;
                cursor: pointer;
                transition: all 0.2s;
                font-weight: bold;
                text-transform: uppercase;
                letter-spacing: 0.5px;
                user-select: none;
            }
            body.theme-light .code-copy-btn {
                background: rgba(0,0,0,0.05);
                border: 1px solid rgba(0,0,0,0.15);
                color: #333;
            }
            .code-copy-btn:hover {
                background-color: rgba(255,255,255,0.2);
                transform: translateY(-1px);
                box-shadow: 0 1px 3px rgba(0,0,0,0.2);
            }
            body.theme-light .code-copy-btn:hover {
                background-color: rgba(0,0,0,0.1);
            }

            /* Actual code area */
            .code-block-container code {
                display: block;
                padding: 15px !important; /* Padding inside code area */
                overflow-x: auto !important; /* Allow horizontal scroll */
                font-family: 'Consolas', 'Monaco', monospace !important;
                font-size: 0.92em; /* Slightly adjust size */
                line-height: 1.5;
                color: #e9e9e9;
                background: transparent !important; /* Ensure no extra bg */
                border-radius: 0 !important; /* No radius on code tag itself */
            }
            body.theme-light .code-block-container code {
                color: #2d2d2d !important;
            }

            /* Inline code */
            .content-container code.inline-code {
                font-family: 'Consolas', 'Monaco', monospace !important;
                background-color: rgba(128, 128, 128, 0.15) !important;
                padding: 2px 5px !important;
                border-radius: 4px !important;
                font-size: 0.9em;
                color: inherit; /* Inherit surrounding text color */
                vertical-align: baseline; /* Align nicely with text */
            }
            body.theme-dark .content-container code.inline-code {
                 background-color: rgba(255, 255, 255, 0.1) !important;
            }
             body.theme-light .content-container code.inline-code {
                 background-color: rgba(0, 0, 0, 0.08) !important;
             }
        `);
    }

})();