<!DOCTYPE html>
<html lang="{{ get_admin_language() }}" dir="{{ 'rtl' if get_admin_language() in ('ar', 'he') else 'ltr' }}">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="csrf-token" content="{{ csrf_token() }}">
<title>{{ t('builder.page_builder') }} — {{ page.title }} — AliothPress</title>

<!-- Builder CSS (self-contained).
     static_v() cache-busting is REQUIRED here: the themed pbDialog's
     JS lives inline in this template (always fresh), but its styles
     live in page-builder-editor.css — with a stale cached stylesheet
     the URL dialog rendered as an unstyled full-width div at the very
     bottom of the page, looking like the link button "did nothing".
     Same versioning the rest of the admin already uses. -->
<link rel="stylesheet" href="{{ static_v('css/page-builder-editor.css') }}">
<link rel="stylesheet" href="{{ static_v('css/components.css') }}">
<link rel="stylesheet" href="{{ static_v('css/page-builder-blocks.css') }}">
<style>{{ theme_css | safe }}</style>

</head>
<body data-theme="{{ current_user.admin_theme or 'light' }}">
<script>
(function(){
    var t = document.body.getAttribute('data-theme');
    if (t === 'system') {
        document.body.setAttribute('data-theme',
            window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
        window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', function(e) {
            document.body.setAttribute('data-theme', e.matches ? 'dark' : 'light');
        });
    }
})();
</script>

<!-- ====== TOP BAR ====== -->
<div class="pb-topbar">
  <div class="pb-topbar__left">
    <a href="{{ url_for('admin_pages_edit', page_id=page.id, return_to=request.args.get('return_to') or None) }}" class="pb-btn pb-btn--ghost" title="{{ t('builder.back_to_editor') }}">
      <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><polyline points="15 18 9 12 15 6"/></svg>
    </a>
    <span class="pb-topbar__title">{{ t('builder.page_builder') }}</span>
    <span class="pb-topbar__sep">·</span>
    <span class="pb-topbar__page-title">{{ page.title }}</span>
  </div>
  <div class="pb-topbar__right">
    <button class="pb-btn pb-btn--ghost pb-topbar__more" id="pbTopbarMore" aria-label="Menu">
      <svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><circle cx="12" cy="5" r="2"/><circle cx="12" cy="12" r="2"/><circle cx="12" cy="19" r="2"/></svg>
    </button>
    <div class="pb-topbar__actions" id="pbTopbarActions">
    <button class="pb-btn pb-btn--outline pb-btn--ai" id="pbAIBtn" onclick="openBuilderAI()">
      <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M12 2a4 4 0 014 4c0 1.95-1.4 3.57-3.25 3.92L12 10V8a2 2 0 10-2-2H8a4 4 0 014-4z"/><path d="M8.5 14.5L5 18"/><path d="M15.5 14.5L19 18"/><circle cx="12" cy="14" r="4"/></svg>
      🤖 AI
    </button>
    <span class="pb-theme-badge" title="{{ active_theme }}">
      <span class="pb-theme-badge__dot"></span>
      {{ active_theme | replace('-', ' ') | title }}
    </span>
    <button class="pb-btn pb-btn--outline" id="pbThemeToggle" onclick="toggleCanvasTheme()" title="{{ t('builder.toggle_preview') }}" aria-label="{{ t('builder.toggle_preview') }}">
      <svg id="pbThemeIconSun" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><circle cx="12" cy="12" r="5"/><line x1="12" y1="1" x2="12" y2="3"/><line x1="12" y1="21" x2="12" y2="23"/><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/><line x1="1" y1="12" x2="3" y2="12"/><line x1="21" y1="12" x2="23" y2="12"/><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/></svg>
      <svg id="pbThemeIconMoon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M21 12.79A9 9 0 1111.21 3 7 7 0 0021 12.79z"/></svg>
    </button>
    <a href="{{ url_for('admin_pages_preview', page_id=page.id) }}" target="_blank" class="pb-btn pb-btn--outline">
      <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg>
      {{ t('builder.preview') }}
    </a>
    </div>
    <button class="pb-btn pb-btn--ghost" id="pbUndo" disabled title="{{ t('builder.undo') }}">
      <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="1 4 1 10 7 10"/><path d="M3.51 15a9 9 0 105.64-8.36L1 10"/></svg>
    </button>
    <button class="pb-btn pb-btn--ghost" id="pbRedo" disabled title="{{ t('builder.redo') }}">
      <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="23 4 23 10 17 10"/><path d="M20.49 15a9 9 0 11-5.64-8.36L23 10"/></svg>
    </button>
    <button class="pb-btn pb-btn--primary" id="pbSave">
      <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M19 21H5a2 2 0 01-2-2V5a2 2 0 012-2h11l5 5v11a2 2 0 01-2 2z"/><polyline points="17 21 17 13 7 13 7 21"/><polyline points="7 3 7 8 15 8"/></svg>
      {{ t('builder.save') }}
    </button>
  </div>
</div>

<!-- ====== MAIN LAYOUT ====== -->
<div class="pb-layout">

  <!-- LEFT SIDEBAR — Block palette -->
  <div class="pb-sidebar" role="navigation" aria-label="{{ t('builder.page_builder') }}">
    <div class="pb-sidebar__section">
      <div class="pb-sidebar__title">{{ t('builder.basic') }}</div>
      <div class="pb-block-grid">
        <button class="pb-block-btn" data-block-type="heading">
          <svg viewBox="0 0 24 24"><path d="M4 4v16"/><path d="M4 12h12"/><path d="M16 4v16"/></svg>
          {{ t('builder.heading') }}
        </button>
        <button class="pb-block-btn" data-block-type="text">
          <svg viewBox="0 0 24 24"><path d="M4 7V4h16v3"/><path d="M9 20h6"/><path d="M12 4v16"/></svg>
          {{ t('builder.text') }}
        </button>
        <button class="pb-block-btn" data-block-type="image">
          <svg viewBox="0 0 24 24"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="8.5" cy="8.5" r="1.5"/><polyline points="21 15 16 10 5 21"/></svg>
          {{ t('builder.image') }}
        </button>
        <button class="pb-block-btn" data-block-type="button">
          <svg viewBox="0 0 24 24"><rect x="2" y="7" width="20" height="10" rx="2"/><path d="M9 12h6"/></svg>
          {{ t('builder.button') }}
        </button>
        <button class="pb-block-btn" data-block-type="divider">
          <svg viewBox="0 0 24 24"><path d="M3 12h18"/></svg>
          {{ t('builder.divider') }}
        </button>
        <button class="pb-block-btn" data-block-type="spacer">
          <svg viewBox="0 0 24 24"><path d="M12 5v14"/><path d="M5 5h14"/><path d="M5 19h14"/></svg>
          {{ t('builder.spacer') }}
        </button>
      </div>
    </div>

    <div class="pb-sidebar__section">
      <div class="pb-sidebar__title">{{ t('builder.structure') }}</div>
      <div class="pb-block-grid">
        <button class="pb-block-btn" data-block-type="columns">
          <svg viewBox="0 0 24 24"><rect x="3" y="3" width="7" height="18"/><rect x="14" y="3" width="7" height="18"/></svg>
          {{ t('builder.columns') }}
        </button>
        <button class="pb-block-btn" data-block-type="section">
          <svg viewBox="0 0 24 24"><rect x="2" y="4" width="20" height="16" rx="2"/><path d="M2 8h20"/></svg>
          {{ t('builder.section') }}
        </button>
      </div>
    </div>

    <div class="pb-sidebar__section">
      <div class="pb-sidebar__title">{{ t('builder.media') }}</div>
      <div class="pb-block-grid">
        <button class="pb-block-btn" data-block-type="video">
          <svg viewBox="0 0 24 24"><polygon points="5 3 19 12 5 21 5 3"/></svg>
          {{ t('builder.video') }}
        </button>
        <button class="pb-block-btn" data-block-type="gallery">
          <svg viewBox="0 0 24 24"><rect x="2" y="2" width="8" height="8" rx="1"/><rect x="14" y="2" width="8" height="8" rx="1"/><rect x="2" y="14" width="8" height="8" rx="1"/><rect x="14" y="14" width="8" height="8" rx="1"/></svg>
          {{ t('builder.gallery') }}
        </button>
        <button class="pb-block-btn" data-block-type="video_gallery">
          <svg viewBox="0 0 24 24"><rect x="1" y="3" width="9" height="7" rx="1"/><polygon points="5 5.5 8 7 5 8.5" fill="currentColor" stroke="none"/><rect x="14" y="3" width="9" height="7" rx="1"/><polygon points="18 5.5 21 7 18 8.5" fill="currentColor" stroke="none"/><rect x="1" y="14" width="9" height="7" rx="1"/><polygon points="5 16.5 8 18 5 19.5" fill="currentColor" stroke="none"/><rect x="14" y="14" width="9" height="7" rx="1"/><polygon points="18 16.5 21 18 18 19.5" fill="currentColor" stroke="none"/></svg>
          {{ t('builder.video_gallery') }}
        </button>
        <button class="pb-block-btn" data-block-type="audio">
          <svg viewBox="0 0 24 24"><path d="M9 18V5l12-2v13"/><circle cx="6" cy="18" r="3"/><circle cx="18" cy="16" r="3"/></svg>
          {{ t('builder.audio') }}
        </button>
        <button class="pb-block-btn" data-block-type="slideshow">
          <svg viewBox="0 0 24 24"><rect x="2" y="4" width="20" height="16" rx="2"/><polyline points="7 15 9 12 7 9"/><polyline points="17 9 15 12 17 15"/><circle cx="12" cy="20" r="1" fill="currentColor"/><circle cx="9" cy="20" r="0.7" fill="currentColor"/><circle cx="15" cy="20" r="0.7" fill="currentColor"/></svg>
          {{ t('builder.slideshow') }}
        </button>
        <button class="pb-block-btn" data-block-type="ticker">
          <svg viewBox="0 0 24 24"><rect x="1" y="7" width="5" height="10" rx="1"/><rect x="8" y="7" width="5" height="10" rx="1"/><rect x="15" y="7" width="5" height="10" rx="1"/><path d="M3 21h18"/><polyline points="18 20 21 21 18 22"/></svg>
          {{ t('builder.ticker') }}
        </button>
      </div>
    </div>

    <div class="pb-sidebar__section">
      <div class="pb-sidebar__title">{{ t('builder.advanced') }}</div>
      <div class="pb-block-grid">
        <button class="pb-block-btn" data-block-type="quote">
          <svg viewBox="0 0 24 24"><path d="M3 21c3 0 7-1 7-8V5c0-1.25-.756-2.017-2-2H4c-1.25 0-2 .75-2 1.972V11c0 1.25.75 2 2 2 1 0 1 0 1 1v1c0 1-1 2-2 2s-1 .008-1 1.031V21z"/><path d="M15 21c3 0 7-1 7-8V5c0-1.25-.757-2.017-2-2h-4c-1.25 0-2 .75-2 1.972V11c0 1.25.75 2 2 2h.75c0 2.25.25 4-2.75 4v3z"/></svg>
          {{ t('builder.quote') }}
        </button>
        <button class="pb-block-btn" data-block-type="table">
          <svg viewBox="0 0 24 24"><rect x="3" y="3" width="18" height="18" rx="2"/><path d="M3 9h18"/><path d="M3 15h18"/><path d="M9 3v18"/></svg>
          {{ t('builder.table') }}
        </button>
        <button class="pb-block-btn" data-block-type="accordion">
          <svg viewBox="0 0 24 24"><path d="M3 6h18"/><path d="M3 12h18"/><path d="M3 18h18"/><polyline points="15 8 18 6 15 4"/></svg>
          {{ t('builder.accordion') }}
        </button>
        <button class="pb-block-btn" data-block-type="tabs">
          <svg viewBox="0 0 24 24"><rect x="3" y="7" width="18" height="14" rx="2"/><path d="M3 7V5a2 2 0 012-2h4l2 4"/><path d="M11 3h8a2 2 0 012 2v2"/></svg>
          {{ t('builder.tabs') }}
        </button>
        <button class="pb-block-btn" data-block-type="html">
          <svg viewBox="0 0 24 24"><polyline points="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 18"/></svg>
          {{ t('builder.html') }}
        </button>
        <button class="pb-block-btn" data-block-type="code">
          <svg viewBox="0 0 24 24"><rect x="4" y="4" width="16" height="16" rx="2"/><path d="M9 10l-2 2 2 2"/><path d="M15 10l2 2-2 2"/></svg>
          {{ t('builder.code') }}
        </button>
        <button class="pb-block-btn" data-block-type="form">
          <svg viewBox="0 0 24 24"><rect x="3" y="3" width="18" height="18" rx="2"/><path d="M7 7h10"/><path d="M7 12h10"/><path d="M7 17h4"/></svg>
          {{ t('builder.form') }}
        </button>
      </div>
    </div>
  </div>

  <!-- CENTER CANVAS -->
  <div class="pb-canvas {% if current_user.admin_theme == 'dark' %}pb-canvas--theme-dark{% else %}pb-canvas--theme-light{% endif %}" id="pbCanvas" role="main" aria-label="{{ t('builder.canvas') }}">
    <div class="pb-canvas__inner" id="pbBlocks" role="list">
      <!-- Blocks rendered here by JS -->
    </div>
  </div>

  <!-- RIGHT PANEL — Block settings -->
  <div class="pb-panel" id="pbPanel" role="complementary" aria-label="{{ t('builder.block_settings') }}">
    <div class="pb-panel__header">
      <span class="pb-panel__title" id="pbPanelTitle">{{ t('builder.block_settings') }}</span>
      <button class="pb-panel__close" id="pbPanelClose" title="{{ t('common.close') }}" aria-label="{{ t('common.close') }}">
        <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
      </button>
    </div>
    <div class="pb-panel__body" id="pbPanelBody">
      <!-- Dynamic settings form rendered here -->
    </div>
    <!-- Shown when no block is selected (panel column stays reserved on
         desktop so the canvas never resizes/jumps on select/deselect).
         width/height attrs on the svg are a hard fallback: even if the
         stylesheet is stale/cached the icon can never render oversized. -->
    <div class="pb-panel__empty" id="pbPanelEmpty">
      <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="4" y="4" width="16" height="16" rx="2"/><path d="M9 12l2 2 4-5"/></svg>
      <span>{{ t('builder.select_block_hint') }}</span>
    </div>
  </div>

</div>

<!-- Toast notifications (stacked; see showToast) -->
<div class="pb-toasts" id="pbToasts"></div>

<!-- ====== CANVAS THEME TOGGLE ====== -->
<script>
(function() {
  var canvas = document.getElementById('pbCanvas');
  var iconSun = document.getElementById('pbThemeIconSun');
  var iconMoon = document.getElementById('pbThemeIconMoon');
  
  function updateIcons() {
    var isDark = document.body.getAttribute('data-theme') === 'dark';
    iconSun.style.display = isDark ? 'block' : 'none';
    iconMoon.style.display = isDark ? 'none' : 'block';
  }
  
  function syncCanvasToBody() {
    var isDark = document.body.getAttribute('data-theme') === 'dark';
    canvas.classList.toggle('pb-canvas--theme-dark', isDark);
    canvas.classList.toggle('pb-canvas--theme-light', !isDark);
    updateIcons();
  }
  
  /* Handle system preference */
  if ('{{ current_user.admin_theme or "light" }}' === 'system') {
    window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', function() {
      syncCanvasToBody();
    });
  }
  
  /* Initial sync */
  syncCanvasToBody();
  
  /* Toggle: switch entire builder + canvas together */
  window.toggleCanvasTheme = function() {
    var isDark = document.body.getAttribute('data-theme') === 'dark';
    document.body.setAttribute('data-theme', isDark ? 'light' : 'dark');
    syncCanvasToBody();
  };
})();
</script>

<!-- Unified media picker core (search + pagination), shared with the rest
     of the admin. Visuals below use the builder's own pb-* classes. -->
<script src="{{ static_v('js/admin-image-picker.js') }}"></script>

<!-- ====== PAGE BUILDER JS ====== -->
<script>
/* i18n translations for builder JS — passed from Python as safe JSON */
var PB_I18N = {{ builder_i18n_json | safe }};

/* Safety net: if a key is missing from builder_i18n_json, return the key name
   (e.g. "gallery_images" instead of undefined). Prevents "undefined" in UI. */
if (typeof Proxy !== 'undefined') {
  PB_I18N = new Proxy(PB_I18N, {
    get: function(target, key) {
      if (key in target) return target[key];
      /* Convert snake_case key to readable: gallery_images → "gallery images" */
      return String(key).replace(/_/g, ' ');
    }
  });
}

