<?php
/**
 * 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
 */

/**
 * Canvas Editor REST API Controller
 *
 * Handles the unified canvas editor interface for visual content block design.
 * Provides endpoints for saving/loading visual configurations and generating previews.
 *
 * @package CastConductor
 * @subpackage API
 * @since 5.0.0
 */

if (!defined('ABSPATH')) {
    exit;
}

/**
 * Canvas Editor Controller Class
 *
 * Unified canvas editor system for content block visual design.
 * Single interface replaces V4's multiple canvas systems.
 */
class CastConductor_Canvas_Editor_Controller extends WP_REST_Controller {
    /** Permission: read access (GET endpoints) */
    public function get_items_permissions_check($request = null) {
        // Allow admins, castconductor_admin, or editors
        return current_user_can('manage_options') || current_user_can('castconductor_admin') || current_user_can('edit_posts');
    }
    /** Permission: write access (create/update endpoints) */
    public function create_item_permissions_check($request = null) {
        return current_user_can('manage_options') || current_user_can('castconductor_admin') || current_user_can('edit_posts');
    }
    /**
     * Silences any stray output (warnings/notices/echo) so REST responses remain valid JSON.
     */
    private function rest_silence_output() {
        $buf = '';
        while (ob_get_level() > 0) {
            $buf .= ob_get_clean();
        }
        if ($buf !== '') {
            error_log('[CastConductor] REST stray output suppressed: ' . substr($buf, 0, 500));
        }
    }

    /**
     * Register the routes for the canvas editor
     */
    public function register_routes() {
    $version = '5';
        $namespace = 'castconductor/v' . $version;
        $base = 'canvas-editor';

        // Load visual configuration for content block
        register_rest_route($namespace, '/' . $base . '/config/(?P<id>\d+)', array(
            array(
                'methods'             => WP_REST_Server::READABLE,
                'callback'            => array($this, 'get_visual_config'),
                'permission_callback' => array($this, 'get_items_permissions_check'),
                'args'               => array(
                    'id' => array(
                        'validate_callback' => function($param, $request, $key) {
                            return is_numeric($param);
                        }
                    ),
                ),
            ),
        ));

        // Save visual configuration for content block
        register_rest_route($namespace, '/' . $base . '/config/(?P<id>\d+)', array(
            array(
                'methods'             => WP_REST_Server::EDITABLE,
                'callback'            => array($this, 'save_visual_config'),
                'permission_callback' => array($this, 'create_item_permissions_check'),
                'args'               => $this->get_visual_config_args(),
            ),
        ));

    // Generate live preview with real data, Option C: server loads config by block_id
        register_rest_route($namespace, '/' . $base . '/preview', array(
            array(
                'methods'             => WP_REST_Server::CREATABLE,
                'callback'            => array($this, 'generate_preview'),
                'permission_callback' => array($this, 'create_item_permissions_check'),
                'args'               => $this->get_preview_args(),
            ),
        ));

        // Get available fonts and design assets
        register_rest_route($namespace, '/' . $base . '/assets', array(
            array(
                'methods'             => WP_REST_Server::READABLE,
                'callback'            => array($this, 'get_design_assets'),
                'permission_callback' => array($this, 'get_items_permissions_check'),
            ),
        ));

        // Validate Roku compatibility for visual config
        register_rest_route($namespace, '/' . $base . '/validate-roku', array(
            array(
                'methods'             => WP_REST_Server::CREATABLE,
                'callback'            => array($this, 'validate_roku_compatibility'),
                'permission_callback' => array($this, 'create_item_permissions_check'),
                'args'               => $this->get_visual_config_args(),
            ),
        ));

        // Scenes support (Editor wiring Phase 1): list + active
        register_rest_route($namespace, '/' . $base . '/scenes', array(
            array(
                'methods'             => WP_REST_Server::READABLE,
                'callback'            => array($this, 'get_scenes_for_editor'),
                'permission_callback' => array($this, 'get_items_permissions_check'),
            ),
        ));
        register_rest_route($namespace, '/' . $base . '/scenes/active', array(
            array(
                'methods'             => WP_REST_Server::READABLE,
                'callback'            => array($this, 'get_active_scene_for_editor'),
                'permission_callback' => array($this, 'get_items_permissions_check'),
            ),
        ));

        // Branding logo management (upload/select and get)
        register_rest_route($namespace, '/' . $base . '/branding/logo', array(
            array(
                'methods'             => WP_REST_Server::READABLE,
                'callback'            => array($this, 'get_branding_logo'),
                'permission_callback' => array($this, 'get_items_permissions_check'),
            ),
            array(
                'methods'             => WP_REST_Server::EDITABLE,
                'callback'            => array($this, 'update_branding_logo'),
                'permission_callback' => array($this, 'create_item_permissions_check'),
                'args'               => array(
                    'attachment_id' => array(
                        'required' => false,
                        'type' => 'integer',
                    ),
                    'url' => array(
                        'required' => false,
                        'type' => 'string',
                    ),
                    'image_data' => array(
                        'required' => false,
                        'type' => 'string', // base64-encoded image data (data URI or raw)
                    ),
                    'filename' => array(
                        'required' => false,
                        'type' => 'string',
                    ),
                    'mode' => array(
                        'required' => false,
                        'type' => 'string',
                        'enum' => array('current', 'default', 'both'),
                    ),
                ),
            ),
        ));

        // CPT Posts list for hierarchical dropdown (shoutouts, sponsors, promos)
        register_rest_route($namespace, '/' . $base . '/cpt-posts/(?P<type>[a-z_]+)', array(
            array(
                'methods'             => WP_REST_Server::READABLE,
                'callback'            => array($this, 'get_cpt_posts'),
                'permission_callback' => array($this, 'get_items_permissions_check'),
                'args'               => array(
                    'type' => array(
                        'required' => true,
                        'type' => 'string',
                        'enum' => array('shoutout', 'sponsor', 'promo'),
                    ),
                ),
            ),
        ));

        // Load CPT post content for editing in canvas
        register_rest_route($namespace, '/' . $base . '/cpt-content/(?P<type>[a-z_]+)/(?P<post_id>\d+)', array(
            array(
                'methods'             => WP_REST_Server::READABLE,
                'callback'            => array($this, 'get_cpt_content'),
                'permission_callback' => array($this, 'get_items_permissions_check'),
                'args'               => array(
                    'type' => array(
                        'required' => true,
                        'type' => 'string',
                        'enum' => array('shoutout', 'sponsor', 'promo'),
                    ),
                    'post_id' => array(
                        'required' => true,
                        'validate_callback' => function($param) { return is_numeric($param); },
                    ),
                ),
            ),
            // PUT/POST to save visual config to CPT post meta
            array(
                'methods'             => WP_REST_Server::CREATABLE,
                'callback'            => array($this, 'save_cpt_visual_config'),
                'permission_callback' => array($this, 'create_item_permissions_check'),
                'args'               => array(
                    'type' => array(
                        'required' => true,
                        'type' => 'string',
                        'enum' => array('shoutout', 'sponsor', 'promo'),
                    ),
                    'post_id' => array(
                        'required' => true,
                        'validate_callback' => function($param) { return is_numeric($param); },
                    ),
                ),
            ),
        ));
    }

    /**
     * Get CPT posts for hierarchical dropdown
     * Returns list of posts for the specified CPT type (shoutout, sponsor, promo)
     */
    public function get_cpt_posts($request) {
        $type = $request['type'];
        $post_type_map = array(
            'shoutout' => 'cc_shoutout',
            'sponsor'  => 'cc_sponsor',
            'promo'    => 'cc_promo',
        );
        
        if (!isset($post_type_map[$type])) {
            return CastConductor_REST_API::error_response('Invalid CPT type', 'invalid_type', 400);
        }
        
        $posts = get_posts(array(
            'post_type'      => $post_type_map[$type],
            'post_status'    => array('publish', 'draft', 'private'),
            'posts_per_page' => 100,
            'orderby'        => 'title',
            'order'          => 'ASC',
        ));
        
        $result = array();
        foreach ($posts as $post) {
            $featured_image = get_the_post_thumbnail_url($post->ID, 'medium');
            $result[] = array(
                'id'              => $post->ID,
                'title'           => $post->post_title,
                'status'          => $post->post_status,
                'featured_image'  => $featured_image ?: null,
                'edit_link'       => get_edit_post_link($post->ID, 'raw'),
            );
        }
        
        return CastConductor_REST_API::format_response($result, true, '', count($result));
    }

    /**
     * Get CPT post content for loading into canvas editor
     * Returns title, content, featured image, and custom fields
     */
    public function get_cpt_content($request) {
        $type = $request['type'];
        $post_id = (int) $request['post_id'];
        
        $post_type_map = array(
            'shoutout' => 'cc_shoutout',
            'sponsor'  => 'cc_sponsor',
            'promo'    => 'cc_promo',
        );
        
        if (!isset($post_type_map[$type])) {
            return CastConductor_REST_API::error_response('Invalid CPT type', 'invalid_type', 400);
        }
        
        $post = get_post($post_id);
        if (!$post || $post->post_type !== $post_type_map[$type]) {
            return CastConductor_REST_API::error_response('Post not found', 'not_found', 404);
        }
        
        // Get featured image in multiple sizes
        $featured_image_id = get_post_thumbnail_id($post_id);
        $featured_images = array();
        if ($featured_image_id) {
            $featured_images = array(
                'id'        => $featured_image_id,
                'thumbnail' => wp_get_attachment_image_url($featured_image_id, 'thumbnail'),
                'medium'    => wp_get_attachment_image_url($featured_image_id, 'medium'),
                'large'     => wp_get_attachment_image_url($featured_image_id, 'large'),
                'full'      => wp_get_attachment_image_url($featured_image_id, 'full'),
            );
        }
        
        // Get all post meta for custom fields
        $meta = get_post_meta($post_id);
        $custom_fields = array();
        foreach ($meta as $key => $values) {
            // Skip internal WordPress meta
            if (strpos($key, '_') === 0) continue;
            $custom_fields[$key] = is_array($values) && count($values) === 1 ? $values[0] : $values;
        }
        
        // Get visual config from post meta if it exists (for re-editing)
        $visual_config_raw = get_post_meta($post_id, '_cc_visual_config', true);
        $visual_config = null;
        if ($visual_config_raw) {
            $visual_config = is_array($visual_config_raw) ? $visual_config_raw : json_decode($visual_config_raw, true);
        }
        
        $result = array(
            'id'              => $post->ID,
            'type'            => $type,
            'post_type'       => $post->post_type,
            'title'           => $post->post_title,
            'content'         => $post->post_content,
            'excerpt'         => $post->post_excerpt,
            'status'          => $post->post_status,
            'featured_image'  => $featured_images,
            'custom_fields'   => $custom_fields,
            'visual_config'   => $visual_config,
            'edit_link'       => get_edit_post_link($post->ID, 'raw'),
        );
        
        return CastConductor_REST_API::format_response($result);
    }

    /**
     * Save visual config to CPT post meta
     * Stores the visual configuration (layers, layout, etc.) in the post's meta
     */
    public function save_cpt_visual_config($request) {
        $type = $request['type'];
        $post_id = (int) $request['post_id'];
        
        $post_type_map = array(
            'shoutout' => 'cc_shoutout',
            'sponsor'  => 'cc_sponsor',
            'promo'    => 'cc_promo',
        );
        
        if (!isset($post_type_map[$type])) {
            return CastConductor_REST_API::error_response('Invalid CPT type', 'invalid_type', 400);
        }
        
        $post = get_post($post_id);
        if (!$post || $post->post_type !== $post_type_map[$type]) {
            return CastConductor_REST_API::error_response('Post not found', 'not_found', 404);
        }
        
        // Get visual config from request body
        $body = $request->get_json_params();
        $visual_config = isset($body['visual_config']) ? $body['visual_config'] : $body;
        
        if (empty($visual_config) || !is_array($visual_config)) {
            return CastConductor_REST_API::error_response('Invalid visual config', 'invalid_config', 400);
        }
        
        // Store as JSON in post meta
        $updated = update_post_meta($post_id, '_cc_visual_config', $visual_config);
        
        // Also update last modified timestamp
        update_post_meta($post_id, '_cc_visual_config_updated', current_time('mysql'));
        
        return CastConductor_REST_API::format_response(array(
            'success'  => true,
            'post_id'  => $post_id,
            'type'     => $type,
            'message'  => 'Visual config saved to ' . $post->post_title,
        ));
    }

    /**
     * Editor convenience: list scenes (id, name, is_active)
     */
    public function get_scenes_for_editor($request) {
        global $wpdb;
        $db = new CastConductor_Database();
        $table = $db->get_table_name('scenes');
        $rows = $wpdb->get_results("SELECT id, name, is_active FROM {$table} ORDER BY is_active DESC, id ASC");
        return CastConductor_REST_API::format_response($rows);
    }

    /**
     * Editor convenience: get active scene (id, name)
     */
    public function get_active_scene_for_editor($request) {
        $db = new CastConductor_Database();
        $row = $db->get_active_scene();
        if (!$row) { $db->ensure_default_scene(); $row = $db->get_active_scene(); }
        if (!$row) { return CastConductor_REST_API::format_response(null, true, 'No active scene'); }
        return CastConductor_REST_API::format_response(array(
            'id' => (int)$row->id,
            'name' => $row->name,
            'background' => $row->background,
            'branding' => $row->branding
        ));
    }

