/**
 * Cast Conductor Proprietary License v5
 * SPDX-License-Identifier: LicenseRef-CastConductor-Proprietary-v5
 * 
 * Copyright (c) 2025 CastConductor.com. All Rights Reserved.
 * 
 * This file is part of Cast Conductor ("Software"). The Software and its source
 * code constitute proprietary, confidential, and trade secret information of
 * CastConductor.com ("Company"). Any access or use is governed strictly by the
 * Cast Conductor Proprietary License v5 ("License"). By installing, copying,
 * accessing, compiling, or otherwise using the Software you agree to be bound by
 * all terms of the License. If you do not agree, you must cease use immediately.
 * 
 * Key Terms (Summary – see full License for binding terms):
 *  1. No Redistribution: You may not publish, distribute, sublicense, rent,
 *     lease, transfer, sell, or otherwise make the Software (or any derivative)
 *     available to any third party without prior written consent of Company.
 *  2. No Modification: Modification, reverse engineering, decompilation, or
 *     disassembly is prohibited except to the limited extent expressly permitted
 *     by applicable law that cannot be contractually waived.
 *  3. Confidentiality: Treat all source code and related artifacts as Company
 *     Confidential Information. Maintain at least the same degree of care as for
 *     your own confidential materials, and not less than reasonable care.
 *  4. No Patent License: No express or implied patent rights are granted. Future
 *     patents (if any) are fully reserved.
 *  5. No Trademark License: Company names, marks, and logos may not be used
 *     without prior written permission.
 *  6. Limited Internal Use: Use is limited solely to internal evaluation and
 *     operation of licensed Cast Conductor deployments. Commercial hosting or
 *     resale as a service requires a separate written agreement.
 *  7. Telemetry & License Validation: The Software may periodically transmit a
 *     hashed installation identifier, domain (or site ID), plugin/app version,
 *     and a truncated (non-reversible) fragment of the license key solely to
 *     validate activation status and enforce licensing. This minimal "phone home"
 *     check contains no personal or content data. If optional telemetry is later
 *     introduced it will be limited to aggregate operational metrics (no PII),
 *     fully documented, and optionally disableable per published instructions.
 *  8. Third-Party Components: The Software may include open source components
 *     covered by their own licenses. See THIRD-PARTY-NOTICES.md. Those licenses
 *     govern their respective components; this License governs all remaining code.
 *  9. Export Compliance: You are responsible for compliance with all applicable
 *     export control and sanctions laws.
 * 10. Warranty Disclaimer: THE SOFTWARE IS PROVIDED "AS IS" WITHOUT WARRANTY OF
 *     ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO MERCHANTABILITY,
 *     FITNESS FOR A PARTICULAR PURPOSE, TITLE, AND NON-INFRINGEMENT.
 * 11. Limitation of Liability: IN NO EVENT WILL COMPANY OR AUTHORS BE LIABLE FOR
 *     ANY INDIRECT, SPECIAL, INCIDENTAL, CONSEQUENTIAL, EXEMPLARY, OR PUNITIVE
 *     DAMAGES, OR LOST PROFITS, EVEN IF ADVISED OF THE POSSIBILITY.
 * 12. Acceptance: Use of the Software constitutes acceptance of the License.
 * 13. Enforcement: Unauthorized reproduction or distribution may result in civil
 *     and criminal penalties and will be prosecuted to the maximum extent allowed
 *     by law.
 * 
 * Authoritative EULA: EULA-v5.1.md (repository root – private) and https://castconductor.com/eula
 * Precedence: If this summary conflicts with the EULA, the EULA governs.
 * Revision: Current EULA revision v5.1 (subject to update; check EULA for current enterprise thresholds).
 * 
 * Full License text available from: licensing@castconductor.com
 * Security reports: security@castconductor.com
 * Commercial inquiries: licensing@castconductor.com
 * 
 * END OF HEADER
 */

// Cache-busting version for ES module imports - update with each build
// This forces browsers to reload modules even when aggressively cached
const CCVE_MODULE_VERSION = '20260113-1200';

// Log module version for cache debugging
console.log('[CCVE] Canvas Editor Module Version:', CCVE_MODULE_VERSION);

import { applyPreviewDisplay } from './modules/preview-display.js'; // (legacy direct usage - scheduled for removal once PreviewEngine stable)
import { attachPreviewGlue } from './modules/preview-glue.js';
// Phase 1 Step 6: artwork-slideshow.js DEPRECATED - replaced by unified slideshow-image layers
// import { attachArtworkSlideshow } from './modules/artwork-slideshow.js';
import { attachQrCodeGenerator } from './modules/qr-code-generator.js';
// Phase 1: Unified Layer System (Nov 24, 2025)
import { renderLayer as layerRenderUnified, clearAllLayers as layerClearAll } from './modules/unified-layer-renderer.js';
import { attachAddLayerDropdown as layerAttachDropdown } from './modules/add-layer-ui.js';
import { attachContainerPresetDropdown as presetAttachDropdown } from './modules/container-preset-ui.js';
import { autoMigrateOnLoad as layerAutoMigrate } from './modules/slideshow-migration.js';
// Phase 2: WordPress Post Integration (Nov 24, 2025)
import { attachWordPressPostSelector as layerAttachWPPostSelector } from './modules/wordpress-post-selector.js';
import { renderContainerPreview as _cpRender, startContainerPreviewCycle as _cpStart, stopContainerPreviewCycle as _cpStop, stopAllPreviewCycles as _cpStopAll, buildContainerSchedule as _cpBuildSchedule, applyContainerPreviewStyles as _cpApplyStyles, removeContainerPreviewStyles as _cpRemoveStyles, fitInnerToOuter as _cpFitInner, maybeSeedUpperThird as _cpSeedUpper } from './modules/container-preview.js';
// Phase 2.6 extracted modules (container canvas & assignments)
import { loadContainerData as _ccLoad, fetchAndRenderContainers as _ccFetchRender, renderContainersList as _ccRenderList, renderContainersOnCanvas as _ccRenderCanvas, ensureCanvasStage as _ccEnsureStage, applyCanvasScale as _ccApplyScale, persistContainerRect as _ccPersistRect } from './modules/container-canvas.js';
import { loadContainerBlocks as _caLoadBlocks, assignBlockToContainer as _caAssignBlock, removeBlockAssignment as _caRemoveBlock, getZoneAssignments as _caGetZoneAssignments, putZoneAssignments as _caPutZoneAssignments } from './modules/container-assignments.js';
import { getContainerPresets, seedPresetContainers } from './modules/container-presets.js';
import { initPreviewStatus, recaptureBaseline } from './modules/preview-status.js';
import { initGeometryHud, updateHud as updateGeometryHud } from './modules/geometry-hud.js';
import { debounce as utilDebounce, escapeHtml as utilEscapeHtml, relativeTime as utilRelativeTime } from './modules/utils.js';
import { fixedToPercent, percentToFixed, applyPresetGeometry } from './modules/geometry-conversion.js';
// Use 1280×720 preset geometry for Content Block Canvas
import { PRESET_GEOMETRY } from './modules/util-preset-geometry.js';
import { loadContentBlock as apiLoadContentBlock, loadLiveContentBlocks as apiLoadLiveContentBlocks, showLivePreview as apiShowLivePreview } from './modules/content-blocks-api.js';
import { ensureBaselineGuides as guidesEnsureBaseline, getActiveSnap as guidesGetActiveSnap, snapValue as guidesSnapValue, applyGuideSnapping as guidesApplySnap, guidesSnappingInternals as _guidesInternals } from './modules/guides-snapping.js';
import { updateBlockEditorRectFromConfig as _blockUpdateRect, bindBlockEditorInteractions as _blockBindInteractions } from './modules/block-stage-geometry.js';
import { bindCanvasInteractions as _macroBindCanvas, bindContainerSelection as _macroBindSelection } from './modules/macro-canvas-interactions.js';
import { loadDesignAssets as designLoad, populateDesignAssets as designPopulate } from './modules/design-assets.js';
import { getDeterministicSeed as seedGet, simpleHash as seedHash } from './modules/seed-utils.js';
import { setupLayoutControls as bindingsSetupLayoutControls, applyPreset as bindingsApplyPreset, layoutBindingsInternals as _layoutInternals } from './modules/bindings-layout.js';
import { showNotification as notifShow, hideNotifications as notifHide } from './modules/notifications.js';
import { validateParity as diagValidateParity, buildFriendlyParityHtml as diagBuildParityHtml } from './modules/diagnostics-parity.js';
import { openZoneGridSettings as zonesOpenGrid, fetchContainerLayout as zonesFetchLayout, putContainerLayout as zonesPutLayout } from './modules/zones-grid.js';
import { buildLayersPanel as layerBuildPanel } from './modules/layer-manager.js';
import { attachModalSystem } from './modules/modal.js';
import { attachBackgroundManagement } from './modules/backgrounds.js';
import { attachContainerFormEnhancements } from './modules/container-form-enhancements.js';
import { attachPublicApiDropdown } from './modules/public-api-ui.js';
import { safeJson as fetchSafeJson } from './modules/fetch-utils.js';
// Stage modules (refactored Nov 25, 2025)
import { StageViewport, AUTHORING_WIDTH, AUTHORING_HEIGHT, ZOOM_STEPS } from './modules/stage-viewport.js';
import { StageRulers, initRulers } from './modules/stage-rulers.js';
import { FullscreenManager } from './modules/stage-fullscreen.js';
import { attachGeometryDiagnostics } from './modules/geometry-diagnostics.js';
import { preferencesStore as prefs } from './modules/preferences-store.js';
import { listScenes as scenesList, getActiveScene as scenesActive, activateScene as scenesActivate, updateScene as scenesUpdate, createScene as scenesCreate, putSceneContainers as scenesPutContainers } from './modules/scenes-api.js';
import { attachScenesPanel } from './modules/scenes-panel.js';
import { attachScenesContainersPanel } from './modules/scenes-containers-panel.js';
// Live Token Rebuild (Phase 1) additions
import { getRegistry as tokenGetRegistry, listAllTokens as tokenListAll, isValidToken as tokenIsValid } from './modules/token-registry.js';
import { extractTokens as tokenExtract, renderTemplate as tokenRender } from './modules/token-parser.js';
import { fetchTokenCategories as tokenFetchCats, deriveCategoriesFromTokens as tokenDeriveCats } from './modules/token-live-data.js';
import { openTokenInsertModal as tokenOpenInsertModal } from './modules/token-insert-modal.js';
import { openTokenImageModal as tokenOpenImageModal } from './modules/token-image-modal.js';
import { openTokenExportModal as tokenOpenExportModal } from './modules/token-export-modal.js';
import { attachTokenLayerInteractions } from './modules/token-layer-interactions.js';
import { openTokenBatchExportModal } from './modules/token-batch-export-modal.js';
import { openTokenStatusModal } from './modules/token-status-modal.js';
import { openFullContentBlockExportModal, openFullBatchExportModal, openFullAllBlocksExportModal } from './modules/full-block-export.js';
// Layer grouping (Dec 29, 2025)
import { getLayerGroupingManager } from './modules/layer-grouping.js';
// Legacy layer hydration (Sep 18 2025)
import { hydrateLegacyLayers, renderHydratedLegacyLayers } from './modules/legacy-layer-hydration.js';
import { attachLegacyLayerInteractions } from './modules/legacy-layer-interactions.js';
// Inject minimal stage protection style (overlay rulers will absolutely position without layout shift)
(()=>{ if(document.getElementById('ccve-stage-protect-style')) return; const s=document.createElement('style'); s.id='ccve-stage-protect-style'; s.textContent=`
    #block-stage-wrapper { position:relative; display:inline-block; overflow:hidden; }
    /* Defensive: ensure ruler containers inside scale host don't occupy layout flow */
    .ccve-scale-host .ccve-ruler-horizontal, .ccve-scale-host .ccve-ruler-vertical, .ccve-scale-host .ccve-ruler-corner { pointer-events:none; }
`; document.head.appendChild(s); })();

// Build 52: Load Google Fonts for canvas preview accuracy
(()=>{
    if(document.getElementById('ccve-google-fonts')) return;
    const link = document.createElement('link');
    link.id = 'ccve-google-fonts';
    link.rel = 'stylesheet';
    link.href = 'https://fonts.googleapis.com/css2?family=Lexend:wght@400;700&family=Oxygen:wght@400;700&family=Share+Tech+Mono&family=Space+Grotesk:wght@400;700&display=swap';
    document.head.appendChild(link);
})();

// ==============================================================
// Advanced Authoring / Diagnostics Gating (Sept 3 temporary)
// Set to true ONLY during active debugging sessions to expose
// advanced zoom, drift test, pin UI, and alignment HUD controls.
// When false (default), the editor operates in simplified mode
// with always-fit scaling and no user-discoverable advanced toggle.
// To re-enable without code edit, temporarily set this constant
// to true in a local build; production bundles should keep false.
// ==============================================================
const CCVE_ENABLE_ADVANCED_DEBUG = false;

// ==============================================================
// Build 52: Custom Font Mapping for Canvas Preview
// Maps Roku-bundled font names to web-safe CSS font stacks.
// The actual Roku fonts are TTF files bundled in the app;
// these are preview-only fallbacks for the web editor.
// ==============================================================
function mapFontFamilyToCSS(fontFamily) {
    const fontMap = {
        'Lexend': '"Lexend", system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
        'Oxygen': '"Oxygen", system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
        'ShareTechMono': '"Share Tech Mono", "Courier New", Consolas, monospace',
        'SpaceGrotesk': '"Space Grotesk", system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
        'system-ui,Arial,Helvetica,sans-serif': 'system-ui, Arial, Helvetica, sans-serif'
    };
    return fontMap[fontFamily] || fontFamily || 'system-ui, Arial, Helvetica, sans-serif';
}

class CastConductorCanvasEditor {
    constructor() {
        this.currentConfig = null;
        this.currentContentBlockId = null;
        this.previewContainer = null;
        this.isPreviewMode = false;
        this.unsavedChanges = false;
        // Track preview context: 'intended' or 'actual'
        this._containerPreviewContext = 'intended';
    // (will be assigned in init())
    // Whether to show content previews inside the 1280x720 canvas
        this.showContentPreview = true;
        // Admin self-heal for Upper Third seeding (disabled by default to avoid hidden coupling)
        // Progressive module integration (preview engine extraction in progress)
        let PreviewEngine;
        try { ({ PreviewEngine } = window.ccveModules || {}); } catch(_) {}
        this.showContentPreview = true;
        // Optional shuffle toggle for schedule (if UI presents one)
        this.previewShuffle = false;
        // Per-container preview cycle state { [containerId]: { timerId, currentIndex, schedule } }
        this.containerPreviewState = {};
        // Optional config cache: { [contentBlockId]: { version, config, content_block_type } }
        this.configCache = {};
        // Version index discovered from containers/:id/blocks payload
        this.blockConfigVersionIndex = {};
        // Deterministic seed used for preview item selection/shuffle (persisted per browser)
        this._deterministicSeed = null;
        // Macro canvas grid/snap settings and interaction state
        this.canvasSnap = 8; // px snap
    this.canvasBounds = { w: 1280, h: 720 };
        this._dragState = null; // { id, type: 'move'|'resize', dir, startX, startY, orig{ x,y,w,h } }
    // Authoring mode: 'preview' (server HTML) or 'layers' (token layers only)
    this._editorMode = (function(){ try { return localStorage.getItem('ccveEditorMode')==='layers' ? 'layers' : 'preview'; } catch(_) { return 'preview'; } })();
    // Live Token Phase 2 fields
    this._liveTokenData = {}; // normalized category → fields map
    this._lastTokenRender = 0;
    this._liveTokenPending = false;
    this._canvasBound = false;
    // Selection state for keyboard nudge
    this.selectedContainerId = null;
    // Geometry mode (fixed|responsive) default
    this._geometryMode = 'fixed';
    // Internal caches for guides
    this._guidesHorizontal = [];
    this._guidesVertical = [];
    this._guideSnapThreshold = 8;
    this._guidesEnabled = true;
    this._smartSnappingEnabled = true;
    this._guideNodes = [];
    // Scenes (Phase 1): editor-side cache
    this._scenes = [];
    this._activeScene = null;
    // Stage modules (refactored Nov 25, 2025)
    this._stageViewport = null;
    this._stageRulers = null;
    this._fullscreenManager = null;
    // Debounced preview (server-backed) generation; kept lightweight (300ms) to coalesce rapid control edits.
    try { this.generatePreviewDebounced = utilDebounce(() => { try { this.generatePreview(); } catch(e) { console.warn('debounced generatePreview failed', e); } }, 300); } catch(_) { /* utilDebounce may not be ready yet */ }
    }

    async initScenesToolbar() {
        try {
            // Mount inside Containers tab only to keep separation of concerns
            const containersTabEl = document.getElementById('containers-tab');
            if (!containersTabEl) return; // no containers tab present in DOM
            // Only initialize when the Containers tab is active to avoid layout/DOM churn while hidden
            try { if (!containersTabEl.classList.contains('active')) return; } catch(_) {}
            let toolbar = containersTabEl.querySelector('#ccve-containers-toolbar');
            if (!toolbar) {
                toolbar = document.createElement('div');
                toolbar.id = 'ccve-containers-toolbar';
                toolbar.style.display = 'flex';
                toolbar.style.gap = '10px';
                toolbar.style.alignItems = 'center';
                toolbar.style.flexWrap = 'wrap';
                toolbar.style.margin = '6px 0';
                toolbar.style.position = 'relative';
                toolbar.style.zIndex = '20';
                toolbar.style.overflow = 'visible';
                // Insert at top of containers tab
                containersTabEl.insertBefore(toolbar, containersTabEl.firstChild);
            }

            // Avoid duplicate section
            let section = toolbar.querySelector('.ccve-scenes-controls');
            if (!section) {
                section = document.createElement('div');
                section.className = 'ccve-scenes-controls';
                section.style.display = 'flex';
                section.style.gap = '6px';
                section.style.alignItems = 'center';
                toolbar.appendChild(section);
                const label = document.createElement('label');
                label.textContent = 'Scene:';
                label.style.fontWeight = '600';
                label.style.marginRight = '4px';
                section.appendChild(label);
                const select = document.createElement('select');
                select.id = 'ccve-scene-select';
                select.style.minWidth = '180px';
                select.style.maxWidth = '260px';
                select.style.zIndex = '21';
                section.appendChild(select);
                const newBtn = document.createElement('button');
                newBtn.type = 'button';
                newBtn.textContent = '+ New Scene';
                newBtn.className = 'button button-small';
                section.appendChild(newBtn);
                const actBtn = document.createElement('button');
                actBtn.type = 'button';
                actBtn.textContent = 'Activate';
                actBtn.className = 'button button-small';
                section.appendChild(actBtn);
                const bbBtn = document.createElement('button');
                bbBtn.type = 'button';
                bbBtn.textContent = 'Background & Branding…';
                bbBtn.className = 'button button-small';
                section.appendChild(bbBtn);
                const scBtn = document.createElement('button');
                scBtn.type = 'button';
                scBtn.textContent = 'Scene Containers…';
                scBtn.className = 'button button-small';
                section.appendChild(scBtn);
            }

            const select = section.querySelector('#ccve-scene-select');
            const btns = section.querySelectorAll('button');
            const newBtn = btns[0];
            const actBtn = btns[1];
            const bbBtn = btns[2];
            const scBtn = btns[3];

            // Load scenes
            let activeId = null;
            try {
                const [listResp, activeResp] = await Promise.all([scenesList(), scenesActive()]);
                this._scenes = (listResp && listResp.data) || [];
                this._activeScene = (activeResp && activeResp.data) || null;
                activeId = this._activeScene ? this._activeScene.id : null;
            } catch (e) {
                console.warn('[CC] Scenes API failed', e);
                this._scenes = this._scenes || [];
                this._activeScene = this._activeScene || null;
            }

            // If no scenes exist, seed a default one (1280x720 fullscreen + lower third container)
            if ((!Array.isArray(this._scenes) || this._scenes.length === 0) && !this.__seedingDefaultScene) {
                this.__seedingDefaultScene = true;
                try {
                    const defaultBg = 'https://test.castconductor.com/wp-content/uploads/2025/09/default-background-1920x1080.jpeg';
                    const defaultLogo = 'https://test.castconductor.com/wp-content/uploads/2025/09/default-center-logo-1280x250.png';
                    const res = await scenesCreate({ name: 'Default Scene', description: 'Auto-seeded', rotation_enabled: false, rotation_interval: 60, branding: { logoUrl: defaultLogo }, background: { type: 'static', image_url: defaultBg }, metadata: {} });
                    const newId = res && res.data && (res.data.id || res.data);
                    if (newId) {
                        try { await scenesActivate(newId); } catch(eAct) { console.warn('[CC] default scene activate failed', eAct); }
                        // Create a default lower third container if none exist
                        try {
                            const cResp = await fetch(`${castconductorCanvasAjax.rest_url}castconductor/v5/containers`, {
                                method: 'POST', headers: { 'Content-Type': 'application/json','X-WP-Nonce': castconductorCanvasAjax.nonce },
                                body: JSON.stringify({ name:'Lower Third', position:'lower_third', width:1280, height:240, x_position:0, y_position:480, z_index:10, rotation_enabled:true, rotation_interval:30 })
                            });
                            let cJson = null; try { cJson = await cResp.json(); } catch(_) {}
                            const containerId = cJson && (cJson.id || cJson.data?.id);
                            if (containerId) {
                                // Map container to this scene with same geometry
                                try { await scenesPutContainers(newId, [{ container_id: parseInt(containerId,10), enabled: true, x:0, y:480, width:1280, height:240, z:10 }]); } catch(_) {}
                            }
                        } catch(_) {}
                        // Refresh list and set active
                        try {
                            const [list, activeR] = await Promise.all([scenesList(), scenesActive()]);
                            this._scenes = (list && list.data) || [];
                            this._activeScene = (activeR && activeR.data) || { id: newId, name: 'Default Scene' };
                            activeId = this._activeScene.id;
                            // Force immediate preview/background so user sees something on first load
                            try { this.renderActiveBackground && await this.renderActiveBackground(); } catch(eBg){ console.warn('[CC] seed render background fail', eBg); }
                            try { this.generatePreview && await this.generatePreview(); } catch(ePr){ console.warn('[CC] seed preview fail', ePr); }
                            try { this.showNotification && this.showNotification('Default Scene seeded','success'); } catch(_) {}
                        } catch(_) {}
                    }
                } catch(e) { console.warn('[CC] default scene seed failed', e); }
                finally { this.__seedingDefaultScene = false; }
            }

            // Retry path: if still no scenes after seeding attempt, schedule a one-time retry
            if ((!Array.isArray(this._scenes) || this._scenes.length === 0) && !this.__defaultSceneRetryScheduled) {
                this.__defaultSceneRetryScheduled = true;
                setTimeout(async () => {
                    if (Array.isArray(this._scenes) && this._scenes.length > 0) return; // already populated
                    try {
                        console.warn('[CC] Retrying default scene seed');
                        const defaultBg = 'https://test.castconductor.com/wp-content/uploads/2025/09/default-background-1920x1080.jpeg';
                        const defaultLogo = 'https://test.castconductor.com/wp-content/uploads/2025/09/default-center-logo-1280x250.png';
                        const res2 = await scenesCreate({ name: 'Default Scene', description: 'Auto-seeded retry', rotation_enabled: false, rotation_interval: 60, branding: { logoUrl: defaultLogo }, background: { type: 'static', image_url: defaultBg }, metadata: {} });
                        const newId2 = res2 && res2.data && (res2.data.id || res2.data);
                        if (newId2) {
                            try { await scenesActivate(newId2); } catch(eAct2){ console.warn('[CC] default scene retry activate failed', eAct2); }
                            try {
                                const [list2, activeR2] = await Promise.all([scenesList(), scenesActive()]);
                                this._scenes = (list2 && list2.data) || [];
                                this._activeScene = (activeR2 && activeR2.data) || { id: newId2, name: 'Default Scene' };
                                activeId = this._activeScene.id;
                                try { this.renderActiveBackground && await this.renderActiveBackground(); } catch(_){ }
                                try { this.generatePreview && await this.generatePreview(); } catch(_){ }
                                try { this.showNotification && this.showNotification('Default Scene seeded (retry)','success'); } catch(_){ }
                            } catch(eList2){ console.warn('[CC] retry fetch scenes failed', eList2); }
                        }
                    } catch(eRetry){ console.warn('[CC] default scene retry failed', eRetry); }
                }, 1200);
            }

            // Populate select
            select.innerHTML = '';
            if (!Array.isArray(this._scenes) || this._scenes.length === 0) {
                const opt = document.createElement('option');
                opt.value = '';
                opt.textContent = 'No scenes found';
                select.appendChild(opt);
                actBtn.disabled = true;
                bbBtn.disabled = true;
                if (scBtn) scBtn.disabled = true;
            } else {
                this._scenes.forEach(sc => {
                const opt = document.createElement('option');
                opt.value = sc.id;
                opt.textContent = sc.name + (sc.is_active ? ' ✓' : '');
                if (activeId && sc.id === activeId) opt.selected = true;
                select.appendChild(opt);
                });
                actBtn.disabled = false; bbBtn.disabled = false; if (scBtn) scBtn.disabled = false;
            }

            // New scene flow
            if (!newBtn.__ccveBound) {
                newBtn.__ccveBound = true;
                newBtn.addEventListener('click', () => {
                    try { this.openNewSceneModal && this.openNewSceneModal(); } catch(_) {}
                });
            }

            actBtn.onclick = async () => {
                const id = parseInt(select.value, 10);
                if (!id || (activeId && id === activeId)) return;
                actBtn.disabled = true; actBtn.textContent = 'Activating…';
                try {
                    const ok = await scenesActivate(id);
                    if (ok) {
                        // refresh list markers
                        const list = await scenesList();
                        this._scenes = (list && list.data) || this._scenes;
                        const newActive = this._scenes.find(s=>s.id===id);
                        this._activeScene = newActive ? { id: newActive.id, name: newActive.name } : { id, name: 'Scene' };
                        select.querySelectorAll('option').forEach(o => {
                            const sid = parseInt(o.value, 10);
                            const sc = this._scenes.find(s=>s.id===sid);
                            o.textContent = sc ? (sc.name + (sc.is_active ? ' ✓' : '')) : o.textContent;
                            o.selected = (sid === id);
                        });
                        try { await this.generatePreview(); } catch(_) {}
                    }
                } finally {
                    actBtn.disabled = false; actBtn.textContent = 'Activate';
                }
            };

            bbBtn.onclick = async () => {
                try {
                    const id = parseInt(select.value, 10) || (this._activeScene && this._activeScene.id);
                    if (!id) {
                        // If no scenes exist, prompt to create one
                        const hasNone = !Array.isArray(this._scenes) || this._scenes.length === 0;
                        if (hasNone) { this.openNewSceneModal && this.openNewSceneModal(); return; }
                        this.showNotification && this.showNotification('No scene selected', 'warning'); return;
                    }
                    // Open Background & Branding editor explicitly for the selected scene
                    if (this.openScenesPanel) { await this.openScenesPanel(id); }
                } catch(e) { console.warn('[CC] openScenesPanel failed', e); }
            };

            if (scBtn && !scBtn.__ccveBound) {
                scBtn.__ccveBound = true;
                scBtn.addEventListener('click', async () => {
                    try {
                        const id = parseInt(select.value, 10) || (this._activeScene && this._activeScene.id);
                        if (!id) { this.showNotification && this.showNotification('No scene selected', 'warning'); return; }
                        this.openSceneContainersPanel && this.openSceneContainersPanel(id);
                    } catch(e){ this.showNotification && this.showNotification('Failed to open Scene Containers','error'); }
                });
            }
        } catch(e) { console.warn('[CC] initScenesToolbar error', e); }
    }

    /** Bind Containers tab primary and secondary actions if present */
    initContainersToolbarBindings() {
        try {
            const host = document.getElementById('containers-tab'); if (!host) return;
            // Primary panel buttons (if present) with data-panel="containers|zones|assignments|scheduling"
            host.querySelectorAll('.ccve-containers-nav [data-panel]').forEach(btn => {
                if (btn.__ccveBound) return; btn.__ccveBound = true;
                btn.addEventListener('click', (e) => {
                    const id = e.currentTarget.getAttribute('data-panel');
                    document.querySelectorAll('[id^="ccve-containers-panel-"]').forEach(p => p.style.display = 'none');
                    const panel = document.getElementById(`ccve-containers-panel-${id}`);
                    if (panel) panel.style.display = 'block';
                    if (id === 'scheduling') this._refreshScheduleDump && this._refreshScheduleDump();
                });
            });
            // Secondary actions
            const bindClick = (el, fn) => { if (el && !el.__ccveBound) { el.__ccveBound = true; el.addEventListener('click', fn); } };
            // New Container
            bindClick(host.querySelector('#ccve-btn-new-container') || host.querySelector('#create-container-btn'), () => this.createNewContainer && this.createNewContainer());
            // Refresh
            bindClick(host.querySelector('#ccve-btn-refresh') || host.querySelector('#refresh-canvas-btn'), async () => { try { await this.loadContainerData?.(); this.refreshContainerCanvas?.(); this.showNotification && this.showNotification('Containers refreshed','success'); } catch(_){ this.showNotification && this.showNotification('Refresh failed','error'); } });
            // Save All
            bindClick(host.querySelector('#ccve-btn-save-all') || host.querySelector('#save-all-containers-btn'), () => this.saveAllContainerChanges && this.saveAllContainerChanges());
            // Validate Parity
            bindClick(host.querySelector('#ccve-btn-validate-parity') || host.querySelector('#validate-parity-btn'), async () => {
                try {
                    const report = await diagValidateParity(this);
                    const html = diagBuildParityHtml(report);
                    this.showModal && this.showModal('Parity Validation', html);
                } catch(e){ this.showNotification && this.showNotification('Parity check failed','error'); }
            });
            // Ensure toolbar region (if any) doesn't clip dropdowns
            const tb = host.querySelector('#ccve-containers-toolbar'); if (tb) { tb.style.position='relative'; tb.style.zIndex='20'; tb.style.overflow='visible'; }
        } catch(e) { console.warn('[CC] initContainersToolbarBindings failed', e); }
    }
    /** Select a token layer by id: add ccve-selected and ensure resize handles present */
    selectTokenLayer(id){
        try {
            const stage = document.getElementById('block-stage'); if(!stage) return;
            const target = stage.querySelector(`.ccve-token-text-layer[data-layer-id="${id}"] , .ccve-token-image-layer[data-layer-id="${id}"]`);
            if(!target) return;
            stage.querySelectorAll('.ccve-token-text-layer, .ccve-token-image-layer').forEach(n=>{
                if(n===target){
                    n.classList.add('ccve-selected');
                    // create handles if missing
                    if(!n.querySelector('.ccve-layer-handle')){
                        const dirs=['nw','n','ne','e','se','s','sw','w'];
                        dirs.forEach(dir=>{ const h=document.createElement('div'); h.className='ccve-layer-handle ccve-layer-h-'+dir; h.dataset.dir=dir; n.appendChild(h); });
                    }
                } else {
                    n.classList.remove('ccve-selected');
                }
            });
            // Populate Typography controls with selected layer's style
            this.populateTypographyFromLayer(id);
        } catch(_) {}
    }
    
    /** Populate Typography tab with layer-specific styles when a text layer is selected */
    populateTypographyFromLayer(layerId) {
        try {
            const cfg = this.currentConfig;
            if (!cfg || !Array.isArray(cfg.layers)) return;
            
            const layer = cfg.layers.find(l => l && l.id === layerId);
            // Support both token-text and static-text layers
            if (!layer || (layer.kind !== 'token-text' && layer.kind !== 'static-text')) return;
            
            // If layer has specific styles, use them; otherwise fall back to global typography
            const style = layer.style || {};
            const typography = cfg.typography || {};
            
            // Populate color - this is the key fix for the reported issue
            if (style.color) {
                this.setControlValue('canvas-font-color', style.color);
            } else if (typography.color) {
                this.setControlValue('canvas-font-color', typography.color);
            }
            
            // Populate other typography controls
            if (style.font_family || typography.font_family) {
                this.setControlValue('canvas-font-family', style.font_family || typography.font_family);
            }
            if (style.font_size || typography.font_size) {
                this.setControlValue('canvas-font-size', style.font_size || typography.font_size);
            }
            if (style.font_weight || typography.font_weight) {
                this.setControlValue('canvas-font-weight', style.font_weight || typography.font_weight);
            }
            if (style.text_align || typography.text_align) {
                this.setControlValue('canvas-text-align', style.text_align || typography.text_align);
            }
            if (style.line_height || typography.line_height) {
                this.setControlValue('canvas-line-height', style.line_height || typography.line_height);
            }
            // Text shadow
            const ts = style.text_shadow || typography.text_shadow || {};
            this.setControlValue('canvas-text-shadow-x', ts.x || 0);
            this.setControlValue('canvas-text-shadow-y', ts.y || 0);
            this.setControlValue('canvas-text-shadow-blur', ts.blur || 0);
            this.setControlValue('canvas-text-shadow-color', ts.color || 'rgba(0,0,0,0)');
        } catch(e) {
            console.warn('[CCVE] Failed to populate typography from layer:', e);
        }
    }
    /** Convert a legacy static layer to a token layer (in-place transform; preserve id, geometry, and content) */
    convertLegacyLayerToToken(legacyId){
        try {
            const stage = document.getElementById('block-stage'); if(!stage) return;
            const cfg = this.currentConfig || (this.currentConfig = this.getDefaultConfig());
            const ov = (cfg.legacy_overrides = cfg.legacy_overrides || {});
            const esc = (typeof CSS !== 'undefined' && typeof CSS.escape === 'function') ? CSS.escape : (s)=>String(s);
            const legacy = (cfg._hydratedLegacyLayers||[]).find(l=>l && l.id===legacyId);
            if(!legacy) return;
            const pos = {
                x: (ov[legacyId]?.x ?? legacy.x) || 0,
                y: (ov[legacyId]?.y ?? legacy.y) || 0,
                width: (ov[legacyId]?.width ?? legacy.width) || 200,
                height: (ov[legacyId]?.height ?? legacy.height) || 40,
            };
            if(!Array.isArray(cfg.layers)) cfg.layers = [];
            // Helper to finalize: mark converted, clean legacy DOM, refresh hydration/panel, and select transformed node
            const finalize = ()=>{
                ov[legacyId] = { ...(ov[legacyId]||{}), converted:true, hidden:true };
                // Remove any leftover legacy DOM node for this id (if we created a fresh token DOM instead of transforming)
                try {
                    stage.querySelectorAll(`.ccve-legacy-layer[data-layer-id="${esc(legacyId)}"]`).forEach(n=>n.remove());
                } catch(_) {}
                // Ensure legacy hydration pass will skip converted ones and DOM reflects updated state
                try { renderHydratedLegacyLayers(this); } catch(_) {}
                this.unsavedChanges = true;
                try { this.buildLayersPanel(); } catch(_) {}
                try {
                    const el = stage.querySelector(`[data-layer-id="${esc(legacyId)}"]`);
                    if(el){
                        // Prefer consistent selection id for panel highlight
                        this.selectedContainerId = null; // not a macro container; clear container selection
                        el.scrollIntoView({block:'nearest', inline:'nearest'});
                        // Ensure token interactions are bound and explicitly select the node for immediate editability
                        try { attachTokenLayerInteractions(this); } catch(_) {}
                        try { this.selectTokenLayer && this.selectTokenLayer(legacyId); } catch(_){ }
                    } else {
                        // Nothing to select (no DOM was created/modified). Avoid synthesizing boxes.
                    }
                } catch(_){ }
                try { this.updateSaveButton && this.updateSaveButton(); } catch(_) {}
            };
            if(legacy.kind==='static-text'){
                let template = (ov[legacyId]?.text ?? legacy.text) || '';
                // If legacy text is empty, prefill with a visible, sensible default so the layer isn't a blank box
                const hadVisibleLegacy = !!String(template).trim();
                if (!hadVisibleLegacy) {
                    // Keep a sensible default in data but DO NOT render a new DOM box
                    template = '{{track.title}} — {{track.artist}}';
                }
                // Prepare token layer object using the SAME id as legacy (push only after DOM transform success)
                let layer = (cfg.layers||[]).find(l=>l && l.id===legacyId) || { id: legacyId };
                layer.kind = 'token-text';
                layer.templateText = String(template).trim();
                // Precompute a rendered value from live data (fallback to current visible text)
                let liveRendered = '';
                try { if (this._liveTokenData && Object.keys(this._liveTokenData).length) { liveRendered = tokenRender(layer.templateText, this._liveTokenData).rendered || ''; } } catch(_) {}
                layer.renderedText = layer.renderedText || liveRendered || '';
                layer.x = pos.x; layer.y = pos.y; layer.width = pos.width; layer.height = pos.height;
                layer.visible = (layer.visible !== false);
                layer.lock = !!layer.lock;
                // Map legacy styles
                const mapped = {
                    font_size: (ov[legacyId]?.fontSize ?? legacy?.style?.fontSize) || 40,
                    color: (ov[legacyId]?.color ?? legacy?.style?.color) || '#ffffff',
                    font_weight: legacy?.style?.fontWeight || '600',
                    text_align: legacy?.style?.textAlign || 'left',
                    font_family: legacy?.style?.fontFamily || 'system-ui,Arial,Helvetica,sans-serif'
                };
                layer.style = Object.assign({}, layer.style||{}, mapped);
                // Transform existing legacy DOM node in-place when present; otherwise rehydrate once, then synthesize at geometry
                let node = stage.querySelector(`.ccve-legacy-layer[data-layer-id="${esc(legacyId)}"]`) || stage.querySelector(`.ccve-token-text-layer[data-layer-id="${esc(legacyId)}"]`);
                if (!node) { try { renderHydratedLegacyLayers(this); } catch(_) {} }
                node = node || stage.querySelector(`.ccve-legacy-layer[data-layer-id="${esc(legacyId)}"]`) || stage.querySelector(`.ccve-token-text-layer[data-layer-id="${esc(legacyId)}"]`);
                if (!node) {
                    // No legacy DOM present (likely empty text). Do not persist a token layer to avoid empty boxes on reload.
                    // Remove any pre-existing token entry matching this id
                    try { if (Array.isArray(cfg.layers)) cfg.layers = cfg.layers.filter(l=>!(l && l.id===legacyId)); } catch(_) {}
                    finalize();
                    try { this.showNotification && this.showNotification('Converted text layer (no visible source to transform).', 'info'); } catch(_) {}
                    return;
                }
                const previousVisibleText = (node.textContent||'').trim();
                node.className = 'canvas-container ccve-token-text-layer';
                // Ensure we do NOT mark token layers as containers
                Object.assign(node.style, {
                    position:'absolute', left:layer.x+'px', top:layer.y+'px', width:layer.width+'px', height:layer.height+'px',
                    display:'flex', alignItems:'center', pointerEvents:'auto',
                    fontSize:(layer.style?.font_size||40)+'px', fontWeight:String(layer.style?.font_weight||'600'), color:String(layer.style?.color||'#ffffff'),
                    textAlign:String(layer.style?.text_align||'left')
                });
                // Ensure inner text container exists and shows template immediately
                let inner = node.querySelector('.ccve-layer-text');
                if (!inner) {
                    node.innerHTML = `<div class="ccve-layer-text" data-token-layer="1" style="width:100%;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;"></div>`;
                    inner = node.querySelector('.ccve-layer-text');
                }
                if (inner) {
                    // Prefer actual rendered text; if unavailable, keep what was visually there before conversion
                    const displayText = (liveRendered && liveRendered.trim()) ? liveRendered : (previousVisibleText || layer.templateText || '');
                    inner.textContent = displayText;
                    inner.style.textAlign = node.style.textAlign;
                }
                // Push layer now that DOM transform succeeded
                try {
                    if (!Array.isArray(cfg.layers)) cfg.layers = [];
                    if (!cfg.layers.find(l=>l && l.id===legacyId)) cfg.layers.push(layer);
                } catch(_) {}
                try { this.refreshLiveTokens && this.refreshLiveTokens(); } catch(_) {}
                finalize();
                return;
            }
            if(legacy.kind==='static-image'){
                // Prompt user for token path; reuse token image modal if present, else quick prompt fallback
                const runWithToken=(tokenPath)=>{
                    if(!tokenPath) return;
                    // Prepare token image layer using SAME id (push only after DOM transform success)
                    let layer = (cfg.layers||[]).find(l=>l && l.id===legacyId) || { id: legacyId };
                    layer.kind='token-image';
                    layer.token = tokenPath;
                    layer.x=pos.x; layer.y=pos.y; layer.width=pos.width; layer.height=pos.height;
                    layer.visible = (layer.visible !== false); layer.lock = !!layer.lock;
                    // Transform existing legacy image DOM node in-place when present; otherwise rehydrate once, then synthesize at geometry
                    let node = stage.querySelector(`.ccve-legacy-layer[data-layer-id="${esc(legacyId)}"]`) || stage.querySelector(`.ccve-token-image-layer[data-layer-id="${esc(legacyId)}"]`);
                    if (!node) { try { renderHydratedLegacyLayers(this); } catch(_) {} }
                    node = node || stage.querySelector(`.ccve-legacy-layer[data-layer-id="${esc(legacyId)}"]`) || stage.querySelector(`.ccve-token-image-layer[data-layer-id="${esc(legacyId)}"]`);
                    if (!node) {
                        // No legacy DOM present. Do not persist token layer; avoid empty box on reload.
                        try { if (Array.isArray(cfg.layers)) cfg.layers = cfg.layers.filter(l=>!(l && l.id===legacyId)); } catch(_) {}
                        finalize();
                        try { this.showNotification && this.showNotification('Converted image layer (no visible source to transform).', 'info'); } catch(_) {}
                        try { this.refreshLiveTokens && this.refreshLiveTokens(); } catch(_) {}
                        return;
                    }
                    node.className='canvas-container ccve-token-image-layer';
                    // Ensure we do NOT mark token layers as containers
                    Object.assign(node.style, { position:'absolute', left:layer.x+'px', top:layer.y+'px', width:layer.width+'px', height:layer.height+'px', display:'flex', alignItems:'center', justifyContent:'center', overflow:'hidden' });
                    // Ensure img exists and mark data-token-image
                    let img = node.querySelector('img[data-token-image="1"]');
                    if (!img){ node.innerHTML = `<img data-token-image="1" alt="token artwork" style="width:100%;height:100%;object-fit:cover;display:block;" />`; img = node.querySelector('img[data-token-image="1"]'); }
                    // Carry over legacy presentation
                    try { const fit=(ov[legacyId]?.fit ?? legacy?.fit) || 'cover'; if(img) img.style.objectFit = fit; } catch(_){}
                    try { const br=(legacy?.borderRadius != null)? legacy.borderRadius : 8; node.style.borderRadius = String(br)+'px'; } catch(_){}
                    // Preserve currently visible image until live token resolves
                    try {
                        const legacyImg = node.querySelector('img') || null;
                        const currentSrc = legacyImg && legacyImg.getAttribute('src');
                        if (currentSrc && img && !img.src) img.src = currentSrc;
                    } catch(_) {}
                    // Push layer now that DOM transform succeeded
                    try {
                        if (!Array.isArray(cfg.layers)) cfg.layers = [];
                        if (!cfg.layers.find(l=>l && l.id===legacyId)) cfg.layers.push(layer);
                    } catch(_) {}
                    finalize();
                    try { this.refreshLiveTokens && this.refreshLiveTokens(); } catch(_){}
                };
                // Prefer modal if available
                try {
                    if(this.showModal){
                        const html = `
                          <div id="ccve-convert-image-token" style="font:12px system-ui,Arial">
                            <div style="margin-bottom:6px"><strong>Select artwork token for converted layer</strong></div>
                            <div style="display:flex;gap:8px;margin-bottom:10px">
                              ${['track.artwork','sponsor.artwork','promo.artwork'].map((t,i)=>`<label style="background:#1e293b;color:#fff;padding:6px 10px;border-radius:4px;border:1px solid #334155;cursor:pointer"><input type="radio" name="ccvetok" value="${t}" ${i===0?'checked':''}/> ${t}</label>`).join('')}
                            </div>
                            <div style="display:flex;justify-content:flex-end;gap:8px">
                              <button type=button id="ccve-cit-cancel" style="background:#64748b;color:#fff;border:1px solid #475569;padding:6px 10px;border-radius:4px;cursor:pointer">Cancel</button>
                              <button type=button id="ccve-cit-apply" style="background:#2563eb;color:#fff;border:1px solid #1d4ed8;padding:6px 14px;border-radius:4px;cursor:pointer">Convert</button>
                            </div>
                          </div>`;
                        this.showModal('Convert Image to Token Layer', html);
                        const root = document.getElementById('ccve-convert-image-token');
                        if(root){
                            root.querySelector('#ccve-cit-cancel')?.addEventListener('click', ()=>this.closeModal());
                            root.querySelector('#ccve-cit-apply')?.addEventListener('click', ()=>{
                                const val = root.querySelector('input[name="ccvetok"]:checked')?.value;
                                if(!val) return; this.closeModal(); runWithToken(val);
                            });
                            return;
                        }
                    }
                } catch(_){}
                // Fallback quick prompt
                const token = (typeof window.prompt==='function') ? window.prompt('Artwork token (track.artwork / sponsor.artwork / promo.artwork):','track.artwork') : 'track.artwork';
                if(token) runWithToken(token);
                return;
            }
        } catch(e){ console.warn('[CCVE] convertLegacyLayerToToken failed', e); }
    }

    /**
     * Phase 4 helper: create a new text layer with token templateText.
     * Minimal positioning logic: stack downward with 50px increments from bottom area.
     * Persists into currentConfig.layers (creating array if missing) and renders a basic DOM node.
     */
    addTokenTextLayer(templateText) {
        if (!this.currentConfig) this.currentConfig = this.getDefaultConfig();
        if (!Array.isArray(this.currentConfig.layers)) this.currentConfig.layers = [];
        const layers = this.currentConfig.layers;
        // 720p baseline: stack upward from bottom with line spacing
        const existingTextCount = layers.filter(l=>l && l.kind==='token-text').length;
        const lineH = 44; const gap = 10; const step = lineH + gap;
        const marginBottom = 20; const xMargin = 40; const width = 1200; // fit within 1280
        const yBase = 720 - marginBottom - lineH; // first line above bottom margin
        const y = Math.max(0, yBase - (existingTextCount * step));
        const id = 'tok_' + Math.random().toString(36).slice(2,9);
        const layerObj = {
            id,
            kind: 'token-text',
            templateText: String(templateText||'').trim(),
            renderedText: '',
            x: xMargin,
            y,
            width,
            height: lineH,
            visible: true,
            lock: false,
            style: { font_size: 40, color: '#ffffff', font_weight: '600', text_align: 'left' }
        };
        layers.push(layerObj);
        // DOM insertion (simplified – reuse block-stage if available)
        try {
            const stage = document.getElementById('block-stage');
            if (stage) {
                const node = document.createElement('div');
                node.className = 'canvas-container ccve-token-text-layer';
                node.setAttribute('data-layer-id', id);
                node.style.position='absolute';
                node.style.left = layerObj.x + 'px';
                node.style.top = layerObj.y + 'px';
                node.style.width = layerObj.width + 'px';
                node.style.height = layerObj.height + 'px';
                node.style.pointerEvents='auto';
                node.style.display='flex';
                node.style.alignItems='center';
                // Apply initial typography styles
                node.style.fontSize = layerObj.style.font_size + 'px';
                node.style.fontWeight = layerObj.style.font_weight;
                node.style.color = layerObj.style.color;
                node.innerHTML = `<div class="ccve-layer-text" data-token-layer="1" style="width:100%;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;"></div>`;
                stage.appendChild(node);
                // Show template text immediately as a visible placeholder until live tokens render
                try {
                    const txt = node.querySelector('.ccve-layer-text');
                    if (txt) {
                        txt.textContent = layerObj.templateText || '';
                        txt.style.textAlign = layerObj.style.text_align || 'left';
                    }
                } catch(_) {}
            }
        } catch(e) { console.warn('[CCVE][tokens] layer DOM create failed', e); }
        // Mark unsaved changes so user can persist config later
        this.unsavedChanges = true;
        try { this.updateSaveButton && this.updateSaveButton(); } catch(_) {}
        // Attempt immediate live render if data already loaded
        try {
            if (this._liveTokenData && Object.keys(this._liveTokenData).length) {
                const { rendered } = tokenRender(layerObj.templateText, this._liveTokenData);
                layerObj.renderedText = rendered;
                const el = document.querySelector(`[data-layer-id="${id}"] .ccve-layer-text`);
                if (el) el.textContent = rendered;
            }
        } catch(_) {}
        // Rebuild layers panel for visibility/order toggles
        try { this.buildLayersPanel(); } catch(_) {}
    try { this._pushTokenUndoState && this._pushTokenUndoState('add-text'); } catch(_) {}
        return layerObj;
    }

    /**
     * Phase 5 precursor: Add an image layer driven by an artwork token (track.artwork, sponsor.artwork, promo.artwork).
     * Fallback: if rendered token value empty, attempts branding fallback from currentConfig.artwork / design assets.
     */
    addTokenImageLayer(tokenPath) {
        if (!/^(track|sponsor|promo)\.artwork$/i.test(tokenPath||'')) return null; // supported scope
        if (!this.currentConfig) this.currentConfig = this.getDefaultConfig();
        if (!Array.isArray(this.currentConfig.layers)) this.currentConfig.layers = [];
        const layers = this.currentConfig.layers;
        const existingImgs = layers.filter(l=>l && l.kind==='token-image').length;
        const id = 'timg_' + Math.random().toString(36).slice(2,9);
    const size = 240; // square default to match export/reseed normalization
        const layerObj = {
            id,
            kind: 'token-image',
            token: tokenPath,
            x: 60 + (existingImgs * (size + 12)),
            y: 560, // place slightly above text row
            width: size,
            height: size,
            visible: true,
            lock: false,
            renderedUrl: ''
        };
        layers.push(layerObj);
        try {
            const stage = document.getElementById('block-stage');
            if (stage) {
                const node = document.createElement('div');
                node.className='canvas-container ccve-token-image-layer';
                node.setAttribute('data-layer-id', id);
                node.style.position='absolute';
                node.style.left=layerObj.x + 'px';
                node.style.top=layerObj.y + 'px';
                node.style.width=layerObj.width + 'px';
                node.style.height=layerObj.height + 'px';
                node.style.display='flex';
                node.style.alignItems='center';
                node.style.justifyContent='center';
                node.style.overflow='hidden';
                node.style.borderRadius='8px';
                node.style.background='#111';
                node.innerHTML = `<img data-token-image="1" alt="token artwork" style="width:100%;height:100%;object-fit:cover;display:block;" />`;
                stage.appendChild(node);
            }
        } catch(e){ console.warn('[CCVE][tokens] image layer DOM create failed', e); }
    this.unsavedChanges = true;
    try { this.updateSaveButton && this.updateSaveButton(); } catch(_) {}
        // Attempt render if data loaded
        this._applyTokenImageRender(layerObj);
        try { this.buildLayersPanel(); } catch(_) {}
    try { this._pushTokenUndoState && this._pushTokenUndoState('add-image'); } catch(_) {}
        return layerObj;
    }

    /** Apply artwork URL or fallback for a token-image layer */
    _applyTokenImageRender(layer) {
        if (!layer || layer.kind !== 'token-image') return;
        let url='';
        try {
            const [cat, field] = (layer.token||'').split('.');
            if (this._liveTokenData && this._liveTokenData[cat] && this._liveTokenData[cat][field]) {
                url = this._liveTokenData[cat][field];
            }
            if (!url) {
                // Branding fallback – prefer activeIndex item, then first, then explicit fallback_url
                const aw = this.currentConfig?.artwork || {};
                const items = Array.isArray(aw.items) ? aw.items : [];
                const ai = Number.isInteger(aw.activeIndex) ? aw.activeIndex : 0;
                const active = items[ai]?.url || items[0]?.url || '';
                const fallback = active || aw.fallback_url || '';
                if (fallback) url = fallback;
            }
        } catch(_) {}
        layer.renderedUrl = url;
        try {
            const img = document.querySelector(`#block-stage [data-layer-id="${layer.id}"] img[data-token-image="1"]`);
            if (img && url) {
                img.src = url;
            }
        } catch(_) {}
    }

    /** Inject token layers into outgoing save payload if present */
    _augmentSavePayload(payload){
        try {
            if (!this.currentConfig || !Array.isArray(this.currentConfig.layers)) return payload;
            // Serialize ALL layer types for persistence (not just token-text/token-image)
            const serializable = this.currentConfig.layers.map(l=>{
                if (!l || !l.kind) return null;
                
                // Base properties all layers share (including label for custom names)
                const base = { 
                    kind: l.kind, 
                    id: l.id, 
                    x: l.x, 
                    y: l.y, 
                    width: l.width, 
                    height: l.height,
                    visible: l.visible !== false,
                    lock: l.lock || false,
                    label: l.label || undefined,  // Persist custom layer names
                    zIndex: l.zIndex || undefined,
                    groupId: l.groupId || undefined  // Persist layer group membership
                };
                
                // Add layer-specific properties based on kind
                switch (l.kind) {
                    case 'token-text':
                        return { ...base, templateText: l.templateText, style: l.style ? { ...l.style } : undefined };
                    case 'token-image':
                        return { ...base, token: l.token, style: l.style ? { ...l.style } : undefined };
                    case 'static-text':
                        return { 
                            ...base, 
                            text: l.text, 
                            style: l.style ? { ...l.style } : undefined,
                            font_size: l.font_size,
                            font_weight: l.font_weight,
                            color: l.color,
                            text_shadow: l.text_shadow,
                            _wp_post_id: l._wp_post_id,
                            _wp_field: l._wp_field
                        };
                    case 'static-image':
                        return { 
                            ...base, 
                            url: l.url, 
                            objectFit: l.objectFit,
                            fit: l.fit,
                            border_radius: l.border_radius,
                            _wp_post_id: l._wp_post_id,
                            _wp_field: l._wp_field
                        };
                    case 'qr-image':
                        // QR code: save both data URL and source URL for re-render
                        return { ...base, url: l.url, sourceUrl: l.sourceUrl, qr_data: l.qr_data, qr_size: l.qr_size, qr_color: l.qr_color, qr_background: l.qr_background };
                    case 'slideshow-image':
                        return { ...base, sources: l.sources, interval: l.interval };
                    case 'background-image':
                        return { ...base, url: l.url, objectFit: l.objectFit || 'cover', opacity: l.opacity };
                    case 'wordpress-post':
                        return { ...base, post_id: l.post_id, fields: l.fields };
                    default:
                        // Unknown layer type - preserve as-is
                        return { ...l };
                }
            }).filter(Boolean);
            
            payload.visual_config = payload.visual_config || {};
            if (serializable.length) {
                payload.visual_config.token_layers = serializable; // side channel for all layer types
            }
            // Also ensure layers array is set directly on payload for direct persistence
            payload.layers = serializable;
            // Ensure authored_space normalization for saves
            try {
                payload.layout = payload.layout || {};
                payload.layout.authored_space = 'admin_1280x720';
            } catch(_) {}
            // Include legacy_overrides when present so transient static layers edits persist non-destructively
            try {
                const ov = this.currentConfig.legacy_overrides || {};
                if (ov && typeof ov === 'object' && Object.keys(ov).length) {
                    payload.visual_config.legacy_overrides = ov;
                }
            } catch(_) {}
            // Include layer groups for grouping feature persistence
            try {
                const groups = this.currentConfig.layerGroups || {};
                if (groups && typeof groups === 'object' && Object.keys(groups).length) {
                    payload.layerGroups = groups;
                }
            } catch(_) {}
        } catch(e){ console.warn('[CCVE][tokens] augment save failed', e); }
        return payload;
    }

    /** Hydrate legacy layers (static text/image/background) after a config is loaded */
    _applyLegacyHydration(){
        try {
            hydrateLegacyLayers(this);
            // Render hydrated DOM layers (static text/image) so they can be selected (read-only for now)
            renderHydratedLegacyLayers(this);
            // Rebuild layers panel to include hydrated entries
            try { this.buildLayersPanel(); } catch(_) {}
        } catch(e){ console.warn('[CCVE][legacy-hydration] failed', e); }
    }

    /** Render token layers (text/image) to DOM to ensure they are selectable and persist visually */
    // Phase 1 Step 7: Auto-Migration from Artwork.Items to Slideshow Layers
    _checkAndMigrateArtwork(){
        try {
            if (!this.currentConfig) return;
            
            // Check if config needs migration (has artwork.items[] but no slideshow layer)
            const needsMigration = 
                this.currentConfig.artwork && 
                Array.isArray(this.currentConfig.artwork.items) && 
                this.currentConfig.artwork.items.length > 0 &&
                !this.currentConfig.artwork._migrated;
            
            if (needsMigration) {
                console.info('[CC] Phase 1: Auto-migrating artwork.items[] to slideshow layer');
                const migrated = layerAutoMigrate(this);
                if (migrated) {
                    console.info('[CC] Phase 1: Migration successful');
                    // Migration marks config as unsaved, trigger re-render
                    this._renderUnifiedLayers();
                }
            }
        } catch(e) {
            console.error('[CC] Auto-migration failed:', e);
        }
    }

    // Phase 1 Step 7: Unified Layer Rendering
    _renderUnifiedLayers(){
        try {
            // DEBUG: Log what layers we have before rendering
            console.log('[CC][RENDER DEBUG] currentConfig.layers:', JSON.stringify(this.currentConfig?.layers?.map(l => ({ id: l?.id, kind: l?.kind, url: l?.url })) || []));
            if (!this.currentConfig || !Array.isArray(this.currentConfig.layers)) return;
            const stage = document.getElementById('block-stage'); if(!stage) return;
            
            // Ensure container boundary indicator exists
            this._ensureContainerBoundary(stage);
            
            // Step 1: Get valid layer IDs from config
            const validLayerIds = new Set(this.currentConfig.layers.map(l => l?.id).filter(Boolean));
            
            // Step 2: Remove orphan DOM elements (layers in DOM but not in config)
            const existingDomLayers = stage.querySelectorAll('.unified-layer');
            existingDomLayers.forEach(el => {
                const layerId = el.getAttribute('data-layer-id');
                if (layerId && !validLayerIds.has(layerId)) {
                    console.debug('[CC] Removing orphan layer from DOM:', layerId);
                    el.remove();
                }
            });
            
            // Step 3: Render all unified layers using centralized renderer
            this.currentConfig.layers.forEach(layer => {
                if (!layer || !layer.kind) return;
                try {
                    layerRenderUnified(this, layer, stage);
                } catch(e) {
                    console.warn('[CC] Failed to render unified layer:', layer.kind, layer.id, e);
                }
            });
            
            // Refresh Layer Manager to show all layers
            try { this.buildLayersPanel && this.buildLayersPanel(); } catch(_) {}
        } catch(e) {
            console.error('[CC] _renderUnifiedLayers failed:', e);
        }
    }

    /**
     * Ensure a visible container boundary overlay exists to show authoring bounds.
     * Shows the intended container area (e.g., 1280×240 for lower third).
     * 
     * COORDINATE SYSTEM: The boundary is positioned using layout.position
     * which tells us where the container renders on the 1280×720 canvas.
     * This is updated when the user changes the Intended Container dropdown.
     */
    _ensureContainerBoundary(stage) {
        // Container boundary visualization is now handled by the .cc-block-editor-wrap solid outline
        // This method is kept for backwards compatibility but does nothing
        // The solid outline in .ccve-block-bg provides the authoritative container geometry visual
        return;
    }

    _renderTokenLayers(){
        try {
            // Phase 1: Delegate to unified layer renderer for consistency
            // Keep this method for backward compatibility, but use unified rendering
            this._renderUnifiedLayers();
            return;
            
            // Legacy token-only rendering (commented out - replaced by unified system)
            /*
            // If mode is 'preview', layers can still render for selection; if 'layers', ensure preview chrome stays hidden via CSS
            if (!this.currentConfig || !Array.isArray(this.currentConfig.layers)) return;
            const stage = document.getElementById('block-stage'); if(!stage) return;
            const layers = this.currentConfig.layers.filter(l=>l && (l.kind==='token-text' || l.kind==='token-image'));
            */
            layers.forEach(layer=>{
                let node = stage.querySelector(`[data-layer-id="${layer.id}"]`);
                if (!node) {
                    node = document.createElement('div');
                    node.className = `canvas-container ${layer.kind==='token-text'?'ccve-token-text-layer':'ccve-token-image-layer'}`;
                    node.setAttribute('data-layer-id', layer.id);
                    stage.appendChild(node);
                }
                node.className = `canvas-container ${layer.kind==='token-text'?'ccve-token-text-layer':'ccve-token-image-layer'}`;
                Object.assign(node.style, { position:'absolute', left:layer.x+'px', top:layer.y+'px', width:layer.width+'px', height:layer.height+'px', pointerEvents:'auto', display:'flex', alignItems:'center' });
                if (layer.kind==='token-text'){
                    const ty = this.currentConfig?.typography || {};
                    node.style.fontSize = ((layer.style?.font_size ?? ty.font_size) || 40) + 'px';
                    node.style.fontWeight = String((layer.style?.font_weight ?? ty.font_weight) || '600');
                    node.style.color = String((layer.style?.color ?? ty.color) || '#ffffff');
                    node.style.textAlign = String((layer.style?.text_align ?? ty.text_align) || 'left');
                    node.style.fontFamily = mapFontFamilyToCSS((layer.style?.font_family ?? ty.font_family) || 'system-ui,Arial,Helvetica,sans-serif');
                    const shadow = (layer.style?.text_shadow ?? ty.text_shadow);
                    if (shadow && (Number.isFinite(shadow.x)||Number.isFinite(shadow.y)||Number.isFinite(shadow.blur)||shadow.color)) {
                        const sx = parseInt(shadow.x||0,10), sy = parseInt(shadow.y||0,10), sb = parseInt(shadow.blur||0,10), sc = shadow.color||'rgba(0,0,0,0)';
                        node.style.textShadow = `${sx}px ${sy}px ${sb}px ${sc}`;
                    } else {
                        node.style.textShadow = 'none';
                    }
                    let inner = node.querySelector('.ccve-layer-text');
                    if (!inner){ node.innerHTML = `<div class="ccve-layer-text" data-token-layer="1" style="width:100%;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;"></div>`; inner = node.querySelector('.ccve-layer-text'); }
                    // Prefer rendered text; fallback to template; last-chance seed from any existing legacy DOM with same id
                    let text = layer.renderedText || layer.templateText || '';
                    if (!text) {
                        try {
                            const legacyNode = stage.querySelector(`.ccve-legacy-layer[data-layer-id="${layer.id}"]`);
                            if (legacyNode && legacyNode.textContent) text = legacyNode.textContent.trim();
                        } catch(_) {}
                    }
                    inner.textContent = text;
                    inner.style.textAlign = node.style.textAlign;
                    if (layer.style?.line_height != null) { inner.style.lineHeight = String(layer.style.line_height); }
                    else if (ty.line_height != null) { inner.style.lineHeight = String(ty.line_height); }
                } else if (layer.kind==='token-image') {
                    node.style.justifyContent='center'; node.style.overflow='hidden'; node.style.borderRadius='8px'; node.style.zIndex='5';
                    let img = node.querySelector('img[data-token-image="1"]');
                    if (!img){ node.innerHTML = `<img data-token-image="1" alt="token artwork" style="width:100%;height:100%;object-fit:cover;display:block;" />`; img = node.querySelector('img[data-token-image="1"]'); }
                    // Allow global artwork fit to influence token image if layer has no explicit fit
                    try { const fit = layer.fit || this.currentConfig?.artwork?.fit || 'cover'; if (img) img.style.objectFit = fit; } catch(_) {}
                    if (layer.renderedUrl) {
                        img.src = layer.renderedUrl;
                    } else {
                        // Last-chance seed from legacy image DOM (if any)
                        try {
                            const legacyImg = stage.querySelector(`.ccve-legacy-layer[data-layer-id="${layer.id}"] img`);
                            const legacySrc = legacyImg?.getAttribute('src') || '';
                            if (legacySrc) img.src = legacySrc;
                        } catch(_) {}
                    }
                }
                // Edit button managed in interactions module
            });
            // Rebind interactions (idempotent guards inside modules)
            try { attachTokenLayerInteractions(this); } catch(_) {}
            try { this._updateEmptyTokenBadges(); } catch(_) {}
        } catch(e){ console.warn('[CCVE][tokens] render layers failed', e); }
    }

    /**
     * Provide a safe default visual configuration so early preview / override merges
     * never operate on undefined structures. (Reintroduced after refactor regression
     * where getDefaultConfig was missing, causing populateControls failures and blank fields.)
     */
    getDefaultConfig() {
        return {
            typography: {
                font_family: 'system-ui,Arial,Helvetica,sans-serif',
                font_size: 24,
                font_weight: '400',
                color: '#ffffff',
                text_align: 'left',
                line_height: 1.2,
                text_shadow: { x:0,y:0,blur:0,color:'rgba(0,0,0,0)' }
            },
            layout: {
                width: 1280,
                height: 240,
                position: { x:0, y:480 },
                padding: { top:0,right:60,bottom:10,left:60 },
                fitMode: 'fill',
                intendedContainer: 'lower_third'
            },
            background: {
                type: 'color',
                color: '#000000'
            },
            overlay: { color:'#000000', opacity:0 },
            artwork: { enabled:true, size:{ width:240, height:240 }, position:'left', gap:12, border_radius:0, apis:[], items:[], activeIndex:0, slideshow:{ enabled:false, interval:5000, transition:'fade' } },
            animation: { fade_in:true, duration:300, easing:'ease-in-out' },
            behavior: { ticker:{ enabled:true, speed:30 }, shuffle_items:false }
        };
    }

    /** Ensure scale host exists (wraps .ccve-stage-outer) */
    ensureScaleHost() {
        const frame = document.getElementById('block-stage-wrapper');
        if (!frame) return null;
        let host = frame.querySelector('.ccve-scale-host');
        if (host) return host;
        let stageOuter = frame.querySelector('.ccve-stage-outer');
        if (!stageOuter) {
            const legacyStage = frame.querySelector('#block-stage');
            if (legacyStage) {
                stageOuter = document.createElement('div');
                stageOuter.className = 'ccve-stage-outer';
                legacyStage.parentNode.insertBefore(stageOuter, legacyStage);
                stageOuter.appendChild(legacyStage);
            }
        }
        if (!stageOuter) return null;
        host = document.createElement('div');
        host.className = 'ccve-scale-host';
        frame.insertBefore(host, frame.firstChild);
        host.appendChild(stageOuter);
        return host;
    }
    
    /**
     * Initialize stage modules (viewport, rulers, fullscreen)
     * Called during editor initialization to set up the modular stage system.
     * @returns {boolean} Success
     */
    initStageModules() {
        const frame = document.getElementById('block-stage-wrapper');
        if (!frame) {
            console.warn('[CCVE] Stage frame not found, cannot initialize stage modules');
            return false;
        }
        
        // Initialize StageViewport
        if (!this._stageViewport) {
            this._stageViewport = new StageViewport({
                frameSelector: '#block-stage-wrapper',
                layoutSelector: '.canvas-editor-layout',
                debugMode: CCVE_ENABLE_ADVANCED_DEBUG,
                onScaleChange: (state) => {
                    // Update internal state for backward compatibility
                    this._blockStageFitScale = state.fitScale;
                    this._blockStageScale = state.appliedScale;
                    this._blockStageRelZoom = state.relativeZoom;
                    // Update scale badge if debug mode
                    this._showScaleBadge(state.appliedScale, state.fitScale, state.appliedScale < 0.35);
                    this._applyCompactChrome(state.appliedScale);
                    // Update zoom select if present
                    const sel = document.getElementById('ccve-zoom-select');
                    if (sel) {
                        const relPct = Math.round(state.relativeZoom * 100);
                        if (sel.value !== String(relPct)) sel.value = String(relPct);
                    }
                },
                onResize: () => {
                    try { this.logGeometryDiagnostics && this.logGeometryDiagnostics('viewport-resize'); } catch(_) {}
                }
            });
            this._stageViewport.init();
            console.debug('[CCVE] StageViewport initialized');
        }
        
        // Initialize StageRulers
        if (!this._stageRulers) {
            this._stageRulers = new StageRulers();
            this._stageRulers.init(frame);
            // Legacy compatibility
            this._rulersApi = {
                renderTicks: () => this._stageRulers.renderTicks(),
                setVisible: (on) => on ? this._stageRulers.show() : this._stageRulers.hide(),
                destroy: () => this._stageRulers.destroy()
            };
            // Set initial visibility from toggle state
            const rulersCb = document.getElementById('toggle-rulers');
            const shouldShow = rulersCb ? !!rulersCb.checked : false; // Default to hidden
            if (shouldShow) {
                this._stageRulers.show();
                frame.classList.add('has-rulers');
            } else {
                this._stageRulers.hide();
                frame.classList.remove('has-rulers');
            }
            console.debug('[CCVE] StageRulers initialized');
        }
        
        // Initialize FullscreenManager
        if (!this._fullscreenManager) {
            this._fullscreenManager = new FullscreenManager({
                containerSelector: '#block-stage-wrapper',
                buttonSelector: '#ccve-fullscreen-btn',
                onEnter: () => {
                    // Reapply scale on fullscreen enter
                    this._stageViewport?.applyScale(true);
                },
                onExit: () => {
                    // Reapply scale on fullscreen exit
                    this._stageViewport?.applyScale(true);
                }
            });
            this._fullscreenManager.init();
            console.debug('[CCVE] FullscreenManager initialized');
        }
        
        return true;
    }
    
    /** Provide shared zoom steps (percent of Fit) */
    getZoomSteps() { return ZOOM_STEPS; }
    toggleArtworkControls(enabled) {
        const panel = document.getElementById('category-artwork');
        if (!panel) return;
        panel.classList.toggle('ccve-artwork-disabled', !enabled);
    }

    /** Compute & apply fit scaling so 1280x720 always fits viewport.
     * REFACTORED: Now delegates to StageViewport module.
     * Legacy method kept for backward compatibility with existing calls.
     * Deterministic model:
     *  - No baselines or ratchets: every call derives size strictly from current viewport.
     *  - Frame width/height are set to EXACT scaled stage dimensions to avoid clipping.
     *  - Rulers are overlay-only and never influence size calculations.
     *  - Optional relative zoom applies only when Advanced (debug UI) is active; otherwise 1x of fit.
     */
    applyBlockStageScale(retry=false) {
        // Delegate to modular viewport if available
        if (this._stageViewport) {
            this._stageViewport.applyScale(retry);
            this._validateRulerAlignment();
            try { this.logGeometryDiagnostics && this.logGeometryDiagnostics(retry ? 'scale-retry-fit' : 'scale-fit'); } catch(_) {}
            return;
        }
        
        // Fallback to legacy implementation if module not initialized
        const frame = document.getElementById('block-stage-wrapper');
        if (!frame) return;
        let host = frame.querySelector('.ccve-scale-host');
        if (!host) host = this.ensureScaleHost();
        if (!host) return;
        // Rulers do not affect sizing; they are purely overlay
    const baseLogicalW = 1280;
    const baseLogicalH = 720;
    // Include rulers gutter in scaled footprint so stage fits inside rulers
    const thickness = (()=>{ try { const cs=getComputedStyle(document.documentElement); return parseInt(cs.getPropertyValue('--ccve-ruler-thickness')||'24',10)||24; } catch(_) { return 24; } })();
    // Available space snapshot (always current) — measure from stable outer layout, not the frame itself
    const layout = document.querySelector('.canvas-editor-layout') || (frame.parentElement || frame);
    const layoutRect = layout.getBoundingClientRect();
    const cs = getComputedStyle(layout);
    const padX = (parseFloat(cs.paddingLeft)||0) + (parseFloat(cs.paddingRight)||0);
        const padY = (parseFloat(cs.paddingTop)||0) + (parseFloat(cs.paddingBottom)||0);
    const availW = Math.max(0, Math.floor((layoutRect.width || layout.clientWidth || window.innerWidth) - padX));
    const availHeight = Math.max(200, Math.floor((layoutRect.height || layout.clientHeight || window.innerHeight) - padY));
        // Deterministic sizing: do not use window top offsets; rely solely on container height
        // Fit recompute with baseline cache to avoid ratchet between identical geometry states
    // FIXED: Rulers are overlay-only - do NOT include thickness in scale calculation
    const layoutKey = `${Math.round(availW)}x${Math.round(availHeight)}@${baseLogicalW}x${baseLogicalH}`;
        const tol = 0.0005;
    // FIXED: Scale based on canvas size only (1280x720), not canvas+rulers
    let fitScale = Math.min(availW / baseLogicalW, availHeight / baseLogicalH, 1);
        if (!this._fitBaselineCache) this._fitBaselineCache = new Map();
        const cached = this._fitBaselineCache.get(layoutKey);
        if (typeof cached === 'number' && Math.abs(cached - fitScale) < tol) {
            fitScale = cached; // snap to baseline to prevent subpixel drift
        } else {
            this._fitBaselineCache.set(layoutKey, fitScale);
        }
        if (!isFinite(fitScale) || fitScale <= 0) fitScale = 0.5;
        // Advanced/Debug gating determines whether relative zoom applies
    const debugUI = CCVE_ENABLE_ADVANCED_DEBUG && ((document.body?.dataset?.ccveDebug === '1') || (()=>{ try { return localStorage.getItem('ccveDebugUI')==='1'; } catch(_) { return false; } })());
        let rel = 1;
        if (debugUI) {
            try {
                const stored = parseFloat(localStorage.getItem('ccveBlockStageZoomRel'));
                if (!isNaN(stored) && stored > 0) rel = Math.min(stored, 1);
            } catch(_) {}
        } else {
            // Force reset (authoring surface always at fit outside advanced)
            try { localStorage.setItem('ccveBlockStageZoomRel','1'); } catch(_) {}
        }
        // Optional monitor-specific baseline adjustment
        try {
            const ua = navigator.userAgent||'';
            const w = window.innerWidth||0, h = window.innerHeight||0;
            const oddMac = /Macintosh/.test(ua) && (w===1470 && h===956);
            if (oddMac) {
                // Re-baseline at 1280 logical width within available space
                const baselineW = 1280; const baselineH = 720;
                const baseFit = Math.min(availW / (baselineW + thickness), availHeight / (baselineH + thickness), 1);
                fitScale = baseFit; // override fit baseline for this monitor profile
                // Update scale after baseline change
            }
        } catch(_) {}
        const scale = fitScale * rel;
        const MIN_READABLE = 0.35;
        const underReadable = scale < MIN_READABLE;
        host.style.transformOrigin = 'top left';
        host.style.transform = `scale(${scale})`;
        // Compute exact visual footprint and set frame to match to avoid clipping
    // FIXED: Visual footprint is canvas size only (1280x720), rulers render outside via negative positioning
    const visualW = baseLogicalW * scale;
    const visualH = baseLogicalH * scale;
    // Always apply exact computed footprint to avoid growth ratchets
    const nextW = Math.max(0, Math.round(visualW * 100) / 100);
    const nextH = Math.max(0, Math.round(visualH * 100) / 100);
    frame.style.width = nextW + 'px';
    frame.style.height = nextH + 'px';
    frame.setAttribute('data-last-w', nextW.toFixed(2));
    frame.setAttribute('data-last-h', nextH.toFixed(2));
        this._blockStageFitScale = fitScale; // authoritative fit
        this._blockStageScale = scale;       // applied (== fit when rel=1)
        this._blockStageRelZoom = rel;       // advanced-only relative zoom
        this._validateRulerAlignment();
        // Auto clear alignment warn if within tolerance
        if (this._rulerAlignWarn) {
            const tolRatio = 0.002;
            const hostR = host.getBoundingClientRect();
            const stage = host.querySelector('.ccve-stage-outer');
            const hR = host.querySelector('.ccve-ruler-horizontal');
            const vR = host.querySelector('.ccve-ruler-vertical');
            if (stage && hR && vR) {
                const sr = stage.getBoundingClientRect();
                const hr = hR.getBoundingClientRect();
                const vr = vR.getBoundingClientRect();
                const offX = Math.abs(hr.left - sr.left);
                const offY = Math.abs(vr.top - sr.top);
                const ratioW = Math.abs(1 - (hr.width / sr.width));
                const ratioH = Math.abs(1 - (vr.height / sr.height));
                if (offX < 1 && offY < 1 && ratioW < tolRatio && ratioH < tolRatio) {
                    this._rulerAlignWarn = false;
                }
            }
        }
    // Expose fit percent for diagnostics (replaces baseline attribute)
        try { frame.setAttribute('data-fit-percent', (fitScale*100).toFixed(2)); } catch(_) {}
        this._showScaleBadge(scale, fitScale, underReadable);
        this._applyCompactChrome(scale);
        try { this.logGeometryDiagnostics && this.logGeometryDiagnostics(retry ? 'scale-retry-fit' : 'scale-fit'); } catch(_) {}
        const sel = document.getElementById('ccve-zoom-select');
        if (sel) {
            const relPct = Math.round(rel * 100);
            if (sel.value !== String(relPct)) sel.value = String(relPct);
        }
    }

    /** Display / update scale badge & warnings */
    _showScaleBadge(scale, fit, underReadable) {
        const frame = document.getElementById('block-stage-wrapper');
        if (!frame) return;
        // Respect debug flag: only show scale HUD when debug UI enabled
    const debugUI = CCVE_ENABLE_ADVANCED_DEBUG && ((document.body?.dataset?.ccveDebug === '1') || (()=>{ try { return localStorage.getItem('ccveDebugUI')==='1'; } catch(_) { return false; } })());
        if (!debugUI) {
            // Remove existing badge if present (user requested HUD removal in normal mode)
            const existing = frame.querySelector('.ccve-scale-badge');
            if (existing) existing.remove();
            return; // Suppress HUD outside debug
        }
        let badge = frame.querySelector('.ccve-scale-badge');
        if (!badge) {
            badge = document.createElement('div');
            badge.className = 'ccve-scale-badge';
            frame.appendChild(badge);
        }
    const pct = (scale / fit) * 100;
    const pctRounded = Math.round(pct);
    const pieces = [];
    pieces.push(`Zoom ${pctRounded}% (Fit ${(fit*100).toFixed(1)}%)`);
    if (underReadable) pieces.push('LOW SCALE');
    if (this._rulerAlignWarn) pieces.push('ALIGN ⚠');
    badge.textContent = pieces.join(' · ');
        badge.classList.toggle('warn', underReadable || this._rulerAlignWarn);
        badge.classList.toggle('warn-low', underReadable);
        badge.classList.toggle('warn-align', !!this._rulerAlignWarn);
    }

    /** Compact chrome (hide nonessential UI) when very small zoom */
    _applyCompactChrome(scale) {
        const threshold = 0.30;
        const pinned = localStorage.getItem('ccveChromePinned') === '1';
        const root = document.querySelector('.canvas-editor-layout') || document.body;
        const wantCompact = scale < threshold && !pinned && !this._fullPixelActive;
        root.classList.toggle('ccve-compact', !!wantCompact);
        // Show hint once when entering compact
        if (wantCompact && !this._compactHintShown) {
            this._compactHintShown = true;
            try {
                const frame = document.getElementById('block-stage-wrapper');
                if (frame && !frame.querySelector('.ccve-compact-hint')) {
                    const hint = document.createElement('div');
                    hint.className='ccve-compact-hint';
                    hint.textContent='UI hidden <0.30 zoom. Increase zoom or Pin UI.';
                    frame.appendChild(hint);
                    setTimeout(()=>hint.remove(), 4000);
                }
            } catch(_) {}
        }
    }

    /** Step zoom relative index by delta over defined steps */
    _stepZoom(steps, delta) {
    const sel = document.getElementById('ccve-zoom-select');
        if (!sel) return;
    steps = Array.isArray(steps) && steps.length ? steps : this.getZoomSteps();
    const current = parseFloat(sel.value);
        const idx = steps.findIndex(s=>s===current);
        let nextIdx = idx === -1 ? steps.length-1 : idx + delta;
        if (nextIdx < 0) nextIdx = 0; else if (nextIdx >= steps.length) nextIdx = steps.length-1;
        if (nextIdx !== idx) {
            sel.value = String(steps[nextIdx]);
            const relVal = parseFloat(sel.value)/100;
            localStorage.setItem('ccveBlockStageZoomRel', String(relVal));
            this.applyBlockStageScale();
        }
    }

    /** Enter Full Pixel (1:1) overlay mode */
    enterFullPixelMode() {
        if (this._fullPixelActive) return;
        const frame = document.getElementById('block-stage-wrapper');
        const host = frame?.querySelector('.ccve-scale-host');
        if (!frame || !host) return;
    this._savedScaleTransform = host.style.transform;
    // Remove transform; we'll compute optional downscale separately
    host.style.transform = '';
    host.classList.add('fullpixel-active');
        const overlay = document.createElement('div');
        overlay.className = 'ccve-fullpixel-overlay';
        overlay.innerHTML = '<div class="ccve-fp-inner"><button type="button" class="ccve-fp-exit">×</button></div>';
        overlay.querySelector('.ccve-fp-inner').appendChild(host);
        document.body.appendChild(overlay);
        this._fullPixelOverlay = overlay;
        this._fullPixelActive = true;
        // If viewport smaller than logical, auto scale down (notice)
    // Full Pixel mode should consider only the true stage size (1280×720),
    // rulers are overlay and must not affect fit calculations.
    const logicalW = 1280;
    const logicalH = 720;
        const fitDown = Math.min((window.innerWidth-80) / logicalW, (window.innerHeight-80) / logicalH, 1);
        if (fitDown < 1) {
            host.style.transform = `scale(${fitDown})`;
            host.style.transformOrigin = 'top left';
            const note = document.createElement('div');
            note.className='ccve-fp-note';
            note.textContent = `Viewport smaller than 1280×720 – scaled to ${(fitDown*100).toFixed(1)}%`;
            overlay.appendChild(note);
        }
        const exit = () => this.exitFullPixelMode();
        overlay.querySelector('.ccve-fp-exit').addEventListener('click', exit);
        this._fpEscHandler = (e)=>{ if(e.key==='Escape') exit(); };
        window.addEventListener('keydown', this._fpEscHandler);
    }

    exitFullPixelMode() {
        if (!this._fullPixelActive) return;
        const overlay = this._fullPixelOverlay;
        const host = overlay?.querySelector('.ccve-scale-host');
        const frame = document.getElementById('block-stage-wrapper');
        if (host && frame) {
            // Restore host into frame
            frame.insertBefore(host, frame.firstChild);
            // Do not restore any prior transform; allow deterministic fitter to compute fresh
            host.style.transform = '';
            host.classList.remove('fullpixel-active');
        }
        overlay?.remove();
        window.removeEventListener('keydown', this._fpEscHandler || (()=>{}));
        this._fullPixelActive = false;
        this._fullPixelOverlay = null;
        this._savedScaleTransform = null;
        // Clear cached sizing so scale recomputes cleanly without ratcheting growth
        try {
            if (frame) {
                frame.removeAttribute('data-last-w');
                frame.removeAttribute('data-last-h');
                // Also clear any cached top offset snapshot to avoid stale baselines
                this._cachedTopOffset = null;
                this._windowSizeSig = null;
                frame.style.width = '';
                frame.style.height = '';
            }
        } catch(_) {}
        // Recompute scale on a settled frame to avoid measuring transient geometry
        try {
            requestAnimationFrame(() => requestAnimationFrame(() => {
                try { this.applyBlockStageScale(); } catch(e){ console.warn('[CCVE] post-exit scale apply failed', e); }
            }));
        } catch(_) { this.applyBlockStageScale(); }
    }

    _validateRulerAlignment() {
        const host = document.querySelector('#block-stage-wrapper .ccve-scale-host');
        const hR = host?.querySelector('.ccve-ruler-horizontal');
        const vR = host?.querySelector('.ccve-ruler-vertical');
        const stageOuter = host?.querySelector('.ccve-stage-outer');
        if (!host || !hR || !vR || !stageOuter) return;
        const sr = stageOuter.getBoundingClientRect();
        const hr = hR.getBoundingClientRect();
        const vr = vR.getBoundingClientRect();
        const offX = Math.round(hr.left - sr.left);
        const offY = Math.round(vr.top - sr.top);
        const ratioW = (hr.width / sr.width).toFixed(4);
        const ratioH = (vr.height / sr.height).toFixed(4);
        const drift = (offX !== 0 || offY !== 0 || Math.abs(1 - hr.width/sr.width) > 0.002 || Math.abs(1 - vr.height/sr.height) > 0.002);
        const prev = !!this._rulerAlignWarn;
        this._rulerAlignWarn = drift;
        if (drift) {
            // Throttle console noise: only log first detection per active drift episode
            if (!prev) console.warn('[CC][ruler-align] drift', { offX, offY, ratioW, ratioH });
        } else if (prev) {
            // Auto-clear notification once back in tolerance
            try { console.debug('[CC][ruler-align] realigned', { offX, offY, ratioW, ratioH }); } catch(_){}
        }
        // Schedule an auto re-check shortly to confirm stability before keeping warn state
        if (drift) {
            clearTimeout(this._rulerAlignRecheckTimer);
            this._rulerAlignRecheckTimer = setTimeout(()=>{
                try {
                    const sr2 = stageOuter.getBoundingClientRect();
                    const hr2 = hR.getBoundingClientRect();
                    const vr2 = vR.getBoundingClientRect();
                    const offX2 = Math.round(hr2.left - sr2.left);
                    const offY2 = Math.round(vr2.top - sr2.top);
                    const ratioW2 = Math.abs(1 - hr2.width/sr2.width);
                    const ratioH2 = Math.abs(1 - vr2.height/sr2.height);
                    if (offX2 === 0 && offY2 === 0 && ratioW2 < 0.002 && ratioH2 < 0.002) {
                        this._rulerAlignWarn = false; this._showScaleBadge(this._blockStageScale||1, this._blockStageFitScale||1, (this._blockStageScale||1) < 0.35);
                    }
                } catch(_){}
            }, 120); // small debounce window
        }
    }

    /** Mark editor as having unsaved changes and update UI */
    markUnsaved() {
        this.unsavedChanges = true;
        try { this.updateSaveButton && this.updateSaveButton(); } catch(_) {}
    }

    /**
     * Enable/disable Save buttons + unsaved indicator based on current state.
     * A save is only allowed when a content block is loaded AND there are unsavedChanges.
     * (Both global actions bar button and inline template button are synchronized.)
     */
    updateSaveButton() {
        try {
            const btns = [
                document.getElementById('ccve-global-save'),
                document.getElementById('canvas-save-config')
            ].filter(Boolean);
            const indicator = document.getElementById('ccve-unsaved-indicator');
            const canSave = !!this.currentContentBlockId && !!this.currentConfig && !!this.unsavedChanges;
            btns.forEach(btn => {
                // Only toggle when state actually changes to avoid unnecessary layout/reflows
                if (!!btn.disabled === canSave) {
                    btn.disabled = !canSave;
                } else {
                    btn.disabled = !canSave;
                }
                btn.title = canSave ? 'Save visual configuration' : 'Save disabled (no changes)';
            });
            if (indicator) indicator.style.display = canSave ? 'inline' : 'none';
        } catch(e){ /* non-fatal */ }
    }

    /**
     * Mark the editor as having unsaved changes and update the save button.
     * This is a convenience method used by layer add/edit operations.
     */
    markUnsavedChanges() {
        this.unsavedChanges = true;
        try { this.updateSaveButton && this.updateSaveButton(); } catch(_) {}
    }

    /**
     * Setup layout control event listeners (delegated to bindings module)
     */
    setupLayoutControls() {
        return bindingsSetupLayoutControls(this);
    }

    /**
     * Legacy wrapper for baseline capture script & external callers to apply intended container geometry presets.
     * Delegates to bindings layout applyPreset helper.
     */
    applyIntendedContainerPreset(presetKey) {
        try { return bindingsApplyPreset(this, presetKey); } catch(e) { console.warn('applyIntendedContainerPreset failed', e); return false; }
    }

    /**
     * Get a deterministic preview seed. Persist per-browser to keep sequences steady across reloads.
     * If you want purely per-session determinism, replace localStorage with a one-time Date.now() without storing.
     */
    getDeterministicSeed() { return seedGet(this); }
    simpleHash(str) { return seedHash(str); }

    /**
     * Load design assets (fonts, colors, etc.)
     */
    async loadDesignAssets() { return designLoad(this); }

    /**
     * Populate design asset controls
     */
    populateDesignAssets(assets) { return designPopulate(this, assets); }

    /**
     * Load content block configuration
     */
    async loadContentBlock(contentBlockId) { return apiLoadContentBlock(this, contentBlockId); }
    // Wrapper with logging
    async loadContentBlockWithLog(contentBlockId) {
        console.debug('[CC] loadContentBlockWithLog start', contentBlockId);
        try {
            try { localStorage.setItem('ccveLastBlockId', String(contentBlockId)); } catch(_) {}
            await this.loadContentBlock(contentBlockId);
            console.debug('[CC] loadContentBlockWithLog success', contentBlockId);
            // Legacy hydration pass (static/background → virtual editable layers)
            try { this._applyLegacyHydration(); } catch(_) {}
        }
        catch(e){ console.error('[CC] loadContentBlockWithLog error', e); this.showNotification && this.showNotification('Failed to load block '+contentBlockId,'error'); }
    // Phase 4 auto token refresh: after a block loads, if it contains token template layers, refresh once (debounced guard by internal pending flag)
    try { this.refreshLiveTokens && this.refreshLiveTokens(); } catch(_) {}
    }

    /**
     * Populate controls with configuration values
     */
    populateControls(config) {
        // Phase 1 Step 7: Check and migrate artwork.items[] to slideshow layers
        try { this._checkAndMigrateArtwork(); } catch(e) { console.warn('[CC] Migration check failed', e); }
        
        // Typography controls
        if (config.typography) {
            this.setControlValue('canvas-font-family', config.typography.font_family);
            this.setControlValue('canvas-font-size', config.typography.font_size);
            this.setControlValue('canvas-font-weight', config.typography.font_weight);
            this.setControlValue('canvas-font-color', config.typography.color);
            this.setControlValue('canvas-text-align', config.typography.text_align);
            this.setControlValue('canvas-line-height', config.typography.line_height);
            if (config.typography.text_shadow) {
                this.setControlValue('canvas-text-shadow-x', config.typography.text_shadow.x);
                this.setControlValue('canvas-text-shadow-y', config.typography.text_shadow.y);
                this.setControlValue('canvas-text-shadow-blur', config.typography.text_shadow.blur);
                this.setControlValue('canvas-text-shadow-color', config.typography.text_shadow.color);
            }
        }

        // Layout controls
        if (config.layout) {
            this.setControlValue('canvas-width', config.layout.width);
            this.setControlValue('canvas-height', config.layout.height);
            if (config.layout.position) {
                this.setControlValue('canvas-position-x', config.layout.position.x);
                this.setControlValue('canvas-position-y', config.layout.position.y);
            }
            if (config.layout.padding) {
                this.setControlValue('canvas-padding-top', config.layout.padding.top);
                this.setControlValue('canvas-padding-right', config.layout.padding.right);
                this.setControlValue('canvas-padding-bottom', config.layout.padding.bottom);
                this.setControlValue('canvas-padding-left', config.layout.padding.left);
            }
            // New: fitMode and intendedContainer
            if (config.layout.fitMode) {
                this.setControlValue('canvas-fit-mode', config.layout.fitMode);
            }
            // Intended container control only exists in Block Template panel; guard.
            if (config.layout.intendedContainer && document.getElementById('canvas-intended-container')) {
                this.setControlValue('canvas-intended-container', config.layout.intendedContainer);
            }
        }

        // Background controls
        if (config.background) {
            this.setControlValue('canvas-bg-type', config.background.type);
            this.setControlValue('canvas-bg-color', config.background.color);
            this.setControlValue('canvas-border-radius', config.background.border_radius);
            if (config.background.border) {
                this.setControlValue('canvas-border-width', config.background.border.width);
                this.setControlValue('canvas-border-color', config.background.border.color);
                this.setControlValue('canvas-border-style', config.background.border.style);
            }
            if (Array.isArray(config.background.gradient_colors)) {
                const [gc1, gc2] = config.background.gradient_colors;
                this.setControlValue('canvas-bg-gradient-color1', gc1);
                this.setControlValue('canvas-bg-gradient-color2', gc2);
            }
            if (config.background.gradient_direction) {
                this.setControlValue('canvas-bg-gradient-direction', config.background.gradient_direction);
            }
            this.toggleGradientControls && this.toggleGradientControls();
        }

        // Overlay controls
        if (config.overlay) {
            this.setControlValue('canvas-overlay-color', config.overlay.color);
            this.setControlValue('canvas-overlay-opacity', config.overlay.opacity);
        }

        // Artwork controls
        if (config.artwork) {
            this.setControlValue('canvas-artwork-enabled', config.artwork.enabled);
            this.toggleArtworkControls(config.artwork.enabled);
            
            if (config.artwork.size) {
                this.setControlValue('canvas-artwork-width', config.artwork.size.width);
                this.setControlValue('canvas-artwork-height', config.artwork.size.height);
            }
            this.setControlValue('canvas-artwork-position', config.artwork.position);
            this.setControlValue('canvas-artwork-border-radius', config.artwork.border_radius);
            
            // Set API checkboxes
            if (config.artwork.apis) {
                config.artwork.apis.forEach(api => {
                    const checkbox = document.querySelector(`input[name="canvas-artwork-apis[]"][value="${api}"]`);
                    if (checkbox) {
                        checkbox.checked = true;
                    }
                });
            }
        }

        // Animation & behavior
        if (config.animation) {
            this.setControlValue('canvas-anim-enabled', !!config.animation.fade_in);
            this.setControlValue('canvas-anim-duration', config.animation.duration);
            this.setControlValue('canvas-anim-easing', config.animation.easing);
        }
        if (config.behavior && config.behavior.ticker) {
            this.setControlValue('canvas-ticker-enabled', !!config.behavior.ticker.enabled);
            this.setControlValue('canvas-ticker-speed', config.behavior.ticker.speed);
        }
        if (config.behavior && typeof config.behavior.shuffle_items !== 'undefined') {
            this.setControlValue('canvas-shuffle-items', !!config.behavior.shuffle_items);
        }
        // After controls load, refresh Background Layers UI if available so overlay appears in the list post-refresh
        try { this.refreshBackgroundLayersUI && this.refreshBackgroundLayersUI(); } catch(_) {}
    }

    /**
     * Attach change/input listeners to core controls to trigger debounced real preview.
     * No dummy content generation: strictly schedules server-backed generatePreview.
     */
    attachPreviewControlWatchers() {
        const ids = [
            'canvas-font-family','canvas-font-size','canvas-font-weight','canvas-font-color','canvas-text-align','canvas-line-height',
            'canvas-text-shadow-x','canvas-text-shadow-y','canvas-text-shadow-blur','canvas-text-shadow-color',
            'canvas-width','canvas-height','canvas-position-x','canvas-position-y',
            'canvas-padding-top','canvas-padding-right','canvas-padding-bottom','canvas-padding-left',
            // Background / border / overlay
            'canvas-bg-type','canvas-bg-color','canvas-bg-gradient-color1','canvas-bg-gradient-color2','canvas-bg-gradient-direction',
            'canvas-border-radius','canvas-border-width','canvas-border-color','canvas-border-style',
            'canvas-overlay-color','canvas-overlay-opacity',
            // Animation
            'canvas-anim-enabled','canvas-anim-duration','canvas-anim-easing',
            // Behavior / ticker
            'canvas-ticker-enabled','canvas-ticker-speed','canvas-shuffle-items'
        ];
        ids.forEach(id => {
            const el = document.getElementById(id);
            if (!el || el.__ccvePreviewWatchBound) return;
            const handler = () => {
                try { this.markUnsaved && this.markUnsaved(); } catch(_) {}
                // Update currentConfig with latest control values (minimal subset)
                if (!this.currentConfig) this.currentConfig = this.getDefaultConfig();
                const cfg = this.currentConfig;
                cfg.layout = cfg.layout || {}; cfg.typography = cfg.typography || {};
                // If a token-text layer is selected, apply typography changes to that layer instead of global config
                // Use unified layer naming: .unified-layer--selected or legacy .ccve-selected
                let selectedNode = document.querySelector('#block-stage .unified-layer.unified-layer--selected[data-layer-kind="token-text"]') ||
                                     document.querySelector('#block-stage .unified-layer.unified-layer--selected[data-layer-kind="static-text"]') ||
                                     document.querySelector('#block-stage .ccve-token-text-layer.ccve-selected');
                let selectedLayerId = selectedNode?.getAttribute('data-layer-id') || null;
                
                // FALLBACK: If no DOM selection but we have a stored last selected text layer ID,
                // use that to find the layer and its DOM node. This handles cases where user clicks
                // on a layer (setting _lastSelectedTextLayerId) then clicks on Typography tab (losing DOM focus).
                if (!selectedLayerId && this._lastSelectedTextLayerId) {
                    selectedLayerId = this._lastSelectedTextLayerId;
                    selectedNode = document.querySelector(`#block-stage .unified-layer[data-layer-id="${selectedLayerId}"]`);
                    console.log('[CCVE][Typography] Using fallback _lastSelectedTextLayerId:', selectedLayerId);
                }
                
                const selectedLayer = selectedLayerId ? (cfg.layers||[]).find(l=>l && (l.kind==='token-text' || l.kind==='static-text') && l.id===selectedLayerId) : null;
                
                // DEBUG: Log layer selection state for typography changes
                if (id.startsWith('canvas-font') || id.startsWith('canvas-text-align') || id.startsWith('canvas-line-height')) {
                    const anySelected = document.querySelector('#block-stage .unified-layer.unified-layer--selected');
                    console.log('[CCVE][Typography] Control changed:', id, {
                        anySelectedLayer: !!anySelected,
                        anySelectedKind: anySelected?.getAttribute('data-layer-kind'),
                        selectedNode: !!selectedNode,
                        selectedLayerId,
                        selectedLayerFound: !!selectedLayer,
                        layersCount: (cfg.layers||[]).length
                    });
                }
                
                if (id.startsWith('canvas-font') || id.startsWith('canvas-text-align') || id.startsWith('canvas-line-height')) {
                    if (selectedLayer) {
                        console.log('[CCVE][Typography] Applying to SELECTED layer:', selectedLayerId, selectedLayer.kind);
                        selectedLayer.style = selectedLayer.style || {};
                        if (id === 'canvas-font-family' || id.startsWith('canvas-font-')) {
                            selectedLayer.style.font_family = this.getControlValue('canvas-font-family');
                            // Clamp font size to Roku-safe range (12-80pt)
                            let v = parseInt(this.getControlValue('canvas-font-size'));
                            if (!isNaN(v)) {
                                if (v < 12) v = 12;
                                if (v > 80) v = 80;
                                selectedLayer.style.font_size = v;
                            }
                            // Normalize font weight to 400 or 700
                            let fw = String(this.getControlValue('canvas-font-weight')||'400');
                            const fwNum = parseInt(fw);
                            selectedLayer.style.font_weight = (!isNaN(fwNum) && fwNum >= 600) ? '700' : '400';
                            selectedLayer.style.color = this.getControlValue('canvas-font-color')||selectedLayer.style.color;
                        }
                        if (id === 'canvas-text-align') {
                            selectedLayer.style.text_align = this.getControlValue('canvas-text-align')||selectedLayer.style.text_align;
                        }
                        if (id === 'canvas-line-height') {
                            const lh = parseFloat(this.getControlValue('canvas-line-height')); if (!isNaN(lh)) selectedLayer.style.line_height = lh;
                        }
                        // Apply to DOM immediately
                        try {
                            if (selectedNode) {
                                const ty = selectedLayer.style;
                                // Find inner text element (unified naming or legacy)
                                const inner = selectedNode.querySelector('.layer-text-content') || selectedNode.querySelector('.ccve-layer-text');
                                if (ty.font_size!=null) { 
                                    selectedNode.style.fontSize = ty.font_size + 'px';
                                    if (inner) inner.style.fontSize = ty.font_size + 'px';
                                }
                                if (ty.font_weight) { 
                                    selectedNode.style.fontWeight = String(ty.font_weight);
                                    if (inner) inner.style.fontWeight = String(ty.font_weight);
                                }
                                if (ty.color) { 
                                    selectedNode.style.color = String(ty.color);
                                    if (inner) inner.style.color = String(ty.color);
                                }
                                if (ty.text_align) { 
                                    selectedNode.style.textAlign = String(ty.text_align);
                                    if (inner) inner.style.textAlign = String(ty.text_align);
                                }
                                if (ty.font_family) { 
                                    selectedNode.style.fontFamily = mapFontFamilyToCSS(ty.font_family);
                                    if (inner) inner.style.fontFamily = mapFontFamilyToCSS(ty.font_family);
                                }
                                if (ty.line_height!=null) { 
                                    if (inner) inner.style.lineHeight = String(ty.line_height);
                                }
                            }
                        } catch(_) {}
                        this.unsavedChanges = true;
                        try { this.updateSaveButton && this.updateSaveButton(); } catch(_) {}
                        // Avoid regenerating preview for per-layer style changes
                        return;
                    } else {
                        // Fallback to global typography when no token-text selection exists
                        console.log('[CCVE][Typography] Applying to GLOBAL typography (no layer selected)');
                        cfg.typography.font_family = this.getControlValue('canvas-font-family');
                        cfg.typography.font_size = this.getControlValue('canvas-font-size');
                        cfg.typography.font_weight = this.getControlValue('canvas-font-weight');
                        cfg.typography.color = this.getControlValue('canvas-font-color');
                        cfg.typography.text_align = this.getControlValue('canvas-text-align');
                        cfg.typography.line_height = this.getControlValue('canvas-line-height');
                    }
                }
                if (id.startsWith('canvas-text-shadow')) {
                    if (selectedLayer) {
                        selectedLayer.style = selectedLayer.style || {};
                        selectedLayer.style.text_shadow = selectedLayer.style.text_shadow || {};
                        const sx = parseInt(this.getControlValue('canvas-text-shadow-x'))||0;
                        const sy = parseInt(this.getControlValue('canvas-text-shadow-y'))||0;
                        const sb = parseInt(this.getControlValue('canvas-text-shadow-blur'))||0;
                        const sc = this.getControlValue('canvas-text-shadow-color')||'rgba(0,0,0,0)';
                        selectedLayer.style.text_shadow.x = sx; selectedLayer.style.text_shadow.y = sy; selectedLayer.style.text_shadow.blur = sb; selectedLayer.style.text_shadow.color = sc;
                        try {
                            if (selectedNode) {
                                const css = `${sx}px ${sy}px ${sb}px ${sc}`;
                                selectedNode.style.textShadow = css;
                            }
                        } catch(_) {}
                        this.unsavedChanges = true;
                        try { this.updateSaveButton && this.updateSaveButton(); } catch(_) {}
                        return;
                    } else {
                        cfg.typography.text_shadow = cfg.typography.text_shadow || {};
                        cfg.typography.text_shadow.x = parseInt(this.getControlValue('canvas-text-shadow-x'))||0;
                        cfg.typography.text_shadow.y = parseInt(this.getControlValue('canvas-text-shadow-y'))||0;
                        cfg.typography.text_shadow.blur = parseInt(this.getControlValue('canvas-text-shadow-blur'))||0;
                        cfg.typography.text_shadow.color = this.getControlValue('canvas-text-shadow-color')||'rgba(0,0,0,0)';
                    }
                }
                if (id.startsWith('canvas-width') || id.startsWith('canvas-height')) {
                    cfg.layout.width = parseInt(this.getControlValue('canvas-width'))||cfg.layout.width||600;
                    cfg.layout.height = parseInt(this.getControlValue('canvas-height'))||cfg.layout.height||200;
                }
                if (id.startsWith('canvas-position')) {
                    cfg.layout.position = cfg.layout.position || { x:0, y:0 };
                    cfg.layout.position.x = parseInt(this.getControlValue('canvas-position-x'))||0;
                    cfg.layout.position.y = parseInt(this.getControlValue('canvas-position-y'))||0;
                }
                if (id.startsWith('canvas-padding')) {
                    cfg.layout.padding = cfg.layout.padding || { top:0,right:0,bottom:0,left:0 };
                    cfg.layout.padding.top = parseInt(this.getControlValue('canvas-padding-top'))||0;
                    cfg.layout.padding.right = parseInt(this.getControlValue('canvas-padding-right'))||0;
                    cfg.layout.padding.bottom = parseInt(this.getControlValue('canvas-padding-bottom'))||0;
                    cfg.layout.padding.left = parseInt(this.getControlValue('canvas-padding-left'))||0;
                }
                if (id.startsWith('canvas-bg-') || id.startsWith('canvas-border-') || id.startsWith('canvas-overlay-')) {
                    cfg.background = cfg.background || {};
                    cfg.background.type = this.getControlValue('canvas-bg-type');
                    cfg.background.color = this.getControlValue('canvas-bg-color');
                    cfg.background.gradient_colors = [
                        this.getControlValue('canvas-bg-gradient-color1')||'#000000',
                        this.getControlValue('canvas-bg-gradient-color2')||'#222222'
                    ];
                    cfg.background.gradient_direction = this.getControlValue('canvas-bg-gradient-direction')||'135deg';
                    cfg.background.border_radius = parseInt(this.getControlValue('canvas-border-radius'))||0;
                    cfg.background.border = {
                        width: parseInt(this.getControlValue('canvas-border-width'))||0,
                        color: this.getControlValue('canvas-border-color')||'#000000',
                        style: this.getControlValue('canvas-border-style')||'solid'
                    };
                    // overlay block
                    cfg.overlay = cfg.overlay || { color:'#000000', opacity:0 };
                    cfg.overlay.color = this.getControlValue('canvas-overlay-color')||cfg.overlay.color;
                    const op = parseFloat(this.getControlValue('canvas-overlay-opacity'));
                    cfg.overlay.opacity = isNaN(op) ? (cfg.overlay.opacity||0) : op;
                    try { this.toggleGradientControls && this.toggleGradientControls(); } catch(_) {}
                }
                if (id.startsWith('canvas-anim-')) {
                    cfg.animation = cfg.animation || { fade_in:true, duration:300, easing:'ease-in-out' };
                    cfg.animation.fade_in = !!document.getElementById('canvas-anim-enabled')?.checked;
                    cfg.animation.duration = parseInt(this.getControlValue('canvas-anim-duration'))||300;
                    cfg.animation.easing = this.getControlValue('canvas-anim-easing')||'ease-in-out';
                }
                if (id.startsWith('canvas-ticker-') || id === 'canvas-shuffle-items') {
                    cfg.behavior = cfg.behavior || { ticker:{enabled:false,speed:30}, shuffle_items:false };
                    cfg.behavior.ticker = cfg.behavior.ticker || { enabled:false, speed:30 };
                    cfg.behavior.ticker.enabled = !!document.getElementById('canvas-ticker-enabled')?.checked;
                    cfg.behavior.ticker.speed = parseInt(this.getControlValue('canvas-ticker-speed'))||30;
                    cfg.behavior.shuffle_items = !!document.getElementById('canvas-shuffle-items')?.checked;
                }
                // Reposition wrap immediately for geometry changes
                try { this.updateBlockEditorRectFromConfig(); } catch(_) {}
                // Debounced server preview
                if (this.generatePreviewDebounced) this.generatePreviewDebounced(); else this.generatePreview();
            };
            el.addEventListener('input', handler);
            el.addEventListener('change', handler);
            el.__ccvePreviewWatchBound = true;
        });
    }

    /**
     * Set control value helper
     */
    setControlValue(controlId, value) {
        const control = document.getElementById(controlId);
        if (control) {
            if (control.type === 'checkbox') {
                control.checked = !!value;
            } else {
                // Avoid setting undefined/empty to inputs that expect a value (e.g., color)
                const safe = (value === undefined || value === null || value === '')
                    ? (control.type === 'color' ? control.value || '#000000' : control.value)
                    : value;
                control.value = safe;
            }
        }
    }

    /**
     * Update typography configuration
     */
    updateTypographyConfig() {
        if (!this.currentConfig) {
            this.currentConfig = this.getDefaultConfig();
        }

        // Validate and clamp font size to Roku-safe range (12-80pt)
        let fontSize = parseInt(this.getControlValue('canvas-font-size'));
        if (isNaN(fontSize)) fontSize = 18;
        if (fontSize < 12) fontSize = 12;
        if (fontSize > 80) fontSize = 80;

        // Normalize font weight to Roku-safe values (400 or 700 only)
        let fontWeight = this.getControlValue('canvas-font-weight');
        const weightNum = parseInt(fontWeight);
        if (!isNaN(weightNum)) {
            fontWeight = (weightNum >= 600) ? '700' : '400';
        } else {
            fontWeight = '400';
        }

        this.currentConfig.typography = {
            font_family: this.getControlValue('canvas-font-family'),
            font_size: fontSize,
            font_weight: fontWeight,
            color: this.getControlValue('canvas-font-color'),
            text_align: this.getControlValue('canvas-text-align'),
            line_height: parseFloat(this.getControlValue('canvas-line-height')),
            text_shadow: {
                x: parseInt(this.getControlValue('canvas-text-shadow-x') || '0', 10),
                y: parseInt(this.getControlValue('canvas-text-shadow-y') || '0', 10),
                blur: parseInt(this.getControlValue('canvas-text-shadow-blur') || '0', 10),
                color: this.getControlValue('canvas-text-shadow-color') || '#000000'
            }
        };
    }

    /**
     * Update layout configuration
     */
    updateLayoutConfig() {
        if (!this.currentConfig) {
            this.currentConfig = this.getDefaultConfig();
        }

        this.currentConfig.layout = {
            width: (()=>{ const v=parseInt(this.getControlValue('canvas-width')); return isNaN(v)?1280:v; })(),
            height: (()=>{ const v=parseInt(this.getControlValue('canvas-height')); return isNaN(v)?360:v; })(),
            position: {
                x: (()=>{ const v=parseInt(this.getControlValue('canvas-position-x')); return isNaN(v)?0:v; })(),
                y: (()=>{ const v=parseInt(this.getControlValue('canvas-position-y')); return isNaN(v)?(720-240):v; })()
            },
            padding: {
                top: (()=>{ const v=parseInt(this.getControlValue('canvas-padding-top')); return isNaN(v)?0:v; })(),
                right: (()=>{ const v=parseInt(this.getControlValue('canvas-padding-right')); return isNaN(v)?60:v; })(),
                bottom: (()=>{ const v=parseInt(this.getControlValue('canvas-padding-bottom')); return isNaN(v)?10:v; })(),
                left: (()=>{ const v=parseInt(this.getControlValue('canvas-padding-left')); return isNaN(v)?60:v; })()
            },
            fitMode: (()=>{ const v = this.getControlValue('canvas-fit-mode') || 'fill'; return v === 'fit' ? 'fill' : v; })(),
            intendedContainer: document.getElementById('canvas-intended-container') ? this.getControlValue('canvas-intended-container') || '' : (this.currentConfig.layout?.intendedContainer||'')
        };
    // Synchronize legacy fields to avoid mismatches elsewhere
    this.currentConfig.layout.x_position = this.currentConfig.layout.position.x;
    this.currentConfig.layout.y_position = this.currentConfig.layout.position.y;
        try {
            if (window?.console) {
                console.debug('[CCVE] layoutConfigUpdated', {
                    w: this.currentConfig.layout.width,
                    h: this.currentConfig.layout.height,

    // (Removed duplicate applyBlockStageScale/_validateRulerAlignment that were incorrectly injected here)
                    x: this.currentConfig.layout.position.x,
                    y: this.currentConfig.layout.position.y,
                    fit: this.currentConfig.layout.fitMode,
                    intended: this.currentConfig.layout.intendedContainer
                });
            }
        } catch(_) {}
    }

    /**
     * Update background configuration
     */
    updateBackgroundConfig() {
        if (!this.currentConfig) {
            this.currentConfig = this.getDefaultConfig();
        }

        // PRESERVE existing background.layers! They are managed by the Background Layers UI,
        // not by the simple control inputs. Wiping them here breaks overlay functionality.
        const existingLayers = this.currentConfig.background?.layers;
        const existingOpacity = this.currentConfig.background?.opacity;
        
        this.currentConfig.background = {
            type: this.getControlValue('canvas-bg-type'),
            color: this.getControlValue('canvas-bg-color'),
            opacity: existingOpacity ?? 0.9, // Preserve existing opacity or use default
            gradient_colors: [
                this.getControlValue('canvas-bg-gradient-color1') || '#000000',
                this.getControlValue('canvas-bg-gradient-color2') || '#333333'
            ],
            gradient_direction: this.getControlValue('canvas-bg-gradient-direction') || '135deg',
            border_radius: parseInt(this.getControlValue('canvas-border-radius')),
            border: {
                width: parseInt(this.getControlValue('canvas-border-width')),
                color: this.getControlValue('canvas-border-color'),
                style: this.getControlValue('canvas-border-style')
            },
            // CRITICAL: Restore layers array to preserve overlay/gradient/image layer settings
            layers: existingLayers || []
        };

        this.currentConfig.overlay = {
            color: this.getControlValue('canvas-overlay-color') || '#000000',
            opacity: parseFloat(this.getControlValue('canvas-overlay-opacity') || '0')
        };
    }

    /**
     * Update artwork configuration
     */
    updateArtworkConfig() {
        if (!this.currentConfig) {
            this.currentConfig = this.getDefaultConfig();
        }

        const enabled = document.getElementById('canvas-artwork-enabled')?.checked || false;
        const apis = Array.from(document.querySelectorAll('input[name="canvas-artwork-apis[]"]:checked'))
                          .map(cb => cb.value);

        this.currentConfig.artwork = {
            enabled: enabled,
            size: {
                width: (()=>{ const v=parseInt(this.getControlValue('canvas-artwork-width')); return isNaN(v)?(this.currentConfig.artwork?.size?.width||150):v; })(),
                height: (()=>{ const v=parseInt(this.getControlValue('canvas-artwork-height')); return isNaN(v)?(this.currentConfig.artwork?.size?.height||150):v; })()
            },
            position: this.getControlValue('canvas-artwork-position'),
            gap: (()=>{ const v=parseInt(this.getControlValue('canvas-artwork-gap')); return isNaN(v)?(this.currentConfig.artwork?.gap||12):v; })(),
            border_radius: parseInt(this.getControlValue('canvas-artwork-border-radius')),
            apis: apis
        };
    }

    /**
     * Get control value helper
     */
    getControlValue(controlId) {
        const control = document.getElementById(controlId);
        if (control) {
            return control.type === 'checkbox' ? control.checked : control.value;
        }
        return '';
    }

    /**
     * Show/hide gradient controls based on background type
     */
    toggleGradientControls() {
        const type = this.getControlValue('canvas-bg-type');
        const block = document.getElementById('canvas-bg-gradient-controls');
        if (!block) return;
        block.style.display = (type === 'gradient') ? '' : 'none';
        // Toggle solid color group visibility
        const bgColorInput = document.getElementById('canvas-bg-color');
        if (bgColorInput && typeof bgColorInput.closest === 'function') {
            const colorGroup = bgColorInput.closest('.canvas-control-group');
            if (colorGroup) {
                colorGroup.style.display = (type === 'solid') ? '' : 'none';
            }
        }
    }

    /**
     * Sync color pickers with adjacent text fields (when present)
     */
    setupColorPickerSync() {
        const pickers = document.querySelectorAll('.canvas-color-picker');
        pickers.forEach(p => {
            const colorInput = p.querySelector('input[type="color"]');
            const textInput = p.querySelector('input.canvas-color-value');
            if (!colorInput || !textInput) return;
            const setText = (val) => { textInput.classList.remove('invalid'); textInput.value = val; };
            // color -> text
            colorInput.addEventListener('input', () => {
                setText(colorInput.value);
                colorInput.dispatchEvent(new Event('change'));
            });
            // text -> color (hex only)
            const applyText = () => {
                const v = (textInput.value || '').trim();
                const hexOk = /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/.test(v);
                if (hexOk) {
                    colorInput.value = v;
                    colorInput.dispatchEvent(new Event('input'));
                } else if (v === '') {
                    textInput.classList.remove('invalid');
                } else {
                    textInput.classList.add('invalid');
                }
            };
            textInput.addEventListener('change', applyText);
            textInput.addEventListener('blur', applyText);
            // init
            setText(colorInput.value);
        });
    }

    // debounce helper migrated to utils.debounce

    // triggerPreview / generatePreview / getPreviewScale moved to preview-glue (attached post-construction)

    /**
     * Focus, scroll to, and shake the first invalid input in the editor
     */
    focusFirstInvalidInput() {
        const invalid = document.querySelector('.canvas-editor input.invalid, .canvas-editor textarea.invalid, .canvas-editor select.invalid');
        if (invalid) {
            invalid.focus();
            if (typeof invalid.scrollIntoView === 'function') {
                invalid.scrollIntoView({ behavior: 'smooth', block: 'center' });
            }
            invalid.classList.add('shake');
            setTimeout(() => invalid.classList.remove('shake'), 500);
        }
    }

    /**
     * Attach unified auto-preview watchers for all canvas-* controls.
     * Idempotent; design captured in 2025-08-31 handoff log.
     */
    attachAutoPreviewControlWatchers() {
        if (this.__autoPreviewWatchersAttached) return;
        this.__autoPreviewWatchersAttached = true;
        const editor = this;
        const container = document.querySelector('.canvas-editor');
        if (!container) return;
        const handler = (ev) => {
            const t = ev.target;
            if (!t || !t.id || !t.id.startsWith('canvas-')) return;
            try {
                editor.updateLayoutConfig();
                editor.updateTypographyConfig();
                editor.updateBackgroundConfig();
                editor.updateArtworkConfig();
            } catch(e) { console.warn('[CC] autoPreview update error', e); }
            editor.unsavedChanges = true; editor.updateSaveButton && editor.updateSaveButton();
            try { editor.triggerPreview(false); } catch(e) { console.warn('[CC] autoPreview trigger error', e); }
            // Also ensure token layers reflect content edits immediately on the canvas
            try { editor._renderTokenLayers && editor._renderTokenLayers(); } catch(_) {}
            try { editor.refreshLiveTokens && editor.refreshLiveTokens(); } catch(_) {}
        };
        container.addEventListener('input', handler, true);
        container.addEventListener('change', handler, true);
    }

    // (preview glue helpers injected by preview-glue module)

    /** Apply currentConfig.layout to the interactive preview wrapper (delegated) */
    updateBlockEditorRectFromConfig() { return _blockUpdateRect(this); }

    /** Ensure persistent HUD badge exists (rulers handled by rulers module) */
    _ensurePersistentScaffolding() {
        try {
            const wrap = this.previewContainer?.querySelector('.cc-block-editor-wrap');
            if (wrap && !wrap.querySelector('.ccve-dim-hud')) {
                const hud = document.createElement('div'); hud.className='ccve-dim-hud'; hud.textContent=''; wrap.appendChild(hud);
            }
        } catch(e) { /* swallow */ }
    }

    // ==================== [REGION] DRAG & RESIZE (Preview Block) ====================
    /** Bind drag/resize for the interactive preview block */
    bindBlockEditorInteractions(wrap) { return _blockBindInteractions(this, wrap); }

    /**
     * Delete the currently loaded content block.
     * Prompts for confirmation before deletion.
     */
    async deleteCurrentContentBlock() {
        if (!this.currentContentBlockId) {
            this.showNotification && this.showNotification('No content block selected to delete', 'warning');
            return;
        }
        const blockName = this.currentConfig?.name || this.currentBlockName || `Block #${this.currentContentBlockId}`;
        
        // Triple confirmation to prevent accidental deletion
        const confirm1 = confirm(`⚠️ WARNING: You are about to delete "${blockName}"\n\nThis action CANNOT be undone.\n\nClick OK to proceed to second confirmation.`);
        if (!confirm1) return;
        
        const confirm2 = confirm(`🔴 SECOND CONFIRMATION\n\nAre you ABSOLUTELY SURE you want to permanently delete:\n\n"${blockName}"\n\nClick OK to proceed to final confirmation.`);
        if (!confirm2) {
            this.showNotification && this.showNotification('Deletion cancelled', 'info');
            return;
        }
        
        const typedName = prompt(`🛑 FINAL CONFIRMATION\n\nTo permanently delete this content block, type the block name exactly:\n\n${blockName}\n\nType the name to confirm deletion:`);
        if (typedName !== blockName) {
            this.showNotification && this.showNotification('Deletion cancelled - name did not match', 'warning');
            return;
        }
        try {
            this.showLoader && this.showLoader('Deleting content block...');
            const url = `${castconductorCanvasAjax.rest_url}castconductor/v5/content-blocks/${this.currentContentBlockId}`;
            const response = await fetch(url, {
                method: 'DELETE',
                headers: { 'X-WP-Nonce': castconductorCanvasAjax.nonce }
            });
            const data = await response.json().catch(() => ({}));
            if (!response.ok) {
                throw new Error(data?.message || 'Delete failed');
            }
            this.showNotification && this.showNotification(`Content block "${blockName}" deleted successfully`, 'success');
            // Clear current state
            this.currentContentBlockId = null;
            this.currentConfig = null;
            this.unsavedChanges = false;
            this.updateSaveButton && this.updateSaveButton();
            // Refresh the content block dropdown
            try { this.loadLiveContentBlocks && this.loadLiveContentBlocks(); } catch(_) {}
            // Clear the stage
            const stage = document.getElementById('block-stage');
            if (stage) stage.innerHTML = '<div style="padding:40px;color:#64748b;text-align:center;">Select a content block to edit</div>';
        } catch(e) {
            console.error('[CC] deleteCurrentContentBlock error', e);
            this.showNotification && this.showNotification('Delete failed: '+(e.message||'error'), 'error');
        } finally {
            this.hideLoader && this.hideLoader();
        }
    }

    /**
     * Open modal to import content blocks from a JSON file.
     */
    openImportContentBlockModal() {
        // Create file input dynamically
        const fileInput = document.createElement('input');
        fileInput.type = 'file';
        fileInput.accept = '.json,application/json';
        fileInput.style.display = 'none';
        document.body.appendChild(fileInput);
        
        fileInput.addEventListener('change', async (e) => {
            const file = e.target.files?.[0];
            if (!file) {
                document.body.removeChild(fileInput);
                return;
            }
            try {
                this.showLoader && this.showLoader('Reading import file...');
                const text = await file.text();
                let importData;
                try {
                    importData = JSON.parse(text);
                } catch(parseErr) {
                    throw new Error('Invalid JSON file format');
                }
                // Support multiple export formats:
                // 1. Array of blocks: [{ name, type, visual_config, ... }, ...]
                // 2. Object with blocks array: { blocks: [...] }
                // 3. Single block wrapper: { content_block: {...} } (from full-block-export.js)
                // 4. Batch export: { content_blocks: [...] }
                // 5. Single block object: { name, type, visual_config, ... }
                let blocks;
                if (Array.isArray(importData)) {
                    blocks = importData;
                } else if (importData.content_block) {
                    // Full block export format: { export_format_version, content_block: {...} }
                    blocks = [importData.content_block];
                } else if (Array.isArray(importData.content_blocks)) {
                    // Batch export format: { content_blocks: [...] }
                    blocks = importData.content_blocks;
                } else if (Array.isArray(importData.blocks)) {
                    // Legacy blocks array format
                    blocks = importData.blocks;
                } else if (importData.name && importData.type) {
                    // Single block object
                    blocks = [importData];
                } else {
                    throw new Error('Unrecognized import format - expected content_block, content_blocks, blocks array, or single block object');
                }
                if (!blocks.length) {
                    throw new Error('No content blocks found in import file');
                }
                console.log('[CC] Import detected', blocks.length, 'block(s) from file format:', 
                    importData.content_block ? 'single (content_block)' :
                    importData.content_blocks ? 'batch (content_blocks)' :
                    importData.blocks ? 'legacy (blocks)' : 
                    Array.isArray(importData) ? 'array' : 'single object');
                const confirmed = confirm(`Import ${blocks.length} content block${blocks.length > 1 ? 's' : ''}?\n\nThis will create new content blocks based on the imported data.`);
                if (!confirmed) {
                    this.hideLoader && this.hideLoader();
                    document.body.removeChild(fileInput);
                    return;
                }
                this.showLoader && this.showLoader(`Importing ${blocks.length} block(s)...`);
                let successCount = 0;
                let errorCount = 0;
                for (const block of blocks) {
                    try {
                        // Prepare block for import (strip IDs, update timestamps)
                        const importBlock = { ...block };
                        delete importBlock.id;
                        delete importBlock.block_id;
                        importBlock.name = importBlock.name ? `${importBlock.name} (Imported)` : 'Imported Block';
                        // POST to create endpoint
                        const url = `${castconductorCanvasAjax.rest_url}castconductor/v5/content-blocks`;
                        const response = await fetch(url, {
                            method: 'POST',
                            headers: { 'Content-Type': 'application/json', 'X-WP-Nonce': castconductorCanvasAjax.nonce },
                            body: JSON.stringify(importBlock)
                        });
                        const result = await response.json().catch(() => ({}));
                        // API returns the created block object directly (with id field) on success
                        // NOT { success: true, data: ... } - it follows WordPress REST conventions
                        if (response.ok && (result.id || result.success)) {
                            successCount++;
                            console.log('[CC] Import block success:', result.id || result.name, importBlock.name);
                        } else {
                            errorCount++;
                            console.warn('[CC] Import block failed:', result?.message || result?.code || 'Unknown error', importBlock.name);
                        }
                    } catch(blockErr) {
                        errorCount++;
                        console.warn('[CC] Import block error:', blockErr);
                    }
                }
                // Refresh the content block list
                try { this.loadLiveContentBlocks && this.loadLiveContentBlocks(); } catch(_) {}
                if (errorCount === 0) {
                    this.showNotification && this.showNotification(`Successfully imported ${successCount} content block${successCount > 1 ? 's' : ''}`, 'success');
                } else {
                    this.showNotification && this.showNotification(`Imported ${successCount} block(s), ${errorCount} failed`, 'warning');
                }
            } catch(err) {
                console.error('[CC] Import error:', err);
                this.showNotification && this.showNotification('Import failed: ' + (err.message || 'Unknown error'), 'error');
            } finally {
                this.hideLoader && this.hideLoader();
                document.body.removeChild(fileInput);
            }
        });
        
        fileInput.click();
    }

    /**
     * Save configuration
     * Handles both regular Content Blocks and CPT posts (shoutout/sponsor/promo)
     */
    async saveConfiguration() {
        if (this._saving) return; // prevent double-fire
        if (!this.currentConfig || !this.currentContentBlockId) {
            this.showNotification && this.showNotification('No configuration to save', 'warning');
            return;
        }
        this._saving = true;
        try {
            this.showLoader && this.showLoader('Saving configuration...');
            
            // Build payload with token_layers side-channel so tokens persist across reloads
            let payload = {};
            try { payload = JSON.parse(JSON.stringify(this.currentConfig)); } catch(_) { payload = this.currentConfig; }
            // DEBUG: Log overlay config before save to diagnose opacity issues
            console.log('[CCVE][SAVE DEBUG] overlay before save:', JSON.stringify(payload?.overlay));
            console.log('[CCVE][SAVE DEBUG] layerGroups before save:', JSON.stringify(payload?.layerGroups));
            console.log('[CCVE][SAVE DEBUG] layers with groupId:', payload?.layers?.filter(l => l?.groupId)?.map(l => ({ id: l.id, groupId: l.groupId })));
            console.log('[CCVE][SAVE DEBUG] background.layers before save:', JSON.stringify(payload?.background?.layers));
            // Sanitize fitMode: 'fit' is not a valid server value, convert to 'fill'
            if (payload.layout && payload.layout.fitMode === 'fit') {
                payload.layout.fitMode = 'fill';
            }
            try { this._augmentSavePayload && this._augmentSavePayload(payload); } catch(_) {}
            // DEBUG: Log final payload before sending
            console.log('[CCVE][SAVE DEBUG] FINAL layerGroups:', JSON.stringify(payload?.layerGroups));
            console.log('[CCVE][SAVE DEBUG] FINAL layers with groupId:', payload?.layers?.filter(l => l?.groupId)?.map(l => ({ id: l.id, groupId: l.groupId })));
            
            let url;
            let saveMethod = 'POST';
            
            // Check if we're editing a CPT post (ID starts with 'cpt:')
            if (typeof this.currentContentBlockId === 'string' && this.currentContentBlockId.startsWith('cpt:')) {
                // Parse CPT info from ID: cpt:{type}:{post_id}
                const parts = this.currentContentBlockId.split(':');
                const apiType = parts[1]; // e.g., 'sponsor'
                const postId = parts[2]; // e.g., '123'
                
                url = `${castconductorCanvasAjax.rest_url}castconductor/v5/canvas-editor/cpt-content/${apiType}/${postId}`;
                payload = { visual_config: payload };
                console.log('[CCVE][saveConfiguration] CPT SAVE - URL:', url, 'apiType:', apiType, 'postId:', postId);
            } else {
                // Regular content block save
                url = `${castconductorCanvasAjax.rest_url}castconductor/v5/canvas-editor/config/${this.currentContentBlockId}`;
                console.log('[CCVE][saveConfiguration] REGULAR SAVE - URL:', url);
            }
            
            console.log('[CCVE][saveConfiguration] Fetching...', { url, method: saveMethod });
            const response = await fetch(url, {
                method: saveMethod,
                headers: { 'Content-Type':'application/json', 'X-WP-Nonce': castconductorCanvasAjax.nonce },
                body: JSON.stringify(payload)
            });
            const rawText = await response.text();
            console.log('[CCVE][saveConfiguration] Response status:', response.status, 'body:', rawText);
            let data = null; try { data = JSON.parse(rawText); } catch(e) { console.error('[CCVE][saveConfiguration] JSON parse error:', e); }
            console.log('[CCVE][saveConfiguration] Parsed data:', data, 'data.success:', data?.success);
            if (!response.ok || !data || !data.success) {
                // Extract validation errors array if present for richer feedback
                if (data && data.data && Array.isArray(data.data.validation_errors)) {
                    const lines = data.data.validation_errors.map(v=>`• ${v}`).join('\n');
                    this.showNotification && this.showNotification(`Save failed:\n${lines}`,'error');
                }
                throw new Error((data && data.message) || 'Save failed');
            }
            this.unsavedChanges = false;
            try { if (this.currentContentBlockId && this.configCache) { delete this.configCache[String(this.currentContentBlockId)]; } } catch(_){}
            this.updateSaveButton && this.updateSaveButton();
            // Capture baseline so preview status badge reflects saved state (clears STALE/DRIFT appropriately)
            try { recaptureBaseline(this); } catch(_) {}
            this.showNotification && this.showNotification('Configuration saved successfully','success');
        } catch(e) {
            console.warn('[CC] saveConfiguration error', e);
            if (!(e && e._notified)) {
                this.showNotification && this.showNotification('Save failed: '+(e.message||'error'),'error');
            }
        } finally {
            this.hideLoader && this.hideLoader();
            this._saving = false;
        }
    }

    /**
     * Create a new content block with proper UX flow
     * - Shows modal with options: Start empty or use template
     * - Prompts for name
     * - Generates unique type from name
     * - Creates and immediately loads the new block
     */
    async createNewContentBlock() {
        console.debug('[CCVE] createNewContentBlock: START');
        try {
            // Build list of existing blocks for template option
            let existingBlocks = [];
            try {
                const sel = document.getElementById('canvas-content-block-select');
                if (sel) {
                    existingBlocks = Array.from(sel.options)
                        .filter(o => o.value && o.value !== '')
                        .map(o => ({ id: o.value, name: o.textContent }));
                }
            } catch(_){}

            // Show creation modal
            const modalHtml = `
            <style>
              .ccve-create-wrap{display:flex;flex-direction:column;gap:12px;font:13px system-ui,Arial;min-width:340px;color:#e2e8f0}
              .ccve-create-wrap h4{margin:0 0 4px 0;font-size:14px;color:#f8fafc}
              .ccve-create-option{display:flex;align-items:center;gap:10px;padding:10px 12px;border:1px solid #334155;border-radius:6px;cursor:pointer;background:#0f172a;transition:all 0.15s}
              .ccve-create-option:hover{border-color:#3b82f6;background:#1e293b}
              .ccve-create-option.selected{border-color:#3b82f6;background:#1e3a5f}
              .ccve-create-option input[type="radio"]{margin:0}
              .ccve-create-option-text{flex:1}
              .ccve-create-option-title{font-weight:600;color:#f8fafc}
              .ccve-create-option-desc{font-size:11px;color:#94a3b8;margin-top:2px}
              .ccve-create-template-select{margin-top:8px;padding:6px 8px;border:1px solid #334155;border-radius:4px;background:#0f172a;color:#e2e8f0;width:100%;display:none}
              .ccve-create-option.selected .ccve-create-template-select{display:block}
              .ccve-create-name-wrap{display:flex;flex-direction:column;gap:4px;margin-top:8px}
              .ccve-create-name-wrap label{font-weight:600;font-size:12px}
              .ccve-create-name-wrap input{padding:8px 10px;border:1px solid #334155;border-radius:4px;background:#0f172a;color:#fff;font-size:13px}
              .ccve-create-actions{display:flex;justify-content:flex-end;gap:8px;margin-top:12px}
              .ccve-create-btn{padding:8px 16px;border-radius:4px;cursor:pointer;font-size:13px;font-weight:500}
              .ccve-create-cancel{background:#475569;color:#fff;border:1px solid #64748b}
              .ccve-create-cancel:hover{background:#64748b}
              .ccve-create-ok{background:#2563eb;color:#fff;border:1px solid #1d4ed8}
              .ccve-create-ok:hover{background:#3b82f6}
            </style>
            <div class="ccve-create-wrap" id="ccve-create-modal">
              <h4>Create New Content Block</h4>
              
              <div class="ccve-create-option selected" data-mode="empty">
                <input type="radio" name="create-mode" value="empty" checked>
                <div class="ccve-create-option-text">
                  <div class="ccve-create-option-title">Start from scratch</div>
                  <div class="ccve-create-option-desc">Create an empty content block</div>
                </div>
              </div>
              
              <div class="ccve-create-option" data-mode="template">
                <input type="radio" name="create-mode" value="template">
                <div class="ccve-create-option-text">
                  <div class="ccve-create-option-title">Use existing block as template</div>
                  <div class="ccve-create-option-desc">Copy layers and settings from another block</div>
                  <select class="ccve-create-template-select" id="ccve-template-select">
                    <option value="">-- Select a template --</option>
                    ${existingBlocks.map(b => `<option value="${b.id}">${b.name}</option>`).join('')}
                  </select>
                </div>
              </div>
              
              <div class="ccve-create-name-wrap">
                <label for="ccve-new-name">Content Block Name</label>
                <input type="text" id="ccve-new-name" placeholder="e.g., My Custom Block" autofocus>
              </div>
              
              <div class="ccve-create-actions">
                <button type="button" class="ccve-create-btn ccve-create-cancel">Cancel</button>
                <button type="button" class="ccve-create-btn ccve-create-ok">Create</button>
              </div>
            </div>`;

            this.showModal('Create New Content Block', modalHtml);
            const root = document.getElementById('ccve-create-modal');
            
            // Wire up option selection
            root.querySelectorAll('.ccve-create-option').forEach(opt => {
                opt.addEventListener('click', () => {
                    root.querySelectorAll('.ccve-create-option').forEach(o => o.classList.remove('selected'));
                    opt.classList.add('selected');
                    opt.querySelector('input[type="radio"]').checked = true;
                });
            });
            
            // Await user decision
            const result = await new Promise((resolve) => {
                const nameInput = root.querySelector('#ccve-new-name');
                const okBtn = root.querySelector('.ccve-create-ok');
                const cancelBtn = root.querySelector('.ccve-create-cancel');
                const templateSelect = root.querySelector('#ccve-template-select');
                
                const done = (val) => { try { this.closeModal(); } catch(_){} resolve(val); };
                
                cancelBtn?.addEventListener('click', () => done(null));
                okBtn?.addEventListener('click', () => {
                    const mode = root.querySelector('input[name="create-mode"]:checked')?.value || 'empty';
                    const name = nameInput?.value?.trim() || '';
                    const templateId = mode === 'template' ? templateSelect?.value : null;
                    done({ mode, name, templateId });
                });
                nameInput?.addEventListener('keydown', (e) => {
                    if (e.key === 'Enter') {
                        e.preventDefault();
                        okBtn?.click();
                    }
                });
                
                setTimeout(() => nameInput?.focus(), 50);
            });
            
            if (!result || !result.name) {
                this.showNotification && this.showNotification('Creation cancelled', 'info');
                return;
            }
            
            console.debug('[CCVE] createNewContentBlock: user chose', result);
            this.showLoader && this.showLoader('Creating content block...');
            
            // Generate unique type from name (slug-ify)
            const uniqueType = this._generateTypeFromName(result.name);
            console.debug('[CCVE] createNewContentBlock: generated type', uniqueType);
            
            // Prepare payload
            let visualConfig = {};
            let dataConfig = null;
            
            if (result.mode === 'template' && result.templateId) {
                // Fetch template block
                try {
                    const resp = await fetch(`${castconductorCanvasAjax.rest_url}castconductor/v5/content-blocks/${result.templateId}`, {
                        method: 'GET',
                        headers: { 'X-WP-Nonce': castconductorCanvasAjax.nonce }
                    });
                    if (resp.ok) {
                        const srcData = await resp.json();
                        visualConfig = this._sanitizeVisualConfigForCreate(srcData?.visual_config || {});
                        dataConfig = srcData?.data_config || null;
                        console.debug('[CCVE] createNewContentBlock: copied from template', result.templateId);
                    }
                } catch(e) {
                    console.warn('[CCVE] createNewContentBlock: failed to fetch template', e);
                }
            } else {
                // Start empty - minimal config
                visualConfig = {
                    width: 1280,
                    height: 200,
                    background: '#1a1a2e',
                    layers: []
                };
            }
            
            const payload = {
                name: result.name,
                type: uniqueType,
                visual_config: visualConfig,
                data_config: dataConfig,
                enabled: true
            };
            
            // Create the block
            const resp = await fetch(`${castconductorCanvasAjax.rest_url}castconductor/v5/content-blocks`, {
                method: 'POST',
                headers: { 'Content-Type': 'application/json', 'X-WP-Nonce': castconductorCanvasAjax.nonce },
                body: JSON.stringify(payload)
            });
            
            const text = await resp.text();
            let data = null; try { data = JSON.parse(text); } catch(_) {}
            
            if (!resp.ok || !data || !data.id) {
                console.error('[CCVE] createNewContentBlock: POST failed', { ok: resp.ok, data, status: resp.status });
                throw new Error((data && data.message) || `Create failed (${resp.status})`);
            }
            
            const newId = data.id;
            console.debug('[CCVE] createNewContentBlock: created block', newId);
            
            this.unsavedChanges = false;
            this.showNotification && this.showNotification(`Created "${result.name}" (#${newId})`, 'success');
            
            try { localStorage.setItem('ccveLastBlockId', String(newId)); } catch(_){}
            
            // Refresh dropdown and load the new block
            try { await this.loadLiveContentBlocks(); } catch(e) { console.warn('[CCVE] createNewContentBlock: loadLiveContentBlocks failed', e); }
            
            try {
                const sel = document.getElementById('canvas-content-block-select');
                if (sel) sel.value = String(newId);
            } catch(_) {}
            
            // Load the new block for immediate editing
            if (this.loadContentBlockWithLog) { 
                await this.loadContentBlockWithLog(newId); 
            }
            
            console.debug('[CCVE] createNewContentBlock: SUCCESS');
            
        } catch(e) {
            console.error('[CCVE] createNewContentBlock failed', e);
            this.showNotification && this.showNotification('Create failed: ' + (e.message||'error'), 'error');
        } finally {
            this.hideLoader && this.hideLoader();
        }
    }

    /**
     * Generate a unique type slug from a content block name
     * e.g., "My Custom Block" -> "my_custom_block"
     */
    _generateTypeFromName(name) {
        if (!name) return 'custom_' + Date.now();
        
        // Convert to lowercase, replace spaces/special chars with underscores
        let slug = name.toLowerCase()
            .replace(/[^a-z0-9]+/g, '_')  // Replace non-alphanumeric with underscore
            .replace(/^_+|_+$/g, '')       // Trim leading/trailing underscores
            .replace(/_+/g, '_');          // Collapse multiple underscores
        
        // Ensure it starts with a letter
        if (!/^[a-z]/.test(slug)) {
            slug = 'block_' + slug;
        }
        
        // Add timestamp suffix for uniqueness
        slug = slug + '_' + Date.now().toString(36);
        
        return slug;
    }

    /** Duplicate current block as new (POST /content-blocks) */
    async duplicateAsNew() {
        console.debug('[CCVE] duplicateAsNew: START', { currentBlockId: this.currentContentBlockId, hasConfig: !!this.currentConfig });
        try {
            if (!this.currentConfig || !this.currentContentBlockId) {
                this.showNotification && this.showNotification('Load a block first to duplicate', 'warning');
                return;
            }
            // Prompt for a name before creating the copy
            const baseName = (this.currentConfig.name || this.currentBlockName || 'Canvas Block');
            let desiredName = '';
            try {
                // Try using a simple inline modal for nicer UX if available
                if (this.showModal && this.closeModal) {
                    const modalHtml = `
                    <style>
                      .dupname-wrap{display:flex;flex-direction:column;gap:8px;font:12px system-ui,Arial}
                      .dupname-wrap input{padding:6px 8px;border:1px solid #334155;border-radius:4px;background:#0b1220;color:#fff}
                      .dupname-actions{display:flex;justify-content:flex-end;gap:8px}
                      .dupname-btn{padding:6px 12px;border-radius:4px;cursor:pointer;font-size:12px}
                      .dupname-cancel{background:#64748b;color:#fff;border:1px solid #475569}
                      .dupname-ok{background:#2563eb;color:#fff;border:1px solid #1d4ed8;font-weight:600}
                    </style>
                    <div class="dupname-wrap" id="ccve-dupname-modal">
                      <label for="dupname-input">Name this copy</label>
                      <input id="dupname-input" type="text" value="${baseName} (Copy)" placeholder="Enter a name" />
                      <div class="dupname-actions">
                        <button type="button" class="dupname-btn dupname-cancel">Cancel</button>
                        <button type="button" class="dupname-btn dupname-ok">Create</button>
                      </div>
                    </div>`;
                    this.showModal('Duplicate As New', modalHtml);
                    const root = document.getElementById('ccve-dupname-modal');
                    // eslint-disable-next-line no-inner-declarations
                    const awaitName = () => new Promise((resolve)=>{
                        const input = root?.querySelector('#dupname-input');
                        const ok = root?.querySelector('.dupname-ok');
                        const cancel = root?.querySelector('.dupname-cancel');
                        const done = (val) => { try { this.closeModal(); } catch(_){} resolve(val); };
                        cancel?.addEventListener('click', ()=>done(null));
                        ok?.addEventListener('click', ()=>done(String(input?.value||'').trim()));
                        input?.addEventListener('keydown', (e)=>{ if(e.key==='Enter'){ e.preventDefault(); done(String(input?.value||'').trim()); }});
                        setTimeout(()=>{ try { input?.focus(); input?.select(); } catch(_){}} , 0);
                    });
                    desiredName = await awaitName();
                } else {
                    // Fallback to prompt()
                    desiredName = (prompt('Name this copy', `${baseName} (Copy)`) || '').trim();
                }
            } catch(_){ desiredName = ''; }
            if (!desiredName) { this.showNotification && this.showNotification('Duplicate canceled', 'info'); return; }
            
            this.showLoader && this.showLoader('Creating new content block...');
            
            // Fetch source block to get data_config (required for proper data source duplication)
            let sourceDataConfig = null;
            try {
                const srcResp = await fetch(`${castconductorCanvasAjax.rest_url}castconductor/v5/content-blocks/${this.currentContentBlockId}`, {
                    method: 'GET',
                    headers: { 'X-WP-Nonce': castconductorCanvasAjax.nonce }
                });
                if (srcResp.ok) {
                    const srcData = await srcResp.json();
                    sourceDataConfig = srcData?.data_config || null;
                    console.debug('[CCVE] duplicateAsNew: source data_config', sourceDataConfig);
                }
            } catch(e) {
                console.warn('[CCVE] Could not fetch source block data_config', e);
            }
            
            // Sanitize visual config to strip editor-only/transient fields
            const vis = this._sanitizeVisualConfigForCreate(this.currentConfig);
            // Ensure side-channel token_layers is present for symmetry with loader
            try {
                const tl = Array.isArray(vis.layers) ? vis.layers.filter(l=>l && (l.kind==='token-text'||l.kind==='token-image')).map(l=>({ ...l })) : [];
                vis.visual_config = vis.visual_config || {};
                vis.visual_config.token_layers = tl;
                if (vis.legacy_overrides) {
                    vis.visual_config.legacy_overrides = { ...vis.legacy_overrides };
                }
            } catch(_) {}
            const payload = {
                name: desiredName,
                type: this.currentContentBlockType || 'track_info',
                visual_config: vis,
                data_config: sourceDataConfig,  // Copy source block's data config for proper duplication
                enabled: true
            };
            const resp = await fetch(`${castconductorCanvasAjax.rest_url}castconductor/v5/content-blocks`, {
                method: 'POST',
                headers: { 'Content-Type': 'application/json', 'X-WP-Nonce': castconductorCanvasAjax.nonce },
                body: JSON.stringify(payload)
            });
            console.debug('[CCVE] duplicateAsNew: POST response status', resp.status, resp.ok);
            const text = await resp.text();
            console.debug('[CCVE] duplicateAsNew: POST response text', text.substring(0, 500));
            let data = null; try { data = JSON.parse(text); } catch(_) {}
            if (!resp.ok || !data || !data.id) {
                console.error('[CCVE] duplicateAsNew: POST failed', { ok: resp.ok, data, status: resp.status });
                throw new Error((data && data.message) || `Create failed (${resp.status})`);
            }
            const newId = data.id;
            console.debug('[CCVE] duplicateAsNew: NEW block created', newId);
            this.unsavedChanges = false;
            this.showNotification && this.showNotification(`Created new block #${newId}`, 'success');
            try { localStorage.setItem('ccveLastBlockId', String(newId)); } catch(_){ }
            // Refresh lists from server to avoid phantom options, then load new block
            console.debug('[CCVE] duplicateAsNew: refreshing block list');
            try { await this.loadLiveContentBlocks(); } catch(e) { console.warn('[CCVE] duplicateAsNew: loadLiveContentBlocks failed', e); }
            // Update dropdown selector to show the new block
            try {
                const sel = document.getElementById('canvas-content-block-select');
                if (sel) {
                    sel.value = String(newId);
                    console.debug('[CCVE] duplicateAsNew: dropdown selector updated to', newId);
                }
            } catch(_) {}
            console.debug('[CCVE] duplicateAsNew: loading new block', newId);
            if (this.loadContentBlockWithLog) { await this.loadContentBlockWithLog(newId); }
            console.debug('[CCVE] duplicateAsNew: SUCCESS');
        } catch(e) {
            console.error('[CCVE] duplicateAsNew failed', e);
            this.showNotification && this.showNotification('Duplicate failed: ' + (e.message||'error'), 'error');
        } finally {
            this.hideLoader && this.hideLoader();
        }
    }

    /** Strip editor-only/transient keys from config prior to persisting via /content-blocks */
    _sanitizeVisualConfigForCreate(cfg){
        let base = {}; try { base = JSON.parse(JSON.stringify(cfg||{})); } catch(_) { base = { ...(cfg||{}) }; }
        // Remove transient flags/derived fields
        delete base.__legacyHydrated; delete base._hydratedLegacyLayers; delete base.__ccveEditorMode;
        // Do not carry preview-only artifacts
        delete base.preview; delete base._debug; delete base._lastPreview;
        // Prevent accidental large nested caches
        if (base.artwork && base.artwork.cache) delete base.artwork.cache;
        return base;
    }

    /**
     * Hide notifications (delegated to notifications module)
     */
    hideNotification() { return notifHide(); }

    /**
     * Show loader
     */
    showLoader(message = 'Loading...') {
        const loader = document.getElementById('canvas-loader');
        if (loader) {
            loader.style.display = 'block';
            const loaderText = loader.querySelector('.loader-text');
            if (loaderText) {
                loaderText.textContent = message;
            }
        }
    }

    /**
     * Hide loader
     */
    hideLoader() {
        const loader = document.getElementById('canvas-loader');
        if (loader) {
            loader.style.display = 'none';
        }
    }

    /**
     * Reset to default configuration
     */
    resetToDefaults() {
        if (confirm('Are you sure you want to reset to default configuration? This will lose any unsaved changes.')) {
            this.currentConfig = this.getDefaultConfig();
            this.populateControls(this.currentConfig);
            this.generatePreview();
            this.markUnsaved();
        }
    }

    /**
     * Toggle preview mode
     */
    togglePreviewMode() {
        this.isPreviewMode = !this.isPreviewMode;
        const previewBtn = document.getElementById('canvas-preview-btn');
        const controlsPanel = document.getElementById('canvas-controls-panel');
        
        if (this.isPreviewMode) {
            previewBtn.textContent = 'Exit Preview';
            controlsPanel?.classList.add('preview-mode');
        } else {
            previewBtn.textContent = 'Preview';
            controlsPanel?.classList.remove('preview-mode');
        }
    }

    // NOTE: createNewContentBlock() is now defined earlier in the class with full modal workflow
    // The old redirect-only version has been removed (Dec 2025)
    
    // Removed temporary mock createNewContentBlockFromTemplate().
    showNotificationFallback(message,type='info'){ try{ this.showNotification(message,type);}catch(_){ console.log('[CC][Notify]['+type+']',message);} }

    /**
     * Create a new content block via REST using the selected base template/type.
     * Returns created block object { id, name, type } on success.
     */
    async createNewContentBlockFromTemplate() {
        const btn = document.getElementById('canvas-new-baseblock-btn');
        const typeSel = document.getElementById('canvas-baseblock-select');
        let type = (typeSel && typeSel.value) ? typeSel.value : 'custom';
        // Validate against allowed types present in the select
        if (typeSel && ![...typeSel.options].some(o=>o.value===type)) {
            type = 'custom';
        }
        const name = (type.charAt(0).toUpperCase()+type.slice(1)).replace(/_/g,' ') + ' Block';
        try {
            btn && (btn.disabled = true, btn.textContent = 'Creating…');
            const resp = await fetch(`${castconductorCanvasAjax.rest_url}castconductor/v5/content-blocks`, {
                method:'POST',
                headers:{ 'Content-Type':'application/json', 'X-WP-Nonce': castconductorCanvasAjax.nonce },
                body: JSON.stringify({ name, type, enabled: true, visual_config: {} })
            });
            const text = await resp.text();
            let data = null; try { data = JSON.parse(text); } catch(e){
                throw new Error('Invalid JSON response');
            }
            if (!resp.ok) {
                throw new Error(data?.message || `HTTP ${resp.status}`);
            }
            const created = { id: data.id || data.data?.id || data?.data?.ID || data?.data?.id, name: data.name || name, type };
            if (!created.id) throw new Error('Create succeeded but no ID returned');
            this.showNotification && this.showNotification('Created '+created.name,'success');
            // Refresh lists & auto-load
            try { await this.loadLiveContentBlocks(); } catch(_) {}
            try { await this.loadContentBlock(created.id); } catch(_) {}
            return created;
        } catch(e) {
            console.warn('[CC] createNewContentBlockFromTemplate error', e);
            this.showNotification && this.showNotification('Create failed: '+(e.message||'error'),'error');
            return null;
        } finally {
            if (btn) { btn.disabled = false; btn.textContent = '+ New Block From Template'; }
        }
    }

    /**
     * Setup tab switching functionality
     */
    setupTabSwitching() {
        const tabButtons = document.querySelectorAll('.canvas-tab-button');
        const tabContents = document.querySelectorAll('.canvas-tab-content');
        // Per-surface UI state (grid, zone edit) persisted independently
        if (!this._surfaceStates) {
            this._surfaceStates = {
                block: { grid: false, hud: true, rulers:false, smartSnap: true },
                containers: { grid: false, zoneEdit: false, hud: true, rulers:false, smartSnap: true }
            };
        }
        this.activeSurface = 'block';

        // Defensive: ensure only one top-level tab content active at start
        let firstActiveApplied = false;
        tabContents.forEach(c => {
            if (c.classList.contains('active') && !firstActiveApplied) { firstActiveApplied = true; }
            else c.classList.remove('active');
        });
        if (!firstActiveApplied && tabContents[0]) tabContents[0].classList.add('active');
        // Ensure corresponding button has active class (may have been stripped by duplicate CSS reloads)
        const anyActiveBtn = document.querySelector('.canvas-tab-button.active');
        if (!anyActiveBtn && tabButtons[0]) tabButtons[0].classList.add('active');
        // Fallback: make sure block stage wrapper is visible when content-blocks active
        const blockStageWrapper = document.getElementById('block-stage-wrapper');
        if (blockStageWrapper && document.querySelector('.canvas-tab-button.active')?.dataset.tab === 'content-blocks') {
            blockStageWrapper.style.display = 'block';
        }
        // Defensive: collapse all category panels except first to avoid vertical bloat before CSS loads
        const catPanels = document.querySelectorAll('.category-panel');
        if (catPanels.length) {
            catPanels.forEach((p,i)=>{ if (i===0) p.classList.add('active'); else p.classList.remove('active'); });
        }

    // (Removed legacy stage-only scaling reassignment to rely on unified host method defined earlier)
        // Retry scaling a few times in case shared geometry script loads late
        this._scheduleScaleRetries = (attempt=0) => {
            if (attempt>2) return; requestAnimationFrame(()=>{ this.applyBlockStageScale(true); this._scheduleScaleRetries(attempt+1); });
        };
        // Expose manual override helper for debugging (use in console: ccveSetBlockStageScale(0.6))
        try {
            window.ccveSetBlockStageScale = (v) => { localStorage.setItem('ccveBlockStageScaleOverride', v); this.applyBlockStageScale(); };
        } catch(_) {}

        window.addEventListener('resize', () => {
            // Re-scale both surfaces if present
                this.applyBlockStageScale(); 
                try { this.logGeometryDiagnostics && this.logGeometryDiagnostics('rulers-toggle'); } catch(_) {}
            this.applyCanvasScale?.();
        });

        const updateSurfaceVisibility = () => {
            const activeTabBtn = document.querySelector('.canvas-tab-button.active');
            const activeTab = activeTabBtn ? activeTabBtn.getAttribute('data-tab') : 'content-blocks';
            try { console.debug('[CC] updateSurfaceVisibility activeTab=', activeTab); } catch(_) {}
            this.activeSurface = (activeTab === 'containers') ? 'containers' : 'block';
            const containerWrapper = document.getElementById('ccve-canvas-wrapper');
            const blockStageWrapper = document.getElementById('block-stage-wrapper');
            const toggles = document.getElementById('canvas-inline-toggles');
            const gridToggle = document.getElementById('toggle-grid')?.closest('label');
            const rulersToggle = document.getElementById('toggle-rulers')?.closest('label');
            const zoneToggle = document.getElementById('toggle-zone-edit')?.closest('label');
            const previewToggle = document.getElementById('toggle-content-preview')?.closest('label');
            // Show container canvas only on containers tab
            if (containerWrapper) { containerWrapper.style.display = (activeTab === 'containers') ? 'block' : 'none'; }
            if (blockStageWrapper) { blockStageWrapper.style.display = (activeTab === 'content-blocks') ? 'block' : 'none'; console.debug('[CC] block-stage-wrapper display ->', blockStageWrapper.style.display); this._assertBlockStageVisible?.(); }
            if (toggles) {
                // Grid & zone toggles only relevant to container canvas for now
                if (gridToggle) gridToggle.style.display = 'inline-block';
                if (rulersToggle) rulersToggle.style.display = 'inline-block';
                if (zoneToggle) zoneToggle.style.display = (activeTab === 'containers') ? 'inline-block' : 'none';
                if (previewToggle) previewToggle.style.display = (activeTab === 'content-blocks') ? 'inline-block' : 'none';
            }
            // If leaving containers tab, remove any lingering drag state overlays etc.
            if (activeTab !== 'containers') {
                document.querySelectorAll('.canvas-container.dragging').forEach(el => el.classList.remove('dragging'));
                // Persist zone edit state then disable if it was on
                const zoneCheckbox = document.getElementById('toggle-zone-edit');
                if (zoneCheckbox && zoneCheckbox.checked) {
                    this._surfaceStates.containers.zoneEdit = true; // remember
                    zoneCheckbox.checked = false; // visually off in hidden context
                    this.toggleZoneEditMode(false);
                }
            }
            // Restore grid state for active surface
            const gridCheckbox = document.getElementById('toggle-grid');
            const rulersCheckbox = document.getElementById('toggle-rulers');
            const smartSnapCheckbox = document.getElementById('toggle-smart-snap');
            if (gridCheckbox) {
                const shouldGrid = this._surfaceStates[this.activeSurface].grid;
                gridCheckbox.checked = shouldGrid;
                this._guidesEnabled = !!shouldGrid;
                this.toggleGuides(shouldGrid);
            }
            if (smartSnapCheckbox) {
                const shouldSmart = this._surfaceStates[this.activeSurface].smartSnap ?? true;
                smartSnapCheckbox.checked = shouldSmart;
                this._smartSnappingEnabled = !!shouldSmart;
            }
            if (rulersCheckbox) {
                const shouldRulers = this._surfaceStates[this.activeSurface].rulers;
                rulersCheckbox.checked = shouldRulers;
                this.toggleRulers?.(shouldRulers);
            }
            // Re-scale whichever surface is visible now
            if (this.activeSurface === 'block') this.applyBlockStageScale(); else this.applyCanvasScale?.();
        };

        tabButtons.forEach(button => {
            button.addEventListener('click', (e) => {
                const targetTab = e.currentTarget.dataset.tab;
                // Remove active class from all tabs and content
                tabButtons.forEach(btn => btn.classList.remove('active'));
                tabContents.forEach(content => content.classList.remove('active'));
                // Add active class to clicked tab and corresponding content
                e.currentTarget.classList.add('active');
                const targetContent = document.getElementById(targetTab + '-tab');
                if (targetContent) {
                    targetContent.classList.add('active');
                    if (targetTab === 'containers') {
                        this.loadContainerData && this.loadContainerData();
                        this.loadLiveContentBlocks && this.loadLiveContentBlocks();
                    }
                }
                // If switching to content-blocks, re-initialize category sub-tabs
                if (targetTab === 'content-blocks') {
                    this.setupCategoryTabSwitching();
                // Phase 2.7 early UI wiring
                this.initPhase27Enhancements();
                    // Re-collapse non-active category panels defensively
                    const panels = document.querySelectorAll('.category-panel');
                    panels.forEach((p,i)=>{ if (i===0) p.classList.add('active'); else p.classList.remove('active'); });
                    // Force show block stage wrapper
                    const bsw = document.getElementById('block-stage-wrapper'); if (bsw) bsw.style.display='block';
                }
                if (targetTab === 'containers') {
                    try { this.initScenesToolbar && this.initScenesToolbar(); } catch(_) {}
                    try { this.initContainersToolbarBindings && this.initContainersToolbarBindings(); } catch(_) {}
                }
                updateSurfaceVisibility();
            });
        });

        // If Containers tab is already active on page load, trigger data load
        const containersTab = document.getElementById('containers-tab');
        const containersBtn = Array.from(tabButtons).find(b => b.dataset.tab === 'containers');
    if (containersTab && containersTab.classList.contains('active')) {
            this.loadContainerData && this.loadContainerData();
            this.loadLiveContentBlocks && this.loadLiveContentBlocks();
            containersBtn && containersBtn.classList.add('active');
        try { this.initScenesToolbar && this.initScenesToolbar(); } catch(_) {}
        try { this.initContainersToolbarBindings && this.initContainersToolbarBindings(); } catch(_) {}
        }
    // Initial surface visibility state
    updateSurfaceVisibility();
    // Persistent scaffolding (rulers & HUD) creation after initial layout decisions
    this._ensurePersistentScaffolding && this._ensurePersistentScaffolding();
    // Attach geometry diagnostics API
    try { attachGeometryDiagnostics(this); } catch(_) {}
    // Initialize stage modules (viewport, rulers, fullscreen) - REFACTORED Nov 25, 2025
    try {
        this.initStageModules();
        try { this.logGeometryDiagnostics && this.logGeometryDiagnostics('stage-modules-init'); } catch(_) {}
    } catch(e) { console.warn('[CC] stage modules init failed', e); }
    // Initial scale for block stage
    setTimeout(() => { this.applyBlockStageScale(); this._scheduleScaleRetries();
        // Inject zoom UI if missing
        try {
            // Ensure ruler overlay styles do not affect layout flow (prevent vertical push)
            try {
                if (!document.getElementById('ccve-ruler-style-fix')) {
                    const st = document.createElement('style');
                    st.id='ccve-ruler-style-fix';
                    st.textContent = '.ccve-scale-host .ccve-ruler-horizontal, .ccve-scale-host .ccve-ruler-vertical { position:absolute !important; }';
                    document.head.appendChild(st);
                }
            } catch(_) {}
            // SAFETY: Route token/tool buttons into a dedicated external toolbar container so they never overlay the visual stage.
            // We create it once adjacent to #block-stage-wrapper parent (NOT inside the scaled/overlay region) to avoid layout collisions
            let togglesBar = document.getElementById('canvas-inline-toggles');
            let tokenToolsHost = document.getElementById('ccve-token-tools-bar');
            try {
                if (!tokenToolsHost) {
                    // Prefer to attach after the existing inline toggles row (if present) otherwise append to editor root wrapper
                    const stageWrapper = document.getElementById('block-stage-wrapper');
                    const parent = (togglesBar && togglesBar.parentElement) ? togglesBar.parentElement : (stageWrapper ? stageWrapper.parentElement : document.body);
                    tokenToolsHost = document.createElement('div');
                    tokenToolsHost.id = 'ccve-token-tools-bar';
                    tokenToolsHost.setAttribute('data-origin','token-layer-safe-toolbar');
                    // RESTORED: display:flex (was display:none which broke viewport sizing)
                    tokenToolsHost.style.cssText = 'margin:6px 0 8px 0;display:flex;flex-wrap:wrap;gap:4px;align-items:center;position:relative;z-index:0;';
                    // Insert after togglesBar if possible for logical grouping
                    if (togglesBar && togglesBar.nextSibling) parent.insertBefore(tokenToolsHost, togglesBar.nextSibling); else parent.appendChild(tokenToolsHost);
                    // Style guard: hide legacy buttons but keep toolbar visible for Fullscreen/authoring badge
                    if (!document.getElementById('ccve-token-toolbar-style-guard')) {
                        const st = document.createElement('style'); st.id='ccve-token-toolbar-style-guard';
                        // Hide individual legacy buttons, NOT the entire toolbar
                        st.textContent = '#ccve-token-tools-bar button{position:relative !important;}#block-stage-wrapper #ccve-token-tools-bar{display:none !important;}#ccve-simple-mode-btn,#ccve-live-data-btn,#ccve-insert-token-btn,#ccve-insert-artwork-btn,#ccve-export-token-btn,#ccve-token-status-btn,#ccve-token-batch-export-btn,#ccve-export-all-blocks-btn,#ccve-promote-preview-btn{display:none !important;}';
                        document.head.appendChild(st);
                    }
                }
            } catch(e){ console.warn('[CC] token tools host init failed', e); }
            // RESTORED: Point togglesBar to tokenToolsHost (now visible) for proper layout
            // Legacy buttons are hidden via CSS, Fullscreen/badge remain visible
            if (tokenToolsHost) togglesBar = tokenToolsHost;
            if (togglesBar && !document.getElementById('ccve-fullpixel-btn')) {
                // Simple Mode toggle (hidden via CSS)
                if(!document.getElementById('ccve-simple-mode-btn')){
                    const simpleBtn = document.createElement('button');
                    simpleBtn.type='button'; simpleBtn.id='ccve-simple-mode-btn';
                    simpleBtn.style.cssText='margin-left:4px;font-size:11px;padding:2px 8px;line-height:1.2;border:1px solid #334155;background:#059669;color:#fff;border-radius:4px;cursor:pointer;';
                    const applyLabel = () => { const isLayers = this._editorMode === 'layers'; simpleBtn.textContent = isLayers ? 'Preview Mode' : 'Layers Mode'; simpleBtn.title = isLayers ? 'Show server preview again' : 'Convert & edit layers only'; };
                    simpleBtn.addEventListener('click', ()=>{
                        const wrap = document.getElementById('block-stage-wrapper');
                        const toLayers = this._editorMode !== 'layers';
                        if (toLayers) {
                            // One-time migration: convert hydrated legacy into canonical token layers if none exist
                            try {
                                const existing = (this.currentConfig?.layers||[]).some(l=>l && (l.kind==='token-text'||l.kind==='token-image'));
                                if (!existing) this.promotePreviewToTokenLayers();
                            } catch(e){ console.warn('migration promote failed', e); }
                            this._editorMode = 'layers';
                            try { localStorage.setItem('ccveEditorMode','layers'); } catch(_){ }
                            if (wrap) wrap.classList.add('ccve-simple-mode');
                            try { this.showNotification && this.showNotification('Layers Mode: preview hidden; edit layers.','success'); } catch(_){}
                        } else {
                            this._editorMode = 'preview';
                            try { localStorage.setItem('ccveEditorMode','preview'); } catch(_){ }
                            if (wrap) wrap.classList.remove('ccve-simple-mode');
                            // Ensure any background suppression is lifted when returning to Preview mode
                            try { this._suppressPreviewForBackground = false; delete this._lastPreviewHash; } catch(_){ }
                            try { this.triggerPreview && this.triggerPreview(true); } catch(_){ }
                            try { renderHydratedLegacyLayers(this); } catch(_){}
                            try { this.showNotification && this.showNotification('Preview Mode: server preview visible.','info'); } catch(_){}
                        }
                        applyLabel();
                    });
                    togglesBar.appendChild(simpleBtn);
                    // Restore persisted mode
                    try { const mode = localStorage.getItem('ccveEditorMode'); const wrap = document.getElementById('block-stage-wrapper'); if (mode==='layers' && wrap && !wrap.classList.contains('ccve-simple-mode')) wrap.classList.add('ccve-simple-mode'); this._editorMode = (mode==='layers')?'layers':'preview'; } catch(_){ }
                    applyLabel();
                }
                // Live Data button (always available) – triggers token refresh
                if (!document.getElementById('ccve-live-data-btn')) {
                    const liveBtn = document.createElement('button');
                    liveBtn.type='button';
                    liveBtn.id='ccve-live-data-btn';
                    liveBtn.textContent='Live Data';
                    liveBtn.style.cssText='margin-left:4px;font-size:11px;padding:2px 8px;line-height:1.2;border:1px solid #334155;background:#0f172a;color:#fff;border-radius:4px;cursor:pointer;';
                    liveBtn.title='Refresh live token values (track/weather/location/etc)';
                    liveBtn.addEventListener('click', ()=>{ try { this.refreshLiveTokens(); } catch(e){ console.warn('live token refresh failed', e); } });
                    togglesBar.appendChild(liveBtn);
                    // Phase 4: Insert Tokens button (opens modal to build a template)
                    const insertBtn = document.createElement('button');
                    insertBtn.type='button';
                    insertBtn.id='ccve-insert-token-btn';
                    insertBtn.textContent='Add Text Layer';
                    insertBtn.style.cssText='margin-left:4px;font-size:11px;padding:2px 8px;line-height:1.2;border:1px solid #334155;background:#1e293b;color:#fff;border-radius:4px;cursor:pointer;';
                    insertBtn.title='Add a token-driven text layer';
                    insertBtn.addEventListener('click', ()=>{ try { tokenOpenInsertModal(this); } catch(e){ console.warn('open token insert modal failed', e); } });
                    togglesBar.appendChild(insertBtn);
                    // Phase 5b: Artwork token image layer button
                    const imgBtn = document.createElement('button');
                    imgBtn.type='button';
                    imgBtn.id='ccve-insert-artwork-btn';
                    imgBtn.textContent='Add Artwork Layer';
                    imgBtn.style.cssText='margin-left:4px;font-size:11px;padding:2px 8px;line-height:1.2;border:1px solid #334155;background:#1e293b;color:#fff;border-radius:4px;cursor:pointer;';
                    imgBtn.title='Add artwork token image layer (track/sponsor/promo)';
                    imgBtn.addEventListener('click', ()=>{ try { tokenOpenImageModal(this); } catch(e){ console.warn('open token image modal failed', e); } });
                    togglesBar.appendChild(imgBtn);
                    // Phase 6 initial: Export tokens
                    const exportBtn = document.createElement('button');
                    exportBtn.type='button';
                    exportBtn.id='ccve-export-token-btn';
                    exportBtn.textContent='Export Content Block';
                    exportBtn.style.cssText='margin-left:4px;font-size:11px;padding:2px 8px;line-height:1.2;border:1px solid #334155;background:#334155;color:#fff;border-radius:4px;cursor:pointer;';
                    exportBtn.title='Export full content block JSON (alpha)';
                    exportBtn.addEventListener('click', ()=>{ try { openFullContentBlockExportModal(this); } catch(e){ console.warn('open full block export modal failed', e); } });
                    togglesBar.appendChild(exportBtn);
                    // Enhancement: Token Status
                    const statusBtn=document.createElement('button');
                    statusBtn.type='button'; statusBtn.id='ccve-token-status-btn'; statusBtn.textContent='Layer Status';
                    statusBtn.style.cssText='margin-left:4px;font-size:11px;padding:2px 8px;line-height:1.2;border:1px solid #334155;background:#475569;color:#fff;border-radius:4px;cursor:pointer;';
                    statusBtn.title='List token-driven layers with missing output';
                    statusBtn.addEventListener('click', ()=>{ try { openTokenStatusModal(this); } catch(e){ console.warn('open token status modal failed', e); } });
                    togglesBar.appendChild(statusBtn);
                    // Enhancement: Batch Export
                    const batchBtn=document.createElement('button');
                    batchBtn.type='button'; batchBtn.id='ccve-token-batch-export-btn'; batchBtn.textContent='Batch Export Blocks';
                    batchBtn.style.cssText='margin-left:4px;font-size:11px;padding:2px 8px;line-height:1.2;border:1px solid #334155;background:#475569;color:#fff;border-radius:4px;cursor:pointer;';
                    batchBtn.title='Export selected content blocks as JSON';
                    batchBtn.addEventListener('click', ()=>{ try { openFullBatchExportModal(this); } catch(e){ console.warn('open full batch export modal failed', e); } });
                    togglesBar.appendChild(batchBtn);
                    // Export All Blocks
                    const allBtn=document.createElement('button');
                    allBtn.type='button'; allBtn.id='ccve-export-all-blocks-btn'; allBtn.textContent='Export All Blocks';
                    allBtn.style.cssText='margin-left:4px;font-size:11px;padding:2px 8px;line-height:1.2;border:1px solid #334155;background:#475569;color:#fff;border-radius:4px;cursor:pointer;';
                    allBtn.title='Export ALL content blocks (full JSON bundle)';
                    allBtn.addEventListener('click', ()=>{ try { openFullAllBlocksExportModal(this); } catch(e){ console.warn('open full all-block export modal failed', e); } });
                    togglesBar.appendChild(allBtn);
                    // Promote Preview → Token Layers (quick split for track_info)
                    try {
                        if (!document.getElementById('ccve-promote-preview-btn')) {
                            const promoteBtn = document.createElement('button');
                            promoteBtn.type='button'; promoteBtn.id='ccve-promote-preview-btn';
                            promoteBtn.textContent='Promote Preview → Layers';
                            promoteBtn.style.cssText='margin-left:4px;font-size:11px;padding:2px 8px;line-height:1.2;border:1px solid #334155;background:#0f172a;color:#fff;border-radius:4px;cursor:pointer;';
                            promoteBtn.title='Split current preview into editable token layers using current JSON geometry (artwork + text).';
                            promoteBtn.addEventListener('click', ()=>{ try { this.promotePreviewToTokenLayers(); } catch(e){ console.warn('promote preview failed', e); } });
                            togglesBar.appendChild(promoteBtn);
                        }
                    } catch(_) {}
                }

            // Background affordance: double-click or right-click on background opens Background modal
            try {
                    const openBackgroundModal = () => {
                        // Remove any existing modal
                        const existing = document.getElementById('ccve-background-modal');
                        if(existing) existing.remove();
                        
                        const ed = window.castconductorCanvasEditor;
                        if(!ed || !ed.currentConfig) return;
                        
                        // Get current background settings from config
                        const bg = ed.currentConfig.background || {};
                        const ov = ed.currentConfig.overlay || {};
                        const bgType = bg.type || 'color';
                        const bgColor = bg.color || '#1a1a2e';
                        const borderRadius = bg.border_radius || 0;
                        const borderWidth = bg.border_width || 0;
                        const borderColor = bg.border_color || '#000000';
                        const borderStyle = bg.border_style || 'solid';
                        const gradColor1 = bg.gradient_color1 || '#1a1a2e';
                        const gradColor2 = bg.gradient_color2 || '#16213e';
                        const gradDirection = bg.gradient_direction || 180;
                        // Overlay: prefer background.overlay_* but fall back to overlay object
                        const overlayColor = bg.overlay_color || ov.color || '#000000';
                        const overlayOpacity = bg.overlay_opacity !== undefined ? bg.overlay_opacity : (ov.opacity !== undefined ? ov.opacity : 0);
                        const bgImageUrl = bg.image_url || '';
                        
                        const modal = document.createElement('div');
                        modal.id = 'ccve-background-modal';
                        modal.className = 'ccve-token-precision-dialog';
                        modal.innerHTML = `
                            <div class="ccve-tp-backdrop"></div>
                            <div class="ccve-tp-modal" style="width: 500px; max-width: 95vw; max-height: 80vh; overflow-y: auto;">
                                <h3 style="position: sticky; top: 0; background: #1e293b; margin: -16px -16px 12px; padding: 12px 16px; border-bottom: 1px solid #334155; z-index: 1;">🎨 Block Background Settings</h3>
                                
                                <div style="font-weight: 600; margin-bottom: 8px;">Background Type</div>
                                <div style="display: grid; gap: 8px; margin-bottom: 16px;">
                                    <label>Type <select id="ccve-bg-modal-type">
                                        <option value="color" ${bgType==='color'?'selected':''}>Solid Color</option>
                                        <option value="gradient" ${bgType==='gradient'?'selected':''}>Gradient</option>
                                        <option value="image" ${bgType==='image'?'selected':''}>Image</option>
                                    </select></label>
                                </div>
                                
                                <div id="ccve-bg-color-section" style="display: ${bgType==='color'?'block':'none'};">
                                    <label>Background Color <input type="color" id="ccve-bg-modal-color" value="${bgColor}" /></label>
                                </div>
                                
                                <div id="ccve-bg-gradient-section" style="display: ${bgType==='gradient'?'block':'none'};">
                                    <div style="display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 8px;">
                                        <label>Color 1 <input type="color" id="ccve-bg-modal-grad1" value="${gradColor1}" /></label>
                                        <label>Color 2 <input type="color" id="ccve-bg-modal-grad2" value="${gradColor2}" /></label>
                                        <label>Direction (°) <input type="number" id="ccve-bg-modal-grad-dir" value="${gradDirection}" min="0" max="360" /></label>
                                    </div>
                                </div>
                                
                                <div id="ccve-bg-image-section" style="display: ${bgType==='image'?'block':'none'};">
                                    <label>Image URL <input type="text" id="ccve-bg-modal-image-url" value="${bgImageUrl}" style="width: 100%;" /></label>
                                    <button type="button" id="ccve-bg-modal-select-image" style="margin-top: 4px; padding: 6px 12px; background: #f3f4f6; border: 1px solid #d1d5db; border-radius: 4px; cursor: pointer;">📁 Media Library</button>
                                </div>
                                
                                <hr style="margin: 16px 0; border: none; border-top: 1px solid #334155;" />
                                <div style="font-weight: 600; margin-bottom: 8px;">Overlay</div>
                                <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 8px;">
                                    <label>Overlay Color <input type="color" id="ccve-bg-modal-overlay-color" value="${overlayColor}" /></label>
                                    <label>Overlay Opacity <input type="number" id="ccve-bg-modal-overlay-opacity" value="${overlayOpacity}" min="0" max="1" step="0.05" /></label>
                                </div>
                                
                                <hr style="margin: 16px 0; border: none; border-top: 1px solid #334155;" />
                                <div style="font-weight: 600; margin-bottom: 8px;">Border</div>
                                <div style="display: grid; grid-template-columns: 1fr 1fr 1fr 1fr; gap: 8px;">
                                    <label>Width <input type="number" id="ccve-bg-modal-border-width" value="${borderWidth}" min="0" max="20" /></label>
                                    <label>Color <input type="color" id="ccve-bg-modal-border-color" value="${borderColor}" /></label>
                                    <label>Style <select id="ccve-bg-modal-border-style">
                                        <option value="solid" ${borderStyle==='solid'?'selected':''}>Solid</option>
                                        <option value="dashed" ${borderStyle==='dashed'?'selected':''}>Dashed</option>
                                        <option value="dotted" ${borderStyle==='dotted'?'selected':''}>Dotted</option>
                                    </select></label>
                                    <label>Radius <input type="number" id="ccve-bg-modal-border-radius" value="${borderRadius}" min="0" max="100" /></label>
                                </div>
                                
                                <div class="ccve-tp-actions" style="position: sticky; bottom: 0; background: #1e293b; margin: 16px -16px -16px; padding: 12px 16px; border-top: 1px solid #334155; display: flex; justify-content: flex-end; gap: 8px;">
                                    <button type="button" id="ccve-bg-modal-cancel" class="ccve-tp-cancel">Cancel</button>
                                    <button type="button" id="ccve-bg-modal-apply" class="ccve-tp-apply">Apply</button>
                                </div>
                            </div>
                        `;
                        document.body.appendChild(modal);
                        
                        // Type change handler to show/hide sections
                        const typeSelect = document.getElementById('ccve-bg-modal-type');
                        typeSelect?.addEventListener('change', () => {
                            const t = typeSelect.value;
                            document.getElementById('ccve-bg-color-section').style.display = t==='color'?'block':'none';
                            document.getElementById('ccve-bg-gradient-section').style.display = t==='gradient'?'block':'none';
                            document.getElementById('ccve-bg-image-section').style.display = t==='image'?'block':'none';
                        });
                        
                        // Media library button
                        const selectImageBtn = document.getElementById('ccve-bg-modal-select-image');
                        selectImageBtn?.addEventListener('click', () => {
                            if(typeof wp !== 'undefined' && wp.media) {
                                const frame = wp.media({ title: 'Select Background Image', multiple: false });
                                frame.on('select', () => {
                                    const att = frame.state().get('selection').first().toJSON();
                                    const urlInput = document.getElementById('ccve-bg-modal-image-url');
                                    if(urlInput) urlInput.value = att.url || '';
                                });
                                frame.open();
                            }
                        });
                        
                        // Cancel
                        document.getElementById('ccve-bg-modal-cancel')?.addEventListener('click', () => modal.remove());
                        modal.querySelector('.ccve-tp-backdrop')?.addEventListener('click', () => modal.remove());
                        
                        // Apply
                        document.getElementById('ccve-bg-modal-apply')?.addEventListener('click', () => {
                            if(!ed.currentConfig.background) ed.currentConfig.background = {};
                            const newBg = ed.currentConfig.background;
                            
                            newBg.type = document.getElementById('ccve-bg-modal-type')?.value || 'color';
                            newBg.color = document.getElementById('ccve-bg-modal-color')?.value || '#1a1a2e';
                            newBg.gradient_color1 = document.getElementById('ccve-bg-modal-grad1')?.value || '#1a1a2e';
                            newBg.gradient_color2 = document.getElementById('ccve-bg-modal-grad2')?.value || '#16213e';
                            newBg.gradient_direction = parseInt(document.getElementById('ccve-bg-modal-grad-dir')?.value || 180);
                            newBg.image_url = document.getElementById('ccve-bg-modal-image-url')?.value || '';
                            newBg.overlay_color = document.getElementById('ccve-bg-modal-overlay-color')?.value || '#000000';
                            newBg.overlay_opacity = parseFloat(document.getElementById('ccve-bg-modal-overlay-opacity')?.value || 0);
                            newBg.border_width = parseInt(document.getElementById('ccve-bg-modal-border-width')?.value || 0);
                            newBg.border_color = document.getElementById('ccve-bg-modal-border-color')?.value || '#000000';
                            newBg.border_style = document.getElementById('ccve-bg-modal-border-style')?.value || 'solid';
                            newBg.border_radius = parseInt(document.getElementById('ccve-bg-modal-border-radius')?.value || 0);
                            
                            // SYNC overlay settings to the primary overlay object
                            // This ensures save/load and Scenes rendering sees correct values
                            if(!ed.currentConfig.overlay) ed.currentConfig.overlay = {};
                            ed.currentConfig.overlay.color = newBg.overlay_color;
                            ed.currentConfig.overlay.opacity = newBg.overlay_opacity;
                            ed.currentConfig.overlay.enabled = newBg.overlay_opacity > 0;
                            
                            // SYNC background.layers array to include overlay layer if opacity > 0
                            if(!Array.isArray(newBg.layers)) newBg.layers = [];
                            const existingOverlayIdx = newBg.layers.findIndex(l => l && l.kind === 'overlay');
                            if(newBg.overlay_opacity > 0) {
                                if(existingOverlayIdx === -1) {
                                    // Add overlay layer at beginning (topmost)
                                    newBg.layers.unshift({ kind: 'overlay', enabled: true });
                                } else {
                                    newBg.layers[existingOverlayIdx].enabled = true;
                                }
                            } else {
                                if(existingOverlayIdx !== -1) {
                                    newBg.layers[existingOverlayIdx].enabled = false;
                                }
                            }
                            
                            // Apply gradient colors array for renderer compatibility
                            if(newBg.type === 'gradient') {
                                newBg.gradient_colors = [newBg.gradient_color1, newBg.gradient_color2];
                            }
                            
                            // Mark unsaved and update canvas
                            ed.unsavedChanges = true;
                            try { ed.updateSaveButton && ed.updateSaveButton(); } catch(_) {}
                            
                            // Apply background styling to the stage
                            try {
                                const bgLayerEl = document.getElementById('canvas-background-layer');
                                const overlayEl = document.getElementById('canvas-overlay-layer');
                                const stageElement = document.getElementById('block-stage');
                                
                                if(bgLayerEl) {
                                    if(newBg.type === 'color') {
                                        bgLayerEl.style.background = newBg.color;
                                    } else if(newBg.type === 'gradient') {
                                        bgLayerEl.style.background = `linear-gradient(${newBg.gradient_direction}deg, ${newBg.gradient_color1}, ${newBg.gradient_color2})`;
                                    } else if(newBg.type === 'image' && newBg.image_url) {
                                        bgLayerEl.style.background = `url(${newBg.image_url}) center/cover no-repeat`;
                                    }
                                    bgLayerEl.style.borderRadius = `${newBg.border_radius}px`;
                                }
                                
                                if(overlayEl && newBg.overlay_opacity > 0) {
                                    overlayEl.style.background = newBg.overlay_color;
                                    overlayEl.style.opacity = newBg.overlay_opacity;
                                } else if(overlayEl) {
                                    overlayEl.style.background = 'transparent';
                                    overlayEl.style.opacity = 0;
                                }
                                
                                if(stageElement) {
                                    stageElement.style.borderWidth = `${newBg.border_width}px`;
                                    stageElement.style.borderColor = newBg.border_color;
                                    stageElement.style.borderStyle = newBg.border_style;
                                    stageElement.style.borderRadius = `${newBg.border_radius}px`;
                                }
                            } catch(_) {}
                            
                            modal.remove();
                        });
                    };
                    const bgLayer = document.getElementById('canvas-background-layer');
                    const stageEl = document.getElementById('block-stage');
                    const targetEls = [bgLayer, stageEl].filter(Boolean);
                    targetEls.forEach(el => {
                        if (!el.__ccveBgOpenBound) {
                            el.addEventListener('dblclick', (e)=>{
                                // Only trigger if click is on empty stage/background, not on token/legacy nodes or the block-editor-wrap (container)
                                const hitToken = e.target.closest?.('[data-layer-id]');
                                const hitLegacy = e.target.closest?.('.ccve-legacy-layer');
                                const hitWrap = e.target.closest?.('.cc-block-editor-wrap');
                                if (hitToken || hitLegacy || hitWrap) return;
                                openBackgroundModal();
                            });
                            el.addEventListener('contextmenu', (e)=>{
                                const hitToken = e.target.closest?.('[data-layer-id]');
                                const hitLegacy = e.target.closest?.('.ccve-legacy-layer');
                                const hitWrap = e.target.closest?.('.cc-block-editor-wrap');
                                if (hitToken || hitLegacy || hitWrap) return; // let layer/wrap context menus win
                                e.preventDefault();
                                openBackgroundModal();
                            });
                            el.__ccveBgOpenBound = true;
                        }
                    });
                } catch(e){ console.warn('[CC] background affordance binding failed', e); }
                // If advanced debug globally disabled, hard reset flags and skip injecting advanced toggle / zoom controls.
                if (!CCVE_ENABLE_ADVANCED_DEBUG) {
                    try { localStorage.setItem('ccveDebugUI','0'); localStorage.setItem('ccveBlockStageZoomRel','1'); } catch(_) {}
                }
                const debugUI = CCVE_ENABLE_ADVANCED_DEBUG && ((document.body?.dataset?.ccveDebug === '1') || (()=>{ try { return localStorage.getItem('ccveDebugUI')==='1'; } catch(_) { return false; } })());
                const steps = this.getZoomSteps();
                const wrap = document.createElement('div');
                wrap.style.display='inline-flex';
                wrap.style.alignItems='center';
                wrap.style.gap='6px';
                wrap.style.marginLeft='6px';
                wrap.style.flexShrink='0';
                wrap.style.maxWidth='100%';
                wrap.style.overflow='visible';
                if (CCVE_ENABLE_ADVANCED_DEBUG) {
                    // Advanced toggle (only when globally enabled)
                    const advToggle = document.createElement('button');
                    advToggle.type='button'; advToggle.id='ccve-advanced-toggle';
                    advToggle.textContent = debugUI ? 'Hide Advanced' : 'Show Advanced';
                    advToggle.style.cssText='margin-left:4px;font-size:11px;padding:2px 6px;line-height:1.2;border:1px solid #334155;background:#1e293b;color:#fff;border-radius:4px;cursor:pointer;';
                    advToggle.title='Toggle advanced zoom & diagnostics controls';
                    advToggle.addEventListener('click', ()=>{
                        const active = localStorage.getItem('ccveDebugUI')==='1';
                        localStorage.setItem('ccveDebugUI', active ? '0':'1');
                        if (active) {
                            ['ccve-zoom-select','ccve-reset-zoom-btn','ccve-drift-btn','ccve-pin-ui-btn'].forEach(id=>{ const n=document.getElementById(id); if(n) n.remove(); });
                            advToggle.textContent='Show Advanced';
                            try { localStorage.setItem('ccveBlockStageZoomRel','1'); } catch(_) {}
                            this.applyBlockStageScale();
                        } else {
                            location.reload();
                        }
                    });
                    wrap.appendChild(advToggle);
                }
                // Debug zoom select (label/help removed in polish pass)
                if (debugUI) {
                    const sel = document.createElement('select'); sel.id='ccve-zoom-select'; sel.style.fontSize='11px'; sel.style.padding='2px 4px';
                    steps.forEach(p=>{ const o=document.createElement('option'); o.value=String(p); o.textContent=p+(p===100?'% (Fit)':'%'); sel.appendChild(o); });
                    let rel = 1; try { const rs = parseFloat(localStorage.getItem('ccveBlockStageZoomRel')); if(!isNaN(rs)&&rs>0) rel=Math.min(rs,1); } catch(_){ }
                    const relPct = rel*100; let closest = steps[steps.length-1];
                    steps.forEach(s=>{ if(Math.abs(s-relPct) < Math.abs(closest-relPct)) closest=s; });
                    sel.value = String(Math.round(closest));
                    sel.addEventListener('change', () => { const relVal = parseFloat(sel.value)/100; localStorage.setItem('ccveBlockStageZoomRel', String(relVal)); this.applyBlockStageScale(); });
                    wrap.appendChild(sel);
                } else {
                    try { localStorage.setItem('ccveBlockStageZoomRel','1'); } catch(_) {}
                }
                // Fullscreen Mode button (always visible) - add to canvas-inline-toggles with other controls
                const fpBtn = document.createElement('button');
                fpBtn.type='button'; fpBtn.id='ccve-fullpixel-btn'; fpBtn.textContent='⛶ Fullscreen';
                fpBtn.style.cssText='font-size:11px;padding:3px 8px;line-height:1.2;border:1px solid #334155;background:#1e293b;color:#fff;border-radius:4px;cursor:pointer;';
                fpBtn.title='Enter 1:1 pixel authoring view (1280×720). Press Esc to exit.';
                fpBtn.addEventListener('click', () => this.enterFullPixelMode());
                wrap.appendChild(fpBtn);
                // Authoring badge (static informational pill)
                const authorBadge = document.createElement('span');
                authorBadge.className='ccve-authoring-badge';
                authorBadge.textContent='1280×720 authoring';
                authorBadge.style.cssText='font-size:10px;padding:2px 6px;border:1px solid #334155;background:#0f172a;color:#94a3b8;border-radius:3px;line-height:1.2;';
                wrap.appendChild(authorBadge);
                // Export buttons (visible in toolbar) - restored from hidden section
                const exportBtnStyle = 'margin-left:4px;font-size:11px;padding:2px 8px;line-height:1.2;border:1px solid #334155;background:#334155;color:#fff;border-radius:4px;cursor:pointer;';
                const exportBtn2 = document.createElement('button');
                exportBtn2.type='button'; exportBtn2.id='ccve-visible-export-btn'; exportBtn2.textContent='Export Block';
                exportBtn2.style.cssText=exportBtnStyle;
                exportBtn2.title='Export full content block JSON';
                exportBtn2.addEventListener('click', ()=>{ try { openFullContentBlockExportModal(this); } catch(e){ console.warn('export block modal failed', e); } });
                wrap.appendChild(exportBtn2);
                const batchBtn2 = document.createElement('button');
                batchBtn2.type='button'; batchBtn2.id='ccve-visible-batch-btn'; batchBtn2.textContent='Batch Export';
                batchBtn2.style.cssText=exportBtnStyle;
                batchBtn2.title='Export selected content blocks as JSON';
                batchBtn2.addEventListener('click', ()=>{ try { openFullBatchExportModal(this); } catch(e){ console.warn('batch export modal failed', e); } });
                wrap.appendChild(batchBtn2);
                const allBtn2 = document.createElement('button');
                allBtn2.type='button'; allBtn2.id='ccve-visible-all-btn'; allBtn2.textContent='Export All';
                allBtn2.style.cssText=exportBtnStyle;
                allBtn2.title='Export ALL content blocks (full JSON bundle)';
                allBtn2.addEventListener('click', ()=>{ try { openFullAllBlocksExportModal(this); } catch(e){ console.warn('export all modal failed', e); } });
                wrap.appendChild(allBtn2);
                // Import button - opens file dialog to import JSON content blocks
                const importBtn = document.createElement('button');
                importBtn.type='button'; importBtn.id='ccve-visible-import-btn'; importBtn.textContent='Import';
                importBtn.style.cssText='margin-left:4px;font-size:11px;padding:2px 8px;line-height:1.2;border:1px solid #334155;background:#1e3a5f;color:#fff;border-radius:4px;cursor:pointer;';
                importBtn.title='Import content blocks from JSON file';
                importBtn.addEventListener('click', ()=>{ try { this.openImportContentBlockModal(); } catch(e){ console.warn('import modal failed', e); } });
                wrap.appendChild(importBtn);
                // Delete current content block button
                const deleteBtn = document.createElement('button');
                deleteBtn.type='button'; deleteBtn.id='ccve-visible-delete-btn'; deleteBtn.textContent='Delete Block';
                deleteBtn.style.cssText='margin-left:4px;font-size:11px;padding:2px 8px;line-height:1.2;border:1px solid #7f1d1d;background:#991b1b;color:#fff;border-radius:4px;cursor:pointer;';
                deleteBtn.title='Delete the currently selected content block';
                deleteBtn.addEventListener('click', ()=>{ try { this.deleteCurrentContentBlock(); } catch(e){ console.warn('delete block failed', e); } });
                wrap.appendChild(deleteBtn);
                if (debugUI) {
                    // Reset button
                    const resetBtn = document.createElement('button');
                    resetBtn.type='button'; resetBtn.id='ccve-reset-zoom-btn'; resetBtn.textContent='Reset';
                    resetBtn.style.cssText='margin-left:4px;font-size:11px;padding:2px 6px;line-height:1.2;border:1px solid #334155;background:#1e293b;color:#fff;border-radius:4px;cursor:pointer;';
                    resetBtn.title='Reset zoom to 100% (Fit baseline)';
                    resetBtn.addEventListener('click', ()=>{ 
                        try { localStorage.setItem('ccveBlockStageZoomRel','1'); } catch(_) {}
                        if(document.getElementById('ccve-zoom-select')) document.getElementById('ccve-zoom-select').value='100';
                        const frame = document.getElementById('block-stage-wrapper');
                        const wBefore = frame?.style.width; const hBefore = frame?.style.height;
                        this.applyBlockStageScale();
                        // Restore to prevent growth jump
                        if (frame) { if (wBefore) frame.style.width = wBefore; if (hBefore) frame.style.height = hBefore; }
                    });
                    wrap.appendChild(resetBtn);
                    // Drift Test button
                    const driftBtn = document.createElement('button');
                    driftBtn.type='button'; driftBtn.id='ccve-drift-btn'; driftBtn.textContent='Drift Test';
                    driftBtn.style.cssText='margin-left:6px;font-size:11px;padding:2px 6px;line-height:1.2;border:1px solid #7f1d1d;background:#431515;color:#fff;border-radius:4px;cursor:pointer;';
                    driftBtn.title='Toggle artificial ruler misalignment for validation';
                    driftBtn.addEventListener('click', () => {
                        const host = document.querySelector('#block-stage-wrapper .ccve-scale-host');
                        if (!host) return;
                        const h = host.querySelector('.ccve-ruler-horizontal');
                        const v = host.querySelector('.ccve-ruler-vertical');
                        if (!h || !v) return;
                        const drifting = host.classList.toggle('ccve-drift-active');
                        if (drifting) { h.style.transform = 'translateX(37px)'; v.style.transform = 'translateY(24px)'; this._rulerAlignWarn = true; }
                        else { h.style.transform=''; v.style.transform=''; this._validateRulerAlignment(); }
                        this._showScaleBadge(this._blockStageScale||1, this._blockStageFitScale||1, (this._blockStageScale||1) < 0.35);
                    });
                    wrap.appendChild(driftBtn);
                    // Pin UI button
                    const pin = document.createElement('button');
                    pin.type='button'; pin.id='ccve-pin-ui-btn'; pin.textContent='Pin UI';
                    pin.style.cssText='margin-left:6px;font-size:11px;padding:2px 6px;line-height:1.2;border:1px solid #334155;background:#0f172a;color:#cbd5e1;border-radius:4px;cursor:pointer;';
                    const syncPinState = ()=>{ const pinned = localStorage.getItem('ccveChromePinned')==='1'; pin.classList.toggle('active', pinned); pin.textContent = pinned ? 'Unpin UI' : 'Pin UI'; };
                    pin.addEventListener('click', ()=>{ const cur = localStorage.getItem('ccveChromePinned')==='1'; localStorage.setItem('ccveChromePinned', cur?'0':'1'); syncPinState(); this.applyBlockStageScale(); });
                    syncPinState();
                    wrap.appendChild(pin);
                }
                togglesBar.appendChild(wrap);
                this.applyBlockStageScale();
                // Debounced resize binding (guard if not already bound)
                if (!this._fitResizeBound) {
                    this._fitResizeBound = true;
                    let resizeTimer; window.addEventListener('resize', () => { clearTimeout(resizeTimer); resizeTimer = setTimeout(()=>{ if(!this._fullPixelActive) this.applyBlockStageScale(); }, 120); });
                }
                // Keyboard shortcuts only when debug UI visible
                // Keyboard zoom shortcuts only in advanced mode (debugUI)
                if (debugUI && !this._zoomKeysBound) {
                    this._zoomKeysBound = true;
                    window.addEventListener('keydown', (e)=>{
                        if (this._fullPixelActive) return;
                        if (e.target && (e.target.tagName==='INPUT' || e.target.tagName==='TEXTAREA' || e.target.isContentEditable)) return;
                        const k = e.key;
                        if (k==='+' || k==='=' || k==='Add') { this._stepZoom(steps, 1); e.preventDefault(); return; }
                        if (k==='-' || k==='_' || k==='Subtract') { this._stepZoom(steps, -1); e.preventDefault(); return; }
                    });
                }
            }
        } catch(e) { console.warn('[CC] zoom UI inject failed', e); }
    }, 0);
    // Ensure category sub-tabs are wired on first load (were only activated after a tab bounce)
    this.setupCategoryTabSwitching();

        // Grid toggle persistence per surface
        const gridCb = document.getElementById('toggle-grid');
        if (gridCb) {
            gridCb.addEventListener('change', (e) => {
                const checked = !!gridCb.checked;
                const surf = this.activeSurface || 'block';
                if (this._surfaceStates?.[surf]) this._surfaceStates[surf].grid = checked;
                this._guidesEnabled = !!checked;
                this.toggleGuides(checked);
            });
        }

        // Smart snapping toggle (affects snapping logic only)
        const smartCb = document.getElementById('toggle-smart-snap');
        if (smartCb) {
            smartCb.addEventListener('change', () => {
                const show = !!smartCb.checked;
                const surf = this.activeSurface || 'block';
                if (!this._surfaceStates[surf]) this._surfaceStates[surf] = {};
                this._surfaceStates[surf].smartSnap = show;
                this._smartSnappingEnabled = !!show;
            });
            // Initialize
            try {
                const surf = this.activeSurface || 'block';
                const should = this._surfaceStates?.[surf]?.smartSnap;
                if (typeof should === 'boolean') { smartCb.checked = should; this._smartSnappingEnabled = should; }
            } catch(_) {}
        }

        // Rulers toggle
        const rulersCb = document.getElementById('toggle-rulers');
        if (rulersCb) {
            rulersCb.addEventListener('change', () => {
                const show = !!rulersCb.checked;
                const surf = this.activeSurface || 'block';
                if (this._surfaceStates?.[surf]) this._surfaceStates[surf].rulers = show;
                this.toggleRulers(show);
            });
        }

        // Provide explicit toggleRulers if absent (ensures outside positioning class applied)
        if (!this.toggleRulers) {
            this.toggleRulers = (show) => { this._toggleRulersInternal(show); };
        }

    // HUD toggle (dimension badge) now controls overlay debug label visibility
        const hudCb = document.getElementById('toggle-hud');
        if (hudCb) {
            hudCb.addEventListener('change', () => {
                const show = !!hudCb.checked;
                const surf = this.activeSurface || 'block';
                if (this._surfaceStates?.[surf]) this._surfaceStates[surf].hud = show;
                this._hudEnabled = show;
        try { document.querySelectorAll('.ccve-overlay-debug-label').forEach(h=> h.style.display = show ? 'block' : 'none'); } catch(_) {}
            });
            // Initialize based on stored state
            try {
                const surf = this.activeSurface || 'block';
                const should = this._surfaceStates?.[surf]?.hud;
                if (typeof should === 'boolean') { hudCb.checked = should; this._hudEnabled = should; }
            } catch(_) {}
        }

        // Zone edit toggle persistence (containers only)
        const zoneCb = document.getElementById('toggle-zone-edit');
        if (zoneCb) {
            zoneCb.addEventListener('change', () => {
                if (this.activeSurface !== 'containers') {
                    // Ignore if not on containers surface
                    zoneCb.checked = false;
                    return;
                }
                this._surfaceStates.containers.zoneEdit = !!zoneCb.checked;
                this.toggleZoneEditMode(!!zoneCb.checked);
            });
        }
    }

    /** Render any existing guides to the current surface (simple lines for thirds/center). */
    _renderExistingGuides() {
        // Only render on containers surface for now
        const surf = this.activeSurface || 'block';
        const host = (surf === 'containers') ? document.getElementById('container-canvas') : document.querySelector('#block-stage-wrapper .ccve-stage-outer');
        if (!host) return;
        // Ensure overlay layer exists
        let layer = host.querySelector('.ccve-guides-layer');
        if (!layer) {
            layer = document.createElement('div');
            layer.className = 'ccve-guides-layer';
            Object.assign(layer.style, { position:'absolute', inset:'0', pointerEvents:'none', zIndex:'5' });
            host.appendChild(layer);
        }
        // Clear & re-render simple guides
        layer.innerHTML = '';
        const addLine = (x1,y1,x2,y2) => {
            const line = document.createElement('div');
            const isVertical = x1 === x2;
            Object.assign(line.style, { position:'absolute', background:'rgba(59,130,246,0.6)' });
            if (isVertical) { line.style.width='1px'; line.style.left=`${x1}px`; line.style.top='0'; line.style.bottom='0'; }
            else { line.style.height='1px'; line.style.top=`${y1}px`; line.style.left='0'; line.style.right='0'; }
            layer.appendChild(line);
        };
        try {
            (this._guidesVertical||[]).forEach(x => addLine(x,0,x,0));
            (this._guidesHorizontal||[]).forEach(y => addLine(0,y,0,y));
        } catch(_) {}
        layer.style.display = this._guidesEnabled ? 'block' : 'none';
    }

    /** Highlight the active snapping guide (one line), or clear when null. */
    _updateActiveGuideHighlight(guide) {
        const surf = this.activeSurface || 'block';
        const host = (surf === 'containers') ? document.getElementById('container-canvas') : document.querySelector('#block-stage-wrapper .ccve-stage-outer');
        if (!host) return;
        
        // Get container offset for block surface (guides are container-relative, DOM is canvas-absolute)
        let offsetX = 0, offsetY = 0;
        if (surf === 'block') {
            const layout = this.currentConfig?.layout;
            if (layout?.position) {
                offsetX = layout.position.x || 0;
                offsetY = layout.position.y || 0;
            } else if (layout?._originalPosition) {
                offsetX = layout._originalPosition.x || 0;
                offsetY = layout._originalPosition.y || 0;
            }
        }
        
        let layer = host.querySelector('.ccve-guides-layer');
        // Create guides layer if missing (ensure it exists for dynamic highlights)
        if (!layer) {
            layer = document.createElement('div');
            layer.className = 'ccve-guides-layer';
            Object.assign(layer.style, { position:'absolute', inset:'0', pointerEvents:'none', zIndex:'5' });
            host.appendChild(layer);
        }
        let hl = layer.querySelector('.ccve-guide-highlight');
        if (!guide) {
            if (hl) hl.remove();
            return;
        }
        if (!hl) {
            hl = document.createElement('div');
            hl.className = 'ccve-guide-highlight';
            Object.assign(hl.style, { position:'absolute', pointerEvents:'none', zIndex:'6', background:'rgba(234,88,12,0.9)' });
            const label = document.createElement('div');
            label.className = 'ccve-guide-label';
            Object.assign(label.style, { position:'absolute', top:'-14px', left:'0', transform:'translateX(-50%)', background:'rgba(0,0,0,0.7)', color:'#fff', fontSize:'10px', padding:'1px 3px', borderRadius:'3px', whiteSpace:'nowrap' });
            hl.appendChild(label);
            layer.appendChild(hl);
        }
        if (guide.axis === 'v') {
            hl.style.width = '2px';
            hl.style.left = `${guide.pos + offsetX}px`;
            hl.style.top = '0';
            hl.style.bottom = '0';
            hl.style.height = '';
            const lbl = hl.querySelector('.ccve-guide-label');
            if (lbl) { lbl.style.left = '1px'; lbl.textContent = guide.type==='center' ? 'center X' : ''; }
        } else {
            hl.style.height = '2px';
            hl.style.top = `${guide.pos + offsetY}px`;
            hl.style.left = '0';
            hl.style.right = '0';
            hl.style.width = '';
            const lbl = hl.querySelector('.ccve-guide-label');
            if (lbl) { lbl.style.left = '50%'; lbl.textContent = guide.type==='center' ? 'center Y' : ''; }
        }
        // brief pulse feedback
        try { hl.classList.remove('snap-pulse'); void hl.offsetWidth; hl.classList.add('snap-pulse'); } catch(_) {}
        layer.style.display = this._guidesEnabled ? 'block' : 'none';
    }

    /** Render measurement overlays: gutter (artwork→text) and inter-line spacing. */
    _updateMeasurementOverlays(measure) {
        const surf = this.activeSurface || 'block';
        const host = (surf === 'containers') ? document.getElementById('container-canvas') : document.querySelector('#block-stage-wrapper .ccve-stage-outer');
        if (!host) return;
        
        // Get container offset for block surface (measurements are container-relative, DOM is canvas-absolute)
        let offsetX = 0, offsetY = 0;
        if (surf === 'block') {
            const layout = this.currentConfig?.layout;
            if (layout?.position) {
                offsetX = layout.position.x || 0;
                offsetY = layout.position.y || 0;
            } else if (layout?._originalPosition) {
                offsetX = layout._originalPosition.x || 0;
                offsetY = layout._originalPosition.y || 0;
            }
        }
        
        let layer = host.querySelector('.ccve-measure-layer');
        if (!layer) {
            layer = document.createElement('div');
            layer.className = 'ccve-measure-layer';
            Object.assign(layer.style, { position:'absolute', inset:'0', pointerEvents:'none', zIndex:'7' });
            host.appendChild(layer);
        }
        layer.innerHTML = '';
        if (!measure) { return; }
        const addBadge = (x, y, text) => {
            const b = document.createElement('div');
            b.className = 'ccve-measure-badge';
            // Add offset to convert container-relative to canvas-absolute
            Object.assign(b.style, { position:'absolute', left:`${x + offsetX}px`, top:`${y + offsetY}px`, transform:'translate(-50%, -100%)', background:'rgba(0,0,0,0.7)', color:'#fff', padding:'2px 4px', fontSize:'10px', borderRadius:'3px' });
            b.textContent = text;
            layer.appendChild(b);
        };
        const addVBracket = (x, y1, y2) => {
            const v = document.createElement('div');
            // Add offset to convert container-relative to canvas-absolute
            Object.assign(v.style, { position:'absolute', left:`${x + offsetX}px`, top:`${Math.min(y1,y2) + offsetY}px`, height:`${Math.abs(y2-y1)}px`, width:'1px', background:'rgba(239,68,68,0.9)' });
            layer.appendChild(v);
        };
        // Gutter measurement
        if (measure.gutter && Number.isFinite(measure.gutter.value) && Number.isFinite(measure.gutter.fromX) && Number.isFinite(measure.gutter.toX) && Number.isFinite(measure.gutter.y)) {
            const { fromX, toX, y, value } = measure.gutter;
            const xMid = fromX + (toX - fromX) / 2;
            addVBracket(fromX, y - 8, y + 24);
            addVBracket(toX, y - 8, y + 24);
            addBadge(xMid, y - 10, `${value}px`);
        }
        // Inter-line spacing measurements
        if (Array.isArray(measure.interlines)) {
            measure.interlines.forEach(m => {
                if (!Number.isFinite(m.leftX) || !Number.isFinite(m.y1) || !Number.isFinite(m.y2) || !Number.isFinite(m.value)) return;
                const x = m.leftX;
                addVBracket(x, m.y1, m.y2);
                addBadge(x, (m.y1 + m.y2) / 2, `${m.value}px`);
            });
        }
    }

    _clearMeasurementOverlays() {
        const surf = this.activeSurface || 'block';
        const host = (surf === 'containers') ? document.getElementById('container-canvas') : document.querySelector('#block-stage-wrapper .ccve-stage-outer');
        if (!host) return;
        const layer = host.querySelector('.ccve-measure-layer');
        if (layer) layer.innerHTML = '';
    }

    /** Render a center crosshair when both center X and center Y are snapped. */
    _updateCenterCrosshair(cross) {
        const surf = this.activeSurface || 'block';
        const host = (surf === 'containers') ? document.getElementById('container-canvas') : document.querySelector('#block-stage-wrapper .ccve-stage-outer');
        if (!host) return;
        
        // Get container offset for block surface (crosshair coords are container-relative, DOM is canvas-absolute)
        let offsetX = 0, offsetY = 0;
        if (surf === 'block') {
            const layout = this.currentConfig?.layout;
            if (layout?.position) {
                offsetX = layout.position.x || 0;
                offsetY = layout.position.y || 0;
            } else if (layout?._originalPosition) {
                offsetX = layout._originalPosition.x || 0;
                offsetY = layout._originalPosition.y || 0;
            }
        }
        
        let layer = host.querySelector('.ccve-guides-layer');
        // Create guides layer if missing (ensure it exists for crosshair rendering)
        if (!layer) {
            layer = document.createElement('div');
            layer.className = 'ccve-guides-layer';
            Object.assign(layer.style, { position:'absolute', inset:'0', pointerEvents:'none', zIndex:'5' });
            host.appendChild(layer);
        }
        let ch = layer.querySelector('.ccve-center-crosshair');
        if (!cross || !Number.isFinite(cross.x) || !Number.isFinite(cross.y)) {
            if (ch) ch.remove();
            return;
        }
        if (!ch) {
            ch = document.createElement('div');
            ch.className = 'ccve-center-crosshair';
            Object.assign(ch.style, { position:'absolute', pointerEvents:'none', zIndex:'8' });
            const v = document.createElement('div'); v.className='ccve-center-crosshair-v';
            const h = document.createElement('div'); h.className='ccve-center-crosshair-h';
            const dot = document.createElement('div'); dot.className='ccve-center-crosshair-dot';
            ch.appendChild(v); ch.appendChild(h); ch.appendChild(dot);
            layer.appendChild(ch);
        }
        const v = ch.querySelector('.ccve-center-crosshair-v');
        const h = ch.querySelector('.ccve-center-crosshair-h');
        const dot = ch.querySelector('.ccve-center-crosshair-dot');
        // Add container offset to convert container-relative coords to canvas-absolute
        const absX = cross.x + offsetX;
        const absY = cross.y + offsetY;
        Object.assign(v.style, { position:'absolute', left:`${absX}px`, top:'0', bottom:'0', width:'2px', background:'rgba(234,88,12,0.9)' });
        Object.assign(h.style, { position:'absolute', top:`${absY}px`, left:'0', right:'0', height:'2px', background:'rgba(234,88,12,0.9)' });
        Object.assign(dot.style, { position:'absolute', left:`${absX-3}px`, top:`${absY-3}px`, width:'6px', height:'6px', borderRadius:'50%', background:'#fff', boxShadow:'0 0 0 2px rgba(234,88,12,0.9)' });
    }

    _clearCenterCrosshair(){
        const surf = this.activeSurface || 'block';
        const host = (surf === 'containers') ? document.getElementById('container-canvas') : document.querySelector('#block-stage-wrapper .ccve-stage-outer');
        if (!host) return;
        const layer = host.querySelector('.ccve-guides-layer');
        if (!layer) return;
        const ch = layer.querySelector('.ccve-center-crosshair');
        if (ch) ch.remove();
    }

    /** Enable/disable all guides overlays for the active surface. */
    toggleGuides(show) {
        this._guidesEnabled = !!show;
        // Re-render for current surface
        try { this._renderExistingGuides(); } catch(_) {}
        // Also toggle grid visuals if present
        const surf = this.activeSurface || 'block';
        if (surf === 'containers') this.toggleCanvasGrid(!!show); else this.toggleBlockStageGrid(!!show);
        // Ensure baseline guides are present for snapping logic
        try { guidesEnsureBaseline(this); } catch(_) {}
    }

    // New: Setup category sub-tab switching for Content Blocks
    setupCategoryTabSwitching() {
    // Idempotent guard so we don't bind duplicate listeners on tab re-entry
    if (this._categoryTabsInitialized) return;
        const categoryTabButtons = document.querySelectorAll('.canvas-category-tab-button');
        const categoryPanels = document.querySelectorAll('.category-panel');
        if (!categoryTabButtons.length || !categoryPanels.length) return;
        // Hide all panels except the first
        categoryPanels.forEach((panel, i) => {
            if (i === 0) {
                panel.classList.add('active');
            } else {
                panel.classList.remove('active');
            }
        });
        categoryTabButtons.forEach((button, i) => {
            if (i === 0) {
                button.classList.add('active');
            } else {
                button.classList.remove('active');
            }
            button.addEventListener('click', function(e) {
                // Remove active from all
                categoryTabButtons.forEach(btn => btn.classList.remove('active'));
                categoryPanels.forEach(panel => panel.classList.remove('active'));
                // Add active to clicked tab and corresponding panel
                button.classList.add('active');
                const targetPanel = document.getElementById('category-' + button.dataset.category);
                if (targetPanel) targetPanel.classList.add('active');
                // When in Layers mode, suppress server preview during Background editing to comply with no-dummy-data (keep DOM intact)
                try {
                    const isBackground = (button.dataset.category === 'background');
                    if (window.castconductorCanvasEditor) {
                        const ed = window.castconductorCanvasEditor;
                        if (ed._editorMode === 'layers') {
                            ed._suppressPreviewForBackground = isBackground;
                            if (!isBackground) { try { delete ed._lastPreviewHash; } catch(_) {} }
                        }
                        // Ensure Background Layers list rehydrates when opening the Background panel
                        if (isBackground && typeof ed.refreshBackgroundLayersUI === 'function') {
                            try { ed.refreshBackgroundLayersUI(); } catch(_) {}
                        }
                    }
                } catch(_) {}
            });
        });
    this._categoryTabsInitialized = true;
    }

    // ==================== Phase 2.7 Early UI Enhancements ====================
    /** Initialize newly added container inner tabs & new block button */
    initPhase27Enhancements() {
    // Prevent re-running (was causing tab state to bounce back to stored last_top_tab)
    if (this._phase27InitDone) return;
        // New Block From Template button (real REST backed)
        const newTplBtn = document.getElementById('canvas-new-baseblock-btn');
        if (newTplBtn && !newTplBtn._ccBound) {
            newTplBtn._ccBound = true;
            newTplBtn.disabled = false;
            newTplBtn.title = 'Create new content block from template';
            newTplBtn.addEventListener('click', async (e) => {
                e.preventDefault();
                await this.createNewContentBlockFromTemplate();
            });
        }
        // Container inner tab switching
        const contTabBtns = document.querySelectorAll('.ccve-cont-tab-btn');
        const panels = document.querySelectorAll('.ccve-cont-tab-panel');
        contTabBtns.forEach(btn => {
            if (btn._ccBound) return; btn._ccBound = true;
            btn.addEventListener('click', () => {
                contTabBtns.forEach(b=>b.classList.remove('active'));
                panels.forEach(p=>p.classList.remove('active'));
                btn.classList.add('active');
                const id = btn.dataset.contTab;
                const panel = document.getElementById(`ccve-containers-panel-${id}`);
                if (panel) panel.classList.add('active');
                try { prefs.set('last_container_subtab', id); } catch(_) {}
                if (id === 'scheduling') this._refreshScheduleDump();
            });
        });
        // Restore last inner tab
        try {
            const last = prefs.get('last_container_subtab', null);
            if (last) {
                const btn = document.querySelector(`.ccve-cont-tab-btn[data-cont-tab="${last}"]`);
                if (btn) btn.click();
            }
        } catch(_) {}
        // Restore last top-level tab only once
        if (!this._restoredTopTab) {
            this._restoredTopTab = true;
            try {
                const lastTop = prefs.get('last_top_tab', null);
                if (lastTop) {
                    const btn = document.querySelector(`.canvas-tab-button[data-tab="${lastTop}"]`);
                    if (btn && !btn.classList.contains('active')) btn.click();
                }
            } catch(_) {}
        }
        // Observe top-level tab clicks to persist
        document.querySelectorAll('.canvas-tab-button').forEach(b=>{
            if (b._ccPersistBound) return; b._ccPersistBound=true;
            b.addEventListener('click',()=>{ try { prefs.set('last_top_tab', b.dataset.tab || 'content-blocks'); } catch(_) {} });
        });
        // BaseBlock template persistence
        const sel = document.getElementById('canvas-baseblock-select');
        if (sel && !sel._ccPersistBound) {
            sel._ccPersistBound = true;
            try { const remembered = prefs.get('baseblock_template', null); if (remembered && [...sel.options].some(o=>o.value===remembered)) { sel.value = remembered; this._baseBlockTemplate = remembered; } } catch(_){ }
            sel.addEventListener('change',()=>{ try { prefs.set('baseblock_template', sel.value); } catch(_) {} });
        }
    this._flashLastCreatedHint();
    this._phase27InitDone = true;
    }

    _flashLastCreatedHint() {
        const hint = document.getElementById('ccve-last-created-hint');
        if (!hint) return;
        try {
            const obj = prefs.get('last_created_block', null);
            if (!obj || !obj.t) return;
            const ageMs = Date.now() - obj.t; if (ageMs > 6 * 60 * 60 * 1000) return;
            const rel = utilRelativeTime(ageMs);
            const span = hint.querySelector('[data-rel-time]'); if (span) span.textContent = `${rel}`;
            hint.style.display='inline';
        } catch(_) {}
    }
    // _relativeTime migrated to utils.relativeTime

    _refreshScheduleDump() {
        const dump = document.getElementById('ccve-schedule-dump'); if (!dump) return;
        const sel = this.selectedContainerId; if (!sel) { dump.textContent='(Select a container to inspect schedule)'; return; }
        const sched = this._containerSchedules?.[String(sel)];
        if (!sched) { dump.textContent='(No schedule built yet)'; return; }
        dump.textContent = JSON.stringify(sched.slice(0,200), null, 2);
    }

    /**
     * Setup container management functionality
     */
    setupContainerManagement() {
    // ==================== [REGION] CONTAINER MANAGEMENT ====================
        // Container toggle switches
        document.addEventListener('change', (e) => {
            if (e.target.classList.contains('container-enabled-toggle')) {
                this.toggleContainer(e.target.dataset.containerId, e.target.checked);
            }
        });

        // Canvas: Show Content Preview toggle
        const showPreviewToggle = document.getElementById('show-content-preview');
        if (showPreviewToggle) {
            this.showContentPreview = !!showPreviewToggle.checked;
            showPreviewToggle.addEventListener('change', () => {
                this.showContentPreview = !!showPreviewToggle.checked;
                // Stop all cycles when toggled off
                if (!this.showContentPreview) {
                    this.stopAllPreviewCycles();
                }
                // Re-render canvas previews according to new toggle state
                this.refreshContainerCanvas();
            });
        }

        // Optional: Shuffle schedule toggle
        const shuffleToggle = document.getElementById('preview-shuffle');
        if (shuffleToggle) {
            this.previewShuffle = !!shuffleToggle.checked;
            shuffleToggle.addEventListener('change', () => {
                this.previewShuffle = !!shuffleToggle.checked;
                this.refreshContainerCanvas();
            });
        }

        // Container edit buttons
        document.addEventListener('click', (e) => {
            if (e.target.classList.contains('container-edit-btn')) {
                this.editContainer(e.target.dataset.containerId);
            }
            if (e.target.classList.contains('container-defaults-btn')) {
                const containerId = e.target.dataset.containerId;
                this.showDefaultsModal(containerId);
            }
        });

        // Container delete buttons
        document.addEventListener('click', (e) => {
            if (e.target.classList.contains('container-delete-btn')) {
                this.deleteContainer(e.target.dataset.containerId);
            }
        });

        // Assign block buttons
        document.addEventListener('click', (e) => {
            if (e.target.classList.contains('assign-block-btn')) {
                this.showBlockAssignmentDialog(e.target.dataset.containerId);
            }
        });

        // Zone selector change reloads assignments list and preview
        document.addEventListener('change', (e) => {
            const sel = e.target.closest('.zone-select');
            if (sel && sel.classList.contains('zone-select')) {
                const containerId = sel.dataset.containerId;
                if (containerId) this.loadContainerBlocks(containerId);
            }
        });

        // Per-container shuffle toggle (in container list)
        document.addEventListener('change', (e) => {
            const toggle = e.target.closest('.container-shuffle-toggle input[type="checkbox"]');
            if (toggle) {
                const parent = toggle.closest('.container-item');
                const containerId = parent?.dataset?.containerId;
                if (!containerId) return;
                // Persist behavior.shuffle_items to backend
                try {
                    fetch(`${castconductorCanvasAjax.rest_url}castconductor/v5/containers/${encodeURIComponent(containerId)}`, {
                        method: 'PATCH',
                        headers: {
                            'Content-Type': 'application/json',
                            'X-WP-Nonce': castconductorCanvasAjax.nonce
                        },
                        body: JSON.stringify({ behavior: { shuffle_items: !!toggle.checked } })
                    }).catch(()=>{});
                } catch(_) {}
                const current = this._assignedBlocksCache?.[String(containerId)] || [];
                this.buildContainerSchedule(containerId, current, !!toggle.checked);
                this.stopContainerPreviewCycle(containerId);
                this.renderContainerPreview(containerId, current);
                if (this.showContentPreview && (current.filter(b => String(b.enabled ?? 1) !== '0').length > 1)) {
                    this.startContainerPreviewCycle(containerId, current);
                }
            }
        });

        // Container actions
        const createContainerBtn = document.getElementById('create-container-btn');
        if (createContainerBtn) {
            createContainerBtn.addEventListener('click', () => this.createNewContainer());
        }

        const saveChangesBtn = document.getElementById('save-container-changes-btn');
        if (saveChangesBtn) {
            saveChangesBtn.addEventListener('click', () => this.saveAllContainerChanges());
        }

        const refreshCanvasBtn = document.getElementById('refresh-canvas-btn');
        if (refreshCanvasBtn) {
            refreshCanvasBtn.addEventListener('click', () => this.refreshContainerCanvas());
        }

        // Apply defaults button (if present)
        const applyDefaultsBtn = document.getElementById('apply-defaults-btn');
        if (applyDefaultsBtn) {
            applyDefaultsBtn.addEventListener('click', async () => {
                const selected = document.querySelector('.container-item');
                const containerId = selected?.dataset?.containerId;
                if (!containerId) {
                    this.showNotification('No container selected', 'error', true);
                    return;
                }
                await this.applyProductionDefaults(containerId);
            });
        }

        // Drag and drop for content blocks
        this.setupDragAndDrop();
    }

    /**
     * Show a small modal to pick interval for defaults and apply
     */
    showDefaultsModal(containerId) {
        const body = `
            <form id="cc-defaults-form" class="cc-form">
                <div class="cc-form-grid">
                    <div class="cc-form-row"><label>Y</label><input type="number" id="cc-field-y" value="480" min="0" /></div>
                    <select id="cc-defaults-interval">
                        <option value="15" selected>15 seconds</option>
                        <option value="12">12 seconds</option>
                    </select>
                            <option value="center">Middle (360)</option>
                            <option value="bottom" selected>Bottom Edge (720 - height)</option>
                    <label>What to apply</label>
                    <select id="cc-defaults-mode">
                        <option value="interval_only" selected>Only update interval (keep current assignments)</option>
                        <option value="full_reset">Reset to 50/30/10/10 and set interval</option>
                    </select>
                </div>
                <div class="cc-form-actions">
                    <button type="button" id="cc-defaults-cancel">Cancel</button>
                    <button type="submit" class="primary">Apply Defaults</button>
                </div>
            </form>
        `;
    this.showModal('Apply Container Defaults (scope: this container only)', body);
        document.getElementById('cc-defaults-cancel')?.addEventListener('click', () => this.closeModal());
        document.getElementById('cc-defaults-form')?.addEventListener('submit', async (e) => {
            e.preventDefault();
            const interval = parseInt(document.getElementById('cc-defaults-interval')?.value || '15', 10);
            const mode = String(document.getElementById('cc-defaults-mode')?.value || 'interval_only');
            await this.applyProductionDefaults(containerId, interval, mode);
            this.closeModal();
        });
    }

    /** Apply server-side production defaults to a container */
    async applyProductionDefaults(containerId, interval = 15, mode = 'interval_only') {
        try {
            const url = `${castconductorCanvasAjax.rest_url}castconductor/v5/containers/${containerId}/defaults`;
            const resp = await fetch(url, {
                method: 'POST',
                headers: { 'Content-Type': 'application/json', 'X-WP-Nonce': castconductorCanvasAjax.nonce },
                body: JSON.stringify({ interval, mode })
            });
            if (!resp.ok) {
                const err = await this.safeJson(resp);
                throw new Error(err?.message || `HTTP ${resp.status}`);
            }
            this.showNotification('Defaults applied', 'success');
            await this.loadContainerBlocks(containerId);
        } catch (e) {
            console.error('Apply defaults failed:', e);
            this.showNotification(e.message || 'Failed to apply defaults', 'error', true);
        }
    }

    /**
     * Load live content blocks data instead of sample data
     */
    async loadLiveContentBlocks() { return apiLoadLiveContentBlocks(this); }

    /**
     * Display content blocks with live data
     */
    displayLiveContentBlocks(contentBlocks) {
        const selector = document.getElementById('canvas-content-block-select');
        const loadBtn = document.getElementById('canvas-load-block-btn');
        const container = document.querySelector('.content-assignment-section .available-blocks');

        // SHELVED (Dec 2025): Filter out custom_api blocks from UI - feature hidden for now
        const filteredBlocks = contentBlocks ? contentBlocks.filter(b => b.type !== 'custom_api') : [];

        if (!filteredBlocks || filteredBlocks.length === 0) {
            if (container) container.innerHTML = '<div class="no-content-blocks">No content blocks available</div>';
            if (selector) selector.innerHTML = '<option value="">-- No Blocks --</option>';
            if (loadBtn) loadBtn.disabled = true;
            return;
        }

        if (selector) {
            selector.innerHTML = filteredBlocks.map(b => `<option value="${this.escapeHtml(String(b.id))}">${this.escapeHtml(b.name || ('Block '+b.id))} (${this.escapeHtml(b.type)})</option>`).join('');
            // Restore last selected content block if present; otherwise don't auto-switch away from the user's current block
            try {
                const lastId = localStorage.getItem('ccveLastBlockId');
                if (!this.currentContentBlockId && lastId) {
                    // If lastId exists in the new list, preselect it (do not auto-load)
                    const exists = filteredBlocks.some(b => String(b.id) === String(lastId));
                    if (exists) {
                        selector.value = String(lastId);
                    }
                }
                // If still no selection (first-time load), prefer a Track Info style block; fallback to first
                if (!selector.value && filteredBlocks.length > 0) {
                    const prefer = filteredBlocks.find(b => {
                        try {
                            const t = String(b.type || '').toLowerCase();
                            return t === 'trackinfo' || t === 'track_info' || t === 'track-info' || t === 'track info' || (t.includes('track') && t.includes('info'));
                        } catch(_) { return false; }
                    });
                    selector.value = String((prefer || filteredBlocks[0]).id);
                }
            } catch(_) { /* non-fatal */ }
        }
        if (loadBtn && !loadBtn._ccBound) {
            loadBtn._ccBound = true;
            loadBtn.addEventListener('click', async () => {
                const raw = selector?.value;
                const id = raw != null ? String(raw).trim() : '';
                if (!id) { this.showNotification && this.showNotification('Select a block first','warning'); return; }
                try {
                    console.debug('[CC] LoadSelected clicked id=', id);
                    // Ensure we are in Preview mode so server preview is allowed to render in normal mode
                    try {
                        this._editorMode = 'preview';
                        try { localStorage.setItem('ccveEditorMode','preview'); } catch(_){ }
                        const wrapEl = document.getElementById('block-stage-wrapper');
                        if (wrapEl) wrapEl.classList.remove('ccve-simple-mode');
                        this._suppressPreviewForBackground = false;
                    } catch(_){ }
                    // Invalidate preview hash so a fresh server preview will be requested
                    try { delete this._lastPreviewHash; } catch(_) {}
                    // Use the same robust path as selector change: persist lastId, hydrate legacy layers, log
                    await this.loadContentBlockWithLog(id);
                } catch(e) {
                    console.warn('[CC] loadContentBlockWithLog threw', e);
                    this.showNotification && this.showNotification('Failed to load selected block','error', true);
                }
            });
        }

        if (selector && !selector._ccBoundChange) {
            selector._ccBoundChange = true;
            selector.addEventListener('change', () => {
                const id = selector.value;
                if (id) {
                    console.debug('[CC] Selector change auto-load id=', id);
                    // Ensure Preview mode for normal-mode rendering
                    try {
                        this._editorMode = 'preview';
                        try { localStorage.setItem('ccveEditorMode','preview'); } catch(_){ }
                        const wrapEl = document.getElementById('block-stage-wrapper');
                        if (wrapEl) wrapEl.classList.remove('ccve-simple-mode');
                        this._suppressPreviewForBackground = false;
                    } catch(_){ }
                    try { localStorage.setItem('ccveLastBlockId', String(id)); } catch(_) {}
                    this.loadContentBlockWithLog(id);
                }
            });
        }

        if (container) {
            container.innerHTML = contentBlocks.map(block => {
                return `
                    <div class="available-block" 
                         data-block-id="${this.escapeHtml(String(block.id))}"
                         data-block-type="${this.escapeHtml(String(block.type))}"
                         draggable="true">
                        <div class="block-header">
                            <h6>${this.escapeHtml(block.name)}</h6>
                            <span class="block-type">${this.escapeHtml(block.type)}</span>
                            <button type="button" class="preview-live-btn" data-block-id="${this.escapeHtml(String(block.id))}" style="float:right;">Preview</button>
                        </div>
                    </div>
                `;
            }).join('');
            container.querySelectorAll('.preview-live-btn').forEach(btn => {
                if (btn._ccBound) return; btn._ccBound = true;
                btn.addEventListener('click', (e) => {
                    e.stopPropagation();
                    const bid = btn.getAttribute('data-block-id');
                    if (bid) this.showLivePreview(bid);
                });
            });
            this.setupDragAndDrop && this.setupDragAndDrop();
        }
        
        // Initialize CPT import section (hierarchical dropdown for shoutouts/sponsors/promos)
        this.initCptImportSection();
    }

    /**
     * Initialize CPT Import Section - Hierarchical dropdown for loading
     * shoutouts, sponsors, and promos into the Content Block editor
     */
    initCptImportSection() {
        const section = document.getElementById('canvas-cpt-import-section');
        const typeSelect = document.getElementById('canvas-cpt-type-select');
        const postSelect = document.getElementById('canvas-cpt-post-select');
        const loadBtn = document.getElementById('canvas-cpt-load-btn');
        const status = document.getElementById('canvas-cpt-status');
        
        if (!section || !typeSelect || !postSelect || !loadBtn) return;
        
        // Show the section now that we've verified the elements exist
        section.style.display = 'block';
        
        // Skip if already bound
        if (typeSelect._ccCptBound) return;
        typeSelect._ccCptBound = true;
        
        // Handle CPT type selection - populate posts dropdown
        typeSelect.addEventListener('change', async () => {
            const cptType = typeSelect.value;
            postSelect.innerHTML = '<option value="">-- Loading... --</option>';
            postSelect.style.display = cptType ? 'block' : 'none';
            loadBtn.disabled = true;
            if (status) status.textContent = '';
            
            if (!cptType) {
                postSelect.innerHTML = '<option value="">-- Select Item --</option>';
                return;
            }
            
            try {
                const resp = await fetch(`${castconductorCanvasAjax.rest_url}castconductor/v5/canvas-editor/cpt-posts/${cptType}`, {
                    method: 'GET',
                    headers: { 'X-WP-Nonce': castconductorCanvasAjax.nonce }
                });
                const json = await resp.json();
                
                if (!resp.ok || !json.success) {
                    throw new Error(json.message || 'Failed to load posts');
                }
                
                const posts = json.data || [];
                if (posts.length === 0) {
                    postSelect.innerHTML = '<option value="">-- No items found --</option>';
                    if (status) status.textContent = 'No ' + cptType + 's found';
                    return;
                }
                
                postSelect.innerHTML = '<option value="">-- Select Item --</option>' +
                    posts.map(p => {
                        const statusBadge = p.status !== 'publish' ? ` [${p.status}]` : '';
                        return `<option value="${this.escapeHtml(String(p.id))}">${this.escapeHtml(p.title || 'Untitled')}${statusBadge}</option>`;
                    }).join('');
                
                if (status) status.textContent = `${posts.length} ${cptType}(s) available`;
                
            } catch (e) {
                console.warn('[CC] CPT posts fetch failed', e);
                postSelect.innerHTML = '<option value="">-- Error loading --</option>';
                if (status) status.textContent = 'Error: ' + (e.message || 'Failed');
            }
        });
        
        // Handle post selection - enable load button
        postSelect.addEventListener('change', () => {
            loadBtn.disabled = !postSelect.value;
        });
        
        // Handle load button - fetch CPT content and apply to canvas
        loadBtn.addEventListener('click', async () => {
            const cptType = typeSelect.value;
            const postId = postSelect.value;
            
            if (!cptType || !postId) {
                this.showNotification && this.showNotification('Select a content type and item first', 'warning');
                return;
            }
            
            try {
                loadBtn.disabled = true;
                loadBtn.textContent = '⏳ Loading...';
                if (status) status.textContent = 'Loading content...';
                
                const resp = await fetch(`${castconductorCanvasAjax.rest_url}castconductor/v5/canvas-editor/cpt-content/${cptType}/${postId}`, {
                    method: 'GET',
                    headers: { 'X-WP-Nonce': castconductorCanvasAjax.nonce }
                });
                const json = await resp.json();
                
                if (!resp.ok || !json.success) {
                    throw new Error(json.message || 'Failed to load content');
                }
                
                const content = json.data;
                console.debug('[CC] CPT content loaded:', content);
                
                // Apply CPT content to current block layers
                this.applyCptContentToBlock(content);
                
                this.showNotification && this.showNotification(`Loaded "${content.title}" content`, 'success');
                if (status) status.textContent = '✓ Content loaded';
                
            } catch (e) {
                console.warn('[CC] CPT content load failed', e);
                this.showNotification && this.showNotification('Failed to load: ' + (e.message || 'error'), 'error');
                if (status) status.textContent = 'Error loading content';
            } finally {
                loadBtn.disabled = false;
                loadBtn.textContent = '📥 Load CPT Content';
            }
        });
    }

    /**
     * Apply CPT content (title, content, featured image) to current block layers
     * Uses same "explode into layers" pattern as WordPress Post Selector
     * Creates new static-text and static-image layers from CPT content
     * Visual config is saved back to the CPT post's meta, not to a new Content Block
     */
    async applyCptContentToBlock(cptContent) {
        // Store CPT source info - we'll save visual config back to this post's meta
        this._editingCptPost = {
            id: cptContent.id,
            type: cptContent.type,
            post_type: cptContent.post_type,
            title: cptContent.title
        };
        
        // Use the CPT post ID as the "content block ID" for save button logic
        // Format: cpt:{type}:{post_id} - using colon to avoid underscore conflicts in post_type
        this.currentContentBlockId = `cpt:${cptContent.type}:${cptContent.id}`;
        
        // Initialize config if needed (may have existing visual config from post meta)
        if (!this.currentConfig) {
            this.currentConfig = { layers: [], layout: { width: 1280, height: 240, x: 0, y: 480 } };
        }
        
        // If the CPT post has existing visual config in meta, use it
        if (cptContent.visual_config && typeof cptContent.visual_config === 'object') {
            console.debug('[CC] applyCptContentToBlock: found existing visual config in CPT meta');
            this.currentConfig = cptContent.visual_config;
            if (!this.currentConfig.layers) this.currentConfig.layers = [];
            // Re-render existing config and return
            try { this._renderUnifiedLayers && this._renderUnifiedLayers(); } catch(e) {}
            if (this.refreshLayerManager) this.refreshLayerManager();
            this.unsavedChanges = false;
            this.updateSaveButton && this.updateSaveButton();
            return;
        }
        
        console.debug('[CC] applyCptContentToBlock: exploding CPT into layers', cptContent);
        
        // Import layer factory functions (same pattern as wordpress-post-selector.js)
        const { createStaticTextLayer, createStaticImageLayer } = await import('./modules/layer-constants.js');
        
        const layers = this.currentConfig.layers || [];
        if (!this.currentConfig.layers) this.currentConfig.layers = layers;
        
        // Get container layout for positioning
        const containerLayout = this.currentConfig?.layout;
        const containerW = containerLayout?.width || 1280;
        const containerH = containerLayout?.height || 240;
        const baseX = 60;
        const baseY = 20;
        
        // Track created layers
        const createdLayers = [];
        
        // Clean title
        const titleText = (cptContent.title || 'Untitled')
            .replace(/&amp;/g, '&')
            .replace(/&lt;/g, '<')
            .replace(/&gt;/g, '>')
            .replace(/&quot;/g, '"')
            .replace(/&#039;/g, "'");
        
        // Clean content (strip HTML tags)
        const contentText = (cptContent.content || '')
            .replace(/<[^>]*>/g, '')
            .replace(/&amp;/g, '&')
            .replace(/&lt;/g, '<')
            .replace(/&gt;/g, '>')
            .replace(/&quot;/g, '"')
            .replace(/&#039;/g, "'")
            .replace(/&nbsp;/g, ' ')
            .trim();
        
        // Get featured image URL
        const featuredImageUrl = cptContent.featured_image?.large || 
                                  cptContent.featured_image?.medium || 
                                  cptContent.featured_image?.full || 
                                  null;
        
        // Create title layer (static-text)
        if (titleText) {
            const titleLayer = createStaticTextLayer(titleText, {
                x: baseX,
                y: baseY,
                width: Math.min(700, containerW - baseX - 200),
                height: 60,
                font_size: 36,
                font_weight: '700',
                color: '#ffffff',
                text_shadow: { x: 2, y: 2, blur: 4, color: 'rgba(0,0,0,0.6)' },
                label: `${cptContent.type} Title`,
                _cptSource: { type: cptContent.type, postId: cptContent.id, field: 'title' }
            });
            layers.push(titleLayer);
            createdLayers.push('Title');
            console.debug('[CC] Created title layer:', titleLayer.id);
        }
        
        // Create content layer if content exists (static-text)
        if (contentText) {
            // Truncate for broadcast overlays
            const shortContent = contentText.length > 300 
                ? contentText.substring(0, 300) + '...' 
                : contentText;
            
            const contentLayer = createStaticTextLayer(shortContent, {
                x: baseX,
                y: baseY + 70,
                width: Math.min(700, containerW - baseX - 200),
                height: Math.min(100, containerH - baseY - 80),
                font_size: 20,
                font_weight: '400',
                color: '#e0e0e0',
                text_shadow: { x: 1, y: 1, blur: 2, color: 'rgba(0,0,0,0.5)' },
                label: `${cptContent.type} Content`,
                _cptSource: { type: cptContent.type, postId: cptContent.id, field: 'content' }
            });
            layers.push(contentLayer);
            createdLayers.push('Content');
            console.debug('[CC] Created content layer:', contentLayer.id);
        }
        
        // Create featured image layer if image exists (static-image)
        if (featuredImageUrl) {
            const imageSize = Math.min(180, containerH - 40);
            const featuredLayer = createStaticImageLayer(featuredImageUrl, {
                x: containerW - imageSize - 40,
                y: Math.max(10, (containerH - imageSize) / 2),
                width: imageSize,
                height: imageSize,
                fit: 'cover',
                border_radius: 8,
                label: `${cptContent.type} Image`,
                _cptSource: { type: cptContent.type, postId: cptContent.id, field: 'featured_image' }
            });
            layers.push(featuredLayer);
            createdLayers.push('Featured Image');
            console.debug('[CC] Created featured image layer:', featuredLayer.id);
        }
        
        if (createdLayers.length === 0) {
            this.showNotification && this.showNotification('No content to import from this CPT', 'warning');
            return;
        }
        
        // Store CPT reference in config
        this.currentConfig._cptSource = {
            type: cptContent.type,
            post_type: cptContent.post_type,
            postId: cptContent.id,
            title: cptContent.title,
            loadedAt: new Date().toISOString()
        };
        
        // Mark as unsaved - CRITICAL: must be set before render calls
        this.unsavedChanges = true;
        console.debug('[CC] applyCptContentToBlock: set unsavedChanges=true, currentContentBlockId=', this.currentContentBlockId);
        
        // Re-render ALL layers using unified renderer (same as wordpress-post-selector.js)
        try {
            this._renderUnifiedLayers && this._renderUnifiedLayers();
        } catch(e) {
            console.error('[CC] applyCptContentToBlock: _renderUnifiedLayers failed', e);
        }
        
        // Refresh layer manager panel if available
        if (this.refreshLayerManager) {
            this.refreshLayerManager();
        }
        
        // Trigger preview update
        try { this.triggerPreview && this.triggerPreview(true); } catch(e) { console.warn('[CC] triggerPreview failed', e); }
        
        // CRITICAL: Update save button AFTER all render operations complete
        // This ensures unsavedChanges hasn't been reset by any callbacks
        this.unsavedChanges = true; // Re-assert in case something reset it
        this.updateSaveButton && this.updateSaveButton();
        
        // Also force-enable the save button directly as a failsafe
        try {
            const saveBtn = document.getElementById('canvas-save-config');
            const globalSaveBtn = document.getElementById('ccve-global-save');
            if (saveBtn && this.currentContentBlockId) {
                saveBtn.disabled = false;
                saveBtn.title = 'Save visual configuration';
            }
            if (globalSaveBtn && this.currentContentBlockId) {
                globalSaveBtn.disabled = false;
            }
        } catch(_) {}
        
        console.info('[CC] applyCptContentToBlock: created', createdLayers.length, 'layers:', createdLayers);
        this.showNotification && this.showNotification(`✅ Imported ${createdLayers.length} layers: ${createdLayers.join(', ')}`, 'success');
    }

    /**
     * Show live preview of content block
     */
    async showLivePreview(blockId) { return apiShowLivePreview(this, blockId); }

    /**
     * Display live data preview modal
     */
    displayLiveDataPreview(liveDataResponse) {
        const { data } = liveDataResponse;
        const { content_block, live_data, data_source } = data;

        // Create modal content
        const modalContent = `
            <div class="live-data-modal">
                <div class="live-data-header">
                    <h3>Live Data Preview: ${this.escapeHtml(content_block.name)}</h3>
                    <p>Type: ${content_block.type}</p>
                    <p>Source: ${data_source.source} (${data_source.status})</p>
                </div>
                <div class="live-data-content">
                    <h4>Current Live Data:</h4>
                    <pre>${JSON.stringify(live_data, null, 2)}</pre>
                </div>
                <div class="live-data-actions">
                    <button type="button" id="refresh-live-data" data-block-id="${content_block.id}">Refresh Live Data</button>
                    <button type="button" id="close-live-preview">Close</button>
                </div>
            </div>
        `;

        // Show modal (you'd implement modal display logic here)
        this.showModal('Live Data Preview', modalContent);
        
        // Add event listeners
        document.getElementById('refresh-live-data')?.addEventListener('click', (e) => {
            this.showLivePreview(e.target.dataset.blockId);
        });
        
        document.getElementById('close-live-preview')?.addEventListener('click', () => {
            this.closeModal();
        });

        this.showNotification(`Live data loaded for ${content_block.name}`, 'success');
    }

    /**
     * Load container data and assignments
     */
    async loadContainerData() { return _ccLoad(this); }

    /**
     * Fetch containers from API and render shell items and canvas boxes
     */
    async fetchAndRenderContainers() { return _ccFetchRender(this); }

    /**
     * Render container cards/list in the management panel
     */
    renderContainersList(containers) { return _ccRenderList(this, containers); }

    /**
     * Render container boxes on the 1920x1080 canvas
     */
    renderContainersOnCanvas(containers) { return _ccRenderCanvas(this, containers); }

    /** Ensure #canvas-containers acts as a 1920x1080 logical stage */
    ensureCanvasStage() { return _ccEnsureStage(this); }

    /** Compute and apply scale transform so 1920x1080 logical stage fits visually */
    applyCanvasScale() { return _ccApplyScale(this); }

    // ==================== [REGION] ZONES GEOMETRY & EDITING ====================
    /** Enable/disable zone edit mode (adds handles and pointer events) */
    toggleZoneEditMode(enabled) {
        const root = document.getElementById('container-canvas');
        if (!root) return;
        if (enabled) root.classList.add('zone-edit-active'); else root.classList.remove('zone-edit-active');
        // Add or remove handles on each zone-rect
        document.querySelectorAll('.zones-overlay .zone-rect').forEach(el => {
            // ensure gear still clickable while editing
            el.style.pointerEvents = enabled ? 'auto' : 'none';
            if (enabled) {
                if (!el.querySelector('.zone-handle.tl')) {
                    ['tl','tr','bl','br'].forEach(k => {
                        const h = document.createElement('div');
                        h.className = `zone-handle ${k}`;
                        h.dataset.dir = k;
                        el.appendChild(h);
                    });
                }
                this.bindZoneEditInteractions(el);
            } else {
                el.querySelectorAll('.zone-handle').forEach(h => h.remove());
                el.onmousedown = null;
            }
        });
    }

    // (Removed duplicate bindBlockEditorInteractions override that broke drag/resize; original method earlier in class delegates correctly)
    
    /** Toggle grid overlay on the container canvas + per-zone snap grids */
    toggleCanvasGrid(show) {
        const wrapper = document.getElementById('container-canvas');
        if (!wrapper) return;
        if (show) wrapper.classList.add('show-grid'); else wrapper.classList.remove('show-grid');
        const zones = document.querySelectorAll('.zones-overlay .zone-rect');
        zones.forEach(z => {
            if (show) {
                const s = parseInt(z.getAttribute('data-grid-snap') || '0', 10) || this.canvasSnap || 8;
                z.style.setProperty('--zone-grid', `${s}px`);
                z.classList.add('show-grid');
            } else {
                z.classList.remove('show-grid');
            }
        });
    }

    /** Toggle grid overlay on the block stage surface */
    toggleBlockStageGrid(show) {
        const blockOuter = document.querySelector('#block-stage-wrapper .ccve-stage-outer');
        // Fallback: if dedicated stage wrapper missing, use preview container parent to ensure grid visibility
        const host = blockOuter || this.previewContainer?.parentElement;
        if (!host) return;
        // Ensure a dedicated grid layer exists (more reliable than pseudo-element across themes)
        let gridLayer = host.querySelector('.ccve-stage-grid');
        if (!gridLayer) {
            gridLayer = document.createElement('div');
            gridLayer.className = 'ccve-stage-grid';
            Object.assign(gridLayer.style, {
                position:'absolute', inset:'0', pointerEvents:'none',
                backgroundImage:'linear-gradient(rgba(255,255,255,0.08) 1px, transparent 1px), linear-gradient(90deg, rgba(255,255,255,0.08) 1px, transparent 1px)',
                backgroundSize:'8px 8px, 8px 8px',
                zIndex:'4'
            });
            host.appendChild(gridLayer);
        }
        if (show) {
            host.classList.add('show-grid');
            gridLayer.style.display = 'block';
        } else {
            host.classList.remove('show-grid');
            gridLayer.style.display = 'none';
        }
        try { console.debug('[CCVE] blockStageGrid', { show, gridLayerExists: !!gridLayer }); } catch(_) {}
    }

    /** Toggle rulers (horizontal & vertical) for current active surface */
    _toggleRulersInternal(show) {
        console.log('[CCVE] _toggleRulersInternal called - v2025-11-25, show:', show);
        const frame = document.getElementById('block-stage-wrapper');
        if (!frame) {
            console.log('[CCVE] _toggleRulersInternal - frame not found');
            return;
        }
        
        // Use the StageRulers module if available (from initStageModules)
        if (this._stageRulers) {
            console.log('[CCVE] _toggleRulersInternal - using StageRulers module');
            if (show) {
                this._stageRulers.show();
            } else {
                this._stageRulers.hide();
            }
            // Update frame class for CSS styling
            frame.classList.toggle('has-rulers', !!show);
            try { this.logGeometryDiagnostics && this.logGeometryDiagnostics('rulers-toggle'); } catch(_) {}
            return;
        }
        
        console.log('[CCVE] _toggleRulersInternal - StageRulers NOT available, using legacy');
        // Legacy fallback if StageRulers not initialized
        let host = frame.querySelector('.ccve-scale-host');
        if (host) host.classList.toggle('rulers-active', !!show);
        
        if (show && (!this._rulersApi || !frame.querySelector('.ccve-ruler-horizontal'))) {
            try {
                this._rulersApi = initRulers(frame, 1280, 720);
                try { this._rulersApi.updateBounds?.(this.canvasBounds?.w||1280, this.canvasBounds?.h||720); } catch(_){ }
                host = frame.querySelector('.ccve-scale-host');
            } catch(e) { console.warn('[CC] rulers reinit failed', e); }
        }
        
        try { this._rulersApi?.setVisible?.(!!show); } catch(_) {}
        // Note: Don't set overflow:hidden - rulers need to be visible outside canvas
        if (this._rulersApi && show) this._rulersApi.renderTicks();
        frame.classList.toggle('has-rulers', !!show);
        try { this.logGeometryDiagnostics && this.logGeometryDiagnostics('rulers-toggle'); } catch(_) {}
    }
    toggleRulers(show) { this._toggleRulersInternal(show); }
    // Legacy ruler helpers removed; handled via modules/rulers.js
    // (Removed orphaned zone selector helper fragment)

    /** Get selected zone id for a container from UI (if present) */
    getSelectedZoneId(containerId) {
        const sel = document.querySelector(`.zone-select[data-container-id="${containerId}"]`);
        return sel ? String(sel.value || '') : '';
    }

    /** Render zones overlay HTML for a container */
    renderZonesOverlayHtml(container) {
        const layout = container?.layout || {};
        const zones = Array.isArray(layout?.zones) ? layout.zones : [];
        if (!zones.length) return '';
        // If no activeZoneIds provided, treat all zones as active by default for OOTB visibility
        const active = new Set(
            Array.isArray(layout?.activeZoneIds) && layout.activeZoneIds.length
                ? layout.activeZoneIds
                : zones.map(z => z.id)
        );
        return `
            <div class="zones-overlay" style="pointer-events:none;">
                ${zones.map(z => `
                <div class="zone-rect ${active.has(z.id)?'active':''}"
                    data-zone-id="${this.escapeHtml(String(z.id))}"
                    data-grid-cols="${parseInt(z.grid?.cols ?? 12, 10)}"
                    data-grid-gutter="${parseInt(z.grid?.gutter ?? 24, 10)}"
                    data-grid-snap="${parseInt(z.grid?.snap ?? 8, 10)}"
                    style="left:${parseInt(z.rect?.x||0,10)}px;top:${parseInt(z.rect?.y||0,10)}px;width:${parseInt(z.rect?.w||0,10)}px;height:${parseInt(z.rect?.h||0,10)}px;">
                        <span class="zone-label">${this.escapeHtml(z.name||z.id)}</span>
                        <button type="button" class="zone-gear" title="Zone settings" style="pointer-events:auto;"
                            data-container-id="${this.escapeHtml(String(container.id))}"
                            data-zone-id="${this.escapeHtml(String(z.id))}">⚙️</button>
                    </div>
                `).join('')}
            </div>
        `;
    }

    /**
     * Load blocks assigned to a specific container
     */
    async loadContainerBlocks(containerId) {
    return _caLoadBlocks(this, containerId);
    }

    /** Pause/resume all preview timers when document visibility changes */
    handleVisibilityChange() {
        if (document.hidden) {
            // Pause all cycles
            this.stopAllPreviewCycles();
        } else {
            // Resume by rebuilding schedules for enabled containers
            const nodes = document.querySelectorAll('.canvas-container');
            nodes.forEach(node => {
                const cid = node.getAttribute('data-container-id');
                if (cid && String(node.dataset.enabled) === '1' && this.showContentPreview) {
                    this.loadContainerBlocks(cid);
                }
            });
        }
    }

    /** Persist container rect to API */
    persistContainerRect(id, rect) {
    return _ccPersistRect(this, id, rect);
    }

    /**
     * Render a live content preview into the 1920x1080 canvas box for a container.
     * Uses the first enabled assigned block (rotation order) and its saved visual config.
     */
    async renderContainerPreview(containerId, blocks) { return _cpRender(this, containerId, blocks); }
    // (method body extracted to modules/container-preview.js)

        // Honor the global toggle
    

    /** Admin self-heal: if Upper Third zones are empty, auto-assign defaults once (Location/Time left, Weather right) */
    async maybeSeedUpperThird(containerObj) { return _cpSeedUpper(this, containerObj); }

    /** Measure the intrinsic size of preview content and scale it to fit the outer box */
    fitInnerToOuter(inner, outer) { return _cpFitInner(this, inner, outer); }

    /** Fetch and render the active background into the canvas background layer */
    async renderActiveBackground() {
        const bgHost = document.querySelector('.canvas-background');
        if (!bgHost) return;
        try {
            const url = `${castconductorCanvasAjax.rest_url}castconductor/v5/backgrounds/active`;
            const resp = await fetch(url, { headers: { 'Content-Type': 'application/json', 'X-WP-Nonce': castconductorCanvasAjax.nonce } });
            if (!resp.ok) return; // keep existing fallback
            const data = await resp.json();
            const type = String(data?.type || 'color');
            const cfg = data?.config || {};
            bgHost.style.backgroundImage = '';
            bgHost.style.backgroundColor = '';
            bgHost.style.backgroundSize = '';
            bgHost.style.backgroundPosition = '';
            bgHost.style.backgroundRepeat = '';
            if (type === 'static' && cfg.image_url) {
                bgHost.style.backgroundImage = `url('${cfg.image_url}')`;
                bgHost.style.backgroundSize = (cfg.fit || 'cover');
                bgHost.style.backgroundPosition = 'center center';
                bgHost.style.backgroundRepeat = 'no-repeat';
            } else if (type === 'color') {
                const color = cfg.color || '#000000';
                if (cfg.gradient && Array.isArray(cfg.gradient_colors) && cfg.gradient_colors.length >= 2) {
                    const dir = cfg.gradient_direction || '135deg';
                    bgHost.style.backgroundImage = `linear-gradient(${dir}, ${cfg.gradient_colors[0]}, ${cfg.gradient_colors[1]})`;
                } else {
                    bgHost.style.backgroundColor = color;
                }
            } else if (type === 'rotating') {
                // For admin preview, use first image if available
                const img = Array.isArray(cfg.images) && cfg.images.length ? cfg.images[0] : null;
                if (img && img.url) {
                    bgHost.style.backgroundImage = `url('${img.url}')`;
                    bgHost.style.backgroundSize = (cfg.fit || 'cover');
                    bgHost.style.backgroundPosition = 'center center';
                    bgHost.style.backgroundRepeat = 'no-repeat';
                }
            }
        } catch (_) {
            // leave fallback gradient
        }
    }

    /** Start timed cycling of container preview through assigned blocks */
    startContainerPreviewCycle(containerId, blocks) { return _cpStart(this, containerId, blocks); }

    /** Stop cycling for a single container */
    stopContainerPreviewCycle(containerId) { return _cpStop(this, containerId); }

    // (Inlined bindBlockEditorInteractions removed – now delegated earlier to module)

    /**
     * Update rotation percentage for a single assignment via v5 endpoint
     */
    async updateAssignmentRotation(containerId, blockId, rotationPercentage) {
        try {
            const zoneId = this.getSelectedZoneId ? this.getSelectedZoneId(containerId) : '';
            if (zoneId) {
                const cur = await this.getZoneAssignments(containerId, zoneId);
                const next = (cur||[]).map(x => {
                    if (String(x.content_block_id) === String(blockId)) {
                        return { ...x, rotation_percentage: parseFloat(rotationPercentage) };
                    }
                    return x;
                });
                await this.putZoneAssignments(containerId, zoneId, next);
                return true;
            } else {
                const url = `${castconductorCanvasAjax.rest_url}castconductor/v5/containers/${containerId}/blocks/${blockId}`;
                const response = await fetch(url, {
                    method: 'PUT',
                    headers: {
                        'Content-Type': 'application/json',
                        'X-WP-Nonce': castconductorCanvasAjax.nonce
                    },
                    body: JSON.stringify({ rotation_percentage: parseFloat(rotationPercentage) })
                });
                if (!response.ok) {
                    const err = await this.safeJson(response);
                    this.showNotification(err?.message || `Failed to update rotation (HTTP ${response.status})`, 'error');
                    return false;
                }
                return true;
            }
        } catch (error) {
            console.error('Update rotation failed:', error);
            this.showNotification('Error updating rotation', 'error');
            return false;
        }
    }

    /**
     * Enable drag-and-drop reordering inside assigned blocks list for a container
     */
    enableAssignedReordering(containerId) {
        const list = document.getElementById(`assigned-blocks-${containerId}`);
        if (!list) return;

    let draggingEl = null;
    let internalDragging = false;

        list.querySelectorAll('.assigned-block').forEach(item => {
            item.addEventListener('dragstart', (e) => {
        draggingEl = item;
        internalDragging = true;
                item.classList.add('dragging');
                e.dataTransfer.effectAllowed = 'move';
            });
            item.addEventListener('dragend', () => {
                if (draggingEl) draggingEl.classList.remove('dragging');
                draggingEl = null;
        internalDragging = false;
            });
        });

        list.addEventListener('dragover', (e) => {
            e.preventDefault();
            const afterEl = this.getDragAfterElement(list, e.clientY);
            if (!draggingEl) return;
            if (afterEl == null) {
                list.appendChild(draggingEl);
            } else if (afterEl !== draggingEl) {
                list.insertBefore(draggingEl, afterEl);
            }
        });

        list.addEventListener('drop', async (e) => {
            e.preventDefault();
            // Only persist when this was an internal reorder operation
            if (internalDragging) {
                await this.persistAssignedOrder(containerId);
            }
        });
    }

    /**
     * Helper to determine insertion point during drag
     */
    getDragAfterElement(container, y) {
        const draggableElements = [...container.querySelectorAll('.assigned-block:not(.dragging)')];
        return draggableElements.reduce((closest, child) => {
            const box = child.getBoundingClientRect();
            const offset = y - box.top - box.height / 2;
            if (offset < 0 && offset > closest.offset) {
                return { offset, element: child };
            } else {
                return closest;
            }
        }, { offset: Number.NEGATIVE_INFINITY, element: null }).element;
    }

    /**
     * Persist the current DOM order to server via v5 reorder endpoint
     */
    async persistAssignedOrder(containerId) {
        try {
            const list = document.getElementById(`assigned-blocks-${containerId}`);
            if (!list) return;
            const sequence = Array.from(list.querySelectorAll('.assigned-block')).map(el => parseInt(el.dataset.blockId, 10));
            const zoneId = this.getSelectedZoneId ? this.getSelectedZoneId(containerId) : '';
            if (zoneId) {
                const cur = await this.getZoneAssignments(containerId, zoneId);
                const orderById = {};
                sequence.forEach((id, idx) => { orderById[String(id)] = idx + 1; });
                const next = (cur||[]).slice().sort((a,b) => (orderById[String(a.content_block_id)]||9999) - (orderById[String(b.content_block_id)]||9999))
                    .map(row => ({ ...row, rotation_order: orderById[String(row.content_block_id)] || row.rotation_order }));
                await this.putZoneAssignments(containerId, zoneId, next);
                this.showNotification('Order saved', 'success');
                return true;
            } else {
                const url = `${castconductorCanvasAjax.rest_url}castconductor/v5/containers/${containerId}/blocks/reorder`;
                const response = await fetch(url, {
                    method: 'PATCH',
                    headers: {
                        'Content-Type': 'application/json',
                        'X-WP-Nonce': castconductorCanvasAjax.nonce
                    },
                    body: JSON.stringify({ sequence })
                });
                if (!response.ok) {
                    const err = await this.safeJson(response);
                    this.showNotification(err?.message || `Failed to reorder (HTTP ${response.status})`, 'error');
                    return false;
                }
                this.showNotification('Order saved', 'success');
                return true;
            }
        } catch (error) {
            console.error('Persist order failed:', error);
            this.showNotification('Error saving order', 'error');
            return false;
        }
    }

    /**
     * Toggle container enabled/disabled state
     */
    async toggleContainer(containerId, enabled) {
        try {
            const response = await fetch(`${castconductorCanvasAjax.rest_url}castconductor/v5/containers/${containerId}`, {
                method: 'PUT',
                headers: {
                    'Content-Type': 'application/json',
                    'X-WP-Nonce': castconductorCanvasAjax.nonce
                },
                body: JSON.stringify({
                    enabled: enabled ? 1 : 0
                })
            });

            if (!response.ok) {
                throw new Error(`HTTP error! status: ${response.status}`);
            }

            // Update visual state
            const canvasContainer = document.querySelector(`[data-container-id="${containerId}"]`);
            if (canvasContainer) {
                canvasContainer.dataset.enabled = enabled ? '1' : '0';
                if (enabled) {
                    canvasContainer.style.opacity = '1';
                } else {
                    canvasContainer.style.opacity = '0.5';
                    const host = canvasContainer.querySelector(`#container-content-${containerId}`) || document.getElementById(`container-content-${containerId}`);
                    if (host) host.innerHTML = '';
                    if (this.removeContainerPreviewStyles) this.removeContainerPreviewStyles(containerId);
                }
            }

            this.showNotification(`Container ${enabled ? 'enabled' : 'disabled'} successfully`, 'success');
            // Manage preview timers
            if (!enabled) {
                this.stopContainerPreviewCycle(containerId);
            } else if (this.showContentPreview) {
                this.loadContainerBlocks(containerId);
            }
        } catch (error) {
            console.error('Error toggling container:', error);
            this.showNotification('Error updating container state', 'error');
        }
    }

    /**
     * Setup drag and drop for content blocks
     */
    setupDragAndDrop() {
        if (this.dragListenersBound) return; // Prevent duplicate bindings
        this.dragListenersBound = true;

        // Make available content block cards draggable (match Containers tab markup)
        document.addEventListener('dragstart', (e) => {
            const card = e.target.closest('.available-block');
            if (card && card.dataset.blockId) {
                card.classList.add('dragging');
                e.dataTransfer.setData('text/plain', JSON.stringify({
                    blockId: card.dataset.blockId,
                    blockType: card.dataset.blockType
                }));
                e.dataTransfer.effectAllowed = 'copy';
            }
        });

        document.addEventListener('dragend', (e) => {
            const card = e.target.closest('.available-block');
            if (card) {
                card.classList.remove('dragging');
            }
        });

        // Set up drop zones (assigned blocks areas)
        document.addEventListener('dragover', (e) => {
            const zone = e.target.closest('.assigned-blocks');
            if (zone) {
                e.preventDefault();
                zone.classList.add('drag-over');
            }
        });

        document.addEventListener('dragleave', (e) => {
            const zone = e.target.closest('.assigned-blocks');
            if (zone) {
                zone.classList.remove('drag-over');
            }
        });

    document.addEventListener('drop', (e) => {
            const zone = e.target.closest('.assigned-blocks');
            if (zone) {
                e.preventDefault();
        e.stopPropagation();
                zone.classList.remove('drag-over');
                let data = null;
                try {
                    const raw = e.dataTransfer.getData('text/plain');
                    if (raw) data = JSON.parse(raw);
                } catch (_) {}
                if (!data || !data.blockId) return;
                const containerId = zone.id.replace('assigned-blocks-', '');
                const current = this.getContainerRotationTotal(containerId);
                if (current >= 100) {
                    this.showNotification('Rotation is already at 100% for this container', 'error');
                    return;
                }
                const defaultPct = 25;
                const pct = Math.min(defaultPct, Math.max(0, 100 - current));
                if (pct <= 0) {
                    this.showNotification('Cannot assign: rotation cap reached', 'error');
                    return;
                }
                this.assignBlockToContainer(containerId, data.blockId, pct);
            }
        });
    }

    // ==================== [REGION] DRAG & RESIZE (Macro Canvas) ====================
    /** Bind drag and resize interactions on the 1920x1080 macro canvas */
    bindCanvasInteractions() { return _macroBindCanvas(this); }

    /** Selection and keyboard nudge bindings */
    bindContainerSelection() { return _macroBindSelection(this); }

    /** Minimal per-zone grid settings editor (cols/gutter/snap) */
    async openZoneGridSettings(containerId, zoneId) { return zonesOpenGrid(this, containerId, zoneId); }
    async fetchContainerLayout(containerId) { return zonesFetchLayout(containerId); }
    async putContainerLayout(containerId, layout) { return zonesPutLayout(containerId, layout); }

    /** Mark one container as selected for keyboard operations */
    setSelectedContainer(id) {
        // Clear previous
        if (this.selectedContainerId) {
            const prev = document.querySelector(`.canvas-container[data-container-id="${this.selectedContainerId}"]`);
            if (prev) prev.classList.remove('selected');
        }
        this.selectedContainerId = id ? String(id) : null;
        if (this.selectedContainerId) {
            const curr = document.querySelector(`.canvas-container[data-container-id="${this.selectedContainerId}"]`);
            if (curr) curr.classList.add('selected');
        }
    }

    /**
     * Assign a content block to a container
     */
    async assignBlockToContainer(containerId, blockId, rotationPercentage = 25) {
    return _caAssignBlock(this, containerId, blockId, rotationPercentage);
    }

    /**
     * Remove block assignment from container
     */
    async removeBlockAssignment(containerId, blockId) {
    return _caRemoveBlock(this, containerId, blockId);
    }

    /** Fetch zone assignments helper */
    async getZoneAssignments(containerId, zoneId) {
    return _caGetZoneAssignments(this, containerId, zoneId);
    }

    /** PUT full zone assignments list */
    async putZoneAssignments(containerId, zoneId, list) {
    return _caPutZoneAssignments(this, containerId, zoneId, list);
    }

    /**
     * Refresh the container canvas display
     */
    refreshContainerCanvas() {
        this.loadContainerData();
        this.showNotification('Container canvas refreshed', 'success');
    }

    /**
     * Show block assignment dialog
     */
    showBlockAssignmentDialog(containerId) {
    // ==================== [REGION] ASSIGNMENTS & SCHEDULING ====================
        this.showAssignSelectorModal(containerId);
    }

    /**
     * Show modal selector to assign a block with rotation, with client-side guardrails
     */
    async showAssignSelectorModal(containerId) {
        try {
            const resp = await fetch(`${castconductorCanvasAjax.rest_url}castconductor/v5/content-blocks`, {
                method: 'GET', headers: { 'X-WP-Nonce': castconductorCanvasAjax.nonce }
            });
            if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
            const blocks = await resp.json();

            const options = (blocks || []).map(b => `
                <label class="cc-radio-row">
                    <input type="radio" name="cc-assign-block-id" value="${this.escapeHtml(String(b.id))}" />
                    <span class="cc-radio-text">
                        <strong>${this.escapeHtml(b.name || `Block #${b.id}`)}</strong>
                        <em>${this.escapeHtml(b.type || '')}</em>
                    </span>
                </label>
            `).join('') || '<div class="no-content-blocks">No content blocks available</div>';

            const body = `
                <form id="cc-assign-form" class="cc-form">
                    <div class="cc-form-row">
                        <label>Rotation Percentage</label>
                        <input type="number" id="cc-assign-rotation" value="25" min="0" max="100" step="1" />
                        <div class="cc-help">Total must not exceed 100%</div>
                        <div class="cc-rotation-total">Current total: <span id="cc-rotation-total-val">${this.getContainerRotationTotal(containerId)}</span>%</div>
                    </div>
                    <div class="cc-form-row">
                        <label>Select Content Block</label>
                        <div class="cc-radio-list">${options}</div>
                    </div>
                    <div class=\"cc-form-row\">
                        <label>Preview</label>
                        <div id=\"cc-assign-preview\" class=\"cc-assign-preview\">Select a block to see a quick preview</div>
                    </div>
                    <div class="cc-form-actions">
                        <button type="button" id="cc-btn-cancel-assign">Cancel</button>
                        <button type="submit" class="primary">Assign</button>
                    </div>
                </form>
            `;

            this.showModal('Assign Content Block', body);
            // Add scoping class to modal body to keep preview CSS contained
            const modal = document.getElementById('canvas-editor-modal');
            modal?.querySelector('.canvas-modal-body')?.classList.add('cc-assign-scope');
            document.getElementById('cc-btn-cancel-assign')?.addEventListener('click', () => this.closeModal());

            const rotationInput = document.getElementById('cc-assign-rotation');
            rotationInput?.addEventListener('input', () => {
                const totalEl = document.getElementById('cc-rotation-total-val');
                if (totalEl) totalEl.textContent = String(this.getContainerRotationTotal(containerId));
            });

            // Bind preview on radio select
            // Debounced preview
            let prevTimer = null;
            const triggerPreview = (id) => {
                if (prevTimer) clearTimeout(prevTimer);
                prevTimer = setTimeout(() => this.renderAssignModalPreview(id), 250);
            };

            document.querySelectorAll('input[name="cc-assign-block-id"]').forEach(r => {
                r.addEventListener('change', async (ev) => {
                    const id = ev.target.value;
                    triggerPreview(id);
                });
            });

            document.getElementById('cc-assign-form')?.addEventListener('submit', async (e) => {
                e.preventDefault();
                const rotation = parseFloat(document.getElementById('cc-assign-rotation')?.value || '0');
                const selected = document.querySelector('input[name="cc-assign-block-id"]:checked');
                if (!selected) {
                    this.showNotification('Select a content block first', 'error');
                    return;
                }
                const current = this.getContainerRotationTotal(containerId);
                if ((current + rotation) > 100) {
                    this.showNotification(`Rotation total would exceed 100% (current ${current}%)`, 'error');
                    return;
                }
                await this.assignBlockToContainer(containerId, selected.value, rotation);
                this.closeModal();
            });
        } catch (err) {
            console.error('Open assign selector failed:', err);
            this.showNotification('Failed to open assignment selector', 'error');
        }
    }

    /**
     * Render inline preview inside the Assign modal for a selected block
     */
    async renderAssignModalPreview(blockId) {
        const previewEl = document.getElementById('cc-assign-preview');
        if (!previewEl) return;
        previewEl.innerHTML = 'Loading preview…';
        try {
            const resp = await fetch(`${castconductorCanvasAjax.rest_url}castconductor/v5/content-blocks/${blockId}/live-data`, {
                method: 'GET',
                headers: { 'Content-Type': 'application/json', 'X-WP-Nonce': castconductorCanvasAjax.nonce }
            });
            if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
            const data = await resp.json();
            const cb = data?.data?.content_block || {};
            const live = data?.data?.live_data || {};
            const source = data?.data?.data_source || {};
            const pretty = this.escapeHtml(JSON.stringify(live, null, 2)).slice(0, 600);
            // Basic JSON view
            let html = `
                <div class=\"cc-preview-card\">
                    <div class=\"cc-preview-meta\"><strong>${this.escapeHtml(cb.name || ('Block #' + blockId))}</strong> <em>${this.escapeHtml(cb.type || '')}</em></div>
                    <div class=\"cc-preview-source\">Source: ${this.escapeHtml(source.source || 'n/a')} (${this.escapeHtml(source.status || '')})</div>
                    <pre class=\"cc-preview-json\">${pretty}${pretty.length>=600?'\n…':''}</pre>
                </div>
            `;

            // Optional canvas-style visual preview using preview endpoint by block_id (Option C)
            if (blockId) {
                try {
                    const pv = await fetch(`${castconductorCanvasAjax.rest_url}castconductor/v5/canvas-editor/preview`, {
                        method: 'POST',
                        headers: { 'Content-Type': 'application/json', 'X-WP-Nonce': castconductorCanvasAjax.nonce },
                        body: JSON.stringify({ ...(this.currentConfig || this.getDefaultConfig()), block_id: parseInt(blockId,10) })
                    });
                    if (pv.ok) {
                        const pvData = await pv.json();
                        if (pvData?.success && pvData.preview?.html) {
                            // Scope the CSS uniquely to avoid colliding with page; reuse existing style tag or create temp
                            const boxId = `cc-assign-visual-${blockId}`;
                            const styleId = `cc-assign-visual-style`; // shared style for modal OK here
                            let styleTag = document.getElementById(styleId);
                            if (!styleTag) {
                                styleTag = document.createElement('style');
                                styleTag.id = styleId;
                                document.head.appendChild(styleTag);
                            }
                            // Insert CSS and wrap HTML in a small box
                            styleTag.textContent = pvData.preview.css;
                            html += `
                                <div class=\"cc-preview-visual\" id=\"${boxId}\">${pvData.preview.html}</div>
                            `;
                        }
                    }
                } catch (e) {
                    // Non-blocking failure; keep JSON fallback
                }
            }

            previewEl.innerHTML = html;
        } catch (err) {
            previewEl.textContent = 'Preview unavailable.';
            console.error('Preview fetch failed:', err);
        }
    }

    /**
     * Compute rotation total for a container from DOM inputs
     */
    getContainerRotationTotal(containerId) {
        const list = document.getElementById(`assigned-blocks-${containerId}`);
        if (!list) return 0;
        let total = 0;
        list.querySelectorAll('.rotation-input').forEach(input => {
            const v = parseFloat(input.value || '0');
            if (Number.isFinite(v)) total += v;
        });
        return Math.round(total);
    }

    /**
     * Update or create a small rotation total indicator near the assignments list
     */
    updateRotationTotalIndicator(containerId) {
        const list = document.getElementById(`assigned-blocks-${containerId}`);
        if (!list) return;
        const parent = list.parentElement;
        if (!parent) return;
        let indicator = parent.querySelector('.rotation-total-indicator');
        if (!indicator) {
            indicator = document.createElement('div');
            indicator.className = 'rotation-total-indicator';
            const assignBtn = parent.querySelector('.assign-block-btn');
            if (assignBtn && assignBtn.parentNode) {
                parent.insertBefore(indicator, assignBtn);
            } else {
                parent.appendChild(indicator);
            }
        }
        const total = this.getContainerRotationTotal(containerId);
        indicator.textContent = `Total rotation: ${total}%`;
        indicator.style.color = total > 100 ? '#c00' : (total === 100 ? '#0a0' : '');
    }

    /**
     * Save all container changes
     */
    saveAllContainerChanges() {
        this.showNotification('All container changes saved', 'success');
    }

    /**
     * Create new container
     */
    createNewContainer() {
        const modalBody = `
            <form id="cc-container-form" class="cc-form">
                <div class="cc-form-row">
                    <label>Name</label>
                    <input type="text" id="cc-field-name" placeholder="Lower Third" value="Lower Third" required />
                </div>
                <div class="cc-form-row">
                    <label>Position Preset</label>
                    <select id="cc-field-position">
                        <option value="lower_third">Lower Third</option>
                        <option value="upper_third">Upper Third</option>
                        <option value="full_screen">Full Screen</option>
                        <option value="left_sidebar">Left Sidebar</option>
                        <option value="right_sidebar">Right Sidebar</option>
                        <option value="custom">Custom</option>
                    </select>
                </div>
                <div class="cc-form-grid">
                    <div class="cc-form-row"><label>Width</label><input type="number" id="cc-field-width" value="1280" min="1" max="1280" required /></div>
                    <div class="cc-form-row"><label>Height</label><input type="number" id="cc-field-height" value="240" min="1" max="720" required /></div>
                </div>
                <div class="cc-form-grid">
                    <div class="cc-form-row"><label>X</label><input type="number" id="cc-field-x" value="0" min="0" /></div>
                    <div class="cc-form-row">
                        <label>X Preset</label>
                        <select id="cc-field-x-preset">
                            <option value="0" selected>Left (0)</option>
                            <option value="center">Center (640)</option>
                            <option value="right">Right Edge (1280 - width)</option>
                        </select>
                    </div>
                </div>
                <div class="cc-form-grid">
                    <div class="cc-form-row"><label>Y</label><input type="number" id="cc-field-y" value="480" min="0" /></div>
                    <div class="cc-form-row">
                        <label>Y Preset</label>
                        <select id="cc-field-y-preset">
                            <option value="0">Top (0)</option>
                            <option value="center">Middle (360)</option>
                            <option value="bottom" selected>Bottom Edge (720 - height)</option>
                        </select>
                    </div>
                </div>
                <div class="cc-form-grid">
                    <div class="cc-form-row"><label>Z-Index</label><input type="number" id="cc-field-z" value="10" min="0" max="999" /></div>
                    <div class="cc-form-row">
                        <label>Z Preset</label>
                        <select id="cc-field-z-preset">
                            <option value="content" selected>Content Layer (10)</option>
                            <option value="background">Background (0)</option>
                            <option value="overlay">Overlay (200)</option>
                        </select>
                    </div>
                </div>
                <div class="cc-form-row"><label>Rotation Enabled</label><input type="checkbox" id="cc-field-rotation-enabled" checked /></div>
                <div class="cc-form-row">
                    <label>Rotation Interval (sec)</label>
                    <input type="number" id="cc-field-rotation-interval" value="30" min="5" max="300" />
                </div>
                <fieldset class="cc-fieldset">
                    <legend>Background (optional)</legend>
                    <div class="cc-form-row">
                        <label>Type</label>
                        <select id="cc-bg-type">
                            <option value="color">Color</option>
                            <option value="static">Static Image</option>
                        </select>
                    </div>
                    <div class="cc-form-row">
                        <label>Color (hex)</label>
                        <input type="text" id="cc-bg-color" placeholder="#000000" />
                    </div>
                                        <div class="cc-form-row">
                                                <label>Image URL</label>
                                                <div style="display:flex;gap:8px;align-items:center">
                                                    <input type="url" id="cc-bg-image-url" placeholder="https://.../image.jpg" style="flex:1" />
                                                    <button type="button" id="cc-bg-select-media">Media…</button>
                                                </div>
                                        </div>
                    <div class="cc-form-grid">
                        <div class="cc-form-row"><label>BG Z-Index</label><input type="number" id="cc-bg-z" value="0" min="0" max="999" /></div>
                        <div class="cc-form-row"><label>BG Rotation Interval</label><input type="number" id="cc-bg-rotation-interval" value="30" min="5" max="300" /></div>
                    </div>
                </fieldset>
                <div class="cc-form-actions">
                    <button type="button" id="cc-btn-cancel-create">Cancel</button>
                    <button type="submit" id="cc-btn-save-create" class="primary">Create</button>
                </div>
            </form>
        `;
        this.showModal('Create Container', modalBody);

        // Bind handlers
        document.getElementById('cc-btn-cancel-create')?.addEventListener('click', () => this.closeModal());
        document.getElementById('cc-container-form')?.addEventListener('submit', async (e) => {
            e.preventDefault();
            await this.submitCreateContainer();
        });
    // Enhance with presets after modal is in DOM
    this.bindContainerFormEnhancements();
    }

    /**
     * Edit container
     */
    editContainer(containerId) {
        this.openEditContainerModal(containerId);
    }

    /**
     * Submit create container to API
     */
    async submitCreateContainer() {
        try {
            const payload = this.collectContainerFormPayload();
            const response = await fetch(`${castconductorCanvasAjax.rest_url}castconductor/v5/containers`, {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json',
                    'X-WP-Nonce': castconductorCanvasAjax.nonce
                },
                body: JSON.stringify(payload)
            });

            if (!response.ok) {
                const err = await this.safeJson(response);
                throw new Error(err?.message || `HTTP ${response.status}`);
            }

            this.showNotification('Container created', 'success');
            this.closeModal();
            await this.fetchAndRenderContainers();
            await this.loadContainerData();
        } catch (error) {
            console.error('Create container failed:', error);
            this.showNotification(error.message || 'Failed to create container', 'error');
        }
    }

    /**
     * Open edit modal, fetch and populate
     */
    async openEditContainerModal(containerId) {
        try {
            const url = `${castconductorCanvasAjax.rest_url}castconductor/v5/containers/${containerId}`;
            const response = await fetch(url, {
                method: 'GET',
                headers: { 'X-WP-Nonce': castconductorCanvasAjax.nonce }
            });
            if (!response.ok) {
                const text = await response.text();
                throw new Error(`HTTP ${response.status}: ${text.slice(0,80)}`);
            }
            let container;
            const raw = await response.text();
            try {
                container = JSON.parse(raw);
            } catch (e) {
                throw new Error(`Invalid JSON from ${url}: ${raw.slice(0,80)}`);
            }

            const modalBody = this.renderContainerEditForm(container);
            this.showModal(`Edit Container #${containerId}`, modalBody);

            document.getElementById('cc-btn-cancel-edit')?.addEventListener('click', () => this.closeModal());
            document.getElementById('cc-container-edit-form')?.addEventListener('submit', async (e) => {
                e.preventDefault();
                await this.submitEditContainer(containerId);
            });
            document.getElementById('cc-btn-delete-container')?.addEventListener('click', async () => {
                await this.deleteContainer(containerId);
            });
            // Enhance with presets after modal is in DOM
            this.bindContainerFormEnhancements();
        } catch (error) {
            console.error('Open edit modal failed:', error);
            this.showNotification('Failed to load container details', 'error');
        }
    }

    /**
     * Render edit form HTML
     */
    renderContainerEditForm(c) {
        const zVal = parseInt(c.z_index, 10) || 0;
        const zPreset = zVal >= 200 ? 'overlay' : (zVal === 0 ? 'background' : 'content');
        return `
            <form id="cc-container-edit-form" class="cc-form">
                <div class="cc-form-row">
                    <label>Name</label>
                    <input type="text" id="cc-field-name" value="${this.escapeHtml(c.name || '')}" required />
                </div>
                <div class="cc-form-row">
                    <label>Position Preset</label>
                    <select id="cc-field-position">
                        ${['lower_third','upper_third','full_screen','left_sidebar','right_sidebar','custom'].map(p => `<option value="${p}" ${c.position===p?'selected':''}>${p.replace('_',' ').replace(/\b\w/g, m=>m.toUpperCase())}</option>`).join('')}
                    </select>
                </div>
                <div class="cc-form-grid">
                    <div class="cc-form-row"><label>Width</label><input type="number" id="cc-field-width" value="${parseInt(c.width,10)||0}" min="1" max="1920" required /></div>
                    <div class="cc-form-row"><label>Height</label><input type="number" id="cc-field-height" value="${parseInt(c.height,10)||0}" min="1" max="1080" required /></div>
                </div>
                <div class="cc-form-grid">
                    <div class="cc-form-row"><label>X</label><input type="number" id="cc-field-x" value="${parseInt(c.x_position,10)||0}" min="0" /></div>
                    <div class="cc-form-row">
                        <label>X Preset</label>
                        <select id="cc-field-x-preset">
                            <option value="0" ${((parseInt(c.x_position,10)||0)===0)?'selected':''}>Left (0)</option>
                            <option value="center">Center (960)</option>
                            <option value="right">Right Edge (1920 - width)</option>
                        </select>
                    </div>
                </div>
                <div class="cc-form-grid">
                    <div class="cc-form-row"><label>Y</label><input type="number" id="cc-field-y" value="${parseInt(c.y_position,10)||0}" min="0" /></div>
                    <div class="cc-form-row">
                        <label>Y Preset</label>
                        <select id="cc-field-y-preset">
                            <option value="0" ${((parseInt(c.y_position,10)||0)===0)?'selected':''}>Top (0)</option>
                            <option value="center">Middle (540)</option>
                            <option value="bottom">Bottom Edge (1080 - height)</option>
                        </select>
                    </div>
                </div>
                <div class="cc-form-grid">
                    <div class="cc-form-row"><label>Z-Index</label><input type="number" id="cc-field-z" value="${parseInt(c.z_index,10)||0}" min="0" max="999" /></div>
                    <div class="cc-form-row">
                        <label>Z Preset</label>
                        <select id="cc-field-z-preset">
                            <option value="content" ${zPreset==='content'?'selected':''}>Content Layer (10)</option>
                            <option value="background" ${zPreset==='background'?'selected':''}>Background (0)</option>
                            <option value="overlay" ${zPreset==='overlay'?'selected':''}>Overlay (200)</option>
                        </select>
                    </div>
                </div>
                <div class="cc-form-row"><label>Rotation Enabled</label><input type="checkbox" id="cc-field-rotation-enabled" ${c.rotation_enabled? 'checked':''} /></div>
                <div class="cc-form-row">
                    <label>Rotation Interval (sec)</label>
                    <input type="number" id="cc-field-rotation-interval" value="${parseInt(c.rotation_interval,10)||30}" min="5" max="300" />
                </div>
                <fieldset class="cc-fieldset">
                    <legend>Background</legend>
                    <div class="cc-form-row">
                        <label>Type</label>
                        <select id="cc-bg-type">
                            ${['color','static'].map(p => `<option value="${p}" ${(c.background?.type||'color')===p?'selected':''}>${p}</option>`).join('')}
                        </select>
                    </div>
                    <div class="cc-form-row">
                        <label>Color (hex)</label>
                        <input type="text" id="cc-bg-color" value="${this.escapeHtml(c.background?.color || '#000000')}" />
                    </div>
                    <div class="cc-form-row">
                        <label>Image URL</label>
                        <input type="url" id="cc-bg-image-url" value="${this.escapeHtml(c.background?.image_url || '')}" />
                    </div>
                    <div class="cc-form-grid">
                        <div class="cc-form-row"><label>BG Z-Index</label><input type="number" id="cc-bg-z" value="${parseInt(c.background?.z_index,10)||0}" min="0" max="999" /></div>
                        <div class="cc-form-row"><label>BG Rotation Interval</label><input type="number" id="cc-bg-rotation-interval" value="${parseInt(c.background?.rotation_interval,10)||30}" min="5" max="300" /></div>
                    </div>
                </fieldset>
                <fieldset class="cc-fieldset">
                    <legend>Overlay / Navigation</legend>
                    <div class="cc-form-row">
                        <label>Display Mode</label>
                        <select id="cc-overlay-display-mode">
                            <option value="visible" ${(c.overlay?.display_mode||'visible')==='visible'?'selected':''}>Always Visible</option>
                            <option value="overlay" ${(c.overlay?.display_mode)==='overlay'?'selected':''}>Overlay (triggered by remote)</option>
                        </select>
                    </div>
                    <div class="cc-form-row cc-overlay-fields" style="display:${(c.overlay?.display_mode)==='overlay'?'block':'none'}">
                        <label>Trigger Button</label>
                        <select id="cc-overlay-trigger">
                            ${['up','down','left','right','ok','menu','back','play','replay','none'].map(t => `<option value="${t}" ${(c.overlay?.trigger||'none')===t?'selected':''}>${t.charAt(0).toUpperCase()+t.slice(1)}</option>`).join('')}
                        </select>
                    </div>
                    <div class="cc-form-row cc-overlay-fields" style="display:${(c.overlay?.display_mode)==='overlay'?'block':'none'}">
                        <label>Position</label>
                        <select id="cc-overlay-position">
                            ${['bottom','top','left','right','fullscreen','center'].map(p => `<option value="${p}" ${(c.overlay?.position||'bottom')===p?'selected':''}>${p.charAt(0).toUpperCase()+p.slice(1)}</option>`).join('')}
                        </select>
                    </div>
                    <div class="cc-form-row cc-overlay-fields" style="display:${(c.overlay?.display_mode)==='overlay'?'block':'none'}">
                        <label>Animation</label>
                        <select id="cc-overlay-animation">
                            ${['slide','fade','none'].map(a => `<option value="${a}" ${(c.overlay?.animation||'slide')===a?'selected':''}>${a.charAt(0).toUpperCase()+a.slice(1)}</option>`).join('')}
                        </select>
                    </div>
                    <div class="cc-form-row cc-overlay-fields" style="display:${(c.overlay?.display_mode)==='overlay'?'block':'none'}">
                        <label>Background Color</label>
                        <input type="text" id="cc-overlay-bg-color" value="${this.escapeHtml(c.overlay?.background_color || '#000000CC')}" placeholder="#000000CC" />
                    </div>
                    <div class="cc-form-row cc-overlay-fields" style="display:${(c.overlay?.display_mode)==='overlay'?'block':'none'}">
                        <label>Dismiss on Select</label>
                        <input type="checkbox" id="cc-overlay-dismiss-on-select" ${(c.overlay?.dismiss_on_select !== false)?'checked':''} />
                    </div>
                    <div class="cc-form-row cc-overlay-fields" style="display:${(c.overlay?.display_mode)==='overlay'?'block':'none'}">
                        <label>Auto-dismiss (sec, 0=off)</label>
                        <input type="number" id="cc-overlay-auto-dismiss" value="${parseInt(c.overlay?.auto_dismiss_seconds,10)||0}" min="0" max="300" />
                    </div>
                </fieldset>
                <div class="cc-form-actions">
                    <button type="button" id="cc-btn-cancel-edit">Cancel</button>
                    <button type="submit" id="cc-btn-save-edit" class="primary">Save</button>
                    <button type="button" id="cc-btn-delete-container" class="danger">Delete</button>
                </div>
            </form>
        `;
    }

    /**
     * Collect form values as payload (for create/edit)
     */
    collectContainerFormPayload() {
        const getNum = (id, def=0) => {
            const v = parseInt(document.getElementById(id)?.value || def, 10);
            return Number.isFinite(v) ? v : def;
        };
        const name = document.getElementById('cc-field-name')?.value?.trim() || '';
        if (!name) throw new Error('Name is required');
        const payload = {
            name,
            position: document.getElementById('cc-field-position')?.value || 'custom',
            width: getNum('cc-field-width', 1280),
            height: getNum('cc-field-height', 240),
            x_position: getNum('cc-field-x', 0),
            y_position: getNum('cc-field-y', 0),
            z_index: getNum('cc-field-z', 0),
            rotation_enabled: !!document.getElementById('cc-field-rotation-enabled')?.checked,
            rotation_interval: getNum('cc-field-rotation-interval', 30)
        };

        // Optional background fields
    const bgType = document.getElementById('cc-bg-type')?.value;
    const bgColor = document.getElementById('cc-bg-color')?.value?.trim();
    const bgImage = document.getElementById('cc-bg-image-url')?.value?.trim();
        const bgZ = getNum('cc-bg-z', 0);
        const bgRot = getNum('cc-bg-rotation-interval', 30);
        const hasBg = !!(bgColor || bgImage);
        if (hasBg) {
            payload.background = {
                type: (bgImage ? 'static' : (bgColor ? 'color' : (bgType || 'color'))),
                color: bgColor || undefined,
                image_url: bgImage || undefined,
                z_index: bgZ,
                rotation_interval: bgRot
            };
        }

        // Overlay / Navigation configuration
        const overlayDisplayMode = document.getElementById('cc-overlay-display-mode')?.value || 'visible';
        payload.overlay = {
            display_mode: overlayDisplayMode,
            trigger: document.getElementById('cc-overlay-trigger')?.value || 'none',
            position: document.getElementById('cc-overlay-position')?.value || 'bottom',
            animation: document.getElementById('cc-overlay-animation')?.value || 'slide',
            background_color: document.getElementById('cc-overlay-bg-color')?.value?.trim() || '#000000CC',
            dismiss_on_select: !!document.getElementById('cc-overlay-dismiss-on-select')?.checked,
            auto_dismiss_seconds: getNum('cc-overlay-auto-dismiss', 0)
        };

    // Inject token layer side channel (non-destructive) for persistence
    try { this._augmentSavePayload && this._augmentSavePayload(payload); } catch(_) {}
    return payload;
    }

    /**
     * Submit edit container to API
     */
    async submitEditContainer(containerId) {
        try {
            const payload = this.collectContainerFormPayload();
            const response = await fetch(`${castconductorCanvasAjax.rest_url}castconductor/v5/containers/${containerId}`, {
                method: 'PUT',
                headers: {
                    'Content-Type': 'application/json',
                    'X-WP-Nonce': castconductorCanvasAjax.nonce
                },
                body: JSON.stringify(payload)
            });

            if (!response.ok) {
                const err = await this.safeJson(response);
                throw new Error(err?.message || `HTTP ${response.status}`);
            }

            this.showNotification('Container updated', 'success');
            this.closeModal();
            await this.fetchAndRenderContainers();
            await this.loadContainerData();
        } catch (error) {
            console.error('Update container failed:', error);
            this.showNotification(error.message || 'Failed to update container', 'error');
        }
    }

    /**
     * Delete container
     */
    async deleteContainer(containerId) {
        if (!containerId) return;
        if (!confirm('Delete this container? This cannot be undone.')) return;

        try {
            const response = await fetch(`${castconductorCanvasAjax.rest_url}castconductor/v5/containers/${containerId}`, {
                method: 'DELETE',
                headers: {
                    'Content-Type': 'application/json',
                    'X-WP-Nonce': castconductorCanvasAjax.nonce
                }
            });

            if (!response.ok) {
                const err = await this.safeJson(response);
                throw new Error(err?.message || `HTTP ${response.status}`);
            }

            this.showNotification('Container deleted', 'success');
            this.closeModal();
            await this.fetchAndRenderContainers();
            // Assignments containers will be refreshed by loadContainerData
            await this.loadContainerData();
        } catch (error) {
            console.error('Delete container failed:', error);
            this.showNotification(error.message || 'Failed to delete container', 'error');
        }
    }

    /**
     * Safe JSON extractor for error responses
     */
    async safeJson(response) { return fetchSafeJson(response); }

    // showModal / closeModal now attached via attachModalSystem (extracted module)

    /**
     * Bind container form enhancements (position and z-index presets)
     */
    // bindContainerFormEnhancements extracted to container-form-enhancements module

    // escapeHtml migrated to utils.escapeHtml (retain method for compatibility)
    escapeHtml(text) { return utilEscapeHtml(text); }

    /**
     * Setup background management functionality
     */
    // setupBackgroundManagement extracted to backgrounds module

    /**
     * Load background data
     */
    // loadBackgroundData extracted

    /**
     * Load active background
     */
    // loadActiveBackground extracted

    /**
     * Load available backgrounds
     */
    // loadAvailableBackgrounds extracted

    /**
     * Display active background info
     */
    // displayActiveBackground extracted

    /**
     * Display available backgrounds
     */
    // displayAvailableBackgrounds extracted

    /**
     * Get background preview style
     */
    // getBackgroundPreviewStyle extracted

    /**
     * Apply background to canvas
     */
    // applyBackgroundToCanvas extracted

    /**
     * Show background configuration panel
     */
    // showBackgroundConfigPanel extracted

    /**
     * Create new background
     */
    // createBackground extracted

    /**
     * Activate background
     */
    // activateBackground extracted

    /**
     * Delete background
     */
    // deleteBackground extracted

    /**
     * Preview background without saving
     */
    // previewBackground extracted

    /**
     * Upload background image (placeholder)
     */
    // uploadBackgroundImage extracted

    /**
     * Add rotation image (placeholder) 
     */
    // addRotationImage extracted

    // Build / refresh the container layers panel (visibility toggles)
    buildLayersPanel() { return layerBuildPanel(this); }
    
    // Alias for add-layer-ui and other modules
    refreshLayerManager() { return this.buildLayersPanel(); }
    
    /** Ensure pseudo background appears as a container for panel listing */
    ensureBackgroundPseudoNode(){
        try {
            const stage = document.getElementById('block-stage'); if(!stage) return;
            const cfg = this.currentConfig; if(!cfg || !cfg._hydratedLegacyLayers) return;
            const bg = cfg._hydratedLegacyLayers.find(l=>l.kind==='background'); if(!bg) return;
            let node = stage.querySelector('[data-layer-id="bg_layer"]');
            if(!node){
                node = document.createElement('div');
                node.className='canvas-container ccve-background-layer';
                node.setAttribute('data-container-id','bg_layer');
                node.setAttribute('data-layer-id','bg_layer');
                node.style.position='absolute';
                node.style.left='0px'; node.style.top='0px'; node.style.width='1920px'; node.style.height='1080px';
                node.style.pointerEvents='none';
                node.style.zIndex='0';
                node.style.opacity='0'; // invisible overlay purely for panel presence
                stage.insertBefore(node, stage.firstChild);
            }
        } catch(_){ }
    }

    /** Visibility assertion helper for Block Stage */
    _assertBlockStageVisible() {
        const activeBtn = document.querySelector('.canvas-tab-button.active');
        if (!activeBtn || activeBtn.dataset.tab !== 'content-blocks') return;
        const wrap = document.getElementById('block-stage-wrapper');
        if (wrap && wrap.style.display === 'none') {
            wrap.style.display = 'block';
            console.debug('[CC] Forced block-stage-wrapper display=block (late assert)');
        }
    }

    /**
     * Resolve a token string to its live value.
     * Used by unified-layer-renderer for live token display.
     * 
     * @param {string} token - Token string like 'track.artwork' or 'track.title'
     * @returns {string|null} Resolved value or null
     */
    resolveToken(token) {
        if (!token || !this._liveTokenData) return null;
        
        // Handle dotted path tokens like 'track.title' or 'track.artwork'
        const parts = token.replace(/^{{|}}/g, '').trim().split('.');
        let value = this._liveTokenData;
        
        for (const part of parts) {
            if (value && typeof value === 'object' && part in value) {
                value = value[part];
            } else {
                return null; // Token not found
            }
        }
        
        return typeof value === 'string' ? value : null;
    }

    /**
     * Phase 2: Refresh live token data and re-render any templateText layers.
     * Non-disruptive: silently returns if no layers present or no tokens detected.
     */
    async refreshLiveTokens() {
        try {
            if (!this.currentConfig || !Array.isArray(this.currentConfig.layers) || this._liveTokenPending) return;
            const textLayers = this.currentConfig.layers.filter(l => l && typeof l.templateText === 'string');
            const imageLayers = this.currentConfig.layers.filter(l => l && l.kind === 'token-image' && typeof l.token === 'string');
            if (!textLayers.length && !imageLayers.length) return;
            const allTokens = new Set();
            textLayers.forEach(l => { try { tokenExtract(l.templateText).forEach(t => allTokens.add(t)); } catch(_){} });
            imageLayers.forEach(l => { try { allTokens.add(l.token); } catch(_){} });
            if (!allTokens.size) return; // nothing valid
            this._liveTokenPending = true;
            const cats = tokenDeriveCats(Array.from(allTokens));
            const ctx = { currentBlockId: this.currentContentBlockId, currentBlockType: this.currentContentBlockType };
            const dataMap = await tokenFetchCats(cats, ctx);
            try {
                console.debug('[CCVE][tokens] categories', cats, 'ctx', ctx, 'data', dataMap);
            } catch(_) {}
            this._liveTokenData = dataMap || {};
            textLayers.forEach(l => {
                const { rendered } = tokenRender(l.templateText, this._liveTokenData);
                l.renderedText = rendered;
                try {
                    // Use unified layer naming: .layer-text-content or legacy .ccve-layer-text
                    const el = document.querySelector(`[data-layer-id="${l.id}"] .layer-text-content`) || 
                               document.querySelector(`[data-layer-id="${l.id}"] .ccve-layer-text`);
                    if (el) el.textContent = rendered;
                } catch(_){ }
            });
            imageLayers.forEach(l => { try { this._applyTokenImageRender(l); } catch(_){} });
            // Empty token badge update
            this._updateEmptyTokenBadges();
            this._lastTokenRender = Date.now();
            document.dispatchEvent(new CustomEvent('ccveTokensRefreshed', { detail: { categories: cats } }));
        } catch(e) {
            console.warn('[CCVE][tokens] refresh failed', e);
        } finally {
            this._liveTokenPending = false;
        }
    }

    /** Render small warning badges on layers missing critical token output */
    _updateEmptyTokenBadges(){
        if (!this.currentConfig || !Array.isArray(this.currentConfig.layers)) return;
    const layers = this.currentConfig.layers.filter(l=>l && (l.kind==='token-text' || l.kind==='token-image'));
        layers.forEach(l=>{
            try {
        const host = document.querySelector(`#block-stage [data-layer-id="${l.id}"]`);
                if (!host) return;
                let missing=false;
                if (l.kind==='token-text') missing = !l.renderedText || !l.renderedText.trim();
                if (l.kind==='token-image') missing = !l.renderedUrl;
                let badge = host.querySelector('.ccve-token-badge');
                if (missing) {
                    if (!badge){
                        badge = document.createElement('div');
                        badge.className='ccve-token-badge';
                        badge.style.cssText='position:absolute;top:-10px;right:-10px;background:#dc2626;color:#fff;font-size:10px;padding:2px 5px;border-radius:10px;z-index:999;font-weight:600;box-shadow:0 0 4px rgba(0,0,0,0.4);';
                        badge.textContent='EMPTY';
                        host.appendChild(badge);
                    }
                } else if (badge) { badge.remove(); }
            } catch(_) {}
        });
    }

    /**
     * Promote the current preview (JSON config) into editable token layers.
     * Initial implementation targets track_info layout with left/right artwork.
     * - Creates/updates a token-image layer for artwork at the configured size/position.
     * - Creates/updates a token-text layer for title/artist combined at configured text area.
     */
    promotePreviewToTokenLayers(){
        try {
            const cfg = this.currentConfig || (this.currentConfig = this.getDefaultConfig());
            if (!Array.isArray(cfg.layers)) cfg.layers = [];
            const stage = document.getElementById('block-stage'); if(!stage) return;
            const layout = cfg.layout || {}; const pad = layout.padding || {top:0,right:0,bottom:0,left:0};
            const L = { w: (layout.width||960), h: (layout.height||200), x: (layout.position?.x||0), y: (layout.position?.y||0) };
            const art = cfg.artwork || { enabled:false, position:'left', gap:12, size:{width:150,height:150}, border_radius:0 };
            const ty = cfg.typography || { font_size:24, font_weight:'400', color:'#ffffff', text_align:'left' };
            // Attempt to seed content from hydrated legacy layers for immediate non-empty rendering
            const hyd = Array.isArray(cfg._hydratedLegacyLayers) ? cfg._hydratedLegacyLayers : [];
            const legacyText = (hyd.find(h=>h && h.kind==='static-text')?.text || '').trim();
            const legacyImgUrl = (hyd.find(h=>h && h.kind==='static-image')?.url || '').trim();
            // Compute artwork rect within the block
            const ax = art.position==='right' ? (L.w - pad.right - (art.size?.width||150)) : (pad.left);
            const ay = Math.round((L.h - (art.size?.height||150)) / 2);
            const aw = (art.size?.width||150), ah = (art.size?.height||150);
            // Compute text rect (simple split: remaining width minus gap and artwork)
            const tx = art.position==='right' ? pad.left : (pad.left + aw + (art.gap||12));
            const tw = Math.max(60, L.w - pad.left - pad.right - (art.enabled ? (aw + (art.gap||12)) : 0));
            const tyTop = Math.round((L.h - Math.max(ty.font_size||24, 28)) / 2);
            const th = Math.max(28, ty.font_size ? Math.ceil(ty.font_size*1.4) : 40);
            // Use stable ids tied to block role
            const artId = 'track_artwork';
            const textId = 'track_title_artist';
            // Upsert artwork token-image layer
            if (art.enabled) {
                let il = cfg.layers.find(l=>l && l.id===artId);
                if (!il) { il = { id: artId, kind:'token-image', token:'track.artwork' }; cfg.layers.push(il); }
                il.kind='token-image'; il.token='track.artwork';
                il.x = L.x + ax; il.y = L.y + ay; il.width = aw; il.height = ah; il.visible = true; il.lock = !!il.lock; il.fit = il.fit || cfg.artwork?.fit || 'cover';
                // Seed immediate content from legacy image if available
                if (legacyImgUrl) {
                    il.renderedUrl = legacyImgUrl;
                    try { const img = document.querySelector(`#block-stage [data-layer-id="${artId}"] img[data-token-image="1"]`); if(img) img.src = legacyImgUrl; } catch(_){ }
                }
            }
            // Upsert combined text token-text layer
            let tl = cfg.layers.find(l=>l && l.id===textId);
            if (!tl) { tl = { id: textId, kind:'token-text', templateText:'{{track.title}} — {{track.artist}}' }; cfg.layers.push(tl); }
            tl.kind='token-text'; tl.templateText = tl.templateText || '{{track.title}} — {{track.artist}}';
            tl.x = L.x + tx; tl.y = L.y + tyTop; tl.width = tw; tl.height = th; tl.visible = true; tl.lock = !!tl.lock;
            tl.style = Object.assign({}, tl.style||{}, { font_size: ty.font_size||24, font_weight: ty.font_weight||'600', color: ty.color||'#ffffff', text_align: ty.text_align||'left', font_family: ty.font_family||'system-ui,Arial,Helvetica,sans-serif' });
            // Seed immediate content from legacy text if available
            if (legacyText) {
                tl.renderedText = legacyText;
                try { const el = document.querySelector(`#block-stage [data-layer-id="${textId}"] .ccve-layer-text`); if(el) el.textContent = legacyText; } catch(_){ }
            }
            // Render layers and refresh data
            try { this._renderTokenLayers && this._renderTokenLayers(); } catch(_) {}
            try { this.refreshLiveTokens && this.refreshLiveTokens(); } catch(_) {}
            try { this.buildLayersPanel && this.buildLayersPanel(); } catch(_) {}
            this.unsavedChanges = true; this.updateSaveButton && this.updateSaveButton();
            this.showNotification && this.showNotification('Preview promoted to editable layers','success');
        } catch(e) { console.warn('[CCVE] promotePreviewToTokenLayers failed', e); this.showNotification && this.showNotification('Promote failed: '+(e.message||'error'),'error'); }
    }
}

async function initCastConductorCanvasEditor() {
    if (window.castconductorCanvasEditor) return; // already initialized
    const rootEl = document.getElementById('canvas-editor-container');
    if (!rootEl) return;
    // Diagnostics: ensure block stage structure exists; if missing, reconstruct minimal shell
    (function ensureBlockStage() {
        let wrapper = document.getElementById('block-stage-wrapper');
        if (!wrapper) {
            const layout = rootEl.querySelector('.canvas-editor-layout');
            if (layout) {
                wrapper = document.createElement('div');
                wrapper.id = 'block-stage-wrapper';
                wrapper.className = 'block-stage-wrapper';
                wrapper.innerHTML = '<div class="ccve-stage-outer" data-surface="block"><div class="canvas-stage" id="block-stage" style="position:relative"><div id="canvas-background-layer" class="canvas-background" style="position:absolute;left:0;top:0;right:0;bottom:0;z-index:0"></div><div id="canvas-preview-container" class="canvas-preview-container ccve-placeholder" style="position:relative;z-index:5"><span>Preview will appear here</span></div><div id="canvas-overlay-layer" class="canvas-overlay" style="position:absolute;left:0;top:0;right:0;bottom:0;z-index:4;pointer-events:none"></div></div></div>';
                layout.insertBefore(wrapper, layout.firstChild);
                console.debug('[CC] Reconstructed missing block-stage-wrapper');
            }
        } else {
            const stage = wrapper.querySelector('#block-stage');
            if (!stage) {
                wrapper.innerHTML = '<div class="ccve-stage-outer" data-surface="block"><div class="canvas-stage" id="block-stage" style="position:relative"><div id="canvas-background-layer" class="canvas-background" style="position:absolute;left:0;top:0;right:0;bottom:0;z-index:0"></div><div id="canvas-overlay-layer" class="canvas-overlay" style="position:absolute;left:0;top:0;right:0;bottom:0;z-index:4;pointer-events:none"></div><div id="canvas-preview-container" class="canvas-preview-container ccve-placeholder" style="position:relative;z-index:5"><span>Preview will appear here</span></div></div></div>';
                console.debug('[CC] Rebuilt missing block-stage inside wrapper');
            }
        }
    })();
    try {
    window.castconductorCanvasEditor = new CastConductorCanvasEditor();
    attachPreviewGlue(window.castconductorCanvasEditor);
    // Phase 1 Step 6: attachArtworkSlideshow() DISABLED - replaced by unified slideshow-image layers
    // Slideshow functionality now handled via slideshow-image layers in unified layer system
    // try { attachArtworkSlideshow(window.castconductorCanvasEditor); } catch(e) { console.warn('attachArtworkSlideshow failed', e); }
    try { attachQrCodeGenerator(window.castconductorCanvasEditor); } catch(e) { console.error('[CC] attachQrCodeGenerator failed:', e); }
    // Phase 1 Step 7: Unified Layer System Integration (Nov 24, 2025)
    try { 
        // Attach "Add Layer" dropdown UI
        layerAttachDropdown(window.castconductorCanvasEditor);
        // Attach "Container Preset" dropdown UI
        presetAttachDropdown(window.castconductorCanvasEditor);
        // SHELVED (Dec 2025): Custom API Content Block hidden from UI - code intact for future development
        // Attach "Public API" dropdown UI (Dec 2025 - Custom API Content Block Phase 1)
        // attachPublicApiDropdown(window.castconductorCanvasEditor);
        // Expose renderLayer method for add-layer-ui and other modules
        window.castconductorCanvasEditor.renderLayer = (layer) => {
            const stage = document.getElementById('block-stage');
            return layerRenderUnified(window.castconductorCanvasEditor, layer, stage);
        };
        console.info('[CC] Phase 1: Unified Layer System attached');
    } catch(e) { 
        console.error('[CC] Unified Layer System attachment failed:', e); 
    }
    // Phase 2: WordPress Post Integration (Nov 24, 2025)
    try {
        // Attach WordPress Post Selector modal
        layerAttachWPPostSelector(window.castconductorCanvasEditor);
        console.info('[CC] Phase 2: WordPress Post Selector attached');
    } catch(e) {
        console.error('[CC] WordPress Post Selector attachment failed:', e);
    }
        attachModalSystem(window.castconductorCanvasEditor);
        attachBackgroundManagement(window.castconductorCanvasEditor);
    try { const { attachBackgroundLayers } = await import('./modules/background-layers.js'); attachBackgroundLayers(window.castconductorCanvasEditor); try { window.castconductorCanvasEditor.refreshBackgroundLayersUI && window.castconductorCanvasEditor.refreshBackgroundLayersUI(); } catch(_) {} } catch(e){ console.warn('[CC] attachBackgroundLayers failed', e); }
    try { attachScenesPanel(window.castconductorCanvasEditor); } catch(e){ console.warn('[CC] attachScenesPanel failed', e); }
    try { attachScenesContainersPanel(window.castconductorCanvasEditor); } catch(e){ console.warn('[CC] attachScenesContainersPanel failed', e); }
    try { attachTokenLayerInteractions(window.castconductorCanvasEditor); } catch(e){ console.warn('[CC] token layer interactions failed', e); }
    try { attachLegacyLayerInteractions(window.castconductorCanvasEditor); } catch(e){ console.warn('[CC] legacy layer interactions failed', e); }
    // Layer Grouping Manager (Dec 29, 2025)
    try {
        const layerGroupingMgr = getLayerGroupingManager(window.castconductorCanvasEditor);
        layerGroupingMgr.init();
        window.castconductorCanvasEditor._layerGrouping = layerGroupingMgr;
        console.info('[CC] Layer Grouping Manager attached');
    } catch(e){ console.warn('[CC] Layer Grouping Manager failed', e); }
        attachContainerFormEnhancements(window.castconductorCanvasEditor);
        // Bind preview container (was previously never assigned, blocking live data rendering)
        try {
            window.castconductorCanvasEditor.previewContainer = document.getElementById('canvas-preview-container');
            if (!window.castconductorCanvasEditor.previewContainer) {
                console.warn('[CC] previewContainer element not found at init');
            }
        } catch(e){ console.warn('[CC] failed to bind previewContainer', e); }
    try { window.castconductorCanvasEditor.attachPreviewControlWatchers(); } catch(e){ console.warn('[CC] attachPreviewControlWatchers failed', e); }
    // Layer Animation tab controls - show/hide based on layer selection and sync with layer data
    try {
        const layerAnimSelect = document.getElementById('canvas-layer-animation');
        const layerAnimSpeed = document.getElementById('canvas-layer-animation-speed');
        const layerAnimSpeedRow = document.getElementById('layer-anim-speed-row');
        const layerAnimSpeedLabel = document.getElementById('canvas-layer-speed-label');
        const layerAnimControls = document.getElementById('layer-anim-controls');
        const layerAnimHint = document.getElementById('layer-anim-hint');
        
        // Helper to get speed label
        const getSpeedLabel = (val) => {
            if (val <= 25) return 'Slow';
            if (val <= 50) return 'Medium';
            if (val <= 75) return 'Fast';
            return 'Very Fast';
        };
        
        // Show/hide animation speed row based on animation type
        if (layerAnimSelect && layerAnimSpeedRow) {
            layerAnimSelect.addEventListener('change', () => {
                layerAnimSpeedRow.style.display = layerAnimSelect.value === 'none' ? 'none' : 'block';
                // Apply to selected layer
                const editor = window.castconductorCanvasEditor;
                const layerId = editor?._lastSelectedTextLayerId || document.querySelector('#block-stage .unified-layer.unified-layer--selected')?.getAttribute('data-layer-id');
                if (layerId && editor?.currentConfig?.layers) {
                    const layer = editor.currentConfig.layers.find(l => l && l.id === layerId);
                    if (layer) {
                        layer.animation = layerAnimSelect.value;
                        editor.unsavedChanges = true;
                        try { editor.updateSaveButton?.(); } catch(_) {}
                    }
                }
            });
        }
        
        // Update speed label and apply to layer
        if (layerAnimSpeed && layerAnimSpeedLabel) {
            layerAnimSpeed.addEventListener('input', () => {
                const val = parseInt(layerAnimSpeed.value, 10);
                layerAnimSpeedLabel.textContent = getSpeedLabel(val);
                // Apply to selected layer
                const editor = window.castconductorCanvasEditor;
                const layerId = editor?._lastSelectedTextLayerId || document.querySelector('#block-stage .unified-layer.unified-layer--selected')?.getAttribute('data-layer-id');
                if (layerId && editor?.currentConfig?.layers) {
                    const layer = editor.currentConfig.layers.find(l => l && l.id === layerId);
                    if (layer) {
                        layer.animation_speed = val;
                        editor.unsavedChanges = true;
                        try { editor.updateSaveButton?.(); } catch(_) {}
                    }
                }
            });
        }
        
        // Listen for layer selection changes to populate animation controls
        document.addEventListener('click', (e) => {
            const layerEl = e.target.closest('#block-stage .unified-layer');
            if (!layerEl) return;
            setTimeout(() => {
                const editor = window.castconductorCanvasEditor;
                const layerId = layerEl.getAttribute('data-layer-id');
                if (layerId && editor?.currentConfig?.layers && layerAnimControls && layerAnimHint) {
                    const layer = editor.currentConfig.layers.find(l => l && l.id === layerId);
                    if (layer) {
                        // Show controls, hide hint
                        layerAnimControls.style.display = 'block';
                        layerAnimHint.style.display = 'none';
                        // Populate with layer values
                        if (layerAnimSelect) {
                            layerAnimSelect.value = layer.animation || 'none';
                            if (layerAnimSpeedRow) {
                                layerAnimSpeedRow.style.display = layer.animation && layer.animation !== 'none' ? 'block' : 'none';
                            }
                        }
                        if (layerAnimSpeed) {
                            layerAnimSpeed.value = layer.animation_speed || 50;
                            if (layerAnimSpeedLabel) {
                                layerAnimSpeedLabel.textContent = getSpeedLabel(layer.animation_speed || 50);
                            }
                        }
                    }
                }
            }, 50);
        });
    } catch(e){ console.warn('[CC] layer animation tab controls failed', e); }
    // Initialize preview status badge lifecycle (LIVE/STALE/DRIFT/ERROR) once container is bound
    try { initPreviewStatus(window.castconductorCanvasEditor); } catch(e){ console.warn('[CC] initPreviewStatus failed', e); }
    // Bind save button if present
    try { const sb = document.getElementById('canvas-save-config'); if (sb && !sb.__ccveBound){ sb.addEventListener('click',(e)=>{e.preventDefault();e.stopPropagation();window.castconductorCanvasEditor.saveConfiguration();}); sb.__ccveBound=true; } } catch(_){ }
    try { const db = document.getElementById('canvas-duplicate-as-new'); if (db && !db.__ccveBound){ db.addEventListener('click',(e)=>{e.preventDefault();e.stopPropagation();window.castconductorCanvasEditor.duplicateAsNew();}); db.__ccveBound=true; } } catch(_){ }
    // Bind Create New button
    try { const cn = document.getElementById('canvas-create-new-btn'); if (cn && !cn.__ccveBound){ cn.addEventListener('click',(e)=>{e.preventDefault();e.stopPropagation();window.castconductorCanvasEditor.createNewContentBlock();}); cn.__ccveBound=true; } } catch(_){ }
    // Scenes: build selector and background/branding controls (minimal v1)
    // Defer Scenes toolbar init to when Containers tab is active
    // Persistent global actions bar wiring
    try {
        const gsave = document.getElementById('ccve-global-save');
        if (gsave && !gsave.__ccveBound) {
            gsave.addEventListener('click',()=>window.castconductorCanvasEditor.saveConfiguration());
            gsave.__ccveBound = true;
        }
        const gaf = document.getElementById('ccve-global-autofix-open');
        if (gaf && !gaf.__ccveBound) {
            gaf.addEventListener('click',()=>{
                const panel = document.getElementById('ccve-global-autofix');
                if (panel) panel.style.display='flex';
            });
            gaf.__ccveBound = true;
        }
    } catch(e){ console.warn('[CC] global actions bar wiring failed', e); }
    // Global Auto-Fix panel wiring
    try {
        const panel = document.getElementById('ccve-global-autofix');
        if (panel && !panel.__ccveBound) {
            const fixBtn = document.getElementById('ccve-global-autofix-fix');
            const dismissBtn = document.getElementById('ccve-global-autofix-dismiss');
            if (fixBtn) fixBtn.onclick = () => { if (window.castconductorCanvasEditor && window.castconductorCanvasEditor._lastAutoFix) { window.castconductorCanvasEditor._lastAutoFix(); } };
            if (dismissBtn) dismissBtn.onclick = () => { panel.style.display='none'; };
            window.addEventListener('ccve:overflowWarning', (ev) => {
                const d = ev.detail || {}; panel.style.display='flex';
                const msg = document.getElementById('ccve-global-autofix-msg');
                if (msg) msg.textContent = d.overflow ? 'Block overflows container – click Auto-Fix to snap inside.' : 'Aspect ratio mismatch – Auto-Fix will set contain mode.';
            });
            panel.__ccveBound = true;
        }
    } catch(e){ console.warn('[CC] global autofix panel wiring failed', e); }
    // Early fetch of live content blocks so selector can populate even before user switches tabs
    try { window.castconductorCanvasEditor.loadLiveContentBlocks && window.castconductorCanvasEditor.loadLiveContentBlocks(); } catch(e){ console.warn('[CC] initial loadLiveContentBlocks failed', e); }
    // Ensure live tokens stay up-to-date while editing: poll in the background
    try {
        if (!window.castconductorCanvasEditor._liveTokenInterval) {
            window.castconductorCanvasEditor._liveTokenInterval = setInterval(() => {
                try { window.castconductorCanvasEditor.refreshLiveTokens && window.castconductorCanvasEditor.refreshLiveTokens(); } catch(_) {}
            }, 10000); // every 10s; inexpensive and respects _liveTokenPending
            window.addEventListener('beforeunload', () => {
                try { clearInterval(window.castconductorCanvasEditor._liveTokenInterval); } catch(_) {}
                delete window.castconductorCanvasEditor._liveTokenInterval;
            }, { once: true });
        }
    } catch(_) {}
    } catch (e) { console.warn('canvas editor core init failed', e); }
    // Post-init wiring (tabs, enhancements)
    try {
        setTimeout(() => {
            try { window.castconductorCanvasEditor?.setupTabSwitching?.(); } catch(e) { console.warn('setupTabSwitching failed', e); }
            try { window.castconductorCanvasEditor?.initPhase27Enhancements?.(); } catch(e) { console.warn('initPhase27Enhancements failed', e); }
        }, 0);
    } catch(e) { console.warn('post-init deferral failed', e); }
}

// Initialize on DOMContentLoaded or immediately if DOM already parsed
if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', initCastConductorCanvasEditor);
} else {
    initCastConductorCanvasEditor();
}