(function() {
  'use strict';

  // ============================================================
  // CSRF PROTECTION — auto-add token to all fetch requests
  // ============================================================
  var _csrfMeta = document.querySelector('meta[name="csrf-token"]');
  var _csrfToken = _csrfMeta ? _csrfMeta.getAttribute('content') : '';
  var _origFetch = window.fetch;
  window.fetch = function(url, opts) {
    opts = opts || {};
    if (opts.method && ['POST','PUT','DELETE','PATCH'].indexOf(opts.method.toUpperCase()) !== -1) {
      opts.headers = opts.headers || {};
      if (!opts.headers['X-CSRFToken']) opts.headers['X-CSRFToken'] = _csrfToken;
    }
    return _origFetch.call(this, url, opts);
  };

  // ============================================================
  // STATE
  // ============================================================
  var PAGE_ID = {{ page.id }};

  // Default block alignment for NEWLY created blocks. Content-flow blocks
  // (prose, headings, tables…) start left-aligned — matching how such
  // content reads on the public site — while visual/decorative blocks keep
  // the centered default. RTL pages get 'right', mirroring the dir= logic
  // in public/base.html. Stored pages are untouched: every saved block
  // carries its explicit alignment, and the '|| center' fallbacks used for
  // legacy/agent-made blocks without settings stay as they were.
  var DEFAULT_CONTENT_ALIGN = '{{ 'right' if page.language in ('ar', 'he') else 'left' }}';
  var CONTENT_FLOW_TYPES = ['heading', 'text', 'quote', 'table', 'accordion', 'tabs', 'code', 'html', 'form'];
  function defaultAlignFor(type) {
    return CONTENT_FLOW_TYPES.indexOf(type) !== -1 ? DEFAULT_CONTENT_ALIGN : 'center';
  }

  var state = {
    blocks: [],
    version: 1,
    selectedId: null,
    isDirty: false,
    // True when this page currently holds editor-authored content and is not
    // yet builder-managed: the first builder save would convert it (regular
    // editor becomes read-only). Set from the blocks API on load; gates a
    // confirmation dialog in saveBlocks(). Empty drafts never set this.
    convertsOnSave: false
  };

  // DOM refs
  var $blocks = document.getElementById('pbBlocks');
  var $canvas = document.getElementById('pbCanvas');
  var $panel = document.getElementById('pbPanel');
  var $panelTitle = document.getElementById('pbPanelTitle');
  var $panelBody = document.getElementById('pbPanelBody');
  var $toasts = document.getElementById('pbToasts');
  var $save = document.getElementById('pbSave');

  // ============================================================
  // BLOCK DEFAULTS — default data for each block type
  // ============================================================
  var BLOCK_DEFAULTS = {
    // Placeholder-style texts (heading/text/button/accordion title) are NOT
    // pre-filled as real content: the canvas shows them as true placeholders
    // (data-placeholder + CSS ::before) that vanish as soon as the user types,
    // so nobody has to manually delete "Start writing here..." first.
    heading:   { text: '', level: 2 },
    text:      { html: '' },
    image:     { url: '', alt: '', caption: '', link_url: '', size: 'full' },
    button:    { text: '', url: '#', style: 'primary', target: '_self', size: 'md' },
    divider:   { style: 'solid' },
    spacer:    { height: '48' },
    columns:   { layout: '1/2+1/2', columns: [{ blocks: [] }, { blocks: [] }] },
    section:   { background_type: 'color', background_value: '', background_image: '', background_overlay: '', background_type_dark: 'color', background_value_dark: '', background_overlay_dark: '', padding: 'lg', text_color: 'auto', min_height: 'auto', full_width: false, content_width: 'default', blocks: [] },
    video:     { source: 'youtube', url: '', embed_id: '', aspect_ratio: '16/9', title: '' },
    gallery:   { images: [], layout: 'grid', columns: 3, gap: 'md', lightbox: true },
    video_gallery: { videos: [], columns: 2, gap: 'md' },
    html:      { code: '<!-- HTML -->' },
    table:     { headers: ['A', 'B', 'C'], rows: [['', '', '']], striped: true, responsive: true, table_width: 'full' },
    accordion: { items: [{ title: '', content: '' }], allow_multiple: false },
    // Tab titles stay pre-filled on purpose: they double as the switching UI
    // in the canvas — two empty tabs would be indistinguishable.
    tabs:      { tabs: [{ title: (PB_I18N.default_tab || '') + ' 1', content: '' }, { title: (PB_I18N.default_tab || '') + ' 2', content: '' }] },
    audio:     { url: '', title: '' },
    slideshow: { mode: 'hero', slides: [{ image: '', alt: '', blocks: [] }], animation: 'slide', autoplay: true, interval: 5, show_arrows: true, show_dots: true, aspect_ratio: '16/9', overlay: '', padding: 'lg', text_color: 'light', full_width: false, content_width: 'default' },
    quote:     { text: '', author: '', style: 'default' },
    code:      { code: '', language: 'javascript' },
    form:      { form_id: '', show_title: true, show_description: true },
    ticker:    { images: [], speed: 'medium', pause_on_hover: true, grayscale: true, height: 'md', direction: 'rtl' }
  };

  var BLOCK_LABELS = {
    heading: PB_I18N.heading, text: PB_I18N.text, image: PB_I18N.image, button: PB_I18N.button,
    divider: PB_I18N.divider, spacer: PB_I18N.spacer, columns: PB_I18N.columns, section: PB_I18N.section,
    video: PB_I18N.video, gallery: PB_I18N.gallery, video_gallery: PB_I18N.video_gallery, html: PB_I18N.html, table: PB_I18N.table,
    accordion: PB_I18N.accordion, tabs: PB_I18N.tabs, audio: PB_I18N.audio, quote: PB_I18N.quote,
    code: PB_I18N.code, form: PB_I18N.form, slideshow: PB_I18N.slideshow, ticker: PB_I18N.ticker
  };

  // ============================================================
  // HELPERS
  // ============================================================
  function uuid() {
    return 'b' + Math.random().toString(36).substr(2, 9);
  }

  function esc(s) {
    /* Escapes &, <, > AND both quote characters. The old textContent →
       innerHTML trick left quotes untouched, but esc() output is inserted
       into double-quoted attributes everywhere (value="…", alt="…",
       data-placeholder="…"). A block title containing a double quote
       terminated the attribute early and mangled the editor markup. */
    return String(s || '')
      .replace(/&/g, '&amp;')
      .replace(/</g, '&lt;')
      .replace(/>/g, '&gt;')
      .replace(/"/g, '&quot;')
      .replace(/'/g, '&#39;');
  }

  // ============================================================
  // THEMED DIALOG — replaces native prompt()/confirm()
  // ------------------------------------------------------------
  // Browser dialogs ignore the admin theme entirely (white chrome
  // popups over a dark builder). Same idea as AliothEditor.dialog
  // in the rich-text editor, but built on the builder's own pb-*
  // tokens and .pb-btn classes so both dialogs look native to
  // their host surface in light AND dark mode.
  //
  //   pbDialog({ title, message, fields:[{name,label,value,
  //              placeholder,type}], okLabel, danger })
  //     -> Promise resolving to {name: value, ...} (or {} when
  //        there are no fields) or null on cancel/Esc/backdrop.
  //   pbPrompt(label, value)   -> Promise<string|null>
  //   pbConfirm(message, opts) -> Promise<boolean>
  //
  // NOTE for contenteditable callers: unlike native prompt(), a
  // real modal steals focus, which collapses the selection in the
  // text being edited. Save the Range BEFORE calling and restore
  // it in .then() (see the createLink handlers below).
  // ============================================================
  function pbDialog(opts) {
    return new Promise(function(resolve) {
      var overlay = document.createElement('div');
      overlay.className = 'pb-dialog-overlay';
      var fieldsHtml = (opts.fields || []).map(function(fld, i) {
        return '<div class="pb-field">' +
          '<label class="pb-field__label" for="pb-dlg-f' + i + '">' + esc(fld.label) + '</label>' +
          '<input id="pb-dlg-f' + i + '" class="pb-field__input" type="' + esc(fld.type || 'text') + '"' +
          ' value="' + esc(fld.value || '') + '"' +
          (fld.placeholder ? ' placeholder="' + esc(fld.placeholder) + '"' : '') +
          ' autocomplete="off">' +
          '</div>';
      }).join('');
      overlay.innerHTML =
        '<div class="pb-dialog" role="dialog" aria-modal="true" aria-label="' + esc(opts.title || '') + '">' +
          '<div class="pb-dialog__head">' +
            '<h3 class="pb-heading-md">' + esc(opts.title || '') + '</h3>' +
            '<button type="button" class="pb-modal-close" aria-label="' + esc(PB_I18N.cancel || 'Cancel') + '">&times;</button>' +
          '</div>' +
          (opts.message ? '<div class="pb-dialog__message">' + esc(opts.message) + '</div>' : '') +
          fieldsHtml +
          '<div class="pb-dialog__actions">' +
            '<button type="button" class="pb-btn pb-btn--sm" data-pb-cancel>' + esc(opts.cancelLabel || PB_I18N.cancel || 'Cancel') + '</button>' +
            '<button type="button" class="pb-btn pb-btn--sm ' + (opts.danger ? 'pb-btn--danger' : 'pb-btn--primary') + '" data-pb-ok>' +
              esc(opts.okLabel || PB_I18N.ok || 'OK') + '</button>' +
          '</div>' +
        '</div>';
      document.body.appendChild(overlay);
      var inputs = overlay.querySelectorAll('input');
      function collect() {
        var out = {};
        (opts.fields || []).forEach(function(fld, i) { out[fld.name] = inputs[i].value; });
        return out;
      }
      function close(result) {
        document.removeEventListener('keydown', onKey, true);
        if (overlay.parentNode) overlay.parentNode.removeChild(overlay);
        resolve(result);
      }
      function onKey(e) {
        if (e.key === 'Escape') { e.stopPropagation(); close(null); }
        else if (e.key === 'Enter' && overlay.contains(e.target)) {
          e.preventDefault(); close(collect());
        }
      }
      overlay.addEventListener('mousedown', function(e) { if (e.target === overlay) close(null); });
      overlay.querySelector('.pb-modal-close').addEventListener('click', function() { close(null); });
      overlay.querySelector('[data-pb-cancel]').addEventListener('click', function() { close(null); });
      overlay.querySelector('[data-pb-ok]').addEventListener('click', function() { close(collect()); });
      document.addEventListener('keydown', onKey, true);
      if (inputs[0]) { inputs[0].focus(); inputs[0].select(); }
      else { overlay.querySelector('[data-pb-ok]').focus(); }
    });
  }

  function pbPrompt(label, value) {
    return pbDialog({
      title: label,
      fields: [{ name: 'v', label: label, value: value || '' }]
    }).then(function(r) { return r ? r.v : null; });
  }

  function pbConfirm(message, opts) {
    opts = opts || {};
    return pbDialog({
      title: opts.title || message,
      message: opts.title ? message : '',
      okLabel: opts.okLabel,
      danger: opts.danger
    }).then(function(r) { return r !== null; });
  }

  /* Selection bridge for contenteditable + pbDialog: a real modal
     steals focus (native prompt didn't), collapsing the selection
     the command should apply to. Save before opening, restore in
     .then() — same pattern as AliothEditor._saveRange/_restoreRange. */
  function pbSaveSelection() {
    var sel = window.getSelection();
    return (sel && sel.rangeCount) ? sel.getRangeAt(0).cloneRange() : null;
  }
  function pbRestoreSelection(range, focusEl) {
    if (focusEl && focusEl.focus) focusEl.focus();
    if (!range) return;
    var sel = window.getSelection();
    sel.removeAllRanges();
    sel.addRange(range);
  }

  /* True when a rich-text HTML string has no real content: nothing but empty
     paragraphs / <br> / whitespace / &nbsp;. Media or structural tags (img,
     table, iframe, hr, …) count as content even without text. Used so the
     canvas placeholder ("Start writing here...") shows over an empty block
     and disappears the moment there is anything real inside. */
  function _isRichHtmlEmpty(html) {
    if (!html) return true;
    if (/<(img|iframe|video|audio|table|hr|embed|object|figure)\b/i.test(html)) return false;
    var text = html
      .replace(/<[^>]*>/g, '')
      .replace(/&nbsp;/gi, ' ')
      .replace(/\u00a0/g, ' ');
    return text.trim() === '';
  }

  /* Same check for a live contenteditable element (more accurate: uses DOM). */
  function _richElIsEmpty(el) {
    if (el.querySelector('img,iframe,video,audio,table,hr,embed,object,figure')) return false;
    return el.textContent.replace(/\u00a0/g, ' ').trim() === '';
  }

  /* Sync a rich contenteditable to the model + toggle its placeholder.
     An "empty" element stores '' (not '<p><br></p>' junk), so the public
     renderer can skip the block entirely. */
  function _syncRichEl(el) {
    var blockId = el.dataset.inlineBlock;
    var key = el.dataset.inlineHtml;
    if (!blockId || !key) return;
    var empty = _richElIsEmpty(el);
    el.classList.toggle('pb-show-placeholder', empty);
    var block = findBlockDeep(blockId);
    if (block) { _setDeepKey(block, key, empty ? '' : el.innerHTML); }
  }

  function stripTags(s) {
    if (!s || typeof s !== 'string') return s || '';
    var d = document.createElement('div');
    d.innerHTML = s;
    return d.textContent || '';
  }

  function showToast(msg, type) {
    // Readability rework (owner): each message is its OWN element
    // with its OWN timer - a second toast stacks above instead of
    // wiping the first (the old shared timer also raced: the first
    // call's timeout used to hide the second message early).
    // Lifetime grows with the text; hovering pauses the clock.
    var el = document.createElement('div');
    el.className = 'pb-toast' + (type ? ' pb-toast--' + type : '');
    el.textContent = msg;
    $toasts.appendChild(el);
    requestAnimationFrame(function() {
      el.classList.add('is-visible');
    });
    var life = Math.min(8000, 2500 + String(msg || '').length * 50);
    var timer = null;
    function leave() {
      el.classList.remove('is-visible');
      setTimeout(function() { el.remove(); }, 350);
    }
    function arm() { timer = setTimeout(leave, life); }
    el.addEventListener('mouseenter', function() {
      clearTimeout(timer);
    });
    el.addEventListener('mouseleave', arm);
    arm();
  }

  function markDirty() {
    state.isDirty = true;
    $save.textContent = PB_I18N.save + ' *';
  }

  // ============================================================
  // UNDO / REDO — snapshot-based history
  // ============================================================
  var _undoStack = [];
  var _redoStack = [];
  var _undoMax = 50;
  var _isRestoring = false;
  var _lastSavedState = null;
  var _undoDebounce = null;

  function _saveUndoState() {
    if (_isRestoring) return;
    clearTimeout(_undoDebounce);
    var current = JSON.stringify(state.blocks);
    if (_lastSavedState !== null && _lastSavedState !== current) {
      _undoStack.push(_lastSavedState);
      if (_undoStack.length > _undoMax) _undoStack.shift();
      _redoStack = [];
    }
    _lastSavedState = current;
    _updateUndoButtons();
  }

  function _saveUndoDebounced() {
    if (_isRestoring) return;
    clearTimeout(_undoDebounce);
    _undoDebounce = setTimeout(_saveUndoState, 800);
  }

  function _undo() {
    if (_undoStack.length === 0) return;
    _isRestoring = true;
    _redoStack.push(JSON.stringify(state.blocks));
    state.blocks = JSON.parse(_undoStack.pop());
    _lastSavedState = JSON.stringify(state.blocks);
    state.isDirty = true;
    $save.textContent = PB_I18N.save + ' *';
    renderCanvas();
    closePanel();
    _isRestoring = false;
    _updateUndoButtons();
  }

  function _redo() {
    if (_redoStack.length === 0) return;
    _isRestoring = true;
    _undoStack.push(JSON.stringify(state.blocks));
    state.blocks = JSON.parse(_redoStack.pop());
    _lastSavedState = JSON.stringify(state.blocks);
    state.isDirty = true;
    $save.textContent = PB_I18N.save + ' *';
    renderCanvas();
    closePanel();
    _isRestoring = false;
    _updateUndoButtons();
  }

  function _updateUndoButtons() {
    var u = document.getElementById('pbUndo');
    var r = document.getElementById('pbRedo');
    if (u) u.disabled = _undoStack.length === 0;
    if (r) r.disabled = _redoStack.length === 0;
  }

  // Patch markDirty to auto-save undo state (debounced for typing)
  var _origMarkDirty = markDirty;
  markDirty = function() {
    _saveUndoDebounced();
    _origMarkDirty();
  };

  // Keyboard shortcuts
  document.addEventListener('keydown', function(e) {
    /* When the user is typing (settings inputs, contenteditable text on
       canvas), Ctrl+Z must do the browser's native TEXT undo. Hijacking it
       here reverted the whole canvas to a snapshot taken ≥800ms ago and
       closed the panel — losing focus and recent typing. Block-level
       undo/redo still works whenever focus is outside editable elements,
       and always via the topbar buttons. */
    var t = e.target;
    if (t && (t.isContentEditable ||
        t.tagName === 'INPUT' || t.tagName === 'TEXTAREA' || t.tagName === 'SELECT')) {
      return;
    }
    if ((e.ctrlKey || e.metaKey) && e.key === 'z' && !e.shiftKey) {
      e.preventDefault();
      _undo();
    }
    if ((e.ctrlKey || e.metaKey) && (e.key === 'y' || (e.key === 'z' && e.shiftKey))) {
      e.preventDefault();
      _redo();
    }
  });

  // Undo/Redo button clicks
  document.getElementById('pbUndo').addEventListener('click', _undo);
  document.getElementById('pbRedo').addEventListener('click', _redo);

  // Topbar mobile menu toggle
  (function() {
    var moreBtn = document.getElementById('pbTopbarMore');
    var actions = document.getElementById('pbTopbarActions');
    if (moreBtn && actions) {
      moreBtn.addEventListener('click', function(e) {
        e.stopPropagation();
        actions.classList.toggle('is-open');
      });
      document.addEventListener('click', function(e) {
        if (!actions.contains(e.target) && e.target !== moreBtn) {
          actions.classList.remove('is-open');
        }
      });
    }
  })();

  // ============================================================
  // LOAD blocks from API
  // ============================================================
  function loadBlocks() {
    console.log('[PageBuilder] Loading blocks for page', PAGE_ID);
    fetch('/api/pages/' + PAGE_ID + '/blocks')
      .then(function(r) { 
        console.log('[PageBuilder] API response status:', r.status);
        return r.json(); 
      })
      .then(function(data) {
        console.log('[PageBuilder] API data:', data);
        if (data.success && data.blocks) {
          state.blocks = data.blocks.blocks || [];
          state.version = data.blocks.version || 1;
          state.convertsOnSave = !!data.converts_on_save;
          console.log('[PageBuilder] Loaded', state.blocks.length, 'blocks');
          _lastSavedState = JSON.stringify(state.blocks);
        }
        renderCanvas();
      })
      .catch(function(err) {
        console.error('[PageBuilder] Load error:', err);
        showToast(PB_I18N.save_failed, 'error');
        renderCanvas();
      });
  }

  // ============================================================
  // SAVE blocks to API
  // ============================================================
  function saveBlocks() {
    // Guard: this page was written in the regular editor and is not yet
    // builder-managed — the first builder save converts it (editor becomes
    // read-only for this page, original markup may change). Ask once; a
    // cancel leaves the page untouched. Empty drafts skip this (the flag
    // is never set for them by the API). The PUT endpoint enforces the
    // same rule server-side and only converts when convert=true is sent,
    // so the flag is cleared on successful save, not on confirmation —
    // a failed request keeps the protection armed for the retry.
    if (state.convertsOnSave) {
      pbConfirm(PB_I18N.convert_from_editor_warning, {
        title: PB_I18N.convert_from_editor_title,
        okLabel: PB_I18N.convert_from_editor_ok,
        danger: true
      }).then(function(ok) {
        if (!ok) return;
        _doSaveBlocks(true);
      });
      return;
    }
    _doSaveBlocks(false);
  }

  function _doSaveBlocks(convert) {
    $save.disabled = true;
    $save.textContent = PB_I18N.saving;

    // Strip transient preview data from form blocks (fields live in Form model)
    var cleanBlocks = JSON.parse(JSON.stringify(state.blocks));
    function stripFormPreview(blocks) {
      (blocks || []).forEach(function(b) {
        if (b.type === 'form' && b.data) {
          delete b.data._fields;
          delete b.data._settings;
          delete b.data._description;
          delete b.data._form_title_raw;
          delete b.data._form_title;
        }
        if (b.type === 'columns' && b.data && b.data.columns) {
          b.data.columns.forEach(function(col) { stripFormPreview(col.blocks); });
        }
        if (b.type === 'section' && b.data && b.data.blocks) {
          stripFormPreview(b.data.blocks);
        }
        if (b.type === 'slideshow' && b.data && b.data.slides) {
          b.data.slides.forEach(function(s) { stripFormPreview(s.blocks); });
        }
      });
    }
    stripFormPreview(cleanBlocks);

    fetch('/api/pages/' + PAGE_ID + '/blocks', {
      method: 'PUT',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        blocks: { blocks: cleanBlocks, version: state.version },
        convert: !!convert
      })
    })
    .then(function(r) { return r.json(); })
    .then(function(data) {
      if (data.success) {
        state.isDirty = false;
        state.convertsOnSave = false;
        $save.textContent = PB_I18N.save;
        showToast(PB_I18N.saved, 'success');
      } else {
        showToast(PB_I18N.save_failed + ': ' + (data.error || ''), 'error');
        $save.textContent = PB_I18N.save + ' *';
      }
    })
    .catch(function() {
      showToast(PB_I18N.network_error, 'error');
      $save.textContent = PB_I18N.save + ' *';
    })
    .finally(function() {
      $save.disabled = false;
    });
  }

  // ============================================================
  // RENDER CANVAS — draws all blocks
  // ============================================================
  function _inserterHTML(index) {
    return '<div class="pb-inserter" data-insert-at="' + index + '">' +
      '<div class="pb-inserter__line"></div>' +
      '<button class="pb-inserter__btn" title="' + (PB_I18N.add_block || 'Add block') + '">' +
      '<svg viewBox="0 0 24 24" width="12" height="12" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>' +
      '</button></div>';
  }

  // Build proxy URL for a video thumbnail. All YouTube/Vimeo thumbnails go
  // through our own server — no img.youtube.com calls from the admin browser.
  function videoThumbUrl(source, id) {
    if (!source || !id) return '';
    return '/media/video-thumb/' + esc(source) + '/' + esc(id) + '.jpg';
  }

  function renderCanvas() {
    if (state.blocks.length === 0) {
      $blocks.innerHTML =
        '<div class="pb-canvas__empty" id="pbEmpty">' +
        '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/></svg>' +
        '<span>' + PB_I18N.drag_hint + '</span>' +
        '</div>';
      return;
    }

    var html = _inserterHTML(0);
    state.blocks.forEach(function(block, i) {
      html += renderBlockItem(block);
      html += _inserterHTML(i + 1);
    });
    $blocks.innerHTML = html;

    // Re-select if was selected
    if (state.selectedId) {
      var el = $blocks.querySelector('[data-id="' + state.selectedId + '"]');
      if (el) el.classList.add('is-selected');
    }

    // Bind column nested block events
    bindColumnEvents();
  }

  function renderBlockItem(block) {
    var preview = getBlockPreview(block);
    var label = BLOCK_LABELS[block.type] || block.type;
    var s = block.settings || {};
    var contentStyle = s.content_alignment ? 'text-align:' + s.content_alignment : 'text-align:' + (s.alignment || 'center');

    return '<div class="pb-item" data-id="' + block.id + '" data-type="' + block.type + '" role="listitem">' +
      '<span class="pb-item__label">' + label + '</span>' +
      '<div class="pb-item__toolbar" role="toolbar" aria-label="' + label + '">' +
        '<button class="pb-item__drag-handle" title="' + (PB_I18N.drag || 'Drag') + '" aria-label="' + (PB_I18N.drag || 'Drag') + ' ' + label + '"><svg viewBox="0 0 24 24"><circle cx="9" cy="5" r="1" fill="currentColor"/><circle cx="15" cy="5" r="1" fill="currentColor"/><circle cx="9" cy="12" r="1" fill="currentColor"/><circle cx="15" cy="12" r="1" fill="currentColor"/><circle cx="9" cy="19" r="1" fill="currentColor"/><circle cx="15" cy="19" r="1" fill="currentColor"/></svg></button>' +
        '<button class="pb-item__move-up" title="' + (PB_I18N.move_up || 'Move up') + '" aria-label="' + (PB_I18N.move_up || 'Move up') + ' ' + label + '"><svg viewBox="0 0 24 24"><polyline points="18 15 12 9 6 15"/></svg></button>' +
        '<button class="pb-item__move-down" title="' + (PB_I18N.move_down || 'Move down') + '" aria-label="' + (PB_I18N.move_down || 'Move down') + ' ' + label + '"><svg viewBox="0 0 24 24"><polyline points="6 9 12 15 18 9"/></svg></button>' +
        '<button class="pb-item__duplicate" title="' + (PB_I18N.duplicate || 'Duplicate') + '" aria-label="' + (PB_I18N.duplicate || 'Duplicate') + ' ' + label + '"><svg viewBox="0 0 24 24"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1"/></svg></button>' +
        '<button class="pb-item__delete" title="' + (PB_I18N.delete || 'Delete') + '" aria-label="' + (PB_I18N.delete || 'Delete') + ' ' + label + '"><svg viewBox="0 0 24 24"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 01-2 2H7a2 2 0 01-2-2V6m3 0V4a2 2 0 012-2h4a2 2 0 012 2v2"/></svg></button>' +
      '</div>' +
      '<div class="pb-item__content"' + (contentStyle ? ' style="' + contentStyle + '"' : '') + '>' + preview + '</div>' +
    '</div>';
  }

  // ============================================================
  // BLOCK PREVIEWS — visual representation in canvas
  // ============================================================

  // Text block toolbar HTML — B, I, U, S, align, lists, link, clear
  var _textToolbarHTML = '<div class="pb-text-toolbar" role="toolbar">' +
    '<button class="pb-text-toolbar__btn" type="button" data-cmd="bold" title="' + (PB_I18N.bold || 'Bold') + '"><svg viewBox="0 0 24 24"><path d="M6 4h8a4 4 0 014 4 4 4 0 01-4 4H6z"/><path d="M6 12h9a4 4 0 014 4 4 4 0 01-4 4H6z"/></svg></button>' +
    '<button class="pb-text-toolbar__btn" type="button" data-cmd="italic" title="' + (PB_I18N.italic || 'Italic') + '"><svg viewBox="0 0 24 24"><line x1="19" y1="4" x2="10" y2="4"/><line x1="14" y1="20" x2="5" y2="20"/><line x1="15" y1="4" x2="9" y2="20"/></svg></button>' +
    '<button class="pb-text-toolbar__btn" type="button" data-cmd="underline" title="' + (PB_I18N.underline || 'Underline') + '"><svg viewBox="0 0 24 24"><path d="M6 3v7a6 6 0 006 6 6 6 0 006-6V3"/><line x1="4" y1="21" x2="20" y2="21"/></svg></button>' +
    '<button class="pb-text-toolbar__btn" type="button" data-cmd="strikeThrough" title="' + (PB_I18N.strikethrough || 'Strikethrough') + '"><svg viewBox="0 0 24 24"><line x1="4" y1="12" x2="20" y2="12"/><path d="M17.3 4.9C16.2 4 14.7 3.5 13 3.5c-3.6 0-5.5 2-5.5 4 0 .7.2 1.4.5 2"/><path d="M8 14.5c0 3 2.5 5 6 5 1.8 0 3.3-.5 4.3-1.4"/></svg></button>' +
    '<span class="pb-text-toolbar__sep"></span>' +
    '<button class="pb-text-toolbar__btn" type="button" data-cmd="justifyLeft" title="' + (PB_I18N.align_left || 'Align left') + '"><svg viewBox="0 0 24 24"><line x1="17" y1="10" x2="3" y2="10"/><line x1="21" y1="6" x2="3" y2="6"/><line x1="21" y1="14" x2="3" y2="14"/><line x1="17" y1="18" x2="3" y2="18"/></svg></button>' +
    '<button class="pb-text-toolbar__btn" type="button" data-cmd="justifyCenter" title="' + (PB_I18N.align_center || 'Align center') + '"><svg viewBox="0 0 24 24"><line x1="18" y1="10" x2="6" y2="10"/><line x1="21" y1="6" x2="3" y2="6"/><line x1="21" y1="14" x2="3" y2="14"/><line x1="18" y1="18" x2="6" y2="18"/></svg></button>' +
    '<button class="pb-text-toolbar__btn" type="button" data-cmd="justifyRight" title="' + (PB_I18N.align_right || 'Align right') + '"><svg viewBox="0 0 24 24"><line x1="21" y1="10" x2="7" y2="10"/><line x1="21" y1="6" x2="3" y2="6"/><line x1="21" y1="14" x2="3" y2="14"/><line x1="21" y1="18" x2="7" y2="18"/></svg></button>' +
    '<button class="pb-text-toolbar__btn" type="button" data-cmd="justifyFull" title="' + (PB_I18N.align_justify || 'Justify') + '"><svg viewBox="0 0 24 24"><line x1="21" y1="6" x2="3" y2="6"/><line x1="21" y1="10" x2="3" y2="10"/><line x1="21" y1="14" x2="3" y2="14"/><line x1="21" y1="18" x2="3" y2="18"/></svg></button>' +
    '<span class="pb-text-toolbar__sep"></span>' +
    '<button class="pb-text-toolbar__btn" type="button" data-cmd="insertUnorderedList" title="' + (PB_I18N.bullet_list || 'Bullet list') + '"><svg viewBox="0 0 24 24"><line x1="8" y1="6" x2="21" y2="6"/><line x1="8" y1="12" x2="21" y2="12"/><line x1="8" y1="18" x2="21" y2="18"/><circle cx="4" cy="6" r="1" fill="currentColor" stroke="none"/><circle cx="4" cy="12" r="1" fill="currentColor" stroke="none"/><circle cx="4" cy="18" r="1" fill="currentColor" stroke="none"/></svg></button>' +
    '<button class="pb-text-toolbar__btn" type="button" data-cmd="insertOrderedList" title="' + (PB_I18N.numbered_list || 'Numbered list') + '"><svg viewBox="0 0 24 24"><line x1="10" y1="6" x2="21" y2="6"/><line x1="10" y1="12" x2="21" y2="12"/><line x1="10" y1="18" x2="21" y2="18"/><text x="4" y="8" font-size="7" fill="currentColor" stroke="none" font-family="var(--pb-font)">1</text><text x="4" y="14" font-size="7" fill="currentColor" stroke="none" font-family="var(--pb-font)">2</text><text x="4" y="20" font-size="7" fill="currentColor" stroke="none" font-family="var(--pb-font)">3</text></svg></button>' +
    '<span class="pb-text-toolbar__sep"></span>' +
    '<button class="pb-text-toolbar__btn" type="button" data-cmd="createLink" title="' + (PB_I18N.link || 'Link') + '"><svg viewBox="0 0 24 24"><path d="M10 13a5 5 0 007.54.54l3-3a5 5 0 00-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 00-7.54-.54l-3 3a5 5 0 007.07 7.07l1.71-1.71"/></svg></button>' +
    '<button class="pb-text-toolbar__btn" type="button" data-cmd="removeFormat" title="' + (PB_I18N.clear_formatting || 'Clear formatting') + '"><svg viewBox="0 0 24 24"><path d="M4 7V4h16v3"/><path d="M9 20h6"/><path d="M12 4v16"/><line x1="3" y1="21" x2="21" y2="3" stroke-width="1.5"/></svg></button>' +
    '</div>';

  // Compact toolbar for table cells — B, I, U, S, link, clear (no lists/alignment)
  var _tableToolbarHTML = '<div class="pb-text-toolbar pb-text-toolbar--compact" role="toolbar">' +
    '<button class="pb-text-toolbar__btn" type="button" data-cmd="bold" title="' + (PB_I18N.bold || 'Bold') + '"><svg viewBox="0 0 24 24"><path d="M6 4h8a4 4 0 014 4 4 4 0 01-4 4H6z"/><path d="M6 12h9a4 4 0 014 4 4 4 0 01-4 4H6z"/></svg></button>' +
    '<button class="pb-text-toolbar__btn" type="button" data-cmd="italic" title="' + (PB_I18N.italic || 'Italic') + '"><svg viewBox="0 0 24 24"><line x1="19" y1="4" x2="10" y2="4"/><line x1="14" y1="20" x2="5" y2="20"/><line x1="15" y1="4" x2="9" y2="20"/></svg></button>' +
    '<button class="pb-text-toolbar__btn" type="button" data-cmd="underline" title="' + (PB_I18N.underline || 'Underline') + '"><svg viewBox="0 0 24 24"><path d="M6 3v7a6 6 0 006 6 6 6 0 006-6V3"/><line x1="4" y1="21" x2="20" y2="21"/></svg></button>' +
    '<button class="pb-text-toolbar__btn" type="button" data-cmd="strikeThrough" title="' + (PB_I18N.strikethrough || 'Strikethrough') + '"><svg viewBox="0 0 24 24"><line x1="4" y1="12" x2="20" y2="12"/><path d="M17.3 4.9C16.2 4 14.7 3.5 13 3.5c-3.6 0-5.5 2-5.5 4 0 .7.2 1.4.5 2"/><path d="M8 14.5c0 3 2.5 5 6 5 1.8 0 3.3-.5 4.3-1.4"/></svg></button>' +
    '<span class="pb-text-toolbar__sep"></span>' +
    '<button class="pb-text-toolbar__btn" type="button" data-cmd="createLink" title="' + (PB_I18N.link || 'Link') + '"><svg viewBox="0 0 24 24"><path d="M10 13a5 5 0 007.54.54l3-3a5 5 0 00-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 00-7.54-.54l-3 3a5 5 0 007.07 7.07l1.71-1.71"/></svg></button>' +
    '<button class="pb-text-toolbar__btn" type="button" data-cmd="removeFormat" title="' + (PB_I18N.clear_formatting || 'Clear formatting') + '"><svg viewBox="0 0 24 24"><path d="M4 7V4h16v3"/><path d="M9 20h6"/><path d="M12 4v16"/><line x1="3" y1="21" x2="21" y2="3" stroke-width="1.5"/></svg></button>' +
    '</div>';

  // Clean sanitized HTML — strip comments, invisible chars, normalize spaces
  function _cleanPasteResult(html) {
    return html
      .replace(/<!--[\s\S]*?-->/g, '')
      .replace(/&nbsp;/g, ' ')
      .replace(/[\u00A0\u200B\u200C\u200D\u2060\uFEFF\u200E\u200F\u2028\u2029]/g, function(ch) {
        return ch === '\u00A0' ? ' ' : '';
      })
      .replace(/ {2,}/g, ' ')
      .trim();
  }

  // Tags to remove entirely with their content (not unwrap)
  var _removeTags = {STYLE:1, SCRIPT:1, HEAD:1, TITLE:1, META:1, LINK:1, NOSCRIPT:1, IFRAME:1, OBJECT:1, EMBED:1};

  // Sanitize table cell HTML — allow only basic inline formatting
  function _sanitizeTableCell(html) {
    if (!html || html.indexOf('<') === -1) return html || '';
    var allowed = {B:1, STRONG:1, I:1, EM:1, U:1, S:1, A:1, BR:1};
    var tmp = document.createElement('div');
    tmp.innerHTML = html;
    var els = tmp.querySelectorAll('*');
    for (var i = els.length - 1; i >= 0; i--) {
      var el = els[i];
      if (_removeTags[el.tagName]) {
        el.parentNode.removeChild(el);
      } else if (!allowed[el.tagName]) {
        while (el.firstChild) el.parentNode.insertBefore(el.firstChild, el);
        el.parentNode.removeChild(el);
      } else if (el.tagName === 'A') {
        var href = el.getAttribute('href');
        while (el.attributes.length > 0) el.removeAttribute(el.attributes[0].name);
        if (href) el.setAttribute('href', href);
      } else {
        while (el.attributes.length > 0) el.removeAttribute(el.attributes[0].name);
      }
    }
    return _cleanPasteResult(tmp.innerHTML);
  }

  // Sanitize rich text paste — allow formatting + structure, strip styles/backgrounds
  function _sanitizeRichPaste(html) {
    if (!html || html.indexOf('<') === -1) return html || '';
    var allowed = {B:1, STRONG:1, I:1, EM:1, U:1, S:1, A:1, BR:1, P:1, UL:1, OL:1, LI:1, H1:1, H2:1, H3:1, H4:1, H5:1, H6:1, BLOCKQUOTE:1};
    var tmp = document.createElement('div');
    tmp.innerHTML = html;
    var els = tmp.querySelectorAll('*');
    for (var i = els.length - 1; i >= 0; i--) {
      var el = els[i];
      if (_removeTags[el.tagName]) {
        el.parentNode.removeChild(el);
      } else if (!allowed[el.tagName]) {
        while (el.firstChild) el.parentNode.insertBefore(el.firstChild, el);
        el.parentNode.removeChild(el);
      } else if (el.tagName === 'A') {
        var href = el.getAttribute('href');
        while (el.attributes.length > 0) el.removeAttribute(el.attributes[0].name);
        if (href) el.setAttribute('href', href);
      } else {
        while (el.attributes.length > 0) el.removeAttribute(el.attributes[0].name);
      }
    }
    return _cleanPasteResult(tmp.innerHTML);
  }

  function getBlockPreview(block) {
    var d = block.data || {};
    switch (block.type) {
      case 'heading':
        var tag = 'h' + (d.level || 2);
        return '<' + tag + ' class="pb-inline-heading" contenteditable="true" data-inline-key="data.text" data-inline-block="' + block.id + '" data-placeholder="' + esc(PB_I18N.default_heading || 'New Heading') + '">' + esc(d.text || '') + '</' + tag + '>';
      case 'text':
        return '<div class="pb-rich-zone">' + _textToolbarHTML +
          '<div class="pb-inline-text' + (_isRichHtmlEmpty(d.html) ? ' pb-show-placeholder' : '') + '" contenteditable="true" data-inline-html="data.html" data-inline-block="' + block.id + '" data-placeholder="' + esc(PB_I18N.default_text || 'Start writing here...') + '">' + (d.html || '<p><br></p>') + '</div></div>';
      case 'image':
        if (d.url) {
          var altTag = d.alt || '';
          var altBadge = altTag
            ? '<div class="pb-text-hint--sm pb-alt-badge-wrap"><span class="pb-thumb-label">alt: ' + esc(altTag) + '</span></div>'
            : '<div class="pb-text-hint--sm pb-alt-badge-wrap"><span class="pb-alt-warn">⚠ ' + (PB_I18N.alt_missing || 'No alt text — select image from library') + '</span></div>';
          return '<div class="pb-img-canvas-wrap"><figure class="pb-img-canvas pb-img-canvas--' + (d.size || 'full') + '"><img src="' + esc(d.url) + '" class="pb-preview-img--canvas" alt="' + esc(d.alt) + '"></figure>' +
            '<button data-clear-image="' + block.id + '" title="' + (PB_I18N.remove || 'Remove') + '" class="pb-clear-btn">✕</button></div>' + altBadge;
        }
        return '<div data-image-pick-block="' + block.id + '" class="pb-picker-zone">' +
          '<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="pb-picker-icon"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="8.5" cy="8.5" r="1.5"/><polyline points="21 15 16 10 5 21"/></svg>' +
          '<div class="pb-picker-label">' + (PB_I18N.click_select_image || 'Click to select image') + '</div>' +
          '<span class="pb-picker-btn">📷 ' + (PB_I18N.choose_image || 'Select Image') + '</span>' +
          '</div>';
      case 'button':
        return '<span class="pb-inline-btn pb-canvas-button" contenteditable="true" data-inline-key="data.text" data-inline-block="' + block.id + '" data-placeholder="' + esc(PB_I18N.default_button || 'Click Me') + '">' + esc(d.text || '') + '</span>';
      case 'divider':
        return '<hr style="border:none;border-top:1px ' + (d.style || 'solid') + ' var(--pb-border)">';
      case 'spacer':
        return '<div class="pb-spacer-preview" style="height:' + (d.height || 48) + 'px">' + (d.height || 48) + 'px</div>';
      case 'columns':
        var cols = d.columns || [];
        var layoutStyle = '';
        switch (d.layout || '1/2+1/2') {
          case '1': layoutStyle = 'grid-template-columns:1fr'; break;
          case '1/2+1/2': layoutStyle = 'grid-template-columns:1fr 1fr'; break;
          case '1/3+1/3+1/3': layoutStyle = 'grid-template-columns:1fr 1fr 1fr'; break;
          case '2/3+1/3': layoutStyle = 'grid-template-columns:2fr 1fr'; break;
          case '1/3+2/3': layoutStyle = 'grid-template-columns:1fr 2fr'; break;
          case '1/4+1/4+1/4+1/4': layoutStyle = 'grid-template-columns:1fr 1fr 1fr 1fr'; break;
          default: layoutStyle = 'grid-template-columns:1fr 1fr';
        }
        var colsHtml = '<div style="display:grid;' + layoutStyle + ';gap:8px">';
        cols.forEach(function(c, ci) {
          colsHtml += '<div class="pb-col-zone" data-col-parent="' + block.id + '" data-col-index="' + ci + '">';
          var colBlocks = c.blocks || [];
          if (colBlocks.length === 0) {
            colsHtml += '<div class="pb-col-zone__empty" data-col-add="' + block.id + '" data-col-idx="' + ci + '">+ ' + (PB_I18N.add_block || 'Add block') + '</div>';
          } else {
            colBlocks.forEach(function(cb, cbi) {
              if (cbi === 0) {
                colsHtml += '<div class="pb-inserter pb-inserter--sm" data-col-insert-parent="' + block.id + '" data-col-insert-col="' + ci + '" data-col-insert-at="0"><div class="pb-inserter__line"></div><button class="pb-inserter__btn" title="' + (PB_I18N.add_block || 'Add block') + '"><svg viewBox="0 0 24 24" width="10" height="10" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg></button></div>';
              }
              var cbPreview = getNestedBlockPreview(cb);
              var cbS = cb.settings || {};
              var cbStyle = cbS.content_alignment ? 'text-align:' + cbS.content_alignment : 'text-align:' + (cbS.alignment || 'center');
              colsHtml += '<div class="pb-col-block" data-col-block-parent="' + block.id + '" data-col-block-col="' + ci + '" data-col-block-idx="' + cbi + '" data-col-block-id="' + cb.id + '">';
              colsHtml += '<div class="pb-col-block__toolbar">';
              colsHtml += '<button class="pb-col-block__drag-handle" title="' + (PB_I18N.drag || 'Drag') + '"><svg viewBox="0 0 24 24" fill="currentColor"><circle cx="9" cy="5" r="1.5"/><circle cx="15" cy="5" r="1.5"/><circle cx="9" cy="12" r="1.5"/><circle cx="15" cy="12" r="1.5"/><circle cx="9" cy="19" r="1.5"/><circle cx="15" cy="19" r="1.5"/></svg></button>';
              colsHtml += '<button class="pb-col-block__move-up" title="' + (PB_I18N.move_up || 'Move up') + '"><svg viewBox="0 0 24 24"><polyline points="18 15 12 9 6 15"/></svg></button>';
              colsHtml += '<button class="pb-col-block__move-down" title="' + (PB_I18N.move_down || 'Move down') + '"><svg viewBox="0 0 24 24"><polyline points="6 9 12 15 18 9"/></svg></button>';
              colsHtml += '<button class="pb-col-block__duplicate" title="' + (PB_I18N.duplicate || 'Duplicate') + '"><svg viewBox="0 0 24 24"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1"/></svg></button>';
              colsHtml += '<button class="pb-col-block__delete" title="' + (PB_I18N.delete || 'Delete') + '"><svg viewBox="0 0 24 24"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 01-2 2H7a2 2 0 01-2-2V6m3 0V4a2 2 0 012-2h4a2 2 0 012 2v2"/></svg></button>';
              colsHtml += '</div>';
              colsHtml += '<div class="pb-col-block__content"' + (cbStyle ? ' style="' + cbStyle + '"' : '') + '>' + cbPreview + '</div>';
              colsHtml += '</div>';
              colsHtml += '<div class="pb-inserter pb-inserter--sm" data-col-insert-parent="' + block.id + '" data-col-insert-col="' + ci + '" data-col-insert-at="' + (cbi + 1) + '"><div class="pb-inserter__line"></div><button class="pb-inserter__btn" title="' + (PB_I18N.add_block || 'Add block') + '"><svg viewBox="0 0 24 24" width="10" height="10" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg></button></div>';
            });
            colsHtml += '<button class="pb-col-add" data-col-add="' + block.id + '" data-col-idx="' + ci + '" title="' + (PB_I18N.add_block || 'Add block') + '">+</button>';
          }
          colsHtml += '</div>';
        });
        colsHtml += '</div>';
        return colsHtml;
      case 'section':
        // Preview writes --pb-section-bg (light) and --pb-section-bg-dark
        // inline. CSS rule .pb-canvas--theme-dark .pb-section-zone picks the
        // dark variable when the canvas is in dark theme — no re-render.
        // Defaults for both vars live at the top of this CSS file.
        var bgType = d.background_type || 'color';
        var bgVal = d.background_value || '';
        var bgTypeDark = d.background_type_dark || 'color';
        var bgValDark = d.background_value_dark || '';

        function _bgVarValue(type, val) {
          if (type === 'color' && val) return esc(val);
          if (type === 'gradient' && val) return esc(val);
          if (type === 'image' && val) return 'url(' + esc(val) + ') center/cover no-repeat';
          return '';  // none or empty — CSS var() fallback handles it
        }
        var lightBg = _bgVarValue(bgType, bgVal);
        var darkBg = _bgVarValue(bgTypeDark, bgValDark);

        var bgStyleParts = [];
        if (lightBg) bgStyleParts.push('--pb-section-bg:' + lightBg);
        if (darkBg)  bgStyleParts.push('--pb-section-bg-dark:' + darkBg);
        if (bgType === 'none' && !lightBg) bgStyleParts.push('--pb-section-bg:transparent');
        if (bgTypeDark === 'none' && !darkBg) bgStyleParts.push('--pb-section-bg-dark:transparent');
        var bgStyle = bgStyleParts.join(';');
        var noneClass = (bgType === 'none' || bgTypeDark === 'none') ? ' pb-section-zone--none' : '';
        var textColorAttr = '';
        if (d.text_color === 'light') textColorAttr = ' data-text-color="light"';
        else if (d.text_color === 'dark') textColorAttr = ' data-text-color="dark"';
        var secBlocks = d.blocks || [];
        var fwBadge = d.full_width ? '<span class="pb-fw-badge">' + (PB_I18N.section_full_width_badge || 'FULL WIDTH') + '</span>' : '';
        var cwLabel = d.content_width && d.content_width !== 'default' ? ' · ' + (d.content_width === 'narrow' ? '768px' : d.content_width === 'wide' ? '1200px' : 'Full') : '';
        var secHtml = '<div class="pb-section-zone' + noneClass + '" data-sec-parent="' + block.id + '"' + textColorAttr + ' style="padding:16px;border-radius:8px;' + bgStyle + ';min-height:60px;position:relative">';
        secHtml += '<div class="pb-section-zone__label">' + PB_I18N.section + fwBadge + '<span class="pb-dim">' + cwLabel + '</span></div>';
        if (secBlocks.length === 0) {
          secHtml += '<div class="pb-sec-zone__empty pb-section-zone__empty" data-sec-add="' + block.id + '">+ ' + (PB_I18N.add_block || 'Add block') + '</div>';
        } else {
          secBlocks.forEach(function(sb, sbi) {
            if (sbi === 0) {
              secHtml += '<div class="pb-inserter pb-inserter--sm" data-sec-insert-parent="' + block.id + '" data-sec-insert-at="0"><div class="pb-inserter__line"></div><button class="pb-inserter__btn" title="' + (PB_I18N.add_block || 'Add block') + '"><svg viewBox="0 0 24 24" width="10" height="10" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg></button></div>';
            }
            var sbPreview = getNestedBlockPreview(sb);
            var sbS = sb.settings || {};
            var sbStyle = sbS.content_alignment ? 'text-align:' + sbS.content_alignment : 'text-align:' + (sbS.alignment || 'center');
            secHtml += '<div class="pb-sec-block pb-sec-block" data-sec-block-parent="' + block.id + '" data-sec-block-idx="' + sbi + '" data-sec-block-id="' + sb.id + '">';
            secHtml += '<div class="pb-sec-block__toolbar">';
            secHtml += '<button class="pb-sec-block__drag-handle pb-sec-toolbar-btn" title="' + (PB_I18N.drag || 'Drag') + '"><svg width="10" height="10" viewBox="0 0 24 24" fill="currentColor"><circle cx="9" cy="5" r="1.5"/><circle cx="15" cy="5" r="1.5"/><circle cx="9" cy="12" r="1.5"/><circle cx="15" cy="12" r="1.5"/><circle cx="9" cy="19" r="1.5"/><circle cx="15" cy="19" r="1.5"/></svg></button>';
            secHtml += '<button class="pb-sec-block__move-up pb-sec-toolbar-btn" title="' + (PB_I18N.move_up || 'Move up') + '"><svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="18 15 12 9 6 15"/></svg></button>';
            secHtml += '<button class="pb-sec-block__move-down pb-sec-toolbar-btn" title="' + (PB_I18N.move_down || 'Move down') + '"><svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="6 9 12 15 18 9"/></svg></button>';
            secHtml += '<button class="pb-sec-block__duplicate pb-sec-toolbar-btn" title="' + (PB_I18N.duplicate || 'Duplicate') + '"><svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1"/></svg></button>';
            secHtml += '<button class="pb-sec-block__delete pb-sec-toolbar-btn" title="' + (PB_I18N.delete || 'Delete') + '"><svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 01-2 2H7a2 2 0 01-2-2V6m3 0V4a2 2 0 012-2h4a2 2 0 012 2v2"/></svg></button>';
            secHtml += '</div>';
            secHtml += '<div class="pb-sec-block__content"' + (sbStyle ? ' style="' + sbStyle + '"' : '') + '>' + sbPreview + '</div>';
            secHtml += '</div>';
            secHtml += '<div class="pb-inserter pb-inserter--sm" data-sec-insert-parent="' + block.id + '" data-sec-insert-at="' + (sbi + 1) + '"><div class="pb-inserter__line"></div><button class="pb-inserter__btn" title="' + (PB_I18N.add_block || 'Add block') + '"><svg viewBox="0 0 24 24" width="10" height="10" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg></button></div>';
          });
          secHtml += '<button class="pb-sec-add pb-dashed-add--compact" data-sec-add="' + block.id + '" title="' + (PB_I18N.add_block || 'Add block') + '">+</button>';
        }
        secHtml += '</div>';
        return secHtml;
      case 'video':
        var vidId = d.embed_id || '';
        var vidSource = d.source || '';
        if (vidId) {
          var vidThumb = '<img src="' + videoThumbUrl(vidSource, vidId) + '" alt="' + esc(d.title || '') + '" class="pb-cover-img">';
          return '<div class="pb-video-wrap">' +
            '<div class="pb-aspect-video">' + vidThumb + '</div>' +
            '<div class="pb-overlay-center"><div class="pb-play-btn pb-play-btn--lg">▶</div></div>' +
            '<button data-clear-video="' + block.id + '" title="' + (PB_I18N.remove || 'Remove') + '" class="pb-clear-btn pb-clear-btn--z2">✕</button>' +
            (d.title ? '<div class="pb-video-title-bar">' + esc(d.title) + '</div>' : '') +
            '</div>';
        }
        return '<div data-video-pick-block="' + block.id + '" class="pb-gal-canvas-empty">' +
          '<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="pb-picker-icon"><rect x="2" y="4" width="20" height="16" rx="2"/><polygon points="10 8 16 12 10 16 10 8"/></svg>' +
          '<div class="pb-picker-label">' + (PB_I18N.video_gallery_empty || 'Click to add video') + '</div>' +
          '<span class="pb-picker-btn">+ ' + (PB_I18N.video_gallery_add || 'Add video') + '</span>' +
          '</div>';
      case 'gallery':
        var galImages = d.images || [];
        var count = galImages.length;
        var galCols = d.columns || 3;
        if (count > 0) {
          var displayCols = Math.min(galCols, count);
          var galHtml = '<div style="display:grid;grid-template-columns:repeat(' + displayCols + ',1fr);gap:6px;width:100%;border-radius:8px;overflow:hidden">';
          galImages.forEach(function(img, gi) {
            galHtml += '<div class="pb-gal-thumb pb-gal-thumb--square">';
            if (img.url) galHtml += '<img src="' + esc(img.url) + '" alt="' + esc(img.alt || '') + '" class="pb-cover-img">';
            galHtml += '<button data-gal-canvas-remove="' + gi + '" data-gal-canvas-block="' + block.id + '" title="' + (PB_I18N.remove || 'Remove') + '" class="pb-gal-thumb__remove">&times;</button>';
            if (count > 1) {
              galHtml += '<div class="pb-gal-thumb__arrows">';
              if (gi > 0) galHtml += '<button data-gal-move-block="' + block.id + '" data-gal-move-from="' + gi + '" data-gal-move-dir="-1" class="pb-gal-thumb__arrow" title="' + (PB_I18N.move_up || 'Move left') + '">‹</button>';
              if (gi < count - 1) galHtml += '<button data-gal-move-block="' + block.id + '" data-gal-move-from="' + gi + '" data-gal-move-dir="1" class="pb-gal-thumb__arrow" title="' + (PB_I18N.move_down || 'Move right') + '">›</button>';
              galHtml += '</div>';
            }
            galHtml += '</div>';
          });
          galHtml += '</div>';
          galHtml += '<div class="pb-mt-2"><button data-gal-canvas-add="' + block.id + '" class="pb-gal-canvas-add">📷 ' + (PB_I18N.gallery_add_from_library || 'Add from Library') + '</button></div>';
          return galHtml;
        }
        return '<div data-gal-canvas-add="' + block.id + '" class="pb-gal-canvas-empty">' +
          '<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="pb-picker-icon"><rect x="2" y="2" width="8" height="8" rx="1"/><rect x="14" y="2" width="8" height="8" rx="1"/><rect x="2" y="14" width="8" height="8" rx="1"/><rect x="14" y="14" width="8" height="8" rx="1"/></svg>' +
          '<div class="pb-picker-label">' + (PB_I18N.click_select_image || 'Click to select images') + '</div>' +
          '<span class="pb-picker-btn">📷 ' + (PB_I18N.gallery_add_from_library || 'Add from Library') + '</span>' +
          '</div>';
      case 'video_gallery':
        var vgVideos = d.videos || [];
        var vgCount = vgVideos.length;
        var vgCols = d.columns || 2;
        if (vgCount > 0) {
          var vgDisplayCols = Math.min(vgCols, vgCount);
          var vgHtml = '<div style="display:grid;grid-template-columns:repeat(' + vgDisplayCols + ',1fr);gap:6px;width:100%">';
          vgVideos.forEach(function(vid, vi) {
            var thumbUrl = '';
            if (vid.embed_id && vid.source) {
              thumbUrl = videoThumbUrl(vid.source, vid.embed_id);
            }
            vgHtml += '<div class="pb-gal-thumb pb-video-wrap--sm">';
            vgHtml += '<div class="pb-aspect-video">';
            if (thumbUrl) {
              vgHtml += '<img src="' + thumbUrl + '" alt="' + esc(vid.title || '') + '" class="pb-cover-img">';
            } else {
              vgHtml += '<div class="pb-video-placeholder">' + (vid.source === 'vimeo' ? 'Vimeo' : '▶') + '</div>';
            }
            vgHtml += '<div class="pb-overlay-center"><div class="pb-play-btn pb-play-btn--sm">▶</div></div>';
            vgHtml += '</div>';
            if (vid.title) vgHtml += '<div class="pb-vg-title--sm">' + esc(vid.title) + '</div>';
            vgHtml += '<button data-vgal-canvas-remove="' + vi + '" data-vgal-canvas-block="' + block.id + '" title="' + (PB_I18N.remove || 'Remove') + '" class="pb-gal-thumb__remove">&times;</button>';
            if (vgCount > 1) {
              vgHtml += '<div class="pb-gal-thumb__arrows">';
              if (vi > 0) vgHtml += '<button data-vgal-move-block="' + block.id + '" data-vgal-move-from="' + vi + '" data-vgal-move-dir="-1" class="pb-gal-thumb__arrow" title="' + (PB_I18N.move_up || 'Move left') + '">‹</button>';
              if (vi < vgCount - 1) vgHtml += '<button data-vgal-move-block="' + block.id + '" data-vgal-move-from="' + vi + '" data-vgal-move-dir="1" class="pb-gal-thumb__arrow" title="' + (PB_I18N.move_down || 'Move right') + '">›</button>';
              vgHtml += '</div>';
            }
            vgHtml += '</div>';
          });
          vgHtml += '</div>';
          vgHtml += '<div class="pb-mt-2 pb-text-center"><button data-vgal-canvas-add="' + block.id + '" class="pb-gal-canvas-add">+ ' + (PB_I18N.video_gallery_add || 'Add video') + '</button></div>';
          return vgHtml;
        }
        return '<div data-vgal-canvas-add="' + block.id + '" class="pb-gal-canvas-empty">' +
          '<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="pb-picker-icon"><rect x="2" y="4" width="20" height="16" rx="2"/><polygon points="10 8 16 12 10 16 10 8"/></svg>' +
          '<div class="pb-picker-label">' + (PB_I18N.video_gallery_empty || 'Click to add videos') + '</div>' +
          '<span class="pb-picker-btn">+ ' + (PB_I18N.video_gallery_add || 'Add video') + '</span>' +
          '</div>';
      case 'html':
        return '<div class="pb-relative"><span class="pb-badge--warning">⚠ ' + (PB_I18N.html_executes || 'Executes on page') + '</span>' +
          '<textarea class="pb-inline-textarea pb-pt-7" data-inline-textarea="data.code" data-inline-block="' + block.id + '" placeholder="<!-- HTML -->">' + esc(d.code || '') + '</textarea></div>';
      case 'table':
        var th = (d.headers || []);
        var tr = (d.rows || []);
        var tblLayout = d.table_width === 'auto' ? 'auto' : '100%';
        var tableHtml = '<div class="pb-inline-table pb-table-wrap">';
        // Compact formatting toolbar
        tableHtml += _tableToolbarHTML;
        // Symbol picker toolbar
        tableHtml += '<div class="pb-table-symbols" role="toolbar">' +
          '<button class="pb-table-symbols__btn" type="button" data-insert-symbol="✅" title="' + (PB_I18N.icon_check || 'Check') + '">✅</button>' +
          '<button class="pb-table-symbols__btn" type="button" data-insert-symbol="❌" title="' + (PB_I18N.icon_cross || 'Cross') + '">❌</button>' +
          '<span class="pb-table-symbols__sep"></span>' +
          '<button class="pb-table-symbols__btn" type="button" data-insert-symbol="⭐" title="' + (PB_I18N.icon_star || 'Star') + '">⭐</button>' +
          '<button class="pb-table-symbols__btn" type="button" data-insert-symbol="⚡" title="' + (PB_I18N.icon_feature || 'Feature') + '">⚡</button>' +
          '<button class="pb-table-symbols__btn" type="button" data-insert-symbol="💡" title="' + (PB_I18N.icon_tip || 'Tip') + '">💡</button>' +
          '<button class="pb-table-symbols__btn" type="button" data-insert-symbol="⚠️" title="' + (PB_I18N.icon_warning || 'Warning') + '">⚠️</button>' +
          '<span class="pb-table-symbols__sep"></span>' +
          '<button class="pb-table-symbols__btn" type="button" data-insert-symbol="➕" title="' + (PB_I18N.icon_plus || 'Plus') + '">➕</button>' +
          '<button class="pb-table-symbols__btn" type="button" data-insert-symbol="➖" title="' + (PB_I18N.icon_minus || 'Minus') + '">➖</button>' +
          '<button class="pb-table-symbols__btn pb-table-symbols__btn--dot" type="button" data-insert-symbol="●" title="' + (PB_I18N.icon_dot || 'Dot') + '">●</button>' +
          '</div>';
        tableHtml += '<table style="width:' + tblLayout + ';border-collapse:collapse">';
        var cw = d.column_widths || [];
        var ca = d.column_alignments || [];
        if (cw.some(function(w) { return w; })) {
          tableHtml += '<colgroup>';
          th.forEach(function(h, hi) { var w = cw[hi]; tableHtml += w ? '<col style="width:' + parseInt(w) + '%">' : '<col>'; });
          tableHtml += '</colgroup>';
        }
        if (th.length > 0) {
          tableHtml += '<tr>';
          th.forEach(function(h, hi) {
            var alignStyle = ca[hi] ? ' style="text-align:' + ca[hi] + '"' : '';
            tableHtml += '<th class="pb-table-th"' + alignStyle + '><div contenteditable="true" data-inline-th="' + hi + '" data-inline-block="' + block.id + '" data-placeholder="' + PB_I18N.heading + '">' + (h || '') + '</div></th>';
          });
          tableHtml += '<th class="pb-table-row-actions"></th>';
          tableHtml += '</tr>';
        }
        var colCount = th.length || (tr[0] || []).length || 1;
        var colSpanAll = colCount + 1;
        tr.forEach(function(row, ri) {
          if (ri === 0) {
            tableHtml += '<tr class="pb-table-insert-row" data-table-canvas-insert="' + block.id + '" data-table-canvas-idx="0"><td colspan="' + colSpanAll + '"><div class="pb-inserter pb-inserter--sm"><div class="pb-inserter__line"></div><button class="pb-inserter__btn" title="' + (PB_I18N.table_insert_row || 'Insert row') + '"><svg viewBox="0 0 24 24" width="10" height="10" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg></button></div></td></tr>';
          }
          tableHtml += '<tr>';
          (row || []).forEach(function(cell, ci) {
            var alignStyle = ca[ci] ? ' style="text-align:' + ca[ci] + '"' : '';
            tableHtml += '<td class="pb-table-td"' + alignStyle + '><div contenteditable="true" data-inline-td="' + ri + ',' + ci + '" data-inline-block="' + block.id + '" data-placeholder="…">' + (cell || '') + '</div></td>';
          });
          tableHtml += '<td class="pb-table-row-actions"><button class="pb-table-row-delete" data-table-canvas-delete="' + block.id + '" data-table-canvas-delete-idx="' + ri + '" title="' + (PB_I18N.remove || 'Remove') + '"><svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg></button></td>';
          tableHtml += '</tr>';
          tableHtml += '<tr class="pb-table-insert-row" data-table-canvas-insert="' + block.id + '" data-table-canvas-idx="' + (ri + 1) + '"><td colspan="' + colSpanAll + '"><div class="pb-inserter pb-inserter--sm"><div class="pb-inserter__line"></div><button class="pb-inserter__btn" title="' + (PB_I18N.table_insert_row || 'Insert row') + '"><svg viewBox="0 0 24 24" width="10" height="10" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg></button></div></td></tr>';
        });
        tableHtml += '</table></div>';
        return tableHtml;
      case 'accordion':
        var items = (d.items || []);
        var accHtml = '';
        items.forEach(function(it, i) {
          accHtml += '<div class="pb-acc-item" data-acc-item>';
          accHtml += '<div class="pb-acc-item__header" data-acc-toggle>';
          accHtml += '<span class="pb-acc-item__arrow">▸</span>';
          accHtml += '<span class="pb-inline-acc-title" contenteditable="true" data-inline-acc="' + i + '" data-inline-block="' + block.id + '" data-placeholder="' + esc(PB_I18N.default_accordion_title || 'Question') + '">' + esc(it.title || '') + '</span>';
          accHtml += '</div>';
          accHtml += '<div class="pb-acc-item__body"><div class="pb-rich-zone">' + _textToolbarHTML +
            '<div class="pb-inline-text' + (_isRichHtmlEmpty(it.content) ? ' pb-show-placeholder' : '') + '" contenteditable="true" data-inline-html="data.items.' + i + '.content" data-inline-block="' + block.id + '" data-placeholder="' + esc(PB_I18N.default_text || 'Start writing here...') + '">' + (it.content || '<p><br></p>') + '</div>' +
            '</div></div>';
          accHtml += '</div>';
        });
        return accHtml;
      case 'tabs':
        var tabs = (d.tabs || []);
        var activeIdx = _activeTabIdx(block.id);
        if (activeIdx >= tabs.length) { activeIdx = 0; _tabEditIdx[block.id] = 0; }
        var tabsHtml = '<div class="pb-tabs-nav">';
        tabs.forEach(function(t, i) {
          tabsHtml += '<span class="pb-tabs-btn' + (i === activeIdx ? ' is-active' : '') + '" contenteditable="true" data-inline-tab="' + i + '" data-inline-block="' + block.id + '" data-placeholder="' + esc(PB_I18N.default_tab || 'Tab') + '" data-tab-switch="' + i + '" data-tab-block="' + block.id + '">' + esc(t.title || '') + '</span>';
        });
        tabsHtml += '</div>';
        tabs.forEach(function(t, i) {
          tabsHtml += '<div class="pb-tab-panel' + (i === activeIdx ? ' is-active' : '') + '" data-tab-panel="' + i + '" data-tab-block="' + block.id + '">';
          tabsHtml += '<div class="pb-rich-zone">' + _textToolbarHTML +
            '<div class="pb-inline-text' + (_isRichHtmlEmpty(t.content) ? ' pb-show-placeholder' : '') + '" contenteditable="true" data-inline-html="data.tabs.' + i + '.content" data-inline-block="' + block.id + '" data-placeholder="' + esc(PB_I18N.default_text || 'Start writing here...') + '">' + (t.content || '<p><br></p>') + '</div></div>';
          tabsHtml += '</div>';
        });
        return tabsHtml;
      case 'audio':
        return '<div class="pb-info-box">' +
          '<div class="pb-audio-row"><span class="pb-emoji-muted">♪</span>' +
          '<input class="pb-inline-input pb-flex-1" type="text" data-inline-input="data.title" data-inline-block="' + block.id + '" value="' + esc(d.title || '') + '" placeholder="' + esc(PB_I18N.audio_title || 'Title') + '"></div>' +
          '<input class="pb-inline-input pb-input--sm" type="text" data-inline-input="data.url" data-inline-block="' + block.id + '" value="' + esc(d.url || '') + '" placeholder="' + esc(PB_I18N.audio_url || 'Audio URL') + '">' +
          '</div>';
      case 'quote':
        return '<blockquote class="pb-inline-quote" contenteditable="true" data-inline-key="data.text" data-inline-block="' + block.id + '" data-placeholder="' + esc(PB_I18N.quote_text || 'Enter quote...') + '">' + esc(d.text || '') + '</blockquote>' + (d.author ? '<small class="pb-quote-author">— ' + esc(d.author) + '</small>' : '');
      case 'code':
        var langLabel = d.language || 'plain text';
        return '<div class="pb-relative"><span class="pb-badge--muted">' + esc(langLabel) + '</span>' +
          '<textarea class="pb-inline-textarea pb-pt-7" data-inline-textarea="data.code" data-inline-block="' + block.id + '" placeholder="// code">' + esc(d.code || '') + '</textarea></div>';
      case 'form':
        if (d.form_id && d._fields) {
          return '<div class="pb-relative">' +
            renderFormPreview(block) +
            '<button data-clear-form="' + block.id + '" title="' + (PB_I18N.remove || 'Remove') + '" class="pb-clear-btn">✕</button>' +
            '</div>';
        }
        if (d.form_id && !d._fields) {
          if (!_formPreviewFetching[d.form_id]) {
            _formPreviewFetching[d.form_id] = true;
            fetch('/api/forms/' + d.form_id)
              .then(function(r) { return r.json(); })
              .then(function(data) {
                var formData = data.fields ? data : (data.form || {});
                if (formData.fields) {
                  block.data._fields = formData.fields;
                  block.data._settings = formData.settings || {};
                  block.data._description = formData.description || '';
                  block.data._form_title_raw = formData.title || '';
                  block.data._form_title = (formData.title || '') + ' (' + (formData.form_type || '') + ', ' + (formData.fields.length || 0) + ' ' + (PB_I18N.form_fields || 'fields') + ')';
                  renderCanvas();
                  if (state.selectedId === block.id) {
                    openPanel(block);
                  }
                }
              })
              .catch(function() {})
              .finally(function() { delete _formPreviewFetching[d.form_id]; });
          }
          var loadingName = d._form_title || d._form_title_raw || ((PB_I18N.form || 'Form') + ' #' + d.form_id);
          return '<div class="pb-relative">' +
            '<div class="pb-form-preview pb-form-preview--loading">' +
            '<div class="ap-form"><div class="pb-form-loading">' + esc(loadingName) + '</div></div></div>' +
            '<button data-clear-form="' + block.id + '" title="' + (PB_I18N.remove || 'Remove') + '" class="pb-clear-btn">✕</button>' +
            '</div>';
        }
        return '<div data-form-pick-block="' + block.id + '" class="pb-picker-zone">' +
          '<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="pb-picker-icon"><rect x="3" y="3" width="18" height="18" rx="2"/><path d="M7 7h10"/><path d="M7 12h10"/><path d="M7 17h4"/></svg>' +
          '<div class="pb-picker-label">' + (PB_I18N.click_select_form || 'Click to select form') + '</div>' +
          '<span class="pb-picker-btn">📋 ' + (PB_I18N.form_select || 'Select form') + '</span>' +
          '</div>';
      case 'slideshow':
        var slideCount = (d.slides || []).length;
        var activeSlideIdx = _activeSlideIdx(block.id);
        if (activeSlideIdx >= slideCount) { activeSlideIdx = 0; _slideEditIdx[block.id] = 0; }
        var activeSlide = (d.slides && d.slides[activeSlideIdx]) ? d.slides[activeSlideIdx] : null;
        var slideImg = activeSlide ? (activeSlide.image || '') : '';
        var slideshowPreview = '<div class="pb-slideshow-wrap">';
        if (slideImg) {
          slideshowPreview += '<img src="' + esc(slideImg) + '" class="pb-hero-img" alt="">';
          slideshowPreview += '<button class="pb-slide-clear-image" data-slide-clear="' + block.id + '" data-slide-clear-idx="' + activeSlideIdx + '" title="' + (PB_I18N.remove || 'Remove') + '">✕</button>';
        } else {
          slideshowPreview += '<div class="pb-slide-placeholder"></div>';
        }
        // Nested blocks zone on active slide
        var slideTextColorAttr = '';
        if (d.text_color === 'light') slideTextColorAttr = ' data-text-color="light"';
        else if (d.text_color === 'dark') slideTextColorAttr = ' data-text-color="dark"';
        slideshowPreview += '<div class="pb-slide-zone pb-hero-overlay" data-slide-parent="' + block.id + '" data-slide-idx="' + activeSlideIdx + '"' + slideTextColorAttr + '>';
          // Image picker at top when no image
          if (!slideImg) {
            slideshowPreview += '<div class="pb-slide-empty-image" data-slide-pick="' + block.id + '" data-slide-pick-idx="' + activeSlideIdx + '"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="8.5" cy="8.5" r="1.5"/><polyline points="21 15 16 10 5 21"/></svg> ' + PB_I18N.click_select_image + '</div>';
          }
          var slideBlocks = (activeSlide && activeSlide.blocks) || [];
          if (slideBlocks.length === 0) {
            slideshowPreview += '<div class="pb-slide-zone__empty pb-section-zone__empty--dark" data-slide-add="' + block.id + '">+ ' + (PB_I18N.slideshow_add_block || 'Add block to slide') + '</div>';
          } else {
            slideBlocks.forEach(function(sb, sbi) {
              if (sbi === 0) {
                slideshowPreview += '<div class="pb-inserter pb-inserter--sm" data-slide-insert-parent="' + block.id + '" data-slide-insert-at="0"><div class="pb-inserter__line"></div><button class="pb-inserter__btn" title="' + (PB_I18N.add_block || 'Add block') + '"><svg viewBox="0 0 24 24" width="10" height="10" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg></button></div>';
              }
              var sbPreview = getNestedBlockPreview(sb);
              var sbS = sb.settings || {};
              var sbStyle = sbS.content_alignment ? 'text-align:' + sbS.content_alignment : 'text-align:' + (sbS.alignment || 'center');
              slideshowPreview += '<div class="pb-slide-block pb-sec-block--dark" data-slide-block-parent="' + block.id + '" data-slide-block-idx="' + sbi + '" data-slide-block-id="' + sb.id + '">';
              slideshowPreview += '<div class="pb-slide-block__toolbar pb-sec-block__toolbar">';
              slideshowPreview += '<button class="pb-slide-block__drag-handle pb-vg-toolbar-btn" title="' + (PB_I18N.drag || 'Drag') + '"><svg width="10" height="10" viewBox="0 0 24 24" fill="currentColor"><circle cx="9" cy="5" r="1.5"/><circle cx="15" cy="5" r="1.5"/><circle cx="9" cy="12" r="1.5"/><circle cx="15" cy="12" r="1.5"/><circle cx="9" cy="19" r="1.5"/><circle cx="15" cy="19" r="1.5"/></svg></button>';
              slideshowPreview += '<button class="pb-slide-block__move-up pb-vg-toolbar-btn" title="' + (PB_I18N.move_up || 'Move up') + '"><svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="18 15 12 9 6 15"/></svg></button>';
              slideshowPreview += '<button class="pb-slide-block__move-down pb-vg-toolbar-btn" title="' + (PB_I18N.move_down || 'Move down') + '"><svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="6 9 12 15 18 9"/></svg></button>';
              slideshowPreview += '<button class="pb-slide-block__duplicate pb-vg-toolbar-btn" title="' + (PB_I18N.duplicate || 'Duplicate') + '"><svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1"/></svg></button>';
              slideshowPreview += '<button class="pb-slide-block__delete pb-vg-toolbar-btn" title="' + (PB_I18N.delete || 'Delete') + '"><svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 01-2 2H7a2 2 0 01-2-2V6m3 0V4a2 2 0 012-2h4a2 2 0 012 2v2"/></svg></button>';
              slideshowPreview += '</div>';
              slideshowPreview += '<div class="pb-slide-block__content"' + (sbStyle ? ' style="' + sbStyle + '"' : '') + '>' + sbPreview + '</div>';
              slideshowPreview += '</div>';
              slideshowPreview += '<div class="pb-inserter pb-inserter--sm" data-slide-insert-parent="' + block.id + '" data-slide-insert-at="' + (sbi + 1) + '"><div class="pb-inserter__line"></div><button class="pb-inserter__btn" title="' + (PB_I18N.add_block || 'Add block') + '"><svg viewBox="0 0 24 24" width="10" height="10" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg></button></div>';
            });
            slideshowPreview += '<button class="pb-slide-add pb-dashed-add--dark" data-slide-add="' + block.id + '">+</button>';
          }
          slideshowPreview += '</div>';
        // Info badge + slide selector tabs + add button (on top of everything)
        slideshowPreview += '<div class="pb-sec-toolbar-pos">';
        for (var si = 0; si < slideCount; si++) {
          var isAct = si === activeSlideIdx;
          slideshowPreview += '<button class="pb-slide-tab' + (isAct ? ' is-active' : '') + '" data-slide-tab-parent="' + block.id + '" data-slide-tab-idx="' + si + '">' + (si + 1) + '</button>';
        }
        slideshowPreview += '<button class="pb-slide-tab-add pb-vg-add-btn" data-slide-canvas-add="' + block.id + '" title="' + (PB_I18N.slideshow_add_slide || 'Add slide') + '">+</button>';
        slideshowPreview += '</div>';
        slideshowPreview += '</div>';
        return slideshowPreview;
      case 'ticker':
        var tickerImgs = d.images || [];
        if (tickerImgs.length > 0) {
          var tickerHtml = '<div class="pb-ticker-preview">';
          tickerImgs.forEach(function(img, ti) {
            tickerHtml += '<div class="pb-gal-thumb pb-gal-thumb--square">';
            if (img.url) tickerHtml += '<img src="' + esc(img.url) + '" alt="' + esc(img.alt || '') + '">';
            tickerHtml += '<button data-ticker-canvas-remove="' + ti + '" data-ticker-canvas-block="' + block.id + '" title="' + (PB_I18N.remove || 'Remove') + '" class="pb-gal-thumb__remove">&times;</button>';
            tickerHtml += '</div>';
          });
          tickerHtml += '</div>';
          tickerHtml += '<div class="pb-mt-2"><button data-ticker-canvas-add="' + block.id + '" class="pb-gal-canvas-add">📷 ' + (PB_I18N.gallery_add_from_library || 'Add from Library') + '</button></div>';
          return tickerHtml;
        }
        return '<div data-ticker-canvas-add="' + block.id + '" class="pb-gal-canvas-empty">' +
          '<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="pb-picker-icon"><rect x="1" y="7" width="5" height="10" rx="1"/><rect x="8" y="7" width="5" height="10" rx="1"/><rect x="15" y="7" width="5" height="10" rx="1"/><path d="M3 21h18"/></svg>' +
          '<div class="pb-picker-label">' + (PB_I18N.ticker || 'Logo Ticker') + '</div>' +
          '<span class="pb-picker-btn">📷 ' + (PB_I18N.gallery_add_from_library || 'Add from Library') + '</span>' +
          '</div>';
      default:
        return '<div class="pb-block-unknown">Unknown block: ' + esc(block.type) + '</div>';
    }
  }

  // ============================================================
  // SETTINGS PANELS — right panel content per block type
  // ============================================================

  // Compact preview for blocks nested inside columns
  function getNestedBlockPreview(block) {
    var d = block.data || {};
    switch (block.type) {
      case 'heading':
        var nTag = 'h' + (d.level || 2);
        return '<' + nTag + ' class="pb-inline-heading pb-nested-heading" contenteditable="true" data-inline-key="data.text" data-inline-block="' + block.id + '" data-placeholder="' + esc(PB_I18N.default_heading || 'New Heading') + '">' + esc(d.text || '') + '</' + nTag + '>';
      case 'text':
        return '<div class="pb-rich-zone">' + _textToolbarHTML +
          '<div class="pb-inline-text pb-nested-text' + (_isRichHtmlEmpty(d.html) ? ' pb-show-placeholder' : '') + '" contenteditable="true" data-inline-html="data.html" data-inline-block="' + block.id + '" data-placeholder="' + esc(PB_I18N.default_text || 'Start writing here...') + '">' + (d.html || '<p><br></p>') + '</div></div>';
      case 'image':
        if (d.url) {
          var nAlt = d.alt || '';
          var nAltBadge = nAlt ? '<div class="pb-thumb-label pb-mt-1 pb-nested-alt">alt: ' + esc(nAlt) + '</div>' : '';
          return '<div class="pb-img-canvas-wrap"><figure class="pb-img-canvas pb-img-canvas--' + (d.size || 'full') + '"><img src="' + esc(d.url) + '" class="pb-preview-img--canvas" alt="' + esc(nAlt) + '"></figure>' +
            '<button data-clear-image="' + block.id + '" title="' + (PB_I18N.remove || 'Remove') + '" class="pb-clear-btn">✕</button></div>' + nAltBadge;
        }
        return '<div data-image-pick-block="' + block.id + '" class="pb-picker-zone--small-center">📷 ' + PB_I18N.image + '</div>';
      case 'button':
        return '<span class="pb-inline-btn pb-canvas-button--sm" contenteditable="true" data-inline-key="data.text" data-inline-block="' + block.id + '" data-placeholder="' + esc(PB_I18N.default_button || 'Click Me') + '">' + esc(d.text || '') + '</span>';
      case 'video':
        var nVidId = d.embed_id || '';
        var nVidSource = d.source || '';
        if (nVidId) {
          var nVidThumb = '<img src="' + videoThumbUrl(nVidSource, nVidId) + '" alt="' + esc(d.title || '') + '" class="pb-cover-img">';
          return '<div class="pb-video-wrap">' +
            '<div class="pb-aspect-video">' + nVidThumb + '</div>' +
            '<div class="pb-overlay-center"><div class="pb-play-btn pb-play-btn--lg">▶</div></div>' +
            '<button data-clear-video="' + block.id + '" title="' + (PB_I18N.remove || 'Remove') + '" class="pb-clear-btn pb-clear-btn--z2">✕</button>' +
            (d.title ? '<div class="pb-video-title-bar">' + esc(d.title) + '</div>' : '') +
            '</div>';
        }
        return '<div data-video-pick-block="' + block.id + '" class="pb-gal-canvas-empty">' +
          '<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="pb-picker-icon"><rect x="2" y="4" width="20" height="16" rx="2"/><polygon points="10 8 16 12 10 16 10 8"/></svg>' +
          '<div class="pb-picker-label">' + (PB_I18N.video_gallery_empty || 'Click to add video') + '</div>' +
          '<span class="pb-picker-btn">+ ' + (PB_I18N.video_gallery_add || 'Add video') + '</span>' +
          '</div>';
      case 'divider':
        return '<hr class="pb-nested-divider">';
      case 'spacer':
        return '<div class="pb-nested-spacer" style="height:' + Math.min(d.height || 24, 40) + 'px"></div>';
      case 'quote':
        return '<blockquote class="pb-inline-quote pb-text-hint" contenteditable="true" data-inline-key="data.text" data-inline-block="' + block.id + '" data-placeholder="' + esc(PB_I18N.quote_text || 'Enter quote...') + '">' + esc(d.text || '') + '</blockquote>';
      case 'audio':
        return '<div class="pb-text-hint">♪ ' + esc(d.title || 'Audio') + '</div>';
      case 'html':
        return '<div class="pb-text-hint--mono">' + esc((d.code || '').substring(0, 80)) + '</div>';
      case 'code':
        return '<div class="pb-text-hint--mono">' + esc((d.code || '').substring(0, 80)) + '</div>';
      case 'table':
        return '<div class="pb-text-hint">' + PB_I18N.table + ': ' + (d.headers || []).join(', ') + '</div>';
      case 'accordion':
        return '<div class="pb-text-hint--dim">' + (d.items || []).map(function(it) { return '▸ ' + esc(it.title); }).join('<br>') + '</div>';
      case 'tabs':
        return '<div class="pb-text-hint--dim">' + (d.tabs || []).map(function(t) { return '[' + esc(t.title) + ']'; }).join(' ') + '</div>';
      case 'gallery':
        return '<div class="pb-text-hint">' + PB_I18N.gallery + ' — ' + (d.images || []).length + ' ' + PB_I18N.gallery_images + '</div>';
      case 'video_gallery':
        var nVgVideos = d.videos || [];
        var nVgCount = nVgVideos.length;
        var nVgCols = d.columns || 2;
        if (nVgCount > 0) {
          var nVgDisplayCols = Math.min(nVgCols, nVgCount);
          var nVgHtml = '<div style="display:grid;grid-template-columns:repeat(' + nVgDisplayCols + ',1fr);gap:6px;width:100%">';
          nVgVideos.forEach(function(vid, vi) {
            var nThumbUrl = '';
            if (vid.embed_id && vid.source) {
              nThumbUrl = videoThumbUrl(vid.source, vid.embed_id);
            }
            nVgHtml += '<div class="pb-gal-thumb pb-video-wrap--sm">';
            nVgHtml += '<div class="pb-aspect-video">';
            if (nThumbUrl) {
              nVgHtml += '<img src="' + nThumbUrl + '" alt="' + esc(vid.title || '') + '" class="pb-cover-img">';
            } else {
              nVgHtml += '<div class="pb-video-placeholder">' + (vid.source === 'vimeo' ? 'Vimeo' : '▶') + '</div>';
            }
            nVgHtml += '<div class="pb-overlay-center"><div class="pb-play-btn pb-play-btn--sm">▶</div></div>';
            nVgHtml += '</div>';
            if (vid.title) nVgHtml += '<div class="pb-vg-title--sm">' + esc(vid.title) + '</div>';
            nVgHtml += '<button data-vgal-canvas-remove="' + vi + '" data-vgal-canvas-block="' + block.id + '" title="' + (PB_I18N.remove || 'Remove') + '" class="pb-gal-thumb__remove">&times;</button>';
            if (nVgCount > 1) {
              nVgHtml += '<div class="pb-gal-thumb__arrows">';
              if (vi > 0) nVgHtml += '<button data-vgal-move-block="' + block.id + '" data-vgal-move-from="' + vi + '" data-vgal-move-dir="-1" class="pb-gal-thumb__arrow" title="' + (PB_I18N.move_up || 'Move left') + '">‹</button>';
              if (vi < nVgCount - 1) nVgHtml += '<button data-vgal-move-block="' + block.id + '" data-vgal-move-from="' + vi + '" data-vgal-move-dir="1" class="pb-gal-thumb__arrow" title="' + (PB_I18N.move_down || 'Move right') + '">›</button>';
              nVgHtml += '</div>';
            }
            nVgHtml += '</div>';
          });
          nVgHtml += '</div>';
          nVgHtml += '<div class="pb-mt-2 pb-text-center"><button data-vgal-canvas-add="' + block.id + '" class="pb-gal-canvas-add">+ ' + (PB_I18N.video_gallery_add || 'Add video') + '</button></div>';
          return nVgHtml;
        }
        return '<div data-vgal-canvas-add="' + block.id + '" class="pb-gal-canvas-empty">' +
          '<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="pb-picker-icon"><rect x="2" y="4" width="20" height="16" rx="2"/><polygon points="10 8 16 12 10 16 10 8"/></svg>' +
          '<div class="pb-picker-label">' + (PB_I18N.video_gallery_empty || 'Click to add videos') + '</div>' +
          '<span class="pb-picker-btn">+ ' + (PB_I18N.video_gallery_add || 'Add video') + '</span>' +
          '</div>';
      case 'columns':
        var nCols = d.columns || [];
        var nLayoutStyle = '';
        switch (d.layout || '1/2+1/2') {
          case '1': nLayoutStyle = 'grid-template-columns:1fr'; break;
          case '1/2+1/2': nLayoutStyle = 'grid-template-columns:1fr 1fr'; break;
          case '1/3+1/3+1/3': nLayoutStyle = 'grid-template-columns:1fr 1fr 1fr'; break;
          case '2/3+1/3': nLayoutStyle = 'grid-template-columns:2fr 1fr'; break;
          case '1/3+2/3': nLayoutStyle = 'grid-template-columns:1fr 2fr'; break;
          case '1/4+1/4+1/4+1/4': nLayoutStyle = 'grid-template-columns:1fr 1fr 1fr 1fr'; break;
          default: nLayoutStyle = 'grid-template-columns:1fr 1fr';
        }
        var nColsHtml = '<div style="display:grid;' + nLayoutStyle + ';gap:8px">';
        nCols.forEach(function(c, ci) {
          nColsHtml += '<div class="pb-col-zone" data-col-parent="' + block.id + '" data-col-index="' + ci + '">';
          var nColBlocks = c.blocks || [];
          if (nColBlocks.length === 0) {
            nColsHtml += '<div class="pb-col-zone__empty" data-col-add="' + block.id + '" data-col-idx="' + ci + '">+ ' + (PB_I18N.add_block || 'Add block') + '</div>';
          } else {
            nColBlocks.forEach(function(cb, cbi) {
              if (cbi === 0) {
                nColsHtml += '<div class="pb-inserter pb-inserter--sm" data-col-insert-parent="' + block.id + '" data-col-insert-col="' + ci + '" data-col-insert-at="0"><div class="pb-inserter__line"></div><button class="pb-inserter__btn" title="' + (PB_I18N.add_block || 'Add block') + '"><svg viewBox="0 0 24 24" width="10" height="10" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg></button></div>';
              }
              var ncbPreview = getNestedBlockPreview(cb);
              nColsHtml += '<div class="pb-col-block" data-col-block-parent="' + block.id + '" data-col-block-col="' + ci + '" data-col-block-idx="' + cbi + '" data-col-block-id="' + cb.id + '">';
              nColsHtml += '<div class="pb-col-block__toolbar">';
              nColsHtml += '<button class="pb-col-block__drag-handle" title="' + (PB_I18N.drag || 'Drag') + '"><svg viewBox="0 0 24 24" fill="currentColor"><circle cx="9" cy="5" r="1.5"/><circle cx="15" cy="5" r="1.5"/><circle cx="9" cy="12" r="1.5"/><circle cx="15" cy="12" r="1.5"/><circle cx="9" cy="19" r="1.5"/><circle cx="15" cy="19" r="1.5"/></svg></button>';
              nColsHtml += '<button class="pb-col-block__move-up" title="' + (PB_I18N.move_up || 'Move up') + '"><svg viewBox="0 0 24 24"><polyline points="18 15 12 9 6 15"/></svg></button>';
              nColsHtml += '<button class="pb-col-block__move-down" title="' + (PB_I18N.move_down || 'Move down') + '"><svg viewBox="0 0 24 24"><polyline points="6 9 12 15 18 9"/></svg></button>';
              nColsHtml += '<button class="pb-col-block__duplicate" title="' + (PB_I18N.duplicate || 'Duplicate') + '"><svg viewBox="0 0 24 24"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1"/></svg></button>';
              nColsHtml += '<button class="pb-col-block__delete" title="' + (PB_I18N.delete || 'Delete') + '"><svg viewBox="0 0 24 24"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 01-2 2H7a2 2 0 01-2-2V6m3 0V4a2 2 0 012-2h4a2 2 0 012 2v2"/></svg></button>';
              nColsHtml += '</div>';
              nColsHtml += '<div class="pb-col-block__content">' + ncbPreview + '</div>';
              nColsHtml += '</div>';
              nColsHtml += '<div class="pb-inserter pb-inserter--sm" data-col-insert-parent="' + block.id + '" data-col-insert-col="' + ci + '" data-col-insert-at="' + (cbi + 1) + '"><div class="pb-inserter__line"></div><button class="pb-inserter__btn" title="' + (PB_I18N.add_block || 'Add block') + '"><svg viewBox="0 0 24 24" width="10" height="10" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg></button></div>';
            });
            nColsHtml += '<button class="pb-col-add" data-col-add="' + block.id + '" data-col-idx="' + ci + '" title="' + (PB_I18N.add_block || 'Add block') + '">+</button>';
          }
          nColsHtml += '</div>';
        });
        nColsHtml += '</div>';
        return nColsHtml;
      case 'slideshow':
        return '<div class="pb-text-hint">🎠 ' + PB_I18N.slideshow + ' — ' + (d.slides || []).length + ' ' + (PB_I18N.slideshow_slides || 'slides').toLowerCase() + '</div>';
      case 'form':
        if (d.form_id && d._fields) {
          return '<div class="pb-relative">' +
            renderFormPreview(block) +
            '<button data-clear-form="' + block.id + '" title="' + (PB_I18N.remove || 'Remove') + '" class="pb-clear-btn">✕</button>' +
            '</div>';
        }
        if (d.form_id && !d._fields) {
          if (!_formPreviewFetching[d.form_id]) {
            _formPreviewFetching[d.form_id] = true;
            fetch('/api/forms/' + d.form_id)
              .then(function(r) { return r.json(); })
              .then(function(data) {
                var formData = data.fields ? data : (data.form || {});
                if (formData.fields) {
                  block.data._fields = formData.fields;
                  block.data._settings = formData.settings || {};
                  block.data._description = formData.description || '';
                  block.data._form_title_raw = formData.title || '';
                  block.data._form_title = (formData.title || '') + ' (' + (formData.form_type || '') + ', ' + (formData.fields.length || 0) + ' ' + (PB_I18N.form_fields || 'fields') + ')';
                  renderCanvas();
                  if (state.selectedId === block.id) {
                    openPanel(block);
                  }
                }
              })
              .catch(function() {})
              .finally(function() { delete _formPreviewFetching[d.form_id]; });
          }
          var nLoadingName = d._form_title || d._form_title_raw || ((PB_I18N.form || 'Form') + ' #' + d.form_id);
          return '<div class="pb-relative">' +
            '<div class="pb-form-preview pb-form-preview--loading">' +
            '<div class="ap-form"><div class="pb-form-loading">' + esc(nLoadingName) + '</div></div></div>' +
            '<button data-clear-form="' + block.id + '" title="' + (PB_I18N.remove || 'Remove') + '" class="pb-clear-btn">✕</button>' +
            '</div>';
        }
        return '<div data-form-pick-block="' + block.id + '" class="pb-picker-zone--small-center">📋 ' + PB_I18N.form + '</div>';
      case 'ticker':
        return '<div class="pb-text-hint">' + PB_I18N.ticker + ' — ' + (d.images || []).length + ' ' + PB_I18N.ticker_images + '</div>';
      default:
        return '<div class="pb-text-hint">' + esc(block.type) + '</div>';
    }
  }

  // ============================================================
  // FORM PREVIEW — renders real form fields in canvas (read-only)
  // ============================================================
  function renderFormPreview(block) {
    var d = block.data || {};
    var fields = d._fields || [];
    var settings = d._settings || {};
    var showTitle = d.show_title !== false;
    var showDesc = d.show_description !== false;

    var html = '<div class="pb-form-preview"><div class="ap-form">';

    if (showTitle && d._form_title_raw) {
      html += '<h3 class="ap-form__title">' + esc(d._form_title_raw) + '</h3>';
    }
    if (showDesc && d._description) {
      html += '<p class="ap-form__desc">' + esc(d._description) + '</p>';
    }

    html += '<div class="ap-form__body">';

    fields.forEach(function(field) {
      var ftype = field.type || 'text';
      var flabel = esc(field.label || '');
      var fplaceholder = esc(field.placeholder || '');
      var fwidth = field.width || 'full';
      var reqMark = field.required ? ' <span class="ap-form__req">*</span>' : '';

      if (ftype === 'hidden') return;

      html += '<div class="ap-form__field ap-form__field--' + esc(fwidth) + '">';

      if (ftype === 'textarea') {
        html += '<label class="ap-form__label">' + flabel + reqMark + '</label>';
        html += '<textarea class="ap-form__textarea" disabled placeholder="' + fplaceholder + '" rows="3"></textarea>';
      } else if (ftype === 'select') {
        html += '<label class="ap-form__label">' + flabel + reqMark + '</label>';
        html += '<select class="ap-form__select" disabled><option>' + (fplaceholder || '—') + '</option></select>';
      } else if (ftype === 'radio') {
        html += '<label class="ap-form__label">' + flabel + reqMark + '</label>';
        html += '<div class="ap-form__options">';
        (field.options || []).forEach(function(opt) {
          html += '<label class="ap-form__radio"><input type="radio" disabled> ' + esc(opt) + '</label>';
        });
        html += '</div>';
      } else if (ftype === 'checkbox') {
        var opts = field.options || [];
        if (opts.length) {
          html += '<label class="ap-form__label">' + flabel + reqMark + '</label>';
          html += '<div class="ap-form__options">';
          opts.forEach(function(opt) {
            html += '<label class="ap-form__checkbox"><input type="checkbox" disabled> ' + esc(opt) + '</label>';
          });
          html += '</div>';
        } else {
          html += '<label class="ap-form__checkbox"><input type="checkbox" disabled> ' + flabel + '</label>';
        }
      } else if (ftype === 'rating') {
        var maxR = field.max_rating || 5;
        html += '<label class="ap-form__label">' + flabel + reqMark + '</label>';
        html += '<div class="ap-form__rating">';
        for (var i = 0; i < maxR; i++) {
          html += '<span class="ap-form__star">★</span>';
        }
        html += '</div>';
      } else if (ftype === 'consent') {
        html += '<label class="ap-form__consent"><input type="checkbox" disabled> <span>' + esc(field.consent_text || '') + '</span></label>';
      } else {
        html += '<label class="ap-form__label">' + flabel + reqMark + '</label>';
        html += '<input class="ap-form__input" type="text" disabled placeholder="' + fplaceholder + '">';
      }

      if (field.help_text && ftype !== 'hidden') {
        html += '<div class="ap-form__help">' + esc(field.help_text) + '</div>';
      }

      html += '</div>';
    });

    var submitLabel = esc(settings.submit_label || PB_I18N.form_submit_default || 'Submit');
    html += '<div class="ap-form__field ap-form__field--full">';
    html += '<button type="button" class="ap-btn ap-btn--primary ap-btn--md ap-form__submit" disabled>' + submitLabel + '</button>';
    html += '</div>';

    html += '</div></div></div>';
    return html;
  }

  // ============================================================
  // COLUMN BLOCK OPERATIONS
  // ============================================================
  function addBlockToColumn(parentId, colIdx, blockType, index) {
    var parent = findBlockDeep(parentId);
    if (!parent || !parent.data.columns[colIdx]) return;
    var data = JSON.parse(JSON.stringify(BLOCK_DEFAULTS[blockType] || {}));
    var newBlock = { id: uuid(), type: blockType, data: data, settings: { alignment: defaultAlignFor(blockType) } };
    if (index !== undefined && index >= 0) {
      parent.data.columns[colIdx].blocks.splice(index, 0, newBlock);
    } else {
      parent.data.columns[colIdx].blocks.push(newBlock);
    }
    markDirty();
    renderCanvas();
  }

  function deleteColumnBlock(parentId, colIdx, blockIdx) {
    var parent = findBlockDeep(parentId);
    if (!parent || !parent.data.columns[colIdx]) return;
    var block = parent.data.columns[colIdx].blocks[blockIdx];
    var label = block ? (BLOCK_LABELS[block.type] || block.type) : '';
    pbConfirm((PB_I18N.delete_block_confirm || 'Delete this {type} block?').replace('{type}', label),
              { okLabel: PB_I18N.delete, danger: true }).then(function(ok) {
      if (!ok) return;
      parent.data.columns[colIdx].blocks.splice(blockIdx, 1);
      markDirty();
      renderCanvas();
    });
  }

  function moveColumnBlock(parentId, colIdx, blockIdx, dir) {
    var parent = findBlockDeep(parentId);
    if (!parent || !parent.data.columns[colIdx]) return;
    var blocks = parent.data.columns[colIdx].blocks;
    var newIdx = blockIdx + dir;
    if (newIdx < 0 || newIdx >= blocks.length) return;
    var b = blocks.splice(blockIdx, 1)[0];
    blocks.splice(newIdx, 0, b);
    markDirty();
    renderCanvas();
  }

  // Section nested block management
  function addBlockToSection(parentId, blockType, index) {
    var parent = findBlock(parentId);
    if (!parent) return;
    if (!parent.data.blocks) parent.data.blocks = [];
    var data = JSON.parse(JSON.stringify(BLOCK_DEFAULTS[blockType] || {}));
    var newBlock = { id: uuid(), type: blockType, data: data, settings: { alignment: defaultAlignFor(blockType) } };
    if (index !== undefined && index >= 0) {
      parent.data.blocks.splice(index, 0, newBlock);
    } else {
      parent.data.blocks.push(newBlock);
    }
    markDirty();
    renderCanvas();
  }

  function deleteSectionBlock(parentId, blockIdx) {
    var parent = findBlock(parentId);
    if (!parent || !parent.data.blocks) return;
    var block = parent.data.blocks[blockIdx];
    var label = block ? (BLOCK_LABELS[block.type] || block.type) : '';
    pbConfirm((PB_I18N.delete_block_confirm || 'Delete this {type} block?').replace('{type}', label),
              { okLabel: PB_I18N.delete, danger: true }).then(function(ok) {
      if (!ok) return;
      parent.data.blocks.splice(blockIdx, 1);
      markDirty();
      renderCanvas();
    });
  }

  function moveSectionBlock(parentId, blockIdx, dir) {
    var parent = findBlock(parentId);
    if (!parent || !parent.data.blocks) return;
    var blocks = parent.data.blocks;
    var newIdx = blockIdx + dir;
    if (newIdx < 0 || newIdx >= blocks.length) return;
    var b = blocks.splice(blockIdx, 1)[0];
    blocks.splice(newIdx, 0, b);
    markDirty();
    renderCanvas();
  }

  function duplicateSectionBlock(parentId, blockIdx) {
    var parent = findBlock(parentId);
    if (!parent || !parent.data.blocks || !parent.data.blocks[blockIdx]) return;
    var copy = JSON.parse(JSON.stringify(parent.data.blocks[blockIdx]));
    _regenerateIds(copy);
    parent.data.blocks.splice(blockIdx + 1, 0, copy);
    markDirty();
    renderCanvas();
  }

  function duplicateColumnBlock(parentId, colIdx, blockIdx) {
    var parent = findBlockDeep(parentId);
    if (!parent || !parent.data.columns[colIdx] || !parent.data.columns[colIdx].blocks[blockIdx]) return;
    var copy = JSON.parse(JSON.stringify(parent.data.columns[colIdx].blocks[blockIdx]));
    _regenerateIds(copy);
    parent.data.columns[colIdx].blocks.splice(blockIdx + 1, 0, copy);
    markDirty();
    renderCanvas();
  }

  // Slideshow nested block management (hero mode — blocks inside slides)
  function addBlockToSlide(parentId, blockType, index) {
    var parent = findBlock(parentId);
    if (!parent || !parent.data.slides || !parent.data.slides[_activeSlideIdx(parentId)]) return;
    var slide = parent.data.slides[_activeSlideIdx(parentId)];
    if (!slide.blocks) slide.blocks = [];
    var data = JSON.parse(JSON.stringify(BLOCK_DEFAULTS[blockType] || {}));
    var newBlock = { id: uuid(), type: blockType, data: data, settings: { alignment: defaultAlignFor(blockType) } };
    if (index !== undefined && index >= 0) {
      slide.blocks.splice(index, 0, newBlock);
    } else {
      slide.blocks.push(newBlock);
    }
    markDirty();
    renderCanvas();
  }

  function deleteSlideBlock(parentId, blockIdx) {
    var parent = findBlock(parentId);
    if (!parent || !parent.data.slides || !parent.data.slides[_activeSlideIdx(parentId)]) return;
    var blocks = parent.data.slides[_activeSlideIdx(parentId)].blocks;
    if (!blocks) return;
    var block = blocks[blockIdx];
    var label = block ? (BLOCK_LABELS[block.type] || block.type) : '';
    pbConfirm((PB_I18N.delete_block_confirm || 'Delete this {type} block?').replace('{type}', label),
              { okLabel: PB_I18N.delete, danger: true }).then(function(ok) {
      if (!ok) return;
      blocks.splice(blockIdx, 1);
      markDirty();
      renderCanvas();
    });
  }

  function moveSlideBlock(parentId, blockIdx, dir) {
    var parent = findBlock(parentId);
    if (!parent || !parent.data.slides || !parent.data.slides[_activeSlideIdx(parentId)]) return;
    var blocks = parent.data.slides[_activeSlideIdx(parentId)].blocks;
    if (!blocks) return;
    var newIdx = blockIdx + dir;
    if (newIdx < 0 || newIdx >= blocks.length) return;
    var b = blocks.splice(blockIdx, 1)[0];
    blocks.splice(newIdx, 0, b);
    markDirty();
    renderCanvas();
  }

  function duplicateSlideBlock(parentId, blockIdx) {
    var parent = findBlock(parentId);
    if (!parent || !parent.data.slides || !parent.data.slides[_activeSlideIdx(parentId)]) return;
    var blocks = parent.data.slides[_activeSlideIdx(parentId)].blocks;
    if (!blocks || !blocks[blockIdx]) return;
    var copy = JSON.parse(JSON.stringify(blocks[blockIdx]));
    _regenerateIds(copy);
    blocks.splice(blockIdx + 1, 0, copy);
    markDirty();
    renderCanvas();
  }

  // Track which slide is being edited per slideshow block
  var _slideEditIdx = {};
  function _activeSlideIdx(parentId) {
    return _slideEditIdx[parentId] || 0;
  }

  // Track which tab is active per tabs block
  var _tabEditIdx = {};
  function _activeTabIdx(blockId) {
    return _tabEditIdx[blockId] || 0;
  }

  var _formPreviewFetching = {};

  // Smart picker positioning — opens above if no room below
  function _positionPicker(picker, anchorEl) {
    picker.style.position = 'fixed';
    picker.style.zIndex = '150';
    picker.style.visibility = 'hidden';
    document.body.appendChild(picker);
    var rect = anchorEl.getBoundingClientRect();
    var ph = picker.offsetHeight;
    var spaceBelow = window.innerHeight - rect.bottom - 8;
    if (spaceBelow >= ph) {
      picker.style.top = (rect.bottom + 4) + 'px';
    } else {
      picker.style.top = Math.max(4, rect.top - ph - 4) + 'px';
    }
    picker.style.left = rect.left + 'px';
    picker.style.visibility = '';
  }

  // Show block type picker near a column
  var activePicker = null;
  function showColumnPicker(parentId, colIdx, anchorEl) {
    closeColumnPicker();
    var blockTypes = [
      { type: 'heading', icon: '<path d="M4 4v16"/><path d="M4 12h12"/><path d="M16 4v16"/>', label: PB_I18N.heading },
      { type: 'text', icon: '<path d="M4 7V4h16v3"/><path d="M9 20h6"/><path d="M12 4v16"/>', label: PB_I18N.text },
      { type: 'image', icon: '<rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="8.5" cy="8.5" r="1.5"/><polyline points="21 15 16 10 5 21"/>', label: PB_I18N.image },
      { type: 'button', icon: '<rect x="2" y="7" width="20" height="10" rx="2"/><path d="M9 12h6"/>', label: PB_I18N.button },
      { type: 'video', icon: '<polygon points="5 3 19 12 5 21 5 3"/>', label: PB_I18N.video },
      { type: 'video_gallery', icon: '<rect x="2" y="4" width="8" height="6" rx="1"/><polygon points="5 6 8 7.5 5 9" fill="currentColor" stroke="none"/><rect x="14" y="4" width="8" height="6" rx="1"/><polygon points="17 6 20 7.5 17 9" fill="currentColor" stroke="none"/><rect x="2" y="14" width="8" height="6" rx="1"/><polygon points="5 16 8 17.5 5 19" fill="currentColor" stroke="none"/><rect x="14" y="14" width="8" height="6" rx="1"/><polygon points="17 16 20 17.5 17 19" fill="currentColor" stroke="none"/>', label: PB_I18N.video_gallery },
      { type: 'quote', icon: '<path d="M3 21c3 0 7-1 7-8V5c0-1.25-.756-2.017-2-2H4c-1.25 0-2 .75-2 1.972V11c0 1.25.75 2 2 2 1 0 1 0 1 1v1c0 1-1 2-2 2s-1 .008-1 1.031V21z"/>', label: PB_I18N.quote },
      { type: 'divider', icon: '<path d="M3 12h18"/>', label: PB_I18N.divider },
      { type: 'spacer', icon: '<path d="M12 5v14"/><path d="M5 5h14"/><path d="M5 19h14"/>', label: PB_I18N.spacer },
      { type: 'html', icon: '<polyline points="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 18"/>', label: PB_I18N.html },
      { type: 'audio', icon: '<path d="M9 18V5l12-2v13"/><circle cx="6" cy="18" r="3"/><circle cx="18" cy="16" r="3"/>', label: PB_I18N.audio },
      { type: 'code', icon: '<rect x="4" y="4" width="16" height="16" rx="2"/><path d="M9 10l-2 2 2 2"/><path d="M15 10l2 2-2 2"/>', label: PB_I18N.code },
      { type: 'form', icon: '<rect x="3" y="3" width="18" height="18" rx="2"/><path d="M7 7h10"/><path d="M7 12h10"/><path d="M7 17h4"/>', label: PB_I18N.form }
    ];
    var picker = document.createElement('div');
    picker.className = 'pb-col-picker';
    picker.innerHTML = blockTypes.map(function(bt) {
      return '<button class="pb-col-picker__btn" data-pick-type="' + bt.type + '">' +
        '<svg viewBox="0 0 24 24">' + bt.icon + '</svg>' + bt.label + '</button>';
    }).join('');
    
    // Position near the anchor
    _positionPicker(picker, anchorEl);
    activePicker = picker;
    
    picker.querySelectorAll('[data-pick-type]').forEach(function(btn) {
      btn.addEventListener('click', function(e) {
        e.stopPropagation();
        addBlockToColumn(parentId, colIdx, btn.dataset.pickType);
        closeColumnPicker();
      });
    });
    
    // Close on click outside
    setTimeout(function() {
      document.addEventListener('click', closeColumnPickerOutside);
    }, 10);
  }

  function closeColumnPicker() {
    if (activePicker) {
      activePicker.remove();
      activePicker = null;
    }
    document.removeEventListener('click', closeColumnPickerOutside);
  }

  function closeColumnPickerOutside(e) {
    if (activePicker && !activePicker.contains(e.target)) {
      closeColumnPicker();
    }
  }

  // ============================================================
  // CANVAS INSERTER — "+" button between blocks to add new blocks
  // ============================================================
  var _canvasPickerTypes = [
    { type: 'heading', icon: '<path d="M4 4v16"/><path d="M4 12h12"/><path d="M16 4v16"/>', label: PB_I18N.heading },
    { type: 'text', icon: '<path d="M4 7V4h16v3"/><path d="M9 20h6"/><path d="M12 4v16"/>', label: PB_I18N.text },
    { type: 'image', icon: '<rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="8.5" cy="8.5" r="1.5"/><polyline points="21 15 16 10 5 21"/>', label: PB_I18N.image },
    { type: 'button', icon: '<rect x="2" y="7" width="20" height="10" rx="2"/><path d="M9 12h6"/>', label: PB_I18N.button },
    { type: 'divider', icon: '<path d="M3 12h18"/>', label: PB_I18N.divider },
    { type: 'spacer', icon: '<path d="M12 5v14"/><path d="M5 5h14"/><path d="M5 19h14"/>', label: PB_I18N.spacer },
    { type: 'columns', icon: '<rect x="3" y="3" width="7" height="18"/><rect x="14" y="3" width="7" height="18"/>', label: PB_I18N.columns },
    { type: 'section', icon: '<rect x="2" y="4" width="20" height="16" rx="2"/><path d="M2 8h20"/>', label: PB_I18N.section },
    { type: 'video', icon: '<polygon points="5 3 19 12 5 21 5 3"/>', label: PB_I18N.video },
    { type: 'gallery', icon: '<rect x="2" y="2" width="8" height="8" rx="1"/><rect x="14" y="2" width="8" height="8" rx="1"/><rect x="2" y="14" width="8" height="8" rx="1"/><rect x="14" y="14" width="8" height="8" rx="1"/>', label: PB_I18N.gallery },
    { type: 'video_gallery', icon: '<rect x="1" y="3" width="9" height="7" rx="1"/><polygon points="5 5.5 8 7 5 8.5" fill="currentColor" stroke="none"/><rect x="14" y="3" width="9" height="7" rx="1"/><polygon points="18 5.5 21 7 18 8.5" fill="currentColor" stroke="none"/><rect x="1" y="14" width="9" height="7" rx="1"/><polygon points="5 16.5 8 18 5 19.5" fill="currentColor" stroke="none"/><rect x="14" y="14" width="9" height="7" rx="1"/><polygon points="18 16.5 21 18 18 19.5" fill="currentColor" stroke="none"/>', label: PB_I18N.video_gallery },
    { type: 'audio', icon: '<path d="M9 18V5l12-2v13"/><circle cx="6" cy="18" r="3"/><circle cx="18" cy="16" r="3"/>', label: PB_I18N.audio },
    { type: 'slideshow', icon: '<rect x="2" y="4" width="20" height="16" rx="2"/><polyline points="7 15 9 12 7 9"/><polyline points="17 9 15 12 17 15"/>', label: PB_I18N.slideshow },
    { type: 'ticker', icon: '<rect x="1" y="7" width="5" height="10" rx="1"/><rect x="8" y="7" width="5" height="10" rx="1"/><rect x="15" y="7" width="5" height="10" rx="1"/><path d="M3 21h18"/>', label: PB_I18N.ticker },
    { type: 'quote', icon: '<path d="M3 21c3 0 7-1 7-8V5c0-1.25-.756-2.017-2-2H4c-1.25 0-2 .75-2 1.972V11c0 1.25.75 2 2 2 1 0 1 0 1 1v1c0 1-1 2-2 2s-1 .008-1 1.031V21z"/>', label: PB_I18N.quote },
    { type: 'table', icon: '<rect x="3" y="3" width="18" height="18" rx="2"/><path d="M3 9h18"/><path d="M3 15h18"/><path d="M9 3v18"/>', label: PB_I18N.table },
    { type: 'accordion', icon: '<path d="M3 6h18"/><path d="M3 12h18"/><path d="M3 18h18"/><polyline points="15 8 18 6 15 4"/>', label: PB_I18N.accordion },
    { type: 'tabs', icon: '<rect x="3" y="7" width="18" height="14" rx="2"/><path d="M3 7V5a2 2 0 012-2h4l2 4"/><path d="M11 3h8a2 2 0 012 2v2"/>', label: PB_I18N.tabs },
    { type: 'html', icon: '<polyline points="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 18"/>', label: PB_I18N.html },
    { type: 'code', icon: '<rect x="4" y="4" width="16" height="16" rx="2"/><path d="M9 10l-2 2 2 2"/><path d="M15 10l2 2-2 2"/>', label: PB_I18N.code },
    { type: 'form', icon: '<rect x="3" y="3" width="18" height="18" rx="2"/><path d="M7 7h10"/><path d="M7 12h10"/><path d="M7 17h4"/>', label: PB_I18N.form }
  ];

  function showCanvasInserter(index, anchorEl) {
    closeColumnPicker();
    var picker = document.createElement('div');
    picker.className = 'pb-col-picker';
    picker.innerHTML = _canvasPickerTypes.map(function(bt) {
      return '<button class="pb-col-picker__btn" data-pick-type="' + bt.type + '">' +
        '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">' + bt.icon + '</svg>' +
        bt.label + '</button>';
    }).join('');
    _positionPicker(picker, anchorEl);
    activePicker = picker;
    picker.querySelectorAll('[data-pick-type]').forEach(function(btn) {
      btn.addEventListener('click', function(e) {
        e.stopPropagation();
        addBlock(btn.dataset.pickType, index);
        closeColumnPicker();
      });
    });
    setTimeout(function() {
      document.addEventListener('click', closeColumnPickerOutside);
    }, 10);
  }

  function bindInserters() {
    $blocks.querySelectorAll('.pb-inserter[data-insert-at]').forEach(function(inserter) {
      var btn = inserter.querySelector('.pb-inserter__btn');
      if (!btn) return;
      btn.addEventListener('click', function(e) {
        e.stopPropagation();
        var index = parseInt(inserter.dataset.insertAt);
        showCanvasInserter(index, btn);
      });
    });
  }

  // ============================================================
  // BIND COLUMN EVENTS — click handlers for nested block actions
  // ============================================================
  function bindColumnEvents() {
    // Add block buttons (empty zones and + buttons)
    $blocks.querySelectorAll('[data-col-add]').forEach(function(el) {
      el.addEventListener('click', function(e) {
        e.stopPropagation();
        var parentId = el.dataset.colAdd;
        var colIdx = parseInt(el.dataset.colIdx);
        showColumnPicker(parentId, colIdx, el);
      });
    });

    // Delete nested block
    $blocks.querySelectorAll('.pb-col-block__delete').forEach(function(btn) {
      btn.addEventListener('click', function(e) {
        e.stopPropagation();
        var block = btn.closest('.pb-col-block');
        deleteColumnBlock(block.dataset.colBlockParent, parseInt(block.dataset.colBlockCol), parseInt(block.dataset.colBlockIdx));
      });
    });

    // Move up nested block
    $blocks.querySelectorAll('.pb-col-block__move-up').forEach(function(btn) {
      btn.addEventListener('click', function(e) {
        e.stopPropagation();
        var block = btn.closest('.pb-col-block');
        moveColumnBlock(block.dataset.colBlockParent, parseInt(block.dataset.colBlockCol), parseInt(block.dataset.colBlockIdx), -1);
      });
    });

    // Move down nested block
    $blocks.querySelectorAll('.pb-col-block__move-down').forEach(function(btn) {
      btn.addEventListener('click', function(e) {
        e.stopPropagation();
        var block = btn.closest('.pb-col-block');
        moveColumnBlock(block.dataset.colBlockParent, parseInt(block.dataset.colBlockCol), parseInt(block.dataset.colBlockIdx), 1);
      });
    });

    // Duplicate nested block in column
    $blocks.querySelectorAll('.pb-col-block__duplicate').forEach(function(btn) {
      btn.addEventListener('click', function(e) {
        e.stopPropagation();
        var block = btn.closest('.pb-col-block');
        duplicateColumnBlock(block.dataset.colBlockParent, parseInt(block.dataset.colBlockCol), parseInt(block.dataset.colBlockIdx));
      });
    });

    // Drag reorder column nested blocks
    var _dragColInfo = null;
    $blocks.querySelectorAll('.pb-col-block').forEach(function(el) {
      var handle = el.querySelector('.pb-col-block__drag-handle');
      if (!handle) return;
      handle.style.cursor = 'grab';
      handle.addEventListener('mousedown', function() { el.setAttribute('draggable', 'true'); handle.style.cursor = 'grabbing'; });
      handle.addEventListener('mouseup', function() { el.removeAttribute('draggable'); handle.style.cursor = 'grab'; });
      el.addEventListener('dragstart', function(e) {
        if (!el.getAttribute('draggable')) { e.preventDefault(); return; }
        e.stopPropagation();
        _dragColInfo = { parentId: el.dataset.colBlockParent, colIdx: parseInt(el.dataset.colBlockCol), blockId: el.dataset.colBlockId };
        e.dataTransfer.setData('application/x-pb-source', 'nested-column');
        e.dataTransfer.effectAllowed = 'move';
        el.style.opacity = '0.4';
      });
      el.addEventListener('dragend', function() {
        el.style.opacity = ''; el.removeAttribute('draggable'); handle.style.cursor = 'grab'; _dragColInfo = null;
        $blocks.querySelectorAll('.pb-col-block').forEach(function(s) { s.style.borderTop = ''; s.style.borderBottom = ''; });
      });
      el.addEventListener('dragover', function(e) {
        if (!_dragColInfo || _dragColInfo.parentId !== el.dataset.colBlockParent || _dragColInfo.colIdx !== parseInt(el.dataset.colBlockCol) || _dragColInfo.blockId === el.dataset.colBlockId) return;
        e.preventDefault(); e.stopPropagation(); e.dataTransfer.dropEffect = 'move';
        var rect = el.getBoundingClientRect(); var midY = rect.top + rect.height / 2;
        el.style.borderTop = e.clientY < midY ? '2px solid var(--pb-primary)' : '';
        el.style.borderBottom = e.clientY >= midY ? '2px solid var(--pb-primary)' : '';
      });
      el.addEventListener('dragleave', function() { el.style.borderTop = ''; el.style.borderBottom = ''; });
      el.addEventListener('drop', function(e) {
        e.preventDefault(); e.stopPropagation(); el.style.borderTop = ''; el.style.borderBottom = '';
        if (!_dragColInfo || _dragColInfo.parentId !== el.dataset.colBlockParent || _dragColInfo.colIdx !== parseInt(el.dataset.colBlockCol) || _dragColInfo.blockId === el.dataset.colBlockId) return;
        var parent = findBlockDeep(_dragColInfo.parentId);
        if (!parent || !parent.data.columns[_dragColInfo.colIdx]) return;
        var blocks = parent.data.columns[_dragColInfo.colIdx].blocks;
        var fromIdx = -1, toIdx = -1;
        blocks.forEach(function(b, i) { if (b.id === _dragColInfo.blockId) fromIdx = i; if (b.id === el.dataset.colBlockId) toIdx = i; });
        if (fromIdx < 0 || toIdx < 0) return;
        var rect = el.getBoundingClientRect(); var insertAfter = e.clientY >= rect.top + rect.height / 2;
        var moved = blocks.splice(fromIdx, 1)[0];
        var newIdx = -1; blocks.forEach(function(b, i) { if (b.id === el.dataset.colBlockId) newIdx = i; });
        if (insertAfter) newIdx++;
        blocks.splice(newIdx, 0, moved);
        _dragColInfo = null; markDirty(); renderCanvas();
      });
    });

    // Drag & drop into columns
    $blocks.querySelectorAll('.pb-col-zone').forEach(function(zone) {
      zone.addEventListener('dragover', function(e) {
        e.preventDefault();
        e.stopPropagation();
        e.dataTransfer.dropEffect = 'copy';
        zone.classList.add('drag-over');
      });
      zone.addEventListener('dragleave', function() {
        zone.classList.remove('drag-over');
      });
      zone.addEventListener('drop', function(e) {
        e.preventDefault();
        e.stopPropagation();
        zone.classList.remove('drag-over');
        var type = e.dataTransfer.getData('text/plain');
        if (type && BLOCK_DEFAULTS[type] && type !== 'columns' && type !== 'section') {
          var parentId = zone.dataset.colParent;
          var colIdx = parseInt(zone.dataset.colIndex);
          addBlockToColumn(parentId, colIdx, type);
        }
      });
    });

    // Click on nested block to open its panel
    $blocks.querySelectorAll('.pb-col-block').forEach(function(el) {
      el.addEventListener('click', function(e) {
        if (e.target.closest('.pb-col-block__toolbar')) return;
        e.stopPropagation();
        var parentId = el.dataset.colBlockParent;
        var colIdx = parseInt(el.dataset.colBlockCol);
        var blockIdx = parseInt(el.dataset.colBlockIdx);
        var parent = findBlockDeep(parentId);
        if (parent && parent.data.columns[colIdx] && parent.data.columns[colIdx].blocks[blockIdx]) {
          var nestedBlock = parent.data.columns[colIdx].blocks[blockIdx];
          openPanel(nestedBlock);
        }
      });
    });

    // ── Section nested blocks ──

    // Add block buttons in sections
    $blocks.querySelectorAll('[data-sec-add]').forEach(function(el) {
      el.addEventListener('click', function(e) {
        e.stopPropagation();
        var parentId = el.dataset.secAdd;
        // Reuse column picker but with section callback
        closeColumnPicker();
        var picker = document.createElement('div');
        picker.className = 'pb-col-picker';
        var blockTypes = [
          { type: 'heading', icon: '<path d="M4 4v16"/><path d="M4 12h12"/><path d="M16 4v16"/>', label: PB_I18N.heading },
          { type: 'text', icon: '<path d="M4 7V4h16v3"/><path d="M9 20h6"/><path d="M12 4v16"/>', label: PB_I18N.text },
          { type: 'image', icon: '<rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="8.5" cy="8.5" r="1.5"/><polyline points="21 15 16 10 5 21"/>', label: PB_I18N.image },
          { type: 'button', icon: '<rect x="2" y="7" width="20" height="10" rx="2"/><path d="M9 12h6"/>', label: PB_I18N.button },
          { type: 'divider', icon: '<path d="M3 12h18"/>', label: PB_I18N.divider },
          { type: 'spacer', icon: '<path d="M12 5v14"/><path d="M5 12h14"/>', label: PB_I18N.spacer },
          { type: 'columns', icon: '<rect x="3" y="3" width="7" height="18"/><rect x="14" y="3" width="7" height="18"/>', label: PB_I18N.columns },
          { type: 'quote', icon: '<path d="M3 21c3 0 7-1 7-8V5c0-1.25-.756-2.017-2-2H4c-1.25 0-2 .75-2 1.972V11c0 1.25.75 2 2 2 1 0 1 0 1 1v1c0 1-1 2-2 2s-1 .008-1 1.031V21z"/>', label: PB_I18N.quote },
          { type: 'form', icon: '<rect x="3" y="3" width="18" height="18" rx="2"/><path d="M7 7h10"/><path d="M7 12h10"/><path d="M7 17h6"/>', label: PB_I18N.form }
        ];
        picker.innerHTML = blockTypes.map(function(bt) {
          return '<button class="pb-col-picker__btn" data-pick-type="' + bt.type + '">' +
            '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">' + bt.icon + '</svg>' +
            bt.label + '</button>';
        }).join('');
        _positionPicker(picker, el);
        activePicker = picker;
        picker.querySelectorAll('[data-pick-type]').forEach(function(btn) {
          btn.addEventListener('click', function() {
            closeColumnPicker();
            addBlockToSection(parentId, btn.dataset.pickType);
          });
        });
        setTimeout(function() {
          document.addEventListener('click', closeColumnPickerOutside);
        }, 10);
      });
    });

    // Delete section nested block
    $blocks.querySelectorAll('.pb-sec-block__delete').forEach(function(btn) {
      btn.addEventListener('click', function(e) {
        e.stopPropagation();
        var block = btn.closest('.pb-sec-block');
        deleteSectionBlock(block.dataset.secBlockParent, parseInt(block.dataset.secBlockIdx));
      });
    });

    // Move up section nested block
    $blocks.querySelectorAll('.pb-sec-block__move-up').forEach(function(btn) {
      btn.addEventListener('click', function(e) {
        e.stopPropagation();
        var block = btn.closest('.pb-sec-block');
        moveSectionBlock(block.dataset.secBlockParent, parseInt(block.dataset.secBlockIdx), -1);
      });
    });

    // Move down section nested block
    $blocks.querySelectorAll('.pb-sec-block__move-down').forEach(function(btn) {
      btn.addEventListener('click', function(e) {
        e.stopPropagation();
        var block = btn.closest('.pb-sec-block');
        moveSectionBlock(block.dataset.secBlockParent, parseInt(block.dataset.secBlockIdx), 1);
      });
    });

    // Duplicate section nested block
    $blocks.querySelectorAll('.pb-sec-block__duplicate').forEach(function(btn) {
      btn.addEventListener('click', function(e) {
        e.stopPropagation();
        var block = btn.closest('.pb-sec-block');
        duplicateSectionBlock(block.dataset.secBlockParent, parseInt(block.dataset.secBlockIdx));
      });
    });

    // Drag reorder section nested blocks
    var _dragSecInfo = null;
    $blocks.querySelectorAll('.pb-sec-block').forEach(function(el) {
      var handle = el.querySelector('.pb-sec-block__drag-handle');
      if (!handle) return;

      handle.style.cursor = 'grab';
      handle.addEventListener('mousedown', function() { el.setAttribute('draggable', 'true'); handle.style.cursor = 'grabbing'; });
      handle.addEventListener('mouseup', function() { el.removeAttribute('draggable'); handle.style.cursor = 'grab'; });

      el.addEventListener('dragstart', function(e) {
        if (!el.getAttribute('draggable')) { e.preventDefault(); return; }
        e.stopPropagation();
        _dragSecInfo = { parentId: el.dataset.secBlockParent, blockId: el.dataset.secBlockId };
        e.dataTransfer.setData('application/x-pb-source', 'nested-section');
        e.dataTransfer.effectAllowed = 'move';
        el.style.opacity = '0.4';
      });

      el.addEventListener('dragend', function() {
        el.style.opacity = '';
        el.removeAttribute('draggable');
        handle.style.cursor = 'grab';
        _dragSecInfo = null;
        $blocks.querySelectorAll('.pb-sec-block').forEach(function(s) {
          s.style.borderTop = '';
          s.style.borderBottom = '';
        });
      });

      el.addEventListener('dragover', function(e) {
        if (!_dragSecInfo) return;
        if (_dragSecInfo.parentId !== el.dataset.secBlockParent) return;
        if (_dragSecInfo.blockId === el.dataset.secBlockId) return;
        e.preventDefault();
        e.stopPropagation();
        e.dataTransfer.dropEffect = 'move';
        var rect = el.getBoundingClientRect();
        var midY = rect.top + rect.height / 2;
        if (e.clientY < midY) {
          el.style.borderTop = '2px solid var(--pb-primary)';
          el.style.borderBottom = '';
        } else {
          el.style.borderTop = '';
          el.style.borderBottom = '2px solid var(--pb-primary)';
        }
      });

      el.addEventListener('dragleave', function() {
        el.style.borderTop = '';
        el.style.borderBottom = '';
      });

      el.addEventListener('drop', function(e) {
        e.preventDefault();
        e.stopPropagation();
        el.style.borderTop = '';
        el.style.borderBottom = '';
        if (!_dragSecInfo || _dragSecInfo.parentId !== el.dataset.secBlockParent) return;
        if (_dragSecInfo.blockId === el.dataset.secBlockId) return;

        var parent = findBlock(_dragSecInfo.parentId);
        if (!parent || !parent.data.blocks) return;

        var fromIdx = -1, toIdx = -1;
        parent.data.blocks.forEach(function(b, i) {
          if (b.id === _dragSecInfo.blockId) fromIdx = i;
          if (b.id === el.dataset.secBlockId) toIdx = i;
        });
        if (fromIdx < 0 || toIdx < 0) return;

        var rect = el.getBoundingClientRect();
        var midY = rect.top + rect.height / 2;
        var insertAfter = e.clientY >= midY;

        var moved = parent.data.blocks.splice(fromIdx, 1)[0];
        var newIdx = -1;
        parent.data.blocks.forEach(function(b, i) { if (b.id === el.dataset.secBlockId) newIdx = i; });
        if (insertAfter) newIdx++;
        parent.data.blocks.splice(newIdx, 0, moved);

        _dragSecInfo = null;
        markDirty();
        renderCanvas();
      });
    });

    // Drag & drop into sections
    $blocks.querySelectorAll('.pb-section-zone').forEach(function(zone) {
      zone.addEventListener('dragover', function(e) {
        e.preventDefault();
        e.stopPropagation();
        e.dataTransfer.dropEffect = 'copy';
        zone.style.outline = '2px dashed var(--pb-primary)';
      });
      zone.addEventListener('dragleave', function() {
        zone.style.outline = '';
      });
      zone.addEventListener('drop', function(e) {
        e.preventDefault();
        e.stopPropagation();
        zone.style.outline = '';
        var type = e.dataTransfer.getData('text/plain');
        if (type && BLOCK_DEFAULTS[type] && type !== 'section') {
          addBlockToSection(zone.dataset.secParent, type);
        }
      });
    });

    // Section inserter — "+" between nested blocks
    $blocks.querySelectorAll('[data-sec-insert-parent] .pb-inserter__btn').forEach(function(btn) {
      btn.addEventListener('click', function(e) {
        e.stopPropagation();
        var inserter = btn.closest('.pb-inserter');
        var parentId = inserter.dataset.secInsertParent;
        var index = parseInt(inserter.dataset.secInsertAt);
        closeColumnPicker();
        var picker = document.createElement('div');
        picker.className = 'pb-col-picker';
        var blockTypes = [
          { type: 'heading', icon: '<path d="M4 4v16"/><path d="M4 12h12"/><path d="M16 4v16"/>', label: PB_I18N.heading },
          { type: 'text', icon: '<path d="M4 7V4h16v3"/><path d="M9 20h6"/><path d="M12 4v16"/>', label: PB_I18N.text },
          { type: 'image', icon: '<rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="8.5" cy="8.5" r="1.5"/><polyline points="21 15 16 10 5 21"/>', label: PB_I18N.image },
          { type: 'button', icon: '<rect x="2" y="7" width="20" height="10" rx="2"/><path d="M9 12h6"/>', label: PB_I18N.button },
          { type: 'divider', icon: '<path d="M3 12h18"/>', label: PB_I18N.divider },
          { type: 'spacer', icon: '<path d="M12 5v14"/><path d="M5 12h14"/>', label: PB_I18N.spacer },
          { type: 'columns', icon: '<rect x="3" y="3" width="7" height="18"/><rect x="14" y="3" width="7" height="18"/>', label: PB_I18N.columns },
          { type: 'quote', icon: '<path d="M3 21c3 0 7-1 7-8V5c0-1.25-.756-2.017-2-2H4c-1.25 0-2 .75-2 1.972V11c0 1.25.75 2 2 2 1 0 1 0 1 1v1c0 1-1 2-2 2s-1 .008-1 1.031V21z"/>', label: PB_I18N.quote },
          { type: 'form', icon: '<rect x="3" y="3" width="18" height="18" rx="2"/><path d="M7 7h10"/><path d="M7 12h10"/><path d="M7 17h6"/>', label: PB_I18N.form }
        ];
        picker.innerHTML = blockTypes.map(function(bt) {
          return '<button class="pb-col-picker__btn" data-pick-type="' + bt.type + '"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">' + bt.icon + '</svg>' + bt.label + '</button>';
        }).join('');
        _positionPicker(picker, btn);
        activePicker = picker;
        picker.querySelectorAll('[data-pick-type]').forEach(function(pbtn) {
          pbtn.addEventListener('click', function() {
            closeColumnPicker();
            addBlockToSection(parentId, pbtn.dataset.pickType, index);
          });
        });
        setTimeout(function() { document.addEventListener('click', closeColumnPickerOutside); }, 10);
      });
    });

    // Column inserter — "+" between nested blocks
    $blocks.querySelectorAll('[data-col-insert-parent] .pb-inserter__btn').forEach(function(btn) {
      btn.addEventListener('click', function(e) {
        e.stopPropagation();
        var inserter = btn.closest('.pb-inserter');
        var parentId = inserter.dataset.colInsertParent;
        var colIdx = parseInt(inserter.dataset.colInsertCol);
        var index = parseInt(inserter.dataset.colInsertAt);
        closeColumnPicker();
        var picker = document.createElement('div');
        picker.className = 'pb-col-picker';
        picker.innerHTML = _canvasPickerTypes.filter(function(bt) {
          return bt.type !== 'columns' && bt.type !== 'section';
        }).map(function(bt) {
          return '<button class="pb-col-picker__btn" data-pick-type="' + bt.type + '"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">' + bt.icon + '</svg>' + bt.label + '</button>';
        }).join('');
        _positionPicker(picker, btn);
        activePicker = picker;
        picker.querySelectorAll('[data-pick-type]').forEach(function(pbtn) {
          pbtn.addEventListener('click', function() {
            closeColumnPicker();
            addBlockToColumn(parentId, colIdx, pbtn.dataset.pickType, index);
          });
        });
        setTimeout(function() { document.addEventListener('click', closeColumnPickerOutside); }, 10);
      });
    });

    // Click on section nested block to open its panel
    $blocks.querySelectorAll('.pb-sec-block').forEach(function(el) {
      el.addEventListener('click', function(e) {
        if (e.target.closest('.pb-sec-block__toolbar')) return;
        e.stopPropagation();
        var parentId = el.dataset.secBlockParent;
        var blockIdx = parseInt(el.dataset.secBlockIdx);
        var parent = findBlock(parentId);
        if (parent && parent.data.blocks && parent.data.blocks[blockIdx]) {
          openPanel(parent.data.blocks[blockIdx]);
        }
      });
    });

    // ── Slideshow nested blocks (hero mode) ──

    // Add block buttons in slideshow slides
    $blocks.querySelectorAll('[data-slide-add]').forEach(function(el) {
      el.addEventListener('click', function(e) {
        e.stopPropagation();
        var parentId = el.dataset.slideAdd;
        closeColumnPicker();
        var picker = document.createElement('div');
        picker.className = 'pb-col-picker';
        var blockTypes = [
          { type: 'heading', icon: '<path d="M4 4v16"/><path d="M4 12h12"/><path d="M16 4v16"/>', label: PB_I18N.heading },
          { type: 'text', icon: '<path d="M4 7V4h16v3"/><path d="M9 20h6"/><path d="M12 4v16"/>', label: PB_I18N.text },
          { type: 'button', icon: '<rect x="2" y="7" width="20" height="10" rx="2"/><path d="M9 12h6"/>', label: PB_I18N.button },
          { type: 'divider', icon: '<path d="M3 12h18"/>', label: PB_I18N.divider },
          { type: 'spacer', icon: '<path d="M12 5v14"/><path d="M5 12h14"/>', label: PB_I18N.spacer }
        ];
        picker.innerHTML = blockTypes.map(function(bt) {
          return '<button class="pb-col-picker__btn" data-pick-type="' + bt.type + '">' +
            '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">' + bt.icon + '</svg>' +
            bt.label + '</button>';
        }).join('');
        _positionPicker(picker, el);
        activePicker = picker;
        picker.querySelectorAll('[data-pick-type]').forEach(function(btn) {
          btn.addEventListener('click', function() {
            closeColumnPicker();
            addBlockToSlide(parentId, btn.dataset.pickType);
          });
        });
        setTimeout(function() {
          document.addEventListener('click', closeColumnPickerOutside);
        }, 10);
      });
    });

    // Delete slideshow nested block
    $blocks.querySelectorAll('.pb-slide-block__delete').forEach(function(btn) {
      btn.addEventListener('click', function(e) {
        e.stopPropagation();
        var block = btn.closest('.pb-slide-block');
        deleteSlideBlock(block.dataset.slideBlockParent, parseInt(block.dataset.slideBlockIdx));
      });
    });

    // Move up slideshow nested block
    $blocks.querySelectorAll('.pb-slide-block__move-up').forEach(function(btn) {
      btn.addEventListener('click', function(e) {
        e.stopPropagation();
        var block = btn.closest('.pb-slide-block');
        moveSlideBlock(block.dataset.slideBlockParent, parseInt(block.dataset.slideBlockIdx), -1);
      });
    });

    // Move down slideshow nested block
    $blocks.querySelectorAll('.pb-slide-block__move-down').forEach(function(btn) {
      btn.addEventListener('click', function(e) {
        e.stopPropagation();
        var block = btn.closest('.pb-slide-block');
        moveSlideBlock(block.dataset.slideBlockParent, parseInt(block.dataset.slideBlockIdx), 1);
      });
    });

    // Duplicate slideshow nested block
    $blocks.querySelectorAll('.pb-slide-block__duplicate').forEach(function(btn) {
      btn.addEventListener('click', function(e) {
        e.stopPropagation();
        var block = btn.closest('.pb-slide-block');
        duplicateSlideBlock(block.dataset.slideBlockParent, parseInt(block.dataset.slideBlockIdx));
      });
    });

    // Drag reorder slideshow nested blocks
    var _dragSlideInfo = null;
    $blocks.querySelectorAll('.pb-slide-block').forEach(function(el) {
      var handle = el.querySelector('.pb-slide-block__drag-handle');
      if (!handle) return;
      handle.style.cursor = 'grab';
      handle.addEventListener('mousedown', function() { el.setAttribute('draggable', 'true'); handle.style.cursor = 'grabbing'; });
      handle.addEventListener('mouseup', function() { el.removeAttribute('draggable'); handle.style.cursor = 'grab'; });
      el.addEventListener('dragstart', function(e) {
        if (!el.getAttribute('draggable')) { e.preventDefault(); return; }
        e.stopPropagation();
        _dragSlideInfo = { parentId: el.dataset.slideBlockParent, blockId: el.dataset.slideBlockId };
        e.dataTransfer.setData('application/x-pb-source', 'nested-slide');
        e.dataTransfer.effectAllowed = 'move';
        el.style.opacity = '0.4';
      });
      el.addEventListener('dragend', function() {
        el.style.opacity = ''; el.removeAttribute('draggable'); handle.style.cursor = 'grab'; _dragSlideInfo = null;
        $blocks.querySelectorAll('.pb-slide-block').forEach(function(s) { s.style.borderTop = ''; s.style.borderBottom = ''; });
      });
      el.addEventListener('dragover', function(e) {
        if (!_dragSlideInfo || _dragSlideInfo.parentId !== el.dataset.slideBlockParent || _dragSlideInfo.blockId === el.dataset.slideBlockId) return;
        e.preventDefault(); e.stopPropagation(); e.dataTransfer.dropEffect = 'move';
        var rect = el.getBoundingClientRect(); var midY = rect.top + rect.height / 2;
        el.style.borderTop = e.clientY < midY ? '2px solid var(--pb-primary)' : '';
        el.style.borderBottom = e.clientY >= midY ? '2px solid var(--pb-primary)' : '';
      });
      el.addEventListener('dragleave', function() { el.style.borderTop = ''; el.style.borderBottom = ''; });
      el.addEventListener('drop', function(e) {
        e.preventDefault(); e.stopPropagation(); el.style.borderTop = ''; el.style.borderBottom = '';
        if (!_dragSlideInfo || _dragSlideInfo.parentId !== el.dataset.slideBlockParent || _dragSlideInfo.blockId === el.dataset.slideBlockId) return;
        var parent = findBlock(_dragSlideInfo.parentId);
        if (!parent || !parent.data.slides || !parent.data.slides[_activeSlideIdx(_dragSlideInfo.parentId)]) return;
        var blocks = parent.data.slides[_activeSlideIdx(_dragSlideInfo.parentId)].blocks;
        if (!blocks) return;
        var fromIdx = -1, toIdx = -1;
        blocks.forEach(function(b, i) { if (b.id === _dragSlideInfo.blockId) fromIdx = i; if (b.id === el.dataset.slideBlockId) toIdx = i; });
        if (fromIdx < 0 || toIdx < 0) return;
        var rect = el.getBoundingClientRect(); var insertAfter = e.clientY >= rect.top + rect.height / 2;
        var moved = blocks.splice(fromIdx, 1)[0];
        var newIdx = -1; blocks.forEach(function(b, i) { if (b.id === el.dataset.slideBlockId) newIdx = i; });
        if (insertAfter) newIdx++;
        blocks.splice(newIdx, 0, moved);
        _dragSlideInfo = null; markDirty(); renderCanvas();
      });
    });

    // Slideshow inserter — "+" between nested blocks
    $blocks.querySelectorAll('[data-slide-insert-parent] .pb-inserter__btn').forEach(function(btn) {
      btn.addEventListener('click', function(e) {
        e.stopPropagation();
        var inserter = btn.closest('.pb-inserter');
        var parentId = inserter.dataset.slideInsertParent;
        var index = parseInt(inserter.dataset.slideInsertAt);
        closeColumnPicker();
        var picker = document.createElement('div');
        picker.className = 'pb-col-picker';
        var blockTypes = [
          { type: 'heading', icon: '<path d="M4 4v16"/><path d="M4 12h12"/><path d="M16 4v16"/>', label: PB_I18N.heading },
          { type: 'text', icon: '<path d="M4 7V4h16v3"/><path d="M9 20h6"/><path d="M12 4v16"/>', label: PB_I18N.text },
          { type: 'image', icon: '<rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="8.5" cy="8.5" r="1.5"/><polyline points="21 15 16 10 5 21"/>', label: PB_I18N.image },
          { type: 'button', icon: '<rect x="2" y="7" width="20" height="10" rx="2"/><path d="M9 12h6"/>', label: PB_I18N.button },
          { type: 'divider', icon: '<path d="M3 12h18"/>', label: PB_I18N.divider },
          { type: 'spacer', icon: '<path d="M12 5v14"/><path d="M5 12h14"/>', label: PB_I18N.spacer }
        ];
        picker.innerHTML = blockTypes.map(function(bt) {
          return '<button class="pb-col-picker__btn" data-pick-type="' + bt.type + '"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">' + bt.icon + '</svg>' + bt.label + '</button>';
        }).join('');
        _positionPicker(picker, btn);
        activePicker = picker;
        picker.querySelectorAll('[data-pick-type]').forEach(function(pbtn) {
          pbtn.addEventListener('click', function() {
            closeColumnPicker();
            addBlockToSlide(parentId, pbtn.dataset.pickType, index);
          });
        });
        setTimeout(function() { document.addEventListener('click', closeColumnPickerOutside); }, 10);
      });
    });

    // Drag & drop into slideshow slides
    $blocks.querySelectorAll('.pb-slide-zone').forEach(function(zone) {
      zone.addEventListener('dragover', function(e) {
        e.preventDefault();
        e.stopPropagation();
        e.dataTransfer.dropEffect = 'copy';
        zone.style.outline = '2px dashed rgba(255,255,255,0.6)';
      });
      zone.addEventListener('dragleave', function() {
        zone.style.outline = '';
      });
      zone.addEventListener('drop', function(e) {
        e.preventDefault();
        e.stopPropagation();
        zone.style.outline = '';
        var type = e.dataTransfer.getData('text/plain');
        if (type && BLOCK_DEFAULTS[type] && type !== 'section' && type !== 'columns' && type !== 'slideshow') {
          addBlockToSlide(zone.dataset.slideParent, type);
        }
      });
    });

    // Click on slideshow nested block to open its panel
    $blocks.querySelectorAll('.pb-slide-block').forEach(function(el) {
      el.addEventListener('click', function(e) {
        if (e.target.closest('.pb-slide-block__toolbar')) return;
        e.stopPropagation();
        var parentId = el.dataset.slideBlockParent;
        var blockIdx = parseInt(el.dataset.slideBlockIdx);
        var parent = findBlock(parentId);
        if (parent && parent.data.slides && parent.data.slides[_activeSlideIdx(parentId)] && parent.data.slides[_activeSlideIdx(parentId)].blocks && parent.data.slides[_activeSlideIdx(parentId)].blocks[blockIdx]) {
          openPanel(parent.data.slides[_activeSlideIdx(parentId)].blocks[blockIdx]);
        }
      });
    });

    // Slide tab selector — switch which slide is being edited
    $blocks.querySelectorAll('.pb-slide-tab').forEach(function(tab) {
      tab.addEventListener('click', function(e) {
        e.stopPropagation();
        var parentId = tab.dataset.slideTabParent;
        var slideIdx = parseInt(tab.dataset.slideTabIdx);
        _slideEditIdx[parentId] = slideIdx;
        renderCanvas();
      });
    });

    // ── Slideshow canvas: add new slide via "+" tab ──
    $blocks.querySelectorAll('[data-slide-canvas-add]').forEach(function(btn) {
      btn.addEventListener('click', function(e) {
        e.stopPropagation();
        var blockId = btn.dataset.slideCanvasAdd;
        var block = findBlock(blockId);
        if (!block) return;
        block.data.slides = block.data.slides || [];
        block.data.slides.push({ image: '', alt: '', blocks: [] });
        _slideEditIdx[blockId] = block.data.slides.length - 1;
        markDirty();
        renderCanvas();
        openPanel(block);
      });
    });

    // ── Slideshow canvas: pick slide background image ──
    $blocks.querySelectorAll('[data-slide-pick]').forEach(function(el) {
      el.addEventListener('click', function(e) {
        e.stopPropagation();
        var blockId = el.dataset.slidePick;
        var slideIdx = parseInt(el.dataset.slidePickIdx);
        var block = findBlock(blockId);
        if (!block || !block.data.slides || !block.data.slides[slideIdx]) return;
        // Open image picker — on select, set slide image + alt
        pbPickImage('data.slides.' + slideIdx + '.image');
        // Temporarily select this block so panel alt field gets synced
        openPanel(block);
      });
    });

    // ── Slideshow canvas: clear slide background image ──
    $blocks.querySelectorAll('[data-slide-clear]').forEach(function(btn) {
      btn.addEventListener('click', function(e) {
        e.stopPropagation();
        var blockId = btn.dataset.slideClear;
        var slideIdx = parseInt(btn.dataset.slideClearIdx);
        var block = findBlock(blockId);
        if (block && block.data.slides && block.data.slides[slideIdx]) {
          block.data.slides[slideIdx].image = '';
          block.data.slides[slideIdx].alt = '';
          markDirty();
          renderCanvas();
          openPanel(block);
        }
      });
    });

    // Tab button click — switch active tab panel via CSS classes
    $blocks.querySelectorAll('[data-tab-switch]').forEach(function(btn) {
      btn.addEventListener('mousedown', function(e) {
        if (btn.classList.contains('is-active')) return;
        e.preventDefault();
        var blockId = btn.dataset.tabBlock;
        var idx = parseInt(btn.dataset.tabSwitch);
        _tabEditIdx[blockId] = idx;
        var pbItem = btn.closest('.pb-item');
        pbItem.querySelectorAll('.pb-tabs-btn[data-tab-block="' + blockId + '"]').forEach(function(b) {
          b.classList.toggle('is-active', parseInt(b.dataset.tabSwitch) === idx);
        });
        pbItem.querySelectorAll('.pb-tab-panel[data-tab-block="' + blockId + '"]').forEach(function(p) {
          p.classList.toggle('is-active', parseInt(p.dataset.tabPanel) === idx);
        });
      });
    });

    // Accordion header click — toggle item open/closed via CSS class
    $blocks.querySelectorAll('[data-acc-toggle]').forEach(function(el) {
      el.addEventListener('click', function(e) {
        if (e.target.closest('.pb-inline-acc-title')) return;
        var item = el.closest('.pb-acc-item');
        if (item) item.classList.toggle('is-open');
      });
    });

    // ── Empty image blocks: click to open panel + auto-trigger library picker ──
    $blocks.querySelectorAll('[data-image-pick-block]').forEach(function(el) {
      el.addEventListener('click', function(e) {
        e.stopPropagation();
        var blockId = el.dataset.imagePickBlock;
        var block = findBlockDeep(blockId);
        if (!block) return;
        openPanel(block);
        // Auto-open image library picker after panel renders
        setTimeout(function() {
          var pickBtn = $panelBody.querySelector('.pb-pick-image-btn[data-target-key="data.url"]');
          if (pickBtn) pickBtn.click();
        }, 150);
      });
    });

    // Clear image — return to empty image block
    $blocks.querySelectorAll('[data-clear-image]').forEach(function(btn) {
      btn.addEventListener('click', function(e) {
        e.stopPropagation();
        var blockId = btn.dataset.clearImage;
        var block = findBlockDeep(blockId);
        if (block) {
          block.data.url = '';
          block.data.alt = '';
          markDirty();
          renderCanvas();
          openPanel(block);
        }
      });
    });

    // ── Empty form blocks: click to open form picker modal ──
    $blocks.querySelectorAll('[data-form-pick-block]').forEach(function(el) {
      el.addEventListener('click', function(e) {
        e.stopPropagation();
        var blockId = el.dataset.formPickBlock;
        var block = findBlockDeep(blockId);
        if (!block) return;
        pbPickForm(block);
      });
    });

    // ── Clear form — return to empty form block ──
    $blocks.querySelectorAll('[data-clear-form]').forEach(function(btn) {
      btn.addEventListener('click', function(e) {
        e.stopPropagation();
        var blockId = btn.dataset.clearForm;
        var block = findBlockDeep(blockId);
        if (block) {
          block.data.form_id = '';
          block.data._form_title = '';
          block.data._form_title_raw = '';
          block.data._fields = null;
          block.data._settings = null;
          block.data._description = '';
          markDirty();
          renderCanvas();
          openPanel(block);
        }
      });
    });

    // ── Video canvas: clear video (remove from single video block) ──
    $blocks.querySelectorAll('[data-clear-video]').forEach(function(btn) {
      btn.addEventListener('click', function(e) {
        e.stopPropagation();
        var blockId = btn.dataset.clearVideo;
        var block = findBlockDeep(blockId);
        if (block) {
          block.data.url = '';
          block.data.embed_id = '';
          block.data.source = '';
          block.data.title = '';
          markDirty();
          renderCanvas();
          openPanel(block);
        }
      });
    });

    // ── Video canvas: pick video (empty state click) ──
    $blocks.querySelectorAll('[data-video-pick-block]').forEach(function(el) {
      el.addEventListener('click', function(e) {
        e.stopPropagation();
        var blockId = el.dataset.videoPickBlock;
        var block = findBlockDeep(blockId);
        if (block) {
          openPanel(block);
          pbPickSingleVideo(block);
        }
      });
    });

    // ── Gallery canvas: remove individual image ──
    $blocks.querySelectorAll('[data-gal-canvas-remove]').forEach(function(btn) {
      btn.addEventListener('click', function(e) {
        e.stopPropagation();
        var blockId = btn.dataset.galCanvasBlock;
        var idx = parseInt(btn.dataset.galCanvasRemove);
        var block = findBlockDeep(blockId);
        if (block && block.data.images && idx >= 0 && idx < block.data.images.length) {
          block.data.images.splice(idx, 1);
          markDirty();
          renderCanvas();
          if (state.selectedId === blockId) openPanel(block);
        }
      });
    });

    // ── Gallery canvas: add images from library ──
    $blocks.querySelectorAll('[data-gal-canvas-add]').forEach(function(el) {
      el.addEventListener('click', function(e) {
        e.stopPropagation();
        var blockId = el.dataset.galCanvasAdd;
        var block = findBlockDeep(blockId);
        if (block) {
          openPanel(block);
          pbPickGalleryImages(block);
        }
      });
    });

    // ── Video Gallery canvas: remove individual video ──
    $blocks.querySelectorAll('[data-vgal-canvas-remove]').forEach(function(btn) {
      btn.addEventListener('click', function(e) {
        e.stopPropagation();
        var blockId = btn.dataset.vgalCanvasBlock;
        var idx = parseInt(btn.dataset.vgalCanvasRemove);
        var block = findBlockDeep(blockId);
        if (block && block.data.videos && idx >= 0 && idx < block.data.videos.length) {
          block.data.videos.splice(idx, 1);
          markDirty();
          renderCanvas();
          if (state.selectedId === blockId) openPanel(block);
        }
      });
    });

    // ── Video Gallery canvas: add video ──
    $blocks.querySelectorAll('[data-vgal-canvas-add]').forEach(function(el) {
      el.addEventListener('click', function(e) {
        e.stopPropagation();
        var blockId = el.dataset.vgalCanvasAdd;
        var block = findBlockDeep(blockId);
        if (block) {
          openPanel(block);
          pbAddVideoToGallery(block);
        }
      });
    });

    // ── Gallery canvas: reorder images ──
    $blocks.querySelectorAll('[data-gal-move-block]').forEach(function(btn) {
      btn.addEventListener('click', function(e) {
        e.stopPropagation();
        var blockId = btn.dataset.galMoveBlock;
        var from = parseInt(btn.dataset.galMoveFrom);
        var dir = parseInt(btn.dataset.galMoveDir);
        var block = findBlockDeep(blockId);
        if (!block || !block.data.images) return;
        var to = from + dir;
        if (to < 0 || to >= block.data.images.length) return;
        var item = block.data.images.splice(from, 1)[0];
        block.data.images.splice(to, 0, item);
        markDirty();
        renderCanvas();
        if (state.selectedId === blockId) openPanel(block);
      });
    });

    // ── Video Gallery canvas: reorder videos ──
    $blocks.querySelectorAll('[data-vgal-move-block]').forEach(function(btn) {
      btn.addEventListener('click', function(e) {
        e.stopPropagation();
        var blockId = btn.dataset.vgalMoveBlock;
        var from = parseInt(btn.dataset.vgalMoveFrom);
        var dir = parseInt(btn.dataset.vgalMoveDir);
        var block = findBlockDeep(blockId);
        if (!block || !block.data.videos) return;
        var to = from + dir;
        if (to < 0 || to >= block.data.videos.length) return;
        var item = block.data.videos.splice(from, 1)[0];
        block.data.videos.splice(to, 0, item);
        markDirty();
        renderCanvas();
        if (state.selectedId === blockId) openPanel(block);
      });
    });

    // ── Ticker canvas: remove individual image ──
    $blocks.querySelectorAll('[data-ticker-canvas-remove]').forEach(function(btn) {
      btn.addEventListener('click', function(e) {
        e.stopPropagation();
        var blockId = btn.dataset.tickerCanvasBlock;
        var idx = parseInt(btn.dataset.tickerCanvasRemove);
        var block = findBlockDeep(blockId);
        if (block && block.data.images && idx >= 0 && idx < block.data.images.length) {
          block.data.images.splice(idx, 1);
          markDirty();
          renderCanvas();
          if (state.selectedId === blockId) openPanel(block);
        }
      });
    });

    // ── Ticker canvas: add images from library ──
    $blocks.querySelectorAll('[data-ticker-canvas-add]').forEach(function(el) {
      el.addEventListener('click', function(e) {
        e.stopPropagation();
        var blockId = el.dataset.tickerCanvasAdd;
        var block = findBlockDeep(blockId);
        if (block) {
          openPanel(block);
          pbPickGalleryImages(block);
        }
      });
    });
  }
  function openPanel(block) {
    state.selectedId = block.id;
    $panel.classList.add('is-open');
    $panelTitle.textContent = (BLOCK_LABELS[block.type] || block.type) + ' — ' + PB_I18N.block_settings;
    $panelBody.innerHTML = getPanelHTML(block);
    bindPanelEvents(block);

    // Highlight in canvas
    $blocks.querySelectorAll('.pb-item').forEach(function(el) {
      el.classList.toggle('is-selected', el.dataset.id === block.id);
    });
  }

  function closePanel() {
    state.selectedId = null;
    $panel.classList.remove('is-open');
    $blocks.querySelectorAll('.pb-item.is-selected').forEach(function(el) {
      el.classList.remove('is-selected');
    });
  }

  function getPanelHTML(block) {
    var d = block.data || {};
    var s = block.settings || {};

    var html = '';

    // Type-specific fields
    switch (block.type) {
      case 'heading':
        html += field(PB_I18N.text_content, 'input', 'data.text', d.text || '');
        html += fieldSelect(PB_I18N.level, 'data.level', d.level || 2, [
          {v: 1, l: 'H1'}, {v: 2, l: 'H2'}, {v: 3, l: 'H3'}, {v: 4, l: 'H4'}, {v: 5, l: 'H5'}, {v: 6, l: 'H6'}
        ]);
        break;
      case 'text':
        html += '<div class="pb-field pb-text-canvas-hint">' +
          '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 20h9"/><path d="M16.5 3.5a2.121 2.121 0 013 3L7 19l-4 1 1-4L16.5 3.5z"/></svg>' +
          '<span>' + (PB_I18N.text_edit_canvas_hint || 'Click the text on canvas to edit') + '</span>' +
          '</div>';
        break;
      case 'image':
        html += fieldImageUrl(PB_I18N.image_url, 'data.url', d.url || '', PB_I18N.image_url_hint);
        // Hidden field for alt — populated by image picker from library
        html += '<input type="hidden" data-key="data.alt" value="' + esc(d.alt || '') + '">';
        // Show image preview in panel
        if (d.url) {
          html += '<div class="pb-panel-preview"><img src="' + esc(d.url) + '" class="pb-panel-preview" alt=""></div>';
        }
        // Show alt from library (read-only)
        if (d.alt) {
          html += '<div class="pb-alt-badge pb-alt-badge--ok">' +
            '<span class="pb-alt-badge__label">alt:</span> ' + esc(d.alt) +
            '</div>';
          html += '<p class="pb-text-hint pb-hint--pull-lg">' + (PB_I18N.alt_text_library_hint || 'Inherited from media library') + '</p>';
        } else if (d.url) {
          html += '<div class="pb-alt-badge pb-alt-badge--warn">' +
            '⚠ ' + (PB_I18N.alt_missing || 'No alt text — select image from library') +
            '</div>';
        }
        html += field(PB_I18N.caption, 'input', 'data.caption', d.caption || '');
        html += field(PB_I18N.link_url, 'input', 'data.link_url', d.link_url || '');
        html += fieldSelect(PB_I18N.size, 'data.size', d.size || 'full', [
          {v: 'full', l: PB_I18N.size_full}, {v: 'medium', l: PB_I18N.size_medium}, {v: 'small', l: PB_I18N.size_small}
        ]);
        break;
      case 'button':
        html += field(PB_I18N.button_text, 'input', 'data.text', d.text || '');
        html += field(PB_I18N.button_url, 'input', 'data.url', d.url || '#');
        html += fieldSelect(PB_I18N.style, 'data.style', d.style || 'primary', [
          {v: 'primary', l: PB_I18N.style_primary}, {v: 'outline', l: PB_I18N.style_outline}, {v: 'ghost', l: PB_I18N.style_ghost}
        ]);
        html += fieldSelect(PB_I18N.size, 'data.size', d.size || 'md', [
          {v: 'sm', l: PB_I18N.size_sm}, {v: 'md', l: PB_I18N.size_md}, {v: 'lg', l: PB_I18N.size_lg}
        ]);
        html += fieldSelect(PB_I18N.target, 'data.target', d.target || '_self', [
          {v: '_self', l: PB_I18N.target_self}, {v: '_blank', l: PB_I18N.target_blank}
        ]);
        break;
      case 'divider':
        html += fieldSelect(PB_I18N.style, 'data.style', d.style || 'solid', [
          {v: 'solid', l: PB_I18N.style_solid}, {v: 'dashed', l: PB_I18N.style_dashed}, {v: 'dotted', l: PB_I18N.style_dotted}, {v: 'gradient', l: PB_I18N.style_gradient}
        ]);
        break;
      case 'spacer':
        html += field(PB_I18N.height_px, 'number', 'data.height', d.height || '48');
        break;
      case 'columns':
        html += fieldSelect(PB_I18N.columns_layout, 'data.layout', d.layout || '1/2+1/2', [
          {v: '1', l: '100% (1 col)'},
          {v: '1/2+1/2', l: '50% + 50%'}, {v: '1/3+1/3+1/3', l: '33% + 33% + 33%'},
          {v: '2/3+1/3', l: '66% + 33%'}, {v: '1/3+2/3', l: '33% + 66%'},
          {v: '1/4+1/4+1/4+1/4', l: '25% × 4'}
        ]);
        html += '<p class="pb-text-hint pb-mt-2 pb-panel-count">' + PB_I18N.columns_hint + '</p>';
        break;
      case 'video':
        if (d.embed_id) {
          var svThumb = '';
          if (d.source && d.embed_id) {
            svThumb = videoThumbUrl(d.source, d.embed_id);
          }
          html += '<div class="pb-video-wrap--sm pb-mb-2">';
          if (svThumb) {
            html += '<div class="pb-aspect-video"><img src="' + svThumb + '" alt="' + esc(d.title || '') + '" class="pb-cover-img"></div>';
          } else {
            html += '<div class="pb-aspect-video pb-video-placeholder pb-video-placeholder--sm">' + (d.source === 'vimeo' ? 'Vimeo' : '▶') + '</div>';
          }
          html += '<div class="pb-overlay-center--no-events"><div class="pb-play-btn pb-play-btn--xs">▶</div></div>';
          if (d.title) {
            html += '<div class="pb-thumb-label" title="' + esc(d.title) + '">' + esc(d.title) + '</div>';
          }
          html += '<button class="pb-video-clear-btn pb-thumb-remove" title="' + (PB_I18N.remove || 'Remove') + '">&times;</button>';
          html += '</div>';
        }
        html += '<button type="button" class="pb-video-pick-btn pb-dashed-add">' + (d.embed_id ? '↻ ' + (PB_I18N.video_url || 'Change video') : '+ ' + (PB_I18N.video_gallery_add || 'Add video')) + '</button>';
        html += fieldSelect(PB_I18N.aspect_ratio, 'data.aspect_ratio', d.aspect_ratio || '16/9', [
          {v: '16/9', l: '16:9'}, {v: '4/3', l: '4:3'}, {v: '1/1', l: '1:1'}
        ]);
        break;
      case 'html':
        html += fieldTextarea(PB_I18N.html_code, 'data.code', d.code || '');
        break;
      case 'quote':
        html += fieldTextarea(PB_I18N.quote_text, 'data.text', d.text || '');
        html += field(PB_I18N.author, 'input', 'data.author', d.author || '');
        break;
      case 'code':
        html += '<div class="pb-field pb-text-canvas-hint">' +
          '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 20h9"/><path d="M16.5 3.5a2.121 2.121 0 013 3L7 19l-4 1 1-4L16.5 3.5z"/></svg>' +
          '<span>' + (PB_I18N.text_edit_canvas_hint || 'Click the text on canvas to edit') + '</span></div>';
        html += fieldSelect(PB_I18N.code_language, 'data.language', d.language || 'javascript', [
          {v: 'html', l: 'HTML'}, {v: 'css', l: 'CSS'}, {v: 'javascript', l: 'JavaScript'},
          {v: 'typescript', l: 'TypeScript'}, {v: 'python', l: 'Python'}, {v: 'php', l: 'PHP'},
          {v: 'sql', l: 'SQL'}, {v: 'bash', l: 'Bash'}, {v: 'json', l: 'JSON'},
          {v: 'xml', l: 'XML'}, {v: 'markdown', l: 'Markdown'}, {v: 'plaintext', l: 'Plain Text'}
        ]);
        break;
      case 'audio':
        html += field(PB_I18N.audio_url, 'input', 'data.url', d.url || '');
        html += field(PB_I18N.audio_title, 'input', 'data.title', d.title || '');
        break;
      case 'accordion':
        html += '<p class="pb-text-hint pb-mb-3 pb-panel-count">' + (d.items||[]).length + ' ' + (PB_I18N.accordion_items || 'items').toLowerCase() + '</p>';
        if ((d.items || []).length === 0) {
          html += '<div class="pb-picker-zone--sm">' + (PB_I18N.empty_hint || 'No items yet. Click the button below to add one.') + '</div>';
        }
        (d.items || []).forEach(function(item, i) {
          html += '<div class="pb-item-card">';
          html += field(PB_I18N.text_content + ' ' + (i+1), 'input', 'data.items.' + i + '.title', item.title || '');
          html += '<div class="pb-field pb-text-canvas-hint">' +
            '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 20h9"/><path d="M16.5 3.5a2.121 2.121 0 013 3L7 19l-4 1 1-4L16.5 3.5z"/></svg>' +
            '<span>' + (PB_I18N.text_edit_canvas_hint || 'Click the text on canvas to edit') + '</span></div>';
          html += '<button class="pb-btn pb-btn--danger pb-btn--sm" data-remove-item="accordion" data-index="' + i + '">' + PB_I18N.remove + '</button>';
          html += '</div>';
        });
        html += '<button class="pb-btn pb-btn--outline pb-btn--sm pb-mt-1" data-add-item="accordion">+ ' + PB_I18N.add_item + '</button>';
        break;
      case 'tabs':
        html += '<p class="pb-text-hint pb-mb-3 pb-panel-count">' + (d.tabs||[]).length + ' ' + (PB_I18N.tabs_count || 'tabs').toLowerCase() + '</p>';
        if ((d.tabs || []).length === 0) {
          html += '<div class="pb-picker-zone--sm">' + (PB_I18N.empty_hint || 'No items yet. Click the button below to add one.') + '</div>';
        }
        (d.tabs || []).forEach(function(tab, i) {
          html += '<div class="pb-item-card">';
          html += field('Tab ' + (i+1), 'input', 'data.tabs.' + i + '.title', tab.title || '');
          html += '<div class="pb-field pb-text-canvas-hint">' +
            '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 20h9"/><path d="M16.5 3.5a2.121 2.121 0 013 3L7 19l-4 1 1-4L16.5 3.5z"/></svg>' +
            '<span>' + (PB_I18N.text_edit_canvas_hint || 'Click the text on canvas to edit') + '</span></div>';
          html += '<button class="pb-btn pb-btn--danger pb-btn--sm" data-remove-item="tabs" data-index="' + i + '">' + PB_I18N.remove + '</button>';
          html += '</div>';
        });
        html += '<button class="pb-btn pb-btn--outline pb-btn--sm pb-mt-1" data-add-item="tabs">+ ' + PB_I18N.add_tab + '</button>';
        break;
      case 'table':
        var headers = d.headers || [];
        var rows = d.rows || [];
        html += fieldSelect((PB_I18N.table_width || 'Table width'), 'data.table_width', d.table_width || 'full', [
          {v: 'full', l: (PB_I18N.size_full || 'Full width')}, {v: 'auto', l: (PB_I18N.size_auto || 'Auto')}
        ]);
        html += '<div class="pb-field">';
        html += '<div class="pb-table-editor" id="pbTableEditor"><table>';
        // Header row with per-column delete buttons
        html += '<tr>';
        headers.forEach(function(h, hi) {
          html += '<th><div class="pb-table-header-row"><input type="text" data-table-header="' + hi + '" value="' + esc(stripTags(h)) + '" placeholder="' + PB_I18N.heading + '" class="pb-flex-1">';
          if (headers.length > 1) {
            html += '<button type="button" data-table-remove-col="' + hi + '" class="pb-icon-btn--ghost-sm" title="' + (PB_I18N.remove || 'Remove') + '">✕</button>';
          }
          html += '</div></th>';
        });
        html += '</tr>';
        // Data rows with per-row delete buttons
        rows.forEach(function(row, ri) {
          html += '<tr>';
          headers.forEach(function(_, ci) {
            var cellVal = (row && row[ci]) ? row[ci] : '';
            html += '<td><input type="text" data-table-cell data-row="' + ri + '" data-col="' + ci + '" value="' + esc(stripTags(cellVal)) + '"></td>';
          });
          if (rows.length > 1) {
            html += '<td class="pb-table-remove-cell"><button type="button" data-table-insert-row="' + ri + '" class="pb-icon-btn--ghost" title="' + (PB_I18N.table_insert_row || 'Insert row') + '">+</button><button type="button" data-table-remove-row="' + ri + '" class="pb-icon-btn--ghost" title="' + (PB_I18N.remove || 'Remove') + '">✕</button></td>';
          }
          html += '</tr>';
        });
        html += '</table></div>';
        html += '<div class="pb-table-actions">';
        html += '<button type="button" class="pb-btn pb-btn--outline pb-btn--sm" data-table-action="add-row">+ ' + (PB_I18N.table_add_row || 'Row') + '</button>';
        html += '<button type="button" class="pb-btn pb-btn--outline pb-btn--sm" data-table-action="add-col">+ ' + (PB_I18N.table_add_col || 'Column') + '</button>';
        html += '</div>';
        // Per-column alignment
        var colAligns = d.column_alignments || [];
        html += '<div class="pb-field"><label class="pb-field__label">' + (PB_I18N.column_alignment || 'Column alignment') + '</label>';
        html += '<div class="pb-table-col-aligns">';
        headers.forEach(function(h, hi) {
          var colVal = colAligns[hi] || '';
          html += '<select data-col-align="' + hi + '" class="pb-field__select pb-field__select--compact">';
          html += '<option value=""' + (colVal === '' ? ' selected' : '') + '>—</option>';
          html += '<option value="left"' + (colVal === 'left' ? ' selected' : '') + '>' + (PB_I18N.left || 'Left') + '</option>';
          html += '<option value="center"' + (colVal === 'center' ? ' selected' : '') + '>' + (PB_I18N.center || 'Center') + '</option>';
          html += '<option value="right"' + (colVal === 'right' ? ' selected' : '') + '>' + (PB_I18N.right || 'Right') + '</option>';
          html += '<option value="justify"' + (colVal === 'justify' ? ' selected' : '') + '>' + (PB_I18N.justify || 'Justify') + '</option>';
          html += '</select>';
        });
        html += '</div></div>';
        // Per-column width (percentage)
        var colWidths = d.column_widths || [];
        html += '<div class="pb-field"><label class="pb-field__label">' + (PB_I18N.column_width || 'Column width') + '</label>';
        html += '<div class="pb-table-col-aligns">';
        headers.forEach(function(h, hi) {
          var widthVal = colWidths[hi] || '';
          html += '<input type="text" data-col-width="' + hi + '" class="pb-field__input pb-field__select--compact" value="' + esc(widthVal) + '" placeholder="auto" style="text-align:center">';
        });
        html += '</div>';
        var widthSum = colWidths.reduce(function(s, w) { return s + (parseInt(w) || 0); }, 0);
        var sumClass = widthSum > 100 ? 'pb-text-hint pb-hint--pull-sm" style="color:var(--pb-danger)' : 'pb-text-hint pb-hint--pull-sm';
        html += '<p class="' + sumClass + '" id="pbColWidthSum">' + (widthSum > 0 ? (PB_I18N.column_width_hint || '%') + ' · ' + (PB_I18N.total || 'Total') + ': ' + widthSum + '%' + (widthSum > 100 ? ' ⚠' : '') : (PB_I18N.column_width_hint || '%')) + '</p>';
        html += '</div></div>';
        break;
      case 'section':
        html += fieldSelect((PB_I18N.bg_light_mode || 'Light mode background'), 'data.background_type', d.background_type || 'color', [
          {v: 'none', l: PB_I18N.bg_none}, {v: 'color', l: PB_I18N.bg_color}, {v: 'gradient', l: PB_I18N.bg_gradient}, {v: 'image', l: PB_I18N.bg_image}
        ]);
        var bgType = d.background_type || 'color';
        if (bgType === 'color') {
          var colorVal = d.background_value || '';
          html += '<div class="pb-field"><label class="pb-field__label">' + (PB_I18N.bg_light_color_pick || 'Pick light mode color') + '</label>' +
            '<div class="pb-panel-header pb-panel-header--flush">' +
            '<input type="color" data-key="data.background_value" value="' + esc(colorVal || '#ececec') + '" class="pb-color-swatch">' +
            '<input class="pb-field__input pb-flex-1" type="text" data-key="data.background_value" value="' + esc(colorVal) + '" placeholder="' + (PB_I18N.bg_theme_default || 'Theme default') + '">' +
            '</div></div>';
        } else if (bgType === 'gradient') {
          html += fieldGradient((PB_I18N.bg_light_gradient_value || 'Light mode gradient'), 'data.background_value', d.background_value || '');
        } else if (bgType === 'image') {
          html += fieldImageUrl((PB_I18N.bg_light_image_url || 'Light mode image URL'), 'data.background_value', d.background_value || '');
          html += '<p class="pb-text-hint pb-hint--pull-md">' + (PB_I18N.bg_image_hint || 'Recommended: 1920×1080px or wider. Image will cover full section.') + '</p>';
          html += fieldColorAlpha((PB_I18N.bg_light_overlay || 'Light mode overlay'), 'data.background_overlay', d.background_overlay || '', 'rgba(0,0,0,0.5)');
        }
        html += fieldSelect((PB_I18N.bg_dark_mode || 'Dark mode background'), 'data.background_type_dark', d.background_type_dark || 'color', [
          {v: 'none', l: PB_I18N.bg_none}, {v: 'color', l: PB_I18N.bg_color}, {v: 'gradient', l: PB_I18N.bg_gradient}, {v: 'image', l: PB_I18N.bg_image}
        ]);
        var bgTypeDark = d.background_type_dark || 'color';
        if (bgTypeDark === 'color') {
          var colorValDark = d.background_value_dark || '';
          html += '<div class="pb-field"><label class="pb-field__label">' + (PB_I18N.bg_dark_color_pick || 'Pick dark mode color') + '</label>' +
            '<div class="pb-panel-header pb-panel-header--flush">' +
            '<input type="color" data-key="data.background_value_dark" value="' + esc(colorValDark || '#1e293b') + '" class="pb-color-swatch">' +
            '<input class="pb-field__input pb-flex-1" type="text" data-key="data.background_value_dark" value="' + esc(colorValDark) + '" placeholder="' + (PB_I18N.bg_theme_default || 'Theme default') + '">' +
            '</div></div>';
        } else if (bgTypeDark === 'gradient') {
          html += fieldGradient((PB_I18N.bg_dark_gradient_value || 'Dark mode gradient'), 'data.background_value_dark', d.background_value_dark || '');
        } else if (bgTypeDark === 'image') {
          html += fieldImageUrl((PB_I18N.bg_dark_image_url || 'Dark mode image URL'), 'data.background_value_dark', d.background_value_dark || '');
          html += fieldColorAlpha((PB_I18N.bg_dark_overlay || 'Dark mode overlay'), 'data.background_overlay_dark', d.background_overlay_dark || '', 'rgba(0,0,0,0.5)');
        }
        html += '<div class="pb-panel-divider">';
        html += fieldSelect(PB_I18N.padding, 'data.padding', d.padding || 'lg', [
          {v: 'sm', l: PB_I18N.padding_sm + ' (32px)'}, {v: 'md', l: PB_I18N.padding_md + ' (56px)'}, {v: 'lg', l: PB_I18N.padding_lg + ' (96px)'}, {v: 'xl', l: PB_I18N.padding_xl + ' (140px)'}
        ]);
        html += fieldSelect((PB_I18N.text_color || 'Text color'), 'data.text_color', d.text_color || 'auto', [
          {v: 'auto', l: PB_I18N.text_color_auto || 'Auto (theme)'}, {v: 'light', l: PB_I18N.text_color_light || 'Light'}, {v: 'dark', l: PB_I18N.text_color_dark || 'Dark'}
        ]);
        html += fieldSelect((PB_I18N.min_height || 'Min height'), 'data.min_height', d.min_height || 'auto', [
          {v: 'auto', l: (PB_I18N.min_height_auto || 'Auto')}, {v: '300px', l: '300px'}, {v: '50vh', l: (PB_I18N.min_height_50_screen || '50% screen')}, {v: '75vh', l: (PB_I18N.min_height_75_screen || '75% screen')}, {v: '100vh', l: (PB_I18N.min_height_full_screen || 'Full screen')}
        ]);
        html += '</div>';
        html += '<div class="pb-panel-divider">';
        html += '<div class="pb-sidebar__title pb-mb-2">' + (PB_I18N.section_layout || 'Section Layout') + '</div>';
        html += '<label class="pb-toggle pb-mb-2"><input type="checkbox" data-key="data.full_width"' + (d.full_width ? ' checked' : '') + '> ' + (PB_I18N.section_full_width || 'Full width (edge-to-edge background)') + '</label>';
        html += fieldSelect((PB_I18N.section_content_width || 'Content width'), 'data.content_width', d.content_width || 'default', [
          {v: 'narrow', l: PB_I18N.section_width_narrow || 'Narrow (768px)'},
          {v: 'default', l: PB_I18N.section_width_default || 'Default (page width)'}
        ]);
        html += '<p class="pb-text-hint pb-hint--pull-section">' + (PB_I18N.section_full_width_hint || 'Full width stretches the background to the screen edges. Content width controls the inner area.') + '</p>';
        html += '</div>';
        break;
      case 'gallery':
        html += '<p class="pb-text-hint pb-mb-2 pb-panel-count">' + (d.images||[]).length + ' ' + PB_I18N.gallery_images + '</p>';
        // Visual image grid with delete buttons
        if ((d.images || []).length > 0) {
          html += '<div class="pb-gallery-grid pb-mb-2 pb-panel-grid-3">';
          (d.images || []).forEach(function(img, i) {
            html += '<div class="pb-video-wrap--sm">';
            if (img.url) {
              html += '<div class="pb-aspect-square"><img src="' + esc(img.url) + '" alt="' + esc(img.alt || '') + '" class="pb-cover-img"></div>';
            } else {
              html += '<div class="pb-aspect-square pb-panel-empty-cell">No URL</div>';
            }
            // Show alt from library
            if (img.alt) {
              html += '<div class="pb-thumb-label" title="' + esc(img.alt) + '">alt: ' + esc(img.alt) + '</div>';
            }
            html += '<button class="pb-gallery-remove pb-thumb-remove" data-gallery-remove="' + i + '" title="' + (PB_I18N.remove || 'Remove') + '">&times;</button>';
            html += '</div>';
          });
          html += '</div>';
        }
        // Add from library button
        html += '<button type="button" class="pb-gallery-pick-btn pb-dashed-add">📷 ' + (PB_I18N.gallery_add_from_library || 'Add from Library') + '</button>';
        html += fieldSelect(PB_I18N.gallery_columns, 'data.columns', d.columns || 3, [
          {v: 1, l: '1'}, {v: 2, l: '2'}, {v: 3, l: '3'}, {v: 4, l: '4'}
        ]);
        break;
      case 'video_gallery':
        html += '<p class="pb-text-hint pb-mb-2 pb-panel-count">' + (d.videos||[]).length + ' ' + (PB_I18N.video_gallery_videos || 'videos') + '</p>';
        if ((d.videos || []).length > 0) {
          html += '<div class="pb-panel-grid-2 pb-mb-2">';
          (d.videos || []).forEach(function(vid, i) {
            var thumbUrl = '';
            if (vid.embed_id && vid.source) {
              thumbUrl = videoThumbUrl(vid.source, vid.embed_id);
            }
            html += '<div class="pb-video-wrap--sm">';
            if (thumbUrl) {
              html += '<div class="pb-aspect-video"><img src="' + thumbUrl + '" alt="' + esc(vid.title || '') + '" class="pb-cover-img"></div>';
            } else {
              html += '<div class="pb-aspect-video pb-video-placeholder pb-video-placeholder--sm">' + (vid.source === 'vimeo' ? 'Vimeo' : '▶') + '</div>';
            }
            html += '<div class="pb-overlay-center--no-events"><div class="pb-play-btn pb-play-btn--xs">▶</div></div>';
            if (vid.title) {
              html += '<div class="pb-thumb-label" title="' + esc(vid.title) + '">' + esc(vid.title) + '</div>';
            }
            html += '<button class="pb-vgal-remove pb-thumb-remove" data-vgal-remove="' + i + '" title="' + (PB_I18N.remove || 'Remove') + '">&times;</button>';
            html += '</div>';
          });
          html += '</div>';
        }
        html += '<button type="button" class="pb-vgal-add-btn pb-dashed-add">+ ' + (PB_I18N.video_gallery_add || 'Add video') + '</button>';
        html += fieldSelect((PB_I18N.gallery_columns || 'Columns'), 'data.columns', d.columns || 2, [
          {v: 1, l: '1'}, {v: 2, l: '2'}, {v: 3, l: '3'}
        ]);
        break;
      case 'slideshow':
        // --- Mode selector ---
        html += fieldSelect((PB_I18N.slideshow_mode || 'Mode'), 'data.mode', d.mode || 'hero', [
          {v: 'hero', l: PB_I18N.slideshow_mode_hero || 'Hero (image + content)'},
          {v: 'image', l: PB_I18N.slideshow_mode_image || 'Image only'}
        ]);
        // --- Visual settings (matching section order) ---
        // Padding
        html += fieldSelect(PB_I18N.padding, 'data.padding', d.padding || 'lg', [
          {v: 'sm', l: PB_I18N.padding_sm + ' (32px)'}, {v: 'md', l: PB_I18N.padding_md + ' (56px)'}, {v: 'lg', l: PB_I18N.padding_lg + ' (96px)'}, {v: 'xl', l: PB_I18N.padding_xl + ' (140px)'}
        ]);
        // Text color
        html += fieldSelect((PB_I18N.text_color || 'Text color'), 'data.text_color', d.text_color || 'light', [
          {v: 'auto', l: PB_I18N.text_color_auto || 'Auto (theme)'}, {v: 'light', l: PB_I18N.text_color_light || 'Light'}, {v: 'dark', l: PB_I18N.text_color_dark || 'Dark'}
        ]);
        // Overlay
        html += fieldColorAlpha((PB_I18N.slideshow_overlay || 'Overlay'), 'data.overlay', d.overlay || '', 'rgba(0,0,0,0.4)');
        // --- Layout (matching section order) ---
        html += '<div class="pb-panel-divider">';
        html += '<div class="pb-sidebar__title pb-mb-2">' + (PB_I18N.section_layout || 'Layout') + '</div>';
        html += '<label class="pb-toggle pb-mb-2"><input type="checkbox" data-key="data.full_width"' + (d.full_width ? ' checked' : '') + '> ' + (PB_I18N.section_full_width || 'Full width (edge-to-edge)') + '</label>';
        html += fieldSelect((PB_I18N.section_content_width || 'Content width'), 'data.content_width', d.content_width || 'default', [
          {v: 'narrow', l: PB_I18N.section_width_narrow || 'Narrow (768px)'},
          {v: 'default', l: PB_I18N.section_width_default || 'Default (page width)'}
        ]);
        html += '</div>';
        // --- Slideshow-specific ---
        html += '<div class="pb-panel-divider">';
        html += '<div class="pb-sidebar__title pb-mb-2">' + (PB_I18N.slideshow_animation || 'Animation') + '</div>';
        html += fieldSelect((PB_I18N.slideshow_animation || 'Animation'), 'data.animation', d.animation || 'slide', [
          {v: 'slide', l: (PB_I18N.animation_slide || 'Slide')}, {v: 'fade', l: (PB_I18N.animation_fade || 'Fade')}
        ]);
        html += fieldSelect((PB_I18N.aspect_ratio || 'Aspect ratio'), 'data.aspect_ratio', d.aspect_ratio || '16/9', [
          {v: '16/9', l: '16:9'}, {v: '4/3', l: '4:3'}, {v: '21/9', l: (PB_I18N.ratio_cinematic || '21:9 (cinematic)')}, {v: '1/1', l: '1:1'}, {v: 'auto', l: (PB_I18N.ratio_auto || 'Auto (content height)')}
        ]);
        html += '<label class="pb-toggle pb-mb-2"><input type="checkbox" data-key="data.autoplay"' + (d.autoplay !== false ? ' checked' : '') + '> ' + (PB_I18N.slideshow_autoplay || 'Autoplay') + '</label>';
        html += field((PB_I18N.slideshow_interval || 'Interval (seconds)'), 'number', 'data.interval', d.interval || 5);
        html += '<label class="pb-toggle pb-mb-2"><input type="checkbox" data-key="data.show_arrows"' + (d.show_arrows !== false ? ' checked' : '') + '> ' + (PB_I18N.slideshow_arrows || 'Show arrows') + '</label>';
        html += '<label class="pb-toggle pb-mb-2"><input type="checkbox" data-key="data.show_dots"' + (d.show_dots !== false ? ' checked' : '') + '> ' + (PB_I18N.slideshow_dots || 'Show dots') + '</label>';
        html += '</div>';
        // --- Slides ---
        html += '<div class="pb-panel-divider">';
        html += '<div class="pb-sidebar__title pb-mb-2">' + (PB_I18N.slideshow_slides || 'Slides') + ' (' + (d.slides||[]).length + ')</div>';
        (d.slides || []).forEach(function(slide, i) {
          html += '<div class="pb-item-card">';
          html += '<div class="pb-text-hint--label">Slide ' + (i+1) + '</div>';
          html += fieldImageUrl((PB_I18N.image_url || 'Image'), 'data.slides.' + i + '.image', slide.image || '', '');
          html += '<input type="hidden" data-key="data.slides.' + i + '.alt" value="' + esc(slide.alt || '') + '">';
          if (slide.image) {
            html += '<div class="pb-slide-preview-wrap"><img src="' + esc(slide.image) + '" class="pb-preview-img--sm" alt=""></div>';
            if (slide.alt) {
              html += '<div class="pb-text-hint--sm pb-slide-alt-hint">alt: ' + esc(slide.alt) + '</div>';
            }
          }
          html += '<button class="pb-btn pb-btn--danger pb-btn--sm" data-remove-item="slideshow" data-index="' + i + '">' + (PB_I18N.slideshow_remove_slide || 'Remove Slide') + '</button>';
          html += '</div>';
        });
        html += '<button class="pb-btn pb-btn--outline pb-btn--sm pb-mt-1" data-add-item="slideshow">+ ' + (PB_I18N.slideshow_add_slide || 'Add slide') + '</button>';
        html += '</div>';
        break;
      case 'form':
        if (d.form_id && (d._form_title_raw || d._fields)) {
          var formCardTitle = d._form_title_raw || (PB_I18N.form || 'Form') + ' #' + d.form_id;
          var formCardMeta = d._form_title || '';
          html += '<div class="pb-field"><label class="pb-field__label">' + PB_I18N.form_select + '</label>';
          html += '<div class="pb-panel-form-card">';
          html += '<div class="pb-panel-form-card__info">';
          html += '<div class="pb-panel-form-card__title">' + esc(formCardTitle) + '</div>';
          html += '<div class="pb-panel-form-card__meta">' + esc(formCardMeta) + '</div>';
          html += '</div>';
          html += '<div class="pb-panel-form-card__actions">';
          html += '<button type="button" class="pb-browse-btn" id="pbFormChangeBtn" title="' + esc(PB_I18N.form_select || 'Change') + '">📋</button>';
          html += '<button type="button" class="pb-icon-btn" id="pbFormClearBtn" title="' + esc(PB_I18N.remove || 'Remove') + '">✕</button>';
          html += '</div></div></div>';
        } else {
          html += '<div class="pb-field"><label class="pb-field__label">' + PB_I18N.form_select + '</label>';
          html += '<button type="button" class="pb-btn pb-btn--outline pb-btn--sm pb-btn--full" id="pbFormPickBtn">📋 ' + esc(PB_I18N.form_select || 'Select form') + '</button>';
          html += '</div>';
        }
        html += '<label class="pb-toggle"><input type="checkbox" data-key="data.show_title"' + (d.show_title !== false ? ' checked' : '') + '> ' + PB_I18N.form_show_title + '</label>';
        html += '<label class="pb-toggle"><input type="checkbox" data-key="data.show_description"' + (d.show_description !== false ? ' checked' : '') + '> ' + PB_I18N.form_show_description + '</label>';
        break;
      case 'ticker':
        html += '<p class="pb-text-hint pb-mb-2 pb-panel-count">' + (d.images||[]).length + ' ' + PB_I18N.ticker_images + '</p>';
        if ((d.images || []).length > 0) {
          html += '<div class="pb-gallery-grid pb-mb-2 pb-panel-grid-3">';
          (d.images || []).forEach(function(img, i) {
            html += '<div class="pb-video-wrap--sm">';
            if (img.url) {
              html += '<div class="pb-aspect-square"><img src="' + esc(img.url) + '" alt="' + esc(img.alt || '') + '" class="pb-cover-img"></div>';
            }
            html += '<button class="pb-gallery-remove pb-thumb-remove" data-ticker-remove="' + i + '" title="' + (PB_I18N.remove || 'Remove') + '">&times;</button>';
            html += '</div>';
          });
          html += '</div>';
        }
        html += '<button type="button" class="pb-ticker-pick-btn pb-dashed-add">📷 ' + (PB_I18N.gallery_add_from_library || 'Add from Library') + '</button>';
        html += fieldSelect(PB_I18N.ticker_speed, 'data.speed', d.speed || 'medium', [
          {v: 'slow', l: PB_I18N.ticker_slow}, {v: 'medium', l: PB_I18N.ticker_medium}, {v: 'fast', l: PB_I18N.ticker_fast}
        ]);
        html += fieldSelect((PB_I18N.ticker_direction || 'Direction'), 'data.direction', d.direction || 'rtl', [
          {v: 'rtl', l: '← ' + (PB_I18N.ticker_dir_rtl || 'Right to left')}, {v: 'ltr', l: '→ ' + (PB_I18N.ticker_dir_ltr || 'Left to right')}
        ]);
        html += fieldSelect(PB_I18N.ticker_height, 'data.height', d.height || 'md', [
          {v: 'sm', l: 'S'}, {v: 'md', l: 'M'}, {v: 'lg', l: 'L'}, {v: 'xl', l: 'XL'}
        ]);
        html += '<label class="pb-toggle"><input type="checkbox" data-key="data.pause_on_hover"' + (d.pause_on_hover !== false ? ' checked' : '') + '> ' + PB_I18N.ticker_pause_hover + '</label>';
        html += '<label class="pb-toggle"><input type="checkbox" data-key="data.grayscale"' + (d.grayscale !== false ? ' checked' : '') + '> ' + PB_I18N.ticker_grayscale + '</label>';
        break;
    }

    // Common settings
    html += '<div class="pb-panel-divider--lg">';
    html += '<div class="pb-sidebar__title pb-mb-2">' + PB_I18N.layout + '</div>';
    // Block Position — available for all blocks
    html += fieldSelect((PB_I18N.block_position || 'Block Position'), 'settings.alignment', s.alignment || 'center', [
      {v: 'left', l: PB_I18N.left}, {v: 'center', l: PB_I18N.center}, {v: 'right', l: PB_I18N.right}
    ]);
    // Text Alignment — hide for blocks that have no text content
    var noTextAlign = ['slideshow', 'button', 'image', 'divider', 'spacer', 'video', 'gallery', 'video_gallery', 'ticker', 'table'];
    if (noTextAlign.indexOf(block.type) === -1) {
      html += fieldSelect((PB_I18N.content_alignment || 'Text Alignment'), 'settings.content_alignment', s.content_alignment || '', [
        {v: '', l: (PB_I18N.alignment_auto || '— Auto')}, {v: 'left', l: PB_I18N.left}, {v: 'center', l: PB_I18N.center}, {v: 'right', l: PB_I18N.right}, {v: 'justify', l: PB_I18N.justify}
      ]);
    }
    html += '<div class="pb-field__row">';
    html += fieldSelect(PB_I18N.top_margin, 'settings.margin_top', s.margin_top || 'none', [
      {v: 'none', l: PB_I18N.none}, {v: 'sm', l: 'S'}, {v: 'md', l: 'M'}, {v: 'lg', l: 'L'}, {v: 'xl', l: 'XL'}
    ]);
    html += fieldSelect(PB_I18N.bottom_margin, 'settings.margin_bottom', s.margin_bottom || 'none', [
      {v: 'none', l: PB_I18N.none}, {v: 'sm', l: 'S'}, {v: 'md', l: 'M'}, {v: 'lg', l: 'L'}, {v: 'xl', l: 'XL'}
    ]);
    html += '</div>';
    // Padding — not for section/slideshow (they have their own)
    var noPadding = ['section', 'slideshow'];
    if (noPadding.indexOf(block.type) === -1) {
      html += fieldSelect((PB_I18N.padding || 'Padding'), 'settings.padding', s.padding || 'none', [
        {v: 'none', l: PB_I18N.none}, {v: 'sm', l: 'S'}, {v: 'md', l: 'M'}, {v: 'lg', l: 'L'}, {v: 'xl', l: 'XL'}
      ]);
    }
    // Visibility — show on all devices, desktop only, or mobile only
    html += fieldSelect((PB_I18N.visibility || 'Visibility'), 'settings.visibility', s.visibility || '', [
      {v: '', l: (PB_I18N.visibility_all || 'All devices')}, {v: 'desktop', l: (PB_I18N.visibility_desktop || 'Desktop only')}, {v: 'mobile', l: (PB_I18N.visibility_mobile || 'Mobile only')}
    ]);
    // Scroll animation — not for slideshow (has its own animation)
    if (block.type !== 'slideshow') {
      html += fieldSelect((PB_I18N.scroll_animation || 'Scroll animation'), 'settings.animation', s.animation || 'none', [
        {v: 'none', l: PB_I18N.none}, {v: 'fade-in', l: (PB_I18N.animation_fade_in || 'Fade in')}, {v: 'slide-up', l: (PB_I18N.animation_slide_up || 'Slide up')}
      ]);
    }
    html += field(PB_I18N.css_class, 'input', 'settings.css_class', s.css_class || '');
    html += field(PB_I18N.anchor_id, 'input', 'settings.anchor_id', s.anchor_id || '');
    html += '</div>';

    return html;
  }

  // Field builders
  function field(label, type, key, value, placeholder) {
    if (type === 'number') {
      return '<div class="pb-field"><label class="pb-field__label">' + label + '</label>' +
        '<div class="pb-stepper">' +
        '<input class="pb-field__input" type="number" data-key="' + key + '" value="' + esc(value) + '"' +
        (placeholder ? ' placeholder="' + esc(placeholder) + '"' : '') + '>' +
        '<div class="pb-stepper__btns">' +
        '<button type="button" class="pb-stepper__btn" data-step-up="' + key + '">▲</button>' +
        '<button type="button" class="pb-stepper__btn" data-step-down="' + key + '">▼</button>' +
        '</div></div></div>';
    }
    return '<div class="pb-field"><label class="pb-field__label">' + label + '</label>' +
      '<input class="pb-field__input" type="' + (type||'text') + '" data-key="' + key + '" value="' + esc(value) + '"' +
      (placeholder ? ' placeholder="' + esc(placeholder) + '"' : '') + '></div>';
  }

  function fieldColorAlpha(label, key, value, placeholder) {
    // Parse rgba or hex value
    var hex = '#000000';
    var alpha = 50;
    if (value) {
      var rgbaMatch = value.match(/rgba?\(\s*(\d+),\s*(\d+),\s*(\d+)(?:,\s*([\d.]+))?\s*\)/);
      if (rgbaMatch) {
        var r = parseInt(rgbaMatch[1]), g = parseInt(rgbaMatch[2]), b = parseInt(rgbaMatch[3]);
        alpha = rgbaMatch[4] !== undefined ? Math.round(parseFloat(rgbaMatch[4]) * 100) : 100;
        hex = '#' + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1);
      } else if (value.startsWith('#')) {
        hex = value;
        alpha = 100;
      }
    }
    return '<div class="pb-field"><label class="pb-field__label">' + label + '</label>' +
      '<div class="pb-color-alpha">' +
      '<div class="pb-color-alpha__row">' +
      '<input type="color" class="pb-color-alpha__swatch" data-ca-color="' + key + '" value="' + hex + '">' +
      '<input type="range" class="pb-color-alpha__slider" data-ca-alpha="' + key + '" min="0" max="100" value="' + alpha + '">' +
      '<span class="pb-color-alpha__label" data-ca-label="' + key + '">' + alpha + '%</span>' +
      '</div>' +
      '<input class="pb-field__input pb-text-hint" type="text" data-key="' + key + '" value="' + esc(value) + '" placeholder="' + esc(placeholder || 'rgba(0,0,0,0.5)') + '">' +
      '</div></div>';
  }

  var GRADIENT_PRESETS = [
    'linear-gradient(135deg, #1a1a2e, #16213e)',
    'linear-gradient(135deg, #0f0c29, #302b63, #24243e)',
    'linear-gradient(135deg, #2d1b69, #11998e)',
    'linear-gradient(135deg, #fc5c7d, #6a82fb)',
    'linear-gradient(135deg, #f12711, #f5af19)',
    'linear-gradient(135deg, #00b09b, #96c93d)',
    'linear-gradient(135deg, #141e30, #243b55)',
    'linear-gradient(135deg, #ff9a9e, #fecfef)',
    'linear-gradient(135deg, #a18cd1, #fbc2eb)',
    'linear-gradient(135deg, #667eea, #764ba2)'
  ];

  function fieldGradient(label, key, value) {
    var html = '<div class="pb-field"><label class="pb-field__label">' + label + '</label>';
    html += '<div class="pb-gradient-presets">';
    GRADIENT_PRESETS.forEach(function(g) {
      var active = value === g ? ' is-active' : '';
      html += '<button type="button" class="pb-gradient-preset' + active + '" data-gradient-pick="' + key + '" data-gradient-val="' + esc(g) + '" style="background:' + g + '"></button>';
    });
    html += '</div>';
    html += '<input class="pb-field__input pb-text-hint" type="text" data-key="' + key + '" value="' + esc(value) + '" placeholder="linear-gradient(135deg, #color1, #color2)">';
    html += '</div>';
    return html;
  }

  function fieldSelect(label, key, value, options) {
    var opts = options.map(function(o) {
      return '<option value="' + o.v + '"' + (String(o.v) === String(value) ? ' selected' : '') + '>' + o.l + '</option>';
    }).join('');
    return '<div class="pb-field"><label class="pb-field__label">' + label + '</label>' +
      '<select class="pb-field__select" data-key="' + key + '">' + opts + '</select></div>';
  }

  function fieldTextarea(label, key, value) {
    return '<div class="pb-field"><label class="pb-field__label">' + label + '</label>' +
      '<textarea class="pb-field__textarea" data-key="' + key + '">' + esc(value) + '</textarea></div>';
  }

  function fieldImageUrl(label, key, value, placeholder) {
    var hasValue = value && value.trim();
    return '<div class="pb-field"><label class="pb-field__label">' + label + '</label>' +
      '<div class="pb-field-image-row">' +
      '<input class="pb-field__input pb-flex-1 pb-input--sm" type="text" data-key="' + key + '" value="' + esc(value) + '"' +
      ' readonly style="cursor:default;opacity:' + (hasValue ? '1' : '0.5') + '"' +
      ' placeholder="' + esc(PB_I18N.choose_image || 'Select from library') + '">' +
      '<button type="button" class="pb-pick-image-btn pb-browse-btn" data-target-key="' + key + '" title="' + (PB_I18N.choose_image || 'Choose') + '">📷</button>' +
      (hasValue ? '<button type="button" class="pb-clear-image-btn pb-icon-btn" data-clear-key="' + key + '" title="' + (PB_I18N.remove || 'Remove') + '">✕</button>' : '') +
      '</div></div>';
  }

  // Image picker modal for Page Builder — shell in the builder's own pb-*
  // classes, logic from the shared AdminImagePicker core, so search and
  // pagination behave exactly like everywhere else in the admin.
  function pbPickImage(targetKey) {
    var modal = document.createElement('div');
    modal.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,0.7);z-index:10000;display:flex;align-items:center;justify-content:center';

    var box = document.createElement('div');
    box.style.cssText = 'background:var(--pb-surface);border-radius:12px;padding:24px;max-width:900px;max-height:80vh;overflow-y:auto;width:90%;color:var(--pb-text)';
    box.innerHTML =
      '<div class="pb-panel-header">' +
      '<h3 class="pb-heading-md">' + (PB_I18N.choose_image || 'Select Image') + '</h3>' +
      '<button onclick="this.closest(\'[style*=inset]\').remove()" class="pb-modal-close">&times;</button>' +
      '</div>' +
      '<div class="pb-field">' +
      '<input type="text" class="pb-field__input" autocomplete="off" placeholder="🔍 ' +
      esc(PB_I18N.picker_search_placeholder || 'Search images…') + '">' +
      '</div>' +
      '<div class="pb-gallery-grid"></div>' +
      '<div class="pb-field-image-row pb-mt-2"></div>';

    modal.appendChild(box);
    document.body.appendChild(modal);
    modal.addEventListener('click', function(e) { if (e.target === modal) modal.remove(); });

    window.AdminImagePicker.attach({
      searchInput: box.querySelector('.pb-field__input'),
      gridEl: box.querySelector('.pb-gallery-grid'),
      pagerEl: box.querySelector('.pb-field-image-row'),
      i18n: { prev: PB_I18N.picker_prev, next: PB_I18N.picker_next },
      renderLoading: function() {
        return '<div class="pb-empty-msg">' + esc(PB_I18N.picker_loading || 'Loading…') + '</div>';
      },
      renderEmpty: function(hasQuery) {
        if (hasQuery) {
          return '<div class="pb-empty-msg"><p>' +
            esc(PB_I18N.picker_no_results || 'No images match your search') + '</p></div>';
        }
        return '<div class="pb-empty-msg">' +
          '<p>{{ t("media.no_images_picker") }}</p><a href="/admin/media/upload" target="_blank" class="pb-color-primary">{{ t("media.upload_first_link") }}</a></div>';
      },
      renderError: function() {
        return '<div class="pb-empty-msg">' + esc(PB_I18N.picker_error || 'Error loading images.') + '</div>';
      },
      renderItem: function(img, idx) {
        return '<div class="pb-media-card" data-pick-idx="' + idx + '" ' +
          'onmouseenter="this.style.borderColor=\'var(--pb-primary)\'" onmouseleave="this.style.borderColor=\'var(--pb-border)\'">' +
          '<img src="' + esc(img.thumb || img.url) + '" alt="' + esc(img.alt_text || '') + '" loading="lazy" class="pb-cover-img--constrained">' +
          '<div class="pb-vg-title">' + esc(img.alt_text || img.filename || '') + '</div>' +
          /* File name under the alt line — people search by the name they
             uploaded, not by the exact alt text. Hidden when it would just
             repeat the title line (alt empty -> title already shows it). */
          (function () {
            var fn = img.original_filename || img.filename || '';
            var titleLine = img.alt_text || img.filename || '';
            return (fn && fn !== titleLine)
              ? '<div class="pb-text-hint" style="overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="' + esc(fn) + '">' + esc(fn) + '</div>'
              : '';
          })() +
          '</div>';
      },
      pager: {
        nav: function(page, label) {
          return '<button type="button" class="pb-btn pb-btn--sm" data-pick-page="' + page + '">' + esc(label) + '</button>';
        },
        current: function(page) {
          return '<button type="button" class="pb-btn pb-btn--sm pb-btn--primary" disabled>' + page + '</button>';
        },
        gap: function() { return '<span class="pb-text-hint">…</span>'; }
      },
      onPick: function(img) {
        var url = img.url || '';
        var alt = img.alt_text || '';
        // Set value in the panel input
        var input = $panelBody.querySelector('[data-key="' + targetKey + '"]');
        if (input) {
          input.value = url;
          input.dispatchEvent(new Event('input', { bubbles: true }));
        }
        // Also set alt if there's an alt field
        // Works for: data.alt (image block), data.slides.N.alt (slideshow)
        var altKey = targetKey.replace(/\.image$/, '.alt').replace(/\.url$/, '.alt');
        if (altKey === targetKey) altKey = 'data.alt'; // fallback for image block
        var altInput = $panelBody.querySelector('[data-key="' + altKey + '"]');
        if (altInput && alt) {
          altInput.value = alt;
          altInput.dispatchEvent(new Event('input', { bubbles: true }));
        }
        modal.remove();
        // Re-open panel to refresh preview and alt badge
        if (state.selectedId) {
          var selBlock = findBlockDeep(state.selectedId);
          if (selBlock) setTimeout(function() { openPanel(selBlock); }, 50);
        }
      }
    });
  }

  // Gallery multi-select image picker — same shared core in multiple mode.
  // Selection survives page changes and search, so admins can collect
  // images from several pages before pressing "Add selected".
  function pbPickGalleryImages(block) {
    var modal = document.createElement('div');
    modal.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,0.7);z-index:10000;display:flex;align-items:center;justify-content:center';

    var box = document.createElement('div');
    box.style.cssText = 'background:var(--pb-surface);border-radius:12px;padding:24px;max-width:900px;max-height:80vh;overflow-y:auto;width:90%;color:var(--pb-text)';
    box.innerHTML =
      '<div class="pb-panel-header">' +
      '<h3 class="pb-heading-md">' + (PB_I18N.gallery_pick_title || 'Select Images') + ' <span id="pbGalPickCount" class="pb-text-hint pb-gal-pick-count">(0 selected)</span></h3>' +
      '<div class="pb-panel-header pb-panel-header--flush">' +
      '<button id="pbGalPickDone" class="pb-submit-primary">' + (PB_I18N.gallery_add_selected || 'Add selected') + '</button>' +
      '<button onclick="this.closest(\'[style*=inset]\').remove()" class="pb-modal-close pb-ms-3">&times;</button>' +
      '</div></div>' +
      '<div class="pb-field">' +
      '<input type="text" class="pb-field__input" autocomplete="off" placeholder="🔍 ' +
      esc(PB_I18N.picker_search_placeholder || 'Search images…') + '">' +
      '</div>' +
      '<div id="pbGalGrid" class="pb-gallery-grid"></div>' +
      '<div class="pb-field-image-row pb-mt-2"></div>';

    modal.appendChild(box);
    document.body.appendChild(modal);
    modal.addEventListener('click', function(e) { if (e.target === modal) modal.remove(); });

    var countEl = box.querySelector('#pbGalPickCount');
    var doneBtn = box.querySelector('#pbGalPickDone');

    var picker = window.AdminImagePicker.attach({
      multiple: true,
      searchInput: box.querySelector('.pb-field__input'),
      gridEl: box.querySelector('#pbGalGrid'),
      pagerEl: box.querySelector('.pb-field-image-row'),
      i18n: { prev: PB_I18N.picker_prev, next: PB_I18N.picker_next },
      renderLoading: function() {
        return '<div class="pb-empty-msg">' + esc(PB_I18N.picker_loading || 'Loading…') + '</div>';
      },
      renderEmpty: function(hasQuery) {
        if (hasQuery) {
          return '<div class="pb-empty-msg"><p>' +
            esc(PB_I18N.picker_no_results || 'No images match your search') + '</p></div>';
        }
        return '<div class="pb-empty-msg">' +
          '<p>{{ t("media.no_images_picker") }}</p><a href="/admin/media/upload" target="_blank" class="pb-color-primary">{{ t("media.upload_first_link") }}</a></div>';
      },
      renderError: function() {
        return '<div class="pb-empty-msg">' + esc(PB_I18N.picker_error || 'Error loading images.') + '</div>';
      },
      renderItem: function(img, idx) {
        return '<div class="pb-gal-pick-card" data-pick-idx="' + idx + '">' +
          '<img src="' + esc(img.thumb || img.url) + '" alt="' + esc(img.alt_text || '') + '" loading="lazy" class="pb-cover-img--constrained">' +
          '<div class="pb-vg-title">' + esc(img.alt_text || '') + '</div>' +
          '<div class="pb-gal-check pb-media-check">✓</div>' +
          '</div>';
      },
      pager: {
        nav: function(page, label) {
          return '<button type="button" class="pb-btn pb-btn--sm" data-pick-page="' + page + '">' + esc(label) + '</button>';
        },
        current: function(page) {
          return '<button type="button" class="pb-btn pb-btn--sm pb-btn--primary" disabled>' + page + '</button>';
        },
        gap: function() { return '<span class="pb-text-hint">…</span>'; }
      },
      markSelected: function(card, on) {
        card.style.borderColor = on ? 'var(--pb-primary)' : 'var(--pb-border)';
        var check = card.querySelector('.pb-gal-check');
        if (check) check.style.display = on ? 'block' : 'none';
      },
      onToggle: function(img, on, count) {
        countEl.textContent = '(' + count + ' selected)';
      }
    });

    doneBtn.addEventListener('click', function() {
      var newImages = picker.getSelected().map(function(img) {
        return { url: img.url, alt: img.alt_text || '', caption: '' };
      });
      if (!newImages.length) { modal.remove(); return; }
      // Append to existing images
      block.data.images = (block.data.images || []).concat(newImages);
      markDirty();
      openPanel(block);
      renderCanvas();
      modal.remove();
    });
  }


  // Video Gallery: add video via URL input modal with auto-title fetch
  function pbAddVideoToGallery(block) {
    var modal = document.createElement('div');
    modal.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,0.7);z-index:10000;display:flex;align-items:center;justify-content:center';

    var box = document.createElement('div');
    box.style.cssText = 'background:var(--pb-surface);border-radius:12px;padding:24px;max-width:500px;width:90%;color:var(--pb-text)';
    box.innerHTML =
      '<div class="pb-panel-header">' +
      '<h3 class="pb-heading-md">' + (PB_I18N.video_gallery_add_title || 'Add Video') + '</h3>' +
      '<button id="pbVgalClose" class="pb-modal-close">&times;</button>' +
      '</div>' +
      '<div class="pb-mb-3">' +
      '<input id="pbVgalUrl" type="text" placeholder="' + esc(PB_I18N.video_url || 'YouTube or Vimeo URL') + '" class="pb-panel-input">' +
      '<p class="pb-text-hint pb-mt-1">' + esc(PB_I18N.video_url_hint || 'e.g. https://youtube.com/watch?v=...') + '</p>' +
      '</div>' +
      '<div class="pb-mb-3">' +
      '<input id="pbVgalTitle" type="text" placeholder="' + esc(PB_I18N.video_title || 'Title (accessibility)') + '" class="pb-panel-input">' +
      '<p id="pbVgalTitleHint" class="pb-text-hint pb-mt-1 pb-d-none"></p>' +
      '</div>' +
      '<div id="pbVgalPreview" class="pb-d-none pb-mb-4 pb-video-wrap pb-aspect-video">' +
      '<img id="pbVgalThumb" class="pb-cover-img">' +
      '<div class="pb-overlay-center"><div class="pb-play-btn pb-play-btn--md">▶</div></div>' +
      '</div>' +
      '<button id="pbVgalAdd" disabled class="pb-submit-disabled">' + (PB_I18N.video_gallery_add_btn || 'Add') + '</button>';

    modal.appendChild(box);
    document.body.appendChild(modal);
    modal.addEventListener('click', function(e) { if (e.target === modal) modal.remove(); });
    box.querySelector('#pbVgalClose').addEventListener('click', function() { modal.remove(); });

    var urlInput = box.querySelector('#pbVgalUrl');
    var titleInput = box.querySelector('#pbVgalTitle');
    var titleHint = box.querySelector('#pbVgalTitleHint');
    var preview = box.querySelector('#pbVgalPreview');
    var thumb = box.querySelector('#pbVgalThumb');
    var addBtn = box.querySelector('#pbVgalAdd');
    var fetchTimer = null;
    var parsedVideo = null;

    function parseVideoUrl(val) {
      var yt = val.match(/(?:youtube\.com\/watch\?v=|youtu\.be\/)([^&?\s]+)/);
      var vm = val.match(/vimeo\.com\/(\d+)/);
      if (yt) return { embed_id: yt[1], source: 'youtube' };
      if (vm) return { embed_id: vm[1], source: 'vimeo' };
      return null;
    }

    urlInput.addEventListener('input', function() {
      var val = urlInput.value.trim();
      parsedVideo = parseVideoUrl(val);

      if (parsedVideo) {
        addBtn.disabled = false;
        addBtn.style.opacity = '1';

        thumb.src = videoThumbUrl(parsedVideo.source, parsedVideo.embed_id);
        preview.style.display = 'block';

        // Auto-fetch title via our own server (no third-party requests)
        clearTimeout(fetchTimer);
        fetchTimer = setTimeout(function() {
          var metaUrl = '/api/video-meta?source=' + encodeURIComponent(parsedVideo.source)
                      + '&id=' + encodeURIComponent(parsedVideo.embed_id);
          fetch(metaUrl)
            .then(function(r) { return r.json(); })
            .then(function(data) {
              if (data && data.title && !titleInput.value.trim()) {
                titleInput.value = data.title;
                titleHint.textContent = '✓ ' + (PB_I18N.video_gallery_title_fetched || 'Title fetched automatically');
                titleHint.style.display = 'block';
              } else if (data && data.error) {
                titleHint.textContent = PB_I18N.video_meta_fetch_failed || 'Couldn\'t fetch video info';
                titleHint.style.display = 'block';
              }
            })
            .catch(function() {
              titleHint.textContent = PB_I18N.video_meta_fetch_failed || 'Couldn\'t fetch video info';
              titleHint.style.display = 'block';
            });
        }, 400);
      } else {
        addBtn.disabled = true;
        addBtn.style.opacity = '0.5';
        preview.style.display = 'none';
        titleHint.style.display = 'none';
      }
    });

    addBtn.addEventListener('click', function() {
      if (!parsedVideo) return;
      var video = {
        url: urlInput.value.trim(),
        embed_id: parsedVideo.embed_id,
        source: parsedVideo.source,
        title: titleInput.value.trim()
      };
      block.data.videos = (block.data.videos || []).concat([video]);
      markDirty();
      openPanel(block);
      renderCanvas();
      modal.remove();
    });

    setTimeout(function() { urlInput.focus(); }, 100);
  }

  // Single Video: pick video via URL input modal (same UX as video gallery add)
  function pbPickSingleVideo(block) {
    var modal = document.createElement('div');
    modal.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,0.7);z-index:10000;display:flex;align-items:center;justify-content:center';

    var box = document.createElement('div');
    box.style.cssText = 'background:var(--pb-surface);border-radius:12px;padding:24px;max-width:500px;width:90%;color:var(--pb-text)';
    box.innerHTML =
      '<div class="pb-panel-header">' +
      '<h3 class="pb-heading-md">' + (PB_I18N.video_gallery_add_title || 'Add Video') + '</h3>' +
      '<button id="pbSvClose" class="pb-modal-close">&times;</button>' +
      '</div>' +
      '<div class="pb-mb-3">' +
      '<input id="pbSvUrl" type="text" placeholder="' + esc(PB_I18N.video_url || 'YouTube or Vimeo URL') + '" class="pb-panel-input">' +
      '<p class="pb-text-hint pb-mt-1">' + esc(PB_I18N.video_url_hint || 'e.g. https://youtube.com/watch?v=...') + '</p>' +
      '</div>' +
      '<div class="pb-mb-3">' +
      '<input id="pbSvTitle" type="text" placeholder="' + esc(PB_I18N.video_title || 'Title (accessibility)') + '" class="pb-panel-input">' +
      '<p id="pbSvTitleHint" class="pb-text-hint pb-mt-1 pb-d-none"></p>' +
      '</div>' +
      '<div id="pbSvPreview" class="pb-d-none pb-mb-4 pb-video-wrap pb-aspect-video">' +
      '<img id="pbSvThumb" class="pb-cover-img">' +
      '<div class="pb-overlay-center"><div class="pb-play-btn pb-play-btn--md">▶</div></div>' +
      '</div>' +
      '<button id="pbSvAdd" disabled class="pb-submit-disabled">' + (PB_I18N.video_gallery_add_btn || 'Add') + '</button>';

    modal.appendChild(box);
    document.body.appendChild(modal);
    modal.addEventListener('click', function(e) { if (e.target === modal) modal.remove(); });
    box.querySelector('#pbSvClose').addEventListener('click', function() { modal.remove(); });

    var urlInput = box.querySelector('#pbSvUrl');
    var titleInput = box.querySelector('#pbSvTitle');
    var titleHint = box.querySelector('#pbSvTitleHint');
    var preview = box.querySelector('#pbSvPreview');
    var thumb = box.querySelector('#pbSvThumb');
    var addBtn = box.querySelector('#pbSvAdd');
    var fetchTimer = null;
    var parsedVideo = null;

    function parseVideoUrl(val) {
      var yt = val.match(/(?:youtube\.com\/watch\?v=|youtu\.be\/)([^&?\s]+)/);
      var vm = val.match(/vimeo\.com\/(\d+)/);
      if (yt) return { embed_id: yt[1], source: 'youtube' };
      if (vm) return { embed_id: vm[1], source: 'vimeo' };
      return null;
    }

    urlInput.addEventListener('input', function() {
      var val = urlInput.value.trim();
      parsedVideo = parseVideoUrl(val);
      if (parsedVideo) {
        addBtn.disabled = false;
        addBtn.style.opacity = '1';
        thumb.src = videoThumbUrl(parsedVideo.source, parsedVideo.embed_id);
        preview.style.display = 'block';
        clearTimeout(fetchTimer);
        fetchTimer = setTimeout(function() {
          var metaUrl = '/api/video-meta?source=' + encodeURIComponent(parsedVideo.source)
                      + '&id=' + encodeURIComponent(parsedVideo.embed_id);
          fetch(metaUrl)
            .then(function(r) { return r.json(); })
            .then(function(data) {
              if (data && data.title && !titleInput.value.trim()) {
                titleInput.value = data.title;
                titleHint.textContent = '✓ ' + (PB_I18N.video_gallery_title_fetched || 'Title fetched automatically');
                titleHint.style.display = 'block';
              } else if (data && data.error) {
                titleHint.textContent = PB_I18N.video_meta_fetch_failed || 'Couldn\'t fetch video info';
                titleHint.style.display = 'block';
              }
            })
            .catch(function() {
              titleHint.textContent = PB_I18N.video_meta_fetch_failed || 'Couldn\'t fetch video info';
              titleHint.style.display = 'block';
            });
        }, 400);
      } else {
        addBtn.disabled = true;
        addBtn.style.opacity = '0.5';
        preview.style.display = 'none';
        titleHint.style.display = 'none';
      }
    });

    addBtn.addEventListener('click', function() {
      if (!parsedVideo) return;
      block.data.url = urlInput.value.trim();
      block.data.embed_id = parsedVideo.embed_id;
      block.data.source = parsedVideo.source;
      block.data.title = titleInput.value.trim();
      markDirty();
      openPanel(block);
      renderCanvas();
      modal.remove();
    });

    setTimeout(function() { urlInput.focus(); }, 100);
  }

  // ============================================================
  // FORM PICKER MODAL — select form from canvas
  // ============================================================
  function pbPickForm(block) {
    var _fpState = { forms: [], map: {}, lang: '' };
    var modal = document.createElement('div');
    modal.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,0.7);z-index:10000;display:flex;align-items:center;justify-content:center';

    var box = document.createElement('div');
    box.style.cssText = 'background:var(--pb-surface);border-radius:12px;padding:24px;max-width:560px;max-height:80vh;overflow-y:auto;width:90%;color:var(--pb-text)';

    var searchId = 'pbFS_' + Date.now();
    box.innerHTML =
      '<div class="pb-panel-header">' +
      '<h3 class="pb-heading-md">📋 ' + esc(PB_I18N.form_select || 'Select form') + '</h3>' +
      '<button class="pb-modal-close" data-fp-close>&times;</button>' +
      '</div>' +
      '<input type="text" id="' + searchId + '" class="pb-field__input pb-mb-2" placeholder="' + esc(PB_I18N.form_search_placeholder || 'Search forms...') + '">' +
      '<div class="pb-form-lang-tabs" data-fp-langs></div>' +
      '<div data-fp-list><div class="pb-empty-msg">' + esc(PB_I18N.loading || 'Loading...') + '</div></div>';

    modal.appendChild(box);
    document.body.appendChild(modal);
    modal.addEventListener('click', function(e) { if (e.target === modal) modal.remove(); });
    box.querySelector('[data-fp-close]').addEventListener('click', function() { modal.remove(); });

    var listEl = box.querySelector('[data-fp-list]');
    var searchEl = document.getElementById(searchId);
    var langTabsEl = box.querySelector('[data-fp-langs]');

    function selectForm(fId) {
      var f = _fpState.map[fId];
      if (!f) return;
      block.data.form_id = fId;
      block.data._form_title = f.title + ' (' + f.form_type + ', ' + f.fields_count + ' fields)';
      block.data._form_title_raw = f.title;
      block.data._fields = f.fields;
      block.data._settings = f.settings;
      block.data._description = f.description || '';
      markDirty();
      renderCanvas();
      openPanel(block);
      modal.remove();
    }

    function renderCards() {
      var query = searchEl ? (searchEl.value || '').toLowerCase().trim() : '';
      var lang = _fpState.lang;
      var filtered = _fpState.forms.filter(function(f) {
        if (lang && f.language !== lang) return false;
        if (query && f.title.toLowerCase().indexOf(query) === -1) return false;
        return true;
      });

      if (filtered.length === 0) {
        listEl.className = '';
        listEl.innerHTML = '<div class="pb-empty-msg"><p>' + esc(PB_I18N.form_no_forms || 'No forms found.') + '</p></div>';
        return;
      }

      listEl.className = 'pb-form-pick-list';
      listEl.innerHTML = filtered.map(function(f) {
        var isSelected = String(f.id) === String(block.data.form_id);
        var langBadge = f.language ? '<span class="pb-form-pick-card__lang">' + esc(f.language.toUpperCase()) + '</span>' : '';
        return '<div class="pb-form-pick-card' + (isSelected ? ' is-selected' : '') + '" data-fp-id="' + f.id + '">' +
          '<svg class="pb-form-pick-card__icon" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"/><path d="M7 7h10"/><path d="M7 12h10"/><path d="M7 17h4"/></svg>' +
          '<div class="pb-form-pick-card__info">' +
          '<div class="pb-form-pick-card__title">' + esc(f.title) + '</div>' +
          '<div class="pb-form-pick-card__meta">' + esc(f.form_type) + ' · ' + f.fields_count + ' ' + esc(PB_I18N.form_fields || 'fields') + '</div>' +
          '</div>' +
          langBadge +
          (isSelected ? '<span class="pb-form-pick-card__check">✓</span>' : '') +
          '</div>';
      }).join('');

      listEl.querySelectorAll('[data-fp-id]').forEach(function(card) {
        card.addEventListener('click', function() {
          selectForm(parseInt(card.dataset.fpId));
        });
      });
    }

    if (searchEl) {
      searchEl.addEventListener('input', renderCards);
      searchEl.addEventListener('keyup', renderCards);
    }

    fetch('/api/forms/list')
      .then(function(r) { return r.json(); })
      .then(function(data) {
        if (!data.forms || data.forms.length === 0) {
          langTabsEl.style.display = 'none';
          if (searchEl) searchEl.style.display = 'none';
          listEl.innerHTML = '<div class="pb-empty-msg">' +
            '<p>' + esc(PB_I18N.form_no_forms || 'No forms yet.') + '</p>' +
            '<a href="/admin/forms/new" target="_blank" class="pb-color-primary">' + esc(PB_I18N.form_new || 'Create a form') + '</a></div>';
          return;
        }

        _fpState.forms = data.forms;
        data.forms.forEach(function(f) { _fpState.map[f.id] = f; });

        var langSet = {};
        data.forms.forEach(function(f) { if (f.language) langSet[f.language] = true; });
        var languages = Object.keys(langSet).sort();

        if (languages.length > 1) {
          var tabsHtml = '<button class="pb-form-lang-tab is-active" data-fp-lang="">' + esc(PB_I18N.form_all_languages || 'All') + '</button>';
          languages.forEach(function(lang) {
            tabsHtml += '<button class="pb-form-lang-tab" data-fp-lang="' + esc(lang) + '">' + esc(lang.toUpperCase()) + '</button>';
          });
          langTabsEl.innerHTML = tabsHtml;
          langTabsEl.querySelectorAll('[data-fp-lang]').forEach(function(tab) {
            tab.addEventListener('click', function() {
              langTabsEl.querySelectorAll('[data-fp-lang]').forEach(function(t) { t.classList.remove('is-active'); });
              tab.classList.add('is-active');
              _fpState.lang = tab.dataset.fpLang;
              renderCards();
            });
          });
        } else {
          langTabsEl.style.display = 'none';
        }

        renderCards();
        if (searchEl) searchEl.focus();
      })
      .catch(function() {
        listEl.innerHTML = '<div class="pb-empty-msg">' + esc(PB_I18N.error_loading_forms || 'Error loading forms') + '</div>';
      });
  }

  // ============================================================
  // BIND PANEL EVENTS — update block data on input change
  // ============================================================
  function bindPanelEvents(block) {
    $panelBody.querySelectorAll('[data-key]').forEach(function(el) {
      var eventType = (el.tagName === 'SELECT' || el.type === 'checkbox') ? 'change' : 'input';
      // Anchor ID: normalize on blur (kebab-case ASCII, matches cleanSlugInput
      // pattern used for page/post slugs). On-input would eat dashes the user
      // is in the middle of typing; on-blur lets the value settle naturally.
      if (el.dataset.key === 'settings.anchor_id') {
        el.addEventListener('blur', function() {
          var clean = (el.value || '').toLowerCase()
            .replace(/\s+/g, '-')
            .replace(/[^a-z0-9\-]/g, '')
            .replace(/-+/g, '-')
            .replace(/^-|-$/g, '');
          if (clean !== el.value) {
            el.value = clean;
            // Sync model — the 'input' listener below won't fire on programmatic
            // value changes, so write directly.
            if (block && block.settings) block.settings.anchor_id = clean;
            markDirty();
          }
        });
      }
      el.addEventListener(eventType, function() {
        var key = el.dataset.key;
        var val = el.type === 'checkbox' ? el.checked : el.value;

        // Parse key path: "data.text" or "settings.alignment" or "data.items.0.title"
        var parts = key.split('.');
        var target = block;
        for (var i = 0; i < parts.length - 1; i++) {
          if (target[parts[i]] === undefined) target[parts[i]] = {};
          target = target[parts[i]];
        }

        var lastKey = parts[parts.length - 1];

        // Special parsing for some fields
        if (key === 'data.images_text' && block.type === 'gallery') {
          block.data.images = val.split('\n').filter(function(s) { return s.trim(); }).map(function(line) {
            var p = line.split('|');
            return { url: (p[0]||'').trim(), alt: (p[1]||'').trim(), caption: (p[2]||'').trim() };
          });
        } else if (key === 'data.url' && block.type === 'video') {
          target[lastKey] = val;
          // Auto-extract YouTube/Vimeo ID
          var yt = val.match(/(?:youtube\.com\/watch\?v=|youtu\.be\/)([^&?\s]+)/);
          var vm = val.match(/vimeo\.com\/(\d+)/);
          if (yt) { block.data.embed_id = yt[1]; block.data.source = 'youtube'; }
          else if (vm) { block.data.embed_id = vm[1]; block.data.source = 'vimeo'; }
        } else if (key === 'data.level') {
          target[lastKey] = parseInt(val);
        } else if (key === 'data.columns' && (block.type === 'gallery' || block.type === 'video_gallery')) {
          target[lastKey] = parseInt(val);
        } else if (key === 'data.interval' && block.type === 'slideshow') {
          target[lastKey] = parseInt(val) || 5;
        } else {
          target[lastKey] = val;
        }

        // Handle columns layout change
        if (key === 'data.layout' && block.type === 'columns') {
          var colCount = { '1': 1, '1/2+1/2': 2, '1/3+1/3+1/3': 3, '2/3+1/3': 2, '1/3+2/3': 2, '1/4+1/4+1/4+1/4': 4 };
          var needed = colCount[val] || 2;
          while (block.data.columns.length < needed) block.data.columns.push({ blocks: [] });
          if (block.data.columns.length > needed) block.data.columns.length = needed;
        }

        // Re-render panel when section background type changes
        if (key === 'data.background_type' && block.type === 'section') {
          // Clear old value when switching types for clean UX
          block.data.background_value = '';
          if (val === 'none') block.data.background_overlay = '';
          openPanel(block);
        }
        if (key === 'data.background_type_dark' && block.type === 'section') {
          block.data.background_value_dark = '';
          if (val === 'none') block.data.background_overlay_dark = '';
          openPanel(block);
        }

        // Sync sidebar text alignment to canvas paragraphs for rich text blocks
        if (key === 'settings.content_alignment' && block.type === 'text') {
          var blockEl = $blocks.querySelector('[data-id="' + block.id + '"] [data-inline-html]');
          if (blockEl) {
            var paragraphs = blockEl.querySelectorAll('p, li, h1, h2, h3, h4, h5, h6');
            paragraphs.forEach(function(p) {
              if (val) {
                p.style.textAlign = val;
              } else {
                p.style.removeProperty('text-align');
              }
            });
            // Update model with new innerHTML
            block.data.html = blockEl.innerHTML;
          }
        }

        _isInlineEditing = false;
        markDirty();
        renderCanvas();
      });
    });

    // Sync color picker <-> text input (both share data-key="data.background_value")
    var bgInputs = $panelBody.querySelectorAll('[data-key="data.background_value"]');
    if (bgInputs.length === 2) {
      bgInputs[0].addEventListener('input', function() { bgInputs[1].value = bgInputs[0].value; });
      bgInputs[1].addEventListener('input', function() { bgInputs[0].value = bgInputs[1].value; });
    }
    var bgInputsDark = $panelBody.querySelectorAll('[data-key="data.background_value_dark"]');
    if (bgInputsDark.length === 2) {
      bgInputsDark[0].addEventListener('input', function() { bgInputsDark[1].value = bgInputsDark[0].value; });
      bgInputsDark[1].addEventListener('input', function() { bgInputsDark[0].value = bgInputsDark[1].value; });
    }

    // Image picker buttons
    $panelBody.querySelectorAll('.pb-pick-image-btn').forEach(function(btn) {
      btn.addEventListener('click', function() {
        pbPickImage(btn.dataset.targetKey);
      });
    });

    // Clear image button (✕) — removes URL and alt, re-renders panel
    $panelBody.querySelectorAll('.pb-clear-image-btn').forEach(function(btn) {
      btn.addEventListener('click', function() {
        var key = btn.dataset.clearKey;
        var input = $panelBody.querySelector('[data-key="' + key + '"]');
        if (input) {
          input.value = '';
          input.dispatchEvent(new Event('input', { bubbles: true }));
        }
        // Clear associated alt
        var altKey = key.replace(/\.image$/, '.alt').replace(/\.url$/, '.alt');
        if (altKey === key) altKey = 'data.alt';
        var altInput = $panelBody.querySelector('[data-key="' + altKey + '"]');
        if (altInput) {
          altInput.value = '';
          altInput.dispatchEvent(new Event('input', { bubbles: true }));
        }
        markDirty();
        openPanel(block);
        renderCanvas();
      });
    });

    // Gallery: add images from library
    var galleryPickBtn = $panelBody.querySelector('.pb-gallery-pick-btn');
    if (galleryPickBtn) {
      galleryPickBtn.addEventListener('click', function() {
        pbPickGalleryImages(block);
      });
    }

    // Gallery: remove individual image
    $panelBody.querySelectorAll('.pb-gallery-remove').forEach(function(btn) {
      btn.addEventListener('click', function(e) {
        e.stopPropagation();
        var idx = parseInt(btn.dataset.galleryRemove);
        if (block.data.images && idx >= 0 && idx < block.data.images.length) {
          block.data.images.splice(idx, 1);
          markDirty();
          openPanel(block);
          renderCanvas();
        }
      });
    });

    // Video Gallery: add video
    var vgalAddBtn = $panelBody.querySelector('.pb-vgal-add-btn');
    if (vgalAddBtn) {
      vgalAddBtn.addEventListener('click', function() {
        pbAddVideoToGallery(block);
      });
    }

    // Ticker: add images from library (reuses gallery picker)
    var tickerPickBtn = $panelBody.querySelector('.pb-ticker-pick-btn');
    if (tickerPickBtn) {
      tickerPickBtn.addEventListener('click', function() {
        pbPickGalleryImages(block);
      });
    }

    // Ticker: remove individual image
    $panelBody.querySelectorAll('[data-ticker-remove]').forEach(function(btn) {
      btn.addEventListener('click', function(e) {
        e.stopPropagation();
        var idx = parseInt(btn.dataset.tickerRemove);
        if (block.data.images && idx >= 0 && idx < block.data.images.length) {
          block.data.images.splice(idx, 1);
          markDirty();
          openPanel(block);
          renderCanvas();
        }
      });
    });

    // Video Gallery: remove individual video
    $panelBody.querySelectorAll('.pb-vgal-remove').forEach(function(btn) {
      btn.addEventListener('click', function(e) {
        e.stopPropagation();
        var idx = parseInt(btn.dataset.vgalRemove);
        if (block.data.videos && idx >= 0 && idx < block.data.videos.length) {
          block.data.videos.splice(idx, 1);
          markDirty();
          openPanel(block);
          renderCanvas();
        }
      });
    });

    // Single Video: pick video button
    var videoPickBtn = $panelBody.querySelector('.pb-video-pick-btn');
    if (videoPickBtn) {
      videoPickBtn.addEventListener('click', function() {
        pbPickSingleVideo(block);
      });
    }

    // Single Video: remove video button
    var videoClearBtn = $panelBody.querySelector('.pb-video-clear-btn');
    if (videoClearBtn) {
      videoClearBtn.addEventListener('click', function(e) {
        e.stopPropagation();
        block.data.url = '';
        block.data.embed_id = '';
        block.data.source = '';
        block.data.title = '';
        markDirty();
        openPanel(block);
        renderCanvas();
      });
    }

    // Add/remove items for accordion and tabs
    $panelBody.querySelectorAll('[data-add-item]').forEach(function(btn) {
      btn.addEventListener('click', function() {
        var type = btn.dataset.addItem;
        if (type === 'accordion') {
          block.data.items = block.data.items || [];
          block.data.items.push({ title: PB_I18N.default_accordion_title || '', content: '<p></p>' });
        } else if (type === 'tabs') {
          block.data.tabs = block.data.tabs || [];
          block.data.tabs.push({ title: (PB_I18N.default_tab || 'Tab') + ' ' + (block.data.tabs.length + 1), content: '<p></p>' });
        } else if (type === 'slideshow') {
          block.data.slides = block.data.slides || [];
          block.data.slides.push({ image: '', alt: '', blocks: [] });
        }
        markDirty();
        openPanel(block);
        renderCanvas();
      });
    });

    $panelBody.querySelectorAll('[data-remove-item]').forEach(function(btn) {
      btn.addEventListener('click', function() {
        pbConfirm(PB_I18N.delete_item_confirm || 'Delete this item?',
                  { okLabel: PB_I18N.delete, danger: true }).then(function(ok) {
          if (!ok) return;
          var type = btn.dataset.removeItem;
          var idx = parseInt(btn.dataset.index);
          if (type === 'accordion' && block.data.items) block.data.items.splice(idx, 1);
          if (type === 'tabs' && block.data.tabs) block.data.tabs.splice(idx, 1);
          if (type === 'slideshow' && block.data.slides) block.data.slides.splice(idx, 1);
          markDirty();
          openPanel(block);
          renderCanvas();
        });
      });
    });

    // Form block: use picker modal (same as canvas click)
    if (block.type === 'form') {
      var formPickBtn = document.getElementById('pbFormPickBtn');
      var formChangeBtn = document.getElementById('pbFormChangeBtn');
      var formClearBtn = document.getElementById('pbFormClearBtn');
      if (formPickBtn) {
        formPickBtn.addEventListener('click', function() { pbPickForm(block); });
      }
      if (formChangeBtn) {
        formChangeBtn.addEventListener('click', function() { pbPickForm(block); });
      }
      if (formClearBtn) {
        formClearBtn.addEventListener('click', function() {
          block.data.form_id = '';
          block.data._form_title = '';
          block.data._form_title_raw = '';
          block.data._fields = null;
          block.data._settings = null;
          block.data._description = '';
          markDirty();
          renderCanvas();
          openPanel(block);
        });
      }
    }

    // ── Number stepper buttons ──
    $panelBody.querySelectorAll('[data-step-up]').forEach(function(btn) {
      btn.addEventListener('click', function(e) {
        e.preventDefault();
        var key = btn.dataset.stepUp;
        var input = $panelBody.querySelector('[data-key="' + key + '"]');
        if (input) { input.value = parseInt(input.value || 0) + 1; input.dispatchEvent(new Event('input', { bubbles: true })); }
      });
    });
    $panelBody.querySelectorAll('[data-step-down]').forEach(function(btn) {
      btn.addEventListener('click', function(e) {
        e.preventDefault();
        var key = btn.dataset.stepDown;
        var input = $panelBody.querySelector('[data-key="' + key + '"]');
        if (input) { var v = parseInt(input.value || 0) - 1; input.value = Math.max(0, v); input.dispatchEvent(new Event('input', { bubbles: true })); }
      });
    });

    // ── Color + Alpha picker ──
    $panelBody.querySelectorAll('[data-ca-color]').forEach(function(swatch) {
      var key = swatch.dataset.caColor;
      var slider = $panelBody.querySelector('[data-ca-alpha="' + key + '"]');
      var label = $panelBody.querySelector('[data-ca-label="' + key + '"]');
      var textInput = $panelBody.querySelector('[data-key="' + key + '"]');
      function syncColorAlpha() {
        var hex = swatch.value;
        var a = parseInt(slider.value) / 100;
        var r = parseInt(hex.substring(1,3), 16);
        var g = parseInt(hex.substring(3,5), 16);
        var b = parseInt(hex.substring(5,7), 16);
        var rgba = 'rgba(' + r + ',' + g + ',' + b + ',' + a.toFixed(2) + ')';
        textInput.value = rgba;
        if (label) label.textContent = slider.value + '%';
        slider.style.background = 'linear-gradient(to right, transparent, ' + hex + ')';
        textInput.dispatchEvent(new Event('input', { bubbles: true }));
      }
      swatch.addEventListener('input', syncColorAlpha);
      slider.addEventListener('input', syncColorAlpha);
      // Init slider background
      slider.style.background = 'linear-gradient(to right, transparent, ' + swatch.value + ')';
    });

    // ── Gradient presets ──
    $panelBody.querySelectorAll('[data-gradient-pick]').forEach(function(btn) {
      btn.addEventListener('click', function(e) {
        e.preventDefault();
        var key = btn.dataset.gradientPick;
        var val = btn.dataset.gradientVal;
        var input = $panelBody.querySelector('[data-key="' + key + '"]');
        if (input) { input.value = val; input.dispatchEvent(new Event('input', { bubbles: true })); }
        // Update active state
        $panelBody.querySelectorAll('[data-gradient-pick="' + key + '"]').forEach(function(b) { b.classList.remove('is-active'); });
        btn.classList.add('is-active');
      });
    });

    // ── Rich text editing: handled on canvas via .pb-rich-zone ──

    // ── Visual table editor ──
    if (block.type === 'table') {
      // Header edits
      $panelBody.querySelectorAll('[data-table-header]').forEach(function(input) {
        input.addEventListener('input', function() {
          var idx = parseInt(input.dataset.tableHeader);
          block.data.headers[idx] = input.value;
          markDirty();
          renderCanvas();
        });
      });
      // Cell edits
      $panelBody.querySelectorAll('[data-table-cell]').forEach(function(input) {
        input.addEventListener('input', function() {
          var ri = parseInt(input.dataset.row);
          var ci = parseInt(input.dataset.col);
          if (!block.data.rows[ri]) block.data.rows[ri] = [];
          block.data.rows[ri][ci] = input.value;
          markDirty();
          renderCanvas();
        });
      });
      // Table actions: add row/col
      $panelBody.querySelectorAll('[data-table-action]').forEach(function(btn) {
        btn.addEventListener('click', function() {
          var action = btn.dataset.tableAction;
          if (action === 'add-row') {
            var cols = block.data.headers.length;
            block.data.rows.push(new Array(cols).fill(''));
          } else if (action === 'add-col') {
            block.data.headers.push('');
            block.data.rows.forEach(function(row) { row.push(''); });
            if (block.data.column_alignments) block.data.column_alignments.push('');
          }
          markDirty();
          openPanel(block);
          renderCanvas();
        });
      });
      // Table: remove specific row
      $panelBody.querySelectorAll('[data-table-remove-row]').forEach(function(btn) {
        btn.addEventListener('click', function() {
          var ri = parseInt(btn.dataset.tableRemoveRow);
          if (block.data.rows.length <= 1) return;
          block.data.rows.splice(ri, 1);
          markDirty();
          openPanel(block);
          renderCanvas();
        });
      });
      // Table: insert row above
      $panelBody.querySelectorAll('[data-table-insert-row]').forEach(function(btn) {
        btn.addEventListener('click', function() {
          var ri = parseInt(btn.dataset.tableInsertRow);
          var cols = block.data.headers.length;
          block.data.rows.splice(ri, 0, new Array(cols).fill(''));
          markDirty();
          openPanel(block);
          renderCanvas();
        });
      });
      // Table: remove specific column
      $panelBody.querySelectorAll('[data-table-remove-col]').forEach(function(btn) {
        btn.addEventListener('click', function() {
          var ci = parseInt(btn.dataset.tableRemoveCol);
          if (block.data.headers.length <= 1) return;
          block.data.headers.splice(ci, 1);
          block.data.rows.forEach(function(row) { row.splice(ci, 1); });
          if (block.data.column_alignments) block.data.column_alignments.splice(ci, 1);
          markDirty();
          openPanel(block);
          renderCanvas();
        });
      });

      // Column alignment dropdowns
      $panelBody.querySelectorAll('[data-col-align]').forEach(function(sel) {
        sel.addEventListener('change', function() {
          var ci = parseInt(sel.dataset.colAlign);
          if (!block.data.column_alignments) block.data.column_alignments = [];
          while (block.data.column_alignments.length <= ci) block.data.column_alignments.push('');
          block.data.column_alignments[ci] = sel.value;
          markDirty();
          renderCanvas();
        });
      });
      // Column width inputs
      $panelBody.querySelectorAll('[data-col-width]').forEach(function(input) {
        input.addEventListener('input', function() {
          var ci = parseInt(input.dataset.colWidth);
          if (!block.data.column_widths) block.data.column_widths = [];
          while (block.data.column_widths.length <= ci) block.data.column_widths.push('');
          block.data.column_widths[ci] = input.value.replace(/[^0-9]/g, '');
          markDirty();
          renderCanvas();
          // Update sum display
          var sumEl = document.getElementById('pbColWidthSum');
          if (sumEl) {
            var total = block.data.column_widths.reduce(function(s, w) { return s + (parseInt(w) || 0); }, 0);
            if (total > 0) {
              sumEl.textContent = (PB_I18N.column_width_hint || '%') + ' · ' + (PB_I18N.total || 'Total') + ': ' + total + '%' + (total > 100 ? ' ⚠' : '');
              sumEl.style.color = total > 100 ? 'var(--pb-danger)' : '';
            } else {
              sumEl.textContent = PB_I18N.column_width_hint || '%';
              sumEl.style.color = '';
            }
          }
        });
      });
    }
  }

  // ============================================================
  // BLOCK OPERATIONS
  // ============================================================
  function findBlock(id) {
    return state.blocks.find(function(b) { return b.id === id; });
  }

  function findIndex(id) {
    return state.blocks.findIndex(function(b) { return b.id === id; });
  }

  function findBlockDeep(id) {
    for (var i = 0; i < state.blocks.length; i++) {
      var b = state.blocks[i];
      if (b.id === id) return b;
      if (b.type === 'columns' && b.data && b.data.columns) {
        for (var c = 0; c < b.data.columns.length; c++) {
          var cblocks = b.data.columns[c].blocks || [];
          for (var j = 0; j < cblocks.length; j++) { if (cblocks[j].id === id) return cblocks[j]; }
        }
      }
      if (b.type === 'section' && b.data && b.data.blocks) {
        for (var j = 0; j < b.data.blocks.length; j++) {
          if (b.data.blocks[j].id === id) return b.data.blocks[j];
          var sb = b.data.blocks[j];
          if (sb.type === 'columns' && sb.data && sb.data.columns) {
            for (var sc = 0; sc < sb.data.columns.length; sc++) {
              var scblocks = sb.data.columns[sc].blocks || [];
              for (var sk = 0; sk < scblocks.length; sk++) { if (scblocks[sk].id === id) return scblocks[sk]; }
            }
          }
        }
      }
      if (b.type === 'slideshow' && b.data && b.data.slides) {
        for (var s = 0; s < b.data.slides.length; s++) {
          var sblocks = (b.data.slides[s].blocks || []);
          for (var j = 0; j < sblocks.length; j++) { if (sblocks[j].id === id) return sblocks[j]; }
        }
      }
    }
    return null;
  }

  function addBlock(type, index) {
    console.log('[PageBuilder] Adding block:', type);
    var data = JSON.parse(JSON.stringify(BLOCK_DEFAULTS[type] || {}));
    var block = { id: uuid(), type: type, data: data, settings: { alignment: defaultAlignFor(type) } };
    if (index === undefined || index < 0) {
      state.blocks.push(block);
    } else {
      state.blocks.splice(index, 0, block);
    }
    markDirty();
    renderCanvas();
    openPanel(block);
  }

  function deleteBlock(id) {
    var idx = findIndex(id);
    if (idx < 0) return;
    var block = state.blocks[idx];
    var label = BLOCK_LABELS[block.type] || block.type;
    pbConfirm((PB_I18N.delete_block_confirm || 'Delete this {type} block?').replace('{type}', label),
              { okLabel: PB_I18N.delete, danger: true }).then(function(ok) {
      if (!ok) return;
      // Re-resolve: the canvas may have re-rendered while the dialog was open.
      var i = findIndex(id);
      if (i < 0) return;
      state.blocks.splice(i, 1);
      if (state.selectedId === id) closePanel();
      markDirty();
      renderCanvas();
    });
  }

  function duplicateBlock(id) {
    var idx = findIndex(id);
    if (idx >= 0) {
      var copy = JSON.parse(JSON.stringify(state.blocks[idx]));
      _regenerateIds(copy);
      state.blocks.splice(idx + 1, 0, copy);
      markDirty();
      renderCanvas();
      openPanel(copy);
    }
  }

  // Recursively regenerate IDs for a block and all nested blocks
  function _regenerateIds(block) {
    block.id = uuid();
    if (block.data) {
      // Section nested blocks
      if (block.data.blocks) {
        block.data.blocks.forEach(_regenerateIds);
      }
      // Column nested blocks
      if (block.data.columns) {
        block.data.columns.forEach(function(col) {
          (col.blocks || []).forEach(_regenerateIds);
        });
      }
      // Slideshow nested blocks
      if (block.data.slides) {
        block.data.slides.forEach(function(slide) {
          (slide.blocks || []).forEach(_regenerateIds);
        });
      }
    }
  }

  function moveBlock(id, dir) {
    var idx = findIndex(id);
    var newIdx = idx + dir;
    if (idx < 0 || newIdx < 0 || newIdx >= state.blocks.length) return;
    var b = state.blocks.splice(idx, 1)[0];
    state.blocks.splice(newIdx, 0, b);
    markDirty();
    renderCanvas();
  }

  // ============================================================
  // EVENT LISTENERS
  // ============================================================

  // Save button
  $save.addEventListener('click', saveBlocks);

  // Keyboard shortcut: Ctrl+S
  document.addEventListener('keydown', function(e) {
    if ((e.ctrlKey || e.metaKey) && e.key === 's') {
      e.preventDefault();
      saveBlocks();
    }
  });

  // Close panel
  document.getElementById('pbPanelClose').addEventListener('click', closePanel);

  // Canvas click delegation
  $blocks.addEventListener('click', function(e) {
    var target = e.target;

    // Skip if click is on nested column block elements (handled by bindColumnEvents)
    if (target.closest('.pb-col-block') || target.closest('.pb-col-add') || target.closest('.pb-col-zone__empty') || target.closest('.pb-col-picker')) return;

    // Skip inline editing elements (handled by bindInlineEditing)
    if (target.closest('[contenteditable]') || target.closest('[data-click-edit]') || target.closest('[data-inline-input]') || target.closest('[data-inline-textarea]') || target.closest('.pb-inline-table input') || target.closest('.pb-text-toolbar')) return;

    // Delete
    var del = target.closest('.pb-item__delete');
    if (del) { deleteBlock(del.closest('.pb-item').dataset.id); return; }

    // Duplicate
    var dup = target.closest('.pb-item__duplicate');
    if (dup) { duplicateBlock(dup.closest('.pb-item').dataset.id); return; }

    // Move up
    var up = target.closest('.pb-item__move-up');
    if (up) { moveBlock(up.closest('.pb-item').dataset.id, -1); return; }

    // Move down
    var down = target.closest('.pb-item__move-down');
    if (down) { moveBlock(down.closest('.pb-item').dataset.id, 1); return; }

    // Select block
    var item = target.closest('.pb-item');
    if (item) {
      var block = findBlock(item.dataset.id);
      if (block) openPanel(block);
    }
  });

  // Sidebar — add block on click
  document.querySelectorAll('.pb-block-btn').forEach(function(btn) {
    btn.addEventListener('click', function() {
      addBlock(btn.dataset.blockType);
    });
  });

  // Drag from sidebar to canvas (basic HTML5 drag & drop)
  var _dragSidebarType = null;
  document.querySelectorAll('.pb-block-btn').forEach(function(btn) {
    btn.draggable = true;
    btn.addEventListener('dragstart', function(e) {
      _dragSidebarType = btn.dataset.blockType;
      e.dataTransfer.setData('text/plain', btn.dataset.blockType);
      e.dataTransfer.setData('application/x-pb-source', 'sidebar');
      e.dataTransfer.effectAllowed = 'copy';
    });
    btn.addEventListener('dragend', function() {
      _dragSidebarType = null;
      // Clear any position indicators left on canvas items
      $blocks.querySelectorAll('.pb-item').forEach(function(el) {
        el.style.borderTop = '';
        el.style.borderBottom = '';
      });
    });
  });

  $canvas.addEventListener('dragover', function(e) {
    e.preventDefault();
    e.dataTransfer.dropEffect = 'copy';
    $canvas.classList.add('pb-canvas--drag-over');
  });

  $canvas.addEventListener('dragleave', function() {
    $canvas.classList.remove('pb-canvas--drag-over');
  });

  $canvas.addEventListener('drop', function(e) {
    e.preventDefault();
    $canvas.classList.remove('pb-canvas--drag-over');
    var type = e.dataTransfer.getData('text/plain');
    var source = e.dataTransfer.getData('application/x-pb-source');
    if (source === 'sidebar' && type && BLOCK_DEFAULTS[type]) {
      addBlock(type);
    }
  });

  // ============================================================
  // DRAG-TO-REORDER — reorder existing blocks by dragging handle
  // ============================================================
  var _dragBlockId = null;

  // Bind drag handles after each render
  function bindDragReorder() {
    $blocks.querySelectorAll('.pb-item').forEach(function(item) {
      var handle = item.querySelector('.pb-item__drag-handle');
      if (!handle) return;

      // Make item draggable only via handle
      handle.addEventListener('mousedown', function() { item.setAttribute('draggable', 'true'); });
      handle.addEventListener('mouseup', function() { item.removeAttribute('draggable'); });

      item.addEventListener('dragstart', function(e) {
        if (!item.getAttribute('draggable')) { e.preventDefault(); return; }
        _dragBlockId = item.dataset.id;
        e.dataTransfer.setData('application/x-pb-source', 'reorder');
        e.dataTransfer.setData('text/plain', '');
        e.dataTransfer.effectAllowed = 'move';
        item.style.opacity = '0.4';
      });

      item.addEventListener('dragend', function() {
        item.style.opacity = '';
        item.removeAttribute('draggable');
        _dragBlockId = null;
        // Clear all indicators
        $blocks.querySelectorAll('.pb-item').forEach(function(el) {
          el.style.borderTop = '';
          el.style.borderBottom = '';
        });
      });

      item.addEventListener('dragover', function(e) {
        var isDrag = _dragBlockId && _dragBlockId !== item.dataset.id;
        var isSidebar = !!_dragSidebarType;
        if (!isDrag && !isSidebar) return;
        e.preventDefault();
        e.stopPropagation();
        e.dataTransfer.dropEffect = isSidebar ? 'copy' : 'move';
        // Show indicator: top or bottom half
        var rect = item.getBoundingClientRect();
        var midY = rect.top + rect.height / 2;
        if (e.clientY < midY) {
          item.style.borderTop = '3px solid var(--pb-primary)';
          item.style.borderBottom = '';
        } else {
          item.style.borderTop = '';
          item.style.borderBottom = '3px solid var(--pb-primary)';
        }
      });

      item.addEventListener('dragleave', function() {
        item.style.borderTop = '';
        item.style.borderBottom = '';
      });

      item.addEventListener('drop', function(e) {
        e.preventDefault();
        e.stopPropagation();
        item.style.borderTop = '';
        item.style.borderBottom = '';

        var rect = item.getBoundingClientRect();
        var midY = rect.top + rect.height / 2;
        var insertAfter = e.clientY >= midY;
        var toIdx = findIndex(item.dataset.id);

        // Sidebar drop: insert new block at position
        if (_dragSidebarType && BLOCK_DEFAULTS[_dragSidebarType]) {
          var insertIdx = (toIdx >= 0 && insertAfter) ? toIdx + 1 : (toIdx >= 0 ? toIdx : undefined);
          addBlock(_dragSidebarType, insertIdx);
          _dragSidebarType = null;
          return;
        }

        // Reorder drop
        if (!_dragBlockId || _dragBlockId === item.dataset.id) return;

        var fromIdx = findIndex(_dragBlockId);
        if (fromIdx < 0 || toIdx < 0) return;

        var block = state.blocks.splice(fromIdx, 1)[0];
        var newIdx = findIndex(item.dataset.id);
        if (insertAfter) newIdx++;
        state.blocks.splice(newIdx, 0, block);

        _dragBlockId = null;
        markDirty();
        renderCanvas();
      });
    });
  }

  // Patch renderCanvas to bind drag reorder after each render
  var _origRenderCanvas = renderCanvas;
  var _isInlineEditing = false;  // Prevent re-render while user types inline
  renderCanvas = function() {
    if (_isInlineEditing) return;  // Don't destroy contenteditable while editing
    _origRenderCanvas();
    bindDragReorder();
    bindInserters();
    try { bindInlineEditing(); } catch(e) { console.warn('[PageBuilder] bindInlineEditing error:', e); }
  };

  // ============================================================
  // INLINE EDITING — edit blocks directly in canvas
  // ============================================================

  // Helper: set a deep key on a block (e.g. "data.text", "data.items.0")
  function _setDeepKey(block, keyPath, value) {
    var parts = keyPath.split('.');
    var target = block;
    for (var i = 0; i < parts.length - 1; i++) {
      if (target[parts[i]] === undefined) target[parts[i]] = {};
      target = target[parts[i]];
    }
    target[parts[parts.length - 1]] = value;
    markDirty();
  }

  // Helper: bind focus/blur on an inline element
  function _bindInlineFocusBlur(el) {
    el.addEventListener('focus', function(e) {
      e.stopPropagation();
      _isInlineEditing = true;
      // Open settings panel for this block
      var blockId = el.dataset.inlineBlock;
      if (blockId) {
        var block = findBlockDeep(blockId);
        if (block && state.selectedId !== blockId) {
          openPanel(block);
        }
      }
    });
    el.addEventListener('blur', function() {
      _isInlineEditing = false;
    });
  }

  // Helper: sync panel input if open
  function _syncPanel(key, value) {
    var panelInput = $panelBody.querySelector('[data-key="' + key + '"]');
    if (panelInput && panelInput.value !== value) {
      panelInput.value = value;
    }
  }

  // Helper: update toolbar button active states
  function _updateToolbarState(toolbar) {
    if (!toolbar) return;
    toolbar.querySelectorAll('.pb-text-toolbar__btn').forEach(function(btn) {
      var cmd = btn.dataset.cmd;
      var active = false;
      try { active = document.queryCommandState(cmd); } catch(e) {}
      btn.classList.toggle('is-active', active);
    });
  }

  function bindInlineEditing() {

    // ── 1. Plain text contenteditable: Heading, Quote, Button (data-inline-key) ──
    $blocks.querySelectorAll('[data-inline-key]').forEach(function(el) {
      _bindInlineFocusBlur(el);
      el.addEventListener('input', function() {
        var blockId = el.dataset.inlineBlock;
        var key = el.dataset.inlineKey;
        if (!blockId || !key) return;
        // When the user deletes everything the browser leaves a stray <br>
        // behind, which breaks the :empty CSS placeholder. Clear it so the
        // data-placeholder hint reappears immediately.
        if (el.textContent === '' && el.innerHTML !== '') { el.innerHTML = ''; }
        var block = findBlockDeep(blockId);
        if (block) {
          _setDeepKey(block, key, el.textContent);
          _syncPanel(key, el.textContent);
        }
      });
      el.addEventListener('keydown', function(e) {
        if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); el.blur(); }
      });
      el.addEventListener('paste', function(e) {
        e.preventDefault();
        var text = (e.clipboardData || window.clipboardData).getData('text/plain');
        document.execCommand('insertText', false, text);
      });
    });

    // ── 2. Rich text contenteditable + toolbar: Text, Tab content, Accordion content (data-inline-html) ──
    $blocks.querySelectorAll('[data-inline-html]').forEach(function(el) {
      _bindInlineFocusBlur(el);
      // Normalize placeholder state on bind — also fixes blocks saved by older
      // versions with a literal empty '<p></p>' as their content.
      el.classList.toggle('pb-show-placeholder', _richElIsEmpty(el));
      var zone = el.closest('.pb-rich-zone');
      var toolbar = zone ? zone.querySelector('.pb-text-toolbar') : null;

      // Toolbar: prevent mousedown from stealing focus
      if (toolbar) {
        toolbar.addEventListener('mousedown', function(e) { e.preventDefault(); });
        toolbar.addEventListener('click', function(e) {
          var btn = e.target.closest('.pb-text-toolbar__btn');
          if (!btn) return;
          var cmd = btn.dataset.cmd;
          if (cmd === 'createLink') {
            var linkRange = pbSaveSelection();
            pbPrompt(PB_I18N.link_url || 'URL:', 'https://').then(function(url) {
              url = (url || '').trim();
              if (!url || url === 'https://') return;
              pbRestoreSelection(linkRange, el);
              document.execCommand('createLink', false, url);
              _syncRichEl(el);
              _updateToolbarState(toolbar);
            });
            return;
          }
          document.execCommand(cmd, false, null);
          _updateToolbarState(toolbar);
        });
      }

      el.addEventListener('input', function() {
        _syncRichEl(el);
        _updateToolbarState(toolbar);
      });
      el.addEventListener('keyup', function() { _updateToolbarState(toolbar); });
      el.addEventListener('mouseup', function() { _updateToolbarState(toolbar); });
      el.addEventListener('paste', function(e) {
        e.preventDefault();
        var html = (e.clipboardData || window.clipboardData).getData('text/html');
        if (html) {
          var clean = _sanitizeRichPaste(html);
          document.execCommand('insertHTML', false, clean);
        } else {
          var text = (e.clipboardData || window.clipboardData).getData('text/plain');
          document.execCommand('insertText', false, text);
        }
        // Sync model + placeholder state
        _syncRichEl(el);
      });
    });

    // ── 3. Input fields: Video URL, Audio URL/Title (data-inline-input) ──
    $blocks.querySelectorAll('[data-inline-input]').forEach(function(el) {
      _bindInlineFocusBlur(el);
      el.addEventListener('input', function() {
        var blockId = el.dataset.inlineBlock;
        var key = el.dataset.inlineInput;
        if (!blockId || !key) return;
        var block = findBlockDeep(blockId);
        if (block) {
          _setDeepKey(block, key, el.value);
          _syncPanel(key, el.value);
          // Auto-extract YouTube/Vimeo ID for video blocks
          if (key === 'data.url' && block.type === 'video') {
            var yt = el.value.match(/(?:youtube\.com\/watch\?v=|youtu\.be\/)([^&?\s]+)/);
            var vm = el.value.match(/vimeo\.com\/(\d+)/);
            if (yt) { block.data.embed_id = yt[1]; block.data.source = 'youtube'; }
            else if (vm) { block.data.embed_id = vm[1]; block.data.source = 'vimeo'; }
          }
        }
      });
    });

    // ── 4. Textareas: HTML block, Code block (data-inline-textarea) ──
    $blocks.querySelectorAll('[data-inline-textarea]').forEach(function(el) {
      _bindInlineFocusBlur(el);
      el.addEventListener('input', function() {
        var blockId = el.dataset.inlineBlock;
        var key = el.dataset.inlineTextarea;
        if (!blockId || !key) return;
        var block = findBlockDeep(blockId);
        if (block) {
          _setDeepKey(block, key, el.value);
          _syncPanel(key, el.value);
        }
      });
    });

    // ── 5. Accordion titles (data-inline-acc) ──
    $blocks.querySelectorAll('[data-inline-acc]').forEach(function(el) {
      _bindInlineFocusBlur(el);
      el.addEventListener('input', function() {
        var blockId = el.dataset.inlineBlock;
        var idx = parseInt(el.dataset.inlineAcc);
        if (!blockId || isNaN(idx)) return;
        var block = findBlockDeep(blockId);
        if (block && block.data.items && block.data.items[idx]) {
          block.data.items[idx].title = el.textContent;
          markDirty();
          _syncPanel('data.items.' + idx + '.title', el.textContent);
        }
      });
      el.addEventListener('keydown', function(e) {
        if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); el.blur(); }
      });
      el.addEventListener('paste', function(e) {
        e.preventDefault();
        var text = (e.clipboardData || window.clipboardData).getData('text/plain');
        document.execCommand('insertText', false, text);
      });
    });

    // ── 7. Tab titles (data-inline-tab) ──
    $blocks.querySelectorAll('[data-inline-tab]').forEach(function(el) {
      _bindInlineFocusBlur(el);
      el.addEventListener('input', function() {
        var blockId = el.dataset.inlineBlock;
        var idx = parseInt(el.dataset.inlineTab);
        if (!blockId || isNaN(idx)) return;
        var block = findBlockDeep(blockId);
        if (block && block.data.tabs && block.data.tabs[idx]) {
          block.data.tabs[idx].title = el.textContent;
          markDirty();
          _syncPanel('data.tabs.' + idx + '.title', el.textContent);
        }
      });
      el.addEventListener('keydown', function(e) {
        if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); el.blur(); }
      });
      el.addEventListener('paste', function(e) {
        e.preventDefault();
        var text = (e.clipboardData || window.clipboardData).getData('text/plain');
        document.execCommand('insertText', false, text);
      });
    });

    // ── 8. Table headers (data-inline-th) and cells (data-inline-td) ──
    $blocks.querySelectorAll('[data-inline-th]').forEach(function(el) {
      _bindInlineFocusBlur(el);
      el.addEventListener('input', function() {
        var blockId = el.dataset.inlineBlock;
        var idx = parseInt(el.dataset.inlineTh);
        if (!blockId || isNaN(idx)) return;
        var block = findBlockDeep(blockId);
        if (block && block.data.headers) {
          block.data.headers[idx] = _sanitizeTableCell(el.innerHTML);
          markDirty();
          // Sync sidebar input (plain text)
          var si = $panelBody.querySelector('[data-table-header="' + idx + '"]');
          if (si) si.value = el.textContent;
        }
      });
    });

    $blocks.querySelectorAll('[data-inline-td]').forEach(function(el) {
      _bindInlineFocusBlur(el);
      el.addEventListener('input', function() {
        var blockId = el.dataset.inlineBlock;
        var coords = (el.dataset.inlineTd || '').split(',');
        var ri = parseInt(coords[0]), ci = parseInt(coords[1]);
        if (!blockId || isNaN(ri) || isNaN(ci)) return;
        var block = findBlockDeep(blockId);
        if (block && block.data.rows && block.data.rows[ri]) {
          block.data.rows[ri][ci] = _sanitizeTableCell(el.innerHTML);
          markDirty();
          // Sync sidebar input (plain text)
          var si = $panelBody.querySelector('[data-table-cell][data-row="' + ri + '"][data-col="' + ci + '"]');
          if (si) si.value = el.textContent;
        }
      });
    });

    // ── 8b. Table paste handler — sanitize and keep basic formatting ──
    $blocks.querySelectorAll('[data-inline-th],[data-inline-td]').forEach(function(el) {
      el.addEventListener('paste', function(e) {
        e.preventDefault();
        var html = (e.clipboardData || window.clipboardData).getData('text/html');
        if (html) {
          var clean = _sanitizeTableCell(html);
          document.execCommand('insertHTML', false, clean);
        } else {
          var text = (e.clipboardData || window.clipboardData).getData('text/plain');
          document.execCommand('insertText', false, text);
        }
      });
    });

    // ── 8c. Table compact formatting toolbar ──
    var _lastTableCell = null;
    $blocks.querySelectorAll('[data-inline-th],[data-inline-td]').forEach(function(el) {
      el.addEventListener('focus', function() { _lastTableCell = el; });
    });
    $blocks.querySelectorAll('.pb-inline-table > .pb-text-toolbar--compact').forEach(function(toolbar) {
      toolbar.addEventListener('mousedown', function(e) { e.preventDefault(); });
      toolbar.addEventListener('click', function(e) {
        var btn = e.target.closest('.pb-text-toolbar__btn');
        if (!btn || !_lastTableCell) return;
        var cmd = btn.dataset.cmd;
        _lastTableCell.focus();
        if (cmd === 'createLink') {
          var cell = _lastTableCell;
          var linkRange = pbSaveSelection();
          pbPrompt(PB_I18N.link_url || 'URL:', 'https://').then(function(url) {
            url = (url || '').trim();
            if (!url || url === 'https://') return;
            pbRestoreSelection(linkRange, cell);
            document.execCommand('createLink', false, url);
            // execCommand fires 'input' in current browsers, but dispatch
            // explicitly so the cell -> model sync never depends on it.
            cell.dispatchEvent(new Event('input', { bubbles: true }));
            _updateToolbarState(toolbar);
          });
          return;
        }
        document.execCommand(cmd, false, null);
        _updateToolbarState(toolbar);
      });
    });

    // ── 9. Table symbol picker ──
    $blocks.querySelectorAll('.pb-table-symbols').forEach(function(toolbar) {
      toolbar.addEventListener('mousedown', function(e) { e.preventDefault(); });
      toolbar.addEventListener('click', function(e) {
        var btn = e.target.closest('[data-insert-symbol]');
        if (!btn || !_lastTableCell) return;
        var sym = btn.dataset.insertSymbol;
        _lastTableCell.focus();
        document.execCommand('insertText', false, sym);
      });
    });

    // ── 10. Table canvas row inserter ──
    $blocks.querySelectorAll('[data-table-canvas-insert]').forEach(function(row) {
      row.querySelector('.pb-inserter__btn').addEventListener('click', function(e) {
        e.stopPropagation();
        var blockId = row.dataset.tableCanvasInsert;
        var idx = parseInt(row.dataset.tableCanvasIdx);
        var block = findBlockDeep(blockId);
        if (!block) return;
        var cols = block.data.headers.length;
        block.data.rows.splice(idx, 0, new Array(cols).fill(''));
        markDirty();
        renderCanvas();
        if (state.selectedId === blockId) openPanel(block);
      });
    });

    // ── 11. Table canvas row delete ──
    $blocks.querySelectorAll('[data-table-canvas-delete]').forEach(function(btn) {
      btn.addEventListener('click', function(e) {
        e.stopPropagation();
        var blockId = btn.dataset.tableCanvasDelete;
        var idx = parseInt(btn.dataset.tableCanvasDeleteIdx);
        var block = findBlockDeep(blockId);
        if (!block || !block.data.rows || block.data.rows.length <= 1) return;
        block.data.rows.splice(idx, 1);
        markDirty();
        renderCanvas();
        if (state.selectedId === blockId) openPanel(block);
      });
    });
  }

  // Warn before leaving with unsaved changes
  window.addEventListener('beforeunload', function(e) {
    if (state.isDirty) {
      e.preventDefault();
      e.returnValue = '';
    }
  });

  // ============================================================
  // INIT — Load blocks
  // ============================================================
  console.log('[PageBuilder] Initializing for page', PAGE_ID);
  console.log('[PageBuilder] PB_I18N loaded:', typeof PB_I18N !== 'undefined');
  console.log('[PageBuilder] Block buttons found:', document.querySelectorAll('.pb-block-btn').length);

  // Expose state for AI integration
  window._pbState = state;
  window._pbDefaultAlignFor = defaultAlignFor;
  window._pbSetBlocks = function(blocks) {
    // AI "Apply" replaces the whole canvas, so it must be a first-class,
    // synchronous undo step. Relying on markDirty's 800ms debounced
    // snapshot leaves a race: Ctrl+Z pressed within that window restores
    // an older snapshot and overwrites _lastSavedState — the pre-Apply
    // layout is lost for good (and a manual edit made <800ms before Apply
    // never reaches the history at all). Flushing via _saveUndoState()
    // BEFORE the swap captures the exact pre-Apply state (including any
    // pending edit), and calling it again AFTER pushes that state onto
    // the undo stack immediately — one Ctrl+Z always returns to it.
    _saveUndoState();
    state.blocks = blocks;
    _saveUndoState();
    markDirty();
    renderCanvas();
    showToast((PB_I18N.ai_blocks_loaded || 'AI blocks loaded — {count} blocks').replace('{count}', blocks.length), 'success');
  };

  loadBlocks();

})();
</script>