    /**
     * Get visual configuration for a content block
     *
     * @param WP_REST_Request $request Full data about the request.
     * @return WP_Error|WP_REST_Response
     */
    public function get_visual_config($request) {
    // Prevent PHP warnings/notices from corrupting JSON
    ob_start();
        global $wpdb;

        $content_block_id = (int) $request['id'];

        try {
            $table_name = $wpdb->prefix . 'castconductor_content_blocks';
            
            $content_block = $wpdb->get_row($wpdb->prepare(
                "SELECT visual_config, type, name, updated_at FROM {$table_name} WHERE id = %d",
                $content_block_id
            ));

            if (!$content_block) {
                return new WP_Error(
                    'content_block_not_found',
                    'Content block not found.',
                    array('status' => 404)
                );
            }

            // Parse JSON config or provide defaults
            $config = json_decode($content_block->visual_config, true);
            if (!$config) {
                $config = $this->get_default_visual_config();
            } else {
                // V5: Enforce strict 1280x720 authoring space.
                if (!empty($config['layout']) && is_array($config['layout'])) {
                    $config['layout']['authored_space'] = 'admin_1280x720';
                }
                // Migration: introduce background.layers if absent
                if (empty($config['background']) || !is_array($config['background'])) {
                    $config['background'] = $this->get_default_visual_config()['background'];
                }
                if (!isset($config['background']['layers']) || !is_array($config['background']['layers'])) {
                    $layers = array();
                    // Legacy image
                    if (!empty($config['background']['image']) && is_array($config['background']['image']) && !empty($config['background']['image']['src'])) {
                        $img = $config['background']['image'];
                        $layers[] = array(
                            'kind' => 'image',
                            'enabled' => true,
                            'image' => array(
                                'attachment_id' => isset($img['attachment_id']) ? intval($img['attachment_id']) : 0,
                                'src' => esc_url_raw($img['src']),
                                'fit' => isset($img['fit']) ? sanitize_text_field($img['fit']) : 'cover',
                                'position' => isset($img['position']) ? sanitize_text_field($img['position']) : 'center',
                                'repeat' => isset($img['repeat']) ? sanitize_text_field($img['repeat']) : 'no-repeat',
                            )
                        );
                    }
                    // Gradient layer based on legacy type
                    if (isset($config['background']['type']) && $config['background']['type'] === 'gradient') {
                        $layers[] = array('kind' => 'gradient', 'enabled' => true);
                    }
                    // Overlay layer if opacity > 0
                    if (!empty($config['overlay']) && is_array($config['overlay'])) {
                        $op = isset($config['overlay']['opacity']) ? floatval($config['overlay']['opacity']) : 0;
                        if ($op > 0) { $layers[] = array('kind' => 'overlay', 'enabled' => true); }
                    }
                    $config['background']['layers'] = $layers;
                }
            }

            $response = new WP_REST_Response(array(
                'success' => true,
                'config' => $config,
                'content_block_id' => $content_block_id,
                'content_block_type' => $content_block->type,
                'content_block_name' => $content_block->name,
                'config_version' => !empty($content_block->updated_at) ? strtotime($content_block->updated_at) : null
            ), 200);
            $this->rest_silence_output();
            return $response;

        } catch (Exception $e) {
            $this->rest_silence_output();
            return new WP_Error(
                'database_error',
                'Failed to retrieve visual configuration: ' . $e->getMessage(),
                array('status' => 500)
            );
        }
    }

    /**
     * Save visual configuration for a content block
     *
     * @param WP_REST_Request $request Full data about the request.
     * @return WP_Error|WP_REST_Response
     */
    public function save_visual_config($request) {
    // Prevent PHP warnings/notices from corrupting JSON
    ob_start();
        global $wpdb;

        $content_block_id = (int) $request['id'];
    // Accept both legacy raw config payload and current wrapped { config: { ... } }
    $payload = $request->get_json_params();
    if (isset($payload['config']) && is_array($payload['config'])) {
        $visual_config = $payload['config'];
    } else {
        $visual_config = is_array($payload) ? $payload : array();
    }
    
    // DEBUG LOGGING
    if (defined('WP_DEBUG') && WP_DEBUG) {
        error_log('[CC] SAVE visual_config for block_id=' . $content_block_id);
        error_log('[CC] SAVE background.opacity: ' . ($visual_config['background']['opacity'] ?? 'MISSING'));
        error_log('[CC] SAVE layout.padding: ' . wp_json_encode($visual_config['layout']['padding'] ?? 'MISSING'));
    }
    
    // Background layered structure sanitization/migration
    if (!isset($visual_config['background']) || !is_array($visual_config['background'])) {
        $visual_config['background'] = $this->get_default_visual_config()['background'];
    }
    $bg =& $visual_config['background'];
    if (!isset($bg['layers']) || !is_array($bg['layers'])) {
        // Build initial layers from legacy fields if present
        $layers = array();
        if (!empty($bg['image']) && is_array($bg['image']) && !empty($bg['image']['src'])) {
            $img = $bg['image'];
            $layers[] = array(
                'kind' => 'image',
                'enabled' => true,
                'image' => array(
                    'attachment_id' => isset($img['attachment_id']) ? intval($img['attachment_id']) : 0,
                    'src' => esc_url_raw($img['src']),
                    'fit' => isset($img['fit']) ? sanitize_text_field($img['fit']) : 'cover',
                    'position' => isset($img['position']) ? sanitize_text_field($img['position']) : 'center',
                    'repeat' => isset($img['repeat']) ? sanitize_text_field($img['repeat']) : 'no-repeat',
                )
            );
        }
        if (isset($bg['type']) && $bg['type'] === 'gradient') {
            $layers[] = array('kind' => 'gradient', 'enabled' => true);
        }
        if (!empty($visual_config['overlay']) && is_array($visual_config['overlay'])) {
            $op = isset($visual_config['overlay']['opacity']) ? floatval($visual_config['overlay']['opacity']) : 0;
            if ($op > 0) { $layers[] = array('kind' => 'overlay', 'enabled' => true); }
        }
        $bg['layers'] = $layers;
    } else {
        // Sanitize provided layers
        $cleanLayers = array();
        foreach($bg['layers'] as $layer) {
            if (!is_array($layer) || empty($layer['kind'])) continue;
            $kind = sanitize_text_field($layer['kind']);
            if (!in_array($kind, array('overlay','gradient','image'), true)) continue;
            $entry = array('kind' => $kind, 'enabled' => !isset($layer['enabled']) || !empty($layer['enabled']));
            if ($kind === 'image') {
                $img = isset($layer['image']) && is_array($layer['image']) ? $layer['image'] : array();
                if (empty($img['src'])) { // allow placeholder blank images to persist for user editing
                    $imgSrc = isset($img['src']) ? esc_url_raw($img['src']) : '';
                }
                $entry['image'] = array(
                    'attachment_id' => isset($img['attachment_id']) ? intval($img['attachment_id']) : 0,
                    'src' => isset($img['src']) ? esc_url_raw($img['src']) : '',
                    'fit' => isset($img['fit']) ? sanitize_text_field($img['fit']) : 'cover',
                    'position' => isset($img['position']) ? sanitize_text_field($img['position']) : 'center',
                    'repeat' => isset($img['repeat']) ? sanitize_text_field($img['repeat']) : 'no-repeat',
                );
            }
            $cleanLayers[] = $entry;
        }
        $bg['layers'] = $cleanLayers;
    }
    // Artwork multi-image normalization (migration support)
    if (isset($visual_config['artwork']) && is_array($visual_config['artwork'])) {
        $aw =& $visual_config['artwork'];
        if (!isset($aw['items']) || !is_array($aw['items'])) {
            $aw['items'] = array();
        }
        // Legacy single fields migration: src/attachment_id/alt at root of artwork
        $legacy = array();
        foreach(array('attachment_id','src','alt','mime','width','height') as $k) {
            if (isset($aw[$k])) { $legacy[$k] = $aw[$k]; unset($aw[$k]); }
        }
        if (!empty($legacy) && empty($aw['items'])) {
            $aw['items'][] = array(
                'attachment_id' => isset($legacy['attachment_id']) ? intval($legacy['attachment_id']) : 0,
                'src' => isset($legacy['src']) ? esc_url_raw($legacy['src']) : '',
                'alt' => isset($legacy['alt']) ? sanitize_text_field($legacy['alt']) : '',
                'mime' => isset($legacy['mime']) ? sanitize_text_field($legacy['mime']) : '',
                'width' => isset($legacy['width']) ? intval($legacy['width']) : 0,
                'height' => isset($legacy['height']) ? intval($legacy['height']) : 0,
            );
        }
        if (!isset($aw['activeIndex']) || !is_numeric($aw['activeIndex'])) { $aw['activeIndex'] = 0; }
        if (!isset($aw['slideshow']) || !is_array($aw['slideshow'])) {
            $aw['slideshow'] = array('enabled'=>false,'interval'=>5000,'transition'=>'fade');
        } else {
            $aw['slideshow']['enabled'] = !empty($aw['slideshow']['enabled']);
            $aw['slideshow']['interval'] = isset($aw['slideshow']['interval']) ? intval($aw['slideshow']['interval']) : 5000;
            $aw['slideshow']['transition'] = isset($aw['slideshow']['transition']) ? sanitize_text_field($aw['slideshow']['transition']) : 'fade';
        }
        // Sanitize each item
        $cleanItems = array();
        foreach($aw['items'] as $item) {
            if (!is_array($item)) continue;
            $cleanItems[] = array(
                'attachment_id' => isset($item['attachment_id']) ? intval($item['attachment_id']) : 0,
                'src' => isset($item['src']) ? esc_url_raw($item['src']) : '',
                'alt' => isset($item['alt']) ? sanitize_text_field($item['alt']) : '',
                'mime' => isset($item['mime']) ? sanitize_text_field($item['mime']) : '',
                'width' => isset($item['width']) ? intval($item['width']) : 0,
                'height' => isset($item['height']) ? intval($item['height']) : 0,
            );
        }
        $aw['items'] = $cleanItems;
        if ($aw['activeIndex'] >= count($aw['items'])) { $aw['activeIndex'] = 0; }
    }

    // Store raw admin 1280x720 geometry (authoring coordinates) without scaling so round-trip persistence is exact.
    // Preview/export paths still call normalize_admin_config_to_roku() when needed.
    if (isset($visual_config['layout']) && is_array($visual_config['layout'])) {
        $visual_config['layout']['authored_space'] = 'admin_1280x720';
    }

    // Enhanced validation and feedback for layout issues
    $layout = isset($visual_config['layout']) ? $visual_config['layout'] : array();
    $errors = array();
    // Geometry checks
    if (isset($layout['width']) && $layout['width'] < 40) {
        $errors[] = 'Width is too small (min 40px).';
    }
    if (isset($layout['height']) && $layout['height'] < 40) {
        $errors[] = 'Height is too small (min 40px).';
    }
    if (isset($layout['position']['x']) && $layout['position']['x'] < 0) {
        $errors[] = 'X position is negative.';
    }
    if (isset($layout['position']['y']) && $layout['position']['y'] < 0) {
        $errors[] = 'Y position is negative.';
    }
    // FitMode checks
    $valid_fit_modes = array('fill','contain','cover','maintain-aspect','anchor');
    if (isset($layout['fitMode']) && !in_array($layout['fitMode'], $valid_fit_modes)) {
        $errors[] = 'Invalid fitMode: ' . $layout['fitMode'];
    }
    // Overflow check (Roku bounds) – bypass hard errors for admin authored 1280x720 space (store raw geometry)
    $is_admin_authored = isset($layout['authored_space']) && $layout['authored_space'] === 'admin_1280x720';
    if (!$is_admin_authored) {
        if (isset($layout['width'],$layout['position']['x']) && $layout['width'] + $layout['position']['x'] > 1280) {
            $errors[] = 'Block overflows Roku width (1280px).';
        }
        if (isset($layout['height'],$layout['position']['y']) && $layout['height'] + $layout['position']['y'] > 720) {
            $errors[] = 'Block overflows Roku height (720px).';
        }
    }
    // Aspect ratio warning for maintain-aspect/contain/cover
    if (isset($layout['fitMode']) && in_array($layout['fitMode'], array('contain','cover','maintain-aspect')) && isset($layout['width'],$layout['height'])) {
        $aspect = $layout['width'] / max(1,$layout['height']);
        if ($aspect < 0.5 || $aspect > 3.0) {
            $errors[] = 'Unusual aspect ratio for fitMode: ' . round($aspect,2);
        }
    }
    if (!empty($errors)) {
        return new WP_Error(
            'validation_failed',
            'Validation errors: ' . implode(' ', $errors),
            array('status' => 400, 'validation_errors' => $errors)
        );
    }

        try {
            // Validate Roku compatibility
            $roku_validation = $this->validate_config_for_roku($visual_config);
            if (!$roku_validation['valid']) {
                return new WP_Error(
                    'roku_validation_failed',
                    'Configuration not compatible with Roku: ' . implode(', ', $roku_validation['errors']),
                    array('status' => 400)
                );
            }

            $table_name = $wpdb->prefix . 'castconductor_content_blocks';
            
            $result = $wpdb->update(
                $table_name,
                array(
                    'visual_config' => json_encode($visual_config),
                    'updated_at' => current_time('mysql')
                ),
                array('id' => $content_block_id),
                array('%s', '%s'),
                array('%d')
            );

            if ($result === false) {
                return new WP_Error(
                    'database_error',
                    'Failed to save visual configuration.',
                    array('status' => 500)
                );
            }

            $response = new WP_REST_Response(array(
                'success' => true,
                'message' => 'Visual configuration saved successfully.',
                'config' => $visual_config,
                'roku_compatible' => true
            ), 200);
            $this->rest_silence_output();
            return $response;

        } catch (Exception $e) {
            $this->rest_silence_output();
            return new WP_Error(
                'save_error',
                'Failed to save visual configuration: ' . $e->getMessage(),
                array('status' => 500)
            );
        }
    }

    /**
     * Normalize a visual config (Pass-through for V5 1280x720 standard).
     */
    private function normalize_admin_config_to_roku($config) {
        // V5: No scaling needed. Admin and Roku spaces are identical (1280x720).
        return $this->enforce_geometry_and_fitmode($config, 'admin_to_roku');
    }

    /**
     * Denormalize a Roku-space visual config (Pass-through for V5 1280x720 standard).
     */
    private function denormalize_roku_config_to_admin($config) {
        // V5: No scaling needed. Admin and Roku spaces are identical (1280x720).
        return $this->enforce_geometry_and_fitmode($config, 'roku_to_admin');
    }

