Arena.ai | Collapsible Code Blocks

Adds collapsible code blocks with clickable headers, footer controls, and a global toolbar toggle

スクリプトをインストールするには、Tampermonkey, GreasemonkeyViolentmonkey のような拡張機能のインストールが必要です。

You will need to install an extension such as Tampermonkey to install this script.

スクリプトをインストールするには、TampermonkeyViolentmonkey のような拡張機能のインストールが必要です。

スクリプトをインストールするには、TampermonkeyUserscripts のような拡張機能のインストールが必要です。

このスクリプトをインストールするには、Tampermonkeyなどの拡張機能をインストールする必要があります。

このスクリプトをインストールするには、ユーザースクリプト管理ツールの拡張機能をインストールする必要があります。

(ユーザースクリプト管理ツールは設定済みなのでインストール!)

Advertisement:

このスタイルをインストールするには、Stylusなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus などの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus tなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

(ユーザースタイル管理ツールは設定済みなのでインストール!)

Advertisement:

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください
// ==UserScript==
// @name         Arena.ai | Collapsible Code Blocks
// @namespace    https://greasyfork.org/en/users/1462137-piknockyou
// @version      5.4.1
// @author       Piknockyou (vibe-coded)
// @license      AGPL-3.0
// @description  Adds collapsible code blocks with clickable headers, footer controls, and a global toolbar toggle
// @match        *://*arena.ai/*
// @match        *://*canaryarena.ai/*
// @icon         https://arena.ai/favicon.ico
// @grant        GM_setValue
// @grant        GM_getValue
// @run-at       document-idle
// ==/UserScript==

/*
    What this script does
    ---------------------
    - Makes the code block header clickable to toggle (collapse/expand).
    - Adds an in-flow footer "Collapse" bar per code block (never covers code).
    - Uses a single global fixed footer above the input when the in-flow footer would be obscured.
      (The in-flow footer hides via visibility to prevent layout/scroll jitter.)
    - Adds a dual toolbar button next to the input controls:
      * Collapse all
      * Expand all
      * Hold collapse to toggle persistent auto-collapse

    Detection/updates model
    -----------------------
    - Initial scan: finds all current code blocks once on load.
    - Incremental scan: MutationObserver looks only at added DOM nodes and initializes new code blocks.
    - Safety net: periodic resync detects missed blocks and runs a full scan.
    - Fixed footer updates on scroll/resize (throttled via requestAnimationFrame).
*/

// ═══════════════════════════════════════════════════════════════════════
// CHANGELOG (newest to oldest)
// ═══════════════════════════════════════════════════════════════════════
//
// v5.4.1
// - Cleanup: removed stale "embedded or floating" comment (floating mode was removed in v5.3)
// - Cleanup: removed extra blank line remnant
//
// v5.4
// - Cleanup: reordered changelog from newest to oldest
// - Cleanup: consolidated duplicate v4.6 entries
// - Cleanup: fixed misplaced v5.2-diag.x entries
//
// v5.3.2
// - Cleanup (Tier B): simplify diagnostic state tracking (keep DIAG_MODE toggle for future debugging)
// - Cleanup: remove unused tooltip CSS (left/right arrows + tooltip-main/hint selectors)
// - Robustness: make toolbar reinjection tolerant of transient form rerenders (reduces flicker; keeps re-injection reliability)
// - Fix: remove stale reference to non-existent toolbar mode variable in post-init verification
//
// v5.3
// - Cleanup: removed hybrid/floating/drag toolbar modes (back to embedded toolbar only)
// - Cleanup: removed dynamic tooltip positioning + drag UI logic (keeps simple tooltips/hold-to-toggle)
// - Cleanup: diagnostics disabled by default (can be re-enabled for troubleshooting)
// - Kept: background-tab safe init (prevents React hydration mismatch / error #418)
//
// v5.2-diag.2
// - Fix: Do not mutate React DOM while the tab is hidden (prevents hydration mismatch / React error #418)
// - Added: Deferred initialization (pendingStart) that runs only after tab becomes visible
// - Diagnostics: log when init is deferred due to background tab state
//
// v5.2-diag.1
// - Fix: Add missing diagnostic helpers (logStage/diagDOMState/initStages) to prevent ReferenceError on load
// - Diagnostics: Expand debug output to help reproduce background-tab initialization issues
//
// v5.2
// - MAJOR: Hybrid toolbar mode - embedded when safe, floating on conflict
// - Embedded mode: integrates into input area, non-draggable (like pre-5.0)
// - Floating mode: activated automatically if React removes embedded button
// - Smart tooltips: dynamically position to never be cut off by viewport
// - Tooltips now hide during drag operations
// - Tooltips explain right-drag functionality
// - Toolbar only appears when chat input area is present
//
// v5.1
// - Fixed: Toolbar now always visible on chat pages (dimmed when no code blocks)
// - Fixed: Default position moved slightly more visible (bottom-right, offset from edge)
//
// v5.0
// - Added: @match *://*canaryarena.ai/*
// - MAJOR: Toolbar is now a floating button (never injected into React DOM)
// - Fixed: No longer conflicts with other userscripts' UI elements
// - Fixed: Works correctly when opening in background tabs
// - Added: Right-drag to move the toolbar button anywhere
// - Added: Position persisted per-session (survives page navigation)
// - Added: Tooltip on hover explaining controls
// - Removed: MutationObserver-based toolbar re-creation (no longer needed)
// - Simplified: Removed form/textarea dependency for toolbar placement
//
// v4.9
// - Renamed from LMArena to Arena.ai
//
// v4.8
// - Removed: width preservation on collapse (caused horizontal scrolling on narrow viewports)
// - Collapsed blocks now use natural width instead of forced minWidth
//
// v4.7
// - Cleanup: removed unused `data` parameter from internal log function
// - Cleanup: removed unused boolean return values from createToolbarButton()
// - Cleanup: consolidated duplicate CSS rules and keyframe definitions
//
// v4.6
// - Fixed: single-block collapse no longer "snaps" the header down to the old footer position in some scroll states
// - Changed: when collapsing a block while its header is visible, the header is used as the scroll anchor (keeps it stable)
// - Fixed: auto-collapse on newly added blocks no longer triggers single-block scroll behavior
// - Fixed: single-block expand now first stabilizes the header position (cancels browser/scroll-area anchoring)
// - Refined: only scrolls header to top if it ends up off-screen or in the bottom ~15% of the viewport
//
// v4.5
// - Refined: single-block expand only scrolls if header is in bottom 1/3 of viewport or off-screen
// - If header is already visible in upper 2/3, no scroll adjustment occurs
//
// v4.4
// - Fixed: single-block expand now actively scrolls header to top of viewport
// - Ensures user always sees expanded code from the beginning (overrides browser scroll anchoring)
//
// v4.3
// - Changed: single-block expand no longer uses scroll anchoring
// - Expanding a block now keeps the header in place; code appears below (no viewport shift)
// - Lets user read expanded code from the beginning instead of being scrolled to the bottom
//
// v4.2
// - Changed: replaced localStorage with GM_setValue/GM_getValue for settings persistence
// - Settings now stored via userscript manager (more reliable, syncs across browsers if supported)
//
// v4.1
// - Cleanup: removed overridden CSS declaration (border-radius: 0 0 0 0)
// - Cleanup: removed unused 'warn' log style
// - Cleanup: removed redundant typeof guard for activeFixedState
// - Cleanup: consolidated duplicate mouseleave handlers on collapse button
//
// v4.0
// - Fixed: fixed footer now repositions when textarea grows during user input
// - Added ResizeObserver on input area to detect height changes
// - Footer no longer conflicts with/covers the textarea when it expands
//
// v3.9
// - Replaced hardcoded delay with dynamic Stabilization Observer
// - Detects React hydration completion by watching for the chat input bar
// - Uses requestIdleCallback for optimal performance/compatibility
//
// v3.8
// - Fixed: conflict with other userscripts (Multi-Provider Chat Export, etc.)
// - Added 1000ms initialization delay to avoid React Hydration Error #418
//
// v3.7
// - Script name changed from "LMArena | Code Block Collapse" to "LMArena | Collapsible Code Blocks"
//
// v3.6
// - Cleanup/docs: removed leftover floating-button wording and unused bits
//
// v3.5
// - Fixed: during smooth auto-scroll, fixed footer now disappears immediately on header collision
// - Prevents "jumping" the fixed footer to a different code block when the bottom-most one collides
// - More robust height math: uses in-flow footer height as fallback when fixed footer is hidden
//
// v3.4
// - Fixed: robust footer swap (in-flow footer hides, separate fixed footer shows) to eliminate flicker/partial overlap
// - Fixed footer never covers the block header: hides as soon as it would touch the header
// - In-flow footer is hidden via visibility (keeps layout stable; prevents scroll/height jitter)
//
// v3.3
// - Changed transition logic: switch to fixed as soon as natural footer TOUCHES input area
// - Natural footer is never partially covered (cleaner visual)
// - Switch back to natural only when full room available (hysteresis prevents flicker)
//
// v3.2
// - Fixed: flickering during transition between natural and fixed footer positions
// - Added hysteresis: switch to fixed only when natural is FULLY hidden behind input
// - Added hysteresis: switch to natural only when natural would be FULLY visible
// - Natural footer now allowed to scroll partially behind input area (no early switch)
//
// v3.1
// - Styled footer bar to match header (uses site's CSS variables for colors/borders)
// - Fixed: footer now positions above input area (not at viewport bottom)
// - Fixed: footer keeps rounded corners when in fixed mode
// - Removed hardcoded footer colors from CONFIG (now uses site theme)
//
// v3.0
// - Fixed: footer now properly follows viewport when scrolling through tall code blocks
// - CSS sticky alone doesn't work for "show at viewport bottom when natural position is below"
// - Re-added JavaScript positioning: fixed position when block bottom is below viewport,
//   natural position when block bottom is visible (never covers code)
// - Re-added scroll/resize handlers with rAF throttling for footer positioning
//
// v2.9
// - Replaced floating collapse button with sticky footer bar
// - Footer bar is part of code block structure (like the header)
// - Footer sticks to viewport bottom while scrolling through tall blocks
// - Footer sits at natural position when code block bottom is visible (never covers code)
// - Removed all floating button positioning logic (simpler, more performant)
//
// v2.8
// - Tooltips now explicitly mention "code blocks" and toolbar buttons include aria-labels
//
// v2.7
// - Fixed: hover tooltips were hidden by inline visibility/opacity styles (now cleared after measurement)
//
// v2.6
// - Fixed: tooltips now properly appear on hover
// - Changed: collapse button icon to fold style (two chevrons pointing inward)
//
// v2.5
// - Changed: expand button icon to unfold style (avoids dropdown menu confusion)
// - Added: fill animation on collapse button during hold gesture
// - Changed: tooltips now appear above buttons (not below/as title attributes)
// - Added: hover tooltips for both buttons explaining their function
//
// v2.4
// - Redesigned: global toggle is now a dual button (collapse/expand side by side)
// - Added: hold collapse button for 1s to toggle persistent auto-collapse mode
// - Added: tooltip hint appears at 0.5s explaining hold-to-activate feature
// - Removed: right-click context menu (replaced by hold gesture)
//
// v2.3
// - Fixed: scroll anchoring when viewing middle of a large code block
//   (now detects "inside block" state and scrolls to show header after collapse)
//
// v2.2
// - Added: scroll anchoring to preserve visual position when collapsing
//   (prevents jarring scroll jumps when collapsing large code blocks)
//
// v2.1
// - Fixed: code blocks now preserve their original width when collapsed
// - Changed: global toggle button moved to bottom-right (left of submit button)
//
// v2.0
// - Cleanup: removed unused animation config (no longer used after switching to display:none collapse)
// - Cleanup: removed unused destructured `animation` variable in injectStyles()
// - Optional cleanup: removed unused debug helpers (getState/getAutoMode) and ContextMenu.isOpen
//
// v1.9
// - Cleanup: removed unused batch state variables left over from pre-v1.8 batching
// - Cleanup: removed stale "auto-collapse/auto-expand" wording
//
// v1.8
// - Fixed UI flicker/stutter when toggling all code blocks
// - Removed requestAnimationFrame batching from collapseAll/expandAll
//   (spreading DOM changes across frames caused multiple layout recalcs,
//   each shifting content by 1-2px before settling; synchronous is smoother
//   since display:none doesn't trigger expensive reflows)
// - Removed auto-expand feature (collapse-only is cleaner UX)
// - Simplified context menu
//
// v1.5
// - Commenting pass / documentation improvements (no functional changes)
// - Corrected init log version string
//
// v1.4
// - Batched global collapse/expand across frames to avoid long UI stalls
//
// v1.3
// - Switched to incremental MutationObserver (only processes added nodes)
// - Added 5-second fallback resync to catch edge cases
// - Performance: avoids repeated full DOM rescans during streaming