<!-- ====== AI ASSISTANT FOR BUILDER ====== -->
<div class="pb-ai-overlay" id="pbAIOverlay">
  <div class="pb-ai-modal">
    <div class="pb-ai-modal__header">
      <h3>🤖 AI — {{ t('ai.page_builder') }}</h3>
      <button class="pb-ai-modal__close" onclick="closeBuilderAI()">&times;</button>
    </div>
    <div class="pb-ai-modal__body">
      <div class="pb-ai-chat" id="pbAIChat"></div>
      <div class="pb-ai-input-row">
        <textarea class="pb-ai-textarea" id="pbAIInput" rows="2" placeholder="{{ t('builder.ai_placeholder') }}"
          onkeydown="if(event.key==='Enter'&&!event.shiftKey){event.preventDefault();sendBuilderAI()}"></textarea>
        <button class="pb-ai-send" id="pbAISend" onclick="sendBuilderAI()">{{ t('ai.send') }}</button>
      </div>
      <div class="pb-ai-actions" id="pbAIActions">
        <button class="pb-ai-apply" id="pbAIApply" onclick="applyBuilderAI()">✅ {{ t('builder.ai_apply_blocks') }}</button>
        <button class="pb-ai-cancel" onclick="closeBuilderAI()">{{ t('common.cancel') }}</button>
      </div>
    </div>
  </div>