    /**
     * Generate live preview with REAL DATA (V5 Philosophy: NO DUMMY DATA)
     * Uses actual content blocks and their live data endpoints
     *
     * @param WP_REST_Request $request Full data about the request.
     * @return WP_Error|WP_REST_Response
     */
    public function generate_preview($request) {
        // Prevent PHP warnings/notices from corrupting JSON
        ob_start();
    $json             = $request->get_json_params();
        $provided_behavior = isset($json['behavior']) ? $json['behavior'] : null;
        $content_type      = $request->get_param('content_type') ?: null;
        $block_id          = $request->get_param('block_id') ? intval($request->get_param('block_id')) : null;
        $item_index        = $request->get_param('item_index');
        $shuffle_flag      = $request->get_param('shuffle');
        $seed              = $request->get_param('seed');
    $frame             = (isset($json['frame']) && is_array($json['frame'])) ? $json['frame'] : null;

        try {
            $visual_config = null;
            $real_data     = null;

            if (defined('WP_DEBUG') && WP_DEBUG) {
                error_log('[CC] Preview request: ' . wp_json_encode(array(
                    'block_id'     => $block_id,
                    'content_type' => $content_type,
                    'item_index'   => $item_index,
                    'shuffle'      => $shuffle_flag,
                    'seed'         => $seed
                )));
            }

            if ($block_id) {
                global $wpdb;
                $tbl = $wpdb->prefix . 'castconductor_content_blocks';
                $content_block = $wpdb->get_row($wpdb->prepare("SELECT * FROM {$tbl} WHERE id = %d", $block_id));
                if (!$content_block) {
                    return new WP_Error('content_block_not_found', 'Content block not found', array('status' => 404));
                }

                $visual_config = !empty($content_block->visual_config)
                    ? (json_decode($content_block->visual_config, true) ?: $this->get_default_visual_config($content_block->type))
                    : $this->get_default_visual_config($content_block->type);

                // Migration: introduce background.layers if absent (mirrors get_visual_config logic)
                if (empty($visual_config['background']) || !is_array($visual_config['background'])) {
                    $visual_config['background'] = $this->get_default_visual_config()['background'];
                }
                if (!isset($visual_config['background']['layers']) || !is_array($visual_config['background']['layers'])) {
                    $layers = array();
                    // Legacy image
                    if (!empty($visual_config['background']['image']) && is_array($visual_config['background']['image']) && !empty($visual_config['background']['image']['src'])) {
                        $img = $visual_config['background']['image'];
                        $layers[] = array(
                            'kind' => 'image',
                            'enabled' => true,
                            'image' => array(
                                'attachment_id' => isset($img['attachment_id']) ? intval($img['attachment_id']) : 0,
                                'src' => esc_url_raw($img['src']),
                                'fit' => isset($img['fit']) ? sanitize_text_field($img['fit']) : 'cover',
                                'position' => isset($img['position']) ? sanitize_text_field($img['position']) : 'center',
                                'repeat' => isset($img['repeat']) ? sanitize_text_field($img['repeat']) : 'no-repeat',
                            )
                        );
                    }
                    // Gradient layer based on legacy type
                    if (isset($visual_config['background']['type']) && $visual_config['background']['type'] === 'gradient') {
                        $layers[] = array('kind' => 'gradient', 'enabled' => true);
                    }
                    // Overlay layer if opacity > 0
                    if (!empty($visual_config['overlay']) && is_array($visual_config['overlay'])) {
                        $op = isset($visual_config['overlay']['opacity']) ? floatval($visual_config['overlay']['opacity']) : 0;
                        if ($op > 0) { $layers[] = array('kind' => 'overlay', 'enabled' => true); }
                    }
                    $visual_config['background']['layers'] = $layers;
                }

                // If client sends override_config (unsaved draft changes), merge it shallowly
                if (isset($json['override_config']) && is_array($json['override_config'])) {
                    $override = $json['override_config'];
                    // Shallow merge top-level groups we recognize
                    foreach (array('layout','typography','background','artwork','behavior') as $k) {
                        if (isset($override[$k]) && is_array($override[$k])) {
                            if (!isset($visual_config[$k]) || !is_array($visual_config[$k])) { $visual_config[$k] = array(); }
                            $visual_config[$k] = array_merge($visual_config[$k], $override[$k]);
                        }
                    }
                    // Pass through token layers for token-aware preview gating (avoid duplicate text/artwork)
                    if (isset($override['layers']) && is_array($override['layers'])) {
                        $visual_config['layers'] = $override['layers'];
                    }
                }

                if (is_array($provided_behavior)) {
                    if (!isset($visual_config['behavior']) || !is_array($visual_config['behavior'])) {
                        $visual_config['behavior'] = array();
                    }
                    $visual_config['behavior'] = array_merge($visual_config['behavior'], $provided_behavior);
                }

                // Determine shuffle
                if ($shuffle_flag !== null) {
                    $shuffle_items = (bool) $shuffle_flag;
                } elseif (isset($visual_config['behavior']['shuffle_items'])) {
                    $shuffle_items = (bool) $visual_config['behavior']['shuffle_items'];
                } else {
                    $shuffle_items = false;
                }

                $cbc = new CastConductor_Content_Blocks_Controller();
                if ($item_index === null) { $item_index = 0; }
                $real_data = method_exists($cbc, 'fetch_live_data_itemized')
                    ? $cbc->fetch_live_data_itemized($content_block, intval($item_index), $shuffle_items, $seed)
                    : $cbc->fetch_live_data($content_block);

                $content_type = $content_block->type;

                // CUSTOM/LAYER-BASED BLOCKS: If live data returns error but block has self-contained layers
                // (wordpress-post, qr-image, static-text, static-image, slideshow-image, background-image),
                // clear the error so layer renderer can proceed. These blocks don't need external data.
                if (isset($real_data['error']) && !empty($visual_config['layers']) && is_array($visual_config['layers'])) {
                    $selfContainedLayerKinds = ['wordpress-post', 'qr-image', 'static-text', 'static-image', 'slideshow-image', 'background-image'];
                    $hasSelfContainedLayers = false;
                    foreach ($visual_config['layers'] as $layer) {
                        if (isset($layer['kind']) && in_array($layer['kind'], $selfContainedLayerKinds)) {
                            $hasSelfContainedLayers = true;
                            break;
                        }
                    }
                    if ($hasSelfContainedLayers) {
                        // Clear the error - this block's content is in its layers, not external data
                        $real_data = array();
                        if (defined('WP_DEBUG') && WP_DEBUG) {
                            error_log('[CC] Custom block with self-contained layers - clearing live data error for block_id=' . $block_id);
                        }
                    }
                }
            } else {
                $content_type  = $content_type ?: 'track_info';
                $visual_config = is_array($json) ? $json : $this->get_default_visual_config($content_type);
                $real_data     = $this->get_real_content_block_data($content_type);
            }

            // If a frame (zone) context is provided, use 100% dimensions to fill Shadow DOM host
            // Artwork will still use fixed authored dimensions for proper sizing
            if ($frame) {
                $fw = isset($frame['width']) ? max(1, intval($frame['width'])) : null;
                $fh = isset($frame['height']) ? max(1, intval($frame['height'])) : null;
                $fx = isset($frame['x']) ? intval($frame['x']) : 0;
                $fy = isset($frame['y']) ? intval($frame['y']) : 0;

                if (!isset($visual_config['layout']) || !is_array($visual_config['layout'])) { $visual_config['layout'] = array(); }
                // Unset dimensions so block uses 100% to fill the container
                unset($visual_config['layout']['width']);
                unset($visual_config['layout']['height']);
                // Record zone identifier for downstream frame-aware preview gating
                if (!isset($visual_config['behavior']) || !is_array($visual_config['behavior'])) { $visual_config['behavior'] = array(); }
                $visual_config['behavior']['frame_context'] = array(
                    'width' => $fw,
                    'height'=> $fh,
                    'x' => $fx,
                    'y' => $fy,
                    'zone_id' => isset($frame['zone_id']) ? sanitize_text_field($frame['zone_id']) : ''
                );
                
                // Auto-calculate appropriate artwork size for Track Info blocks in containers
                if ($content_type === 'track_info' && $fh) {
                    if (!isset($visual_config['display_settings']) || !is_array($visual_config['display_settings'])) {
                        $visual_config['display_settings'] = array();
                    }
                    // Only auto-set if not explicitly configured
                    if (empty($visual_config['display_settings']['artwork_size'])) {
                        // Calculate appropriate size based on container height
                        // Account for padding (typically 16px top + 16px bottom = 32px)
                        $available_height = $fh - 32;
                        if ($available_height <= 120) {
                            $visual_config['display_settings']['artwork_size'] = 'small';  // 100px
                        } elseif ($available_height <= 220) {
                            $visual_config['display_settings']['artwork_size'] = 'small';  // 100px for 240px containers
                        } elseif ($available_height <= 320) {
                            $visual_config['display_settings']['artwork_size'] = 'medium'; // 200px
                        } elseif ($available_height <= 420) {
                            $visual_config['display_settings']['artwork_size'] = 'large';  // 300px
                        } else {
                            $visual_config['display_settings']['artwork_size'] = 'xlarge'; // 400px
                        }
                    }
                }
            }

            // DEBUG LOGGING: Track what config values we're actually using
            if (defined('WP_DEBUG') && WP_DEBUG) {
                error_log('[CC] Preview config diagnostic for block_id=' . $block_id);
                error_log('[CC] Typography: ' . wp_json_encode($visual_config['typography'] ?? 'MISSING'));
                error_log('[CC] Layout.padding: ' . wp_json_encode($visual_config['layout']['padding'] ?? 'MISSING'));
                error_log('[CC] Background.color: ' . ($visual_config['background']['color'] ?? 'MISSING'));
                error_log('[CC] Background.opacity: ' . ($visual_config['background']['opacity'] ?? 'MISSING'));
                error_log('[CC] Overlay.opacity: ' . ($visual_config['overlay']['opacity'] ?? 'MISSING'));
                error_log('[CC] Overlay layer enabled: ' . (($visual_config['background']['layers'][0]['enabled'] ?? false) ? 'YES' : 'NO'));
                error_log('[CC] Artwork.enabled: ' . ($visual_config['artwork']['enabled'] ?? 'MISSING'));
            }

            $visual_config = $this->enforce_geometry_and_fitmode($visual_config, 'admin_to_roku');
            $preview_html  = $this->render_preview_html($visual_config, $real_data, $content_type);
            $preview_css   = $this->generate_preview_css($visual_config);
            
            // DEBUG: Show first 1500 chars of generated CSS to see typography rules
            if (defined('WP_DEBUG') && WP_DEBUG) {
                error_log('[CC] Preview CSS (first 1500 chars): ' . substr($preview_css, 0, 1500));
            }

            ob_end_clean();
            if (defined('WP_DEBUG') && WP_DEBUG) {
                error_log('[CC] Preview built: html_len=' . (is_string($preview_html)?strlen($preview_html):0) . ' css_len=' . (is_string($preview_css)?strlen($preview_css):0));
            }

            $response = new WP_REST_Response(array(
                'success' => true,
                'preview' => array(
                    'html'         => $preview_html,
                    'css'          => $preview_css,
                    'config'       => $visual_config,
                    'real_data'    => $real_data,
                    'content_type' => $content_type,
                )
            ), 200);
            $this->rest_silence_output();
            return $response;
        } catch (Exception $e) {
            $this->rest_silence_output();
            return new WP_Error('preview_error', 'Failed to generate preview: ' . $e->getMessage(), array('status' => 500));
        }
    }

    /**
     * Shared geometry and fitMode enforcement for admin/export parity
     * $direction: 'admin_to_roku' or 'roku_to_admin'
     */
    /**
     * Shared geometry and fitMode enforcement for admin/export parity
     * V5 UPDATE: Strict 1280x720 pass-through. No scaling.
     */
    private function enforce_geometry_and_fitmode($config, $direction = 'admin_to_roku') {
        if (!is_array($config) || !isset($config['layout']) || !is_array($config['layout'])) return $config;
        
        $layout = $config['layout'];
        
        // Ensure authored_space is always set to admin_1280x720
        if (!isset($layout['authored_space'])) {
            $layout['authored_space'] = 'admin_1280x720';
        }

        // Pass-through geometry (clamping handled by client/Roku if needed, but we store what is authored)
        $config['layout'] = $layout;
        return $config;
    }
    /**
     * Shared geometry and fitMode enforcement for admin/export parity
     * $direction: 'admin_to_roku' or 'roku_to_admin'
     */

    /**
     * Get available design assets (fonts, colors, etc.)
     *
     * @param WP_REST_Request $request Full data about the request.
     * @return WP_Error|WP_REST_Response
     */
    public function get_design_assets($request) {
    // Prevent PHP warnings/notices from corrupting JSON
    ob_start();
        try {
            $assets = array(
                'fonts' => array(
                    array('name' => 'Roboto', 'value' => 'Roboto, sans-serif', 'category' => 'sans-serif'),
                    array('name' => 'Open Sans', 'value' => 'Open Sans, sans-serif', 'category' => 'sans-serif'),
                    array('name' => 'Lato', 'value' => 'Lato, sans-serif', 'category' => 'sans-serif'),
                    array('name' => 'Montserrat', 'value' => 'Montserrat, sans-serif', 'category' => 'sans-serif'),
                    array('name' => 'Source Sans Pro', 'value' => 'Source Sans Pro, sans-serif', 'category' => 'sans-serif'),
                    array('name' => 'Playfair Display', 'value' => 'Playfair Display, serif', 'category' => 'serif'),
                    array('name' => 'Merriweather', 'value' => 'Merriweather, serif', 'category' => 'serif'),
                    array('name' => 'Courier New', 'value' => 'Courier New, monospace', 'category' => 'monospace'),
                ),
                'colors' => array(
                    'primary' => array('#007cba', '#0073aa', '#005177'),
                    'secondary' => array('#50575e', '#32373c', '#23282d'),
                    'accent' => array('#00a0d2', '#0085ba', '#006799'),
                    'text' => array('#ffffff', '#000000', '#444444', '#666666'),
                    'background' => array('#ffffff', '#f1f1f1', '#23282d', '#32373c'),
                ),
                'sizes' => array(
                    'roku_safe' => array(
                        'width' => array('min' => 100, 'max' => 1280, 'default' => 600),
                        'height' => array('min' => 50, 'max' => 720, 'default' => 200),
                    ),
                    'font_sizes' => array(12, 14, 16, 18, 20, 24, 28, 32, 36, 48, 64),
                ),
                'layouts' => array(
                    array('name' => 'Single Line', 'value' => 'single_line'),
                    array('name' => 'Two Lines', 'value' => 'two_lines'),
                    array('name' => 'Three Lines', 'value' => 'three_lines'),
                    array('name' => 'With Artwork', 'value' => 'with_artwork'),
                    array('name' => 'Compact', 'value' => 'compact'),
                ),
            );

            $response = new WP_REST_Response(array(
                'success' => true,
                'assets' => $assets
            ), 200);
            $this->rest_silence_output();
            return $response;

        } catch (Exception $e) {
            $this->rest_silence_output();
            return new WP_Error(
                'assets_error',
                'Failed to retrieve design assets: ' . $e->getMessage(),
                array('status' => 500)
            );
        }
    }