(function() {
    'use strict';

    // ═══════════════════════════════════════════════════════════════════════
    // DEBUG (DIAGNOSTIC MODE)
    // ═══════════════════════════════════════════════════════════════════════
    // Toggle this to silence console output from this script.
    const DEBUG = true;

    // Diagnostic verbosity (extra logs + DOM snapshots).
    const DIAG_MODE = false;

    // Simple structured logger (colored prefix + levels). When DEBUG=false, it’s a no-op.
    const log = (msg, type = 'info') => {
        if (!DEBUG) return;

        // Silence diagnostic-only log levels unless DIAG_MODE is enabled
        if (!DIAG_MODE && (type === 'diag' || type === 'critical')) return;

        const prefix = '[CBC]';
        const timestamp = new Date().toISOString().slice(11, 23); // HH:MM:SS.mmm

        const styles = {
            info: 'color: #8ab4f8',
            success: 'color: #34a853',
            error: 'color: #ea4335',
            diag: 'color: #f59e0b; font-weight: 700',
            critical: 'color: #ff3333; font-weight: 800'
        };

        console.log(`%c${prefix} [${timestamp}] ${msg}`, styles[type] || styles.info);
    };

    // Minimal runtime state for robustness gating.
    // (We intentionally avoid touching React DOM while the tab is hidden.)
    let hasStarted = false;
    let initComplete = false;

    // Lightweight diagnostic stage tracking (only used when DIAG_MODE=true).
    const diagStages = new Map(); // stage -> boolean

    const logStage = (stage, success = true) => {
        if (!DIAG_MODE) return;

        diagStages.set(stage, success);
        log(`📍 STAGE: ${stage} = ${success}`, success ? 'diag' : 'error');

        const entries = Array.from(diagStages.entries());
        const completed = entries.filter(([, v]) => v).map(([k]) => k);
        const pending = entries.filter(([, v]) => !v).map(([k]) => k);

        console.log(
            '%c[CBC] Init Progress:',
            'color: #8ab4f8',
            `✓ ${completed.length}/${entries.length || 1}`,
            '\nCompleted:', completed.join(', ') || '(none)',
            '\nPending:', pending.join(', ') || '(none)'
        );
    };

    // Diagnostic helper - logs current DOM state as a table.
    const diagDOMState = (label) => {
        if (!DIAG_MODE) return null;

        const submitBtn = document.querySelector('form button[type="submit"]');
        const buttonRow = submitBtn?.closest('.flex.items-center.gap-2') || null;

        const state = {
            label,
            url: location.href,
            documentReady: document.readyState,
            visibilityState: document.visibilityState,
            hidden: document.hidden,

            bodyExists: !!document.body,
            headExists: !!document.head,

            formExists: !!document.querySelector('form'),
            textareaExists: !!document.querySelector('form textarea'),
            submitBtnExists: !!submitBtn,
            buttonContainerExists: !!buttonRow,

            codeBlockCount: document.querySelectorAll('[data-code-block="true"]').length,

            cbcToolbarExists: !!document.getElementById('cbc-toolbar'),
            cbcToolbarInDOM: (() => {
                const t = document.getElementById('cbc-toolbar');
                return !!(t && document.body && document.body.contains(t));
            })(),

            // Optional: detect the export script button (if installed)
            otherScript_chatExportBtn: !!document.getElementById('chat-export-btn')
        };

        console.log('%c[CBC] 🔍 DOM STATE:', 'color: #f59e0b; font-weight: 800', label);
        try { console.table(state); } catch (_) { console.log(state); }
        return state;
    };

    // Mark script start early (helps diagnose background-tab issues).
    logStage('scriptStart');
    log('🚀 Script file executing (diagnostic mode)', 'critical');
    diagDOMState('Script Start');

    // ═══════════════════════════════════════════════════════════════════════
    // CONFIGURATION
    // ═══════════════════════════════════════════════════════════════════════
    // Notes:
    // - `resyncInterval` is a safety net for missed mutations (rare edge cases).
    const CONFIG = {
        header: {
            hoverBg: 'rgba(255,255,255,0.04)',
            activeBg: 'rgba(255,255,255,0.08)',
            collapsedHoverBg: 'rgba(255,255,255,0.06)',
            collapsedActiveBg: 'rgba(255,255,255,0.10)'
        },
        storageKey: 'arena_codeblock_collapse_settings',
        resyncInterval: 5000 // Fallback resync every 5 seconds
    };

    // ═══════════════════════════════════════════════════════════════════════
    // STATE & STORAGE
    // ═══════════════════════════════════════════════════════════════════════

    // Tracks each initialized code block element -> per-block state object.
    // This is what prevents us from re-initializing blocks on every mutation.
    const blockState = new Map();

    // Persisted user preference: should new blocks auto-collapse?
    const DEFAULT_SETTINGS = {
        autoMode: 'off' // 'off' or 'collapse'
    };

    let settings = { ...DEFAULT_SETTINGS };

    // ResizeObserver for textarea height changes (typing causes growth)
    let inputResizeObserver = null;
    let resizeRafId = null;

    // Load settings from userscript manager storage.
    function loadSettings() {
        try {
            const saved = GM_getValue(CONFIG.storageKey, {});
            settings = { ...DEFAULT_SETTINGS, ...saved };
            log(`Settings loaded: autoMode=${settings.autoMode}`, 'success');
        } catch (e) {
            log('Failed to load settings: ' + e.message, 'error');
        }
    }

    // Save settings to userscript manager storage.
    function saveSettings() {
        try {
            GM_setValue(CONFIG.storageKey, settings);
            log(`Settings saved: autoMode=${settings.autoMode}`, 'success');
        } catch (e) {
            log('Failed to save settings: ' + e.message, 'error');
        }
    }

    // ═══════════════════════════════════════════════════════════════════════
    // STYLES
    // ═══════════════════════════════════════════════════════════════════════

    // Inject our stylesheet once. We key off #cbc-styles to avoid duplicates
    // across SPA navigation/rerenders.
    function injectStyles() {
        if (document.getElementById('cbc-styles')) return;

        const { header } = CONFIG;

        const style = document.createElement('style');
        style.id = 'cbc-styles';
        style.textContent = `
            /* Code container - instant toggle, no animation */
            .cbc-container {
                overflow: hidden;
            }
            .cbc-container.cbc-collapsed {
                display: none !important;
            }

            /* Clickable header - always interactive */
            .cbc-header-interactive {
                transition: background-color 0.15s ease, border-radius 0.15s ease;
                position: relative;
                cursor: pointer;
                contain: layout style;
            }
            .cbc-header-interactive:hover {
                background-color: ${header.hoverBg};
            }
            .cbc-header-interactive:active {
                background-color: ${header.activeBg};
            }

            /* Collapsed state styling */
            .cbc-header-interactive.cbc-header-collapsed {
                border-bottom-color: transparent !important;
                border-radius: 6px !important;
            }
            .cbc-header-interactive.cbc-header-collapsed:hover {
                background-color: ${header.collapsedHoverBg};
            }
            .cbc-header-interactive.cbc-header-collapsed:active {
                background-color: ${header.collapsedActiveBg};
            }

            /* State hint indicator (appears on header hover) - centered */
            .cbc-state-hint {
                position: absolute;
                left: 50%;
                top: 50%;
                transform: translate(-50%, -50%);
                display: flex;
                align-items: center;
                gap: 6px;
                padding: 4px 10px;
                font-size: 11px;
                color: rgba(255,255,255,0.3);
                font-family: system-ui, -apple-system, sans-serif;
                background: rgba(30,30,30,0.95);
                border-radius: 4px;
                opacity: 0;
                pointer-events: none;
                z-index: 10;
                white-space: nowrap;
            }
            .cbc-header-interactive:hover .cbc-state-hint {
                opacity: 1;
                color: rgba(255,255,255,0.7);
            }
            .cbc-state-hint svg {
                opacity: 0.7;
            }

            /* Hint text changes based on state */
            .cbc-state-hint .hint-expand { display: none; }
            .cbc-state-hint .hint-collapse { display: flex; align-items: center; gap: 6px; }
            .cbc-header-collapsed .cbc-state-hint .hint-expand { display: flex; align-items: center; gap: 6px; }
            .cbc-header-collapsed .cbc-state-hint .hint-collapse { display: none; }

            /* Footer bar for collapse action - matches header style */
            .cbc-footer {
                position: relative;
                z-index: 10;
                display: flex;
                align-items: center;
                justify-content: center;
                gap: 6px;
                padding: 8px 16px;
                border-top: 1px solid var(--border, rgba(255,255,255,0.1));
                border-radius: 0 0 6px 6px;
                cursor: pointer;
                transition: background-color 0.15s ease;
                user-select: none;
                font-size: 13px;
                font-weight: 500;
                color: var(--text-secondary, rgba(255,255,255,0.5));
            }
            .cbc-footer:hover {
                background-color: ${header.hoverBg};
                color: var(--text-primary, rgba(255,255,255,0.9));
            }
            .cbc-footer:active {
                background-color: ${header.activeBg};
            }
            .cbc-footer svg {
                opacity: 0.7;
                transition: opacity 0.15s ease;
            }
            .cbc-footer:hover svg {
                opacity: 1;
            }
            .cbc-footer-hidden {
                display: none !important;
            }

            /* Hidden because a separate global fixed footer is active (keeps layout stable) */
            .cbc-footer-hidden-by-fixed {
                visibility: hidden !important;
                pointer-events: none !important;
            }

            /* Footer in fixed mode (global, appended to <body>) */
            .cbc-footer.cbc-footer-fixed {
                position: fixed;
                border-radius: 0 0 6px 6px;
                box-shadow: 0 -2px 8px rgba(0,0,0,0.25);
                background-color: var(--surface-primary, #1a1a1a);
                backdrop-filter: blur(8px);
                -webkit-backdrop-filter: blur(8px);
            }

            /* Embedded toolbar (inside React form) */
            .cbc-toolbar-embedded {
                display: inline-flex;
                align-items: stretch;
                border-radius: 6px;
                overflow: hidden;
                border: 1px solid rgba(255,255,255,0.15);
                background: rgba(255,255,255,0.03);
                height: 32px;
            }
            .cbc-toolbar-embedded button {
                display: flex;
                align-items: center;
                justify-content: center;
                width: 28px;
                height: 100%;
                border: none;
                background: transparent;
                color: rgba(255,255,255,0.6);
                cursor: pointer;
                transition: background 0.15s ease, color 0.15s ease;
                padding: 0;
                margin: 0;
                outline: none;
            }
            .cbc-toolbar-embedded button:hover {
                background: rgba(255,255,255,0.08);
                color: rgba(255,255,255,0.9);
            }
            .cbc-toolbar-embedded button:active {
                background: rgba(255,255,255,0.12);
            }
            .cbc-toolbar-embedded .cbc-collapse-btn {
                border-right: 1px solid rgba(255,255,255,0.1);
                position: relative;
                overflow: hidden;
            }
            .cbc-toolbar-embedded.auto-active {
                border-color: rgba(249, 115, 22, 0.4);
            }
            .cbc-toolbar-embedded.auto-active .cbc-collapse-btn {
                background: rgba(249, 115, 22, 0.15);
                color: #f97316;
            }
            .cbc-toolbar-embedded.auto-active .cbc-collapse-btn:hover {
                background: rgba(249, 115, 22, 0.25);
                color: #fb923c;
            }
            .cbc-toolbar-embedded.no-blocks button {
                opacity: 0.3;
                pointer-events: none;
            }

            /* Smart tooltip with dynamic positioning */
            .cbc-hold-tooltip {
                position: fixed;
                background: #1a1a1a;
                border: 1px solid #3a3a3a;
                border-radius: 6px;
                padding: 6px 10px;
                font-family: system-ui, -apple-system, sans-serif;
                font-size: 11px;
                color: #e0e0e0;
                box-shadow: 0 4px 12px rgba(0,0,0,0.4);
                z-index: 2147483647;
                pointer-events: none;
                opacity: 0;
                visibility: hidden;
                transition: opacity 0.15s ease, visibility 0.15s ease;
                white-space: nowrap;
                max-width: 220px;
            }
            .cbc-hold-tooltip.visible {
                opacity: 1;
                visibility: visible;
            }
            /* Arrow for bottom position (tooltip above button) */
            .cbc-hold-tooltip[data-arrow="bottom"]::before,
            .cbc-hold-tooltip[data-arrow="bottom"]::after {
                content: '';
                position: absolute;
                left: 50%;
                transform: translateX(-50%);
            }
            .cbc-hold-tooltip[data-arrow="bottom"]::before {
                bottom: -6px;
                border-left: 6px solid transparent;
                border-right: 6px solid transparent;
                border-top: 6px solid #3a3a3a;
            }
            .cbc-hold-tooltip[data-arrow="bottom"]::after {
                bottom: -5px;
                border-left: 5px solid transparent;
                border-right: 5px solid transparent;
                border-top: 5px solid #1a1a1a;
            }
            /* Arrow for top position (tooltip below button) */
            .cbc-hold-tooltip[data-arrow="top"]::before,
            .cbc-hold-tooltip[data-arrow="top"]::after {
                content: '';
                position: absolute;
                left: 50%;
                transform: translateX(-50%);
            }
            .cbc-hold-tooltip[data-arrow="top"]::before {
                top: -6px;
                border-left: 6px solid transparent;
                border-right: 6px solid transparent;
                border-bottom: 6px solid #3a3a3a;
            }
            .cbc-hold-tooltip[data-arrow="top"]::after {
                top: -5px;
                border-left: 5px solid transparent;
                border-right: 5px solid transparent;
                border-bottom: 5px solid #1a1a1a;
            }
            /* Confirmation states */
            .cbc-hold-tooltip.confirmed {
                background: rgba(249, 115, 22, 0.9);
                border-color: #f97316;
                color: #fff;
                font-weight: 500;
            }
            .cbc-hold-tooltip.confirmed[data-arrow="bottom"]::before { border-top-color: #f97316; }
            .cbc-hold-tooltip.confirmed[data-arrow="bottom"]::after { border-top-color: rgba(249, 115, 22, 0.9); }
            .cbc-hold-tooltip.confirmed[data-arrow="top"]::before { border-bottom-color: #f97316; }
            .cbc-hold-tooltip.confirmed[data-arrow="top"]::after { border-bottom-color: rgba(249, 115, 22, 0.9); }
            .cbc-hold-tooltip.deactivated {
                background: rgba(100, 100, 100, 0.95);
                border-color: #666;
                color: #fff;
                font-weight: 500;
            }
            .cbc-hold-tooltip.deactivated[data-arrow="bottom"]::before { border-top-color: #666; }
            .cbc-hold-tooltip.deactivated[data-arrow="bottom"]::after { border-top-color: rgba(100, 100, 100, 0.95); }
            .cbc-hold-tooltip.deactivated[data-arrow="top"]::before { border-bottom-color: #666; }
            .cbc-hold-tooltip.deactivated[data-arrow="top"]::after { border-bottom-color: rgba(100, 100, 100, 0.95); }
        `;
        document.head.appendChild(style);
    }

    // ═══════════════════════════════════════════════════════════════════════
    // SCROLL ANCHORING
    // ═══════════════════════════════════════════════════════════════════════
    // When collapsing code blocks, content below shifts up. Without anchoring,
    // this causes jarring scroll jumps. We fix this by:
    // 1. Finding a visible "anchor" element before collapse
    // 2. Recording its viewport position
    // 3. After collapse, adjusting scroll to restore anchor's visual position

    // Get the scrollable container (Radix scroll area viewport).
    function getScrollContainer() {
        return document.querySelector('[data-radix-scroll-area-viewport]');
    }

    // Find the best anchor element currently visible in the viewport.
    // Returns { element, offsetTop } or null.
    // Prefers code block headers since they stay visible when collapsed.
    function findScrollAnchor() {
        const scrollContainer = getScrollContainer();
        if (!scrollContainer) return null;

        const containerRect = scrollContainer.getBoundingClientRect();
        const viewportTop = containerRect.top;
        const viewportBottom = containerRect.bottom;
        const viewportCenter = viewportTop + containerRect.height / 2;

        // Priority: code block headers (stay visible), then message prose
        const candidates = [
            ...document.querySelectorAll('.cbc-header-interactive'),
            ...document.querySelectorAll('.prose > p'),
        ];

        let bestAnchor = null;
        let bestDistance = Infinity;

        for (const el of candidates) {
            const rect = el.getBoundingClientRect();
            // Must be at least partially visible in the scroll container
            if (rect.bottom < viewportTop || rect.top > viewportBottom) continue;

            // Prefer elements closer to center of viewport (more stable feel)
            const elCenter = rect.top + rect.height / 2;
            const distance = Math.abs(elCenter - viewportCenter);

            if (distance < bestDistance) {
                bestDistance = distance;
                bestAnchor = el;
            }
        }

        if (!bestAnchor) return null;

        return {
            element: bestAnchor,
            offsetTop: bestAnchor.getBoundingClientRect().top
        };
    }

    // Restore scroll position to keep anchor element at its original viewport position.
    function restoreScrollAnchor(anchor) {
        if (!anchor || !anchor.element) return;

        const scrollContainer = getScrollContainer();
        if (!scrollContainer) return;

        // Element may have been removed from DOM (edge case)
        if (!document.body.contains(anchor.element)) return;

        const newRect = anchor.element.getBoundingClientRect();
        const delta = newRect.top - anchor.offsetTop;

        // Only adjust if there's meaningful shift (avoid sub-pixel noise)
        if (Math.abs(delta) > 1) {
            scrollContainer.scrollTop += delta;
        }
    }

    // Check if user is currently viewing "inside" a code block's content.
    // True when: header is above/at viewport top, but code content is visible.
    // This happens when scrolled partway through a tall code block.
    function isViewingBlockContent(state) {
        const scrollContainer = getScrollContainer();
        if (!scrollContainer) return false;

        const header = state.header;
        const container = state.container;

        const scrollRect = scrollContainer.getBoundingClientRect();
        const headerRect = header.getBoundingClientRect();
        const containerRect = container.getBoundingClientRect();

        // Header is above or at the very top of the scroll viewport (with small buffer)
        const headerAboveViewport = headerRect.bottom <= scrollRect.top + 5;

        // But the code container extends into the visible viewport
        const contentVisible = containerRect.top < scrollRect.bottom &&
                               containerRect.bottom > scrollRect.top;

        return headerAboveViewport && contentVisible;
    }

    // Get the visible bounds of the scroll container (safe zone for fixed elements).
    // Also accounts for the input area at the bottom.
    function getScrollBounds() {
        const scrollContainer = getScrollContainer();
        let top = 0;
        let bottom = window.innerHeight;

        if (scrollContainer) {
            const rect = scrollContainer.getBoundingClientRect();
            top = rect.top;
            bottom = rect.bottom;
        }

        // Account for input area at bottom
        const inputArea = document.querySelector('.relative.flex.flex-col.items-center.pb-6') ||
                          document.querySelector('form')?.closest('div[class*="pb-"]');
        if (inputArea) {
            const inputRect = inputArea.getBoundingClientRect();
            bottom = Math.min(bottom, inputRect.top);
        }

        return { top, bottom };
    }

    // Scroll so the block's header is positioned at the top of the viewport.
    // Used after collapsing a block we were "inside" of.
    function scrollHeaderToTop(state) {
        const scrollContainer = getScrollContainer();
        if (!scrollContainer) return;

        const header = state.header;
        const scrollRect = scrollContainer.getBoundingClientRect();
        const headerRect = header.getBoundingClientRect();

        // Calculate offset from header's current position to top of scroll container
        // Add a small margin (8px) so header isn't flush against the top
        const offset = headerRect.top - scrollRect.top - 8;

        if (Math.abs(offset) > 1) {
            scrollContainer.scrollTop += offset;
        }
    }

    // ═══════════════════════════════════════════════════════════════════════
    // COLLAPSE/EXPAND LOGIC
    // ═══════════════════════════════════════════════════════════════════════

    // Collapse a single code block (instant, no animation).
    // If anchor is provided, scroll anchoring is handled externally (bulk operations).
    // If anchor is null and not explicitly skipped, we find and restore our own anchor.
    function collapseBlock(state, externalAnchor = undefined) {
        log(`Collapsing block`, 'info');
        const { block, container, header, footer } = state;

        // For single-block collapse, determine scroll strategy BEFORE modifying DOM
        // Goals:
        // - If the header is visible: keep THAT header visually stable (use it as the anchor).
        //   This prevents rare "snap down to footer position" behavior caused by browser/scroll-area anchoring.
        // - If the header is NOT visible (often when collapsing from the footer): scroll to show the header.
        let scrollStrategy = null; // 'anchor' | 'scrollToHeader' | null
        let anchor = null;

        if (externalAnchor === undefined) {
            const scrollContainer = getScrollContainer();
            const inside = isViewingBlockContent(state);

            // If we can't detect the scroll container, default to "header is visible"
            // so we don't try to scroll-to-header using a container we can't control.
            let headerVisible = true;

            if (scrollContainer) {
                const scrollRect = scrollContainer.getBoundingClientRect();
                const headerRect = header.getBoundingClientRect();
                headerVisible = headerRect.bottom > scrollRect.top && headerRect.top < scrollRect.bottom;
            }

            // If user is inside the block OR the header isn't visible, collapsing should bring them to the header.
            if (inside || !headerVisible) {
                scrollStrategy = 'scrollToHeader';
                if (inside) {
                    log('Collapse: inside block, will scroll to header', 'info');
                }
            } else {
                // Anchor to THIS header (not an unrelated element near viewport center)
                anchor = { element: header, offsetTop: header.getBoundingClientRect().top };
                scrollStrategy = 'anchor';
            }
        } else {
            anchor = externalAnchor;
        }

        container.classList.add('cbc-collapsed');
        header.classList.add('cbc-header-collapsed');
        footer.classList.add('cbc-footer-hidden');
        footer.classList.remove('cbc-footer-hidden-by-fixed');

        // If this block currently owns the global fixed footer, hide it now.
        if (activeFixedState === state) {
            hideGlobalFixedFooter();
        }

        state.collapsed = true;

        // Handle scroll adjustment for single-block operations
        if (externalAnchor === undefined) {
            if (scrollStrategy === 'scrollToHeader') {
                scrollHeaderToTop(state);
            } else if (scrollStrategy === 'anchor' && anchor) {
                restoreScrollAnchor(anchor);
            }
        }
    }

    // Expand a single code block (instant, no animation).
    // For single-block expand:
    // 1) Stabilize the header position (cancels browser/scroll-area scroll anchoring)
    // 2) Only if the header ends up too low/off-screen, scroll it to the top for readability
    //
    // For bulk expand (externalAnchor !== undefined): caller handles scroll anchoring.
    function expandBlock(state, externalAnchor = undefined) {
        log(`Expanding block`, 'info');
        const { block, container, header, footer } = state;

        // For single-block expand: record header position BEFORE DOM changes
        const scrollContainer = externalAnchor === undefined ? getScrollContainer() : null;
        const preHeaderTop = scrollContainer ? header.getBoundingClientRect().top : null;

        container.classList.remove('cbc-collapsed');
        header.classList.remove('cbc-header-collapsed');
        footer.classList.remove('cbc-footer-hidden');

        state.collapsed = false;

        // Single-block: stabilize header position, then (optionally) move it to top only if it's too low
        if (externalAnchor === undefined && scrollContainer && preHeaderTop !== null) {
            requestAnimationFrame(() => {
                // Step 1: cancel scroll anchoring effects by restoring header's pre-expand viewport position
                const postHeaderTop = header.getBoundingClientRect().top;
                const delta = postHeaderTop - preHeaderTop;

                // Only adjust if there's meaningful shift (avoid sub-pixel noise)
                if (Math.abs(delta) > 1) {
                    scrollContainer.scrollTop += delta;
                }

                // Step 2: if header is still off-screen or very low, bring it to top
                const scrollRect = scrollContainer.getBoundingClientRect();
                const headerRect = header.getBoundingClientRect();

                // "Off-screen" here means fully outside the scroll viewport (not merely clipped by 1px)
                const headerFullyOffScreen =
                    headerRect.bottom <= scrollRect.top ||
                    headerRect.top >= scrollRect.bottom;

                // Only treat the bottom ~15% as "too low" (prevents unnecessary snapping from the middle)
                const bottomZoneStart = scrollRect.top + (scrollRect.height * 0.85);

                if (headerFullyOffScreen || headerRect.top > bottomZoneStart) {
                    scrollHeaderToTop(state);
                }

                updateFixedFooter();
            });
            return;
        }

        // Bulk expand: just update footer
        requestAnimationFrame(() => {
            updateFixedFooter();
        });
    }

    // Simple toggle helper for header click handling.
    function toggleBlock(state) {
        if (state.collapsed) {
            expandBlock(state);
        } else {
            collapseBlock(state);
        }
    }

    // ═══════════════════════════════════════════════════════════════════════
    // PUBLIC API
    // ═══════════════════════════════════════════════════════════════════════
    // Exposed on window for debugging/automation:
    //   window.CodeBlockCollapse.collapseAll()
    //   window.CodeBlockCollapse.expandAll()
    //   window.CodeBlockCollapse.toggleAll()
    //   window.CodeBlockCollapse.setAutoMode('collapse'|'off')
    const CodeBlockCollapse = {
        // Collapse all expanded blocks in a single synchronous pass.
        //
        // Why NOT batched across frames (requestAnimationFrame)?
        // -------------------------------------------------------
        // Previously we chunked DOM updates across multiple frames to avoid
        // "long task" warnings. However, this caused visible UI flicker:
        // each frame triggered a separate layout recalculation, shifting
        // content by 1-2 pixels before settling.
        //
        // Synchronous single-pass works better here because:
        // 1. We use display:none which is cheap (no reflow of hidden content)
        // 2. Browser batches all changes into one layout pass automatically
        // 3. No intermediate states = no flicker
        //
        // Scroll Anchoring:
        // We find a single anchor BEFORE collapsing, then restore AFTER all
        // collapses complete. This prevents jarring scroll jumps.
        //
        // Special case: if we're "inside" one of the blocks being collapsed
        // (header above viewport, content visible), we scroll to show that
        // block's header after all collapses complete.
        collapseAll() {
            const blocks = Array.from(blockState.values()).filter(s => !s.collapsed);
            if (blocks.length === 0) return;

            // BEFORE any DOM changes: determine scroll strategy
            // Priority: if inside a block being collapsed, scroll to its header
            const insideBlock = blocks.find(s => isViewingBlockContent(s));
            const anchor = insideBlock ? null : findScrollAnchor();

            if (insideBlock) {
                log('CollapseAll: detected inside block, will scroll to header', 'info');
            }

            log(`Collapsing ${blocks.length} blocks`, 'info');
            // Pass null to skip per-block anchoring; we handle it once at the end
            blocks.forEach(state => collapseBlock(state, null));

            // AFTER all collapses: adjust scroll position
            if (insideBlock) {
                scrollHeaderToTop(insideBlock);
            } else if (anchor) {
                restoreScrollAnchor(anchor);
            }

            log(`Collapsed ${blocks.length} blocks`, 'success');
        },

        // Expand all collapsed blocks in a single synchronous pass.
        // See collapseAll() for rationale on why we don't batch across frames.
        // Same scroll anchoring strategy as collapseAll().
        expandAll() {
            const blocks = Array.from(blockState.values()).filter(s => s.collapsed);
            if (blocks.length === 0) return;

            // Find anchor before any DOM changes
            const anchor = findScrollAnchor();

            log(`Expanding ${blocks.length} blocks`, 'info');
            // Pass null to skip per-block anchoring; we handle it once at the end
            blocks.forEach(state => expandBlock(state, null));

            // Restore scroll position once after all expands
            restoreScrollAnchor(anchor);

            log(`Expanded ${blocks.length} blocks`, 'success');
        },

        // Toggle all code blocks:
        // - If any are expanded → collapse all
        // - If all are collapsed → expand all
        toggleAll() {
            const anyExpanded = Array.from(blockState.values()).some(s => !s.collapsed);
            if (anyExpanded) {
                this.collapseAll();
            } else {
                this.expandAll();
            }
        },

        // Persisted auto-collapse mode:
        // - 'collapse': auto-collapse existing + new code blocks
        // - 'off': do nothing automatically
        setAutoMode(mode) {
            log(`Setting auto mode: ${mode}`, 'info');
            settings.autoMode = mode;
            saveSettings();
            this.applyAutoMode();
            updateToolbarButton();
        },

        // Apply the current auto-mode to all currently tracked blocks.
        applyAutoMode() {
            if (settings.autoMode === 'collapse') {
                this.collapseAll();
            }
        },

        // Apply auto-mode to a single newly added block at setup time.
        applyAutoModeToBlock(state) {
            if (settings.autoMode === 'collapse' && !state.collapsed) {
                // Avoid any single-block scroll behavior during auto-collapse of newly added blocks
                collapseBlock(state, null);
            }
        }
    };

    // Expose globally (useful for debugging/automation).
    window.CodeBlockCollapse = CodeBlockCollapse;

    // ═══════════════════════════════════════════════════════════════════════
    // FOOTER POSITIONING
    // ═══════════════════════════════════════════════════════════════════════
    // The footer bar needs special handling:
    // - When code block's bottom is visible in viewport → footer at natural position (end of block)
    // - When code block's bottom is BELOW viewport → footer fixed at viewport bottom
    // This ensures the collapse button is always accessible while never covering code.

    // Global fixed footer (single element) that is shown for the "active" code block.
    // This avoids flicker/partial overlap because:
    // - the in-flow footer can be hidden (visibility) without changing layout
    // - the fixed footer is a separate DOM node (not fighting with in-flow geometry)
    let globalFixedFooter = null;
    let activeFixedState = null;

    function getGlobalFixedFooter() {
        if (globalFixedFooter && document.body.contains(globalFixedFooter)) {
            return globalFixedFooter;
        }

        const el = document.createElement('div');
        el.className = 'cbc-footer cbc-footer-fixed cbc-footer-hidden';
        el.setAttribute('role', 'button');
        el.setAttribute('tabindex', '0');

        el.innerHTML = `
            <svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
                <polyline points="18 15 12 9 6 15"></polyline>
            </svg>
            <span>Collapse</span>
        `;

        el.addEventListener('click', (e) => {
            e.preventDefault();
            e.stopPropagation();
            if (activeFixedState && !activeFixedState.collapsed) {
                collapseBlock(activeFixedState);
            }
        });

        el.addEventListener('keydown', (e) => {
            if (e.key === 'Enter' || e.key === ' ') {
                e.preventDefault();
                if (activeFixedState && !activeFixedState.collapsed) {
                    collapseBlock(activeFixedState);
                }
            }
        });

        document.body.appendChild(el);
        globalFixedFooter = el;
        return el;
    }

    function showGlobalFixedFooterForState(state, blockRect, scrollBounds) {
        const fixed = getGlobalFixedFooter();

        // Restore previous state's in-flow footer visibility
        if (activeFixedState && activeFixedState !== state && activeFixedState.footer && !activeFixedState.collapsed) {
            activeFixedState.footer.classList.remove('cbc-footer-hidden-by-fixed');
        }

        activeFixedState = state;

        // Hide in-flow footer without changing layout/height
        if (state.footer) {
            state.footer.classList.add('cbc-footer-hidden-by-fixed');
        }

        fixed.classList.remove('cbc-footer-hidden');
        fixed.style.left = `${blockRect.left}px`;
        fixed.style.width = `${blockRect.width}px`;
        fixed.style.bottom = `${window.innerHeight - scrollBounds.bottom}px`;
    }

    function hideGlobalFixedFooter() {
        const fixed = getGlobalFixedFooter();

        fixed.classList.add('cbc-footer-hidden');
        fixed.style.left = '';
        fixed.style.width = '';
        fixed.style.bottom = '';

        if (activeFixedState && activeFixedState.footer && !activeFixedState.collapsed) {
            activeFixedState.footer.classList.remove('cbc-footer-hidden-by-fixed');
        }

        activeFixedState = null;
    }

    function updateFixedFooter() {
        // Early exit: don't create/manipulate fixed footer if no code blocks exist
        if (blockState.size === 0) {
            if (globalFixedFooter && document.body.contains(globalFixedFooter)) hideGlobalFixedFooter();
            return;
        }

        const fixed = getGlobalFixedFooter();
        const scrollBounds = getScrollBounds();

        // Height can be 0 if the fixed footer is hidden via display:none.
        // Fall back to any in-flow footer height (those are always measurable even if visibility:hidden).
        let footerHeight = fixed.offsetHeight || 0;
        if (!footerHeight) {
            for (const s of blockState.values()) {
                if (s && s.footer && document.body.contains(s.footer)) {
                    footerHeight = Math.max(footerHeight, s.footer.offsetHeight || 0);
                }
            }
        }
        footerHeight = footerHeight || 32;

        const collisionPadding = 1; // hide fixed slightly BEFORE it would touch the header
        const fixedTop = scrollBounds.bottom - footerHeight;

        let lowestTouchingState = null;
        let lowestTouchingRect = null;
        let bestScore = -Infinity;

        for (const state of blockState.values()) {
            if (!state || state.collapsed) continue;
            if (!state.block || !document.body.contains(state.block)) continue;

            const rect = state.block.getBoundingClientRect();

            // Not visible at all in the scroll viewport
            if (rect.bottom < scrollBounds.top || rect.top > scrollBounds.bottom) {
                if (state.footer && state !== activeFixedState) {
                    state.footer.classList.remove('cbc-footer-hidden-by-fixed');
                }
                continue;
            }

            // Only consider fixed-mode when the block bottom touches/enters the input area zone
            if (rect.bottom < scrollBounds.bottom) {
                if (state.footer && state !== activeFixedState) {
                    state.footer.classList.remove('cbc-footer-hidden-by-fixed');
                }
                continue;
            }

            // Candidate: pick the lowest-on-screen touching block (closest to input)
            const score = rect.top;
            if (score > bestScore) {
                bestScore = score;
                lowestTouchingState = state;
                lowestTouchingRect = rect;
            }
        }

        if (!lowestTouchingState) {
            hideGlobalFixedFooter();
            return;
        }

        // If the fixed footer would touch/cover THIS block's header, hide fixed entirely.
        // (Do not "jump" to another block's fixed footer; that feels wrong during auto-scroll.)
        const headerRect = lowestTouchingState.header.getBoundingClientRect();
        if (headerRect.bottom >= fixedTop - collisionPadding) {
            hideGlobalFixedFooter();
            return;
        }

        showGlobalFixedFooterForState(lowestTouchingState, lowestTouchingRect, scrollBounds);
    }

    // ═══════════════════════════════════════════════════════════════════════
    // TOOLBAR (Embedded Only — React-safe when tab is visible)
    // ═══════════════════════════════════════════════════════════════════════
    //
    // Key rule (robustness):
    // - Never insert into the React form while the tab is hidden.
    //   (The init gate at the bottom ensures start() only runs when visible,
    //   and initializeToolbar() below also refuses to run while hidden.)
    //
    // Other UX features (floating/drag/dynamic tooltips) were removed for simplicity.

    let toolbarElement = null;

    // Tooltip + hold gesture timers
    let holdTooltip = null;
    let holdTimer = null;
    let tooltipTimer = null;
    let confirmTimer = null;

    function getHoldTooltip() {
        if (holdTooltip && document.body.contains(holdTooltip)) return holdTooltip;
        holdTooltip = document.createElement('div');
        holdTooltip.className = 'cbc-hold-tooltip';
        document.body.appendChild(holdTooltip);
        return holdTooltip;
    }

    function showHoldTooltip(btn, message, isConfirmation = false, isDeactivation = false) {
        if (!btn) return;

        const tooltip = getHoldTooltip();
        tooltip.textContent = message;
        tooltip.classList.remove('confirmed', 'deactivated');

        if (isConfirmation) tooltip.classList.add('confirmed');
        if (isDeactivation) tooltip.classList.add('deactivated');

        // Position above button; if not enough space, flip below.
        const rect = btn.getBoundingClientRect();

        tooltip.classList.remove('visible');
        tooltip.style.display = 'block';
        tooltip.style.visibility = 'hidden';
        tooltip.style.opacity = '0';

        const tw = tooltip.offsetWidth || 200;
        const th = tooltip.offsetHeight || 28;

        const margin = 8;
        const vw = window.innerWidth;
        const vh = window.innerHeight;

        let left = rect.left + (rect.width / 2) - (tw / 2);
        left = Math.max(margin, Math.min(left, vw - tw - margin));

        // Prefer above
        let top = rect.top - th - margin;
        let arrow = 'bottom';

        // Flip below if needed
        if (top < margin) {
            top = rect.bottom + margin;
            arrow = 'top';
            if (top + th > vh - margin) {
                // Clamp if still not enough room
                top = Math.max(margin, Math.min(top, vh - th - margin));
            }
        }

        tooltip.style.left = `${left}px`;
        tooltip.style.top = `${top}px`;
        tooltip.setAttribute('data-arrow', arrow);

        tooltip.style.visibility = '';
        tooltip.style.opacity = '';
        tooltip.style.display = '';
        tooltip.offsetHeight; // force reflow
        tooltip.classList.add('visible');
    }

    function hideHoldTooltip() {
        if (holdTooltip) holdTooltip.classList.remove('visible');
    }

    function clearHoldTimers() {
        if (tooltipTimer) { clearTimeout(tooltipTimer); tooltipTimer = null; }
        if (holdTimer) { clearTimeout(holdTimer); holdTimer = null; }
        if (confirmTimer) { clearTimeout(confirmTimer); confirmTimer = null; }
    }

    function updateToolbarState() {
        if (!toolbarElement || !document.body.contains(toolbarElement)) return;

        // Auto-mode visual state
        if (settings.autoMode === 'collapse') {
            toolbarElement.classList.add('auto-active');
        } else {
            toolbarElement.classList.remove('auto-active');
        }

        // Code blocks presence state
        const hasCodeBlocks = document.querySelector('[data-code-block="true"]') !== null;
        if (hasCodeBlocks) toolbarElement.classList.remove('no-blocks');
        else toolbarElement.classList.add('no-blocks');
    }

    // Legacy function names used elsewhere in the script
    function updateToolbarButton() { updateToolbarState(); }
    function updateToolbarVisibility() { updateToolbarState(); }

    function buildToolbarElement() {
        const el = document.createElement('div');
        el.id = 'cbc-toolbar';
        el.className = 'cbc-toolbar-embedded';

        el.innerHTML = `
            <button class="cbc-collapse-btn" type="button" aria-label="Collapse code blocks (hold to toggle auto-collapse)">
                <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
                    <polyline points="7 4 12 9 17 4"></polyline>
                    <polyline points="7 20 12 15 17 20"></polyline>
                </svg>
            </button>
            <button class="cbc-expand-btn" type="button" aria-label="Expand code blocks">
                <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
                    <polyline points="7 8 12 3 17 8"></polyline>
                    <polyline points="7 16 12 21 17 16"></polyline>
                </svg>
            </button>
        `;

        const collapseBtn = el.querySelector('.cbc-collapse-btn');
        const expandBtn = el.querySelector('.cbc-expand-btn');

        let holdCompleted = false;

        // Collapse: click = collapse all, hold 1s = toggle auto-collapse
        collapseBtn.addEventListener('mousedown', (e) => {
            if (e.button !== 0) return; // left only
            e.preventDefault();

            holdCompleted = false;
            const isCurrentlyActive = settings.autoMode === 'collapse';

            // Show hint tooltip after 500ms
            tooltipTimer = setTimeout(() => {
                const hint = isCurrentlyActive
                    ? 'Keep holding to disable auto-collapse...'
                    : 'Keep holding to enable auto-collapse...';
                showHoldTooltip(collapseBtn, hint);
                collapseBtn.classList.remove('filling-up', 'filling-down');
                collapseBtn.classList.add(isCurrentlyActive ? 'filling-down' : 'filling-up');
            }, 500);

            // Toggle mode after 1000ms
            holdTimer = setTimeout(() => {
                holdCompleted = true;
                clearHoldTimers();

                const newMode = isCurrentlyActive ? 'off' : 'collapse';
                CodeBlockCollapse.setAutoMode(newMode);

                const confirmMessage = newMode === 'collapse'
                    ? '✓ Auto-collapse ON'
                    : '✓ Auto-collapse OFF';

                showHoldTooltip(collapseBtn, confirmMessage, newMode === 'collapse', newMode === 'off');

                confirmTimer = setTimeout(() => {
                    hideHoldTooltip();
                }, 1200);
            }, 1000);
        });

        collapseBtn.addEventListener('mouseup', (e) => {
            if (e.button !== 0) return;

            clearHoldTimers();
            hideHoldTooltip();
            collapseBtn.classList.remove('filling-up', 'filling-down');

            if (!holdCompleted) {
                CodeBlockCollapse.collapseAll();
            }
            holdCompleted = false;
        });

        collapseBtn.addEventListener('mouseleave', () => {
            clearHoldTimers();
            hideHoldTooltip();
            collapseBtn.classList.remove('filling-up', 'filling-down');
            holdCompleted = false;
        });

        // Hover tooltip
        let collapseHoverTimer = null;
        collapseBtn.addEventListener('mouseenter', () => {
            collapseHoverTimer = setTimeout(() => {
                const msg = settings.autoMode === 'collapse'
                    ? 'Collapse code blocks (hold to disable auto)'
                    : 'Collapse code blocks (hold to enable auto)';
                showHoldTooltip(collapseBtn, msg);
            }, 120);
        });
        collapseBtn.addEventListener('mouseleave', () => {
            if (collapseHoverTimer) { clearTimeout(collapseHoverTimer); collapseHoverTimer = null; }
        });

        // Expand: click
        expandBtn.addEventListener('click', (e) => {
            e.preventDefault();
            e.stopPropagation();
            CodeBlockCollapse.expandAll();
        });

        let expandHoverTimer = null;
        expandBtn.addEventListener('mouseenter', () => {
            expandHoverTimer = setTimeout(() => {
                showHoldTooltip(expandBtn, 'Expand code blocks');
            }, 120);
        });
        expandBtn.addEventListener('mouseleave', () => {
            if (expandHoverTimer) { clearTimeout(expandHoverTimer); expandHoverTimer = null; }
            hideHoldTooltip();
        });

        // Prevent native context menu on the toolbar buttons (keeps hold UX clean)
        el.addEventListener('contextmenu', (e) => {
            e.preventDefault();
        });

        return el;
    }

    function findToolbarHost() {
        const form = document.querySelector('form');
        if (!form) return null;

        const submitBtn = form.querySelector('button[type="submit"]');
        if (!submitBtn) return null;

        return submitBtn.closest('.flex.items-center.gap-2');
    }

    function destroyToolbar() {
        if (toolbarElement && toolbarElement.parentNode) {
            toolbarElement.remove();
        }
        toolbarElement = null;
    }

    function initializeToolbar() {
        // Hard rule: never touch React DOM while hidden
        if (document.visibilityState !== 'visible') return;

        const host = findToolbarHost();
        if (!host) {
            // If the textarea exists, the form is likely being re-rendered transiently.
            // In that case, DON'T aggressively remove the toolbar; just retry shortly.
            // (This keeps reinjection reliable and reduces flicker during long-chat reloads.)
            if (document.querySelector('form textarea')) {
                setTimeout(initializeToolbar, 200);
            } else {
                destroyToolbar();
            }
            return;
        }

        // If our toolbar is already there, just refresh state
        const existing = document.getElementById('cbc-toolbar');
        if (existing && document.body.contains(existing)) {
            toolbarElement = existing;
            updateToolbarState();
            return;
        }

        toolbarElement = buildToolbarElement();
        host.insertBefore(toolbarElement, host.firstChild);
        updateToolbarState();
    }

    // ═══════════════════════════════════════════════════════════════════════
    // CODE BLOCK SETUP
    // ═══════════════════════════════════════════════════════════════════════

    // Initializes a single code block if it hasn’t been processed.
    // Adds:
    // - header toggle behavior
    // - state hint element
    // - per-block footer bar (plus global fixed footer when needed)
    function setupCodeBlock(block) {
        if (blockState.has(block)) return;

        // arena.ai code blocks are wrapped with [data-code-block="true"].
        // We position relative to the code container and use the header bar for toggling.
        const container = block.querySelector('.code-block_container__lbMX4') ||
                          block.querySelector('pre:last-child');
        const header = block.querySelector('.border-b');

        if (!container || !header) return;

        log('Setting up code block', 'info');

        container.classList.add('cbc-container');
        header.classList.add('cbc-header-interactive');

        // Header hint (changes text depending on collapsed state via CSS).
        const hint = document.createElement('span');
        hint.className = 'cbc-state-hint';
        hint.innerHTML = `
            <span class="hint-collapse">
                <svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
                    <polyline points="18 15 12 9 6 15"></polyline>
                </svg>
                <span>Click to collapse code</span>
            </span>
            <span class="hint-expand">
                <svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
                    <polyline points="6 9 12 15 18 9"></polyline>
                </svg>
                <span>Click to expand code</span>
            </span>
        `;
        header.appendChild(hint);

        // Create sticky footer bar for collapse action
        const footer = document.createElement('div');
        footer.className = 'cbc-footer';
        footer.setAttribute('role', 'button');
        footer.setAttribute('tabindex', '0');
        footer.innerHTML = `
            <svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
                <polyline points="18 15 12 9 6 15"></polyline>
            </svg>
            <span>Collapse</span>
        `;
        block.appendChild(footer);

        // Per-block state record used by toggle logic.
        const state = {
            block,
            container,
            header,
            footer,
            collapsed: false
        };

        blockState.set(block, state);

        // Footer bar click: collapse the code block
        footer.addEventListener('click', (e) => {
            e.preventDefault();
            e.stopPropagation();
            collapseBlock(state);
        });

        // Keyboard accessibility for footer: Enter/Space collapses.
        footer.addEventListener('keydown', (e) => {
            if (e.key === 'Enter' || e.key === ' ') {
                e.preventDefault();
                collapseBlock(state);
            }
        });

        // Header behavior: toggles both directions.
        // Important: ignore clicks on native buttons inside header (copy button, etc.).
        header.addEventListener('click', (e) => {
            if (e.target.closest('button')) return;

            e.preventDefault();
            e.stopPropagation();
            toggleBlock(state);
        });

        // Keyboard accessibility: Enter/Space toggles.
        header.addEventListener('keydown', (e) => {
            if (e.key === 'Enter' || e.key === ' ') {
                e.preventDefault();
                toggleBlock(state);
            }
        });

        // Make header focusable for keyboard use.
        header.setAttribute('tabindex', '0');
        header.setAttribute('role', 'button');

        // Apply persistent mode to newly created blocks.
        CodeBlockCollapse.applyAutoModeToBlock(state);

        // Initial fixed-footer evaluation
        requestAnimationFrame(() => {
            updateFixedFooter();
        });
    }

    // Full scan:
    // - Used on initial load.
    // - Also used by the safety resync.
    function processAll() {
        logStage('processAllCalled');
        diagDOMState('processAll() called');

        const codeBlocks = document.querySelectorAll('[data-code-block="true"]');
        log(`processAll: Found ${codeBlocks.length} code blocks`, 'info');

        codeBlocks.forEach(setupCodeBlock);

        // Initialize toolbar
        initializeToolbar();
    }

    // Removes entries for blocks that disappeared from DOM and updates footer positions.
    function updateAll() {
        for (const [el, state] of blockState) {
            if (!document.body.contains(el)) {
                // Code block removed due to chat switch / rerender; clean up.
                if (state.footer && state.footer.parentNode) {
                    state.footer.remove();
                }
                blockState.delete(el);
            }
        }
        // Update global fixed footer state/position
        updateFixedFooter();
    }

    // Throttled footer update for ResizeObserver (separate from scroll rAF).
    function throttledUpdateFooter() {
        if (resizeRafId) return;
        resizeRafId = requestAnimationFrame(() => {
            updateFixedFooter();
            resizeRafId = null;
        });
    }

    // Sets up ResizeObserver on the textarea to detect height changes.
    // Called on init and when form is re-rendered by React.
    function setupInputResizeObserver() {
        if (!window.ResizeObserver) return;

        const textarea = document.querySelector('form textarea');
        if (!textarea) return;

        // Disconnect existing observer (handles React re-renders)
        if (inputResizeObserver) {
            inputResizeObserver.disconnect();
        }

        inputResizeObserver = new ResizeObserver(() => {
            throttledUpdateFooter();
        });

        // Observe textarea directly (grows as user types)
        inputResizeObserver.observe(textarea);

        // Also observe form container (catches overall input area resizes)
        const form = textarea.closest('form');
        if (form) {
            inputResizeObserver.observe(form);
        }

        log('Input ResizeObserver attached', 'info');
    }

    // ═══════════════════════════════════════════════════════════════════════
    // INCREMENTAL MUTATION OBSERVER (v1.3)
    // ═══════════════════════════════════════════════════════════════════════
    // Efficient approach:
    // - Do NOT scan the entire document on every mutation.
    // - Only inspect added nodes and their descendants for code blocks.
    const observer = new MutationObserver((mutations) => {
        let newBlocksFound = 0;
        let toolbarChecked = false;

        for (const mutation of mutations) {
            for (const node of mutation.addedNodes) {
                if (node.nodeType !== Node.ELEMENT_NODE) continue;

                // If the added node itself is a code block.
                if (node.matches?.('[data-code-block="true"]')) {
                    setupCodeBlock(node);
                    newBlocksFound++;
                }

                // Otherwise scan inside it for code blocks.
                if (node.querySelectorAll) {
                    const blocks = node.querySelectorAll('[data-code-block="true"]');
                    blocks.forEach(block => {
                        setupCodeBlock(block);
                        newBlocksFound++;
                    });

                    // Re-check toolbar and input resize observer if form is re-rendered
                    if (!toolbarChecked && (node.matches?.('form') || node.querySelector?.('form'))) {
                        setupInputResizeObserver();
                        setTimeout(initializeToolbar, 100);
                        toolbarChecked = true;
                    }
                }
            }
        }

        if (newBlocksFound > 0) {
            log(`Mutation: processed ${newBlocksFound} new code block(s)`, 'success');
        }

        // Always update positions and cleanup (cheap relative to full rescan).
        updateAll();

        // Update toolbar visibility (show/hide based on code block presence)
        updateToolbarVisibility();
    });

    // ═══════════════════════════════════════════════════════════════════════
    // INITIALIZATION (Dynamic Stabilization Observer)
    // ═══════════════════════════════════════════════════════════════════════

    const start = () => {
        logStage('startCalled');
        hasStarted = true;
        log('🎬 start() called - Beginning initialization', 'critical');
        diagDOMState('start() entry');

        log('Loading settings...', 'diag');
        loadSettings();
        logStage('settingsLoaded');

        log('Injecting styles...', 'diag');
        injectStyles();
        logStage('stylesInjected');

        // Initial full scan.
        log('Calling processAll()...', 'diag');
        processAll();
        updateAll();

        // Apply persisted mode immediately on load.
        log('Applying auto mode...', 'diag');
        CodeBlockCollapse.applyAutoMode();

        // Start observers and listeners only after initial setup
        log('Starting MutationObserver...', 'diag');
        observer.observe(document.body, { childList: true, subtree: true });
        logStage('observerStarted');

        setInterval(() => {
            const currentCount = blockState.size;
            const totalOnPage = document.querySelectorAll('[data-code-block="true"]').length;
            if (totalOnPage !== currentCount) {
                log(`Resync triggered: tracked=${currentCount}, onPage=${totalOnPage}`, 'info');
                processAll();
                updateAll();
            }

            // Also check if toolbar exists (only when visible; never mutate React DOM while hidden)
            const toolbar = document.getElementById('cbc-toolbar');
            if (!toolbar && document.querySelector('form textarea') && document.visibilityState === 'visible') {
                initializeToolbar();
            }
        }, CONFIG.resyncInterval);

        let rafId = null;
        const onScrollResize = () => {
            if (rafId) return;
            rafId = requestAnimationFrame(() => {
                updateAll();
                rafId = null;
            });
        };

        window.addEventListener('scroll', onScrollResize, { passive: true });
        window.addEventListener('resize', onScrollResize, { passive: true });
        document.addEventListener('scroll', onScrollResize, { passive: true, capture: true });

        // Watch textarea for resize (grows as user types multi-line input)
        setupInputResizeObserver();

        initComplete = true;
        logStage('initComplete');
        log(`✅ INITIALIZATION COMPLETE. Auto-collapse: ${settings.autoMode === 'collapse' ? 'ON' : 'OFF'}`, 'critical');
        diagDOMState('Initialization complete');

        // Final verification after a delay
        setTimeout(() => {
            log('🔍 Post-init verification (500ms later):', 'critical');
            diagDOMState('Post-init verification');

            const toolbar = document.getElementById('cbc-toolbar');
            if (!toolbar) {
                log('Toolbar not found after init (may be re-rendering; will re-inject)', 'diag');
            } else {
                log('Toolbar present after init', 'diag');
            }
        }, 500);
    };

    /**
     * React Stabilization Watcher:
     * Instead of a hardcoded delay, we wait for the main chat form to appear.
     * This signals that React hydration is complete and it is safe to touch the DOM.
     */
    // If the tab is opened in the background, do NOT touch React-managed DOM until visible.
    // This prevents hydration mismatch / React error #418 which can break the page and other scripts' UI.
    let pendingStart = false;

    const scheduleStartSafely = (reason) => {
        if (hasStarted) return;

        const hasReady = !!document.querySelector('form textarea');
        const visible = document.visibilityState === 'visible';

        log(`scheduleStartSafely: reason=${reason}, hasReady=${hasReady}, visible=${visible}`, 'diag');

        if (!hasReady) return;

        if (!visible) {
            pendingStart = true;
            log('Deferring start() because tab is hidden (will run on visibilitychange)', 'diag');
            return;
        }

        // Small delay gives React time to finish any last hydration/microtasks after the tab becomes visible.
        setTimeout(() => {
            if (!hasStarted && document.visibilityState === 'visible' && document.querySelector('form textarea')) {
                log('Calling start() (safe schedule)', 'diag');
                start();
            }
        }, 250);
    };

    const initWhenReady = () => {
        logStage('initWhenReadyCalled');
        log('initWhenReady() called', 'diag');
        diagDOMState('initWhenReady entry');

        const READY_SELECTOR = 'form textarea';
        const readyElement = document.querySelector(READY_SELECTOR);

        if (readyElement) {
            logStage('readySelectorFound');
            log('✅ Ready selector found immediately!', 'success');

            // If tab is hidden, DO NOT start yet (avoid React hydration mismatch).
            if (document.visibilityState !== 'visible') {
                pendingStart = true;
                log('Ready selector found but tab is hidden → deferring initialization until visible', 'diag');
                return;
            }

            // In background tabs, requestIdleCallback may not fire; but we're visible here, so it's safe to use.
            log('Scheduling start() via requestIdleCallback with fallback...', 'diag');

            const startWithLog = () => {
                log('Idle/timeout callback fired, calling start()', 'diag');
                start();
            };

            if (window.requestIdleCallback) {
                const idleId = window.requestIdleCallback(startWithLog, { timeout: 1000 });
                setTimeout(() => {
                    if (!hasStarted) {
                        log('⚠️ Idle callback did not fire, using backup timeout', 'error');
                        window.cancelIdleCallback(idleId);
                        startWithLog();
                    }
                }, 1500);
            } else {
                setTimeout(startWithLog, 100);
            }
            return;
        }

        log('Ready selector not found, creating observer...', 'diag');
        logStage('readyObserverCreated');

        // Otherwise, observe until the form appears
        const readyObserver = new MutationObserver(() => {
            const found = document.querySelector(READY_SELECTOR);
            if (found) {
                logStage('readyObserverFired');
                log('✅ Ready observer fired - form textarea found!', 'success');
                diagDOMState('Ready observer fired');

                readyObserver.disconnect();

                // If hidden, defer. If visible, start after settle.
                pendingStart = (document.visibilityState !== 'visible');
                if (pendingStart) {
                    log('Form appeared but tab is hidden → deferring initialization until visible', 'diag');
                    return;
                }

                log('Waiting 200ms for React to settle...', 'diag');
                setTimeout(() => {
                    scheduleStartSafely('observer-path-settle');
                }, 200);
            }
        });

        if (document.body) {
            readyObserver.observe(document.body, { childList: true, subtree: true });
            log('Ready observer started on document.body', 'diag');
        } else {
            log('❌ document.body not available!', 'error');
            document.addEventListener('DOMContentLoaded', () => {
                log('DOMContentLoaded fired, retrying initWhenReady', 'diag');
                initWhenReady();
            });
        }
    };

    // Add visibility change handler to detect when background tab becomes visible
    document.addEventListener('visibilitychange', () => {
        log(`👁️ Visibility changed: ${document.visibilityState}`, 'diag');
        diagDOMState('Visibility change');

        if (document.visibilityState !== 'visible') return;

        log('Tab became visible, checking initialization state...', 'diag');

        // If we deferred init while hidden, run it now.
        if (pendingStart && !hasStarted) {
            pendingStart = false;
            scheduleStartSafely('visibilitychange-pendingStart');
            return;
        }

        // If we haven't completed initialization, try now (only when visible)
        if (!initComplete) {
            log('⚠️ Init not complete; attempting safe start()', 'error');
            scheduleStartSafely('visibilitychange-initIncomplete');
        }

        // If toolbar is missing but start already happened, attempt to restore
        const toolbar = document.getElementById('cbc-toolbar');
        if (!toolbar && hasStarted && document.querySelector('form textarea')) {
            log('⚠️ Toolbar missing after visibility change, reinitializing toolbar', 'error');
            initializeToolbar();
        }
    });

    log('📋 Calling initWhenReady()...', 'diag');
    initWhenReady();

    // Emergency fallback: ONLY act when visible. If hidden, defer.
    setTimeout(() => {
        log('⏰ 5-second emergency check...', 'diag');
        diagDOMState('5-second emergency check');

        if (document.visibilityState !== 'visible') {
            pendingStart = pendingStart || !!document.querySelector('form textarea');
            log('Emergency check: tab is hidden → deferring any forced init until visible', 'diag');
            return;
        }

        if (!initComplete) {
            log('❌ EMERGENCY: Init not complete after 5s (visible tab)!', 'critical');
            console.log('[CBC] Emergency: init not complete');

            if (document.querySelector('form textarea')) {
                log('Form exists, forcing start()...', 'critical');
                start();
            } else {
                log('Form not found, cannot force start', 'error');
            }
        } else {
            log('✅ 5-second check passed - init was complete', 'success');
        }
    }, 5000);
})();