</div>

<script>
(function() {
  var overlay = document.getElementById('pbAIOverlay');
  var chat = document.getElementById('pbAIChat');
  var input = document.getElementById('pbAIInput');
  var sendBtn = document.getElementById('pbAISend');
  var actionsDiv = document.getElementById('pbAIActions');
  var aiState = { messages: [], lastParsed: null, busy: false };

  // Auto-resize textarea
  input.addEventListener('input', function() {
    this.style.height = 'auto';
    this.style.height = Math.min(this.scrollHeight, 300) + 'px';
  });

  function escHtml(s) { var d = document.createElement('div'); d.textContent = s || ''; return d.innerHTML; }

  function addMsg(role, text) {
    var div = document.createElement('div');
    div.className = 'pb-ai-msg pb-ai-msg--' + role;
    div.innerHTML = text;
    chat.appendChild(div);
    chat.scrollTop = chat.scrollHeight;
    aiState.messages.push({ role: role, text: text });
    return div;
  }

  window.openBuilderAI = function() {
    aiState = { messages: [], lastParsed: null, busy: false };
    chat.innerHTML = '';
    actionsDiv.style.display = 'none';
    input.value = '';
    input.style.height = 'auto';
    input.disabled = false;
    sendBtn.disabled = false;
    addMsg('ai', PB_I18N.ai_welcome || 'Tell me what page you\'d like to build! Describe the purpose, sections, and content. The more detail you give, the better the result.');
    overlay.classList.add('is-open');
    setTimeout(function() { input.focus(); }, 100);
  };

  window.closeBuilderAI = function() {
    overlay.classList.remove('is-open');
  };

  window.sendBuilderAI = function() {
    if (aiState.busy) return;
    var text = input.value.trim();
    if (!text) return;
    addMsg('user', escHtml(text));
    input.value = '';
    input.style.height = 'auto';
    input.disabled = true;
    sendBtn.disabled = true;
    aiState.busy = true;

    var loading = addMsg('ai', PB_I18N.ai_thinking || 'Thinking...');
    loading.classList.add('pb-ai-msg--loading');

    // Build conversation history
    var history = aiState.messages.filter(function(m) {
      return m.role === 'user' || m.role === 'ai';
    }).slice(0, -1).map(function(m) {
      return (m.role === 'user' ? 'User' : 'Assistant') + ': ' + m.text;
    }).join('\n');

    fetch('/admin/ai/generate', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        task: 'builder_blocks',
        prompt: text,
        content_type: 'page',
        language: '{{ page.get_language_name() if page.get_language_name else "English" }}',
        title: '{{ page.title|e }}',
        conversation_history: history
      })
    })
    .then(function(r) { return r.json(); })
    .then(function(data) {
      loading.remove();
      aiState.messages.pop();

      if (data.error) {
        addMsg('system', '❌ ' + escHtml(data.error));
        return;
      }

      try {
        var jsonStr = data.result;
        var match = jsonStr.match(/```(?:json)?\s*([\s\S]*?)```/);
        if (match) jsonStr = match[1];
        var parsed = JSON.parse(jsonStr.trim());
        aiState.lastParsed = parsed;

        if (parsed.ai_message) {
          addMsg('ai', escHtml(parsed.ai_message));
        }

        var blockCount = (parsed.blocks || []).length;
        if (blockCount > 0) {
          addMsg('ai', (PB_I18N.ai_generated || '✅ Generated <strong>{count} blocks</strong>. Click "Apply" to load them into the canvas.').replace('{count}', blockCount));
          actionsDiv.style.display = 'flex';
        } else {
          addMsg('ai', PB_I18N.ai_no_blocks || 'No blocks generated. Try being more specific about the page structure.');
        }
      } catch (e) {
        // AI responded with text, not JSON — show as conversation
        addMsg('ai', escHtml(data.result).replace(/\n/g, '<br>'));
      }
    })
    .catch(function(err) {
      loading.remove();
      aiState.messages.pop();
      addMsg('system', '❌ ' + escHtml(err.message));
    })
    .finally(function() {
      input.disabled = false;
      sendBtn.disabled = false;
      aiState.busy = false;
      input.focus();
    });
  };

  window.applyBuilderAI = function() {
    if (!aiState.lastParsed || !aiState.lastParsed.blocks) return;
    var newBlocks = aiState.lastParsed.blocks;

    // Ensure all blocks have IDs and the same default alignment a
    // human-created block gets (see defaultAlignFor) — so the canvas
    // preview matches exactly what the save will store.
    function ensureBlockBasics(b) {
      if (!b.id) b.id = 'b' + Math.random().toString(36).substr(2, 9);
      if (!b.settings) b.settings = {};
      if (!b.settings.alignment) {
        b.settings.alignment = window._pbDefaultAlignFor
          ? window._pbDefaultAlignFor(b.type) : 'center';
      }
    }
    newBlocks.forEach(function(b) {
      ensureBlockBasics(b);
      // Fix nested column blocks
      if (b.type === 'columns' && b.data && b.data.columns) {
        b.data.columns.forEach(function(col) {
          (col.blocks || []).forEach(ensureBlockBasics);
        });
      }
      // Fix nested section blocks
      if (b.type === 'section' && b.data && b.data.blocks) {
        b.data.blocks.forEach(ensureBlockBasics);
      }
      // Fix nested slideshow blocks (hero mode — blocks inside slides)
      if (b.type === 'slideshow' && b.data && b.data.slides) {
        b.data.slides.forEach(function(slide) {
          (slide.blocks || []).forEach(ensureBlockBasics);
        });
      }
    });

    if (window._pbSetBlocks) {
      window._pbSetBlocks(newBlocks);
    }
    closeBuilderAI();
  };

  // Close on overlay click
  overlay.addEventListener('click', function(e) { if (e.target === overlay) closeBuilderAI(); });
  document.addEventListener('keydown', function(e) { if (e.key === 'Escape' && overlay.classList.contains('is-open')) closeBuilderAI(); });
})();
</script>

</body>
</html>