    /**
     * Validate Roku compatibility for visual configuration
     *
     * @param WP_REST_Request $request Full data about the request.
     * @return WP_Error|WP_REST_Response
     */
    public function validate_roku_compatibility($request) {
    $visual_config = $request->get_json_params();
    $visual_config = $this->normalize_admin_config_to_roku($visual_config);
        // Prevent PHP warnings/notices from corrupting JSON
        ob_start();

        try {
            $validation = $this->validate_config_for_roku($visual_config);

            $response = new WP_REST_Response(array(
                'success' => true,
                'valid' => $validation['valid'],
                'errors' => $validation['errors'],
                'warnings' => $validation['warnings'],
                'config' => $visual_config
            ), 200);
            $this->rest_silence_output();
            return $response;

        } catch (Exception $e) {
            $this->rest_silence_output();
            return new WP_Error(
                'validation_error',
                'Failed to validate configuration: ' . $e->getMessage(),
                array('status' => 500)
            );
        }
    }

    /**
     * Get default visual configuration
     *
     * @return array Default visual config
     */
    private function get_default_visual_config($type = null) {
        $config = array(
            'typography' => array(
                'font_family' => 'Roboto, sans-serif',
                'font_size' => 24,
                'font_weight' => 'normal',
                'color' => '#ffffff',
                'text_align' => 'left',
                'line_height' => 1.2,
                'text_shadow' => array('x' => 0, 'y' => 0, 'blur' => 0, 'color' => 'rgba(0,0,0,0)')
            ),
            'layout' => array(
                'width' => 600,
                'height' => 200,
                'position' => array('x' => 60, 'y' => 740),
                'padding' => array('top' => 10, 'right' => 15, 'bottom' => 10, 'left' => 15),
                'margin' => array('top' => 0, 'right' => 0, 'bottom' => 0, 'left' => 0),
            ),
            'background' => array(
                'type' => 'solid', // solid|gradient (legacy root classification; layers now preferred)
                'color' => 'rgba(0, 0, 0, 0.7)',
                'gradient_colors' => array('#000000', '#333333'),
                'gradient_direction' => '135deg',
                'border_radius' => 8,
                'border' => array(
                    'width' => 0,
                    'color' => '#000000',
                    'style' => 'solid',
                ),
                // New layered background structure (top -> bottom ordering for CSS background-image stacking)
                // Each layer: { kind: 'overlay'|'gradient'|'image', enabled: bool, image?: { attachment_id:int, src:str, fit, position, repeat } }
                'layers' => array(),
            ),
            'overlay' => array(
                'color' => '#000000',
                'opacity' => 0.0,
            ),
            'artwork' => array(
                'enabled' => true,
                'size' => array('width' => 240, 'height' => 240),
                'position' => 'left',
                'gap' => 12,
                'border_radius' => 4,
                'apis' => array('itunes', 'musicbrainz', 'deezer'),
                // New multi-image slideshow support
                'items' => array(), // each: { attachment_id:int, src:string, alt:string, width:int,height:int,mime:string }
                'activeIndex' => 0,
                'slideshow' => array(
                    'enabled' => false,
                    'interval' => 5000, // ms
                    'transition' => 'fade' // fade|none (future: slide, crossfade)
                )
            ),
            'animation' => array(
                'entrance' => 'fade_in',
                'duration' => 300,
                'easing' => 'ease-in-out',
            ),
            'behavior' => array(
                'ticker' => array('enabled' => false, 'speed' => 30),
                'shuffle_items' => false
            ),
            'roku_validated' => true,
        );

        // Apply type-specific defaults to match Content Block Editor seeding
        if ($type === 'track_info') {
            $config['typography']['color'] = '#FFD700'; // Gold
            $config['typography']['font_size'] = 48;
            $config['typography']['font_weight'] = 'bold';
        }

        return $config;
    }

    /**
     * Validate configuration for Roku compatibility
     *
     * @param array $config Visual configuration
     * @return array Validation result
     */
    private function validate_config_for_roku($config) {
        $errors = array();
        $warnings = array();

    $space = isset($config['layout']['authored_space']) ? $config['layout']['authored_space'] : '';
    $admin_authoring = ($space === 'admin_1280x720');

        // Check dimensions (downgrade to warnings when in admin authoring space so raw geometry can persist)
        if (isset($config['layout']['width']) && $config['layout']['width'] > 1280) {
            if ($admin_authoring) { $warnings[] = 'Width exceeds Roku 1280px (authoring space retained)'; } else { $errors[] = 'Width exceeds Roku maximum of 1280px'; }
        }
        if (isset($config['layout']['height']) && $config['layout']['height'] > 720) {
            if ($admin_authoring) { $warnings[] = 'Height exceeds Roku 720px (authoring space retained)'; } else { $errors[] = 'Height exceeds Roku maximum of 720px'; }
        }

        // Check position bounds
        if (isset($config['layout']['position'])) {
            $x = $config['layout']['position']['x'] ?? 0;
            $y = $config['layout']['position']['y'] ?? 0;
            $width = $config['layout']['width'] ?? 600;
            $height = $config['layout']['height'] ?? 200;

            if ($x + $width > 1280) {
                if ($admin_authoring) { $warnings[] = 'Content block extends beyond Roku screen width (authoring geometry)'; } else { $errors[] = 'Content block extends beyond Roku screen width (1280px)'; }
            }
            if ($y + $height > 720) {
                if ($admin_authoring) { $warnings[] = 'Content block extends beyond Roku screen height (authoring geometry)'; } else { $errors[] = 'Content block extends beyond Roku screen height (720px)'; }
            }
        }

        // Check font sizes for readability on TV
        if (isset($config['typography']['font_size']) && $config['typography']['font_size'] < 16) {
            $warnings[] = 'Font size below 16px may not be readable on TV screens';
        }

        // Check artwork dimensions for Roku optimization
        if (isset($config['artwork']['size'])) {
            $artwork_width = $config['artwork']['size']['width'] ?? 240;
            $artwork_height = $config['artwork']['size']['height'] ?? 240;
            
            if ($artwork_width > 600 || $artwork_height > 600) {
                $warnings[] = 'Artwork size may impact performance on Roku devices';
            }
        }

        return array(
            'valid' => empty($errors),
            'errors' => $errors,
            'warnings' => $warnings,
        );
    }

    /**
     * Get live content data for a content block
     * This method fetches real data from configured sources
     *
     * @param object $content_block Content block with data_config
     * @return array Live data from configured source
     */
    private function get_live_content_data($content_block) {
        // Parse data configuration
        $data_config = null;
        if (!empty($content_block->data_config)) {
            $data_config = json_decode($content_block->data_config, true);
        }
        
        if (!$data_config) {
            return $this->get_fallback_data($content_block->type);
        }
        
        // Fetch live data based on the configured data source
        switch ($data_config['data_source']) {
            case 'metadata_api':
                return $this->fetch_metadata_api_data($data_config);
                
            case 'openweathermap_api_roku_viewer_ip':
                return $this->fetch_weather_data($data_config);
                
            case 'roku_ip_geolocation_api':
                return $this->fetch_location_time_data($data_config);
                
            case 'wp_posts_cc_shoutout':
                return $this->fetch_shoutout_data($data_config);
                
            case 'wp_posts_cc_sponsor':
                return $this->fetch_sponsor_data($data_config);
                
            case 'wp_posts_cc_promo':
                return $this->fetch_promo_data($data_config);
                
            case 'external_api':
                return $this->fetch_external_api_data($data_config);
                
            case 'manual_entry':
                return $this->fetch_manual_data($data_config);
                
            default:
                return $this->get_fallback_data($content_block->type);
        }
    }
    
    /**
     * Fetch live metadata from configured audio metadata API
     */
    private function fetch_metadata_api_data($data_config) {
        $metadata_url = $data_config['endpoint_override'] ?? get_option('castconductor_metadata_url', '');
        
        if (empty($metadata_url)) {
            return array(
                'artist' => 'Configuration Required',
                'title' => 'Set metadata URL in CastConductor settings',
                'album' => 'Setup Required',
                'artwork_url' => '',
                'duration' => '0:00'
            );
        }
        
        // For now, return configuration message - actual API integration would go here
        return array(
            'artist' => 'Live Metadata API',
            'title' => 'Connected to: ' . parse_url($metadata_url, PHP_URL_HOST),
            'album' => 'Real-time Data',
            // Use local branding image for preview to avoid hardcoded external URLs
            'artwork_url' => $this->get_branding_square_logo_url(),
            'duration' => 'LIVE'
        );
    }
    
    /**
     * Fetch live weather data
     */
    /**
     * Fetch live weather data
     */
    private function fetch_weather_data($data_config) {
        // V5: Return styled representative state for editor preview
        return array(
            'location' => 'New York, NY',
            'temperature' => '72°F',
            'condition' => 'Sunny',
            'humidity' => '45%',
            'wind' => '5 mph'
        );
    }
    
    /**
     * Fetch location and time data
     */
    /**
     * Fetch location and time data
     */
    private function fetch_location_time_data($data_config) {
        // V5: Return styled representative state for editor preview
        return array(
            'city' => 'New York',
            'state' => 'NY',
            'time' => '12:00 PM',
            'timezone' => 'EST',
            'date' => 'Wednesday, Oct 2'
        );
    }
    
    /**
     * Fetch shoutout data from actual API endpoint
     */
    private function fetch_shoutout_data($data_config) {
        // Use the actual Content API endpoint instead of direct database query
        $response = wp_remote_get(rest_url('castconductor/v5/content/shoutouts/active'));
        
    if (is_wp_error($response)) {
            return array(
                'name' => 'API Error',
                'message' => 'Failed to fetch shoutout data',
                'location' => 'Error: ' . $response->get_error_message(),
    'artwork_url' => $this->get_branding_square_logo_url()
            );
        }
        
        $body = wp_remote_retrieve_body($response);
        $data = json_decode($body, true);
        
        if (!empty($data['data']) && is_array($data['data'])) {
            $shoutout = $data['data'][0]; // Get first shoutout
            
            // Ensure proper fallback branding image for shoutouts
            $branding_image = $this->get_branding_square_logo_url();
            
            return array(
                'name' => $shoutout['name'] ?? 'Anonymous',
                'message' => $shoutout['message'] ?? 'No message',
                'location' => $shoutout['location'] ?? 'Unknown Location',
                'author' => $shoutout['name'] ?? 'Anonymous',
                'timestamp' => isset($shoutout['timestamp']) ? strtotime($shoutout['timestamp']) : current_time('timestamp'),
                'artwork_url' => $branding_image
            );
        }
        
        return array(
            'name' => 'No Shoutouts Yet',
            'message' => 'Shoutouts will appear here when submitted via the dashboard.',
            'location' => 'CastConductor',
            'author' => 'System',
            'timestamp' => current_time('timestamp'),
            'artwork_url' => $this->get_branding_square_logo_url()
        );
    }

    /**
     * Helper to strip leading/trailing single/double quotes from strings
     */
    private function strip_quotes_if_needed($value) {
        if (!is_string($value)) {
            return $value;
        }
        return trim($value, "\"'\t\n\r ");
    }

    /**
     * Resolve branding square logo URL dynamically from Media Library or fallback to plugin asset
     */
    private function get_branding_square_logo_url() {
        // Prefer current branding logo set by the wizard
        $attachment_id = get_option('castconductor_current_square_logo_600x600');
        if (!empty($attachment_id)) {
            $url = wp_get_attachment_image_url($attachment_id, 'full');
            if (!empty($url)) {
                return $url;
            }
        }

        // Fallback to default branding logo set by the wizard
        $default_id = get_option('castconductor_default_square_logo_600x600');
        if (!empty($default_id)) {
            $url = wp_get_attachment_image_url($default_id, 'full');
            if (!empty($url)) {
                return $url;
            }
        }

        // Final fallback: bundled plugin asset
        return plugin_dir_url(__FILE__) . '../../wizard-content/default-square-logo-600x600.png';
    }

    /**
     * Get current and default branding logo info
     */
    public function get_branding_logo($request) {
        $current_id = get_option('castconductor_current_square_logo_600x600');
        $default_id = get_option('castconductor_default_square_logo_600x600');

        return new WP_REST_Response(array(
            'success' => true,
            'current' => array(
                'id' => $current_id ? (int) $current_id : null,
                'url' => $current_id ? wp_get_attachment_image_url($current_id, 'full') : null,
            ),
            'default' => array(
                'id' => $default_id ? (int) $default_id : null,
                'url' => $default_id ? wp_get_attachment_image_url($default_id, 'full') : null,
            ),
            'fallback' => plugin_dir_url(__FILE__) . '../../wizard-content/default-square-logo-600x600.png',
            'effective_url' => $this->get_branding_square_logo_url(),
        ), 200);
    }

    /**
     * Upload or select a branding logo and update options so previews use it immediately
     */
    public function update_branding_logo($request) {
        if (!current_user_can('manage_options')) {
            return new WP_Error('forbidden', 'Insufficient permissions.', array('status' => 403));
        }

        // Ensure media functions are available
        if (!function_exists('wp_handle_sideload')) {
            require_once ABSPATH . 'wp-admin/includes/file.php';
        }
        if (!function_exists('media_handle_sideload')) {
            require_once ABSPATH . 'wp-admin/includes/media.php';
        }
        if (!function_exists('wp_generate_attachment_metadata')) {
            require_once ABSPATH . 'wp-admin/includes/image.php';
        }

        $mode = $request->get_param('mode') ?: 'both'; // default: update both current and default
        $attachment_id = $request->get_param('attachment_id');
        $image_data = $request->get_param('image_data');
        $url = $request->get_param('url');
        $filename = $request->get_param('filename');

        try {
            if ($attachment_id) {
                $attachment_id = (int) $attachment_id;
                if (get_post_type($attachment_id) !== 'attachment') {
                    return new WP_Error('invalid_attachment', 'Provided attachment_id is not an attachment.', array('status' => 400));
                }
            } elseif (!empty($image_data)) {
                $attachment_id = $this->upload_image_from_base64($image_data, $filename ?: 'branding-logo.png');
            } elseif (!empty($url)) {
                $attachment_id = $this->sideload_image_from_url($url, $filename);
            } else {
                return new WP_Error('missing_image', 'Provide attachment_id, image_data (base64) or url.', array('status' => 400));
            }

            if (!$attachment_id || is_wp_error($attachment_id)) {
                $err = is_wp_error($attachment_id) ? $attachment_id->get_error_message() : 'Upload failed';
                return new WP_Error('upload_failed', $err, array('status' => 500));
            }

            // Update options based on mode
            if ($mode === 'current' || $mode === 'both') {
                update_option('castconductor_current_square_logo_600x600', (int) $attachment_id);
            }
            if ($mode === 'default' || $mode === 'both') {
                update_option('castconductor_default_square_logo_600x600', (int) $attachment_id);
            }

            return new WP_REST_Response(array(
                'success' => true,
                'message' => 'Branding logo updated.',
                'mode' => $mode,
                'attachment' => array(
                    'id' => (int) $attachment_id,
                    'url' => wp_get_attachment_image_url($attachment_id, 'full'),
                ),
                'effective_url' => $this->get_branding_square_logo_url(),
            ), 200);
        } catch (Exception $e) {
            return new WP_Error('branding_update_failed', 'Failed to update branding logo: ' . $e->getMessage(), array('status' => 500));
        }
    }

    /**
     * Handle base64 image upload to Media Library, returns attachment ID
     */
    private function upload_image_from_base64($image_data, $filename = 'branding-logo.png') {
        // Support data URIs: data:image/png;base64,XXXX
        if (strpos($image_data, 'base64,') !== false) {
            $image_data = substr($image_data, strpos($image_data, 'base64,') + 7);
        }
        $decoded = base64_decode($image_data);
        if ($decoded === false) {
            throw new Exception('Invalid base64 image data');
        }

        $upload = wp_upload_bits($filename, null, $decoded);
        if ($upload['error']) {
            throw new Exception($upload['error']);
        }

        $filetype = wp_check_filetype($upload['file'], null);
        $attachment = array(
            'post_mime_type' => $filetype['type'],
            'post_title' => sanitize_file_name($filename),
            'post_content' => '',
            'post_status' => 'inherit'
        );
        $attach_id = wp_insert_attachment($attachment, $upload['file']);

        $attach_data = wp_generate_attachment_metadata($attach_id, $upload['file']);
        wp_update_attachment_metadata($attach_id, $attach_data);

        return $attach_id;
    }

    /**
     * Sideload an image from URL into Media Library, returns attachment ID
     */
    private function sideload_image_from_url($url, $filename = null) {
        // Create a temp file and use media_handle_sideload
        $tmp = download_url($url);
        if (is_wp_error($tmp)) {
            throw new Exception('Failed to download image: ' . $tmp->get_error_message());
        }

        $name = $filename ?: basename(parse_url($url, PHP_URL_PATH)) ?: 'branding-logo.png';
        $file_array = array(
            'name' => $name,
            'tmp_name' => $tmp,
        );

        // Do the sideload
        $attach_id = media_handle_sideload($file_array, 0);
        if (is_wp_error($attach_id)) {
            @unlink($tmp);
            throw new Exception('Failed to sideload image: ' . $attach_id->get_error_message());
        }

        return $attach_id;
    }
    
    /**
     * Fetch sponsor data from actual API endpoint
     */
    private function fetch_sponsor_data($data_config) {
        // Use the actual Content API endpoint instead of direct database query
        $response = wp_remote_get(rest_url('castconductor/v5/content/sponsors/active'));
        
        if (is_wp_error($response)) {
            return array(
                'title' => 'API Error',
                'content' => 'Failed to fetch sponsor data: ' . $response->get_error_message(),
                'artwork_url' => $this->get_branding_square_logo_url(),
                'website' => 'Error State',
                'campaign_end' => 'API Unavailable'
            );
        }
        
        $body = wp_remote_retrieve_body($response);
        $data = json_decode($body, true);
        
        if (!empty($data['data']) && is_array($data['data'])) {
            $sponsor = $data['data'][0]; // Get first sponsor
            return array(
                'title' => $sponsor['title'] ?? 'Untitled Sponsor',
                'content' => $sponsor['content'] ?? 'No sponsor content',
                'artwork_url' => $sponsor['featured_media_url'] ?? $this->get_branding_square_logo_url(),
                'website' => 'Live Campaign Data',
                'campaign_end' => 'Active Campaign'
            );
        }
        
        return array(
            'title' => 'No Active Sponsors',
            'content' => 'Sponsor campaigns will appear here when active.',
            'artwork_url' => $this->get_branding_square_logo_url(),
            'website' => 'CastConductor',
            'campaign_end' => 'Setup Required'
        );
    }
    
    /**
     * Fetch promo data from actual API endpoint
     */
    private function fetch_promo_data($data_config) {
        // Use the actual Content API endpoint instead of direct database query
        $response = wp_remote_get(rest_url('castconductor/v5/content/promos/active'));
        
        if (is_wp_error($response)) {
            return array(
                'title' => 'API Error',
                'content' => 'Failed to fetch promo data: ' . $response->get_error_message(),
                'artwork_url' => $this->get_branding_square_logo_url(),
                'event_date' => 'Error State',
                'start_date' => 'API Unavailable',
                'end_date' => 'Error'
            );
        }
        
        $body = wp_remote_retrieve_body($response);
        $data = json_decode($body, true);
        
        if (!empty($data['data']) && is_array($data['data'])) {
            $promo = $data['data'][0]; // Get first promo
            return array(
                'title' => $promo['title'] ?? 'Untitled Promo',
                'content' => $promo['content'] ?? 'No promo content',
                'artwork_url' => $promo['featured_media_url'] ?? $this->get_branding_square_logo_url(),
                'event_date' => 'Live Event',
                'start_date' => 'Live Promo',
                'end_date' => 'Active Now'
            );
        }
        
        return array(
            'title' => 'No Active Promos',
            'content' => 'Promotional content will appear here when scheduled.',
            'artwork_url' => $this->get_branding_square_logo_url(),
            'event_date' => 'Setup Required',
            'start_date' => 'CastConductor',
            'end_date' => 'Setup Required'
        );
    }
    
    /**
     * Fetch external API data
     */
    private function fetch_external_api_data($data_config) {
        return array(
            'title' => 'External API Connected',
            'description' => 'Live data from configured API endpoint',
            'status' => 'Real-time Integration',
            'last_update' => 'Live Updates'
        );
    }
    
    /**
     * Fetch manual entry data
     */
    private function fetch_manual_data($data_config) {
        return array(
            'title' => 'Manual Content Entry',
            'message' => 'Content managed through WordPress admin',
            'note' => 'Live admin-managed content'
        );
    }
    
    /**
     * Get fallback data when configuration is missing
     */
    private function get_fallback_data($content_type) {
        return array(
            'error' => 'Configuration Missing',
            'message' => 'Content block needs data source configuration',
            'type' => $content_type,
            'status' => 'Setup Required'
        );
    }

    /**
     * Get REAL content block data (V5 Philosophy: NO DUMMY DATA)
     * Fetches actual content blocks and their live data
     *
     * @param string $content_type Content block type
     * @return array Real live data from actual content blocks
     */
    private function get_real_content_block_data($content_type) {
        global $wpdb;
        
        // Find actual content block of the requested type
        $table_name = $wpdb->prefix . 'castconductor_content_blocks';
        $content_block = $wpdb->get_row($wpdb->prepare(
            "SELECT * FROM {$table_name} WHERE type = %s AND enabled = 1 ORDER BY created_at DESC LIMIT 1",
            $content_type
        ));
        
        if (!$content_block) {
            // No content block found - return configuration message instead of dummy data
            return [
                'error' => true,
                'message' => "No {$content_type} content block configured yet",
                'action' => 'Create a content block in the Canvas Editor to see live preview data'
            ];
        }
        
        // Use the Content Blocks Controller to fetch live data
        $content_blocks_controller = new CastConductor_Content_Blocks_Controller();
        $live_data = $content_blocks_controller->fetch_live_data($content_block);
        
        // If live data fetch failed, return error info instead of dummy data
        if (isset($live_data['error'])) {
            return [
                'error' => true,
                'message' => $live_data['error'],
                'content_block_id' => $content_block->id,
                'content_block_name' => $content_block->name
            ];
        }
        
        return $live_data;
    }

    /**
     * Public helper to get preview HTML for a content block
     * Used by shortcodes and other integrations
     *
     * @param object $block Content block database row
     * @param array $visual_config Parsed visual configuration
     * @param array $live_data Live data from fetch_live_data
     * @return string HTML preview
     */
    public function get_preview_html_for_block($block, $visual_config = null, $live_data = null) {
        if (!$visual_config && !empty($block->visual_config)) {
            $visual_config = json_decode($block->visual_config, true);
        }
        if (!$live_data) {
            require_once CASTCONDUCTOR_PLUGIN_DIR . 'includes/api/class-content-blocks-controller.php';
            $cbc = new CastConductor_Content_Blocks_Controller();
            $live_data = $cbc->fetch_live_data($block);
        }
        return $this->render_preview_html($visual_config ?: [], $live_data ?: [], $block->type);
    }

    /**
     * Render preview HTML with REAL DATA
     *
     * @param array $config Visual configuration
     * @param array $data Real live data (NO DUMMY DATA)
     * @return string HTML preview
     */
    private function render_preview_html($config, $data, $content_type = null) {
        // Device-rendered blocks (location/weather): show informative message in WP admin only
        // This message is for WordPress admin preview ONLY - Roku never sees this
        if (isset($data['device_rendered']) && $data['device_rendered']) {
            $html  = '<div class="castconductor-preview-device-rendered" style="';
            $html .= 'position:absolute;top:0;left:0;right:0;bottom:0;';
            $html .= 'display:flex;flex-direction:column;align-items:center;justify-content:center;';
            $html .= 'background:rgba(0,0,0,0.75);';
            $html .= 'padding:24px;text-align:center;color:#fff;font-family:system-ui,sans-serif;';
            $html .= '">';
            $html .= '<div style="font-size:32px;margin-bottom:12px;">📍</div>';
            $html .= '<h3 style="margin:0 0 12px 0;font-size:18px;font-weight:600;color:#fff;">Device-Rendered Content</h3>';
            if (!empty($data['message'])) {
                $html .= '<p style="margin:0 0 10px 0;font-size:14px;color:#e2e8f0;line-height:1.4;">' . esc_html($data['message']) . '</p>';
            }
            if (!empty($data['privacy_note'])) {
                $html .= '<p style="margin:0 0 10px 0;font-size:13px;color:#94a3b8;">🔒 ' . esc_html($data['privacy_note']) . '</p>';
            }
            if (!empty($data['preview_note'])) {
                $html .= '<p style="margin:0;font-size:12px;font-style:italic;color:#64748b;">' . esc_html($data['preview_note']) . '</p>';
            }
            $html .= '</div>';
            return $html;
        }
        
        // Error state: show config guidance but no fabricated data
        if (isset($data['error']) && $data['error']) {
            $html  = '<div class="castconductor-preview-error">';
            $html .= '<h3>⚠️ No Live Data Available</h3>';
            if (!empty($data['message'])) {
                $html .= '<p><strong>Message:</strong> ' . esc_html($data['message']) . '</p>';
            }
            if (!empty($data['action'])) {
                $html .= '<p><strong>Action needed:</strong> ' . esc_html($data['action']) . '</p>';
            }
            $html .= '<p><em>No dummy preview. Configure a content block to see live output.</em></p>';
            $html .= '</div>';
            return $html;
        }

        // Minimal inline styles to ensure typography parity even if external CSS is not applied
        $fontFamily = isset($config['typography']['font_family']) && $config['typography']['font_family'] ? $config['typography']['font_family'] : 'system-ui,Arial,Helvetica,sans-serif';
        $fontSize = isset($config['typography']['font_size']) ? (int)$config['typography']['font_size'] : 24;
        $fontWeight = isset($config['typography']['font_weight']) && $config['typography']['font_weight'] !== '' ? $config['typography']['font_weight'] : '400';
        $fontColor = isset($config['typography']['color']) && $config['typography']['color'] ? $config['typography']['color'] : '#ffffff';
        $lineHeight = isset($config['typography']['line_height']) && floatval($config['typography']['line_height'])>0 ? floatval($config['typography']['line_height']) : 1.2;
        $textAlign = isset($config['typography']['text_align']) && $config['typography']['text_align'] ? $config['typography']['text_align'] : 'left';
        $inlineStyle = sprintf('font-family:%s;color:%s;', esc_attr($fontFamily), esc_attr($fontColor));
        $inlineTextWrap = sprintf('font-size:%dpx;line-height:%s;text-align:%s;font-weight:%s;display:flex;flex-direction:column;gap:2px;',
            $fontSize,
            esc_attr($lineHeight),
            esc_attr($textAlign),
            esc_attr($fontWeight)
        );

        $html = '<div class="castconductor-preview-block" style="' . $inlineStyle . '" data-config="' . esc_attr(json_encode($config)) . '">';

        // Context-aware token gating: In Canvas Editor (no frame_context),
        // suppress server-rendered text/artwork if token layers exist to avoid double render.
        // In Scenes & Containers (frame_context present), always render real data.
        $hasTokenText = false;
        $hasTokenImage = false;
        if (!empty($config['layers']) && is_array($config['layers'])) {
            foreach ($config['layers'] as $layer) {
                if (!is_array($layer) || empty($layer['kind'])) continue;
                if ($layer['kind'] === 'token-text') { $hasTokenText = true; }
                if ($layer['kind'] === 'token-image') { $hasTokenImage = true; }
                if ($hasTokenText && $hasTokenImage) break;
            }
        }
        $frameCtx = isset($config['behavior']['frame_context']) ? $config['behavior']['frame_context'] : array();
        $isScenesPreview = is_array($frameCtx) && (isset($frameCtx['zone_id']) || isset($frameCtx['width']) || isset($frameCtx['height']) || isset($frameCtx['x']) || isset($frameCtx['y']));
        $suppressText = $hasTokenText;
        $suppressImage = $hasTokenImage;

    // Artwork / slideshow (multi-image). Prefer config artwork items; fallback to live data single url
        if (!empty($config['artwork']['enabled']) && !$suppressImage) {
            $html .= '<div class="artwork-container">';
            $items = array();
            if (!empty($config['artwork']['items']) && is_array($config['artwork']['items'])) {
                $items = $config['artwork']['items'];
            } elseif (!empty($data['artwork_url'])) {
                $items = array(array('src' => $data['artwork_url'], 'alt' => ''));
            }
            $activeIndex = isset($config['artwork']['activeIndex']) ? intval($config['artwork']['activeIndex']) : 0;
            $i = 0;
            foreach ($items as $it) {
                if (empty($it['src'])) { $i++; continue; }
                $alt = isset($it['alt']) ? $it['alt'] : '';
                $cls = ($i === $activeIndex) ? 'ccve-artwork-active' : '';
                $html .= '<img class="' . esc_attr($cls) . '" src="' . esc_url($it['src']) . '" alt="' . esc_attr($alt) . '" />';
                $i++;
            }
            $html .= '</div>';
        }

        $ticker_enabled = !empty($config['behavior']['ticker']['enabled']);
        if (!$suppressText && $ticker_enabled) {
            $html .= '<div class="text-content" style="' . $inlineTextWrap . '"><div class="ticker"><div class="ticker-inner">';
        } elseif (!$suppressText) {
            $html .= '<div class="text-content" style="' . $inlineTextWrap . '">';
        }

        // Collect real data fields WITHOUT substituting placeholders, frame-relative
            $ct = $content_type ?: '';
            
            // NEW: Server-Side Layer Rendering (SSLR)
            // If layers are defined, we MUST render them to respect the authored design (position, style, content).
            // This replaces the previous hardcoded data dump which ignored the user's layout.
            // UPDATED: Now supports Scenes/Containers via coordinate translation (Global -> Local).
            if (!empty($config['layers']) && is_array($config['layers'])) {
                // Calculate offsets for Scenes/Containers
                // Check for 'container-relative' coordinate system flag - if set, layers are already
                // relative to container origin and don't need offset subtraction
                $offsetX = 0;
                $offsetY = 0;
                $coordSystem = isset($config['layout']['coords']) ? $config['layout']['coords'] : 'canvas-absolute';
                
                if ($coordSystem !== 'container-relative' && $isScenesPreview && isset($frameCtx['x']) && isset($frameCtx['y'])) {
                    // Legacy: layers stored with absolute canvas coordinates, need to subtract container offset
                    $offsetX = intval($frameCtx['x']);
                    $offsetY = intval($frameCtx['y']);
                }
                // If coords === 'container-relative', layers are already relative to container, use as-is

                $html .= '<div class="cc-layers-renderer" style="position:absolute;top:0;left:0;width:100%;height:100%;pointer-events:none;">';
                
                // FIRST: Render background/overlay layers from config['background']['layers']
                // These layers form the visual foundation (backgrounds, overlays) and must render BELOW content layers
                if (!empty($config['background']['layers']) && is_array($config['background']['layers'])) {
                    foreach ($config['background']['layers'] as $bgLayer) {
                        if (empty($bgLayer['kind']) || (isset($bgLayer['enabled']) && !$bgLayer['enabled'])) continue;
                        
                        // Background/overlay layers are full-canvas by default
                        $css = 'position:absolute;left:0;top:0;width:100%;height:100%;pointer-events:none;';
                        
                        if ($bgLayer['kind'] === 'overlay') {
                            // Use overlay config for color/opacity
                            $overlayConfig = isset($config['overlay']) ? $config['overlay'] : array();
                            $overlayColor = !empty($overlayConfig['color']) ? $overlayConfig['color'] : 'rgba(0,0,0,0.5)';
                            $overlayOpacity = isset($overlayConfig['opacity']) ? floatval($overlayConfig['opacity']) : 0.5;
                            
                            // Convert hex to rgba if needed
                            if (strpos($overlayColor, '#') === 0) {
                                $overlayColor = $this->hex_to_rgba($overlayColor, $overlayOpacity);
                            } elseif (strpos($overlayColor, 'rgba') !== 0 && strpos($overlayColor, 'rgb') !== 0) {
                                // If it's not already rgb/rgba, wrap it
                                $overlayColor = "rgba(0,0,0,{$overlayOpacity})";
                            }
                            
                            $css .= 'background-color:' . esc_attr($overlayColor) . ';';
                            $html .= '<div class="cc-layer-background cc-layer-overlay" style="' . esc_attr($css) . '"></div>';
                            
                        } elseif ($bgLayer['kind'] === 'image' && !empty($bgLayer['image']['src'])) {
                            // Background image layer
                            $src = esc_url($bgLayer['image']['src']);
                            $fit = !empty($bgLayer['image']['fit']) ? $bgLayer['image']['fit'] : 'cover';
                            $position = !empty($bgLayer['image']['position']) ? $bgLayer['image']['position'] : 'center';
                            $css .= 'background-image:url(' . $src . ');';
                            $css .= 'background-size:' . esc_attr($fit) . ';';
                            $css .= 'background-position:' . esc_attr($position) . ';';
                            $html .= '<div class="cc-layer-background cc-layer-image" style="' . esc_attr($css) . '"></div>';
                            
                        } elseif ($bgLayer['kind'] === 'gradient') {
                            // Gradient layer
                            $bg = isset($config['background']) ? $config['background'] : array();
                            $colors = isset($bg['gradient_colors']) && is_array($bg['gradient_colors']) ? $bg['gradient_colors'] : array();
                            if (count($colors) >= 2) {
                                $dir = isset($bg['gradient_direction']) ? $bg['gradient_direction'] : '135deg';
                                $css .= 'background-image:linear-gradient(' . esc_attr($dir) . ', ' . implode(', ', array_map('esc_attr', $colors)) . ');';
                                $html .= '<div class="cc-layer-background cc-layer-gradient" style="' . esc_attr($css) . '"></div>';
                            }
                        }
                    }
                }
                
                // SECOND: Render content layers (text, images) from config['layers']
                foreach ($config['layers'] as $layer) {
                    if (empty($layer['kind']) || (isset($layer['visible']) && !$layer['visible'])) continue;
                    
                    // Resolve style
                    $style = isset($layer['style']) ? $layer['style'] : array();
                    $css = 'position:absolute;';
                    
                    // Apply translation
                    $layerX = (isset($layer['x']) ? intval($layer['x']) : 0) - $offsetX;
                    $layerY = (isset($layer['y']) ? intval($layer['y']) : 0) - $offsetY;
                    
                    $css .= 'left:' . $layerX . 'px;';
                    $css .= 'top:' . $layerY . 'px;';
                    $css .= 'width:' . (isset($layer['width']) ? intval($layer['width']) : 100) . 'px;';
                    $css .= 'height:' . (isset($layer['height']) ? intval($layer['height']) : 40) . 'px;';
                    
                    if ($layer['kind'] === 'token-text' || $layer['kind'] === 'static-text') {
                        // Typography
                        $fontFamily = !empty($style['font_family']) ? $style['font_family'] : 'inherit';
                        $fontSize = !empty($style['font_size']) ? intval($style['font_size']) : 24;
                        $fontWeight = !empty($style['font_weight']) ? $style['font_weight'] : 'normal';
                        $color = !empty($style['color']) ? $style['color'] : 'inherit';
                        $textAlign = !empty($style['text_align']) ? $style['text_align'] : 'left';
                        $lineHeight = !empty($style['line_height']) ? $style['line_height'] : 1.2;
                        
                        $css .= sprintf('font-family:%s;font-size:%dpx;font-weight:%s;color:%s;text-align:%s;line-height:%s;',
                            esc_attr($fontFamily), $fontSize, esc_attr($fontWeight), esc_attr($color), esc_attr($textAlign), esc_attr($lineHeight)
                        );
                        
                        // Content & Token Replacement
                        $text = isset($layer['templateText']) ? $layer['templateText'] : (isset($layer['text']) ? $layer['text'] : '');
                        
                        if ($layer['kind'] === 'token-text') {
                            // Simple token replacement (parity with JS resolveTemplate)
                            $replacements = array(
                                '{{track.artist}}' => isset($data['artist']) ? $data['artist'] : '',
                                '{{track.title}}' => isset($data['title']) ? $data['title'] : '',
                                '{{location.time}}' => isset($data['time']) ? $data['time'] : '',
                                '{{location.city}}' => isset($data['city']) ? $data['city'] : '',
                                '{{location.state}}' => isset($data['state']) ? $data['state'] : '',
                                '{{location.date}}' => isset($data['date']) ? $data['date'] : '',
                                '{{location}}' => isset($data['location']) ? $data['location'] : '',
                                '{{time}}' => isset($data['time']) ? $data['time'] : '',
                                '{{date}}' => isset($data['date']) ? $data['date'] : '',
                                '{{weather.temperature}}' => isset($data['temperature']) ? $data['temperature'] : '',
                                '{{weather.condition}}' => isset($data['condition']) ? $data['condition'] : '',
                                '{{weather.unit}}' => isset($data['unit']) ? $data['unit'] : '°F',
                                '{{weather.location}}' => isset($data['location']) ? $data['location'] : '',
                                '{{shoutout.name}}' => isset($data['name']) ? $data['name'] : '',
                                '{{shoutout.location}}' => isset($data['location']) ? $data['location'] : '',
                                '{{shoutout.message}}' => isset($data['message']) ? $data['message'] : '',
                                '{{promo.title}}' => isset($data['title']) ? $data['title'] : '',
                                '{{promo.content}}' => isset($data['content']) ? $data['content'] : '',
                                '{{sponsor.title}}' => isset($data['title']) ? $data['title'] : '',
                                '{{sponsor.content}}' => isset($data['content']) ? $data['content'] : '',
                            );
                            
                            foreach ($replacements as $token => $val) {
                                $text = str_replace($token, $val, $text);
                            }
                        }
                        
                        // Ticker / Scrolling Text Support
                        // If 'ticker' behavior is enabled for this layer, wrap in marquee structure
                        $isTicker = !empty($layer['behavior']['ticker']);
                        if ($isTicker) {
                            $css .= 'white-space:nowrap;overflow:hidden;';
                            $html .= '<div class="cc-layer-text cc-ticker-host" style="' . esc_attr($css) . '">';
                            $html .= '<div class="cc-ticker-inner" style="display:inline-block;padding-left:100%;animation:cc-ticker-scroll 10s linear infinite;">';
                            $html .= esc_html($text);
                            $html .= '</div></div>';
                            // Note: Animation keyframes must be present in global CSS or injected
                        } else {
                            $html .= '<div class="cc-layer-text" style="' . esc_attr($css) . '">' . esc_html($text) . '</div>';
                        }
                        
                    } elseif ($layer['kind'] === 'token-image' || $layer['kind'] === 'static-image') {
                        // Image handling
                        $src = '';
                        if ($layer['kind'] === 'token-image') {
                            $token = isset($layer['token']) ? $layer['token'] : '';
                            // Map tokens to data fields
                            if ($token === 'track.artwork' && !empty($data['artwork_url'])) $src = $data['artwork_url'];
                            elseif ($token === 'sponsor.artwork' && !empty($data['artwork_url'])) $src = $data['artwork_url'];
                            elseif ($token === 'promo.artwork' && !empty($data['artwork_url'])) $src = $data['artwork_url'];
                            elseif ($token === 'shoutout.artwork' && !empty($data['artwork_url'])) $src = $data['artwork_url'];
                            // Fallback to layer image if no dynamic data
                            if (empty($src) && !empty($layer['image']['src'])) $src = $layer['image']['src'];
                        } else {
                            // Static image - uses 'url' property
                            $src = !empty($layer['url']) ? $layer['url'] : '';
                        }
                        
                        if (!empty($src)) {
                            $fit = !empty($layer['fit']) ? $layer['fit'] : 'cover';
                            $css .= 'object-fit:' . esc_attr($fit) . ';';
                            $html .= '<img class="cc-layer-image" src="' . esc_url($src) . '" style="' . esc_attr($css) . '" />';
                        }
                    } elseif ($layer['kind'] === 'qr-image') {
                        // QR Code layer - render the pre-generated QR image URL
                        $qrUrl = isset($layer['url']) ? $layer['url'] : '';
                        if (empty($qrUrl) && isset($layer['sourceUrl'])) {
                            // QR not pre-generated, would need to generate on-the-fly
                            // For now, skip or use placeholder
                            $qrUrl = '';
                        }
                        if (!empty($qrUrl)) {
                            $css .= 'object-fit:contain;';
                            $html .= '<img class="cc-layer-qr" src="' . esc_attr($qrUrl) . '" style="' . esc_attr($css) . '" />';
                        }
                    } elseif ($layer['kind'] === 'slideshow-image') {
                        // Slideshow layer - render the first image (static preview)
                        $sources = isset($layer['sources']) && is_array($layer['sources']) ? $layer['sources'] : [];
                        if (!empty($sources) && isset($sources[0]['url'])) {
                            $src = $sources[0]['url'];
                            $fit = !empty($layer['objectFit']) ? $layer['objectFit'] : 'cover';
                            $css .= 'object-fit:' . esc_attr($fit) . ';';
                            $html .= '<img class="cc-layer-slideshow" src="' . esc_url($src) . '" style="' . esc_attr($css) . '" />';
                        }
                    } elseif ($layer['kind'] === 'background-image') {
                        // Background image layer - full-canvas background
                        $src = isset($layer['url']) ? $layer['url'] : '';
                        if (!empty($src)) {
                            $fit = !empty($layer['object_fit']) ? $layer['object_fit'] : 'cover';
                            $css .= 'object-fit:' . esc_attr($fit) . ';';
                            $opacity = isset($layer['opacity']) ? floatval($layer['opacity']) : 1.0;
                            if ($opacity < 1.0) {
                                $css .= 'opacity:' . $opacity . ';';
                            }
                            $html .= '<img class="cc-layer-bg-image" src="' . esc_url($src) . '" style="' . esc_attr($css) . '" />';
                        }
                    } elseif ($layer['kind'] === 'wordpress-post') {
                        // WordPress Post layer - render post content if loaded
                        $postId = isset($layer['post_id']) ? intval($layer['post_id']) : 0;
                        if ($postId > 0) {
                            $post = get_post($postId);
                            if ($post) {
                                $html .= '<div class="cc-layer-wp-post" style="' . esc_attr($css) . '">';
                                $html .= '<div class="wp-post-title" style="font-weight:bold;">' . esc_html($post->post_title) . '</div>';
                                $excerpt = wp_trim_words($post->post_content, 20, '...');
                                $html .= '<div class="wp-post-excerpt">' . esc_html($excerpt) . '</div>';
                                $html .= '</div>';
                            }
                        }
                    } elseif ($layer['kind'] === 'background' || $layer['kind'] === 'overlay') {
                        // Background/Overlay Layer (Shape)
                        // Usually just a colored rectangle, possibly with opacity
                        $bgColor = !empty($style['background_color']) ? $style['background_color'] : 'transparent';
                        $opacity = isset($style['opacity']) ? floatval($style['opacity']) : 1.0;
                        
                        // If opacity is < 1, convert hex to rgba or use opacity property
                        if ($opacity < 1.0) {
                            $css .= 'opacity:' . $opacity . ';';
                        }
                        $css .= 'background-color:' . esc_attr($bgColor) . ';';
                        
                        $html .= '<div class="cc-layer-shape" style="' . esc_attr($css) . '"></div>';
                    }
                }
                $html .= '</div>';
                
            } elseif ($suppressText) {
                // No text markup when suppressed (Canvas Editor will render token-text itself)
            } else {
                // Fallback: Hardcoded data dump (Legacy behavior for blocks without layers)
            if ($ct === 'track_info') {
                if (isset($data['artist'])) {
                    $html .= ($ticker_enabled ? '<div class="ticker-line artist">' : '<div class="artist">') . esc_html($data['artist']) . '</div>';
                    $renderedAnyTextLine = true;
                }
                if (isset($data['title'])) {
                    $html .= ($ticker_enabled ? '<div class="ticker-line title">' : '<div class="title">') . esc_html($data['title']) . '</div>';
                    $renderedAnyTextLine = true;
                }
            } else if ($ct === 'location_time') {
                if (isset($data['city']) || isset($data['state'])) {
                    $html .= ($ticker_enabled ? '<div class="ticker-line location">' : '<div class="location">') . esc_html(trim(($data['city'] ?? '') . (isset($data['state']) ? (', ' . $data['state']) : ''))) . '</div>';
                    $renderedAnyTextLine = true;
                }
                if (isset($data['time'])) {
                    $html .= ($ticker_enabled ? '<div class="ticker-line time">' : '<div class="time">') . esc_html($data['time']) . '</div>';
                    $renderedAnyTextLine = true;
                }
                if (isset($data['date'])) {
                    $html .= ($ticker_enabled ? '<div class="ticker-line date">' : '<div class="date">') . esc_html($data['date']) . '</div>';
                    $renderedAnyTextLine = true;
                }
            } else if ($ct === 'shoutout') {
                if (isset($data['name'])) {
                    $html .= ($ticker_enabled ? '<div class="ticker-line name">' : '<div class="name">') . esc_html($data['name']) . '</div>';
                    $renderedAnyTextLine = true;
                }
                if (isset($data['location'])) {
                    $html .= ($ticker_enabled ? '<div class="ticker-line location">' : '<div class="location">') . esc_html($data['location']) . '</div>';
                    $renderedAnyTextLine = true;
                }
                if (isset($data['message'])) {
                    $html .= ($ticker_enabled ? '<div class="ticker-line message">' : '<div class="message">') . esc_html($data['message']) . '</div>';
                    $renderedAnyTextLine = true;
                }
            } else if ($ct === 'sponsor') {
                if (isset($data['title'])) {
                    $html .= ($ticker_enabled ? '<div class="ticker-line title">' : '<div class="title">') . esc_html($data['title']) . '</div>';
                    $renderedAnyTextLine = true;
                }
                if (isset($data['content'])) {
                    $html .= ($ticker_enabled ? '<div class="ticker-line content">' : '<div class="content">') . esc_html(wp_strip_all_tags($data['content'])) . '</div>';
                    $renderedAnyTextLine = true;
                }
            } else if ($ct === 'promo') {
                if (isset($data['title'])) {
                    $html .= ($ticker_enabled ? '<div class="ticker-line title">' : '<div class="title">') . esc_html($data['title']) . '</div>';
                    $renderedAnyTextLine = true;
                }
                if (isset($data['event_date'])) {
                    $html .= ($ticker_enabled ? '<div class="ticker-line date">' : '<div class="date">') . esc_html($data['event_date']) . '</div>';
                    $renderedAnyTextLine = true;
                }
                if (isset($data['content'])) {
                    $html .= ($ticker_enabled ? '<div class="ticker-line content">' : '<div class="content">') . esc_html(wp_strip_all_tags($data['content'])) . '</div>';
                    $renderedAnyTextLine = true;
                }
            } else if ($ct === 'weather') {
                if (isset($data['location'])) {
                    $html .= ($ticker_enabled ? '<div class="ticker-line location">' : '<div class="location">') . esc_html($data['location']) . '</div>';
                    $renderedAnyTextLine = true;
                }
                if (isset($data['temperature']) || isset($data['condition'])) {
                    $line = trim(($data['temperature'] ?? '') . (isset($data['condition']) ? (' • ' . $data['condition']) : ''));
                    if ($line !== '') {
                        $html .= ($ticker_enabled ? '<div class="ticker-line weather">' : '<div class="weather">') . esc_html($line) . '</div>';
                        $renderedAnyTextLine = true;
                    }
                }
            } else {
                // Generic fallback
                if (isset($data['message'])) {
                    $html .= ($ticker_enabled ? '<div class="ticker-line message">' : '<div class="message">') . esc_html($data['message']) . '</div>';
                    $renderedAnyTextLine = true;
                }
                if (isset($data['content'])) {
                    $html .= ($ticker_enabled ? '<div class="ticker-line content">' : '<div class="content">') . esc_html(wp_strip_all_tags($data['content'])) . '</div>';
                    $renderedAnyTextLine = true;
                }
                if (!isset($data['message']) && !isset($data['content'])) {
                    // Attempt common track-like fields if present
                    if (isset($data['artist'])) {
                        $html .= ($ticker_enabled ? '<div class="ticker-line artist">' : '<div class="artist">') . esc_html($data['artist']) . '</div>';
                        $renderedAnyTextLine = true;
                    }
                    if (isset($data['title'])) {
                        $html .= ($ticker_enabled ? '<div class="ticker-line title">' : '<div class="title">') . esc_html($data['title']) . '</div>';
                        $renderedAnyTextLine = true;
                    }
                }
            }
        }
    // Intentionally omit internal meta fields like 'status' (e.g. 'live_data') to avoid polluting user preview.

        // Close wrappers
        if (!$suppressText && $ticker_enabled) {
            $html .= '</div></div></div>'; // ticker-inner, ticker, text-content
        } elseif (!$suppressText) {
            $html .= '</div>'; // text-content
        }
        $html .= '</div>'; // outer block

        return $html;
    }

    /**
     * Generate preview CSS extracted from previously inlined (and corrupted) logic.
     * This produces styling only; no dummy content. Safe defaults only when explicitly configured.
     */
    private function generate_preview_css($config) {
        $css = '';
        
        // Import Google Fonts if needed (for Shadow DOM isolation)
        $fontFamily = isset($config['typography']['font_family']) && $config['typography']['font_family'] ? $config['typography']['font_family'] : 'system-ui,Arial,Helvetica,sans-serif';
        if (stripos($fontFamily, 'Roboto') !== false) {
            $css .= '@import url(\'https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500;700&display=swap\');';
        }

        // Ticker Animation Keyframes
        $css .= '@keyframes cc-ticker-scroll { 0% { transform: translateX(0); } 100% { transform: translateX(-100%); } }';

        // Basic container styling
        $css .= '.castconductor-preview-block {';
        // Basic box model / layout
        $css .= 'display:flex;align-items:center;box-sizing:border-box;position:relative;';
        // Fill frame by default; if explicit width/height provided use them.
        // IMPORTANT INVARIANT: For canonical lower_third designs (1280×240) we want
        // 1:1 parity between Canvas Editor and Scenes/Containers. That means we
        // respect authored layout width/height here and let the outer frame decide
        // when to scale, rather than forcing 100% height from the frame side.
        if (!empty($config['layout']['width'])) {
            $css .= 'width:' . intval($config['layout']['width']) . 'px;';
        } else {
            $css .= 'width:100%;';
        }
        if (!empty($config['layout']['height'])) {
            $css .= 'height:' . intval($config['layout']['height']) . 'px;';
        } else {
            $css .= 'height:100%;';
        }

        // Layout
        $layout = isset($config['layout']) ? $config['layout'] : array();
        $padding = isset($layout['padding']) ? $layout['padding'] : array();
        
        // Legacy padding fallback
        if (empty($padding) && isset($config['padding'])) {
            $padding = $config['padding'];
        }

        $top = isset($padding['top']) ? intval($padding['top']) : 0;
        $right = isset($padding['right']) ? intval($padding['right']) : 0;
        $bottom = isset($padding['bottom']) ? intval($padding['bottom']) : 0;
        $left = isset($padding['left']) ? intval($padding['left']) : 0;

        // DEBUG LOGGING
        if (defined('WP_DEBUG') && WP_DEBUG) {
            error_log('[CC] CSS gen padding: top=' . $top . ' right=' . $right . ' bottom=' . $bottom . ' left=' . $left);
        }

        $css .= 'padding:' . $top . 'px ' . $right . 'px ' . $bottom . 'px ' . $left . 'px;';

        // Background layering (image / gradient / overlay) using background.layers ordering (top -> bottom)
        if (!empty($config['background']) && is_array($config['background'])) {
            $bg = $config['background'];
            $layers = isset($bg['layers']) && is_array($bg['layers']) ? $bg['layers'] : array();
            $bgImages = array();
            
            // Determine fallback background-color
            $baseColor = isset($bg['color']) ? $bg['color'] : 'transparent';
            $baseOpacity = isset($bg['opacity']) ? floatval($bg['opacity']) : 1.0;
            
            // Convert hex to rgba if opacity < 1 and color is not transparent
            if ($baseColor !== 'transparent' && $baseColor !== '' && $baseOpacity < 1.0) {
                $baseColor = $this->hex_to_rgba($baseColor, $baseOpacity);
            }

            // DEBUG LOGGING
            if (defined('WP_DEBUG') && WP_DEBUG) {
                error_log('[CC] CSS gen background: baseColor=' . $baseColor . ' opacity=' . $baseOpacity);
            }

            // Only use base color if explicitly provided in config; do not paint implicit dark bases
            $useBaseColor = isset($bg['color']) && $bg['color'] !== '' && strtolower(trim($bg['color'])) !== 'transparent';
            foreach($layers as $layer) {
                if (empty($layer['kind']) || (isset($layer['enabled']) && !$layer['enabled'])) continue;
                $kind = $layer['kind'];
                if ($kind === 'overlay') {
                    $overlayColor = $this->normalize_overlay_color(isset($config['overlay']) ? $config['overlay'] : array());
                    if ($overlayColor) {
                        // Overlay should be topmost -> push at beginning (we will reverse later if needed)
                        $bgImages[] = 'linear-gradient(0deg,' . $overlayColor . ',' . $overlayColor . ')';
                        // IMPORTANT: When overlay is present, do NOT add base color as an additional layer.
                        // The overlay IS the opacity control - adding base color creates double-darkening.
                        // The overlay renders OVER the scene background/branding, controlled by overlay.opacity.
                        $useBaseColor = false;
                    }
                } elseif ($kind === 'gradient') {
                    $colors = isset($bg['gradient_colors']) && is_array($bg['gradient_colors']) ? $bg['gradient_colors'] : array();
                    if (count($colors) >= 2) {
                        $dir = isset($bg['gradient_direction']) ? $bg['gradient_direction'] : '135deg';
                        $bgImages[] = 'linear-gradient(' . $dir . ', ' . implode(', ', $colors) . ')';
                        // Gradient explicitly defines its own background; do not add an extra base
                        // Keep $useBaseColor as-is (it already reflects explicit solid color presence)
                    }
                } elseif ($kind === 'image') {
                if (isset($layer['image']) && is_array($layer['image']) && !empty($layer['image']['src'])) {
                    $src = esc_url_raw($layer['image']['src']);
                    $bgImages[] = 'url("' . $src . '")';
                }
            }
        }
        
        if (!empty($bgImages)) {
            // If we have layers, use them. 
            // If we ALSO have a base color (and it's not transparent), add it as the last layer
            if ($useBaseColor) {
                $bgImages[] = 'linear-gradient(0deg, ' . $baseColor . ', ' . $baseColor . ')';
            }
            $css .= 'background-image:' . implode(',', $bgImages) . ';';
            $css .= 'background-size:cover;background-position:center;';
        } elseif ($useBaseColor) {
            // No layers, just base color
            $css .= 'background-color:' . $baseColor . ';';
        }

        if (isset($bg['border']['width']) && ($bg['border']['width'] > 0)) {
            $b = $bg['border'];
            $css .= 'border:' . ($b['width'] ?? 0) . 'px ' . ($b['style'] ?? 'solid') . ' ' . ($b['color'] ?? '#000000') . ';';
        }
        if (isset($bg['border_radius'])) {
            $css .= 'border-radius:' . intval($bg['border_radius']) . 'px;';
        }
    }
    $css .= '}';

    // Artwork Sizing & Layout
    // Prioritize explicit Canvas Editor settings, fallback to Block Presets (e.g. Track Info)
    $artWidth = '';
    $artHeight = '';
    
    if (!empty($config['artwork']['width'])) {
        $artWidth = intval($config['artwork']['width']) . 'px';
    }
    if (!empty($config['artwork']['height'])) {
        $artHeight = intval($config['artwork']['height']) . 'px';
    }
    
    // Fallback to display_settings.artwork_size if no explicit geometry
    if (empty($artWidth) || $artWidth === '0px') {
        $size = !empty($config['display_settings']['artwork_size']) ? $config['display_settings']['artwork_size'] : 'medium';
        
        if (defined('WP_DEBUG') && WP_DEBUG) {
            error_log('[CC] Artwork sizing debug: size=' . $size . ' enabled=' . ($config['artwork']['enabled'] ?? 'unset'));
        }

        // If artwork is enabled but no size set, default to medium (200px) to prevent explosion
        if (!empty($config['artwork']['enabled']) || !empty($config['display_settings']['artwork_size'])) {
             switch ($size) {
                case 'small':  $artWidth = '100px'; $artHeight = '100px'; break;
                case 'large':  $artWidth = '300px'; $artHeight = '300px'; break;
                case 'xlarge': $artWidth = '400px'; $artHeight = '400px'; break;
                case 'medium': 
                default:       $artWidth = '200px'; $artHeight = '200px'; break;
            }
        }
    }

    // Apply artwork styles if we have dimensions
    if ($artWidth && $artWidth !== '0px') {
        $css .= '.castconductor-preview-block .artwork-container { width:' . $artWidth . ' !important; height:' . ($artHeight ? $artHeight : 'auto') . ' !important; flex-shrink:0 !important; overflow:hidden !important; }';
        $css .= '.castconductor-preview-block .artwork-container img { width:100% !important; height:100% !important; object-fit:cover !important; display:block !important; }';
        
        // Apply gap if configured (Canvas Editor)
        if (!empty($config['artwork']['gap'])) {
            $css .= '.castconductor-preview-block { gap:' . intval($config['artwork']['gap']) . 'px; }';
        } elseif (!empty($config['display_settings']['artwork_size'])) {
            // Default gap for Track Info presets
            $css .= '.castconductor-preview-block { gap:16px; }';
        }
    } else {
        // Default behavior if no size set: hide artwork container if disabled
        if (empty($config['artwork']['enabled'])) {
            $css .= '.castconductor-preview-block .artwork-container { display:none !important; }';
        } else {
             // Safety net: Artwork enabled but no size resolved? Force 200px.
             $css .= '.castconductor-preview-block .artwork-container { width:200px !important; height:200px !important; flex-shrink:0 !important; overflow:hidden !important; }';
             $css .= '.castconductor-preview-block .artwork-container img { width:100% !important; height:100% !important; object-fit:cover !important; display:block !important; }';
             $css .= '.castconductor-preview-block { gap:16px; }';
        }
    }


        $fontFamily = isset($config['typography']['font_family']) && $config['typography']['font_family'] ? $config['typography']['font_family'] : 'system-ui,Arial,Helvetica,sans-serif';
        $fontSize = isset($config['typography']['font_size']) ? (int)$config['typography']['font_size'] : 24;
        $fontWeight = isset($config['typography']['font_weight']) && $config['typography']['font_weight'] !== '' ? $config['typography']['font_weight'] : '400';
        $fontColor = isset($config['typography']['color']) && $config['typography']['color'] ? $config['typography']['color'] : '#ffffff';
        $lineHeight = isset($config['typography']['line_height']) && floatval($config['typography']['line_height'])>0 ? floatval($config['typography']['line_height']) : 1.2;
        $textAlign = isset($config['typography']['text_align']) && $config['typography']['text_align'] ? $config['typography']['text_align'] : 'left';
        $css .= '.castconductor-preview-block, .castconductor-preview-block .text-content{font-family:' . esc_attr($fontFamily) . ';color:' . esc_attr($fontColor) . ';}';
    $css .= '.castconductor-preview-block .text-content{flex:1;overflow:hidden;font-size:' . $fontSize . 'px;line-height:' . $lineHeight . ';text-align:' . $textAlign . ';font-weight:' . esc_attr($fontWeight) . ';display:flex;flex-direction:column;gap:2px;}';
        $css .= '.castconductor-preview-block .artist,.castconductor-preview-block .name{font-weight:600;margin-bottom:4px;font-family:inherit;color:inherit;}';
        $css .= '.castconductor-preview-block .title,.castconductor-preview-block .message{opacity:0.9;font-family:inherit;color:inherit;}';
        // Content specific styles for Shadow DOM parity
        $css .= '.castconductor-preview-block .date{font-size:0.85em;opacity:0.7;margin-bottom:4px;font-family:inherit;color:inherit;}';
        $css .= '.castconductor-preview-block .content{font-size:0.95em;line-height:1.4;font-family:inherit;color:inherit;}';
        $css .= '.castconductor-preview-block .location{font-weight:600;margin-bottom:2px;font-family:inherit;color:inherit;}';
        $css .= '.castconductor-preview-block .weather{font-size:1.1em;font-family:inherit;color:inherit;}';
        $css .= '.castconductor-preview-block .time{font-size:1.1em;font-family:inherit;color:inherit;}';
        $css .= '.castconductor-preview-block .ticker-line{display:inline-block;margin-right:2em;font-family:inherit;color:inherit;}';

        // Text shadow
        if (!empty($config['typography']['text_shadow'])) {
            $ts = $config['typography']['text_shadow'];
            $x = isset($ts['x']) ? (int)$ts['x'] : 0;
            $y = isset($ts['y']) ? (int)$ts['y'] : 0;
            $b = isset($ts['blur']) ? (int)$ts['blur'] : 0;
            $c = isset($ts['color']) ? $ts['color'] : 'rgba(0,0,0,0)';
            $css .= '.castconductor-preview-block,.castconductor-preview-block .text-content{text-shadow:' . $x . 'px ' . $y . 'px ' . $b . 'px ' . $c . ';}';
        }

        // Fade-in animation
        $fadeIn = (!empty($config['animation']['fade_in']) && $config['animation']['fade_in'])
            || (!empty($config['animation']['entrance']) && $config['animation']['entrance'] === 'fade_in');
        if ($fadeIn) {
            $dur = isset($config['animation']['duration']) ? (int)$config['animation']['duration'] : 300;
            $easing = isset($config['animation']['easing']) ? $config['animation']['easing'] : 'ease-in-out';
            $css .= '.castconductor-preview-block{animation:ccFadeIn ' . max(50,$dur) . 'ms ' . $easing . ' both;}';
            $css .= '@keyframes ccFadeIn{from{opacity:0;transform:translateY(4px);}to{opacity:1;transform:translateY(0);}}';
        }

        // Ticker (client activates marquee via class if overflow)
        if (!empty($config['behavior']['ticker']['enabled'])) {
            $speed = isset($config['behavior']['ticker']['speed']) ? (int)$config['behavior']['ticker']['speed'] : 30;
            $speed = max(5, min($speed, 120));
            $css .= '.castconductor-preview-block .ticker{overflow:hidden;}';
            $css .= '.castconductor-preview-block .ticker-inner{display:inline-block;will-change:transform;}';
            $css .= '.castconductor-preview-block .ticker-line{white-space:nowrap;display:block;}';
            $css .= '.castconductor-preview-block .ticker.marquee-active .ticker-inner{animation:ccMarquee ' . (200 / ($speed / 30)) . 's linear infinite;}';
            $css .= '@keyframes ccMarquee{0%{transform:translateX(0);}100%{transform:translateX(-100%);}}';
        }

        // Minimal placeholder styling to keep Scenes stage fallbacks small and unobtrusive
        $css .= '.castconductor-preview-block .ccve-placeholder{opacity:0.75;font-size:0.92em;letter-spacing:0.2px;}';
        $css .= '.castconductor-preview-block .ccve-line{display:block;}';
        $css .= '.castconductor-preview-block .ccve-weather:before{content:"\2601\00A0";opacity:0.8;margin-right:4px;}';
        $css .= '.castconductor-preview-block .ccve-locationtime:before{content:"\23F0\00A0";opacity:0.8;margin-right:4px;}';

        return $css;
    }

    /**
     * Normalize overlay color from config to an rgba() string.
     * Accepts hex colors with optional 'opacity' (0..1). If already rgba/ rgb, returns as-is.
     */
    private function normalize_overlay_color($overlay) {
        if (empty($overlay) || empty($overlay['color'])) {
            return '';
        }
        $color = trim($overlay['color']);
        $opacity = isset($overlay['opacity']) ? floatval($overlay['opacity']) : null;

        // If already rgba and no override opacity requested, pass through
        if (stripos($color, 'rgba(') === 0 && ($opacity === null)) {
            return $color;
        }
        // If rgb() and have opacity provided, convert
        if (stripos($color, 'rgb(') === 0 && ($opacity !== null)) {
            $inner = trim(substr($color, 4), ' )');
            return 'rgba(' . $inner . ', ' . max(0, min(1, $opacity)) . ')';
        }
        // If hex color, convert to rgba using provided opacity or default 0
        if ($color[0] === '#') {
            return $this->hex_to_rgba($color, $opacity ?? 0);
        }
        // Fallback: if non-hex string and have opacity, attempt to apply by wrapping as rgba if it's comma-separated
        if ($opacity !== null) {
            // Not a robust parse; assume color is like "r, g, b"
            return 'rgba(' . $color . ', ' . max(0, min(1, $opacity)) . ')';
        }
        return $color;
    }

    /**
     * Convert a hex color (#RGB, #RRGGBB) to rgba string with given opacity (0..1)
     */
    private function hex_to_rgba($hex, $opacity = 1) {
        $hex = trim($hex);
        // If empty or not a hex string (e.g. already rgba, transparent, or named color), return as-is
        if (empty($hex) || $hex[0] !== '#') {
            return $hex;
        }

        $hex = ltrim($hex, '#');
        if (strlen($hex) === 3) {
            $r = hexdec(str_repeat(substr($hex, 0, 1), 2));
            $g = hexdec(str_repeat(substr($hex, 1, 1), 2));
            $b = hexdec(str_repeat(substr($hex, 2, 1), 2));
        } elseif (strlen($hex) === 6) {
            $r = hexdec(substr($hex, 0, 2));
            $g = hexdec(substr($hex, 2, 2));
            $b = hexdec(substr($hex, 4, 2));
        } else {
            // Invalid hex, fallback to black
            $r = $g = $b = 0;
        }
        $opacity = max(0, min(1, floatval($opacity)));
        return 'rgba(' . $r . ', ' . $g . ', ' . $b . ', ' . $opacity . ')';
    }

    /**
     * Get visual config endpoint arguments
     *
     * @return array Arguments
     */
    private function get_visual_config_args() {
        return array(
            'typography' => array(
                'required' => false,
                'type' => 'object',
            ),
            'layout' => array(
                'required' => false,
                'type' => 'object',
            ),
            'background' => array(
                'required' => false,
                'type' => 'object',
            ),
            'overlay' => array(
                'required' => false,
                'type' => 'object',
            ),
            'artwork' => array(
                'required' => false,
                'type' => 'object',
            ),
            'animation' => array(
                'required' => false,
                'type' => 'object',
            ),
            'behavior' => array(
                'required' => false,
                'type' => 'object',
            ),
        );
    }

    /**
     * Get preview endpoint arguments
     *
     * @return array Arguments
     */
    private function get_preview_args() {
        return array(
            'content_type' => array(
                'required' => false,
                'type' => 'string',
                'enum' => array('track_info', 'shoutout', 'sponsor', 'promo', 'weather', 'location_time', 'custom_api', 'custom'),
            ),
            'block_id' => array(
                'required' => false,
                'type' => 'integer',
            ),
            'item_index' => array(
                'required' => false,
                'type' => 'integer',
            ),
            'shuffle' => array(
                'required' => false,
                'type' => 'boolean',
            ),
            'seed' => array(
                'required' => false,
                'type' => 'string',
            ),
            'frame' => array(
                'required' => false,
                'type' => 'object',
                'properties' => array(
                    'width' => array('type' => 'integer'),
                    'height' => array('type' => 'integer'),
                    'zone_id' => array('type' => 'string'),
                ),
            ),
        );
    }

    /**
     * Check permissions for reading canvas editor data
     *
     * @param WP_REST_Request $request Full data about the request.
     * @return WP_Error|bool
     */
    // (removed: older duplicate get_items_permissions_check)

    /**
     * Check permissions for creating/updating canvas editor data
     *
     * @param WP_REST_Request $request Full data about the request.
     * @return WP_Error|bool
     */
    // (removed: older duplicate create_item_permissions_check)

}
