/**
 * Aura Builder — editor fallback (vanilla JS).
 * Document tree = source of truth.
 * - Import Elementor/HTML on first open
 * - Selection toolbar + resize handles + drag reorder
 */
(function () {
  "use strict";

  var CFG = window.AuraBuilderConfig || {};
  var postId = CFG.postId;
  var restUrl = (CFG.restUrl || "").replace(/\/$/, "");
  var nonce = CFG.nonce || "";
  var backUrl = CFG.backUrl || "admin.php?page=aura-builder-pages";
  var editorMode = CFG.editorMode || "page"; // page | header | footer | single
  var isThemePart = editorMode === "header" || editorMode === "footer" || editorMode === "single";
  // theme part meta (display conditions, sticky, etc.)
  var themePartMeta = {
    enabled: true,
    display: "all",
    include_ids: "",
    exclude_ids: "",
    source: "builder",
    sticky: "0",
    sticky_bg: "#ffffff",
    transparent: "0",
  };

  /** Widget catalog — Elementor-like groups + SVG icon keys */
  var CATALOG = [
    // —— Layout (estrutural) ——
    {
      type: "section",
      title: "Seção",
      group: "layout",
      subgroup: "sections",
      icon: "section",
      container: true,
      defaults: { layout: "full", direction: "grid", columns: 1, gap: "16px" },
    },
    {
      type: "section",
      title: "Seção interna",
      group: "layout",
      subgroup: "internal",
      icon: "section",
      container: true,
      defaults: { layout: "boxed", direction: "grid", columns: 1, gap: "16px" },
    },
    {
      type: "container",
      title: "Container",
      group: "layout",
      subgroup: "containers",
      icon: "container",
      container: true,
      defaults: { display: "flex", direction: "column", gap: "16px", width: "100%", col_span: 12 },
    },
    {
      type: "container",
      title: "Linha (row)",
      group: "layout",
      subgroup: "containers",
      icon: "container",
      container: true,
      defaults: { display: "flex", direction: "row", gap: "16px", width: "100%", col_span: 12 },
    },
    {
      type: "container",
      title: "Coluna",
      group: "layout",
      subgroup: "containers",
      icon: "container",
      container: true,
      defaults: { display: "flex", direction: "column", gap: "12px", width: "50%", col_span: 6 },
    },
    {
      type: "sidebar",
      title: "Barra lateral",
      group: "layout",
      subgroup: "aside",
      icon: "sidebar",
      defaults: { sidebar: "sidebar-1" },
    },
    // Basic — Elementor-like full set (sem duplicar layout)
    { type: "heading", title: "Título", group: "basic", icon: "heading", defaults: { text: "Novo título", tag: "h2", align: "left" } },
    { type: "text", title: "Editor de texto", group: "basic", icon: "text", defaults: { text: "Escreva seu texto aqui.", align: "left" } },
    { type: "button", title: "Botão", group: "basic", icon: "button", defaults: { text: "Saiba mais", url: "#", align: "left" } },
    { type: "star_rating", title: "Avaliação", group: "basic", icon: "star", defaults: { rating: "5", max: "5", align: "left", show_score: "1" } },
    { type: "divider", title: "Divisor", group: "basic", icon: "divider", defaults: {} },
    { type: "google_maps", title: "Google Maps", group: "basic", icon: "maps", defaults: { address: "São Paulo, Brasil", zoom: "14", height: "320" } },
    { type: "icon", title: "Ícone", group: "basic", icon: "icon", defaults: { icon: "★", size: "32", align: "center", link: "" } },
    { type: "icon_box", title: "Caixa de ícone", group: "basic", icon: "icon_box", defaults: { icon: "◆", title: "Título", description: "Descrição da caixa.", link: "", align: "center" } },
    { type: "icon_list", title: "Lista de ícones", group: "basic", icon: "icon_list", defaults: { items: "✓ Item um\n✓ Item dois\n✓ Item três", align: "left" } },
    {
      type: "counter",
      title: "Contador",
      group: "basic",
      icon: "counter",
      defaults: {
        number: "100",
        prefix: "",
        suffix: "+",
        title: "Clientes",
        align: "center",
        animate: "1",
        duration: "1600",
      },
    },
    { type: "spacer", title: "Espaço", group: "basic", icon: "spacer", defaults: { height: 40 } },
    { type: "testimonial", title: "Depoimento", group: "basic", icon: "testimonial", defaults: { content: "Excelente escola e equipe dedicada.", name: "Maria Silva", role: "Responsável", image: "", align: "left" } },
    {
      type: "tabs",
      title: "Abas",
      group: "basic",
      icon: "tabs",
      defaults: {
        items: JSON.stringify([
          { title: "Aba 1", content: "Conteúdo da primeira aba.", image: "" },
          { title: "Aba 2", content: "Conteúdo da segunda aba.", image: "" },
        ]),
      },
    },
    {
      type: "accordion",
      title: "Acordeão",
      group: "basic",
      icon: "accordion",
      defaults: {
        source: "manual",
        open_first: "1",
        items: JSON.stringify([
          { title: "Pergunta 1", content: "Resposta um.", image: "" },
          { title: "Pergunta 2", content: "Resposta dois.", image: "" },
        ]),
        query_source: "post",
        posts_per_page: "6",
      },
    },
    {
      type: "toggle",
      title: "Toggle",
      group: "basic",
      icon: "toggle",
      defaults: {
        open_first: "0",
        items: JSON.stringify([
          { title: "Clique para expandir", content: "Conteúdo oculto.", image: "" },
        ]),
      },
    },
    {
      type: "social_icons",
      title: "Ícones sociais",
      group: "basic",
      icon: "social",
      defaults: {
        items: JSON.stringify([
          { network: "facebook", url: "https://facebook.com", label: "" },
          { network: "instagram", url: "https://instagram.com", label: "" },
          { network: "youtube", url: "https://youtube.com", label: "" },
        ]),
        align: "left",
        shape: "circle",
        show_label: "0",
        size: "40",
      },
    },
    { type: "progress_bar", title: "Barra de progresso", group: "basic", icon: "progress", defaults: { title: "Progresso", percent: "75", show_pct: "1" } },
    { type: "shortcode", title: "Shortcode", group: "basic", icon: "shortcode", defaults: { code: "" } },
    { type: "html", title: "HTML", group: "basic", icon: "html", defaults: { code: "<p>HTML</p>" } },
    { type: "menu_anchor", title: "Âncora de menu", group: "basic", icon: "anchor", defaults: { anchor: "secao" } },
    { type: "alert", title: "Alerta", group: "basic", icon: "alert", defaults: { title: "Atenção", content: "Mensagem importante.", type: "info" } },
    { type: "text_path", title: "Texto em caminho", group: "basic", icon: "text_path", defaults: { text: "Aura Builder · Texto em caminho", align: "center" } },
    // Mídias — imagens, vídeo, galeria, embeds
    { type: "image", title: "Imagem", group: "media", icon: "image", defaults: { url: "", alt: "", align: "center" } },
    { type: "video", title: "Vídeo", group: "media", icon: "video", defaults: { url: "", align: "center", autoplay: "0" } },
    {
      type: "image_box",
      title: "Caixa de imagem",
      group: "media",
      icon: "image_box",
      defaults: {
        url: "",
        title: "Título",
        description: "Descrição da caixa.",
        link: "",
        align: "left",
        image_position: "top",
        image_height: "",
        object_fit: "cover",
        object_position: "50% 50%",
        radius: "12",
        shadow: "0",
        border: "0",
        border_color: "#e2e8f0",
        media_width: "40",
      },
    },
    {
      type: "gallery",
      title: "Galeria",
      group: "media",
      icon: "gallery",
      defaults: {
        images: "[]",
        columns: "3",
        gap: "8",
        layout: "grid",
        autoplay: "1",
        autoplay_speed: "4500",
      },
    },
    { type: "carousel", title: "Carrossel", group: "media", icon: "carousel", defaults: { images: "[]", height: "280" } },
    {
      type: "popup",
      title: "Popup",
      group: "media",
      icon: "popup",
      defaults: {
        mode: "image",
        images: "[]",
        scope: "current",
        frequency: "session",
        delay: "800",
        slide_time: "4000",
        autoplay: "1",
        show_close: "1",
        overlay: "1",
        max_width: "720",
        open_effect: "fadein",
        slide_effect: "fade",
        enabled: "1",
      },
    },
    { type: "embed", title: "SoundCloud", group: "media", icon: "soundcloud", defaults: { provider: "oembed", url: "", align: "center" } },
    // Dynamic (site / post)
    // Loop / Query Grid (Elementor Loop Grid)
    {
      type: "loop_grid",
      title: "Loop de consulta",
      group: "dynamic",
      icon: "loop",
      defaults: {
        template_type: "posts",
        columns: "3",
        columns_tablet: "2",
        columns_mobile: "1",
        gap: "24",
        posts_per_page: "6",
        show_image: "1",
        image_position: "top",
        title_position: "outside",
        show_title: "1",
        show_date: "1",
        show_author: "1",
        show_excerpt: "1",
        show_read_more: "1",
        card_shadow: "0",
        card_border: "1",
        as_slider: "0",
        excerpt_length: "18",
        read_more_text: "Leia mais",
        image_size: "large",
        query_source: "post",
        orderby: "date",
        order: "DESC",
        offset: "0",
        include_ids: "",
        exclude_ids: "",
        category: "",
        tag: "",
        author: "",
        ignore_sticky: "1",
        pagination: "0",
      },
    },
    {
      type: "comments_form",
      title: "Formulário de comentários",
      group: "dynamic",
      icon: "comments",
      defaults: {
        title: "Deixe um comentário",
        show_title: "1",
        show_list: "1",
        list_title: "Comentários",
        submit_label: "Publicar comentário",
      },
    },
    { type: "dynamic", title: "Navegação", group: "dynamic", icon: "nav", defaults: { source: "navigation", align: "left", link: "1" } },
    { type: "dynamic", title: "Logo do site", group: "dynamic", icon: "logo", defaults: { source: "site_logo", align: "left", link: "1" } },
    { type: "dynamic", title: "Título do site", group: "dynamic", icon: "siteTitle", defaults: { source: "site_title", align: "left", link: "1" } },
    { type: "dynamic", title: "Descrição do site", group: "dynamic", icon: "tagline", defaults: { source: "site_tagline", align: "left", link: "0" } },
    { type: "dynamic", title: "Avatar", group: "dynamic", icon: "avatar", defaults: { source: "avatar", align: "center", link: "0" } },
    // Single / arquivo (como FSE: título, imagem, conteúdo, comentários…).
    { type: "dynamic", title: "Título do post", group: "dynamic", icon: "heading", defaults: { source: "post_title", align: "left", link: "0" } },
    { type: "dynamic", title: "Imagem destacada", group: "dynamic", icon: "image", defaults: { source: "featured_image", align: "center", link: "0" } },
    { type: "dynamic", title: "Conteúdo do post", group: "dynamic", icon: "text", defaults: { source: "post_content", align: "left", link: "0" } },
    { type: "dynamic", title: "Resumo", group: "dynamic", icon: "text", defaults: { source: "post_excerpt", align: "left", link: "0" } },
    { type: "dynamic", title: "Data", group: "dynamic", icon: "date", defaults: { source: "post_date", align: "left", link: "0" } },
    { type: "dynamic", title: "Data modificada", group: "dynamic", icon: "date", defaults: { source: "post_modified", align: "left", link: "0" } },
    { type: "dynamic", title: "Nome do autor", group: "dynamic", icon: "avatar", defaults: { source: "post_author", align: "left", link: "1" } },
    { type: "dynamic", title: "Biografia do autor", group: "dynamic", icon: "text", defaults: { source: "author_bio", align: "left", link: "0" } },
    { type: "dynamic", title: "Contagem de comentários", group: "dynamic", icon: "comments", defaults: { source: "comment_count", align: "left", link: "0" } },
    { type: "dynamic", title: "Categorias", group: "dynamic", icon: "tags", defaults: { source: "categories", align: "left", link: "1" } },
    { type: "dynamic", title: "Tags", group: "dynamic", icon: "tags", defaults: { source: "tags", align: "left", link: "1" } },
    { type: "dynamic", title: "Post anterior", group: "dynamic", icon: "prev", defaults: { source: "prev_post", align: "left", link: "1" } },
    { type: "dynamic", title: "Próximo post", group: "dynamic", icon: "next", defaults: { source: "next_post", align: "left", link: "1" } },
    { type: "dynamic", title: "Tempo de leitura", group: "dynamic", icon: "time", defaults: { source: "read_time", align: "left", link: "0" } },
    { type: "dynamic", title: "Contagem de palavras", group: "dynamic", icon: "words", defaults: { source: "word_count", align: "left", link: "0" } },
    { type: "dynamic", title: "Leia mais", group: "dynamic", icon: "readMore", defaults: { source: "read_more", align: "left", link: "1" } },
    { type: "dynamic", title: "Caminho de navegação", group: "dynamic", icon: "breadcrumbs", defaults: { source: "breadcrumbs", align: "left", link: "1" } },
    { type: "dynamic", title: "Acessar/Sair", group: "dynamic", icon: "login", defaults: { source: "login_logout", align: "left", link: "1" } },
    { type: "dynamic", title: "Título do arquivo", group: "dynamic", icon: "heading", defaults: { source: "archive_title", align: "left", link: "0" } },
    { type: "dynamic", title: "Título da busca", group: "dynamic", icon: "heading", defaults: { source: "search_title", align: "left", link: "0" } },
    { type: "dynamic", title: "Nome do termo", group: "dynamic", icon: "tags", defaults: { source: "term_name", align: "left", link: "0" } },
    { type: "dynamic", title: "Descrição do termo", group: "dynamic", icon: "text", defaults: { source: "term_description", align: "left", link: "0" } },
    { type: "dynamic", title: "Contagem de termos", group: "dynamic", icon: "words", defaults: { source: "term_count", align: "left", link: "0" } },
    {
      type: "page_content",
      title: "Conteúdo de página",
      group: "dynamic",
      icon: "page",
      defaults: { page_id: "", source_type: "page", show_title: "0" },
    },
    // Creative (Turbo Addons–inspired)
    {
      type: "creative_cards",
      title: "Grade criativa",
      group: "creative",
      icon: "creative",
      defaults: {
        columns: "4",
        columns_tablet: "2",
        columns_mobile: "1",
        gap: "16",
        height: "160",
        radius: "16",
        open_first: "0",
        items:
          "image||||Retrato\ncolor|#e74c3c|▶|YouTube|Editor nativo|Hello Title||Clique no card para ver o conteúdo.\ncolor|#9b59b6|👤|John Smith|Web Developer|Perfil||Desenvolvedor full-stack.\ncolor|#2ecc71|👤|Karen Pena|Photographer|Galeria||Fotografia de produto e retrato.\nimage||||Moda\nimage||||Street\ncolor|#3498db|👤|Estilo|Custom color|Built-in style||Com cor personalizada.\ncolor|#2c3e50|⬇|Download|CodeCanyon|Plugin||Baixe e personalize.",
      },
    },
    {
      type: "advanced_accordion",
      title: "Acordeão avançado",
      group: "creative",
      icon: "accordion",
      defaults: {
        items:
          "Missão|Formar cidadãos críticos e solidários com excelência acadêmica.|🎯|#7c3aed\nVisão|Ser referência em educação integral na região.|👁|#2563eb\nValores|Respeito, responsabilidade, criatividade e colaboração.|💎|#059669",
        style: "modern",
        multiple: "0",
        open_first: "1",
      },
    },
    {
      type: "preview_card",
      title: "Card de preview",
      group: "creative",
      icon: "image_box",
      defaults: {
        image: "",
        title: "Feira das Nações",
        subtitle: "Evento escolar",
        text: "Uma imersão cultural com apresentações, gastronomia e oficinas para toda a família.",
        button_text: "Saiba mais",
        button_url: "#",
        style: "hover",
        radius: "16",
      },
    },
    {
      type: "info_box",
      title: "Caixa de informação",
      group: "creative",
      icon: "icon_box",
      defaults: {
        icon: "📚",
        title: "Ensino de excelência",
        text: "Metodologia ativa, professores qualificados e acompanhamento individual do aluno.",
        button_text: "Conhecer",
        button_url: "#",
        style: "card",
        accent: "#7c3aed",
        align: "center",
      },
    },
    {
      type: "social_bar",
      title: "Barra social",
      group: "creative",
      icon: "social",
      defaults: {
        items:
          "Facebook|https://facebook.com\nInstagram|https://instagram.com\nYouTube|https://youtube.com\nWhatsApp|https://wa.me/\nLinkedIn|https://linkedin.com",
        direction: "row",
        shape: "circle",
        show_label: "0",
        align: "center",
        size: "44",
      },
    },
    {
      type: "text_animation",
      title: "Animação de texto",
      group: "creative",
      icon: "text_path",
      defaults: {
        prefix: "Somos o ",
        words: "Colégio JM Monteiro|futuro|excelência|cuidado",
        suffix: "",
        effect: "typing",
        speed: "80",
        pause: "2000",
        tag: "h2",
        align: "center",
      },
    },
    {
      type: "image_float",
      title: "Imagem flutuante",
      group: "media",
      icon: "image",
      defaults: {
        image: "",
        alt: "",
        effect: "float",
        height: "320",
        radius: "16",
        shadow: "1",
      },
    },
    {
      type: "image_overlay",
      title: "Sobreposição de imagem",
      group: "media",
      icon: "image_box",
      defaults: {
        image: "",
        title: "Infantil",
        text: "Descoberta e brincadeira com propósito",
        link: "#",
        effect: "fade",
        color: "#0f172a",
        opacity: "70",
        height: "280",
        radius: "16",
      },
    },
    {
      type: "image_carousel_pro",
      title: "Carrossel de imagens",
      group: "media",
      icon: "carousel",
      defaults: {
        images:
          "|Infantil\n|Sala de aula\n|Esportes",
        height: "360",
        autoplay: "1",
        dots: "1",
        arrows: "1",
        interval: "4500",
        radius: "12",
      },
    },
    {
      type: "nav_menu",
      title: "Menu de navegação",
      group: "basic",
      icon: "list",
      defaults: {
        source: "wp",
        menu_id: "0",
        items:
          "Início|/\nSegmentos|#\n> Educação Infantil|#\n> Anos Iniciais|#\n> Anos Finais|#\n> Ensino Médio|#\nContato|#",
        layout: "horizontal",
        style: "underline",
        align: "center",
        gap: "8",
        accent: "#eab308",
        mobile_style: "drawer",
        show_toggle: "1",
        toggle_label: "Menu",
      },
    },
    // Mídias (embeds externos)
    { type: "embed", title: "YouTube", group: "media", icon: "youtube", defaults: { provider: "youtube", url: "", align: "center" } },
    { type: "embed", title: "X mídia", group: "media", icon: "x", defaults: { provider: "x", url: "", align: "center" } },
    { type: "embed", title: "Mídia incorporada", group: "media", icon: "embed", defaults: { provider: "oembed", url: "", align: "center" } },
  ];
  var CATALOG_GROUPS = [
    { id: "layout", title: "Layout", icon: "layout" },
    { id: "basic", title: "Básicos", icon: "basic" },
    { id: "media", title: "Mídias", icon: "image" },
    { id: "creative", title: "Criativo", icon: "creative" },
    { id: "dynamic", title: "Dinâmicos", icon: "dynamic" },
  ];

  /** Subgrupos na aba Layout (separados por linha horizontal). */
  var LAYOUT_SUBGROUPS = [
    { id: "sections", title: "Seções" },
    { id: "internal", title: "Seção interna" },
    { id: "containers", title: "Containers · Linhas · Colunas" },
    { id: "aside", title: "Barra lateral" },
  ];

  /** Category titles for template library (pages). */
  var TPL_CATEGORY_LABELS = {
    education: "Escolar",
    business: "Empresa",
    blog: "Blog",
    news: "Notícias",
    restaurant: "Restaurante",
    realestate: "Imobiliária",
    portfolio: "Portfólio",
    landing: "Landing",
    general: "Geral",
    custom: "Personalizado",
  };
  var TPL_CATEGORY_ORDER = [
    "education",
    "business",
    "blog",
    "news",
    "restaurant",
    "realestate",
    "portfolio",
    "landing",
    "general",
    "custom",
  ];

  /** Property controls per widget — always selects for enums (PT labels). */
  var ALIGN_OPTS = [
    { value: "left", label: "Esquerda" },
    { value: "center", label: "Centro" },
    { value: "right", label: "Direita" },
    { value: "justify", label: "Justificado" },
  ];
  var FONT_FAMILY_OPTS = [
    { value: "", label: "Padrão do tema" },
    { value: "inherit", label: "Herdar" },
    { value: "system-ui, -apple-system, Segoe UI, Roboto, sans-serif", label: "System UI" },
    { value: "Georgia, 'Times New Roman', serif", label: "Serif (Georgia)" },
    { value: "'Times New Roman', Times, serif", label: "Times" },
    { value: "Arial, Helvetica, sans-serif", label: "Arial" },
    { value: "'Helvetica Neue', Helvetica, Arial, sans-serif", label: "Helvetica" },
    { value: "Verdana, Geneva, sans-serif", label: "Verdana" },
    { value: "Tahoma, Geneva, sans-serif", label: "Tahoma" },
    { value: "'Trebuchet MS', Helvetica, sans-serif", label: "Trebuchet" },
    { value: "'Courier New', Courier, monospace", label: "Courier" },
    { value: "var(--wp--preset--font-family--system-font, system-ui)", label: "Tema · System" },
    { value: "var(--wp--preset--font-family--body, inherit)", label: "Tema · Body" },
    { value: "var(--wp--preset--font-family--heading, inherit)", label: "Tema · Heading" },
    // Popular Google Fonts (loaded on demand)
    { value: "Roboto, sans-serif", label: "Google · Roboto" },
    { value: "'Open Sans', sans-serif", label: "Google · Open Sans" },
    { value: "Lato, sans-serif", label: "Google · Lato" },
    { value: "Montserrat, sans-serif", label: "Google · Montserrat" },
    { value: "Poppins, sans-serif", label: "Google · Poppins" },
    { value: "Oswald, sans-serif", label: "Google · Oswald" },
    { value: "Raleway, sans-serif", label: "Google · Raleway" },
    { value: "Nunito, sans-serif", label: "Google · Nunito" },
    { value: "Merriweather, serif", label: "Google · Merriweather" },
    { value: "'Playfair Display', serif", label: "Google · Playfair Display" },
    { value: "Inter, sans-serif", label: "Google · Inter" },
    { value: "'Work Sans', sans-serif", label: "Google · Work Sans" },
    { value: "Ubuntu, sans-serif", label: "Google · Ubuntu" },
    { value: "Rubik, sans-serif", label: "Google · Rubik" },
    { value: "'DM Sans', sans-serif", label: "Google · DM Sans" },
    { value: "Manrope, sans-serif", label: "Google · Manrope" },
    { value: "Outfit, sans-serif", label: "Google · Outfit" },
    { value: "Quicksand, sans-serif", label: "Google · Quicksand" },
    { value: "'Josefin Sans', sans-serif", label: "Google · Josefin Sans" },
    { value: "Barlow, sans-serif", label: "Google · Barlow" },
    { value: "'Source Sans 3', sans-serif", label: "Google · Source Sans 3" },
    { value: "custom", label: "Personalizado / outra Google Font…" },
  ];

  var GOOGLE_FONT_NAMES = [
    "Roboto", "Open Sans", "Lato", "Montserrat", "Poppins", "Oswald", "Raleway",
    "Nunito", "Merriweather", "Playfair Display", "Inter", "Work Sans", "Ubuntu",
    "Rubik", "DM Sans", "Manrope", "Outfit", "Quicksand", "Josefin Sans", "Barlow",
    "Source Sans 3", "Noto Sans", "PT Sans", "Fira Sans", "Mukta", "Mulish",
    "Cabin", "Karla", "Inconsolata", "Libre Baskerville", "Bebas Neue", "Dancing Script",
    "Pacifico", "Comfortaa", "Exo 2", "Titillium Web", "Crimson Text", "IBM Plex Sans",
  ];

  /** Load a Google Font stylesheet into the editor (and remember for paint). */
  function ensureGoogleFont(familyCss) {
    if (!familyCss) return;
    var first = String(familyCss).split(",")[0].replace(/["']/g, "").trim();
    if (!first || first.indexOf("var(") === 0) return;
    var system = /^(system-ui|georgia|arial|helvetica|times|verdana|tahoma|trebuchet|courier|inherit|serif|sans-serif|monospace|segoes ui)/i;
    if (system.test(first)) return;
    var id = "ab-gf-" + first.toLowerCase().replace(/\s+/g, "-");
    if (document.getElementById(id)) return;
    var link = document.createElement("link");
    link.id = id;
    link.rel = "stylesheet";
    link.href =
      "https://fonts.googleapis.com/css2?family=" +
      encodeURIComponent(first).replace(/%20/g, "+") +
      ":wght@300;400;500;600;700;800&display=swap";
    document.head.appendChild(link);
  }

  /** Expanded Google Fonts catalog for the picker popup. */
  var GOOGLE_FONTS_CATALOG = [
    { name: "Roboto", category: "Sans" },
    { name: "Open Sans", category: "Sans" },
    { name: "Lato", category: "Sans" },
    { name: "Montserrat", category: "Sans" },
    { name: "Poppins", category: "Sans" },
    { name: "Oswald", category: "Sans" },
    { name: "Raleway", category: "Sans" },
    { name: "Nunito", category: "Sans" },
    { name: "Inter", category: "Sans" },
    { name: "Work Sans", category: "Sans" },
    { name: "Ubuntu", category: "Sans" },
    { name: "Rubik", category: "Sans" },
    { name: "DM Sans", category: "Sans" },
    { name: "Manrope", category: "Sans" },
    { name: "Outfit", category: "Sans" },
    { name: "Quicksand", category: "Sans" },
    { name: "Josefin Sans", category: "Sans" },
    { name: "Barlow", category: "Sans" },
    { name: "Source Sans 3", category: "Sans" },
    { name: "Noto Sans", category: "Sans" },
    { name: "PT Sans", category: "Sans" },
    { name: "Fira Sans", category: "Sans" },
    { name: "Mukta", category: "Sans" },
    { name: "Mulish", category: "Sans" },
    { name: "Cabin", category: "Sans" },
    { name: "Karla", category: "Sans" },
    { name: "Exo 2", category: "Sans" },
    { name: "Titillium Web", category: "Sans" },
    { name: "IBM Plex Sans", category: "Sans" },
    { name: "Nunito Sans", category: "Sans" },
    { name: "Figtree", category: "Sans" },
    { name: "Plus Jakarta Sans", category: "Sans" },
    { name: "Merriweather", category: "Serif" },
    { name: "Playfair Display", category: "Serif" },
    { name: "Lora", category: "Serif" },
    { name: "PT Serif", category: "Serif" },
    { name: "Libre Baskerville", category: "Serif" },
    { name: "Crimson Text", category: "Serif" },
    { name: "Source Serif 4", category: "Serif" },
    { name: "EB Garamond", category: "Serif" },
    { name: "Cormorant Garamond", category: "Serif" },
    { name: "Bitter", category: "Serif" },
    { name: "Inconsolata", category: "Mono" },
    { name: "Roboto Mono", category: "Mono" },
    { name: "Source Code Pro", category: "Mono" },
    { name: "JetBrains Mono", category: "Mono" },
    { name: "Fira Code", category: "Mono" },
    { name: "Space Mono", category: "Mono" },
    { name: "Dancing Script", category: "Display" },
    { name: "Pacifico", category: "Display" },
    { name: "Bebas Neue", category: "Display" },
    { name: "Comfortaa", category: "Display" },
    { name: "Lobster", category: "Display" },
    { name: "Abril Fatface", category: "Display" },
    { name: "Righteous", category: "Display" },
    { name: "Fredoka", category: "Display" },
  ];

  /**
   * Google Fonts picker popup with live preview of each face.
   * onPick(cssFamilyValue) e.g. '"Poppins", sans-serif'
   */
  function openGoogleFontsPicker(currentName, onPick) {
    var existing = document.getElementById("ab-gf-modal");
    if (existing) existing.remove();

    var modal = document.createElement("div");
    modal.id = "ab-gf-modal";
    modal.className = "ab-gf-modal";
    modal.innerHTML =
      '<div class="ab-gf-modal__panel" role="dialog" aria-label="Google Fonts">' +
      '<div class="ab-gf-modal__head">' +
      "<div><strong>Google Fonts</strong>" +
      '<p class="ab-gf-modal__sub">Pesquise e visualize antes de aplicar</p></div>' +
      '<button type="button" class="ab-gf-modal__close" aria-label="Fechar">×</button>' +
      "</div>" +
      '<div class="ab-gf-modal__toolbar">' +
      '<input type="search" class="ab-gf-modal__search" placeholder="Pesquisar fonte…" autocomplete="off"/>' +
      '<div class="ab-gf-modal__cats">' +
      '<button type="button" class="ab-gf-cat is-on" data-cat="all">Todas</button>' +
      '<button type="button" class="ab-gf-cat" data-cat="Sans">Sans</button>' +
      '<button type="button" class="ab-gf-cat" data-cat="Serif">Serif</button>' +
      '<button type="button" class="ab-gf-cat" data-cat="Mono">Mono</button>' +
      '<button type="button" class="ab-gf-cat" data-cat="Display">Display</button>' +
      "</div>" +
      '<input type="text" class="ab-gf-modal__sample" value="Colégio JM Monteiro — ABC abc 123" placeholder="Texto de exemplo"/>' +
      "</div>" +
      '<div class="ab-gf-modal__grid"></div>' +
      '<div class="ab-gf-modal__foot">' +
      '<span class="ab-gf-modal__hint">Clique numa fonte para aplicar · duplo clique também fecha</span>' +
      '<button type="button" class="ab-btn ab-btn--primary ab-gf-modal__apply" disabled>Aplicar fonte</button>' +
      "</div>" +
      "</div>";
    document.body.appendChild(modal);

    var grid = modal.querySelector(".ab-gf-modal__grid");
    var search = modal.querySelector(".ab-gf-modal__search");
    var sample = modal.querySelector(".ab-gf-modal__sample");
    var applyBtn = modal.querySelector(".ab-gf-modal__apply");
    var activeCat = "all";
    var selected = currentName || "";
    var loadedBatch = {};

    function cssFor(name) {
      return /\s/.test(name) ? '"' + name + '", sans-serif' : '"' + name + '", sans-serif';
    }

    function loadBatch(names) {
      var pending = names.filter(function (n) {
        return !loadedBatch[n] && !document.getElementById("ab-gf-" + n.toLowerCase().replace(/\s+/g, "-"));
      });
      if (!pending.length) return;
      pending.forEach(function (n) {
        loadedBatch[n] = true;
        ensureGoogleFont(cssFor(n));
      });
    }

    function render() {
      var q = (search.value || "").toLowerCase().trim();
      var sampleText = sample.value || "Colégio JM Monteiro — ABC abc 123";
      var list = GOOGLE_FONTS_CATALOG.filter(function (f) {
        if (activeCat !== "all" && f.category !== activeCat) return false;
        if (!q) return true;
        return f.name.toLowerCase().indexOf(q) >= 0 || f.category.toLowerCase().indexOf(q) >= 0;
      });
      loadBatch(list.slice(0, 24).map(function (f) { return f.name; }));
      grid.innerHTML = list
        .map(function (f) {
          var isOn = selected && selected.toLowerCase() === f.name.toLowerCase();
          return (
            '<button type="button" class="ab-gf-card' +
            (isOn ? " is-on" : "") +
            '" data-font="' +
            esc(f.name) +
            '">' +
            '<span class="ab-gf-card__meta"><span class="ab-gf-card__name">' +
            esc(f.name) +
            '</span><span class="ab-gf-card__cat">' +
            esc(f.category) +
            "</span></span>" +
            '<span class="ab-gf-card__preview" style="font-family:\'' +
            esc(f.name).replace(/'/g, "") +
            "', sans-serif\">" +
            esc(sampleText) +
            "</span></button>"
          );
        })
        .join("");

      grid.querySelectorAll("[data-font]").forEach(function (card) {
        card.addEventListener("mouseenter", function () {
          ensureGoogleFont(cssFor(card.getAttribute("data-font")));
        });
        card.addEventListener("click", function () {
          selected = card.getAttribute("data-font");
          ensureGoogleFont(cssFor(selected));
          grid.querySelectorAll(".ab-gf-card").forEach(function (c) {
            c.classList.toggle("is-on", c.getAttribute("data-font") === selected);
          });
          applyBtn.disabled = !selected;
        });
        card.addEventListener("dblclick", function () {
          selected = card.getAttribute("data-font");
          apply();
        });
      });
      applyBtn.disabled = !selected;
    }

    function apply() {
      if (!selected) return;
      var css = cssFor(selected);
      ensureGoogleFont(css);
      if (typeof onPick === "function") onPick(css, selected);
      close();
    }

    function close() {
      modal.remove();
    }

    modal.addEventListener("click", function (e) {
      if (e.target === modal) close();
    });
    modal.querySelector(".ab-gf-modal__close").addEventListener("click", close);
    applyBtn.addEventListener("click", apply);
    search.addEventListener("input", function () {
      render();
    });
    sample.addEventListener("input", function () {
      render();
    });
    modal.querySelectorAll("[data-cat]").forEach(function (btn) {
      btn.addEventListener("click", function () {
        activeCat = btn.getAttribute("data-cat");
        modal.querySelectorAll(".ab-gf-cat").forEach(function (b) {
          b.classList.toggle("is-on", b === btn);
        });
        render();
      });
    });

    // Lazy-load more fonts as user scrolls
    grid.addEventListener("scroll", function () {
      var cards = grid.querySelectorAll("[data-font]");
      var names = [];
      cards.forEach(function (c) {
        var rect = c.getBoundingClientRect();
        var gRect = grid.getBoundingClientRect();
        if (rect.top < gRect.bottom + 200) names.push(c.getAttribute("data-font"));
      });
      loadBatch(names);
    });

    render();
    setTimeout(function () {
      search.focus();
    }, 40);
  }

  function normalizeFontFamilyCss(val) {
    val = String(val || "").trim();
    if (!val || val === "custom") return "";
    if (val.indexOf("var(") === 0 || val.indexOf(",") >= 0) return val;
    var name = val.replace(/^["']|["']$/g, "").trim();
    if (!name) return "";
    if (/\s/.test(name)) return '"' + name + '", sans-serif';
    var generic = { serif: 1, "sans-serif": 1, monospace: 1, cursive: 1, fantasy: 1, "system-ui": 1, inherit: 1 };
    if (generic[name.toLowerCase()]) return name.toLowerCase();
    return '"' + name + '", sans-serif';
  }
  var FONT_WEIGHT_OPTS = [
    { value: "", label: "Padrão" },
    { value: "300", label: "Light (300)" },
    { value: "400", label: "Normal (400)" },
    { value: "500", label: "Medium (500)" },
    { value: "600", label: "Semi-bold (600)" },
    { value: "700", label: "Bold (700)" },
    { value: "800", label: "Extra-bold (800)" },
    { value: "900", label: "Black (900)" },
  ];
  var LINE_HEIGHT_OPTS = [
    { value: "", label: "Padrão" },
    { value: "1", label: "1" },
    { value: "1.15", label: "1.15" },
    { value: "1.25", label: "1.25" },
    { value: "1.4", label: "1.4" },
    { value: "1.5", label: "1.5" },
    { value: "1.6", label: "1.6" },
    { value: "1.8", label: "1.8" },
    { value: "2", label: "2" },
    { value: "custom", label: "Personalizado…" },
  ];
  var LETTER_SPACING_OPTS = [
    { value: "", label: "Padrão" },
    { value: "0", label: "0" },
    { value: "0.02em", label: "0.02em" },
    { value: "0.05em", label: "0.05em" },
    { value: "0.1em", label: "0.1em" },
    { value: "1px", label: "1px" },
    { value: "2px", label: "2px" },
    { value: "custom", label: "Personalizado…" },
  ];
  var TEXT_TRANSFORM_OPTS = [
    { value: "", label: "Padrão" },
    { value: "none", label: "Nenhuma" },
    { value: "uppercase", label: "MAIÚSCULAS" },
    { value: "lowercase", label: "minúsculas" },
    { value: "capitalize", label: "Capitalizar" },
  ];
  var FONT_STYLE_OPTS = [
    { value: "", label: "Padrão" },
    { value: "normal", label: "Normal" },
    { value: "italic", label: "Itálico" },
  ];
  var TEXT_DECORATION_OPTS = [
    { value: "", label: "Padrão" },
    { value: "none", label: "Nenhuma" },
    { value: "underline", label: "Sublinhado" },
    { value: "line-through", label: "Riscado" },
  ];
  var HEADING_SIZE_OPTS = [
    { value: "", label: "Padrão (tag HTML)" },
    { value: "default", label: "Default" },
    { value: "small", label: "Small" },
    { value: "medium", label: "Medium" },
    { value: "large", label: "Large" },
    { value: "xl", label: "XL" },
    { value: "xxl", label: "XXL" },
  ];
  var HEADING_SIZE_MAP = {
    default: "",
    small: "1rem",
    medium: "1.25rem",
    large: "1.75rem",
    xl: "2.25rem",
    xxl: "3rem",
  };
  var WIDTH_OPTS = [
    { value: "", label: "Automático" },
    { value: "100%", label: "100% (largura total)" },
    { value: "75%", label: "75%" },
    { value: "66%", label: "66% (2/3)" },
    { value: "50%", label: "50% (metade)" },
    { value: "33%", label: "33% (1/3)" },
    { value: "25%", label: "25%" },
    { value: "custom", label: "Personalizado…" },
  ];
  /** Cap on width — empty/none = full bleed (with width 100%). */
  var MAX_WIDTH_OPTS = [
    { value: "", label: "Nenhum (ocupa 100% do pai)" },
    { value: "none", label: "Nenhum" },
    { value: "100%", label: "100%" },
    { value: "1140px", label: "1140 px (conteúdo)" },
    { value: "1200px", label: "1200 px" },
    { value: "1400px", label: "1400 px" },
    { value: "custom", label: "Personalizado…" },
  ];
  var GAP_OPTS = [
    { value: "0", label: "Nenhum" },
    { value: "8px", label: "8px" },
    { value: "12px", label: "12px" },
    { value: "16px", label: "16px" },
    { value: "24px", label: "24px" },
    { value: "32px", label: "32px" },
    { value: "custom", label: "Personalizado…" },
  ];
  var COL_WIDTH_OPTS = [
    { value: "100%", label: "100%" },
    { value: "75%", label: "75%" },
    { value: "66%", label: "66%" },
    { value: "50%", label: "50%" },
    { value: "33%", label: "33%" },
    { value: "25%", label: "25%" },
    { value: "custom", label: "Personalizado…" },
  ];
  /** 12-column grid spans */
  var COL_SPAN_OPTS = [];
  for (var _i = 1; _i <= 12; _i++) {
    COL_SPAN_OPTS.push({
      value: String(_i),
      label: _i + "/12" + (_i === 3 ? " (25%)" : _i === 4 ? " (33%)" : _i === 6 ? " (50%)" : _i === 8 ? " (66%)" : _i === 9 ? " (75%)" : _i === 12 ? " (100%)" : ""),
    });
  }
  var GRID_COLS_OPTS = [];
  for (var _c = 1; _c <= 6; _c++) {
    GRID_COLS_OPTS.push({
      value: String(_c),
      label: _c === 1 ? "1 coluna" : _c + " colunas",
    });
  }
  // Advanced spans still allow up to 12 for custom layouts via col_span.
  for (var _c2 = 7; _c2 <= 12; _c2++) {
    GRID_COLS_OPTS.push({
      value: String(_c2),
      label: _c2 + " colunas",
    });
  }
  var BG_SIZE_OPTS = [
    { value: "cover", label: "Cobrir (cover)" },
    { value: "contain", label: "Conter (contain)" },
    { value: "auto", label: "Automático" },
    { value: "100% 100%", label: "Esticar 100%" },
  ];
  var BG_POS_OPTS = [
    { value: "center center", label: "Centro" },
    { value: "top center", label: "Topo" },
    { value: "bottom center", label: "Base" },
    { value: "left center", label: "Esquerda" },
    { value: "right center", label: "Direita" },
  ];
  var BG_REPEAT_OPTS = [
    { value: "no-repeat", label: "Não repetir" },
    { value: "repeat", label: "Repetir" },
    { value: "repeat-x", label: "Repetir horizontal" },
    { value: "repeat-y", label: "Repetir vertical" },
  ];
  var FONT_SIZE_OPTS = [
    { value: "", label: "Padrão do tema" },
    { value: "12px", label: "12px" },
    { value: "14px", label: "14px" },
    { value: "16px", label: "16px" },
    { value: "18px", label: "18px" },
    { value: "20px", label: "20px" },
    { value: "24px", label: "24px" },
    { value: "32px", label: "32px" },
    { value: "40px", label: "40px" },
    { value: "48px", label: "48px" },
    { value: "custom", label: "Personalizado…" },
  ];

  var PROP_SCHEMA = {
    root: [
      {
        key: "content_width",
        label: "Largura do conteúdo",
        type: "select",
        options: [
          { value: "full", label: "Largura total (100%)" },
          { value: "wide", label: "Wide (max personalizado)" },
          { value: "content", label: "Conteúdo do tema (estreito)" },
        ],
      },
      {
        key: "max_width",
        label: "Largura máxima (só no modo Wide)",
        type: "select",
        // Só aparece quando content_width = wide — evita confusão com 1200px em modo 100%.
        showIf: { key: "content_width", value: "wide" },
        options: [
          { value: "960px", label: "960px" },
          { value: "1140px", label: "1140px" },
          { value: "1200px", label: "1200px" },
          { value: "1400px", label: "1400px" },
          { value: "100%", label: "100%" },
          { value: "custom", label: "Personalizado…" },
        ],
      },
      {
        key: "gap",
        label: "Espaço entre seções",
        type: "select",
        options: GAP_OPTS,
      },
    ],
    section: [
      {
        key: "layout",
        label: "Largura da seção",
        type: "select",
        options: [
          { value: "boxed", label: "Caixa (centralizado)" },
          { value: "full", label: "Largura total" },
        ],
      },
      {
        key: "direction",
        label: "Sistema de layout",
        type: "select",
        options: [
          { value: "column", label: "Empilhado (vertical)" },
          { value: "row", label: "Flex linha (larguras %)" },
          { value: "grid", label: "Grade 12 colunas (recomendado)" },
        ],
      },
      {
        key: "columns",
        label: "Número de colunas",
        type: "select",
        options: GRID_COLS_OPTS,
      },
      { key: "gap", label: "Espaço entre colunas", type: "select", options: GAP_OPTS },
    ],
    container: [
      {
        key: "direction",
        label: "Direção dos itens",
        type: "select",
        options: [
          { value: "column", label: "Coluna (vertical / empilhado)" },
          { value: "row", label: "Linha (horizontal / mesma linha)" },
        ],
      },
      {
        key: "justify_content",
        label: "Alinhamento na linha (horizontal)",
        type: "select",
        showIf: { key: "direction", value: "row" },
        options: [
          { value: "flex-start", label: "⟸ Esquerda" },
          { value: "center", label: "⇔ Centro" },
          { value: "flex-end", label: "Direita ⟹" },
          { value: "space-between", label: "Espaçar (entre)" },
          { value: "space-around", label: "Espaçar (à volta)" },
          { value: "space-evenly", label: "Espaçar (igual)" },
        ],
      },
      {
        key: "align_items",
        label: "Alinhamento vertical na linha",
        type: "select",
        showIf: { key: "direction", value: "row" },
        options: [
          { value: "flex-start", label: "Topo" },
          { value: "center", label: "Meio" },
          { value: "flex-end", label: "Base" },
          { value: "stretch", label: "Esticar" },
        ],
      },
      {
        key: "justify_content",
        label: "Alinhamento vertical (empilhado)",
        type: "select",
        showIf: { key: "direction", value: "column" },
        options: [
          { value: "flex-start", label: "Topo" },
          { value: "center", label: "Meio" },
          { value: "flex-end", label: "Base" },
          { value: "space-between", label: "Espaçar (entre)" },
        ],
      },
      {
        key: "align_items",
        label: "Alinhamento horizontal (empilhado)",
        type: "select",
        showIf: { key: "direction", value: "column" },
        options: [
          { value: "stretch", label: "Largura total" },
          { value: "flex-start", label: "Esquerda" },
          { value: "center", label: "Centro" },
          { value: "flex-end", label: "Direita" },
        ],
      },
      { key: "gap", label: "Espaço entre itens", type: "select", options: GAP_OPTS },
      {
        key: "col_span",
        label: "Span na grade (1–12)",
        type: "select",
        options: COL_SPAN_OPTS,
      },
      { key: "width", label: "Largura (modo flex %)", type: "select", options: COL_WIDTH_OPTS },
    ],
    heading: [
      { key: "text", label: "Título", type: "text", plain: true },
      { key: "link", label: "Link", type: "url", placeholder: "https://…" },
      {
        key: "size",
        label: "Tamanho",
        type: "select",
        options: HEADING_SIZE_OPTS,
      },
      {
        key: "tag",
        label: "Tag HTML",
        type: "select",
        options: [
          { value: "h1", label: "H1" },
          { value: "h2", label: "H2" },
          { value: "h3", label: "H3" },
          { value: "h4", label: "H4" },
          { value: "h5", label: "H5" },
          { value: "h6", label: "H6" },
          { value: "div", label: "div" },
          { value: "span", label: "span" },
          { value: "p", label: "p" },
        ],
      },
      { key: "align", label: "Alinhamento", type: "align" },
    ],
    text: [
      { key: "text", label: "Texto", type: "textarea", plain: true },
      { key: "align", label: "Alinhamento", type: "align" },
    ],
    image: [
      { key: "url", label: "Imagem", type: "media", placeholder: "https://…" },
      { key: "alt", label: "Texto alternativo (alt)", type: "text" },
      { key: "align", label: "Alinhamento do bloco (E/C/D)", type: "align" },
      {
        key: "width",
        label: "Largura da imagem",
        type: "select",
        options: [
          { value: "", label: "Automática (100%)" },
          { value: "25%", label: "25%" },
          { value: "33%", label: "33%" },
          { value: "50%", label: "50%" },
          { value: "66%", label: "66%" },
          { value: "75%", label: "75%" },
          { value: "100%", label: "100%" },
          { value: "200px", label: "200 px" },
          { value: "320px", label: "320 px" },
          { value: "480px", label: "480 px" },
        ],
      },
      {
        key: "object_fit",
        label: "Encaixe no quadro",
        type: "select",
        options: [
          { value: "cover", label: "Cobrir o quadro (recorte)" },
          { value: "contain", label: "Cabendo inteira" },
          { value: "fill", label: "Esticar" },
          { value: "none", label: "Tamanho original" },
        ],
      },
      {
        key: "object_position",
        label: "Posição no quadro (arraste a bolinha)",
        type: "focal_point",
      },
    ],
    dynamic: [
      {
        key: "source",
        label: "Fonte dinâmica",
        type: "select",
        options: [
          { value: "site_logo", label: "Logo do site" },
          { value: "site_title", label: "Título do site" },
          { value: "site_tagline", label: "Descrição do site" },
          { value: "navigation", label: "Navegação (menu)" },
          { value: "avatar", label: "Avatar" },
          { value: "featured_image", label: "Imagem destacada" },
          { value: "post_title", label: "Título do post" },
          { value: "post_content", label: "Conteúdo do post (corpo)" },
          { value: "post_excerpt", label: "Resumo" },
          { value: "post_date", label: "Data" },
          { value: "post_modified", label: "Data modificada" },
          { value: "post_author", label: "Nome do autor" },
          { value: "author_bio", label: "Biografia do autor" },
          { value: "comment_count", label: "Contagem de comentários" },
          { value: "categories", label: "Categorias" },
          { value: "tags", label: "Tags" },
          { value: "prev_post", label: "Post anterior" },
          { value: "next_post", label: "Próximo post" },
          { value: "read_time", label: "Tempo de leitura" },
          { value: "word_count", label: "Contagem de palavras" },
          { value: "read_more", label: "Leia mais" },
          { value: "breadcrumbs", label: "Caminho de navegação" },
          { value: "login_logout", label: "Acessar/Sair" },
          { value: "archive_title", label: "Título do arquivo" },
          { value: "search_title", label: "Título da busca" },
          { value: "term_name", label: "Nome do termo" },
          { value: "term_description", label: "Descrição do termo" },
          { value: "term_count", label: "Contagem de termos" },
        ],
      },
      { key: "align", label: "Alinhamento", type: "align" },
      {
        key: "link",
        label: "Com link",
        type: "select",
        options: [
          { value: "1", label: "Sim" },
          { value: "0", label: "Não" },
        ],
      },
    ],
    embed: [
      {
        key: "provider",
        label: "Provedor",
        type: "select",
        options: [
          { value: "youtube", label: "YouTube" },
          { value: "x", label: "X (Twitter)" },
          { value: "oembed", label: "Mídia incorporada (oEmbed)" },
        ],
      },
      { key: "url", label: "URL da mídia", type: "url", placeholder: "https://…" },
      { key: "align", label: "Alinhamento", type: "align" },
    ],
    button: [
      { key: "text", label: "Texto do botão", type: "text", plain: true },
      { key: "url", label: "Link (URL)", type: "url", placeholder: "https://…" },
      { key: "align", label: "Alinhamento", type: "align" },
    ],
    spacer: [
      {
        key: "height",
        label: "Altura",
        type: "select",
        options: [
          { value: "16", label: "16 px" },
          { value: "24", label: "24 px" },
          { value: "32", label: "32 px" },
          { value: "40", label: "40 px" },
          { value: "60", label: "60 px" },
          { value: "80", label: "80 px" },
          { value: "120", label: "120 px" },
          { value: "custom", label: "Personalizado…" },
        ],
      },
    ],
    divider: [],
    html: [{ key: "code", label: "Código HTML", type: "textarea" }],
    video: [
      { key: "url", label: "URL do vídeo", type: "url", placeholder: "YouTube / Vimeo / .mp4" },
      { key: "align", label: "Alinhamento", type: "align" },
      {
        key: "autoplay",
        label: "Autoplay",
        type: "select",
        options: [
          { value: "0", label: "Não" },
          { value: "1", label: "Sim" },
        ],
      },
    ],
    star_rating: [
      { key: "rating", label: "Nota", type: "text", placeholder: "4.5" },
      { key: "max", label: "Máximo de estrelas", type: "text", placeholder: "5" },
      { key: "align", label: "Alinhamento", type: "align" },
      {
        key: "show_score",
        label: "Mostrar número",
        type: "select",
        options: [
          { value: "1", label: "Sim" },
          { value: "0", label: "Não" },
        ],
      },
    ],
    google_maps: [
      { key: "address", label: "Endereço", type: "text", placeholder: "Cidade, País" },
      { key: "zoom", label: "Zoom (1–21)", type: "number" },
      { key: "height", label: "Altura (px)", type: "number" },
    ],
    icon: [
      { key: "icon", label: "Ícone", type: "icon" },
      { key: "size", label: "Tamanho (px)", type: "number" },
      { key: "align", label: "Alinhamento", type: "align" },
      { key: "link", label: "Link", type: "url" },
    ],
    image_box: [
      { key: "url", label: "Imagem", type: "media" },
      { key: "title", label: "Título", type: "text" },
      { key: "description", label: "Descrição", type: "textarea" },
      { key: "link", label: "Link", type: "url" },
      {
        key: "image_position",
        label: "Posição da imagem",
        type: "select",
        options: [
          { value: "top", label: "Topo" },
          { value: "bottom", label: "Base" },
          { value: "left", label: "Esquerda" },
          { value: "right", label: "Direita" },
        ],
      },
      {
        key: "media_width",
        label: "Largura da imagem (%, lado a lado)",
        type: "select",
        options: [
          { value: "30", label: "30%" },
          { value: "35", label: "35%" },
          { value: "40", label: "40%" },
          { value: "45", label: "45%" },
          { value: "50", label: "50%" },
        ],
      },
      { key: "image_height", label: "Altura fixa (px, vazio = auto)", type: "text", placeholder: "ex.: 220" },
      {
        key: "object_fit",
        label: "Encaixe da imagem",
        type: "select",
        options: [
          { value: "cover", label: "Cobrir (cover)" },
          { value: "contain", label: "Conter (contain)" },
          { value: "fill", label: "Esticar" },
        ],
      },
      {
        key: "object_position",
        label: "Posição no quadro (arraste a bolinha)",
        type: "focal_point",
      },
      { key: "radius", label: "Arredondamento (px)", type: "number" },
      { key: "shadow", label: "Sombra", type: "switch" },
      { key: "border", label: "Contorno", type: "switch" },
      { key: "border_color", label: "Cor do contorno", type: "color" },
      { key: "align", label: "Alinhamento do texto", type: "align" },
    ],
    icon_box: [
      { key: "icon", label: "Ícone", type: "icon" },
      { key: "title", label: "Título", type: "text" },
      { key: "description", label: "Descrição", type: "textarea" },
      { key: "link", label: "Link", type: "url" },
      { key: "align", label: "Alinhamento", type: "align" },
    ],
    gallery: [
      { key: "images", label: "Imagens", type: "gallery_media" },
      {
        key: "layout",
        label: "Modelo",
        type: "select",
        options: [
          { value: "grid", label: "Grade (várias colunas)" },
          { value: "slider", label: "Slider (deslizar)" },
          { value: "masonry", label: "Masonry (quebrado)" },
        ],
      },
      {
        key: "columns",
        label: "Colunas (grade)",
        type: "select",
        options: [
          { value: "1", label: "1" },
          { value: "2", label: "2" },
          { value: "3", label: "3" },
          { value: "4", label: "4" },
          { value: "5", label: "5" },
          { value: "6", label: "6" },
        ],
      },
      {
        key: "per_view",
        label: "Visíveis no slider",
        type: "select",
        options: [
          { value: "1", label: "1 imagem" },
          { value: "2", label: "2 imagens" },
          { value: "3", label: "3 imagens" },
          { value: "4", label: "4 imagens" },
        ],
      },
      {
        key: "autoplay",
        label: "Rolar automaticamente (slider)",
        type: "switch",
      },
      {
        key: "autoplay_speed",
        label: "Intervalo do autoplay (ms)",
        type: "select",
        options: [
          { value: "2500", label: "2,5 s (rápido)" },
          { value: "3500", label: "3,5 s" },
          { value: "4500", label: "4,5 s (padrão)" },
          { value: "6000", label: "6 s" },
          { value: "8000", label: "8 s" },
          { value: "10000", label: "10 s" },
        ],
      },
      {
        key: "ratio",
        label: "Proporção",
        type: "select",
        options: [
          { value: "auto", label: "Automática" },
          { value: "1/1", label: "1:1 quadrado" },
          { value: "4/3", label: "4:3" },
          { value: "3/2", label: "3:2" },
          { value: "16/9", label: "16:9" },
          { value: "3/4", label: "3:4 retrato" },
        ],
      },
      { key: "gap", label: "Espaçamento (px)", type: "number" },
    ],
    carousel: [
      { key: "images", label: "Imagens", type: "gallery_media" },
      {
        key: "layout",
        label: "Modelo de slide",
        type: "select",
        options: [
          { value: "single", label: "1 imagem por vez" },
          { value: "multi", label: "Várias visíveis (carrossel)" },
          { value: "fade", label: "Fade full" },
        ],
      },
      {
        key: "slide_effect",
        label: "Efeito entre slides",
        type: "select",
        options: [
          { value: "slide", label: "Deslizar" },
          { value: "fade", label: "Fade" },
          { value: "zoom", label: "Zoom" },
          { value: "flip", label: "Flip 3D" },
        ],
      },
      { key: "slide_speed", label: "Velocidade troca (ms)", type: "number" },
      {
        key: "per_view",
        label: "Imagens visíveis (multi)",
        type: "select",
        options: [
          { value: "1", label: "1" },
          { value: "2", label: "2" },
          { value: "3", label: "3" },
          { value: "4", label: "4" },
        ],
      },
      { key: "height", label: "Altura (px)", type: "number" },
      { key: "gap", label: "Espaço entre slides (px)", type: "number" },
      { key: "autoplay", label: "Autoplay", type: "switch" },
      { key: "dots", label: "Pontos", type: "switch" },
      { key: "arrows", label: "Setas", type: "switch" },
    ],
    popup: [
      { key: "enabled", label: "Ativo", type: "switch" },
      {
        key: "mode",
        label: "Conteúdo",
        type: "select",
        options: [
          { value: "image", label: "Imagem única" },
          { value: "carousel", label: "Carrossel / slides" },
        ],
      },
      { key: "images", label: "Imagens do popup", type: "gallery_media" },
      {
        key: "scope",
        label: "Onde abrir",
        type: "select",
        options: [
          { value: "current", label: "Só esta página" },
          { value: "home", label: "Só na Home" },
          { value: "all", label: "Todas as páginas" },
        ],
      },
      {
        key: "frequency",
        label: "Frequência",
        type: "select",
        options: [
          { value: "every", label: "Sempre que carregar" },
          { value: "session", label: "Uma vez por sessão" },
          { value: "once", label: "Uma vez (cookie)" },
        ],
      },
      { key: "delay", label: "Atraso para abrir (ms)", type: "number" },
      { key: "slide_time", label: "Tempo entre slides (ms)", type: "number" },
      { key: "autoplay", label: "Autoplay do carrossel", type: "switch" },
      {
        key: "open_effect",
        label: "Efeito de abertura",
        type: "select",
        options: [
          { value: "fadein", label: "Fade" },
          { value: "fadeinup", label: "Fade ↑" },
          { value: "zoomin", label: "Zoom in" },
          { value: "slideinup", label: "Slide ↑" },
          { value: "bouncein", label: "Bounce" },
          { value: "none", label: "Nenhum" },
        ],
      },
      {
        key: "slide_effect",
        label: "Efeito entre slides",
        type: "select",
        options: [
          { value: "fade", label: "Fade" },
          { value: "slide", label: "Deslizar" },
          { value: "zoom", label: "Zoom" },
        ],
      },
      { key: "max_width", label: "Largura máx. (px)", type: "number" },
      { key: "show_close", label: "Botão fechar", type: "switch" },
      { key: "overlay", label: "Overlay escuro", type: "switch" },
    ],
    icon_list: [
      { key: "items", label: "Itens da lista", type: "icon_list_items" },
      { key: "align", label: "Alinhamento", type: "align" },
    ],
    counter: [
      { key: "number", label: "Número", type: "text" },
      { key: "prefix", label: "Prefixo", type: "text" },
      { key: "suffix", label: "Sufixo", type: "text" },
      { key: "title", label: "Título", type: "text" },
      { key: "animate", label: "Animar (contar até o valor)", type: "switch" },
      {
        key: "duration",
        label: "Duração da animação (ms)",
        type: "select",
        options: [
          { value: "800", label: "Rápida (800ms)" },
          { value: "1200", label: "1,2 s" },
          { value: "1600", label: "1,6 s (padrão)" },
          { value: "2200", label: "2,2 s" },
          { value: "3000", label: "3 s" },
        ],
      },
      { key: "align", label: "Alinhamento", type: "align" },
    ],
    testimonial: [
      { key: "content", label: "Depoimento", type: "textarea" },
      { key: "name", label: "Nome", type: "text" },
      { key: "role", label: "Cargo", type: "text" },
      { key: "image", label: "Foto", type: "media" },
      { key: "align", label: "Alinhamento", type: "align" },
    ],
    tabs: [
      {
        key: "items",
        label: "Abas",
        type: "accordion_items",
      },
      {
        key: "object_fit",
        label: "Encaixe da imagem",
        type: "select",
        options: [
          { value: "cover", label: "Cobrir (cover)" },
          { value: "contain", label: "Conter (contain)" },
          { value: "fill", label: "Esticar" },
        ],
      },
      {
        key: "object_position",
        label: "Posição no quadro (arraste a bolinha)",
        type: "focal_point",
      },
    ],
    accordion: [
      {
        key: "source",
        label: "Fonte dos itens",
        type: "select",
        options: [
          { value: "manual", label: "Manual (editar itens)" },
          { value: "posts", label: "Posts / páginas (consulta)" },
        ],
      },
      { key: "open_first", label: "Abrir primeiro item", type: "switch" },
      { key: "multiple", label: "Permitir vários abertos", type: "switch" },
      {
        key: "icon_style",
        label: "Ícone do cabeçalho",
        type: "select",
        options: [
          { value: "plus", label: "Mais (+)" },
          { value: "chevron", label: "Seta (▾)" },
        ],
      },
      {
        key: "image_position",
        label: "Posição da imagem no item",
        type: "select",
        options: [
          { value: "top", label: "Acima do texto" },
          { value: "left", label: "Esquerda" },
          { value: "right", label: "Direita" },
        ],
      },
      {
        key: "object_fit",
        label: "Encaixe da imagem",
        type: "select",
        options: [
          { value: "cover", label: "Cobrir (cover)" },
          { value: "contain", label: "Conter (contain)" },
          { value: "fill", label: "Esticar" },
        ],
      },
      {
        key: "object_position",
        label: "Posição no quadro (arraste a bolinha)",
        type: "focal_point",
      },
      {
        key: "items",
        label: "Itens do acordeão",
        type: "accordion_items",
        showIf: { key: "source", value: "manual" },
      },
      {
        key: "posts_per_page",
        label: "Quantidade",
        type: "number",
        showIf: { key: "source", value: "posts" },
      },
      {
        key: "query_source",
        label: "Tipo de conteúdo",
        type: "select",
        options: [
          { value: "post", label: "Posts" },
          { value: "page", label: "Páginas" },
          { value: "any", label: "Posts e páginas" },
        ],
        showIf: { key: "source", value: "posts" },
      },
      {
        key: "category",
        label: "Categoria (só posts)",
        type: "category_search",
        showIf: { key: "source", value: "posts" },
      },
      {
        key: "orderby",
        label: "Ordenar por",
        type: "select",
        options: [
          { value: "date", label: "Data" },
          { value: "title", label: "Título" },
          { value: "menu_order", label: "Ordem do menu" },
          { value: "rand", label: "Aleatório" },
          { value: "modified", label: "Modificado" },
        ],
        showIf: { key: "source", value: "posts" },
      },
      {
        key: "order",
        label: "Ordem",
        type: "select",
        options: [
          { value: "DESC", label: "Decrescente" },
          { value: "ASC", label: "Crescente" },
        ],
        showIf: { key: "source", value: "posts" },
      },
      {
        key: "include_ids",
        label: "IDs inclusos (opcional)",
        type: "text",
        showIf: { key: "source", value: "posts" },
      },
      {
        key: "exclude_ids",
        label: "IDs excluídos",
        type: "text",
        showIf: { key: "source", value: "posts" },
      },
      {
        key: "show_image",
        label: "Imagem destacada",
        type: "switch",
        showIf: { key: "source", value: "posts" },
      },
      {
        key: "show_excerpt",
        label: "Usar resumo",
        type: "switch",
        showIf: { key: "source", value: "posts" },
      },
      {
        key: "excerpt_length",
        label: "Tamanho do resumo (palavras)",
        type: "number",
        showIf: { key: "source", value: "posts" },
      },
    ],
    toggle: [
      { key: "open_first", label: "Abrir primeiro item", type: "switch" },
      {
        key: "object_fit",
        label: "Encaixe da imagem",
        type: "select",
        options: [
          { value: "cover", label: "Cobrir (cover)" },
          { value: "contain", label: "Conter (contain)" },
          { value: "fill", label: "Esticar" },
        ],
      },
      {
        key: "object_position",
        label: "Posição no quadro (arraste a bolinha)",
        type: "focal_point",
      },
      {
        key: "items",
        label: "Itens do toggle",
        type: "accordion_items",
      },
    ],
    social_icons: [
      { key: "items", label: "Redes sociais", type: "social_items" },
      { key: "align", label: "Alinhamento", type: "align" },
      {
        key: "shape",
        label: "Forma",
        type: "select",
        options: [
          { value: "circle", label: "Círculo" },
          { value: "rounded", label: "Arredondado" },
          { value: "square", label: "Quadrado" },
        ],
      },
      { key: "show_label", label: "Mostrar nome", type: "switch" },
      { key: "size", label: "Tamanho (px)", type: "number" },
    ],
    page_content: [
      {
        key: "source_type",
        label: "Tipo",
        type: "select",
        options: [
          { value: "page", label: "Página" },
          { value: "post", label: "Post" },
        ],
      },
      { key: "page_id", label: "Página / post", type: "page_search" },
      { key: "show_title", label: "Mostrar título", type: "switch" },
    ],
    progress_bar: [
      { key: "title", label: "Título", type: "text" },
      { key: "percent", label: "Percentual (0–100)", type: "number" },
      {
        key: "show_pct",
        label: "Mostrar %",
        type: "select",
        options: [
          { value: "1", label: "Sim" },
          { value: "0", label: "Não" },
        ],
      },
    ],
    shortcode: [{ key: "code", label: "Shortcode", type: "text", placeholder: "[gallery]" }],
    menu_anchor: [{ key: "anchor", label: "ID da âncora", type: "text", placeholder: "secao-sobre" }],
    alert: [
      { key: "title", label: "Título", type: "text" },
      { key: "content", label: "Mensagem", type: "textarea" },
      {
        key: "type",
        label: "Tipo",
        type: "select",
        options: [
          { value: "info", label: "Info" },
          { value: "success", label: "Sucesso" },
          { value: "warning", label: "Aviso" },
          { value: "danger", label: "Perigo" },
        ],
      },
    ],
    sidebar: [{ key: "sidebar", label: "ID da sidebar", type: "text", placeholder: "sidebar-1" }],
    text_path: [
      { key: "text", label: "Texto", type: "text" },
      { key: "align", label: "Alinhamento", type: "align" },
    ],
    comments_form: [
      { key: "title", label: "Título do formulário", type: "text" },
      { key: "show_title", label: "Mostrar título", type: "switch" },
      { key: "show_list", label: "Listar comentários", type: "switch" },
      { key: "list_title", label: "Título da lista", type: "text" },
      { key: "submit_label", label: "Texto do botão", type: "text" },
      { key: "btn_bg", label: "Cor do botão", type: "color" },
      { key: "btn_color", label: "Cor do texto do botão", type: "color" },
      {
        key: "btn_full_width",
        label: "Botão em largura total",
        type: "switch",
      },
      {
        key: "btn_radius",
        label: "Cantos do botão (px)",
        type: "number",
        placeholder: "8",
      },
    ],
    loop_grid: [
      {
        key: "template_type",
        label: "Tipo de listagem",
        type: "select",
        options: [
          { value: "posts", label: "Posts" },
          { value: "products", label: "Produtos (WooCommerce)" },
        ],
      },
      {
        key: "columns",
        label: "Colunas",
        type: "select",
        responsive: true,
        options: [
          { value: "1", label: "1" },
          { value: "2", label: "2" },
          { value: "3", label: "3" },
          { value: "4", label: "4" },
          { value: "5", label: "5" },
          { value: "6", label: "6" },
        ],
      },
      { key: "gap", label: "Espaçamento (px)", type: "number", responsive: true },
      { key: "posts_per_page", label: "Itens por página", type: "number", responsive: true },
      { key: "as_slider", label: "Exibir como slide (carrossel)", type: "switch", responsive: true },
      // —— Imagem do card (como bloco WP) ——
      { key: "show_image", label: "Imagem destacada", type: "switch", responsive: true },
      {
        key: "image_position",
        label: "Posição da imagem",
        type: "select",
        responsive: true,
        showIf: { key: "show_image", value: "1" },
        options: [
          { value: "top", label: "Topo" },
          { value: "left", label: "Esquerda" },
          { value: "right", label: "Direita" },
        ],
      },
      {
        key: "image_size",
        label: "Resolução da imagem",
        type: "select",
        showIf: { key: "show_image", value: "1" },
        options: [
          { value: "thumbnail", label: "Miniatura" },
          { value: "medium", label: "Média" },
          { value: "medium_large", label: "Média-grande" },
          { value: "large", label: "Grande" },
          { value: "full", label: "Tamanho completo" },
        ],
      },
      {
        key: "image_ratio",
        label: "Proporção da imagem",
        type: "select",
        showIf: { key: "show_image", value: "1" },
        options: [
          { value: "auto", label: "Automática" },
          { value: "1/1", label: "1:1 quadrado" },
          { value: "4/3", label: "4:3" },
          { value: "3/2", label: "3:2" },
          { value: "16/9", label: "16:9" },
          { value: "3/4", label: "3:4 retrato" },
          { value: "21/9", label: "21:9 ultrawide" },
        ],
      },
      {
        key: "image_object_fit",
        label: "Encaixe da imagem",
        type: "select",
        showIf: { key: "show_image", value: "1" },
        options: [
          { value: "cover", label: "Cobrir (cover)" },
          { value: "contain", label: "Conter (contain)" },
          { value: "fill", label: "Esticar (fill)" },
        ],
      },
      {
        key: "image_object_position",
        label: "Posição no quadro (arraste a bolinha)",
        type: "focal_point",
        showIf: { key: "show_image", value: "1" },
      },
      { key: "image_link", label: "Imagem aponta para o post", type: "switch", showIf: { key: "show_image", value: "1" } },
      { key: "show_category", label: "Badge de categoria na imagem", type: "switch" },
      // —— Conteúdo do card ——
      { key: "show_date", label: "Data", type: "switch", responsive: true },
      { key: "show_author", label: "Autor", type: "switch", responsive: true },
      { key: "show_title", label: "Título", type: "switch", responsive: true },
      {
        key: "title_position",
        label: "Posição do título",
        type: "select",
        responsive: true,
        showIf: { key: "show_title", value: "1" },
        options: [
          { value: "outside", label: "Fora da imagem" },
          { value: "over_image", label: "Sobre a imagem" },
        ],
      },
      { key: "show_excerpt", label: "Resumo", type: "switch", responsive: true },
      { key: "excerpt_length", label: "Palavras no resumo", type: "number", responsive: true },
      { key: "show_read_more", label: "Leia mais", type: "switch", responsive: true },
      { key: "read_more_text", label: "Texto do botão “Leia mais”", type: "text", responsive: true },
      { key: "card_shadow", label: "Sombra no card", type: "switch", responsive: true },
      { key: "card_border", label: "Borda no card", type: "switch", responsive: true },
      // —— Consulta ——
      {
        key: "query_source",
        label: "Fonte (tipo de conteúdo)",
        type: "select",
        options: [
          { value: "post", label: "Posts" },
          { value: "page", label: "Páginas" },
          { value: "product", label: "Produtos" },
          { value: "current_query", label: "Consulta atual do arquivo" },
        ],
      },
      {
        key: "orderby",
        label: "Ordenar por",
        type: "select",
        options: [
          { value: "date", label: "Data" },
          { value: "title", label: "Título" },
          { value: "modified", label: "Modificado" },
          { value: "comment_count", label: "Comentários" },
          { value: "rand", label: "Aleatório" },
          { value: "menu_order", label: "Ordem do menu" },
          { value: "ID", label: "ID" },
        ],
      },
      {
        key: "order",
        label: "Ordem",
        type: "select",
        options: [
          { value: "DESC", label: "DESC (mais recentes)" },
          { value: "ASC", label: "ASC (mais antigos)" },
        ],
      },
      { key: "category", label: "Categorias (várias)", type: "category_search", multi: true },
      { key: "tag", label: "Tag (slug ou IDs)", type: "text" },
      { key: "include_ids", label: "Incluir IDs", type: "text", placeholder: "12,34" },
      { key: "exclude_ids", label: "Excluir IDs", type: "text" },
      { key: "offset", label: "Offset", type: "number" },
      {
        key: "pagination",
        label: "Paginação",
        type: "select",
        options: [
          { value: "0", label: "Desligada" },
          { value: "numbers", label: "Números" },
        ],
      },
      { key: "ignore_sticky", label: "Ignorar sticky", type: "switch" },
    ],
    advanced_accordion: [
      {
        key: "items",
        label: "Itens (Título|Conteúdo|Ícone|#cor)",
        type: "textarea",
      },
      {
        key: "style",
        label: "Estilo",
        type: "select",
        options: [
          { value: "modern", label: "Moderno" },
          { value: "classic", label: "Clássico" },
          { value: "bordered", label: "Com borda" },
          { value: "soft", label: "Suave" },
        ],
      },
      { key: "open_first", label: "Abrir primeiro", type: "switch" },
      { key: "multiple", label: "Permitir vários abertos", type: "switch" },
    ],
    preview_card: [
      { key: "image", label: "Imagem", type: "media" },
      { key: "title", label: "Título", type: "text", plain: true },
      { key: "subtitle", label: "Subtítulo", type: "text", plain: true },
      { key: "text", label: "Texto", type: "textarea", plain: true },
      { key: "button_text", label: "Texto do botão", type: "text", plain: true },
      { key: "button_url", label: "Link do botão", type: "url" },
      {
        key: "style",
        label: "Estilo",
        type: "select",
        options: [
          { value: "hover", label: "Hover (sobre a imagem)" },
          { value: "below", label: "Conteúdo abaixo" },
          { value: "overlay", label: "Overlay permanente" },
        ],
      },
      { key: "radius", label: "Cantos (px)", type: "number" },
    ],
    info_box: [
      { key: "icon", label: "Ícone", type: "icon" },
      { key: "title", label: "Título", type: "text", plain: true },
      { key: "text", label: "Texto", type: "textarea", plain: true },
      { key: "button_text", label: "Texto do botão", type: "text", plain: true },
      { key: "button_url", label: "Link do botão", type: "url" },
      {
        key: "style",
        label: "Estilo",
        type: "select",
        options: [
          { value: "card", label: "Card" },
          { value: "border", label: "Borda" },
          { value: "flat", label: "Flat" },
          { value: "soft", label: "Suave" },
        ],
      },
      { key: "accent", label: "Cor de destaque", type: "text" },
      { key: "align", label: "Alinhamento", type: "align" },
    ],
    social_bar: [
      {
        key: "items",
        label: "Redes (Nome|URL por linha)",
        type: "textarea",
      },
      {
        key: "direction",
        label: "Direção",
        type: "select",
        options: [
          { value: "row", label: "Horizontal" },
          { value: "column", label: "Vertical" },
        ],
      },
      {
        key: "shape",
        label: "Forma",
        type: "select",
        options: [
          { value: "circle", label: "Círculo" },
          { value: "rounded", label: "Arredondado" },
          { value: "square", label: "Quadrado" },
        ],
      },
      { key: "show_label", label: "Mostrar rótulo", type: "switch" },
      { key: "align", label: "Alinhamento", type: "align" },
      { key: "size", label: "Tamanho (px)", type: "number" },
    ],
    text_animation: [
      { key: "prefix", label: "Prefixo (fixo)", type: "text", plain: true },
      {
        key: "words",
        label: "Palavras animadas (sep. por |)",
        type: "text",
        plain: true,
      },
      { key: "suffix", label: "Sufixo (fixo)", type: "text", plain: true },
      {
        key: "effect",
        label: "Efeito",
        type: "select",
        options: [
          { value: "typing", label: "Digitando" },
          { value: "fade", label: "Fade" },
          { value: "slide", label: "Slide" },
          { value: "bounce", label: "Bounce" },
          { value: "gradient", label: "Gradiente" },
        ],
      },
      { key: "speed", label: "Velocidade (ms/letra)", type: "number" },
      { key: "pause", label: "Pausa entre palavras (ms)", type: "number" },
      {
        key: "tag",
        label: "Tag HTML",
        type: "select",
        options: [
          { value: "h1", label: "H1" },
          { value: "h2", label: "H2" },
          { value: "h3", label: "H3" },
          { value: "h4", label: "H4" },
          { value: "p", label: "Parágrafo" },
          { value: "div", label: "Div" },
        ],
      },
      { key: "align", label: "Alinhamento", type: "align" },
    ],
    image_float: [
      { key: "image", label: "Imagem", type: "media" },
      { key: "alt", label: "Texto alternativo", type: "text", plain: true },
      {
        key: "effect",
        label: "Efeito",
        type: "select",
        options: [
          { value: "float", label: "Flutuar" },
          { value: "tilt", label: "Inclinar" },
          { value: "pulse", label: "Pulso" },
          { value: "rotate", label: "Girar suave" },
          { value: "bob", label: "Sobe e desce" },
        ],
      },
      { key: "height", label: "Altura (px)", type: "number" },
      {
        key: "object_position",
        label: "Posição no quadro (arraste a bolinha)",
        type: "focal_point",
      },
      { key: "radius", label: "Cantos (px)", type: "number" },
      { key: "shadow", label: "Sombra", type: "switch" },
    ],
    image_overlay: [
      { key: "image", label: "Imagem", type: "media" },
      { key: "title", label: "Título", type: "text", plain: true },
      { key: "text", label: "Texto", type: "textarea", plain: true },
      { key: "link", label: "Link", type: "url" },
      {
        key: "effect",
        label: "Efeito hover",
        type: "select",
        options: [
          { value: "fade", label: "Fade" },
          { value: "slide-up", label: "Slide up" },
          { value: "zoom", label: "Zoom" },
          { value: "color", label: "Cor" },
          { value: "blur", label: "Blur" },
        ],
      },
      { key: "color", label: "Cor do overlay", type: "text" },
      { key: "opacity", label: "Opacidade (%)", type: "number" },
      { key: "height", label: "Altura (px)", type: "number" },
      {
        key: "object_position",
        label: "Posição no quadro (arraste a bolinha)",
        type: "focal_point",
      },
      { key: "radius", label: "Cantos (px)", type: "number" },
    ],
    image_carousel_pro: [
      { key: "images", label: "Imagens", type: "gallery_media" },
      { key: "height", label: "Altura (px)", type: "number" },
      { key: "autoplay", label: "Autoplay", type: "switch" },
      { key: "dots", label: "Pontos", type: "switch" },
      { key: "arrows", label: "Setas", type: "switch" },
      { key: "interval", label: "Intervalo (ms)", type: "number" },
      { key: "radius", label: "Cantos (px)", type: "number" },
    ],
    nav_menu: [
      {
        key: "source",
        label: "Origem do menu",
        type: "select",
        options: [
          { value: "wp", label: "Menu do WordPress" },
          { value: "custom", label: "Itens editáveis" },
        ],
      },
      {
        key: "menu_id",
        label: "Menu do WP",
        type: "select",
        showIf: { key: "source", value: "wp" },
        options: (function () {
          var menus = Array.isArray(CFG.navMenus) ? CFG.navMenus : [];
          var opts = [{ value: "0", label: "— Primeiro menu disponível —" }];
          menus.forEach(function (m) {
            opts.push({
              value: String(m.id),
              label: m.name + " (" + m.count + " itens)",
            });
          });
          return opts;
        })(),
      },
      {
        key: "items",
        label: "Itens do menu",
        type: "nav_menu_items",
      },
      {
        key: "layout",
        label: "Layout",
        type: "select",
        options: [
          { value: "horizontal", label: "Horizontal" },
          { value: "vertical", label: "Vertical" },
        ],
      },
      {
        key: "style",
        label: "Estilo do menu",
        type: "select",
        options: [
          { value: "underline", label: "Sublinhado (destaque dourado)" },
          { value: "classic", label: "Clássico (texto limpo)" },
          { value: "pills", label: "Pílulas" },
          { value: "buttons", label: "Botões (CTA)" },
          { value: "minimal", label: "Minimal" },
          { value: "bordered", label: "Com borda" },
        ],
      },
      {
        key: "accent",
        label: "Cor de destaque (ativo / underline)",
        type: "color",
      },
      {
        key: "hover_color",
        label: "Cor no hover",
        type: "color",
      },
      { key: "align", label: "Alinhamento", type: "align" },
      {
        key: "mobile_style",
        label: "Mobile",
        type: "select",
        options: [
          { value: "drawer", label: "Drawer (hambúrguer)" },
          { value: "stack", label: "Empilhar" },
          { value: "none", label: "Sem adaptação" },
        ],
      },
      { key: "show_toggle", label: "Botão menu mobile", type: "switch" },
      { key: "toggle_label", label: "Texto do botão mobile", type: "text" },
      { key: "gap", label: "Espaçamento (px)", type: "number" },
    ],
    creative_cards: [
      {
        key: "columns",
        label: "Colunas (desktop)",
        type: "select",
        options: [
          { value: "2", label: "2" },
          { value: "3", label: "3" },
          { value: "4", label: "4" },
          { value: "5", label: "5" },
          { value: "6", label: "6" },
        ],
      },
      {
        key: "columns_tablet",
        label: "Colunas (tablet)",
        type: "select",
        options: [
          { value: "1", label: "1" },
          { value: "2", label: "2" },
          { value: "3", label: "3" },
        ],
      },
      {
        key: "columns_mobile",
        label: "Colunas (mobile)",
        type: "select",
        options: [
          { value: "1", label: "1" },
          { value: "2", label: "2" },
        ],
      },
      { key: "gap", label: "Espaçamento (px)", type: "number" },
      { key: "height", label: "Altura do card (px)", type: "number" },
      { key: "radius", label: "Cantos arredondados (px)", type: "number" },
      { key: "open_first", label: "Abrir primeiro item", type: "switch" },
      {
        key: "items",
        label: "Itens (um por linha)",
        type: "textarea",
        placeholder:
          "image|URL|Título|Subtítulo|Conteúdo\ncolor|#e74c3c|▶|Título|Sub|Conteúdo com || para parágrafos",
      },
    ],
  };

  var SWITCH_OPTS = [
    { value: "1", label: "Sim" },
    { value: "0", label: "Não" },
  ];

  var TYPE_LABELS = {
    section: "Seção",
    container: "Container",
    heading: "Título",
    text: "Texto",
    image: "Imagem",
    video: "Vídeo",
    button: "Botão",
    star_rating: "Avaliação",
    divider: "Divisor",
    google_maps: "Google Maps",
    icon: "Ícone",
    image_box: "Caixa de imagem",
    icon_box: "Caixa de ícone",
    gallery: "Galeria",
    carousel: "Carrossel",
    popup: "Popup",
    icon_list: "Lista de ícones",
    counter: "Contador",
    spacer: "Espaço",
    testimonial: "Depoimento",
    tabs: "Abas",
    accordion: "Acordeão",
    toggle: "Toggle",
    social_icons: "Ícones sociais",
    page_content: "Conteúdo de página",
    progress_bar: "Progresso",
    shortcode: "Shortcode",
    html: "HTML",
    menu_anchor: "Âncora",
    alert: "Alerta",
    sidebar: "Sidebar",
    text_path: "Texto em caminho",
    loop_grid: "Loop de consulta",
    comments_form: "Formulário de comentários",
    creative_cards: "Grade criativa",
    advanced_accordion: "Acordeão avançado",
    preview_card: "Card de preview",
    info_box: "Caixa de informação",
    social_bar: "Barra social",
    text_animation: "Animação de texto",
    image_float: "Imagem flutuante",
    image_overlay: "Sobreposição de imagem",
    image_carousel_pro: "Carrossel de imagens",
    nav_menu: "Menu de navegação",
    dynamic: "Dinâmico",
    embed: "Mídia",
    root: "Raiz",
  };

  var DYNAMIC_LABELS = {
    site_logo: "Logo do site",
    site_title: "Título do site",
    site_tagline: "Descrição do site",
    navigation: "Navegação",
    avatar: "Avatar",
    featured_image: "Imagem destacada",
    post_title: "Título do post",
    post_content: "Conteúdo do post",
    post_excerpt: "Resumo",
    post_date: "Data",
    post_modified: "Data modificada",
    post_author: "Autor",
    author_bio: "Biografia",
    comment_count: "Comentários",
    categories: "Categorias",
    tags: "Tags",
    prev_post: "Post anterior",
    next_post: "Próximo post",
    read_time: "Tempo de leitura",
    word_count: "Palavras",
    read_more: "Leia mais",
    breadcrumbs: "Breadcrumbs",
    login_logout: "Acessar/Sair",
    archive_title: "Arquivo",
    search_title: "Busca",
    term_name: "Termo",
    term_description: "Descrição do termo",
    term_count: "Contagem",
  };

  var state = {
    document: null,
    title: "",
    viewUrl: CFG.viewUrl || "",
    editUrl: CFG.editUrl || "",
    selectedId: null,
    /** Multi-select (Ctrl/Cmd+click). Order = selection order; document order used for wrap. */
    selectedIds: [],
    dirty: false,
    saving: false,
    status: "Carregando…",
    statusClass: "",
    history: [],
    future: [],
    imported: false,
    source: "",
    theme: {
      header: "",
      footer: "",
      hasHeader: false,
      hasFooter: false,
      styles: { links: [], inline: "" },
      homeUrl: "",
    },
    /** desktop | tablet | mobile — which style bag is being edited */
    breakpoint: "desktop",
    /** desktop | tablet | mobile — canvas preview only (does not change props) */
    viewBp: "desktop",
    /** left panel: widgets | layers | settings */
    leftPanel: "widgets",
    /** elements tab: basic | media | creative | dynamic | layout */
    widgetTab: "basic",
    /** hide left/right sidebars for full canvas preview */
    hideSidebars: false,
    /** collapse each sidebar independently (not full focus mode) */
    leftCollapsed: false,
    rightCollapsed: false,
    /** force "Personalizado…" input even when value matches a preset */
    forceCustom: {},
    /** page | global — settings panel mode */
    settingsTab: "page",
    /** Site design tokens (layout / colors / typography) */
    globals: null,
    globalsDirty: false,
    /** Copied visual styles (fonts, colors, bg, border…) for paste onto another node */
    styleClipboard: null,
    /** Props panel: principal | estilo | avancado | responsivo */
    propsTab: "principal",
    /** Live search query in props panel */
    propsSearch: "",
    /** Accordion open state: groupId → boolean (null = defaults) */
    propsOpenGroups: null,
    /** Which nav menu item index is expanded in props (−1 = none) */
    navMenuOpenIdx: -1,
    /** Multi-select indices of top-level nav items (for “enviar para submenu”) */
    navMenuSelected: [],
  };

  /** Visual style keys copied between elements (not layout like width/flex). */
  var STYLE_COPY_KEYS = [
    "color",
    "background",
    "bg_image",
    "bg_size",
    "bg_position",
    "bg_repeat",
    "bg_attachment",
    "font_family",
    "font_size",
    "font_weight",
    "font_style",
    "line_height",
    "letter_spacing",
    "text_align",
    "text_transform",
    "text_decoration",
    "border",
    "radius",
    "padding",
    "margin",
    "opacity",
  ];

  function copyNodeStyle(id) {
    var node = getNode(id);
    if (!node) return;
    normalizeNodeStyles(node);
    var bag = node.styles[state.breakpoint] || {};
    var clip = {};
    STYLE_COPY_KEYS.forEach(function (k) {
      if (bag[k] != null && bag[k] !== "") clip[k] = bag[k];
    });
    // Fallback: also pull cascaded view styles so something useful is copied
    if (!Object.keys(clip).length) {
      var cascaded = stylesFor(node, "view");
      STYLE_COPY_KEYS.forEach(function (k) {
        if (cascaded[k] != null && cascaded[k] !== "") clip[k] = cascaded[k];
      });
    }
    state.styleClipboard = clip;
    var n = Object.keys(clip).length;
    setStatus(
      n ? "Estilo copiado (" + n + " props · " + (BP_LABEL[state.breakpoint] || state.breakpoint) + ")" : "Nada para copiar neste breakpoint",
      n ? "is-ok" : "is-err"
    );
    updateSelectionUI();
    paintProps();
  }

  function pasteNodeStyle(id) {
    var clip = state.styleClipboard;
    if (!clip || !Object.keys(clip).length) {
      setStatus("Copie um estilo antes de colar", "is-err");
      return;
    }
    var node = getNode(id);
    if (!node) return;
    pushHistory();
    var d = ensureStyles(node, state.breakpoint);
    Object.keys(clip).forEach(function (k) {
      d[k] = clip[k];
    });
    if (clip.font_family) ensureGoogleFont(clip.font_family);
    markDirty();
    refreshNodeVisual(id);
    paintProps();
    setStatus("Estilo colado · " + (BP_LABEL[state.breakpoint] || state.breakpoint), "is-dirty");
  }

  function defaultGlobals() {
    return {
      layout: {
        content_max: "1200px",
        content_width: "90%",
        column_gap: "24px",
      },
      colors: {
        primary: "#7c3aed",
        secondary: "#0f172a",
        text: "#0f172a",
        text_muted: "#64748b",
        bg: "#ffffff",
        border: "#e2e8f0",
        btn_text: "#ffffff",
      },
      typography: {
        font_body: "",
        font_heading: "",
        h1: "2.5rem",
        h2: "2rem",
        h3: "1.5rem",
        body: "1rem",
      },
      radius: { md: "8px" },
      chrome: {
        header_sticky: "0",
        header_sticky_bg: "#ffffff",
        header_transparent: "0",
        footer_bg: "",
        footer_text: "",
        credit_text_color: "#4f46e5",
        credit_bg_color: "#ffffff",
      },
    };
  }

  function ensureGlobals() {
    if (!state.globals) state.globals = defaultGlobals();
    var d = defaultGlobals();
    state.globals.layout = Object.assign({}, d.layout, state.globals.layout || {});
    state.globals.colors = Object.assign({}, d.colors, state.globals.colors || {});
    state.globals.typography = Object.assign({}, d.typography, state.globals.typography || {});
    state.globals.radius = Object.assign({}, d.radius, state.globals.radius || {});
    state.globals.chrome = Object.assign({}, d.chrome, state.globals.chrome || {});
    // custom_css removed (WP.org: no arbitrary CSS injection — use Customizer).
    delete state.globals.custom_css;
    return state.globals;
  }

  /**
   * Inject / update design tokens in the editor.
   * Scope to #ab-page-body so WP admin chrome does not swallow :root vars,
   * and the canvas updates live when colors/fonts change.
   */
  function applyGlobalsCss(cssText) {
    var id = "aura-builder-globals";
    var el = document.getElementById(id);
    if (!el) {
      el = document.createElement("style");
      el.id = id;
      document.head.appendChild(el);
    }
    var g = ensureGlobals();
    var c = g.colors;
    var t = g.typography;
    var l = g.layout;
    var r = g.radius;

    // Always load Google fonts used in tokens so the canvas can render them.
    if (t.font_body) ensureGoogleFont(t.font_body);
    if (t.font_heading) ensureGoogleFont(t.font_heading);

    if (cssText) {
      // Server CSS targets front; wrap/duplicate for editor canvas live preview.
      el.textContent =
        cssText +
        "\n" +
        buildEditorGlobalsPreviewCss(g);
      return;
    }
    el.textContent = buildEditorGlobalsPreviewCss(g);
  }

  function buildEditorGlobalsPreviewCss(g) {
    g = g || ensureGlobals();
    var c = g.colors || {};
    var t = g.typography || {};
    var l = g.layout || {};
    var r = g.radius || {};
    var primary = c.primary || "#7c3aed";
    var secondary = c.secondary || "#0f172a";
    var text = c.text || "#0f172a";
    var muted = c.text_muted || "#64748b";
    var bg = c.bg || "#ffffff";
    var border = c.border || "#e2e8f0";
    var btnText = c.btn_text || "#ffffff";
    var fontBody = t.font_body || "system-ui, sans-serif";
    var fontHead = t.font_heading || fontBody;
    var h1 = t.h1 || "2.5rem";
    var h2 = t.h2 || "2rem";
    var h3 = t.h3 || "1.5rem";
    var bodySz = t.body || "1rem";
    var radius = r.md || "8px";

    var vars =
      "--ab-color-primary:" +
      primary +
      ";--ab-color-secondary:" +
      secondary +
      ";--ab-color-text:" +
      text +
      ";--ab-color-text-muted:" +
      muted +
      ";--ab-color-bg:" +
      bg +
      ";--ab-color-border:" +
      border +
      ";--ab-color-btn-text:" +
      btnText +
      ";--ab-content-max:" +
      (l.content_max || "1200px") +
      ";--ab-content-width:" +
      (l.content_width || "90%") +
      ";--ab-column-gap:" +
      (l.column_gap || "24px") +
      ";--ab-radius-md:" +
      radius +
      ";--ab-font-size-h1:" +
      h1 +
      ";--ab-font-size-h2:" +
      h2 +
      ";--ab-font-size-h3:" +
      h3 +
      ";--ab-font-size-body:" +
      bodySz +
      ";--ab-font-body:" +
      fontBody +
      ";--ab-font-heading:" +
      fontHead +
      ";";

    // :root (admin) + canvas scope — canvas must win for live editing.
    var css =
      ":root{" +
      vars +
      "}" +
      "#ab-page-body,.ab-document--editor,.ab-page-body{" +
      vars +
      "color:var(--ab-color-text);" +
      "background-color:var(--ab-color-bg);" +
      "font-family:var(--ab-font-body);" +
      "font-size:var(--ab-font-size-body);" +
      "}" +
      /* Headings / body text — live sizes & fonts (local inline still wins if set) */
      "#ab-page-body .ab-heading," +
      "#ab-page-body .ab-type-heading .ab-heading{" +
      "font-family:var(--ab-font-heading)!important;" +
      "}" +
      "#ab-page-body .ab-heading.ab-h1," +
      '#ab-page-body .ab-heading[data-tag="h1"],' +
      "#ab-page-body h1.ab-heading{font-size:var(--ab-font-size-h1)!important;}" +
      "#ab-page-body .ab-heading.ab-h2," +
      '#ab-page-body .ab-heading[data-tag="h2"],' +
      "#ab-page-body h2.ab-heading{font-size:var(--ab-font-size-h2)!important;}" +
      "#ab-page-body .ab-heading.ab-h3," +
      '#ab-page-body .ab-heading[data-tag="h3"],' +
      "#ab-page-body h3.ab-heading{font-size:var(--ab-font-size-h3)!important;}" +
      "#ab-page-body .ab-text," +
      "#ab-page-body .ab-icon-box__desc," +
      "#ab-page-body .ab-image-box__desc{" +
      "font-family:var(--ab-font-body)!important;" +
      "font-size:var(--ab-font-size-body);" +
      "}" +
      /* Buttons — defaults only (NO !important). Node styles / StyleEngine must win
         so the canvas matches the public site colors. */
      "#ab-page-body .ab-btn," +
      "#ab-page-body a.ab-btn{" +
      "background:var(--ab-color-primary);" +
      "background-color:var(--ab-color-primary);" +
      "color:var(--ab-color-btn-text);" +
      "border-radius:var(--ab-radius-md);" +
      "}" +
      /* Theme block CSS must not recolor builder buttons */
      "#ab-page-body .ab-type-button .ab-btn," +
      "#ab-page-body .ab-btn-wrap .ab-btn{" +
      "text-decoration:none;" +
      "}" +
      /* Soft text / borders tokens for widgets without local override */
      "#ab-page-body .ab-icon-box__title," +
      "#ab-page-body .ab-image-box__title," +
      "#ab-page-body .ab-counter__title{" +
      "color:var(--ab-color-text);" +
      "font-family:var(--ab-font-heading);" +
      "}" +
      "#ab-page-body .ab-field-hint," +
      "#ab-page-body .ab-loop-card__meta," +
      "#ab-page-body .ab-testimonial__role{" +
      "color:var(--ab-color-text-muted);" +
      "}";

    return css;
  }

  function loadGlobals() {
    return api("/globals")
      .then(function (res) {
        state.globals = res.globals || defaultGlobals();
        ensureGlobals();
        state.globalsDirty = false;
        applyGlobalsCss(res.css || "");
        return state.globals;
      })
      .catch(function () {
        state.globals = defaultGlobals();
        applyGlobalsCss("");
        return state.globals;
      });
  }

  function saveGlobals() {
    ensureGlobals();
    return api("/globals", { method: "POST", body: { globals: state.globals } }).then(function (res) {
      state.globals = res.globals || state.globals;
      state.globalsDirty = false;
      applyGlobalsCss(res.css || "");
      setStatus("Globais salvos", "is-ok");
      return res;
    });
  }

  var BP_WIDTH = { desktop: "100%", tablet: "768px", mobile: "390px" };
  var BP_LABEL = { desktop: "Desktop", tablet: "Tablet", mobile: "Mobile" };

  /** Compact SVG icons (stroke) — titles via title/tooltip, not text labels */
  function svgIcon(name) {
    var common = ' xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"';
    var map = {
      widgets: '<svg' + common + '><rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/></svg>',
      layers: '<svg' + common + '><polygon points="12 2 2 7 12 12 22 7 12 2"/><polyline points="2 17 12 22 22 17"/><polyline points="2 12 12 17 22 12"/></svg>',
      settings: '<svg' + common + '><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 1 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 1 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 1 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 1 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg>',
      desktop: '<svg' + common + '><rect x="2" y="3" width="20" height="14" rx="2"/><line x1="8" y1="21" x2="16" y2="21"/><line x1="12" y1="17" x2="12" y2="21"/></svg>',
      tablet: '<svg' + common + '><rect x="4" y="2" width="16" height="20" rx="2"/><line x1="12" y1="18" x2="12.01" y2="18"/></svg>',
      mobile: '<svg' + common + '><rect x="7" y="2" width="10" height="20" rx="2"/><line x1="12" y1="18" x2="12.01" y2="18"/></svg>',
      undo: '<svg' + common + '><polyline points="1 4 1 10 7 10"/><path d="M3.51 15a9 9 0 1 0 2.13-9.36L1 10"/></svg>',
      redo: '<svg' + common + '><polyline points="23 4 23 10 17 10"/><path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"/></svg>',
      save: '<svg' + common + '><path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"/><polyline points="17 21 17 13 7 13 7 21"/><polyline points="7 3 7 8 15 8"/></svg>',
      library: '<svg' + common + '><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/><line x1="10" y1="7" x2="16" y2="7"/><line x1="10" y1="11" x2="16" y2="11"/></svg>',
      view: '<svg' + common + '><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>',
      back: '<svg' + common + '><path d="M19 12H5"/><polyline points="12 19 5 12 12 5"/></svg>',
      section: '<svg' + common + '><rect x="3" y="4" width="18" height="16" rx="1"/><line x1="3" y1="10" x2="21" y2="10"/></svg>',
      container: '<svg' + common + '><rect x="3" y="3" width="18" height="18" rx="2"/><line x1="3" y1="9" x2="21" y2="9"/><line x1="9" y1="21" x2="9" y2="9"/></svg>',
      heading: '<svg' + common + '><path d="M6 4v16"/><path d="M18 4v16"/><path d="M6 12h12"/></svg>',
      text: '<svg' + common + '><line x1="4" y1="6" x2="20" y2="6"/><line x1="4" y1="12" x2="16" y2="12"/><line x1="4" y1="18" x2="12" y2="18"/></svg>',
      image: '<svg' + common + '><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>',
      button: '<svg' + common + '><rect x="3" y="8" width="18" height="8" rx="2"/><line x1="8" y1="12" x2="16" y2="12"/></svg>',
      spacer: '<svg' + common + '><line x1="12" y1="5" x2="12" y2="19"/><polyline points="8 9 12 5 16 9"/><polyline points="8 15 12 19 16 15"/></svg>',
      divider: '<svg' + common + '><line x1="4" y1="12" x2="20" y2="12"/></svg>',
      html: '<svg' + common + '><polyline points="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 18"/></svg>',
      layout: '<svg' + common + '><rect x="3" y="3" width="18" height="18" rx="2"/><line x1="3" y1="9" x2="21" y2="9"/><line x1="9" y1="21" x2="9" y2="9"/></svg>',
      plus: '<svg' + common + '><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>',
      close: '<svg' + common + '><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>',
      header: '<svg' + common + '><rect x="3" y="3" width="18" height="6" rx="1"/><line x1="3" y1="14" x2="21" y2="14"/><line x1="3" y1="18" x2="14" y2="18"/></svg>',
      footer: '<svg' + common + '><line x1="3" y1="6" x2="21" y2="6"/><line x1="3" y1="10" x2="14" y2="10"/><rect x="3" y="15" width="18" height="6" rx="1"/></svg>',
      color: '<svg' + common + '><circle cx="12" cy="12" r="9"/><path d="M12 3a9 9 0 0 1 0 18"/></svg>',
      nav: '<svg' + common + '><circle cx="5" cy="12" r="1.5" fill="currentColor" stroke="none"/><circle cx="12" cy="12" r="1.5" fill="currentColor" stroke="none"/><circle cx="19" cy="12" r="1.5" fill="currentColor" stroke="none"/><path d="M5 12h14" opacity=".3"/></svg>',
      home: '<svg' + common + '><path d="M3 11l9-8 9 8"/><path d="M5 10v10h14V10"/></svg>',
      logo: '<svg' + common + '><circle cx="12" cy="12" r="9"/><path d="M12 7v5l3 2"/></svg>',
      siteTitle: '<svg' + common + '><path d="M4 7h16"/><path d="M4 12h10"/><path d="M4 17h8"/></svg>',
      tagline: '<svg' + common + '><line x1="4" y1="8" x2="20" y2="8"/><line x1="4" y1="12" x2="16" y2="12"/><line x1="4" y1="16" x2="12" y2="16"/></svg>',
      avatar: '<svg' + common + '><circle cx="12" cy="8" r="4"/><path d="M4 20c0-4 3.5-7 8-7s8 3 8 7"/></svg>',
      comments: '<svg' + common + '><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>',
      date: '<svg' + common + '><rect x="3" y="4" width="18" height="18" rx="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/></svg>',
      tags: '<svg' + common + '><path d="M20.59 13.41l-7.17 7.17a2 2 0 0 1-2.83 0L2 12V2h10l8.59 8.59a2 2 0 0 1 0 2.82z"/><line x1="7" y1="7" x2="7.01" y2="7"/></svg>',
      prev: '<svg' + common + '><polyline points="15 18 9 12 15 6"/></svg>',
      next: '<svg' + common + '><polyline points="9 18 15 12 9 6"/></svg>',
      time: '<svg' + common + '><circle cx="12" cy="12" r="9"/><polyline points="12 7 12 12 15 15"/></svg>',
      words: '<svg' + common + '><path d="M4 7h4"/><path d="M6 7v10"/><path d="M14 7h6"/><path d="M14 12h6"/><path d="M14 17h4"/></svg>',
      readMore: '<svg' + common + '><path d="M5 12h14"/><polyline points="12 5 19 12 12 19"/></svg>',
      breadcrumbs: '<svg' + common + '><circle cx="6" cy="12" r="2"/><circle cx="18" cy="12" r="2"/><path d="M8 12h8"/></svg>',
      login: '<svg' + common + '><path d="M15 3h4a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-4"/><polyline points="10 17 15 12 10 7"/><line x1="15" y1="12" x2="3" y2="12"/></svg>',
      youtube: '<svg' + common + '><rect x="2" y="5" width="20" height="14" rx="3"/><polygon points="10 9 16 12 10 15 10 9" fill="currentColor" stroke="none"/></svg>',
      x: '<svg' + common + '><path d="M4 4l7 8-7 8h3l5-6 5 6h3l-7-8 7-8h-3l-5 6-5-6z"/></svg>',
      embed: '<svg' + common + '><polyline points="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 18"/></svg>',
      gallery: '<svg' + common + '><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>',
      video: '<svg' + common + '><rect x="2" y="5" width="20" height="14" rx="2"/><polygon points="10 9 16 12 10 15 10 9" fill="currentColor" stroke="none"/></svg>',
      star: '<svg' + common + '><polygon points="12 2 15 9 22 9 17 14 19 21 12 17 5 21 7 14 2 9 9 9"/></svg>',
      maps: '<svg' + common + '><path d="M21 10c0 7-9 13-9 13S3 17 3 10a9 9 0 1 1 18 0z"/><circle cx="12" cy="10" r="3"/></svg>',
      icon: '<svg' + common + '><circle cx="12" cy="12" r="9"/><path d="M12 8v4l2 2"/></svg>',
      image_box: '<svg' + common + '><rect x="3" y="3" width="18" height="12" rx="1"/><line x1="6" y1="19" x2="18" y2="19"/><line x1="8" y1="22" x2="16" y2="22"/></svg>',
      icon_box: '<svg' + common + '><rect x="4" y="4" width="16" height="16" rx="2"/><circle cx="12" cy="10" r="2"/><line x1="8" y1="16" x2="16" y2="16"/></svg>',
      carousel: '<svg' + common + '><rect x="2" y="6" width="20" height="12" rx="2"/><polyline points="8 12 11 15 16 9"/></svg>',
      popup: '<svg' + common + '><rect x="3" y="5" width="18" height="14" rx="2"/><path d="M8 9h8M8 13h5"/><circle cx="18" cy="6" r="3" fill="currentColor" stroke="none"/></svg>',
      icon_list: '<svg' + common + '><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.5" fill="currentColor" stroke="none"/><circle cx="4" cy="12" r="1.5" fill="currentColor" stroke="none"/><circle cx="4" cy="18" r="1.5" fill="currentColor" stroke="none"/></svg>',
      counter: '<svg' + common + '><path d="M4 19V5"/><path d="M4 19h16"/><path d="M8 15l3-8 3 5 3-3"/></svg>',
      testimonial: '<svg' + common + '><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>',
      tabs: '<svg' + common + '><rect x="3" y="8" width="18" height="13" rx="1"/><path d="M3 8V6a1 1 0 0 1 1-1h4v3"/><path d="M12 5h4a1 1 0 0 1 1 1v2"/></svg>',
      accordion: '<svg' + common + '><rect x="3" y="4" width="18" height="5" rx="1"/><rect x="3" y="11" width="18" height="5" rx="1"/><rect x="3" y="18" width="18" height="2" rx="1"/></svg>',
      toggle: '<svg' + common + '><rect x="3" y="6" width="18" height="4" rx="1"/><path d="M7 16h10"/><path d="M7 20h6"/></svg>',
      social: '<svg' + common + '><circle cx="6" cy="12" r="2"/><circle cx="18" cy="6" r="2"/><circle cx="18" cy="18" r="2"/><path d="M8 11l8-4M8 13l8 4"/></svg>',
      progress: '<svg' + common + '><line x1="3" y1="12" x2="21" y2="12"/><line x1="3" y1="12" x2="14" y2="12" stroke-width="3"/></svg>',
      soundcloud: '<svg' + common + '><path d="M4 14a4 4 0 0 1 4-4 5 5 0 0 1 9.5 1.5A3.5 3.5 0 0 1 18 18H6a3 3 0 0 1-2-5.2"/></svg>',
      shortcode: '<svg' + common + '><polyline points="8 6 2 12 8 18"/><polyline points="16 6 22 12 16 18"/></svg>',
      anchor: '<svg' + common + '><circle cx="12" cy="5" r="2"/><path d="M12 7v12"/><path d="M5 13h14"/><path d="M8 20c0-2 1.5-3 4-3s4 1 4 3"/></svg>',
      alert: '<svg' + common + '><circle cx="12" cy="12" r="9"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg>',
      sidebar: '<svg' + common + '><rect x="3" y="3" width="18" height="18" rx="1"/><line x1="9" y1="3" x2="9" y2="21"/></svg>',
      text_path: '<svg' + common + '><path d="M4 16c4-8 12-8 16 0"/><path d="M8 10l-1-3 3 1"/><path d="M16 10l1-3-3 1"/></svg>',
      loop: '<svg' + common + '><rect x="3" y="3" width="7" height="7" rx="1"/><rect x="14" y="3" width="7" height="7" rx="1"/><rect x="3" y="14" width="7" height="7" rx="1"/><rect x="14" y="14" width="7" height="7" rx="1"/></svg>',
      /** Básicos — texto / tipografia (não grade) */
      basic:
        '<svg' +
        common +
        '><path d="M4 6h16"/><path d="M4 12h12"/><path d="M4 18h8"/><path d="M18 14v6"/><path d="M16 18h4"/></svg>',
      /** Criativo — estrelas / brilho */
      creative:
        '<svg' +
        common +
        '><path d="M12 3l1.5 4.5L18 9l-4.5 1.5L12 15l-1.5-4.5L6 9l4.5-1.5L12 3z"/><path d="M19 14l.8 2.2L22 17l-2.2.8L19 20l-.8-2.2L16 17l2.2-.8L19 14z"/><path d="M5 15l.6 1.6L7 17.2l-1.4.6L5 19.4l-.6-1.6L3 17.2l1.4-.6L5 15z"/></svg>',
      /** Dinâmicos — setas de atualização / dados vivos */
      dynamic:
        '<svg' +
        common +
        '><path d="M21 12a9 9 0 0 0-15.5-6.4"/><polyline points="3 4 3 9 8 9"/><path d="M3 12a9 9 0 0 0 15.5 6.4"/><polyline points="21 20 21 15 16 15"/></svg>',
      panels: '<svg' + common + '><rect x="3" y="4" width="18" height="4" rx="1"/><rect x="3" y="10" width="18" height="10" rx="1"/></svg>',
      list: '<svg' + common + '><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.5" fill="currentColor" stroke="none"/><circle cx="4" cy="12" r="1.5" fill="currentColor" stroke="none"/><circle cx="4" cy="18" r="1.5" fill="currentColor" stroke="none"/></svg>',
      image: '<svg' + common + '><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="8.5" cy="8.5" r="1.5"/><path d="M21 15l-5-5L5 21"/></svg>',
      page: '<svg' + common + '><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="8" y1="13" x2="16" y2="13"/><line x1="8" y1="17" x2="14" y2="17"/></svg>',
      /** Responsive / multi-device */
      responsive:
        '<svg' +
        common +
        '><rect x="2" y="5" width="10" height="8" rx="1"/><rect x="14" y="3" width="8" height="12" rx="1.5"/><line x1="17" y1="17" x2="19" y2="17"/></svg>',
      /** Page/menu item (Gutenberg list row) */
      menuitem:
        '<svg' +
        common +
        '><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/></svg>',
    };
    return map[name] || map.widgets;
  }

  function forceCustomKey(name, attr) {
    return (state.selectedId || "") + "|" + (attr || "data-prop") + "|" + name;
  }
  function isForceCustom(name, attr) {
    return !!(state.forceCustom && state.forceCustom[forceCustomKey(name, attr)]);
  }
  function setForceCustom(name, attr, on) {
    state.forceCustom = state.forceCustom || {};
    var k = forceCustomKey(name, attr);
    if (on) state.forceCustom[k] = true;
    else delete state.forceCustom[k];
  }

  function getBreakpoints() {
    ensureDocSettings();
    var bp = (state.document && state.document.settings && state.document.settings.breakpoints) || {};
    return {
      mobile: parseInt(bp.mobile, 10) || 767,
      tablet: parseInt(bp.tablet, 10) || 1024,
      laptop: parseInt(bp.laptop, 10) || 1400,
    };
  }

  var dragState = null;
  var resizeState = null;

  function uid() {
    return "node_" + Math.random().toString(36).slice(2, 12);
  }

  function esc(s) {
    return String(s == null ? "" : s)
      .replace(/&/g, "&amp;")
      .replace(/</g, "&lt;")
      .replace(/>/g, "&gt;")
      .replace(/"/g, "&quot;");
  }

  /** Decode HTML entities from WP REST titles (e.g. &#8211; → –). */
  function decodeEntities(str) {
    if (str == null || str === "") return "";
    var t = document.createElement("textarea");
    t.innerHTML = String(str);
    return t.value || String(str);
  }

  /**
   * Renova X-WP-Nonce via admin-ajax (não depende de REST cookie/nonce).
   * Evita "A verificação do cookie falhou" após sessão longa no editor.
   */
  function refreshNonce() {
    var url = CFG.ajaxUrl || (typeof window.ajaxurl === "string" ? window.ajaxurl : "") || "/wp-admin/admin-ajax.php";
    var sep = url.indexOf("?") >= 0 ? "&" : "?";
    return fetch(url + sep + "action=aptb_rest_nonce", {
      method: "POST",
      credentials: "same-origin",
      headers: { "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8" },
      body: "",
    }).then(function (r) {
      return r.json().then(function (j) {
        var n =
          (j && j.success && j.data && j.data.nonce) ||
          (j && j.data && j.data.nonce) ||
          (j && j.nonce) ||
          "";
        if (!n) {
          throw new Error(
            (j && j.data && j.data.message) ||
              (j && j.message) ||
              "Não foi possível renovar a sessão"
          );
        }
        nonce = n;
        CFG.nonce = n;
        return n;
      });
    });
  }

  function stripWpHtml(msg) {
    return String(msg || "")
      .replace(/<[^>]+>/g, "")
      .replace(/&hellip;/g, "…")
      .replace(/&nbsp;/g, " ")
      .trim();
  }

  function isRestCookieError(status, code, msg) {
    if (status !== 403 && status !== 401) return false;
    if (code === "rest_cookie_invalid_nonce" || code === "rest_forbidden") return true;
    return /verifica[cç][aã]o.*cookie|cookie check failed|invalid.?nonce|rest_cookie/i.test(
      String(msg || "") + " " + String(code || "")
    );
  }

  function api(path, opts) {
    opts = opts || {};
    var attempt = opts._retry || 0;
    return fetch(restUrl + path, {
      method: opts.method || "GET",
      headers: { "Content-Type": "application/json", "X-WP-Nonce": nonce },
      body: opts.body ? JSON.stringify(opts.body) : undefined,
      credentials: "same-origin",
    }).then(function (r) {
      return r.text().then(function (text) {
        var j = null;
        if (text) {
          try {
            j = JSON.parse(text);
          } catch (e) {
            j = null;
          }
        }
        if (!r.ok) {
          var code = j && j.code;
          var rawMsg = (j && (j.message || j.code)) || text || "Erro " + r.status;
          var msg = stripWpHtml(rawMsg) || "Erro " + r.status;
          if (isRestCookieError(r.status, code, msg) && attempt < 1) {
            return refreshNonce()
              .then(function () {
                return api(path, Object.assign({}, opts, { _retry: attempt + 1 }));
              })
              .catch(function () {
                throw new Error(
                  "Sessão expirada — recarregue a página (F5) e salve de novo"
                );
              });
          }
          if (isRestCookieError(r.status, code, msg)) {
            throw new Error(
              "Sessão expirada — recarregue a página (F5) e salve de novo"
            );
          }
          throw new Error(msg);
        }
        if (j && j.nonce) {
          nonce = j.nonce;
          CFG.nonce = j.nonce;
        }
        return j;
      });
    });
  }

  function cloneDoc(doc) {
    return JSON.parse(JSON.stringify(doc));
  }

  function pushHistory() {
    if (!state.document) return;
    state.history.push(cloneDoc(state.document));
    if (state.history.length > 50) state.history.shift();
    state.future = [];
  }

  function undo() {
    if (!state.history.length) return;
    state.future.push(cloneDoc(state.document));
    state.document = state.history.pop();
    state.dirty = true;
    setStatus("Desfeito", "is-dirty");
    paint();
  }

  function redo() {
    if (!state.future.length) return;
    state.history.push(cloneDoc(state.document));
    state.document = state.future.pop();
    state.dirty = true;
    setStatus("Refeito", "is-dirty");
    paint();
  }

  function markDirty() {
    state.dirty = true;
    setStatus("Não salvo", "is-dirty");
  }

  function setStatus(msg, cls) {
    state.status = msg;
    state.statusClass = cls || "";
    var el = document.getElementById("ab-status");
    if (el) {
      el.textContent = msg;
      el.title = msg; // mensagem completa no hover (barra trunca em alguns layouts)
      el.className = "ab-status " + (cls || "");
    }
  }

  function getNode(id) {
    return state.document && state.document.nodes ? state.document.nodes[id] : null;
  }

  /**
   * Canvas node element only — never the layers list.
   * Layers also use [data-node-id]; document.querySelector hits them first and
   * makes live style updates look broken until a full paint (device icon click).
   */
  function canvasNodeEl(id) {
    if (!id) return null;
    var root =
      document.getElementById("ab-page-body") ||
      document.getElementById("ab-canvas");
    if (!root) return null;
    try {
      return root.querySelector('[data-node-id="' + id + '"]');
    } catch (e) {
      return null;
    }
  }

  function execute(cmd) {
    pushHistory();
    cmd();
    markDirty();
    paint();
  }

  /**
   * Coerce styles bags to plain objects.
   * PHP encodes empty arrays as [] — keys assigned on [] are LOST in JSON.stringify.
   */
  function plainObj(v) {
    if (!v || typeof v !== "object") return {};
    if (!Array.isArray(v)) return v;
    var o = {};
    Object.keys(v).forEach(function (k) {
      if (isNaN(Number(k))) o[k] = v[k];
    });
    return o;
  }

  function normalizeNodeStyles(node) {
    if (!node) return;
    if (!node.styles || Array.isArray(node.styles)) node.styles = {};
    node.styles.desktop = plainObj(node.styles.desktop);
    node.styles.tablet = plainObj(node.styles.tablet);
    node.styles.mobile = plainObj(node.styles.mobile);
  }

  function normalizeDocumentStyles(doc) {
    if (!doc || !doc.nodes) return;
    Object.keys(doc.nodes).forEach(function (id) {
      normalizeNodeStyles(doc.nodes[id]);
    });
  }

  function ensureStyles(node, bp) {
    bp = bp || state.breakpoint || "desktop";
    normalizeNodeStyles(node);
    return node.styles[bp];
  }

  /**
   * Cascaded styles desktop ← tablet ← mobile.
   * @param {object} node
   * @param {string} [mode] "view" = canvas preview (viewBp); "edit" = style bag being edited
   */
  function stylesFor(node, mode) {
    normalizeNodeStyles(node);
    var bp = mode === "edit" ? state.breakpoint || "desktop" : state.viewBp || state.breakpoint || "desktop";
    var out = {};
    var d = node.styles.desktop || {};
    Object.keys(d).forEach(function (k) {
      if (d[k] !== "" && d[k] != null) out[k] = d[k];
    });
    if (bp === "tablet" || bp === "mobile") {
      var t = node.styles.tablet || {};
      Object.keys(t).forEach(function (k) {
        if (t[k] !== "" && t[k] != null) out[k] = t[k];
      });
    }
    if (bp === "mobile") {
      var m = node.styles.mobile || {};
      Object.keys(m).forEach(function (k) {
        if (m[k] !== "" && m[k] != null) out[k] = m[k];
      });
    }
    return out;
  }

  function styleValAt(node, key, bp) {
    normalizeNodeStyles(node);
    var bag = node.styles[bp] || {};
    return bag[key] != null && bag[key] !== "" ? bag[key] : "";
  }

  function plainText(html) {
    if (html == null) return "";
    var s = String(html);
    if (!/<[a-z][\s\S]*>/i.test(s)) return s;
    var d = document.createElement("div");
    d.innerHTML = s;
    return (d.textContent || d.innerText || "").replace(/\s+/g, " ").trim();
  }

  /**
   * Format ISO/REST date with WordPress date_format (CFG.dateFormat).
   * Supports common tokens: d j m n Y y F M.
   */
  function formatWpDate(iso, format) {
    if (!iso) return "";
    var fmt = format || CFG.dateFormat || "d/m/Y";
    var raw = String(iso);
    // WP REST often returns "2026-08-12T10:00:00" without Z — treat as local.
    var d = new Date(raw.indexOf("T") >= 0 && raw.indexOf("Z") < 0 && raw.indexOf("+") < 0 ? raw.replace(" ", "T") : raw);
    if (isNaN(d.getTime())) {
      // Fallback: YYYY-MM-DD → try parse
      var m = raw.match(/^(\d{4})-(\d{2})-(\d{2})/);
      if (m) d = new Date(parseInt(m[1], 10), parseInt(m[2], 10) - 1, parseInt(m[3], 10));
    }
    if (isNaN(d.getTime())) return raw.slice(0, 10);
    var months = [
      "Janeiro",
      "Fevereiro",
      "Março",
      "Abril",
      "Maio",
      "Junho",
      "Julho",
      "Agosto",
      "Setembro",
      "Outubro",
      "Novembro",
      "Dezembro",
    ];
    var monthsShort = ["Jan", "Fev", "Mar", "Abr", "Mai", "Jun", "Jul", "Ago", "Set", "Out", "Nov", "Dez"];
    var map = {
      d: String(d.getDate()).padStart(2, "0"),
      j: String(d.getDate()),
      m: String(d.getMonth() + 1).padStart(2, "0"),
      n: String(d.getMonth() + 1),
      Y: String(d.getFullYear()),
      y: String(d.getFullYear()).slice(-2),
      F: months[d.getMonth()],
      M: monthsShort[d.getMonth()],
    };
    return String(fmt).replace(/\\?([djmnYyFM])/g, function (all, tok) {
      if (all.charAt(0) === "\\") return tok;
      return map[tok] != null ? map[tok] : tok;
    });
  }

  /**
   * Safe CSS url() for use inside HTML style="..." attributes.
   * Double quotes inside style="..." break the attribute — always use single quotes.
   */
  function cssUrl(u) {
    var s = String(u || "").trim();
    if (!s) return "none";
    // Escape backslashes and single quotes for CSS string
    s = s.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
    return "url('" + s + "')";
  }

  /** Parse gradient stops from style bag (JSON or default 2 colors). */
  function parseGradStops(raw, fallbackSolid) {
    var defA = fallbackSolid || "#1e3a8a";
    var defB = "#c41e3a";
    var stops = [];
    try {
      if (raw && String(raw).charAt(0) === "[") {
        var arr = JSON.parse(raw);
        if (Array.isArray(arr)) {
          arr.forEach(function (row) {
            if (!row) return;
            var c = String(row.c || row.color || "").trim();
            var p = parseInt(row.p != null ? row.p : row.pos, 10);
            if (!c) return;
            if (isNaN(p)) p = stops.length === 0 ? 0 : 100;
            p = Math.max(0, Math.min(100, p));
            stops.push({ c: c, p: p });
          });
        }
      }
    } catch (e) {}
    if (stops.length < 2) {
      stops = [
        { c: defA, p: 0 },
        { c: defB, p: 100 },
      ];
    }
    // sort by position
    stops.sort(function (a, b) {
      return a.p - b.p;
    });
    return stops;
  }

  function buildGradientCss(kind, angle, stops) {
    kind = kind === "radial" ? "radial" : "linear";
    angle = parseInt(angle, 10);
    if (isNaN(angle)) angle = 135;
    angle = ((angle % 360) + 360) % 360;
    var parts = stops.map(function (s) {
      return s.c + " " + s.p + "%";
    });
    if (kind === "radial") {
      return "radial-gradient(circle at center, " + parts.join(", ") + ")";
    }
    return "linear-gradient(" + angle + "deg, " + parts.join(", ") + ")";
  }

  function resolveBgGradient(st) {
    if (!st) return "";
    if (st.bg_type === "gradient" || (st.bg_grad_stops && String(st.bg_grad_stops).charAt(0) === "[")) {
      if (st.bg_type && st.bg_type !== "gradient") return "";
      var stops = parseGradStops(st.bg_grad_stops, st.background);
      return buildGradientCss(st.bg_grad_kind || "linear", st.bg_grad_angle || "135", stops);
    }
    if (st.bg_gradient && /gradient\s*\(/i.test(String(st.bg_gradient))) {
      return String(st.bg_gradient).trim();
    }
    return "";
  }

  /** CSS custom properties for comments form button (wins over StyleEngine text color). */
  function commentsFormCssVars(settings) {
    settings = settings || {};
    var parts = [];
    var bg = String(settings.btn_bg || "").trim();
    var fg = String(settings.btn_color || "").trim();
    var rad = settings.btn_radius;
    if (bg) parts.push("--ab-cf-btn-bg:" + bg);
    if (fg) parts.push("--ab-cf-btn-color:" + fg);
    if (rad !== undefined && rad !== null && String(rad) !== "") {
      var r = String(rad);
      if (/^\d+$/.test(r)) r += "px";
      parts.push("--ab-cf-btn-radius:" + r);
    }
    return parts.join(";");
  }

  /** Live-apply comments form styles after leaf re-render. */
  function applyCommentsFormLiveStyles(el, node, st) {
    if (!el || !node) return;
    st = st || {};
    var s = node.settings || {};
    var form = el.querySelector(".ab-comments-form") || el;
    var vars = commentsFormCssVars(s);
    if (vars) {
      vars.split(";").forEach(function (pair) {
        var i = pair.indexOf(":");
        if (i < 1) return;
        form.style.setProperty(pair.slice(0, i).trim(), pair.slice(i + 1).trim());
      });
    }
    var btn = el.querySelector(
      ".ab-comment-form__submit, .ab-comments-form .ab-btn.submit, .ab-comments-form button[type='submit']"
    );
    if (btn) {
      // Force button colors via vars + direct props (never inherit text color).
      var bg = String(s.btn_bg || "").trim();
      var fg = String(s.btn_color || "").trim() || "#ffffff";
      var rad = s.btn_radius;
      if (bg) {
        btn.style.setProperty("background", bg, "important");
        btn.style.setProperty("background-color", bg, "important");
        btn.style.setProperty("background-image", "none", "important");
      }
      btn.style.setProperty("color", fg, "important");
      btn.style.setProperty("-webkit-text-fill-color", fg, "important");
      if (rad !== undefined && rad !== null && String(rad) !== "") {
        var r = String(rad);
        if (/^\d+$/.test(r)) r += "px";
        btn.style.setProperty("border-radius", r, "important");
      }
      btn.classList.toggle("is-full", (s.btn_full_width || "0") === "1");
    }
    // Typography / text color only on content — never on chrome or submit.
    var textTargets = el.querySelectorAll(
      ".ab-comments-form__title, .comment-reply-title, .ab-comment-form label, .ab-comments-form__list-title"
    );
    if (st.font_family) {
      var ff = normalizeFontFamilyCss(st.font_family) || st.font_family;
      ensureGoogleFont(ff);
      textTargets.forEach(function (ch) {
        ch.style.setProperty("font-family", ff, "important");
      });
    }
    if (st.font_size) {
      textTargets.forEach(function (ch) {
        ch.style.setProperty("font-size", st.font_size, "important");
      });
    }
    if (st.font_weight) {
      el.querySelectorAll(".ab-comments-form__title, .comment-reply-title").forEach(function (ch) {
        ch.style.setProperty("font-weight", st.font_weight, "important");
      });
    }
    if (st.color) {
      textTargets.forEach(function (ch) {
        ch.style.setProperty("color", st.color, "important");
        ch.style.setProperty("-webkit-text-fill-color", st.color, "important");
      });
    }
  }

  /** Skip editor chrome + resize handles when cascading styles. */
  function isEditorChromeEl(ch) {
    if (!ch || !ch.closest) return false;
    return !!(
      ch.closest(".ab-overlay-chrome") ||
      ch.closest(".ab-resize-handle") ||
      (ch.classList &&
        (ch.classList.contains("ab-overlay-chrome") ||
          ch.classList.contains("ab-resize-handle") ||
          ch.classList.contains("ab-overlay-label") ||
          ch.classList.contains("ab-overlay-actions")))
    );
  }

  /** Apply background color / gradient + image via CSSOM. */
  function applyNodeBackground(el, st) {
    if (!el || !st) return;
    var grad = resolveBgGradient(st);
    var layers = [];
    if (st.bg_image) {
      layers.push(cssUrl(st.bg_image));
    }
    if (grad) {
      layers.push(grad);
    }
    if (layers.length) {
      el.style.setProperty("background-image", layers.join(", "), "important");
      if (st.bg_image) {
        el.style.setProperty("background-size", st.bg_size || "cover", "important");
        el.style.setProperty(
          "background-position",
          st.bg_position || "center center",
          "important"
        );
        el.style.setProperty("background-repeat", st.bg_repeat || "no-repeat", "important");
        if (st.bg_attachment) {
          el.style.setProperty("background-attachment", st.bg_attachment, "important");
        }
      } else if (grad) {
        el.style.setProperty("background-size", "auto", "important");
        el.style.setProperty("background-repeat", "no-repeat", "important");
      }
    } else {
      el.style.removeProperty("background-image");
    }
    if (st.background && !grad) {
      el.style.setProperty("background-color", String(st.background), "important");
    } else if (grad) {
      // Fallback color under gradient (first stop)
      var stops = parseGradStops(st.bg_grad_stops, st.background);
      el.style.setProperty("background-color", stops[0].c, "important");
    } else {
      el.style.removeProperty("background-color");
    }
  }

  /** True when margin L/R are 0/empty (not auto) — used to auto-center max_width boxes. */
  function marginHorizontalIsZero(margin) {
    var m = String(margin || "")
      .trim()
      .toLowerCase()
      .replace(/\s+/g, " ");
    if (!m) return true;
    var parts = m.split(" ");
    var left = "0";
    var right = "0";
    if (parts.length === 1) {
      return parts[0] === "0" || parts[0] === "0px";
    }
    if (parts.length === 2 || parts.length === 3) {
      left = right = parts[1];
    } else if (parts.length >= 4) {
      right = parts[1];
      left = parts[3];
    }
    if (left === "auto" || right === "auto") return false;
    function z(v) {
      return v === "0" || v === "0px" || v === "";
    }
    return z(left) && z(right);
  }

  function styleAttr(node) {
    var parts = [];
    var s = stylesFor(node, "view");
    if (s.hide === true || s.hide === "1" || s.hide === 1) {
      parts.push("display:none");
      return ' style="' + parts.join(";") + '"';
    }
    // Colors / bg image first with !important so chrome CSS cannot wipe them.
    // Never use the "background" shorthand — it resets background-image.
    // Use url('...') so double-quoted HTML style="..." is not truncated.
    var grad = resolveBgGradient(s);
    var bgLayers = [];
    if (s.bg_image) bgLayers.push(cssUrl(s.bg_image));
    if (grad) bgLayers.push(grad);
    if (s.background && !grad) {
      parts.push("background-color:" + s.background + " !important");
    } else if (grad) {
      var gStops = parseGradStops(s.bg_grad_stops, s.background);
      parts.push("background-color:" + gStops[0].c + " !important");
    }
    // Do NOT put color on the node wrapper — it inherits into the purple
    // editor chrome/tab (ab-overlay-chrome). Color is applied to text leaves
    // only in refreshNodeVisual / StyleEngine deep selectors.
    if (bgLayers.length) {
      parts.push("background-image:" + bgLayers.join(", ") + " !important");
      if (s.bg_image) {
        parts.push("background-size:" + (s.bg_size || "cover") + " !important");
        parts.push("background-position:" + (s.bg_position || "center center") + " !important");
        parts.push("background-repeat:" + (s.bg_repeat || "no-repeat") + " !important");
        if (s.bg_attachment) {
          parts.push("background-attachment:" + s.bg_attachment + " !important");
        }
      }
    }
    var map = {
      width: "width",
      height: "height",
      min_height: "min-height",
      margin: "margin",
      padding: "padding",
      font_size: "font-size",
      font_weight: "font-weight",
      text_align: "text-align",
      opacity: "opacity",
      radius: "border-radius",
      max_width: "max-width",
      z_index: "z-index",
      position: "position",
      border: "border",
      border_width: "border-width",
      border_style: "border-style",
      border_color: "border-color",
    };
    // Spacing / box model need !important so they beat front.css resets
    // (.ab-type-section margin:0, theme margin-inline:auto, etc.).
    var importantKeys = {
      margin: 1,
      padding: 1,
      width: 1,
      max_width: 1,
      height: 1,
      min_height: 1,
      radius: 1,
      border: 1,
      border_width: 1,
      border_style: 1,
      border_color: 1,
    };
    Object.keys(map).forEach(function (k) {
      if (s[k] !== undefined && s[k] !== null && s[k] !== "") {
        // Caixa de imagem: contorno é controlado pelo switch "Contorno" (settings.border),
        // não pelo estilo genérico — evita borda cinza residual.
        if (
          node.type === "image_box" &&
          (k === "border" || k === "border_width" || k === "border_style" || k === "border_color") &&
          (node.settings || {}).border !== "1"
        ) {
          return;
        }
        // Raiz: largura é só content_width / max_width em settings — estilos width/max_width
        // não devem sobrescrever e “voltar” para 1200px.
        if (node.type === "root" && (k === "max_width" || k === "width")) {
          return;
        }
        parts.push(map[k] + ":" + s[k] + (importantKeys[k] ? " !important" : ""));
        // CSS var so .is-selected can elevate chrome without wiping user z-index.
        if (k === "z_index") {
          parts.push("--ab-z:" + s[k]);
        }
        // Radius CSS var only — NEVER overflow:hidden on the node itself in the
        // editor (that clips resize handles which sit at top/left:-5px).
        // Clipping is done on inner media via CSS (border-radius + overflow).
        if (k === "radius") {
          var rad = String(s[k]);
          if (rad && rad !== "0" && rad !== "0px") {
            parts.push("--ab-radius:" + rad);
          }
        }
      }
    });
    // max_width with L/R margin 0 → center (same as StyleEngine on the front).
    if (
      node.type !== "root" &&
      s.max_width &&
      s.max_width !== "none" &&
      s.max_width !== "100%" &&
      marginHorizontalIsZero(s.margin || "")
    ) {
      parts.push("margin-left:auto !important");
      parts.push("margin-right:auto !important");
    }
    // Full width without max_width → no cap in the canvas.
    if (
      node.type !== "root" &&
      (s.width === "100%" || s.width === "100") &&
      (!s.max_width || s.max_width === "none" || s.max_width === "100%")
    ) {
      parts.push("width:100% !important");
      parts.push("max-width:none !important");
    }
    if (node.type === "image_box" && (node.settings || {}).border !== "1") {
      parts.push("border:none");
    }

    if (node.type === "root") {
      parts.push("box-sizing:border-box");
      parts.push("width:100%");
      // Empilha seções; gap = espaço entre seções (padrão 0 — sem “vão” branco).
      parts.push("display:flex");
      parts.push("flex-direction:column");
      parts.push("align-items:stretch");
      var rootGap = (node.settings && node.settings.gap != null && node.settings.gap !== "")
        ? node.settings.gap
        : "0";
      parts.push("gap:" + rootGap);
      // Default full — matches Elementor full-width page; wide/content are opt-in.
      // Only settings.content_width (not styles.max_width) controls this.
      var cw = (node.settings && node.settings.content_width) || "full";
      var maxw = (node.settings && node.settings.max_width) || "1200px";
      if (maxw && !/%|px|rem|em|vw|vh|ch$/i.test(String(maxw)) && /^\d+(\.\d+)?$/.test(String(maxw))) {
        maxw = String(maxw) + "px";
      }
      if (cw === "content") {
        parts.push("max-width:var(--wp--style--global--content-size,650px)");
        parts.push("margin-left:auto");
        parts.push("margin-right:auto");
      } else if (cw === "wide") {
        parts.push("max-width:" + maxw);
        parts.push("--ab-root-max:" + maxw);
        parts.push("margin-left:auto");
        parts.push("margin-right:auto");
      } else {
        // full / 100%
        parts.push("max-width:none");
        parts.push("width:100%");
        parts.push("margin-left:0");
        parts.push("margin-right:0");
      }
    }

    if (node.type === "container") {
      var dir = node.settings && node.settings.direction === "row" ? "row" : "column";
      var gap = (node.settings && node.settings.gap) || "16px";
      var width = (node.settings && node.settings.width) || s.width || "100%";
      var span = resolveColumnSpan(node);
      parts.push("display:flex");
      parts.push("flex-direction:" + dir);
      parts.push("gap:" + gap);
      parts.push("box-sizing:border-box");
      parts.push("min-width:0");
      // Alignment of children (settings on the container)
      var just =
        (node.settings && node.settings.justify_content) ||
        (dir === "row" ? "flex-start" : "flex-start");
      var aitems =
        (node.settings && node.settings.align_items) ||
        (dir === "row" ? "flex-start" : "stretch");
      parts.push("justify-content:" + just);
      parts.push("align-items:" + aitems);
      // CSS vars so front.css can respect user align without fighting
      parts.push("--ab-row-justify:" + just);
      parts.push("--ab-row-align:" + aitems);
      // Horizontal rows must hug content height — never stretch into a tall abyss.
      if (dir === "row") {
        parts.push("flex-wrap:wrap");
        parts.push("height:auto");
        parts.push("min-height:0");
      }

      var parent = node.parentId ? getNode(node.parentId) : null;
      var parentGrid = parent && parent.type === "section" && parent.settings && parent.settings.direction === "grid";
      var parentRow =
        parent &&
        ((parent.type === "container" && parent.settings && parent.settings.direction === "row") ||
          (parent.type === "section" && parent.settings && parent.settings.direction === "row"));
      if (parentGrid) {
        // 12-col grid: span drives width (75% → span 9, 25% → span 3).
        parts.push("grid-column:span " + span + " !important");
        parts.push("width:100%");
        parts.push("max-width:100%");
        parts.push("min-width:0");
        // Stretch with siblings so cards share a row height (CTA buttons align).
        parts.push("align-self:stretch");
        if (!(node.children && node.children.length)) {
          parts.push("min-height:80px");
        }
      } else if (parentRow) {
        // Sibling inside a horizontal parent.
        // Use flex-grow from col_span + basis 0 so gap does NOT push the last
        // column to the next line (width:25% * 4 + gaps > 100%).
        var rowSpan = resolveColumnSpan(node);
        if (width === "auto") {
          parts.push("width:auto");
          parts.push("flex:0 1 auto");
          parts.push("max-width:100%");
        } else {
          parts.push("flex:" + rowSpan + " 1 0%");
          parts.push("min-width:0");
          parts.push("width:auto");
          parts.push("max-width:100%");
        }
        parts.push("height:auto");
        parts.push("min-height:0");
      } else {
        // Parent is a column stack: width only — NEVER flex:0 0 100%
        // (that would interpret 100% as HEIGHT on the main axis → vertical abyss).
        parts.push("width:" + width);
        parts.push("max-width:" + width);
        parts.push("flex:0 0 auto");
        parts.push("height:auto");
        parts.push("min-height:0");
        parts.push("align-self:stretch");
      }
    }

    if (node.type === "section") {
      parts.push("box-sizing:border-box");
      parts.push("width:100%");
      // Gutters = .ab-page-body / .ab-content-wrap (same on front). Default = full width.
      var secLayout = (node.settings && node.settings.layout) || "full";
      if (secLayout === "boxed") {
        parts.push("max-width:var(--ab-content-max,1140px)");
        parts.push("width:var(--ab-content-width,90%)");
        // Only force horizontal centering when the user has not set a custom margin.
        if (!s.margin) {
          parts.push("margin-left:auto");
          parts.push("margin-right:auto");
        }
      } else {
        parts.push("max-width:none");
      }
      // Prefer explicit direction; if columns>1 without direction, treat as grid.
      var sdir = (node.settings && node.settings.direction) || "";
      if (!sdir) {
        var colN = parseInt(node.settings && node.settings.columns, 10) || 0;
        sdir = colN > 1 ? "grid" : "column";
      }
      var sgap = (node.settings && node.settings.gap) || "16px";
      // Align/justify from style bag (header: center + space-between).
      var secAlign = s.align_items || "";
      var secJust = s.justify_content || "";
      if (sdir === "grid") {
        // Always 12 tracks so col_span % widths work (Elementor-like).
        parts.push("display:grid");
        parts.push("grid-template-columns:repeat(12,minmax(0,1fr))");
        parts.push("gap:" + sgap);
        parts.push("align-items:" + (secAlign || "stretch"));
        if (secJust) parts.push("justify-content:" + secJust);
      } else if (sdir === "row") {
        parts.push("display:flex");
        parts.push("flex-direction:row");
        parts.push("flex-wrap:wrap");
        parts.push("align-items:" + (secAlign || "center"));
        parts.push("justify-content:" + (secJust || "space-between"));
        parts.push("gap:" + sgap);
      } else {
        // column
        parts.push("display:flex");
        parts.push("flex-direction:column");
        parts.push("align-items:" + (secAlign || "stretch"));
        parts.push("gap:" + sgap);
      }
    }

    // Leaf widgets direct under a grid section → full row (avoid 1/12 squash).
    if (
      node.type !== "section" &&
      node.type !== "container" &&
      node.type !== "root" &&
      node.parentId
    ) {
      var pGrid = getNode(node.parentId);
      if (
        pGrid &&
        pGrid.type === "section" &&
        pGrid.settings &&
        pGrid.settings.direction === "grid"
      ) {
        parts.push("grid-column:1 / -1");
        parts.push("max-width:100%");
      }
      if (
        pGrid &&
        pGrid.type === "section" &&
        pGrid.settings &&
        pGrid.settings.direction === "row"
      ) {
        // Header-like row: don't force 100% width from leaf default.
        if (node.type === "nav_menu") {
          parts.push("flex:1 1 auto");
          parts.push("min-width:0");
          parts.push("width:auto");
          parts.push("max-width:100%");
        } else {
          parts.push("flex:0 1 auto");
          parts.push("width:auto");
          parts.push("max-width:100%");
        }
      }
    }
    return parts.length ? ' style="' + parts.join(";") + '"' : "";
  }

  /** % width → col_span 1–12 (75% → 9). */
  function widthPctToSpan(width) {
    var raw = String(width == null ? "" : width).trim();
    var m = raw.match(/([\d.]+)/);
    if (!m) return 12;
    var n = parseFloat(m[1]);
    if (isNaN(n) || n <= 0) return 12;
    // Bare number 1–12 treated as span already.
    if (!/%/.test(raw) && n >= 1 && n <= 12 && Math.abs(n - Math.round(n)) < 0.001) {
      return Math.max(1, Math.min(12, Math.round(n)));
    }
    if (n > 100) n = 100;
    return Math.max(1, Math.min(12, Math.round((n / 100) * 12)));
  }

  function spanToWidthPct(span) {
    span = Math.max(1, Math.min(12, parseInt(span, 10) || 12));
    return Math.round((span / 12) * 100) + "%";
  }

  /** Resolve span from col_span or width % on a column container. */
  function resolveColumnSpan(node) {
    if (!node || !node.settings) return 12;
    var st = node.settings;
    if (st.col_span != null && st.col_span !== "") {
      var sp = parseInt(st.col_span, 10);
      if (!isNaN(sp) && sp >= 1) return Math.max(1, Math.min(12, sp));
    }
    if (st.width) return widthPctToSpan(st.width);
    return 12;
  }

  /**
   * After changing one column width/span, keep siblings summing to 12.
   */
  function rebalanceColumnSpans(secId, fixedId) {
    var sec = getNode(secId);
    if (!sec || sec.type !== "section") return;
    var colIds = (sec.children || []).filter(function (cid) {
      var c = getNode(cid);
      return c && c.type === "container";
    });
    if (!colIds.length) return;
    var fixed = getNode(fixedId);
    if (!fixed) return;
    var fixedSpan = resolveColumnSpan(fixed);
    fixed.settings = fixed.settings || {};
    fixed.settings.col_span = fixedSpan;
    fixed.settings.width = spanToWidthPct(fixedSpan);

    var others = colIds.filter(function (id) {
      return id !== fixedId;
    });
    if (!others.length) return;
    var remain = 12 - fixedSpan;
    if (remain < others.length) {
      fixedSpan = 12 - others.length;
      fixed.settings.col_span = fixedSpan;
      fixed.settings.width = spanToWidthPct(fixedSpan);
      remain = others.length;
    }
    var each = Math.floor(remain / others.length);
    var extra = remain - each * others.length;
    others.forEach(function (id, i) {
      var n = getNode(id);
      if (!n) return;
      var sp = Math.max(1, each + (i < extra ? 1 : 0));
      n.settings = n.settings || {};
      n.settings.col_span = sp;
      n.settings.width = spanToWidthPct(sp);
    });
  }

  /** When setting column width outside a grid section, use flex row. */
  function ensureParentRowForColumns(nodeId) {
    var node = getNode(nodeId);
    if (!node || !node.parentId) return;
    var parent = getNode(node.parentId);
    if (!parent) return;
    // Grid sections keep 12-col grid — width maps to col_span, not flex row.
    if (parent.type === "section" && parent.settings && parent.settings.direction === "grid") {
      return;
    }
    if (parent.type === "section" || parent.type === "container") {
      parent.settings = parent.settings || {};
      parent.settings.direction = "row";
      if (!parent.settings.gap) parent.settings.gap = "16px";
    }
  }

  /**
   * Create an empty column container under a section (Elementor-like).
   * span/width set by caller (equal split) or defaults to full.
   */
  function createColumnContainer(secId, index, span) {
    span = Math.max(1, Math.min(12, parseInt(span, 10) || 12));
    var cid = uid();
    state.document.nodes[cid] = {
      id: cid,
      type: "container",
      parentId: secId,
      children: [],
      settings: {
        display: "flex",
        direction: "column",
        gap: "16px",
        width: spanToWidthPct(span),
        col_span: span,
        display_name: "Coluna " + (index || 1),
      },
      styles: { desktop: {}, tablet: {}, mobile: {} },
      attributes: {},
      classes: [],
      locked: false,
      hidden: false,
    };
    return cid;
  }

  /**
   * Sync section to N equal columns (Elementor behavior):
   * - direction = grid, columns = N
   * - existing content → column 1
   * - remaining columns empty with dashed drop zone
   * - removing columns merges content into the previous column
   */
  function syncSectionColumns(secId, n) {
    var sec = getNode(secId);
    if (!sec || sec.type !== "section") return;
    n = Math.max(1, Math.min(12, parseInt(n, 10) || 1));
    sec.settings = sec.settings || {};
    sec.settings.direction = "grid";
    sec.settings.columns = n;
    if (!sec.settings.gap) sec.settings.gap = "16px";
    if (!sec.children) sec.children = [];

    // Partition direct children: column containers vs orphan widgets.
    var colIds = [];
    var orphanIds = [];
    sec.children.forEach(function (cid) {
      var ch = getNode(cid);
      if (!ch) return;
      if (ch.type === "container") colIds.push(cid);
      else orphanIds.push(cid);
    });

    // Wrap orphan widgets into the first column (or create it).
    if (orphanIds.length) {
      var wrapId = colIds[0];
      if (!wrapId) {
        wrapId = createColumnContainer(secId, 1);
        colIds.unshift(wrapId);
      }
      var wrap = getNode(wrapId);
      if (wrap) {
        if (!wrap.children) wrap.children = [];
        orphanIds.forEach(function (oid) {
          var o = getNode(oid);
          if (!o) return;
          // Detach from section list first.
          o.parentId = wrapId;
          if (wrap.children.indexOf(oid) < 0) wrap.children.push(oid);
        });
      }
      // Section direct children = only containers (plus any still-orphaned).
      sec.children = colIds.slice();
    }

    // Refresh column list from section children (only containers).
    colIds = (sec.children || []).filter(function (cid) {
      var ch = getNode(cid);
      return ch && ch.type === "container";
    });
    // Keep non-containers if any slipped through (shouldn't after wrap).
    var leftover = (sec.children || []).filter(function (cid) {
      var ch = getNode(cid);
      return ch && ch.type !== "container";
    });
    if (leftover.length && colIds[0]) {
      var w2 = getNode(colIds[0]);
      leftover.forEach(function (oid) {
        var o = getNode(oid);
        if (!o || !w2) return;
        o.parentId = colIds[0];
        if (!w2.children) w2.children = [];
        if (w2.children.indexOf(oid) < 0) w2.children.push(oid);
      });
    }

    // Equal split on 12-col grid (2 → 6+6, 3 → 4+4+4, 5 → 3+3+2+2+2 …).
    var baseSpan = Math.floor(12 / n);
    var spanRem = 12 - baseSpan * n;
    function spanForIndex(i) {
      return Math.max(1, baseSpan + (i < spanRem ? 1 : 0));
    }

    // Add empty columns up to N.
    while (colIds.length < n) {
      var idxNew = colIds.length;
      var newId = createColumnContainer(secId, idxNew + 1, spanForIndex(idxNew));
      colIds.push(newId);
    }

    // Remove extra columns: move their children into the last kept column.
    while (colIds.length > n) {
      var removeId = colIds.pop();
      var rem = getNode(removeId);
      var targetId = colIds[colIds.length - 1] || null;
      if (rem && targetId) {
        var target = getNode(targetId);
        if (target) {
          if (!target.children) target.children = [];
          (rem.children || []).slice().forEach(function (kid) {
            var kn = getNode(kid);
            if (!kn) return;
            kn.parentId = targetId;
            if (target.children.indexOf(kid) < 0) target.children.push(kid);
          });
        }
      }
      if (rem) delete state.document.nodes[removeId];
    }

    // Final order + equal spans (reset widths when changing column count).
    sec.children = colIds.slice();
    colIds.forEach(function (cid, i) {
      var c = getNode(cid);
      if (!c) return;
      c.settings = c.settings || {};
      c.parentId = secId;
      var sp = spanForIndex(i);
      c.settings.col_span = sp;
      c.settings.width = spanToWidthPct(sp);
      c.settings.direction = c.settings.direction || "column";
      c.settings.gap = c.settings.gap || "16px";
      var dn = (c.settings.display_name || "") + "";
      if (!dn || /^Coluna\s+\d+$/i.test(dn)) {
        c.settings.display_name = "Coluna " + (i + 1);
      }
    });
  }

  function addColumnsPreset(ratios) {
    // ratios e.g. [50, 50] or [33, 33, 34] → equal visual columns (N tracks).
    execute(function () {
      var rootId = state.document.rootId;
      var secId = uid();
      var n = ratios.length || 1;
      var section = {
        id: secId,
        type: "section",
        parentId: rootId,
        children: [],
        settings: {
          layout: "full",
          direction: "grid",
          columns: n,
          gap: "16px",
        },
        styles: { desktop: {}, tablet: {}, mobile: {} },
        attributes: {},
        classes: [],
        locked: false,
        hidden: false,
      };
      state.document.nodes[secId] = section;
      var root = getNode(rootId);
      if (!root.children) root.children = [];
      root.children.push(secId);
      ratios.forEach(function (r, idx) {
        var cid = uid();
        state.document.nodes[cid] = {
          id: cid,
          type: "container",
          parentId: secId,
          children: [],
          settings: {
            display: "flex",
            direction: "column",
            gap: "16px",
            width: "100%",
            col_span: 1,
            display_name: "Coluna " + (idx + 1),
          },
          styles: { desktop: {}, tablet: {}, mobile: {} },
          attributes: {},
          classes: [],
          locked: false,
          hidden: false,
        };
        section.children.push(cid);
      });
      state.selectedId = section.children[0];
    });
  }

  function nodeDisplayName(node) {
    if (!node) return "";
    var custom = node.settings && node.settings.display_name;
    if (custom && String(custom).trim()) return String(custom).trim();
    return TYPE_LABELS[node.type] || node.type;
  }

  /** Sibling index within parent.children (-1 if none). */
  function siblingIndex(id) {
    var node = getNode(id);
    if (!node || !node.parentId) return -1;
    var parent = getNode(node.parentId);
    if (!parent || !parent.children) return -1;
    return parent.children.indexOf(id);
  }

  /**
   * Move element one step up/down among siblings (document order / stacking).
   * @param {string} id
   * @param {"up"|"down"} dir
   * @returns {boolean}
   */
  function moveNodeSibling(id, dir) {
    var node = getNode(id);
    if (!node || node.type === "root" || !node.parentId) return false;
    var parent = getNode(node.parentId);
    if (!parent || !parent.children) return false;
    var kids = parent.children;
    var idx = kids.indexOf(id);
    if (idx < 0) return false;
    var newIdx = dir === "up" ? idx - 1 : idx + 1;
    if (newIdx < 0 || newIdx >= kids.length) return false;
    kids.splice(idx, 1);
    kids.splice(newIdx, 0, id);
    return true;
  }

  function chromeHtml(node) {
    if (state.selectedId !== node.id || node.type === "root") return "";
    var label = nodeDisplayName(node);
    var sIdx = siblingIndex(node.id);
    var parent = node.parentId ? getNode(node.parentId) : null;
    var total = parent && parent.children ? parent.children.length : 0;
    var canUp = sIdx > 0;
    var canDown = sIdx >= 0 && sIdx < total - 1;
    return (
      '<div class="ab-overlay-chrome" data-chrome="' +
      esc(node.id) +
      '">' +
      '<span class="ab-overlay-label"><span class="ab-drag-handle" data-drag="' +
      esc(node.id) +
      '" title="Arrastar">⠿</span>' +
      esc(label) +
      "</span>" +
      '<span class="ab-overlay-actions">' +
      '<button type="button" class="ab-chrome-act ab-chrome-act--order' +
      (canUp ? "" : " is-disabled") +
      '" data-act="move-up" data-id="' +
      esc(node.id) +
      '" title="Subir posição"' +
      (canUp ? "" : " disabled aria-disabled=\"true\"") +
      ">▲</button>" +
      '<button type="button" class="ab-chrome-act ab-chrome-act--order' +
      (canDown ? "" : " is-disabled") +
      '" data-act="move-down" data-id="' +
      esc(node.id) +
      '" title="Descer posição"' +
      (canDown ? "" : " disabled aria-disabled=\"true\"") +
      ">▼</button>" +
      '<button type="button" class="ab-chrome-act ab-chrome-act--copy-style" data-act="copy-style" data-id="' +
      esc(node.id) +
      '" title="Copiar estilo (cores, fontes, fundo, borda)">Estilo</button>' +
      (state.styleClipboard && Object.keys(state.styleClipboard).length
        ? '<button type="button" class="ab-chrome-act ab-chrome-act--paste-style" data-act="paste-style" data-id="' +
          esc(node.id) +
          '" title="Colar estilo copiado">Colar</button>'
        : "") +
      '<button type="button" class="ab-chrome-act ab-chrome-act--dup" data-act="dup" data-id="' +
      esc(node.id) +
      '" title="Duplicar elemento">Duplicar</button>' +
      '<button type="button" class="ab-chrome-act ab-chrome-act--del" data-act="del" data-id="' +
      esc(node.id) +
      '" title="Remover">×</button>' +
      "</span></div>" +
      resizeHandlesHtml(node) +
      ""
    );
  }

  /**
   * Resize handles for a node.
   * Text/heading/button: only width (E/W) — height follows content.
   * Spacer/image/media: full box. Sections/containers: full box.
   */
  function resizeHandlesHtml(node) {
    if (!node || node.type === "root") return "";
    var id = esc(node.id);
    var widthOnly = {
      text: 1,
      heading: 1,
      button: 1,
      icon: 1,
      divider: 1,
      star_rating: 1,
      counter: 1,
      social_icons: 1,
      social_bar: 1,
      dynamic: 1,
    };
    var dirs = widthOnly[node.type]
      ? ["e", "w"]
      : ["n", "s", "e", "w", "ne", "nw", "se", "sw"];
    return dirs
      .map(function (d) {
        return (
          '<div class="ab-resize-handle ' +
          d +
          '" data-resize="' +
          d +
          '" data-id="' +
          id +
          '"></div>'
        );
      })
      .join("");
  }

  /* ── document ops ── */
  /**
   * @param {string} type
   * @param {string} parentId
   * @param {number|object} [indexOrExtra] index or settings defaults override
   * @param {number} [maybeIndex]
   */
  function addNode(type, parentId, indexOrExtra, maybeIndex) {
    var extra = {};
    var index = indexOrExtra;
    if (indexOrExtra && typeof indexOrExtra === "object" && !Array.isArray(indexOrExtra)) {
      extra = indexOrExtra;
      index = maybeIndex;
    }
    var meta =
      CATALOG.find(function (c) {
        if (c.type !== type) return false;
        if (extra.source && c.defaults && c.defaults.source) {
          return c.defaults.source === extra.source;
        }
        if (extra.provider && c.defaults && c.defaults.provider) {
          return c.defaults.provider === extra.provider;
        }
        return true;
      }) ||
      CATALOG.find(function (c) {
        return c.type === type;
      });
    if (!meta) return;
    var parent = getNode(parentId);
    if (!parent) return;

    if (type === "section" && parent.type !== "root") {
      parentId = state.document.rootId;
      parent = getNode(parentId);
    }
    if (type === "container" && parent.type === "root") {
      var root = getNode(state.document.rootId);
      if (root && root.children && root.children[0]) {
        parentId = root.children[0];
        parent = getNode(parentId);
      }
    }

    var id = uid();
    var node = {
      id: id,
      type: type,
      parentId: parentId,
      children: [],
      settings: Object.assign({}, meta.defaults || {}, extra || {}),
      styles: {},
      attributes: {},
      classes: [],
      locked: false,
      hidden: false,
    };

    if (type === "section") {
      var cid = uid();
      node.children = [cid];
      state.document.nodes[cid] = {
        id: cid,
        type: "container",
        parentId: id,
        children: [],
        settings: { display: "flex", direction: "column", gap: "16px", width: "100%" },
        styles: {},
        attributes: {},
        classes: [],
        locked: false,
        hidden: false,
      };
    }

    state.document.nodes[id] = node;
    parent = getNode(parentId);
    if (!parent.children) parent.children = [];
    if (typeof index !== "number" || index < 0 || index > parent.children.length) {
      parent.children.push(id);
    } else {
      parent.children.splice(index, 0, id);
    }
    state.selectedId = type === "section" && node.children[0] ? node.children[0] : id;
  }

  function removeNode(id) {
    var node = getNode(id);
    if (!node || node.type === "root" || node.locked) return;
    function wipe(nid) {
      var n = getNode(nid);
      if (!n) return;
      (n.children || []).slice().forEach(wipe);
      delete state.document.nodes[nid];
    }
    if (node.parentId) {
      var p = getNode(node.parentId);
      if (p && p.children) {
        p.children = p.children.filter(function (c) {
          return c !== id;
        });
      }
    }
    wipe(id);
    state.selectedId = null;
  }

  function updateSettings(id, key, value) {
    var node = getNode(id);
    if (!node) return;
    node.settings = node.settings || {};
    node.settings[key] = value;
  }

  function updateStyle(id, key, value) {
    if (key === "font_family" && value) {
      value = normalizeFontFamilyCss(value) || value;
      ensureGoogleFont(value);
    }
    var node = getNode(id);
    if (!node) return;
    var d = ensureStyles(node, state.breakpoint);
    // max_width "none" / empty → remove cap so 100% can fill the parent.
    if (key === "max_width" && (value === "" || value == null || value === "none")) {
      delete d.max_width;
      return;
    }
    if (value === "" || value == null) {
      delete d[key];
    } else {
      d[key] = value;
    }
    // Choosing full width clears an accidental content-width cap (1140px etc.).
    if (key === "width" && (value === "100%" || value === "100")) {
      delete d.max_width;
    }
    if (key === "width" && node.type === "container") {
      node.settings = node.settings || {};
      node.settings.width = value || "100%";
      var par = node.parentId ? getNode(node.parentId) : null;
      if (par && par.type === "section" && par.settings && par.settings.direction === "grid") {
        // Grid columns: % width → col_span on 12-col system.
        var sp = widthPctToSpan(value || "100%");
        node.settings.col_span = sp;
        node.settings.width = spanToWidthPct(sp);
        rebalanceColumnSpans(par.id, id);
      } else if (value && value !== "100%") {
        ensureParentRowForColumns(id);
      }
    }
  }

  /** Per-gallery index map (editor canvas). */
  var editorGalIdx = {};
  var editorCarIdx = {};

  function editorGalKey(el) {
    var n = el && el.closest && el.closest("[data-node-id]");
    return (n && n.getAttribute("data-node-id")) || "gal";
  }

  function editorGalleryGo(el, n) {
    if (!el) return;
    var track = el.querySelector(".ab-gallery__track");
    var items = Array.prototype.slice.call(el.querySelectorAll(".ab-gallery__item"));
    if (!track || items.length < 2) return;
    var per = parseInt(el.getAttribute("data-per"), 10) || 1;
    var max = Math.max(0, items.length - per);
    var key = editorGalKey(el);
    var i = typeof n === "number" ? n : editorGalIdx[key] || 0;
    i = ((i % (max + 1)) + (max + 1)) % (max + 1);
    editorGalIdx[key] = i;
    var gap = parseFloat(getComputedStyle(el).getPropertyValue("--ab-gal-gap")) || 8;
    var w = items[0].offsetWidth || items[0].getBoundingClientRect().width || el.clientWidth || 0;
    if (w < 2) w = el.clientWidth || 300;
    w = w + gap;
    track.style.transition = "transform .4s ease";
    track.style.transform = "translate3d(-" + i * w + "px,0,0)";
    items.forEach(function (it, idx) {
      it.classList.toggle("is-active", idx === i);
    });
  }

  function editorCarouselGo(el, n) {
    if (!el) return;
    var slides = Array.prototype.slice.call(el.querySelectorAll(".ab-carousel__slide"));
    if (slides.length < 2) return;
    var key = el.closest("[data-node-id]")
      ? el.closest("[data-node-id]").getAttribute("data-node-id")
      : el.className;
    var layout =
      el.getAttribute("data-layout") ||
      (el.classList.contains("ab-carousel--multi") ? "multi" : "single");
    var per = parseInt(el.getAttribute("data-per"), 10) || 1;
    var max = layout === "multi" ? Math.max(0, slides.length - per) : slides.length - 1;
    var i = typeof n === "number" ? n : editorCarIdx[key] || 0;
    i = ((i % (max + 1)) + (max + 1)) % (max + 1);
    editorCarIdx[key] = i;
    var track = el.querySelector(".ab-carousel__track");
    if (layout === "multi" && track) {
      var gap = parseFloat(getComputedStyle(el).getPropertyValue("--ab-carousel-gap")) || 8;
      var sw = slides[0].offsetWidth || slides[0].getBoundingClientRect().width || 0;
      track.style.transform = "translate3d(-" + i * (sw + gap) + "px,0,0)";
    } else {
      slides.forEach(function (s, idx) {
        s.classList.toggle("is-active", idx === i);
      });
    }
  }

  /**
   * Gallery + carousel interactions in the editor canvas.
   * Capture-phase listener so nav clicks beat selection/drag handlers.
   */
  function initGallerySliders(root) {
    root = root || document.getElementById("ab-page-body") || document;
    if (!window._abEditorSliderClick) {
      window._abEditorSliderClick = true;
      document.addEventListener(
        "click",
        function (e) {
          var canvas = document.getElementById("ab-canvas");
          if (!canvas || !canvas.contains(e.target)) return;

          var galPrev = e.target.closest(
            "[data-ab-gal-prev], .ab-gallery__nav--prev"
          );
          var galNext = e.target.closest(
            "[data-ab-gal-next], .ab-gallery__nav--next"
          );
          if (galPrev || galNext) {
            var gal = (galPrev || galNext).closest(
              "[data-ab-gallery-slider], .ab-gallery--slider"
            );
            if (gal) {
              e.preventDefault();
              e.stopPropagation();
              var gkey = editorGalKey(gal);
              var gi = editorGalIdx[gkey] || 0;
              editorGalleryGo(gal, gi + (galNext ? 1 : -1));
              return;
            }
          }

          var carPrev = e.target.closest(
            "[data-ab-car-prev], .ab-carousel__nav--prev"
          );
          var carNext = e.target.closest(
            "[data-ab-car-next], .ab-carousel__nav--next"
          );
          if (carPrev || carNext) {
            var car = (carPrev || carNext).closest(
              "[data-ab-carousel], .ab-carousel"
            );
            if (car) {
              e.preventDefault();
              e.stopPropagation();
              var ckey =
                (car.closest("[data-node-id]") &&
                  car.closest("[data-node-id]").getAttribute("data-node-id")) ||
                "c";
              var ci = editorCarIdx[ckey] || 0;
              editorCarouselGo(car, ci + (carNext ? 1 : -1));
              return;
            }
          }
        },
        true
      );
    }

    if (!root || !root.querySelectorAll) return;
    // Reset positions after paint (indices kept in editorGalIdx).
    root.querySelectorAll("[data-ab-gallery-slider], .ab-gallery--slider").forEach(function (el) {
      if (!el.getAttribute("data-ab-gallery-slider")) {
        el.setAttribute("data-ab-gallery-slider", "1");
      }
      var key =
        (el.closest("[data-node-id]") &&
          el.closest("[data-node-id]").getAttribute("data-node-id")) ||
        "g";
      editorGalleryGo(el, editorGalIdx[key] || 0);
      // Light autoplay in editor canvas so users can preview (cleared on re-paint).
      if (el._abEditorGalTimer) {
        clearInterval(el._abEditorGalTimer);
        el._abEditorGalTimer = null;
      }
      var ap = el.getAttribute("data-autoplay");
      if (ap === null || ap === "") ap = "1";
      if (ap !== "0" && ap !== "false") {
        var ms = parseInt(el.getAttribute("data-autoplay-ms") || "4500", 10) || 4500;
        if (ms < 1500) ms = 4500;
        el._abEditorGalTimer = setInterval(function () {
          if (!document.body.contains(el)) {
            clearInterval(el._abEditorGalTimer);
            el._abEditorGalTimer = null;
            return;
          }
          var k =
            (el.closest("[data-node-id]") &&
              el.closest("[data-node-id]").getAttribute("data-node-id")) ||
            "g";
          var cur = editorGalIdx[k] || 0;
          editorGalleryGo(el, cur + 1);
        }, ms);
      }
    });
    root.querySelectorAll("[data-ab-carousel], .ab-carousel").forEach(function (el) {
      var key =
        (el.closest("[data-node-id]") &&
          el.closest("[data-node-id]").getAttribute("data-node-id")) ||
        "c";
      editorCarouselGo(el, editorCarIdx[key] || 0);
    });
  }

  /** Animate counters in the editor canvas (count-up). */
  function runCounterAnimations(scope) {
    var root = scope || document.getElementById("ab-page-body") || document;
    var nodes = root.querySelectorAll
      ? root.querySelectorAll(".ab-counter__number[data-target]")
      : [];
    if (scope && scope.querySelectorAll && scope.classList && scope.classList.contains("ab-counter")) {
      nodes = scope.querySelectorAll(".ab-counter__number[data-target]");
    } else if (scope && scope.matches && scope.matches(".ab-counter__number[data-target]")) {
      nodes = [scope];
    }
    Array.prototype.forEach.call(nodes, function (el) {
      // Reset so re-paint re-animates.
      el.removeAttribute("data-ab-counted");
      var wrap = el.closest("[data-ab-counter]");
      if (wrap && wrap.getAttribute("data-animate") === "0") {
        el.textContent =
          (el.getAttribute("data-prefix") || "") +
          (el.getAttribute("data-target") || "0") +
          (el.getAttribute("data-suffix") || "");
        return;
      }
      var targetRaw = el.getAttribute("data-target") || "0";
      var target = parseFloat(String(targetRaw).replace(",", ".")) || 0;
      var prefix = el.getAttribute("data-prefix") || "";
      var suffix = el.getAttribute("data-suffix") || "";
      var duration = parseInt(el.getAttribute("data-duration"), 10) || 1600;
      var start = null;
      function frame(ts) {
        if (start == null) start = ts;
        var p = Math.min(1, (ts - start) / duration);
        var eased = 1 - Math.pow(1 - p, 3);
        var cur = target * eased;
        var display = /^\d+$/.test(String(targetRaw).trim())
          ? String(Math.round(cur))
          : cur.toFixed(1);
        el.textContent = prefix + display + suffix;
        if (p < 1) requestAnimationFrame(frame);
        else el.textContent = prefix + targetRaw + suffix;
      }
      el.textContent = prefix + "0" + suffix;
      requestAnimationFrame(frame);
    });
  }

  /** Fill loop_grid previews with real posts from WP REST. */
  function hydrateLoopGrids() {
    var nodes = document.querySelectorAll("[data-ab-loop-preview]");
    if (!nodes.length) return;
    nodes.forEach(function (wrap) {
      var id = wrap.getAttribute("data-node-loop");
      var node = getNode(id);
      if (!node) return;
      var s = node.settings || {};
      var ppp = Math.min(12, Math.max(1, parseInt(s.posts_per_page, 10) || 6));
      var type = s.query_source || "post";
      if (type === "current_query") type = "post";
      if (type === "product" || s.template_type === "products") type = "product";
      var endpoint =
        type === "page"
          ? "/wp/v2/pages"
          : type === "product"
            ? "/wp/v2/product"
            : "/wp/v2/posts";
      var base = (CFG.restUrl || "").replace(/aura-builder\/v1\/?$/, "").replace(/\/$/, "");
      // restUrl is like .../wp-json/aura-builder/v1 → wp-json root
      if (!base || base.indexOf("wp-json") < 0) {
        base = window.location.origin + "/wp-json";
      } else {
        var idx = base.indexOf("/wp-json");
        base = base.slice(0, idx + "/wp-json".length);
      }
      var orderby = s.orderby || "date";
      if (orderby === "rand") orderby = "date";
      var qs =
        "?per_page=" +
        ppp +
        "&_embed=1&orderby=" +
        encodeURIComponent(orderby) +
        "&order=" +
        encodeURIComponent((s.order || "desc").toLowerCase());
      if (s.category && type === "post") {
        var catVal = String(s.category).trim();
        if (/^\d+$/.test(catVal)) {
          qs += "&categories=" + encodeURIComponent(catVal);
        } else if (Array.isArray(CFG.categories)) {
          // resolve slug → id
          CFG.categories.forEach(function (c) {
            if (c.slug === catVal || String(c.id) === catVal) {
              qs += "&categories=" + encodeURIComponent(String(c.id));
            }
          });
        }
      }
      var list = wrap.querySelector("[data-loop-list]");
      if (!list) return;
      fetch(base + endpoint + qs, { credentials: "same-origin" })
        .then(function (r) {
          return r.ok ? r.json() : [];
        })
        .then(function (posts) {
          if (!Array.isArray(posts) || !posts.length) {
            list.innerHTML =
              '<div class="ab-loop-empty">Nenhum post público encontrado. Publique posts ou mude a consulta.</div>';
            return;
          }
          var html = "";
          var imgPos = s.image_position || "top";
          var titlePos = s.title_position || "outside";
          var cardExtra =
            " ab-loop-card--img-" +
            imgPos +
            (titlePos === "over_image" ? " ab-loop-card--title-over" : "") +
            (s.card_shadow === "1" ? " has-shadow" : "") +
            (s.card_border !== "0" ? " has-border" : "");
          posts.forEach(function (p, pIdx) {
            var title = (p.title && p.title.rendered) || "Sem título";
            title = decodeEntities(title.replace(/<[^>]+>/g, ""));
            var link = p.link || "#";
            var excerpt = (p.excerpt && p.excerpt.rendered) || "";
            var tmp = document.createElement("div");
            tmp.innerHTML = excerpt;
            excerpt = decodeEntities((tmp.textContent || "").trim()).slice(0, 140);
            var img = "";
            try {
              var emb = p._embedded && p._embedded["wp:featuredmedia"] && p._embedded["wp:featuredmedia"][0];
              if (emb) {
                img =
                  (emb.media_details &&
                    emb.media_details.sizes &&
                    emb.media_details.sizes.large &&
                    emb.media_details.sizes.large.source_url) ||
                  emb.source_url ||
                  "";
              }
            } catch (e2) {}
            var date = p.date ? formatWpDate(p.date) : "";
            var author = "";
            try {
              author =
                (p._embedded &&
                  p._embedded.author &&
                  p._embedded.author[0] &&
                  p._embedded.author[0].name) ||
                "";
            } catch (e3) {}
            var titleHtml =
              s.show_title !== "0"
                ? '<h3 class="ab-loop-card__title"><a href="' +
                  esc(link) +
                  '" onclick="return false">' +
                  esc(title) +
                  "</a></h3>"
                : "";
            var mediaHtml = "";
            if (s.show_image !== "0") {
              var ratioRaw = s.image_ratio || "16/9";
              var ratioCss =
                ratioRaw === "auto"
                  ? "auto"
                  : String(ratioRaw).indexOf("/") >= 0
                    ? String(ratioRaw).replace(/\s*\/\s*/, " / ")
                    : "16 / 9";
              var fitCss = s.image_object_fit || "cover";
              var posCss = s.image_object_position || "50% 50%";
              mediaHtml =
                '<div class="ab-loop-card__media-wrap" style="--ab-loop-ratio:' +
                esc(ratioCss) +
                ";--ab-loop-fit:" +
                esc(fitCss) +
                ";--ab-loop-pos:" +
                esc(posCss) +
                '"><a class="ab-loop-card__media" href="' +
                esc(link) +
                '" onclick="return false">' +
                (img
                  ? '<img class="ab-loop-card__img" src="' + esc(img) + '" alt=""/>'
                  : '<div class="ab-loop-card__media-ph"></div>') +
                "</a>";
              if (titlePos === "over_image" && titleHtml) {
                mediaHtml += '<div class="ab-loop-card__overlay">' + titleHtml + "</div>";
              }
              mediaHtml += "</div>";
            }
            var body = '<div class="ab-loop-card__body">';
            var meta = [];
            if (s.show_date !== "0" && date) meta.push("<time>" + esc(date) + "</time>");
            if (s.show_author !== "0" && author)
              meta.push('<span class="ab-loop-card__author">' + esc(author) + "</span>");
            if (meta.length) {
              body +=
                '<div class="ab-loop-card__meta">' +
                meta.join('<span class="ab-loop-card__sep">·</span>') +
                "</div>";
            }
            if (s.show_title !== "0" && titlePos !== "over_image") {
              body += titleHtml;
            }
            if (s.show_excerpt !== "0" && excerpt) {
              body += '<p class="ab-loop-card__excerpt">' + esc(excerpt) + "…</p>";
            }
            if (s.show_read_more !== "0") {
              body +=
                '<a class="ab-loop-card__more" href="' +
                esc(link) +
                '" onclick="return false">' +
                esc(s.read_more_text || "Leia mais") +
                "</a>";
            }
            body += "</div>";
            html +=
              '<article class="ab-loop-card' +
              cardExtra +
              (pIdx === 0 ? " is-active-slide" : "") +
              '" data-slide-index="' +
              pIdx +
              '">';
            html += imgPos === "right" ? body + mediaHtml : mediaHtml + body;
            html += "</article>";
          });
          list.innerHTML = html;
          if (s.as_slider === "1") {
            wrap.classList.add("ab-loop-grid--slider");
            list.classList.add("ab-loop-slider__track");
            // dots
            var oldDots = wrap.querySelector(".ab-loop-slider__dots");
            if (oldDots) oldDots.remove();
            var dots = document.createElement("div");
            dots.className = "ab-loop-slider__dots";
            posts.forEach(function (_, di) {
              var b = document.createElement("button");
              b.type = "button";
              b.className = "ab-loop-slider__dot" + (di === 0 ? " is-active" : "");
              b.setAttribute("data-ab-loop-dot", String(di));
              dots.appendChild(b);
            });
            wrap.appendChild(dots);
          } else {
            wrap.classList.remove("ab-loop-grid--slider");
            list.classList.remove("ab-loop-slider__track");
            var od = wrap.querySelector(".ab-loop-slider__dots");
            if (od) od.remove();
          }
        })
        .catch(function () {
          list.innerHTML =
            '<div class="ab-loop-empty">Não foi possível carregar a prévia. Salve e veja no site.</div>';
        });
    });
  }

  /**
   * Keep media widgets filling the node box ONLY when the user set an explicit
   * height (or during live resize). Never force width:100% on plain images —
   * that blows up logos in flex/grid columns.
   */
  function syncMediaFill(el, node, st) {
    if (!el || !node) return;
    st = st || stylesFor(node, "view") || {};
    var hRaw = st.height || st.min_height || "";
    var h =
      hRaw && String(hRaw) !== "auto"
        ? /^\d+$/.test(String(hRaw))
          ? String(hRaw) + "px"
          : String(hRaw)
        : "";
    var wRaw = st.width || "";
    var w =
      wRaw && String(wRaw) !== "auto"
        ? /^\d+$/.test(String(wRaw))
          ? String(wRaw) + "px"
          : String(wRaw)
        : "";

    var mediaTypes = {
      image: 1,
      gallery: 1,
      carousel: 1,
      image_carousel_pro: 1,
      image_float: 1,
      image_overlay: 1,
      image_box: 1,
      video: 1,
      preview_card: 1,
      embed: 1,
    };

    // Class flags only — never force height/overflow on sections/containers.
    if (h && mediaTypes[node.type]) {
      el.classList.add("ab-has-h");
    } else if (!mediaTypes[node.type] || !h) {
      if (!h) el.classList.remove("ab-has-h");
    }
    if (w && mediaTypes[node.type]) {
      el.classList.add("ab-has-w");
    } else if (!w) {
      el.classList.remove("ab-has-w");
    }

    if (!mediaTypes[node.type]) return;

    el.style.boxSizing = "border-box";
    // Never set overflow:hidden on the node wrapper — clips editor resize handles.
    el.style.overflow = "";

    // ---- image: natural size unless explicit height (must match front StyleEngine) ----
    if (node.type === "image") {
      var wrap = el.querySelector(".ab-image");
      var img = el.querySelector(".ab-image img, img");
      var imgSet = node.settings || {};
      var imgW = imgSet.width || "";
      // Fixed height → default cover (fill box). Respect explicit non-default fit.
      var imgFit = imgSet.object_fit || (h ? "cover" : "contain");
      if (h && (!imgSet.object_fit || imgSet.object_fit === "contain")) {
        imgFit = "cover";
      }
      var imgPos = imgSet.object_position || "center center";
      var imgAlign = imgSet.align || "center";
      el.style.setProperty("--ab-img-pos", imgPos);
      if (!h) {
        // Natural size — same as public page without fixed height.
        if (wrap) {
          wrap.style.width = "";
          wrap.style.maxWidth = "100%";
          wrap.style.height = "";
          wrap.style.display = "";
          wrap.style.overflow = "";
          wrap.style.textAlign = imgAlign;
        }
        if (img) {
          img.style.width = imgW || "auto";
          img.style.maxWidth = "100%";
          img.style.height = "auto";
          img.style.objectFit = imgFit || "contain";
          img.style.objectPosition = imgPos;
          img.style.display = "inline-block";
          img.style.verticalAlign = "middle";
        }
        return;
      }
      // Fixed height: fill the node box (editor ↔ site parity).
      if (wrap) {
        wrap.style.setProperty("width", "100%", "important");
        wrap.style.setProperty("max-width", "100%", "important");
        wrap.style.setProperty("height", "100%", "important");
        wrap.style.setProperty("display", "block", "important");
        wrap.style.overflow = "hidden";
        wrap.style.lineHeight = "0";
        wrap.style.boxSizing = "border-box";
        wrap.style.textAlign = imgAlign;
        wrap.style.setProperty("--ab-img-pos", imgPos);
      }
      if (img) {
        img.style.setProperty("width", "100%", "important");
        img.style.setProperty("max-width", "100%", "important");
        img.style.setProperty("height", "100%", "important");
        img.style.setProperty("object-fit", imgFit || "cover", "important");
        img.style.setProperty("object-position", imgPos, "important");
        img.style.setProperty("display", "block", "important");
        img.style.verticalAlign = "top";
      }
      return;
    }

    if (node.type === "gallery") {
      var gal = el.querySelector(".ab-gallery");
      if (!gal) return;
      gal.style.width = "100%";
      gal.style.maxWidth = "100%";
      gal.style.boxSizing = "border-box";
      gal.style.minWidth = "0";
      gal.style.overflow = "hidden";
      if (h) {
        gal.style.height = "100%";
        gal.style.minHeight = "0";
        gal.style.flex = "1 1 auto";
        if (gal.classList.contains("ab-gallery--grid")) {
          gal.style.alignContent = "stretch";
          gal.style.gridAutoRows = "minmax(0, 1fr)";
        }
        if (gal.classList.contains("ab-gallery--slider")) {
          var track = gal.querySelector(".ab-gallery__track");
          if (track) track.style.height = "100%";
        }
        gal.querySelectorAll(".ab-gallery__item").forEach(function (item) {
          item.style.aspectRatio = "auto";
          item.style.height = "100%";
          item.style.minHeight = "0";
          item.style.overflow = "hidden";
        });
      } else {
        gal.style.height = "";
        gal.style.minHeight = "";
        gal.style.flex = "";
        gal.style.alignContent = "";
        gal.style.gridAutoRows = "";
        gal.querySelectorAll(".ab-gallery__item").forEach(function (item) {
          item.style.aspectRatio = "";
          item.style.height = "";
          item.style.minHeight = "";
          item.style.overflow = "";
        });
      }
      // Gallery imgs always cover their figure (aspect-ratio or fixed h).
      gal.querySelectorAll("img").forEach(function (im) {
        im.style.width = "100%";
        im.style.maxWidth = "100%";
        im.style.height = "100%";
        im.style.objectFit = "cover";
        im.style.display = "block";
      });
      return;
    }

    if (node.type === "carousel" || node.type === "image_carousel_pro") {
      var car = el.querySelector(".ab-carousel, .ab-img-carousel");
      if (!car) return;
      car.style.width = "100%";
      car.style.maxWidth = "100%";
      car.style.boxSizing = "border-box";
      var ch =
        h ||
        (node.settings && node.settings.height
          ? String(node.settings.height) +
            (/^\d+$/.test(String(node.settings.height)) ? "px" : "")
          : "");
      if (ch) {
        if (/^\d+$/.test(String(ch))) ch = ch + "px";
        car.style.setProperty("--ab-carousel-h", ch);
        car.style.setProperty("--ab-ic-h", ch);
        car.style.height = ch;
      }
      car.querySelectorAll("img").forEach(function (im) {
        im.style.width = "100%";
        im.style.maxWidth = "100%";
        im.style.height = "100%";
        im.style.objectFit = "cover";
        im.style.display = "block";
      });
      return;
    }

    // Other media: only force fill when explicit height is set.
    if (!h) return;

    if (node.type === "image_float" || node.type === "image_overlay") {
      var box = el.querySelector(".ab-img-float, .ab-img-overlay");
      if (box) {
        box.style.setProperty("--ab-if-h", h);
        box.style.setProperty("--ab-io-h", h);
        box.style.height = h;
      }
      el.querySelectorAll("img").forEach(function (im) {
        im.style.width = "100%";
        im.style.height = "100%";
        im.style.objectFit = "cover";
        im.style.display = "block";
      });
      return;
    }

    if (node.type === "image_box") {
      el.querySelectorAll(".ab-image-box img").forEach(function (n) {
        n.style.maxWidth = "100%";
        n.style.objectFit = "cover";
        n.style.display = "block";
      });
      return;
    }

    var inner = el.querySelector(
      ".ab-video, .ab-embed, .ab-embed-responsive, .ab-preview-card, iframe"
    );
    if (inner) {
      if (inner.classList && inner.classList.contains("ab-embed-responsive")) {
        inner.style.height = "100%";
        inner.style.paddingBottom = "0";
      } else {
        inner.style.height = "100%";
        inner.style.maxWidth = "100%";
      }
    }
  }

  /**
   * Live re-apply styles of one node in the canvas (no full paint / no loss of selection).
   * Call after any style change so Mobile/Tablet effects show immediately.
   */
  function refreshNodeVisual(id) {
    id = id || state.selectedId;
    var node = getNode(id);
    if (!node) return;
    var el = canvasNodeEl(id);
    if (!el) return;

    // Rebuild style attribute from cascade for current VIEW breakpoint.
    var tmp = document.createElement("div");
    tmp.innerHTML = "<div" + styleAttr(node) + "></div>";
    var styled = tmp.firstChild;
    if (styled && styled.getAttribute("style")) {
      el.setAttribute("style", styled.getAttribute("style"));
    } else {
      el.removeAttribute("style");
    }

    // Rebuild leaf text content styles for current breakpoint.
    var isLeaf =
      node.type !== "root" &&
      node.type !== "section" &&
      node.type !== "container";
    if (isLeaf) {
      var chrome = el.querySelector(".ab-overlay-chrome");
      var chromeHtmlStr = chrome ? chrome.outerHTML : "";
      var handles = [];
      el.querySelectorAll(".ab-resize-handle").forEach(function (h) {
        handles.push(h.outerHTML);
      });
      var leaf = renderLeaf(node);
      el.innerHTML = chromeHtmlStr + handles.join("") + leaf;
      // Re-mark selected chrome if needed
      if (state.selectedId === id && !el.querySelector(".ab-overlay-chrome")) {
        el.insertAdjacentHTML("afterbegin", chromeHtml(node));
      }
    }

    // Force cascade colors / fonts onto text leaves only (never the node root —
    // that paints the purple editor chrome/tab via inheritance).
    var st = stylesFor(node, "view");
    // Keep overlay chrome locked to purple/white regardless of text color.
    el.querySelectorAll(".ab-overlay-chrome, .ab-overlay-chrome *").forEach(function (ch) {
      ch.style.setProperty("color", "#fff", "important");
      ch.style.setProperty("-webkit-text-fill-color", "#fff", "important");
    });
    if (st.color) {
      // Do NOT set color on el root (breaks chrome). Apply to content leaves only.
      var colorSel =
        node.type === "button"
          ? ".ab-btn, .ab-btn span"
          : node.type === "comments_form"
            ? ".ab-comments-form__title, .comment-reply-title, .ab-comment-form label, .ab-comments-form__list-title"
            : ".ab-text, .ab-heading, [data-field], .ab-btn, .ab-btn span, .ab-html, .ab-loop-card__title, .ab-loop-card__title a, .ab-loop-card__more, .ab-loop-card__excerpt, .ab-loop-card__meta, .ab-loop-card__author";
      el.querySelectorAll(colorSel).forEach(function (ch) {
        if (isEditorChromeEl(ch)) return;
        // Never recolor comments submit with text color.
        if (ch.classList && ch.classList.contains("ab-comment-form__submit")) return;
        if (ch.closest && ch.closest(".ab-comment-form__submit")) return;
        ch.style.setProperty("color", st.color, "important");
        ch.style.setProperty("-webkit-text-fill-color", st.color, "important");
      });
    } else {
      el.style.removeProperty("color");
      el.style.removeProperty("-webkit-text-fill-color");
    }
    // Never set the "background" shorthand — it wipes background-image.
    // Buttons: paint .ab-btn face only (wrapper stays full-width for align, not a box).
    if (node.type === "button") {
      el.style.setProperty("background", "transparent", "important");
      el.style.setProperty("background-color", "transparent", "important");
      el.style.setProperty("background-image", "none", "important");
      el.style.setProperty("border", "0", "important");
      el.style.setProperty("padding", "0", "important");
      var face = el.querySelector(".ab-btn");
      if (face) {
        // Solid + gradient live update (do not use "background" shorthand).
        applyNodeBackground(face, st);
        if (st.color) {
          face.style.setProperty("color", st.color, "important");
          face.style.setProperty("-webkit-text-fill-color", st.color, "important");
        }
        if (st.padding) face.style.setProperty("padding", st.padding, "important");
        if (st.radius) face.style.setProperty("border-radius", st.radius, "important");
        if (st.border) face.style.setProperty("border", st.border, "important");
        if (st.font_weight) face.style.setProperty("font-weight", st.font_weight, "important");
        if (st.font_size) face.style.setProperty("font-size", st.font_size, "important");
        if (st.font_family) {
          face.style.setProperty(
            "font-family",
            normalizeFontFamilyCss(st.font_family) || st.font_family,
            "important"
          );
        }
      }
    } else if (st.background || resolveBgGradient(st) || st.bg_image) {
      // applyNodeBackground handles solid color + gradient + image layers.
      applyNodeBackground(el, st);
      if (st.background && !resolveBgGradient(st)) {
        el.querySelectorAll(".ab-loop-card").forEach(function (card) {
          card.style.setProperty("background-color", st.background, "important");
        });
      }
    } else {
      el.style.removeProperty("background-color");
      el.style.removeProperty("background");
      el.style.removeProperty("background-image");
      el.style.removeProperty("background-size");
      el.style.removeProperty("background-position");
      el.style.removeProperty("background-repeat");
      el.style.removeProperty("background-attachment");
    }
    // Comments form submit button uses dedicated settings + style color.
    if (node.type === "comments_form") {
      applyCommentsFormLiveStyles(el, node, st);
    }
    // Typography cascade into descendants (container/section → all texts/headings).
    var typoSel =
      ".ab-text, .ab-heading, [data-field], .ab-loop-card__title, .ab-icon-box__title, .ab-image-box__title, .ab-info-box__title, .ab-image-box__desc, .ab-icon-box__desc, .ab-info-box__text, .ab-btn, .ab-comments-form__title, .comment-reply-title, .ab-comment-form label, h1, h2, h3, h4, h5, h6, p";
    function applyTypoProp(prop, cssProp, val) {
      if (!val) return;
      el.querySelectorAll(typoSel).forEach(function (ch) {
        if (isEditorChromeEl(ch)) return;
        if (ch.classList && ch.classList.contains("ab-comment-form__submit")) return;
        if (ch.closest && ch.closest(".ab-comment-form__submit")) return;
        ch.style.setProperty(cssProp, val, "important");
      });
    }
    if (st.font_size) applyTypoProp("font_size", "font-size", st.font_size);
    if (st.font_weight) applyTypoProp("font_weight", "font-weight", st.font_weight);
    if (st.font_style) applyTypoProp("font_style", "font-style", st.font_style);
    if (st.line_height) applyTypoProp("line_height", "line-height", st.line_height);
    if (st.letter_spacing) applyTypoProp("letter_spacing", "letter-spacing", st.letter_spacing);
    if (st.text_transform) applyTypoProp("text_transform", "text-transform", st.text_transform);
    if (st.text_decoration) applyTypoProp("text_decoration", "text-decoration", st.text_decoration);
    if (st.font_family) {
      var ff = normalizeFontFamilyCss(st.font_family) || st.font_family;
      ensureGoogleFont(ff);
      applyTypoProp("font_family", "font-family", ff);
    }

    // Sync motion-effect classes / CSS vars after style edits
    try {
      var fxKeep = {};
      Array.prototype.forEach.call(el.classList, function (c) {
        if (c.indexOf("ab-fx") === 0) fxKeep[c] = true;
      });
      Object.keys(fxKeep).forEach(function (c) {
        el.classList.remove(c);
      });
      var fxNew = fxClassList(st);
      fxNew.forEach(function (c) {
        el.classList.add(c);
      });
      if (fxNew.indexOf("ab-fx-entrance") >= 0) el.classList.add("ab-fx-in");
      if (fxNew.length) el.setAttribute("data-ab-fx", "1");
      else el.removeAttribute("data-ab-fx");
      var fxStLive = fxInlineStyle(st);
      if (fxStLive) {
        fxStLive.split(";").forEach(function (pair) {
          var kv = pair.split(":");
          if (kv.length >= 2) {
            el.style.setProperty(kv[0].trim(), kv.slice(1).join(":").trim());
          }
        });
      }
    } catch (eFx) {}
    if (st.font_weight) {
      el.style.setProperty("font-weight", st.font_weight, "important");
      el.querySelectorAll(
        ".ab-text, .ab-heading, [data-field], .ab-loop-card__title, .ab-icon-box__title, .ab-image-box__title, .ab-btn, h1, h2, h3, h4, h5, h6"
      ).forEach(function (ch) {
        ch.style.setProperty("font-weight", st.font_weight, "important");
      });
    }
    if (st.text_align) {
      el.style.setProperty("text-align", st.text_align, "important");
      el.querySelectorAll(".ab-text, .ab-heading, [data-field]").forEach(function (ch) {
        ch.style.setProperty("text-align", st.text_align, "important");
      });
    }
    // Keep media widgets filling the node box (handles + content stay aligned).
    syncMediaFill(el, node, st);
    if (node.type === "image_box") {
      var ib = node.settings || {};
      var box = el.querySelector(".ab-image-box");
      if (box) {
        var pos = ib.image_position || "top";
        var hasBorder = ib.border === "1";
        box.className =
          "ab-image-box ab-image-box--" +
          pos +
          (ib.shadow === "1" ? " ab-image-box--shadow" : "") +
          (hasBorder ? " ab-image-box--border" : " ab-image-box--no-border");
        box.style.setProperty("--ab-ib-radius", (ib.radius || "12") + "px");
        box.style.setProperty("--ab-ib-media-w", (ib.media_width || "40") + "%");
        box.style.setProperty("--ab-ib-fit", ib.object_fit || "cover");
        box.style.setProperty("--ab-ib-pos", ib.object_position || "50% 50%");
        box.style.setProperty("--ab-ib-border", ib.border_color || "#e2e8f0");
        if (!hasBorder) {
          box.style.setProperty("border", "none", "important");
          box.style.outline = "none";
          if (ib.shadow !== "1") {
            box.style.boxShadow = "none";
          }
        }
        if (ib.image_height) {
          var h = String(ib.image_height);
          if (/^\d+$/.test(h)) h += "px";
          box.style.setProperty("--ab-ib-height", h);
        } else {
          box.style.removeProperty("--ab-ib-height");
        }
        box.style.textAlign = ib.align || "left";
      }
      el.querySelectorAll(".ab-image-box img").forEach(function (img) {
        img.style.display = "block";
        img.style.width = "100%";
        img.style.maxWidth = "100%";
        img.style.border = "none";
        img.style.objectFit = ib.object_fit || "cover";
        img.style.objectPosition = ib.object_position || "50% 50%";
        if (ib.image_height) {
          img.style.height = "100%";
        } else {
          img.style.height = "auto";
        }
      });
    }
    if (node.type === "accordion" || node.type === "toggle" || node.type === "tabs") {
      var accS = node.settings || {};
      var accFit = accS.object_fit || "cover";
      var accPos = accS.object_position || "50% 50%";
      var accRoot = el.querySelector(".ab-accordion, .ab-toggle-list, .ab-tabs");
      if (accRoot) {
        if (node.type === "accordion") {
          accRoot.style.setProperty("--ab-acc-fit", accFit);
          accRoot.style.setProperty("--ab-acc-pos", accPos);
        } else if (node.type === "toggle") {
          accRoot.style.setProperty("--ab-tg-fit", accFit);
          accRoot.style.setProperty("--ab-tg-pos", accPos);
        } else {
          accRoot.style.setProperty("--ab-tabs-fit", accFit);
          accRoot.style.setProperty("--ab-tabs-pos", accPos);
        }
      }
      el.querySelectorAll(
        ".ab-accordion__media img, .ab-toggle-w__media img, .ab-tabs__media img"
      ).forEach(function (img) {
        img.style.objectFit = accFit;
        img.style.objectPosition = accPos;
      });
    }
    if (node.type === "image_float") {
      var ifS = node.settings || {};
      var ifBox = el.querySelector(".ab-img-float");
      var ifPos = ifS.object_position || "50% 50%";
      if (ifBox) ifBox.style.setProperty("--ab-if-pos", ifPos);
      el.querySelectorAll(".ab-img-float img").forEach(function (img) {
        img.style.objectFit = "cover";
        img.style.objectPosition = ifPos;
      });
    }
    if (node.type === "image_overlay") {
      var ioS = node.settings || {};
      var ioBox = el.querySelector(".ab-img-overlay");
      var ioPos = ioS.object_position || "50% 50%";
      if (ioBox) ioBox.style.setProperty("--ab-io-pos", ioPos);
      el.querySelectorAll(".ab-img-overlay__img, .ab-img-overlay img").forEach(function (img) {
        img.style.objectFit = "cover";
        img.style.objectPosition = ioPos;
      });
    }
    if (node.type === "loop_grid") {
      var lgS = node.settings || {};
      var lgPos = lgS.image_object_position || "50% 50%";
      var lgFit = lgS.image_object_fit || "cover";
      el.querySelectorAll(".ab-loop-card__media-wrap").forEach(function (wrap) {
        wrap.style.setProperty("--ab-loop-pos", lgPos);
        wrap.style.setProperty("--ab-loop-fit", lgFit);
      });
      el.querySelectorAll(".ab-loop-card__img").forEach(function (img) {
        img.style.objectFit = lgFit;
        img.style.objectPosition = lgPos;
      });
    }
    if (node.type === "counter") {
      runCounterAnimations(el);
    }
    if (node.type === "loop_grid") {
      // Don't re-fetch posts on every style tweak if list already has real cards —
      // only re-apply colors above. Re-hydrate when empty/placeholder.
      var list = el.querySelector("[data-loop-list]");
      var hasCards = list && list.querySelector(".ab-loop-card:not(.ab-loop-card--ph)");
      if (!hasCards) hydrateLoopGrids();
    }
  }

  function duplicateNode(id) {
    var node = getNode(id);
    if (!node || node.type === "root") return;
    function cloneTree(nid, newParent) {
      var src = getNode(nid);
      if (!src) return null;
      var newId = uid();
      var copy = cloneDoc(src);
      copy.id = newId;
      copy.parentId = newParent;
      copy.children = [];
      state.document.nodes[newId] = copy;
      (src.children || []).forEach(function (cid) {
        var kid = cloneTree(cid, newId);
        if (kid) copy.children.push(kid);
      });
      return newId;
    }
    var parent = getNode(node.parentId);
    if (!parent) return;
    var newId = cloneTree(id, node.parentId);
    var idx = parent.children.indexOf(id);
    parent.children.splice(idx + 1, 0, newId);
    state.selectedId = newId;
  }

  function moveNode(id, newParentId, index) {
    var node = getNode(id);
    var newParent = getNode(newParentId);
    if (!node || !newParent || node.type === "root") return;
    if (id === newParentId) return;
    // prevent move into self/descendant
    var walk = newParentId;
    while (walk) {
      if (walk === id) return;
      var w = getNode(walk);
      walk = w ? w.parentId : null;
    }
    if (node.parentId) {
      var oldP = getNode(node.parentId);
      if (oldP && oldP.children) {
        oldP.children = oldP.children.filter(function (c) {
          return c !== id;
        });
      }
    }
    node.parentId = newParentId;
    if (!newParent.children) newParent.children = [];
    if (typeof index !== "number" || index < 0 || index > newParent.children.length) {
      newParent.children.push(id);
    } else {
      newParent.children.splice(index, 0, id);
    }
  }

  /* ── render ── */
  function renderNode(id) {
    var node = getNode(id);
    if (!node || node.hidden) return "";
    var multiOn = isIdSelected(id);
    var sel =
      multiOn && getSelectedIds().length > 1
        ? " is-multi-selected" + (state.selectedId === id ? " is-selected" : "")
        : state.selectedId === id
          ? " is-selected"
          : "";
    var stView = stylesFor(node, "view");
    var fxCls = fxClassList(stView);
    // In editor, entrance shows as already-in so layout stays readable; preview button re-triggers.
    if (fxCls.indexOf("ab-fx-entrance") >= 0) fxCls.push("ab-fx-in");
    var classes =
      "ab-node ab-type-" +
      node.type +
      " ab-node-" +
      node.id +
      sel +
      (fxCls.length ? " " + fxCls.join(" ") : "");
    // Fixed size flags — CSS fills media into the box (editor + front parity).
    var hFlag = stView.height || stView.min_height || "";
    if (hFlag && String(hFlag) !== "auto") classes += " ab-has-h";
    if (stView.width && String(stView.width) !== "auto") classes += " ab-has-w";
    if (stView.z_index !== undefined && stView.z_index !== null && stView.z_index !== "") {
      classes += " ab-has-z";
    }
    // Section layout class for CSS (row header vs grid columns).
    if (node.type === "section") {
      var secDirCls = (node.settings && node.settings.direction) || "grid";
      if (secDirCls === "row") classes += " ab-section--row";
      else if (secDirCls === "grid") classes += " ab-section--grid";
      else classes += " ab-section--column";
    }
    // Container row class — children (buttons etc.) sit side-by-side
    if (node.type === "container") {
      var cDir = (node.settings && node.settings.direction) || "column";
      if (cDir === "row") classes += " ab-container--row";
      else classes += " ab-container--column";
    }
    var attr =
      ' class="' +
      classes +
      '" data-node-id="' +
      esc(node.id) +
      '" data-type="' +
      esc(node.type) +
      '" data-ab-editable="1"' +
      (fxCls.length ? ' data-ab-fx="1"' : "");
    var style = styleAttr(node);
    var fxSt = fxInlineStyle(stView);
    if (fxSt) {
      if (style && style.indexOf('style="') === 0) {
        style = style.replace('style="', 'style="' + fxSt + ";");
      } else {
        style = ' style="' + fxSt + '"';
      }
    }

    var isContainer = node.type === "root" || node.type === "section" || node.type === "container";
    if (isContainer) {
      var inner = "";
      (node.children || []).forEach(function (cid) {
        inner += renderNode(cid);
      });
      if (!inner && node.type !== "root") {
        inner = '<div class="ab-drop-empty" data-parent="' + esc(node.id) + '">+ Adicionar elemento</div>';
      }
      if (node.type === "root") {
        return (
          '<div class="ab-document ab-document--editor" data-doc="' +
          esc(state.document.id) +
          '">' +
          inner +
          "</div>"
        );
      }
      var tag = node.type === "section" ? "section" : "div";
      return (
        "<" +
        tag +
        attr +
        style +
        ">" +
        chromeHtml(node) +
        inner +
        "</" +
        tag +
        ">"
      );
    }

    return "<div" + attr + style + ">" + chromeHtml(node) + renderLeaf(node) + "</div>";
  }

  /** Inline typography styles for the visible text leaf (must beat theme CSS). */
  function leafTextStyle(node, extra) {
    var st = stylesFor(node, "view");
    var s = (node && node.settings) || {};
    var parts = [];
    if (extra) parts.push(extra);
    if (st.color) {
      parts.push("color:" + st.color + " !important");
      parts.push("-webkit-text-fill-color:" + st.color + " !important");
    }
    // Size preset on heading maps to font-size if no explicit font_size
    var fontSize = st.font_size;
    if (!fontSize && s.size && HEADING_SIZE_MAP[s.size]) {
      fontSize = HEADING_SIZE_MAP[s.size];
    }
    if (fontSize) parts.push("font-size:" + fontSize + " !important");
    if (st.font_family) {
      var ffLeaf = normalizeFontFamilyCss(st.font_family) || st.font_family;
      ensureGoogleFont(ffLeaf);
      parts.push("font-family:" + ffLeaf + " !important");
    }
    if (st.font_weight) parts.push("font-weight:" + st.font_weight + " !important");
    if (st.font_style) parts.push("font-style:" + st.font_style);
    if (st.line_height) parts.push("line-height:" + st.line_height);
    if (st.letter_spacing) parts.push("letter-spacing:" + st.letter_spacing);
    if (st.text_transform) parts.push("text-transform:" + st.text_transform);
    if (st.text_decoration) parts.push("text-decoration:" + st.text_decoration);
    if (st.background) {
      parts.push("background-color:" + st.background + " !important");
    }
    return parts.length ? parts.join(";") : "";
  }

  function renderLeaf(node) {
    var s = node.settings || {};
    var st = stylesFor(node, "view");
    var align = st.text_align || s.align || "left";
    switch (node.type) {
      case "heading": {
        var allowedTags = ["h1", "h2", "h3", "h4", "h5", "h6", "div", "span", "p"];
        var tag = allowedTags.indexOf(s.tag) >= 0 ? s.tag : "h2";
        var hStyle = leafTextStyle(node, "text-align:" + align + ";margin:0 0 .5em");
        var inner = esc(plainText(s.text || ""));
        var hClass = "ab-heading";
        if (/^h[1-6]$/.test(tag)) hClass += " ab-h" + tag.charAt(1);
        var headingEl =
          "<" +
          tag +
          ' class="' +
          hClass +
          '" data-tag="' +
          esc(tag) +
          '" style="' +
          esc(hStyle) +
          '" contenteditable="true" data-field="text">' +
          inner +
          "</" +
          tag +
          ">";
        if (s.link) {
          return (
            '<div class="ab-heading-wrap" style="text-align:' +
            esc(align) +
            '"><a class="ab-heading-link" href="' +
            esc(s.link) +
            '" onclick="return false" style="color:inherit;text-decoration:none">' +
            headingEl +
            "</a></div>"
          );
        }
        return headingEl;
      }
      case "text": {
        var tStyle = leafTextStyle(node, "text-align:" + align);
        return (
          '<div class="ab-text" style="' +
          esc(tStyle) +
          '" contenteditable="true" data-field="text">' +
          esc(plainText(s.text || "")) +
          "</div>"
        );
      }
      case "image":
        if (!s.url) {
          return (
            '<div class="ab-img-ph">Imagem — use Galeria ou URL' +
            (CFG.hasMedia ? "" : "") +
            "</div>"
          );
        }
        var imgW = s.width || "";
        var stImg = stylesFor(node, "view") || {};
        var hasHImg =
          !!(stImg.height && stImg.height !== "auto") ||
          !!(stImg.min_height && stImg.min_height !== "auto" && stImg.min_height !== "0" && stImg.min_height !== "0px");
        var imgFit = s.object_fit || (hasHImg ? "cover" : "contain");
        if (hasHImg && (!s.object_fit || s.object_fit === "contain")) imgFit = "cover";
        var imgPos = s.object_position || "center center";
        var imgInline = [];
        if (hasHImg) {
          // Fill box — CSS .ab-has-h + applyMediaFill also enforce this.
          imgInline.push("max-width:100%");
          imgInline.push("object-position:" + imgPos);
        } else {
          if (imgW) imgInline.push("width:" + imgW + ";max-width:100%");
          else imgInline.push("max-width:100%;height:auto;width:auto");
          imgInline.push("object-fit:" + imgFit);
          imgInline.push("object-position:" + imgPos);
          imgInline.push("display:inline-block;vertical-align:middle");
        }
        return (
          '<div class="ab-image' +
          (hasHImg ? " ab-image--fill" : "") +
          '" style="text-align:' +
          esc(align || "center") +
          ";--ab-img-pos:" +
          esc(imgPos) +
          '" data-fit="' +
          esc(imgFit) +
          '" data-pos="' +
          esc(imgPos) +
          '"><img src="' +
          esc(s.url) +
          '" alt="' +
          esc(s.alt || "") +
          '" loading="lazy" draggable="false"' +
          (imgInline.length ? ' style="' + esc(imgInline.join(";")) + '"' : "") +
          "/></div>"
        );
      case "dynamic": {
        var src = s.source || "site_title";
        var lab = DYNAMIC_LABELS[src] || src;
        var preview = "";
        if (src === "site_logo" && CFG.logoUrl) {
          preview = '<img src="' + esc(CFG.logoUrl) + '" alt="logo" style="max-height:64px;width:auto"/>';
        } else if (src === "site_title") {
          preview = esc(CFG.siteName || "Título do site");
        } else if (src === "site_tagline") {
          preview = esc(CFG.siteTagline || "Descrição do site");
        } else if (src === "featured_image") {
          preview = '<div class="ab-img-ph">Imagem destacada</div>';
        } else if (src === "post_content") {
          preview =
            '<div class="ab-dynamic-post-content ab-dynamic-placeholder entry-content">' +
            "<p><strong>Conteúdo do post</strong></p>" +
            "<p>Corpo do post/página no site público (como o bloco Conteúdo do editor de site).</p>" +
            "</div>";
        } else if (src === "post_title") {
          preview = esc("Título do post");
        } else if (src === "post_date" || src === "post_modified") {
          preview = esc(formatWpDate(new Date().toISOString()));
        } else if (src === "post_author") {
          preview = esc("Autor");
        } else if (src === "navigation") {
          preview = '<nav class="ab-dynamic-nav-preview">Menu · Navegação</nav>';
        } else if (src === "avatar") {
          preview = '<span class="ab-dynamic-avatar-ph">👤</span>';
        } else {
          preview = esc(lab);
        }
        return (
          '<div class="ab-dynamic ab-dynamic--' +
          esc(src) +
          '" style="text-align:' +
          esc(align) +
          '" data-dynamic="' +
          esc(src) +
          '"><span class="ab-dynamic-badge">' +
          esc(lab) +
          "</span>" +
          preview +
          "</div>"
        );
      }
      case "embed": {
        var prov = s.provider || "youtube";
        var url = s.url || "";
        if (!url) {
          return (
            '<div class="ab-embed ab-embed--empty" style="text-align:' +
            esc(align) +
            '">Cole a URL · ' +
            esc(prov === "x" ? "X" : prov === "youtube" ? "YouTube" : "oEmbed") +
            "</div>"
          );
        }
        if (prov === "youtube") {
          var yid = "";
          var ym = String(url).match(
            /(?:youtu\.be\/|youtube\.com\/(?:watch\?v=|embed\/|shorts\/|live\/))([A-Za-z0-9_-]{6,})/
          );
          if (ym) yid = ym[1];
          if (yid) {
            return (
              '<div class="ab-embed ab-embed--youtube" style="text-align:' +
              esc(align) +
              '"><div class="ab-embed-responsive"><iframe src="https://www.youtube.com/embed/' +
              esc(yid) +
              '" title="YouTube" frameborder="0" allowfullscreen loading="lazy"></iframe></div></div>'
            );
          }
        }
        return (
          '<div class="ab-embed" style="text-align:' +
          esc(align) +
          '"><a href="' +
          esc(url) +
          '" target="_blank" rel="noopener">' +
          esc(url) +
          "</a></div>"
        );
      }
      case "video": {
        var vu = s.url || "";
        if (!vu) return '<div class="ab-video ab-empty">Vídeo — cole a URL</div>';
        var yid2 = "";
        var ym2 = String(vu).match(
          /(?:youtu\.be\/|youtube\.com\/(?:watch\?v=|embed\/|shorts\/))([A-Za-z0-9_-]{6,})/
        );
        if (ym2) yid2 = ym2[1];
        if (yid2) {
          return (
            '<div class="ab-video" style="text-align:' +
            esc(align) +
            '"><div class="ab-embed-responsive"><iframe src="https://www.youtube.com/embed/' +
            esc(yid2) +
            '" title="Video" frameborder="0" allowfullscreen loading="lazy"></iframe></div></div>'
          );
        }
        return (
          '<div class="ab-video" style="text-align:' +
          esc(align) +
          '"><a href="' +
          esc(vu) +
          '">' +
          esc(vu) +
          "</a></div>"
        );
      }
      case "star_rating": {
        var rating = parseFloat(s.rating) || 0;
        var maxS = parseInt(s.max, 10) || 5;
        var stars = "";
        for (var si = 1; si <= maxS; si++) {
          stars +=
            '<span class="ab-star ' +
            (si <= Math.floor(rating) ? "is-full" : "is-empty") +
            '">★</span>';
        }
        return (
          '<div class="ab-star-rating" style="text-align:' +
          esc(align) +
          '">' +
          stars +
          (s.show_score !== "0"
            ? '<span class="ab-star-score">' + esc(String(rating)) + "</span>"
            : "") +
          "</div>"
        );
      }
      case "google_maps":
        return (
          '<div class="ab-maps ab-maps--editor"><span>📍 ' +
          esc(s.address || "Endereço") +
          "</span></div>"
        );
      case "icon":
        return (
          '<div class="ab-icon-wrap" style="text-align:' +
          esc(align) +
          '"><span class="ab-icon" style="font-size:' +
          esc(String(s.size || 32)) +
          'px">' +
          esc(s.icon || "★") +
          "</span></div>"
        );
      case "icon_box":
        return (
          '<div class="ab-icon-box" style="text-align:' +
          esc(align) +
          '"><div class="ab-icon-box__icon">' +
          esc(s.icon || "◆") +
          "</div><h3 class=\"ab-icon-box__title\">" +
          esc(s.title || "") +
          '</h3><p class="ab-icon-box__desc">' +
          esc(s.description || "") +
          "</p></div>"
        );
      case "gallery": {
        var gImgs = parseImagesValue(s.images || "[]");
        if (!gImgs.length) return '<div class="ab-gallery ab-empty">Galeria — abra a mídia</div>';
        var gCols = parseInt(s.columns, 10) || 3;
        var gGap = parseInt(s.gap, 10) || 8;
        var gLayout = s.layout || "grid";
        var gRatio = s.ratio || "4/3";
        var gPer = parseInt(s.per_view, 10) || 1;
        var gSt = stylesFor(node, "view") || {};
        var gRad = gSt.radius || "";
        var gStyle =
          "--ab-gal-cols:" +
          gCols +
          ";--ab-gal-gap:" +
          gGap +
          "px;--ab-gal-ratio:" +
          (gRatio === "auto" ? "auto" : gRatio.replace("/", " / ")) +
          ";--ab-gal-per:" +
          gPer;
        if (gRad) gStyle += ";--ab-radius:" + gRad + ";border-radius:" + gRad;
        var gMod =
          gLayout === "slider"
            ? " ab-gallery--slider"
            : gLayout === "masonry"
              ? " ab-gallery--masonry"
              : " ab-gallery--grid";
        var gAuto = s.autoplay !== "0" && s.autoplay !== "false";
        var gSpeed = parseInt(s.autoplay_speed, 10) || 4500;
        var ghtml =
          '<div class="ab-gallery' +
          gMod +
          '"' +
          (gLayout === "slider"
            ? ' data-ab-gallery-slider="1" data-per="' +
              gPer +
              '" data-autoplay="' +
              (gAuto ? "1" : "0") +
              '" data-autoplay-ms="' +
              gSpeed +
              '"'
            : "") +
          ' style="' +
          gStyle +
          '">';
        if (gLayout === "slider") ghtml += '<div class="ab-gallery__track">';
        gImgs.forEach(function (u, i) {
          ghtml +=
            '<figure class="ab-gallery__item' +
            (i === 0 ? " is-active" : "") +
            '"><img src="' +
            esc(u) +
            '" alt=""/></figure>';
        });
        if (gLayout === "slider") {
          ghtml += "</div>";
          if (gImgs.length > gPer) {
            ghtml +=
              '<button type="button" class="ab-gallery__nav ab-gallery__nav--prev" data-ab-gal-prev aria-label="Anterior">‹</button>' +
              '<button type="button" class="ab-gallery__nav ab-gallery__nav--next" data-ab-gal-next aria-label="Próximo">›</button>';
          }
        }
        ghtml += "</div>";
        return ghtml;
      }
      case "carousel": {
        var cImgs = parseImagesValue(s.images || "[]");
        if (!cImgs.length) return '<div class="ab-carousel ab-empty">Carrossel — abra a mídia</div>';
        var cH = parseInt(s.height, 10) || 280;
        var cPer = parseInt(s.per_view, 10) || 1;
        var cLayout = s.layout || "single";
        if (cLayout === "single") cPer = 1;
        var cGap = parseInt(s.gap, 10) || 8;
        var cFx = s.slide_effect || (cLayout === "fade" ? "fade" : "slide");
        var cSpeed = parseInt(s.slide_speed, 10) || 450;
        var chtml =
          '<div class="ab-carousel ab-carousel--' +
          esc(cLayout) +
          " ab-carousel--fx-" +
          esc(cFx) +
          '" style="--ab-carousel-h:' +
          cH +
          "px;--ab-carousel-per:" +
          cPer +
          ";--ab-carousel-gap:" +
          cGap +
          "px;--ab-carousel-speed:" +
          cSpeed +
          'ms"><div class="ab-carousel__track">';
        cImgs.forEach(function (u, i) {
          chtml +=
            '<div class="ab-carousel__slide' +
            (i === 0 ? " is-active" : "") +
            '"><img src="' +
            esc(u) +
            '" alt=""/></div>';
        });
        chtml += "</div></div>";
        return chtml;
      }
      case "icon_list": {
        var ilItems = parseIconListItems(s.items || "[]");
        var li = '<ul class="ab-icon-list" style="text-align:' + esc(align) + '">';
        ilItems.forEach(function (it) {
          li +=
            '<li class="ab-icon-list__item"><span class="ab-icon-list__icon">' +
            esc(it.icon || "✓") +
            '</span><span class="ab-icon-list__text">' +
            esc(it.text || "") +
            "</span></li>";
        });
        return li + "</ul>";
      }
      case "image_box": {
        var ibPos = s.image_position || "top";
        var ibHasBorder = s.border === "1";
        var ibCls =
          "ab-image-box ab-image-box--" +
          ibPos +
          (s.shadow === "1" ? " ab-image-box--shadow" : "") +
          (ibHasBorder ? " ab-image-box--border" : " ab-image-box--no-border");
        var ibH = s.image_height ? String(s.image_height) : "";
        if (ibH && /^\d+$/.test(ibH)) ibH += "px";
        var ibStyle =
          "text-align:" +
          esc(s.align || "left") +
          ";--ab-ib-radius:" +
          esc(s.radius || "12") +
          "px;--ab-ib-media-w:" +
          esc(s.media_width || "40") +
          "%;--ab-ib-fit:" +
          esc(s.object_fit || "cover") +
          ";--ab-ib-pos:" +
          esc(s.object_position || "50% 50%") +
          ";--ab-ib-border:" +
          esc(s.border_color || "#e2e8f0") +
          ";" +
          (ibHasBorder ? "" : "border:none!important;outline:none;") +
          (ibH ? "--ab-ib-height:" + esc(ibH) + ";" : "");
        var ibImg = s.url
          ? '<img src="' +
            esc(s.url) +
            '" alt="' +
            esc(s.title || "") +
            '" style="object-fit:' +
            esc(s.object_fit || "cover") +
            ";object-position:" +
            esc(s.object_position || "50% 50%") +
            '"/>'
          : '<div class="ab-img-ph">Imagem</div>';
        var ibInner =
          '<div class="ab-image-box__media">' +
          ibImg +
          '</div><div class="ab-image-box__body">' +
          (s.title ? '<h3 class="ab-image-box__title">' + esc(s.title) + "</h3>" : "") +
          (s.description ? '<p class="ab-image-box__desc">' + esc(s.description) + "</p>" : "") +
          "</div>";
        if (s.link) {
          ibInner = '<a class="ab-image-box__link" href="' + esc(s.link) + '">' + ibInner + "</a>";
        }
        return '<div class="' + ibCls + '" style="' + ibStyle + '">' + ibInner + "</div>";
      }
      case "counter": {
        var cAnim = s.animate !== "0";
        var cStart = cAnim
          ? (s.prefix || "") + "0" + (s.suffix || "")
          : (s.prefix || "") + (s.number || "0") + (s.suffix || "");
        return (
          '<div class="ab-counter" data-ab-counter="1" data-animate="' +
          (cAnim ? "1" : "0") +
          '" style="text-align:' +
          esc(align) +
          '"><div class="ab-counter__number" data-target="' +
          esc(s.number || "0") +
          '" data-prefix="' +
          esc(s.prefix || "") +
          '" data-suffix="' +
          esc(s.suffix || "") +
          '" data-duration="' +
          esc(s.duration || "1600") +
          '">' +
          esc(cStart) +
          '</div><div class="ab-counter__title">' +
          esc(s.title || "") +
          "</div></div>"
        );
      }
      case "testimonial":
        return (
          '<blockquote class="ab-testimonial" style="text-align:' +
          esc(align) +
          '"><p>' +
          esc(s.content || "") +
          "</p><footer><strong>" +
          esc(s.name || "") +
          "</strong> · " +
          esc(s.role || "") +
          "</footer></blockquote>"
        );
      case "tabs": {
        var tItems = parseAccordionItems(s.items || "[]");
        if (!tItems.length) return '<div class="ab-tabs ab-empty">Abas — adicione itens</div>';
        var tFit = s.object_fit || "cover";
        var tPos = s.object_position || "50% 50%";
        var th =
          '<div class="ab-tabs" style="--ab-tabs-fit:' +
          esc(tFit) +
          ";--ab-tabs-pos:" +
          esc(tPos) +
          '"><div class="ab-tabs__nav">';
        tItems.forEach(function (it, ix) {
          th +=
            '<span class="ab-tabs__tab' +
            (ix === 0 ? " is-active" : "") +
            '">' +
            esc(it.title || "Aba") +
            "</span>";
        });
        th += '</div><div class="ab-tabs__panels">';
        tItems.forEach(function (it, ix) {
          th +=
            '<div class="ab-tabs__panel' +
            (ix === 0 ? " is-active" : "") +
            '">' +
            (it.image
              ? '<figure class="ab-tabs__media"><img src="' +
                esc(it.image) +
                '" alt="" style="object-fit:' +
                esc(tFit) +
                ";object-position:" +
                esc(tPos) +
                '"/></figure>'
              : "") +
            '<p class="ab-tabs__text">' +
            esc(it.content || "") +
            "</p></div>";
        });
        return th + "</div></div>";
      }
      case "accordion": {
        if (s.source === "posts") {
          var qs = s.query_source || "post";
          var qsLab =
            qs === "page" ? "páginas" : qs === "any" ? "posts e páginas" : "posts";
          return (
            '<div class="ab-accordion ab-accordion--posts-preview">' +
            '<div class="ab-accordion__item is-open"><button type="button" class="ab-accordion__head"><span class="ab-accordion__title">Consulta: ' +
            esc(qsLab) +
            " (" +
            esc(String(s.posts_per_page || "6")) +
            ")</span><span class=\"ab-accordion__icon\">+</span></button>" +
            '<div class="ab-accordion__body"><p class="ab-accordion__text">No site público os itens vêm da consulta (tipo, categoria só em posts, ordem, quantidade). Imagem destacada e resumo no painel.</p></div></div></div>'
          );
        }
        var accItems = parseAccordionItems(s.items || "[]");
        if (!accItems.length) {
          return '<div class="ab-accordion ab-empty">Acordeão — adicione itens</div>';
        }
        var accIcon = s.icon_style === "chevron" ? "▾" : "+";
        var accImgPos = s.image_position || "top";
        var accFit = s.object_fit || "cover";
        var accFoc = s.object_position || "50% 50%";
        var ah =
          '<div class="ab-accordion ab-accordion--img-' +
          esc(accImgPos) +
          " ab-accordion--icon-" +
          esc(s.icon_style || "plus") +
          '" data-ab-accordion="1" data-multiple="' +
          (s.multiple === "1" ? "1" : "0") +
          '" style="--ab-acc-fit:' +
          esc(accFit) +
          ";--ab-acc-pos:" +
          esc(accFoc) +
          '">';
        accItems.forEach(function (it, ix) {
          ah +=
            '<div class="ab-accordion__item' +
            (ix === 0 && s.open_first !== "0" ? " is-open" : "") +
            '"><button type="button" class="ab-accordion__head"><span class="ab-accordion__title">' +
            esc(it.title || "Item") +
            '</span><span class="ab-accordion__icon">' +
            accIcon +
            "</span></button><div class=\"ab-accordion__body\">" +
            (it.image
              ? '<figure class="ab-accordion__media"><img src="' +
                esc(it.image) +
                '" alt="" style="object-fit:' +
                esc(accFit) +
                ";object-position:" +
                esc(accFoc) +
                '"/></figure>'
              : "") +
            (it.content
              ? '<div class="ab-accordion__content"><p class="ab-accordion__text">' +
                esc(it.content) +
                "</p></div>"
              : "") +
            "</div></div>";
        });
        return ah + "</div>";
      }
      case "toggle": {
        var tgItems = parseAccordionItems(s.items || "[]");
        // Legacy single title/content
        if (!tgItems.length && (s.title || s.content)) {
          tgItems = [{ title: s.title || "", content: s.content || "", image: "" }];
        }
        if (!tgItems.length) return '<div class="ab-toggle-list ab-empty">Toggle — adicione itens</div>';
        var tgFit = s.object_fit || "cover";
        var tgPos = s.object_position || "50% 50%";
        var tg =
          '<div class="ab-toggle-list" style="--ab-tg-fit:' +
          esc(tgFit) +
          ";--ab-tg-pos:" +
          esc(tgPos) +
          '">';
        tgItems.forEach(function (it, ix) {
          tg +=
            '<div class="ab-toggle-w' +
            (ix === 0 && s.open_first === "1" ? " is-open" : "") +
            '"><div class="ab-toggle-w__head"><span class="ab-toggle-w__title">' +
            esc(it.title || "Toggle") +
            '</span><span class="ab-toggle-w__icon">▾</span></div><div class="ab-toggle-w__body">' +
            (it.image
              ? '<figure class="ab-toggle-w__media"><img src="' +
                esc(it.image) +
                '" alt="" style="object-fit:' +
                esc(tgFit) +
                ";object-position:" +
                esc(tgPos) +
                '"/></figure>'
              : "") +
            '<p class="ab-toggle-w__text">' +
            esc(it.content || "") +
            "</p></div></div>";
        });
        return tg + "</div>";
      }
      case "social_icons": {
        var socItems = parseSocialItems(s.items || "[]");
        var sh =
          '<div class="ab-social ab-social--' +
          esc(s.shape || "circle") +
          " ab-social--" +
          esc(align) +
          '" style="--ab-social-size:' +
          esc(s.size || "40") +
          'px">';
        socItems.forEach(function (it) {
          var meta =
            SOCIAL_NETWORK_OPTS.find(function (n) {
              return n.value === it.network;
            }) || SOCIAL_NETWORK_OPTS[SOCIAL_NETWORK_OPTS.length - 1];
          sh +=
            '<span class="ab-social__item ab-social__item--' +
            esc(it.network) +
            '" style="--ab-social-color:' +
            esc(meta.color) +
            '"><span class="ab-social__badge">' +
            esc(meta.glyph) +
            "</span>" +
            (s.show_label === "1"
              ? '<span class="ab-social__label">' + esc(meta.label) + "</span>"
              : "") +
            "</span>";
        });
        return sh + "</div>";
      }
      case "page_content":
        return (
          '<div class="ab-page-content ab-page-content--editor">' +
          (s.page_id
            ? "<strong>Página/post #" +
              esc(String(s.page_id)) +
              "</strong><p class=\"ab-field-hint\">O conteúdo completo aparece no site público.</p>"
            : '<span class="ab-empty">Escolha uma página no painel →</span>') +
          "</div>"
        );
      case "progress_bar": {
        var pct = Math.max(0, Math.min(100, parseInt(s.percent, 10) || 0));
        return (
          '<div class="ab-progress"><div class="ab-progress__head"><span>' +
          esc(s.title || "") +
          "</span><span>" +
          pct +
          '%</span></div><div class="ab-progress__track"><div class="ab-progress__bar" style="width:' +
          pct +
          '%"></div></div></div>'
        );
      }
      case "shortcode":
        return (
          '<div class="ab-shortcode ab-shortcode--editor"><code>' +
          esc(s.code || "[shortcode]") +
          "</code></div>"
        );
      case "menu_anchor":
        return (
          '<div class="ab-menu-anchor"><span class="ab-anchor-label"># ' +
          esc(s.anchor || "secao") +
          "</span></div>"
        );
      case "alert":
        return (
          '<div class="ab-alert ab-alert--' +
          esc(s.type || "info") +
          '"><strong>' +
          esc(s.title || "") +
          "</strong><div>" +
          esc(s.content || "") +
          "</div></div>"
        );
      case "sidebar":
        return (
          '<div class="ab-sidebar ab-sidebar--editor">Sidebar: <code>' +
          esc(s.sidebar || "sidebar-1") +
          "</code></div>"
        );
      case "text_path":
        return (
          '<div class="ab-text-path" style="text-align:' +
          esc(align) +
          '"><svg viewBox="0 0 200 200" width="160" height="160"><defs><path id="abedpath" d="M100,100 m-70,0 a70,70 0 1,1 140,0 a70,70 0 1,1 -140,0"/></defs><text font-size="14"><textPath href="#abedpath">' +
          esc(s.text || "") +
          "</textPath></text></svg></div>"
        );
      case "advanced_accordion": {
        var aaLines = String(s.items || "").split(/\r?\n/);
        var aaHtml =
          '<div class="ab-adv-acc ab-adv-acc--' +
          esc(s.style || "modern") +
          ' ab-adv-acc--editor">';
        aaLines.forEach(function (ln, ix) {
          ln = ln.trim();
          if (!ln) return;
          var p = ln.split("|");
          var col = (p[3] || "#7c3aed").trim();
          aaHtml +=
            '<div class="ab-adv-acc__item' +
            (ix === 0 ? " is-open" : "") +
            '" style="--ab-acc-accent:' +
            esc(col) +
            '"><div class="ab-adv-acc__head"><span class="ab-adv-acc__icon">' +
            esc((p[2] || "").trim()) +
            '</span><span class="ab-adv-acc__title">' +
            esc((p[0] || "").trim()) +
            '</span><span class="ab-adv-acc__chev"></span></div><div class="ab-adv-acc__body"><div class="ab-adv-acc__body-inner">' +
            esc((p[1] || "").trim()) +
            "</div></div></div>";
        });
        return aaHtml + "</div>";
      }
      case "preview_card":
        return (
          '<article class="ab-preview-card ab-preview-card--' +
          esc(s.style || "hover") +
          '" style="--ab-pc-r:' +
          esc(s.radius || "16") +
          'px"><div class="ab-preview-card__media">' +
          (s.image
            ? '<img src="' + esc(s.image) + '" alt=""/>'
            : '<div class="ab-preview-card__ph"></div>') +
          '<div class="ab-preview-card__shade"></div></div><div class="ab-preview-card__body">' +
          (s.subtitle
            ? '<span class="ab-preview-card__sub">' + esc(s.subtitle) + "</span>"
            : "") +
          '<h3 class="ab-preview-card__title">' +
          esc(s.title || "Card") +
          "</h3>" +
          (s.text ? '<p class="ab-preview-card__text">' + esc(s.text) + "</p>" : "") +
          (s.button_text
            ? '<span class="ab-preview-card__btn">' + esc(s.button_text) + "</span>"
            : "") +
          "</div></article>"
        );
      case "info_box":
        return (
          '<div class="ab-info-box ab-info-box--' +
          esc(s.style || "card") +
          " ab-info-box--" +
          esc(s.align || "center") +
          '" style="--ab-ib-accent:' +
          esc(s.accent || "#7c3aed") +
          '">' +
          (s.icon ? '<div class="ab-info-box__icon">' + esc(s.icon) + "</div>" : "") +
          '<h3 class="ab-info-box__title">' +
          esc(s.title || "") +
          "</h3>" +
          (s.text ? '<p class="ab-info-box__text">' + esc(s.text) + "</p>" : "") +
          (s.button_text
            ? '<span class="ab-info-box__btn">' + esc(s.button_text) + "</span>"
            : "") +
          "</div>"
        );
      case "social_bar": {
        var sbLines = String(s.items || "").split(/\r?\n/);
        var sb =
          '<div class="ab-social-bar ab-social-bar--' +
          esc(s.direction || "row") +
          " ab-social-bar--" +
          esc(s.shape || "circle") +
          " ab-social-bar--" +
          esc(s.align || "center") +
          '" style="--ab-sb-size:' +
          esc(s.size || "44") +
          'px">';
        sbLines.forEach(function (ln) {
          ln = ln.trim();
          if (!ln) return;
          var p = ln.split("|");
          var name = (p[0] || "").trim();
          if (!name) return;
          sb +=
            '<span class="ab-social-bar__item ab-social-bar__item--' +
            esc(name.toLowerCase().replace(/\s+/g, "-")) +
            '"><span class="ab-social-bar__badge">' +
            esc(name.charAt(0).toUpperCase()) +
            "</span>" +
            (s.show_label === "1"
              ? '<span class="ab-social-bar__label">' + esc(name) + "</span>"
              : "") +
            "</span>";
        });
        return sb + "</div>";
      }
      case "text_animation": {
        var taWords = String(s.words || "")
          .split("|")
          .map(function (w) {
            return w.trim();
          })
          .filter(Boolean);
        var taFirst = taWords[0] || "…";
        return (
          '<div class="ab-text-anim ab-text-anim--' +
          esc(s.effect || "typing") +
          " ab-text-anim--" +
          esc(s.align || "center") +
          '">' +
          (s.prefix ? '<span class="ab-text-anim__prefix">' + esc(s.prefix) + "</span>" : "") +
          '<span class="ab-text-anim__word">' +
          esc(taFirst) +
          "</span>" +
          (s.effect === "typing" || !s.effect
            ? '<span class="ab-text-anim__cursor">|</span>'
            : "") +
          (s.suffix ? '<span class="ab-text-anim__suffix">' + esc(s.suffix) + "</span>" : "") +
          "</div>"
        );
      }
      case "image_float":
        return s.image
          ? '<div class="ab-img-float ab-img-float--' +
              esc(s.effect || "float") +
              (s.shadow !== "0" ? " has-shadow" : "") +
              '" style="--ab-if-h:' +
              esc(s.height || "320") +
              "px;--ab-if-r:" +
              esc(s.radius || "16") +
              "px;--ab-if-pos:" +
              esc(s.object_position || "50% 50%") +
              '"><img src="' +
              esc(s.image) +
              '" alt="" style="object-fit:cover;object-position:' +
              esc(s.object_position || "50% 50%") +
              '"/></div>'
          : '<div class="ab-img-float ab-empty">Imagem flutuante</div>';
      case "image_overlay":
        return s.image
          ? '<div class="ab-img-overlay ab-img-overlay--' +
              esc(s.effect || "fade") +
              ' is-preview" style="--ab-io-h:' +
              esc(s.height || "280") +
              "px;--ab-io-r:" +
              esc(s.radius || "16") +
              "px;--ab-io-color:" +
              esc(s.color || "#0f172a") +
              ";--ab-io-op:" +
              esc(String((parseInt(s.opacity, 10) || 70) / 100)) +
              ";--ab-io-pos:" +
              esc(s.object_position || "50% 50%") +
              '"><img class="ab-img-overlay__img" src="' +
              esc(s.image) +
              '" alt="" style="object-fit:cover;object-position:' +
              esc(s.object_position || "50% 50%") +
              '"/><span class="ab-img-overlay__mask"></span><span class="ab-img-overlay__content">' +
              (s.title
                ? '<span class="ab-img-overlay__title">' + esc(s.title) + "</span>"
                : "") +
              (s.text ? '<span class="ab-img-overlay__text">' + esc(s.text) + "</span>" : "") +
              "</span></div>"
          : '<div class="ab-img-overlay ab-empty">Sobreposição de imagem</div>';
      case "image_carousel_pro": {
        var icUrls = parseImagesValue(s.images || "[]");
        if (!icUrls.length)
          return '<div class="ab-img-carousel ab-empty">Carrossel — abra a mídia</div>';
        var dotsEd = "";
        if (icUrls.length > 1) {
          dotsEd =
            '<div class="ab-img-carousel__dots">' +
            icUrls
              .map(function (_, i) {
                return (
                  '<span class="ab-img-carousel__dot' +
                  (i === 0 ? " is-active" : "") +
                  '"></span>'
                );
              })
              .join("") +
            "</div>";
        }
        return (
          '<div class="ab-img-carousel ab-img-carousel--editor" style="--ab-ic-h:' +
          esc(s.height || "360") +
          "px;--ab-ic-r:" +
          esc(s.radius || "12") +
          'px"><div class="ab-img-carousel__track"><figure class="ab-img-carousel__slide is-active"><img src="' +
          esc(icUrls[0]) +
          '" alt=""/></figure></div>' +
          dotsEd +
          "</div>"
        );
      }
      case "nav_menu": {
        // Prévia real dos itens (CFG.navMenus com children do PHP).
        function parseNavManual(raw) {
          var tree = [];
          var last = -1;
          String(raw || "")
            .split(/\r?\n/)
            .forEach(function (ln) {
              if (!String(ln).trim()) return;
              var child = false;
              if (/^\s*>\s*/.test(ln) || /^\s{2,}/.test(ln)) {
                child = true;
                ln = ln.replace(/^\s*>\s*/, "").replace(/^\s+/, "");
              }
              ln = ln.trim();
              var p = ln.split("|");
              var lab = (p[0] || "").trim();
              if (!lab) return;
              var node = { label: lab, url: (p[1] || "#").trim(), children: [] };
              if (child && last >= 0) {
                tree[last].children = tree[last].children || [];
                tree[last].children.push(node);
              } else {
                tree.push(node);
                last = tree.length - 1;
              }
            });
          return tree;
        }
        function renderNavTree(items) {
          var html = "";
          (items || []).forEach(function (it) {
            if (!it) return;
            var kids = Array.isArray(it.children) ? it.children : [];
            var has = kids.length > 0;
            html +=
              '<li class="ab-nav-menu__item' +
              (has ? " has-children" : "") +
              '"><span class="ab-nav-menu__link">' +
              esc(it.label || "") +
              (has
                ? '<span class="ab-nav-menu__caret" aria-hidden="true">▾</span>'
                : "") +
              "</span>";
            if (has) {
              html += '<ul class="ab-nav-menu__sub" role="menu">';
              kids.forEach(function (ch) {
                html +=
                  '<li class="ab-nav-menu__sub-item"><span class="ab-nav-menu__sub-link">' +
                  esc(ch.label || "") +
                  "</span></li>";
              });
              html += "</ul>";
            }
            html += "</li>";
          });
          return html;
        }
        var nmItems = [];
        var nmSource = s.source || "wp";
        if (nmSource === "wp" && Array.isArray(CFG.navMenus) && CFG.navMenus.length) {
          var mid = String(s.menu_id || "0");
          var picked = null;
          CFG.navMenus.forEach(function (m) {
            if (String(m.id) === mid) picked = m;
          });
          if (!picked && mid === "0") picked = CFG.navMenus[0];
          if (picked && Array.isArray(picked.items) && picked.items.length) {
            nmItems = picked.items;
          }
        }
        if (nmSource === "custom" || !nmItems.length) {
          nmItems = parseNavManual(s.items || "");
        }
        if (!nmItems.length) {
          return '<div class="ab-nav-menu ab-empty">Menu — escolha um menu do WordPress ou itens manuais</div>';
        }
        var nmLayout = s.layout || "horizontal";
        var nmMobile = s.mobile_style || "drawer";
        var nmToggle = (s.show_toggle || "1") === "1";
        var nmTLabel = s.toggle_label || "Menu";
        var nmAccent = s.accent || "#eab308";
        var nmHover = s.hover_color || nmAccent;
        var nm =
          '<nav class="ab-nav-menu ab-nav-menu--' +
          esc(nmLayout) +
          " ab-nav-menu--" +
          esc(s.style || "underline") +
          " ab-nav-menu--" +
          esc(s.align || "center") +
          " ab-nav-menu--mobile-" +
          esc(nmMobile) +
          '" style="--ab-nm-gap:' +
          esc(s.gap || "8") +
          "px;--ab-nm-accent:" +
          esc(nmAccent) +
          ";--ab-nm-hover:" +
          esc(nmHover) +
          '" data-ab-nav-menu="1" aria-label="Menu">';
        if (nmToggle && nmLayout === "horizontal" && nmMobile !== "none") {
          nm +=
            '<button type="button" class="ab-nav-menu__toggle" data-ab-nav-toggle aria-expanded="false" aria-label="' +
            esc(nmTLabel) +
            '"><span class="ab-nav-menu__burger" aria-hidden="true"></span><span class="ab-nav-menu__toggle-text">' +
            esc(nmTLabel) +
            "</span></button>";
        }
        nm += '<ul class="ab-nav-menu__list">' + renderNavTree(nmItems) + "</ul></nav>";
        return nm;
      }
      case "creative_cards": {
        var ccCols = parseInt(s.columns, 10) || 4;
        var ccGap = parseInt(s.gap, 10) || 16;
        var ccH = parseInt(s.height, 10) || 160;
        var ccR = parseInt(s.radius, 10) || 16;
        var lines = String(s.items || "").split(/\r?\n/).filter(function (ln) {
          return ln.trim();
        });
        var cardsHtml = "";
        lines.slice(0, 12).forEach(function (ln, i) {
          var p = ln.split("|");
          var kind = (p[0] || "color").toLowerCase().trim();
          if (kind === "image") {
            var imgU = (p[1] || "").trim();
            cardsHtml +=
              '<button type="button" class="ab-creative-card' +
              (i === 0 ? " is-active" : "") +
              '" data-ab-cc-idx="' +
              i +
              '"><span class="ab-creative-card__media">' +
              (imgU
                ? '<img src="' + esc(imgU) + '" alt=""/>'
                : '<span class="ab-creative-card__media-ph"></span>') +
              "</span></button>";
          } else {
            var bg = (p[1] || "#7c3aed").trim();
            var ic = (p[2] || "").trim();
            var tt = (p[3] || "").trim();
            var sub = (p[4] || "").trim();
            cardsHtml +=
              '<button type="button" class="ab-creative-card' +
              (i === 0 ? " is-active" : "") +
              '" data-ab-cc-idx="' +
              i +
              '" style="--ab-cc-bg:' +
              esc(bg) +
              '"><span class="ab-creative-card__solid" style="background:' +
              esc(bg) +
              '">' +
              (ic ? '<span class="ab-creative-card__icon">' + esc(ic) + "</span>" : "") +
              (tt ? '<span class="ab-creative-card__title">' + esc(tt) + "</span>" : "") +
              (sub ? '<span class="ab-creative-card__sub">' + esc(sub) + "</span>" : "") +
              "</span></button>";
          }
        });
        if (!cardsHtml) {
          cardsHtml = '<div class="ab-empty">Adicione itens na grade criativa</div>';
        }
        return (
          '<div class="ab-creative-cards ab-creative-cards--editor" style="--ab-cc-cols:' +
          ccCols +
          ";--ab-cc-cols-t:2;--ab-cc-cols-m:1;--ab-cc-gap:" +
          ccGap +
          "px;--ab-cc-h:" +
          ccH +
          "px;--ab-cc-r:" +
          ccR +
          'px" data-ab-creative="1">' +
          '<div class="ab-creative-cards__grid">' +
          cardsHtml +
          "</div>" +
          '<div class="ab-creative-cards__panels"><div class="ab-creative-panel is-open" style="--ab-cc-panel-bg:#7cb342">' +
          '<p class="ab-creative-panel__text">Clique em um card para carregar o conteúdo (funciona no site público).</p></div></div></div>'
        );
      }
      case "loop_grid": {
        var viewBp = state.viewBp || state.breakpoint || "desktop";
        function loopVal(key, def) {
          if (viewBp === "mobile") {
            if (s[key + "_mobile"] !== undefined && s[key + "_mobile"] !== null && s[key + "_mobile"] !== "")
              return s[key + "_mobile"];
            if (s[key + "_tablet"] !== undefined && s[key + "_tablet"] !== null && s[key + "_tablet"] !== "")
              return s[key + "_tablet"];
          } else if (viewBp === "tablet") {
            if (s[key + "_tablet"] !== undefined && s[key + "_tablet"] !== null && s[key + "_tablet"] !== "")
              return s[key + "_tablet"];
          }
          return s[key] != null && s[key] !== "" ? s[key] : def;
        }
        var cols = parseInt(loopVal("columns", s.columns || 3), 10) || 3;
        var colsT = parseInt(s.columns_tablet, 10) || 2;
        var colsM = parseInt(s.columns_mobile, 10) || 1;
        var gap = parseInt(loopVal("gap", s.gap || 24), 10) || 24;
        var gapT = parseInt(s.gap_tablet || s.gap || 24, 10) || 24;
        var gapM = parseInt(s.gap_mobile || s.gap_tablet || s.gap || 24, 10) || 24;
        var ppp = parseInt(loopVal("posts_per_page", s.posts_per_page || 6), 10) || 6;
        var ph =
          '<div class="ab-loop-card ab-loop-card--ph"><div class="ab-loop-card__media ab-loop-card__media--ph"></div><div class="ab-loop-card__body"><div class="ab-loop-ph-line"></div><div class="ab-loop-ph-line ab-loop-ph-line--lg"></div><div class="ab-loop-ph-line"></div></div></div>';
        var nPh = Math.min(6, Math.max(1, cols));
        var phs = "";
        for (var pi = 0; pi < nPh; pi++) phs += ph;
        return (
          '<div class="ab-loop-grid ab-loop-grid--editor" data-ab-loop-preview="1" data-node-loop="' +
          esc(node.id) +
          '" style="--ab-loop-cols:' +
          cols +
          ";--ab-loop-cols-t:" +
          colsT +
          ";--ab-loop-cols-m:" +
          colsM +
          ";--ab-loop-gap:" +
          gap +
          "px;--ab-loop-gap-t:" +
          gapT +
          "px;--ab-loop-gap-m:" +
          gapM +
          'px">' +
          '<div class="ab-loop-grid__banner">Loop de consulta · ' +
          esc(s.template_type || "posts") +
          " · " +
          ppp +
          " itens · " +
          cols +
          " col. (" +
          esc(viewBp) +
          ")</div>" +
          '<div class="ab-loop-grid__list" data-loop-list="1">' +
          phs +
          "</div></div>"
        );
      }
      case "popup": {
        var pImgs = parseGalleryItems(s.images || "[]");
        var pThumb = pImgs[0] ? pImgs[0].url : "";
        var pScope = s.scope || "current";
        var pScopeLab =
          pScope === "all" ? "Todas as páginas" : pScope === "home" ? "Só Home" : "Só esta página";
        return (
          '<div class="ab-popup-placeholder" data-ab-popup-ph="1">' +
          '<div class="ab-popup-placeholder__badge">Popup · ' +
          esc(pScopeLab) +
          "</div>" +
          (pThumb
            ? '<img src="' + esc(pThumb) + '" alt="" class="ab-popup-placeholder__img"/>'
            : '<p class="ab-popup-placeholder__empty">Adicione imagens no painel. O popup abre no site público.</p>') +
          '<p class="ab-popup-placeholder__hint">' +
          pImgs.length +
          " imagem(ns) · atraso " +
          esc(String(s.delay || "800")) +
          "ms · " +
          esc(s.mode || "image") +
          "</p></div>"
        );
      }
      case "comments_form": {
        // Preview mirrors public form. Button colors via CSS vars on wrapper.
        var cfVars = commentsFormCssVars(s);
        return (
          '<div class="ab-comments-form ab-comments-form--editor" data-ab-comments="1"' +
          (cfVars ? ' style="' + esc(cfVars) + '"' : "") +
          ">" +
          ((s.show_title || "1") === "1"
            ? '<h3 class="ab-comments-form__title">' + esc(s.title || "Deixe um comentário") + "</h3>"
            : "") +
          '<div class="ab-comments-form__form ab-comment-form ab-comment-form--preview">' +
          '<p class="ab-comment-form__logged-in logged-in-as">' +
          "Conectado como <strong>Visitante</strong>. " +
          '<span class="ab-comment-form__required-note">Campos obrigatórios são marcados com *</span></p>' +
          '<p class="ab-comment-form__row ab-comment-form__row--comment">' +
          '<label>Comentário <span class="required">*</span></label>' +
          '<span class="ab-comment-form__control ab-comment-form__textarea ab-comment-form__control--fake" ' +
          'data-placeholder="Escreva seu comentário…"></span></p>' +
          '<p class="ab-comment-form__row ab-comment-form__row--submit">' +
          '<button type="button" class="ab-btn ab-comment-form__submit' +
          ((s.btn_full_width || "0") === "1" ? " is-full" : "") +
          '" disabled>' +
          esc(s.submit_label || "Publicar comentário") +
          "</button></p></div></div>"
        );
      }
      case "button": {
        var bStyle = leafTextStyle(node, "");
        // Button face: solid or gradient
        var btnExtra = "";
        var btnGrad = resolveBgGradient(st);
        if (btnGrad) {
          var btnStops = parseGradStops(st.bg_grad_stops, st.background);
          btnExtra +=
            "background-image:" +
            btnGrad +
            " !important;background-color:" +
            btnStops[0].c +
            " !important;";
        } else if (st.background) {
          btnExtra +=
            "background-color:" +
            st.background +
            " !important;";
        }
        if (st.color) {
          btnExtra +=
            "color:" +
            st.color +
            " !important;-webkit-text-fill-color:" +
            st.color +
            " !important;";
        } else {
          // Default white face text (theme link color must not win).
          btnExtra += "color:#ffffff !important;-webkit-text-fill-color:#ffffff !important;";
        }
        return (
          '<div class="ab-btn-wrap" style="text-align:' +
          esc(s.align || "left") +
          '"><a class="ab-btn wp-element-button" href="' +
          esc(s.url || "#") +
          '" onclick="return false" style="' +
          esc(btnExtra) +
          '"><span contenteditable="true" data-field="text" style="' +
          esc(bStyle) +
          '">' +
          esc(plainText(s.text || "Botão")) +
          "</span></a></div>"
        );
      }
      case "spacer":
        return '<div class="ab-spacer" style="height:' + (parseInt(s.height, 10) || 40) + 'px"></div>';
      case "divider":
        return '<hr class="ab-divider"/>';
      case "html":
        return (
          '<div class="ab-html" style="' +
          esc(leafTextStyle(node, "")) +
          '">' +
          (s.code || "") +
          "</div>"
        );
      default:
        return "";
    }
  }

  function layersHtml(id, depth) {
    var node = getNode(id);
    if (!node) return "";
    depth = depth || 0;
    var extra = "";
    if (!(node.settings && node.settings.display_name)) {
      if (node.settings && node.settings.text) {
        extra = " — " + plainText(node.settings.text).slice(0, 28);
      } else if (node.type === "container" && node.settings && node.settings.width) {
        extra = " (" + node.settings.width + ")";
      } else if (node.type === "section" && node.settings && node.settings.direction === "row") {
        extra = " · colunas";
      }
    }
    var childCount = (node.children || []).length;
    // Show nest count on containers so hierarchy is obvious even at a glance.
    if (
      (node.type === "container" || node.type === "section") &&
      childCount > 0 &&
      extra.indexOf("·") < 0
    ) {
      extra += " · " + childCount + (childCount === 1 ? " item" : " itens");
    }
    var label = nodeDisplayName(node) + extra;
    var multi = isIdSelected(id);
    var active =
      multi && getSelectedIds().length > 1
        ? " is-active is-multi"
        : state.selectedId === id
          ? " is-active"
          : "";
    // Dynamic indent — never cap (old depth-3 max made Image look like sibling of its Container).
    var pad = 8 + depth * 14;
    var branch =
      depth > 0
        ? '<span class="ab-layer-branch" aria-hidden="true">└</span>'
        : '<span class="ab-layer-branch ab-layer-branch--root" aria-hidden="true"></span>';
    var isParent = node.type === "root" || node.type === "section" || node.type === "container";
    var html =
      '<li class="depth-' +
      depth +
      (isParent ? " is-parent" : " is-leaf") +
      active +
      '" data-node-id="' +
      esc(id) +
      '" data-depth="' +
      depth +
      '" style="padding-left:' +
      pad +
      'px" draggable="' +
      (node.type !== "root" ? "true" : "false") +
      '" title="Arraste para reordenar · Ctrl/⌘+clique multi-seleção">' +
      '<span class="ab-layer-handle">⠿</span>' +
      branch +
      '<span class="ab-layer-label">' +
      esc(label) +
      "</span>" +
      "</li>";
    (node.children || []).forEach(function (cid) {
      html += layersHtml(cid, depth + 1);
    });
    return html;
  }

  function bindLayersDrag(layersEl) {
    if (!layersEl || layersEl._abDragBound) return;
    layersEl._abDragBound = true;
    var dragId = null;
    layersEl.addEventListener("dragstart", function (e) {
      var li = e.target.closest("li[data-node-id]");
      if (!li || li.getAttribute("draggable") !== "true") {
        e.preventDefault();
        return;
      }
      dragId = li.getAttribute("data-node-id");
      e.dataTransfer.setData("text/plain", dragId);
      e.dataTransfer.effectAllowed = "move";
      li.classList.add("is-dragging");
    });
    layersEl.addEventListener("dragend", function () {
      layersEl.querySelectorAll(".is-dragging, .ab-layer-drop").forEach(function (el) {
        el.classList.remove("is-dragging", "ab-layer-drop");
      });
      dragId = null;
    });
    layersEl.addEventListener("dragover", function (e) {
      e.preventDefault();
      var li = e.target.closest("li[data-node-id]");
      layersEl.querySelectorAll(".ab-layer-drop").forEach(function (el) {
        el.classList.remove("ab-layer-drop");
      });
      if (li && li.getAttribute("data-node-id") !== dragId) {
        li.classList.add("ab-layer-drop");
      }
    });
    layersEl.addEventListener("drop", function (e) {
      e.preventDefault();
      var targetLi = e.target.closest("li[data-node-id]");
      var fromId = dragId || e.dataTransfer.getData("text/plain");
      if (!targetLi || !fromId) return;
      var toId = targetLi.getAttribute("data-node-id");
      if (!toId || toId === fromId) return;
      var from = getNode(fromId);
      var to = getNode(toId);
      if (!from || !to || from.type === "root") return;

      // Drop on container/section → append inside; else reorder as sibling.
      execute(function () {
        if (to.type === "container" || to.type === "section" || to.type === "root") {
          moveNode(fromId, toId, (to.children || []).length);
        } else {
          var parentId = to.parentId;
          if (!parentId) return;
          var parent = getNode(parentId);
          var idx = (parent.children || []).indexOf(toId);
          moveNode(fromId, parentId, idx);
        }
        state.selectedId = fromId;
      });
    });
  }

  function sourceLabel(src) {
    if (!src) return "import";
    return String(src)
      .replace(/elementor/g, "Elementor")
      .replace(/post_content/g, "HTML da página")
      .replace(/aura_legacy/g, "Aura antigo")
      .replace(/\+/g, " + ");
  }

  function ensureDocSettings() {
    if (!state.document) return;
    if (!state.document.settings) state.document.settings = {};
    if (typeof state.document.settings.showHeader === "undefined") {
      state.document.settings.showHeader = true;
    }
    if (typeof state.document.settings.showFooter === "undefined") {
      state.document.settings.showFooter = true;
    }
    if (!state.document.settings.breakpoints || typeof state.document.settings.breakpoints !== "object") {
      state.document.settings.breakpoints = { mobile: 767, tablet: 1024, laptop: 1400 };
    } else {
      var b = state.document.settings.breakpoints;
      if (!b.mobile) b.mobile = 767;
      if (!b.tablet) b.tablet = 1024;
      if (!b.laptop) b.laptop = 1400;
    }
    var ds = state.document.settings;
    if (typeof ds.showHeader === "undefined") ds.showHeader = true;
    if (typeof ds.showFooter === "undefined") ds.showFooter = true;
    if (typeof ds.is_home_page === "undefined") ds.is_home_page = false;
    if (typeof ds.site_bg === "undefined") ds.site_bg = "";
    if (typeof ds.site_bg_mode === "undefined") ds.site_bg_mode = "color";
    if (typeof ds.site_bg_image === "undefined") ds.site_bg_image = "";
    if (typeof ds.site_bg_images === "undefined") ds.site_bg_images = "[]";
    if (typeof ds.site_bg_size === "undefined") ds.site_bg_size = "cover";
    if (typeof ds.site_bg_size_pct === "undefined") ds.site_bg_size_pct = "100";
    if (typeof ds.site_bg_position === "undefined") ds.site_bg_position = "center center";
    if (typeof ds.site_bg_repeat === "undefined") ds.site_bg_repeat = "no-repeat";
    if (typeof ds.site_bg_carousel_speed === "undefined") ds.site_bg_carousel_speed = 5000;
    if (typeof ds.site_bg_carousel_fx === "undefined") ds.site_bg_carousel_fx = "fade";
  }

  /** Resolve CSS background-size from settings (cover/contain/%). */
  function resolveSiteBgSize(settings) {
    settings = settings || {};
    var size = String(settings.site_bg_size || "cover");
    if (size === "percent" || size === "custom") {
      var pct = parseInt(settings.site_bg_size_pct, 10);
      if (isNaN(pct) || pct < 1) pct = 100;
      if (pct > 300) pct = 300;
      return pct + "%";
    }
    if (size === "percent_xy") {
      var px = parseInt(settings.site_bg_size_pct_x, 10) || 100;
      var py = parseInt(settings.site_bg_size_pct_y, 10) || 100;
      return px + "% " + py + "%";
    }
    if (/^[\d.]+%\s*([\d.]+%|auto)?$/.test(size) || size === "cover" || size === "contain" || size === "auto" || size === "100% 100%") {
      return size;
    }
    return "cover";
  }

  /** Apply page background preview on editor canvas (#ab-page-body). */
  function applyPageBackgroundPreview() {
    ensureDocSettings();
    var body = document.getElementById("ab-page-body");
    if (!body) return;
    var s = state.document.settings;
    var mode = s.site_bg_mode || "color";
    var color = (s.site_bg || "") + "";
    body.style.removeProperty("background-image");
    body.style.removeProperty("background-size");
    body.style.removeProperty("background-position");
    body.style.removeProperty("background-repeat");
    if (color) body.style.setProperty("background-color", color, "important");
    else body.style.removeProperty("background-color");

    // Remove previous carousel layer
    var oldCar = body.querySelector(".ab-site-bg-carousel");
    if (oldCar) oldCar.remove();

    var size = resolveSiteBgSize(s);
    var pos = s.site_bg_position || "center center";
    var rep = s.site_bg_repeat || "no-repeat";

    if (mode === "image" && s.site_bg_image) {
      body.style.setProperty("background-image", cssUrl(s.site_bg_image), "important");
      body.style.setProperty("background-size", size, "important");
      body.style.setProperty("background-position", pos, "important");
      body.style.setProperty("background-repeat", rep, "important");
    } else if (mode === "carousel") {
      var urls = parseGalleryItems(s.site_bg_images || "[]")
        .map(function (it) {
          return it.url;
        })
        .filter(Boolean);
      if (urls.length) {
        var car = document.createElement("div");
        car.className = "ab-site-bg-carousel ab-site-bg-carousel--editor";
        car.setAttribute("data-ab-site-bg-carousel", "1");
        car.setAttribute("data-speed", String(s.site_bg_carousel_speed || 5000));
        car.setAttribute("data-fx", s.site_bg_carousel_fx || "fade");
        car.setAttribute("data-size", size);
        car.setAttribute("data-pos", pos);
        var slides = urls
          .map(function (u, i) {
            return (
              '<div class="ab-site-bg-carousel__slide' +
              (i === 0 ? " is-active" : "") +
              '" style="background-image:url(\'' +
              String(u).replace(/'/g, "\\'") +
              "');background-size:" +
              size +
              ";background-position:" +
              pos +
              ';background-repeat:no-repeat"></div>'
            );
          })
          .join("");
        car.innerHTML = slides;
        body.insertBefore(car, body.firstChild);
        // simple editor auto-rotate
        if (urls.length > 1) {
          var idx = 0;
          if (body._abBgTimer) clearInterval(body._abBgTimer);
          body._abBgTimer = setInterval(function () {
            var slidesEl = car.querySelectorAll(".ab-site-bg-carousel__slide");
            if (!slidesEl.length) return;
            idx = (idx + 1) % slidesEl.length;
            slidesEl.forEach(function (sl, j) {
              sl.classList.toggle("is-active", j === idx);
            });
          }, Math.max(1500, parseInt(s.site_bg_carousel_speed, 10) || 5000));
        }
      }
    } else if (body._abBgTimer) {
      clearInterval(body._abBgTimer);
      body._abBgTimer = null;
    }
  }

  /**
   * Build a mini HTML document with theme styles so header/footer
   * look like the public site (isolated from wp-admin CSS).
   * @param {string} bodyHtml
   * @param {string} [viewBp] desktop | tablet | mobile — match page canvas device.
   */
  function buildChromeDocument(bodyHtml, viewBp) {
    viewBp = viewBp || state.viewBp || "desktop";
    var isMobile = viewBp === "mobile";
    var isTablet = viewBp === "tablet";
    var isNarrow = isMobile || isTablet;
    var styles = (state.theme && state.theme.styles) || { links: [], inline: "" };
    var home = (state.theme && state.theme.homeUrl) || "/";
    var links = (styles.links || [])
      .map(function (u) {
        return '<link rel="stylesheet" href="' + String(u).replace(/"/g, "&quot;") + '" />';
      })
      .join("");
    var inline = styles.inline || "";
    var vpW = isMobile ? 390 : isTablet ? 768 : 1400;
    var bodyClass =
      "ab-chrome-frame wp-embed-responsive" +
      (isMobile ? " ab-chrome-frame--mobile" : isTablet ? " ab-chrome-frame--tablet" : " ab-force-desktop-nav");

    // Shared base layout CSS
    var layoutCss =
      "html,body{margin:0!important;padding:0!important;overflow-x:hidden!important;box-sizing:border-box;}" +
      "*,*:before,*:after{box-sizing:border-box;}" +
      ".wp-site-blocks{padding:0;width:100%;max-width:100%;}" +
      ".ab-document{width:100%;box-sizing:border-box;display:flow-root;}" +
      ".ab-node{position:relative;box-sizing:border-box;float:none!important;max-width:100%;min-width:0;}" +
      ".ab-type-section.ab-section--grid{display:grid!important;align-items:center!important;width:100%!important;}" +
      ".ab-type-section.ab-section--grid>.ab-type-container{clear:none!important;float:none!important;min-width:0;max-width:100%;}" +
      ".ab-nav-menu{width:100%;max-width:100%;position:relative;z-index:40;}" +
      ".ab-nav-menu__sub-item,.ab-nav-menu__sub>li{list-style:none!important;display:block!important;margin:0!important;padding:0!important;}" +
      ".ab-nav-menu__sub-item::marker,.ab-nav-menu__sub>li::marker{content:none!important;}" +
      ".ab-nav-menu__sub-link{display:block!important;padding:11px 16px!important;color:#1e293b!important;text-decoration:none!important;white-space:nowrap;}" +
      ".ab-theme-builder-header img,.ab-theme-builder-header .ab-image img{max-width:100%!important;height:auto!important;}" +
      ".ab-theme-builder-header .ab-type-image,.ab-theme-builder-header .ab-image{max-width:100%!important;}";

    var modeCss;
    var scriptJs;
    if (isMobile) {
      // Mobile: stack logo (full, not cropped) + Menu toggle — match public site.
      modeCss =
        "html,body{min-width:0!important;width:100%!important;height:auto!important;overflow:visible!important;}" +
        ".wp-site-blocks{min-width:0!important;overflow:visible!important;}" +
        ".ab-theme-builder-header,.ab-theme-builder-header .ab-document," +
        ".ab-theme-builder-header .ab-type-root,.ab-theme-builder-header .ab-type-section," +
        ".ab-theme-builder-header .ab-type-container{overflow:visible!important;height:auto!important;min-height:0!important;max-height:none!important;}" +
        ".ab-type-section.ab-section--grid{grid-template-columns:1fr!important;gap:12px!important;padding:12px 16px!important;align-items:start!important;}" +
        ".ab-theme-builder-header .ab-type-section.ab-section--grid>.ab-type-container," +
        ".ab-theme-builder-footer .ab-type-section.ab-section--grid>.ab-type-container," +
        ".ab-type-section.ab-section--grid>.ab-type-container{grid-column:1/-1!important;width:100%!important;max-width:100%!important;height:auto!important;}" +
        ".ab-theme-builder-header .ab-type-section.ab-section--row," +
        ".ab-theme-builder-header .ab-type-section[style*='flex-direction:row']{" +
        "display:flex!important;flex-direction:column!important;align-items:stretch!important;gap:12px!important;padding:12px 16px!important;}" +
        ".ab-theme-builder-header .ab-type-section>.ab-type-container," +
        ".ab-theme-builder-header .ab-type-section>.ab-type-nav_menu," +
        ".ab-theme-builder-header .ab-type-section>.ab-type-dynamic{width:100%!important;max-width:100%!important;}" +
        /* Logo: never crop with fixed height / object-fit cover (ab-has-h from desktop styles) */
        ".ab-theme-builder-header .ab-type-image," +
        ".ab-theme-builder-header .ab-type-dynamic," +
        ".ab-theme-builder-header .ab-dynamic," +
        ".ab-theme-builder-header .ab-image," +
        ".ab-theme-builder-header .custom-logo-link{" +
        "display:block!important;width:auto!important;max-width:100%!important;height:auto!important;" +
        "min-height:0!important;max-height:none!important;overflow:visible!important;line-height:0!important;}" +
        ".ab-theme-builder-header img," +
        ".ab-theme-builder-header .custom-logo," +
        ".ab-theme-builder-header .ab-image img," +
        ".ab-theme-builder-header .ab-type-image img," +
        ".ab-theme-builder-header .ab-type-image.ab-has-h img," +
        ".ab-theme-builder-header .ab-type-image.ab-has-h>.ab-image," +
        ".ab-theme-builder-header .ab-type-image.ab-has-h>.ab-image img{" +
        "display:block!important;width:auto!important;max-width:min(220px,72vw)!important;" +
        "height:auto!important;max-height:120px!important;object-fit:contain!important;" +
        "object-position:left center!important;}" +
        /* Menu toggle — same look as public site */
        ".ab-nav-menu{width:100%!important;}" +
        ".ab-nav-menu--mobile-drawer .ab-nav-menu__toggle," +
        ".ab-nav-menu .ab-nav-menu__toggle{" +
        "display:inline-flex!important;align-items:center;gap:8px;visibility:visible!important;opacity:1!important;" +
        "border:1px solid #e2e8f0;background:#fff;border-radius:8px;padding:10px 14px;" +
        "font:600 13px system-ui,sans-serif;cursor:pointer;color:#0f172a;" +
        "box-shadow:0 1px 2px rgba(15,23,42,.06);}" +
        ".ab-nav-menu__burger{width:18px;height:2px;background:currentColor;" +
        "box-shadow:0 5px 0 currentColor,0 -5px 0 currentColor;flex-shrink:0;}" +
        ".ab-nav-menu--mobile-drawer .ab-nav-menu__list," +
        ".ab-nav-menu--horizontal.ab-nav-menu--mobile-drawer .ab-nav-menu__list," +
        ".ab-nav-menu__list{display:none!important;}" +
        ".ab-nav-menu.is-open .ab-nav-menu__list," +
        ".ab-nav-menu--mobile-drawer.is-open .ab-nav-menu__list{" +
        "display:flex!important;flex-direction:column!important;flex-wrap:nowrap!important;align-items:stretch!important;" +
        "position:relative!important;width:100%!important;max-width:100%!important;margin:8px 0 0!important;padding:10px!important;" +
        "list-style:none!important;background:#fff!important;border:1px solid #e2e8f0!important;border-radius:12px!important;" +
        "box-shadow:0 8px 24px rgba(15,23,42,.12)!important;gap:4px!important;}" +
        ".ab-nav-menu.is-open .ab-nav-menu__item,.ab-nav-menu--mobile-drawer.is-open .ab-nav-menu__item{width:100%!important;display:block!important;}" +
        ".ab-nav-menu.is-open .ab-nav-menu__link,.ab-nav-menu--mobile-drawer.is-open .ab-nav-menu__link{" +
        "display:block!important;width:100%!important;padding:12px 14px!important;white-space:normal!important;}" +
        ".ab-nav-menu__sub{position:static!important;opacity:1!important;visibility:visible!important;pointer-events:auto!important;" +
        "box-shadow:none!important;border:0!important;padding:0 0 4px 12px!important;background:transparent!important;transform:none!important;}" +
        ".wp-block-navigation__responsive-container:not(.is-menu-open){display:none!important;}" +
        ".wp-block-navigation__responsive-container-open{display:flex!important;}" +
        ".wp-block-navigation__responsive-container-close{display:none!important;}";

      scriptJs =
        "function measure(){" +
        "var b=document.body;var root=document.querySelector('.wp-site-blocks')||b;" +
        "var header=document.querySelector('.ab-theme-builder-header')||root;" +
        /* Prefer content box over body so empty white space is not counted */
        "var height=Math.ceil(Math.max(" +
        "header.getBoundingClientRect().height||0," +
        "header.scrollHeight||0," +
        "root.scrollHeight||0," +
        "b.scrollHeight||0," +
        "160));" +
        "parent.postMessage({type:'ab-chrome-height',id:window.name,height:height},'*');" +
        "}" +
        "function setupMobile(){" +
        "document.querySelectorAll('.ab-nav-menu').forEach(function(nav){" +
        "nav.classList.remove('is-open');" +
        "var tog=nav.querySelector('.ab-nav-menu__toggle');" +
        "if(tog){tog.style.display='inline-flex';tog.setAttribute('aria-expanded','false');" +
        "tog.onclick=function(e){e.preventDefault();e.stopPropagation();" +
        "var open=!nav.classList.contains('is-open');nav.classList.toggle('is-open',open);" +
        "tog.setAttribute('aria-expanded',open?'true':'false');setTimeout(measure,30);};}" +
        "});" +
        /* Strip fixed heights on logo nodes that crop the image */
        "document.querySelectorAll('.ab-theme-builder-header .ab-type-image,.ab-theme-builder-header .ab-type-dynamic," +
        ".ab-theme-builder-header .ab-image,.ab-theme-builder-header img').forEach(function(el){" +
        "el.style.setProperty('height','auto','important');" +
        "el.style.setProperty('max-height','120px','important');" +
        "el.style.setProperty('overflow','visible','important');" +
        "if(el.tagName==='IMG'){el.style.setProperty('object-fit','contain','important');" +
        "el.style.setProperty('width','auto','important');}" +
        "});" +
        "measure();" +
        "}" +
        "setupMobile();" +
        "window.addEventListener('load',function(){setupMobile();setTimeout(measure,50);setTimeout(measure,200);setTimeout(measure,600);});" +
        "document.querySelectorAll('img').forEach(function(img){" +
        "if(!img.complete){img.addEventListener('load',function(){setTimeout(measure,20);});}" +
        "else{setTimeout(measure,20);}" +
        "});" +
        "if(window.ResizeObserver){try{new ResizeObserver(function(){measure();}).observe(document.body);}catch(e){}}";
    } else if (isTablet) {
      // Tablet: full width, horizontal menu that wraps — no 1100px min-width.
      modeCss =
        "html,body{min-width:0!important;width:100%!important;}" +
        ".wp-site-blocks{min-width:0!important;}" +
        ".ab-type-section.ab-section--grid{grid-template-columns:repeat(12,minmax(0,1fr))!important;}" +
        ".ab-theme-builder-header .ab-type-section.ab-section--grid>.ab-type-container:first-child:nth-last-child(2){grid-column:span 4;}" +
        ".ab-theme-builder-header .ab-type-section.ab-section--grid>.ab-type-container:last-child:nth-child(2){grid-column:span 8;}" +
        ".ab-theme-builder-footer .ab-type-section.ab-section--grid>.ab-type-container{grid-column:span 4;}" +
        ".ab-nav-menu__toggle{display:none!important;}" +
        ".ab-nav-menu--horizontal .ab-nav-menu__list,.ab-nav-menu--mobile-drawer .ab-nav-menu__list," +
        ".ab-nav-menu__list{display:flex!important;flex-direction:row!important;flex-wrap:wrap!important;" +
        "align-items:center!important;position:static!important;width:auto!important;max-width:100%!important;" +
        "background:transparent!important;border:0!important;box-shadow:none!important;padding:0!important;list-style:none!important;gap:6px!important;}" +
        ".ab-nav-menu__item{width:auto!important;flex:0 0 auto!important;position:relative!important;}" +
        ".ab-nav-menu__link{display:inline-flex!important;width:auto!important;white-space:nowrap;font-size:13px!important;padding:6px 8px!important;}" +
        ".ab-nav-menu__sub{list-style:none!important;position:absolute!important;top:calc(100% + 4px)!important;left:0!important;" +
        "min-width:180px!important;padding:6px 0!important;margin:0!important;background:#fff!important;border:1px solid #e8edf3!important;" +
        "border-radius:10px!important;box-shadow:0 12px 32px rgba(15,23,42,.14)!important;z-index:300!important;" +
        "opacity:0!important;visibility:hidden!important;pointer-events:none!important;}" +
        ".ab-nav-menu__item.has-children:hover>.ab-nav-menu__sub,.ab-nav-menu__item.has-children.is-open>.ab-nav-menu__sub{" +
        "opacity:1!important;visibility:visible!important;pointer-events:auto!important;}" +
        ".wp-block-navigation__responsive-container-content,.wp-block-navigation__container,.wp-block-page-list{" +
        "display:flex!important;flex-direction:row!important;flex-wrap:wrap!important;align-items:center!important;}" +
        ".wp-block-navigation-item{display:inline-flex!important;width:auto!important;}";

      scriptJs =
        "function measure(){" +
        "var b=document.body;var root=document.querySelector('.wp-site-blocks')||b;" +
        "var height=Math.ceil(Math.max(root.scrollHeight,root.offsetHeight,b.scrollHeight,48));" +
        "parent.postMessage({type:'ab-chrome-height',id:window.name,height:height},'*');" +
        "}" +
        "function setup(){" +
        "document.querySelectorAll('.ab-nav-menu__toggle').forEach(function(el){el.style.display='none';});" +
        "document.querySelectorAll('.ab-nav-menu').forEach(function(nav){nav.classList.remove('is-open');});" +
        "measure();" +
        "}" +
        "setup();window.addEventListener('load',function(){setup();setTimeout(measure,80);setTimeout(measure,300);});" +
        "if(window.ResizeObserver){try{new ResizeObserver(measure).observe(document.body);}catch(e){}}";
    } else {
      // Desktop: wide preview, horizontal nav (legacy force-desktop).
      modeCss =
        "html,body{min-width:1100px!important;}" +
        ".wp-site-blocks{min-width:1100px;}" +
        ".ab-type-section.ab-section--grid{grid-template-columns:repeat(12,minmax(0,1fr))!important;}" +
        ".ab-theme-builder-header .ab-type-section.ab-section--grid>.ab-type-container:first-child:nth-last-child(2){grid-column:span 3;}" +
        ".ab-theme-builder-header .ab-type-section.ab-section--grid>.ab-type-container:last-child:nth-child(2){grid-column:span 9;}" +
        ".ab-theme-builder-footer .ab-type-section.ab-section--grid>.ab-type-container{grid-column:span 4;}" +
        ".ab-nav-menu__toggle{display:none!important;}" +
        ".ab-nav-menu--horizontal .ab-nav-menu__list,.ab-nav-menu--mobile-drawer .ab-nav-menu__list{" +
        "display:flex!important;flex-direction:row!important;flex-wrap:wrap!important;" +
        "position:static!important;width:auto!important;max-width:100%!important;" +
        "background:transparent!important;border:0!important;box-shadow:none!important;padding:0!important;list-style:none!important;}" +
        ".ab-nav-menu--horizontal .ab-nav-menu__item,.ab-nav-menu__item{width:auto!important;flex:0 0 auto!important;position:relative!important;list-style:none!important;}" +
        ".ab-nav-menu__sub,.ab-nav-menu--mobile-drawer .ab-nav-menu__sub{" +
        "list-style:none!important;position:absolute!important;top:calc(100% + 6px)!important;left:0!important;" +
        "min-width:220px!important;padding:6px 0!important;margin:0!important;" +
        "background:#fff!important;border:1px solid #e8edf3!important;border-radius:10px!important;" +
        "box-shadow:0 12px 32px rgba(15,23,42,.14)!important;z-index:300!important;" +
        "opacity:0!important;visibility:hidden!important;pointer-events:none!important;transform:translateY(6px);}" +
        ".ab-nav-menu__item.has-children:hover>.ab-nav-menu__sub," +
        ".ab-nav-menu__item.has-children.is-open>.ab-nav-menu__sub{" +
        "opacity:1!important;visibility:visible!important;pointer-events:auto!important;transform:none!important;}" +
        ".ab-theme-builder-header,.ab-theme-builder-header .ab-type-section," +
        ".ab-theme-builder-header .ab-type-container,.ab-theme-builder-header .ab-type-nav_menu{overflow:visible!important;}" +
        ".wp-block-navigation__responsive-container-content,.wp-block-navigation__container,.wp-block-page-list{" +
        "display:flex!important;flex-direction:row!important;flex-wrap:wrap!important;align-items:center!important;}" +
        ".wp-block-navigation-item{display:inline-flex!important;width:auto!important;}";

      scriptJs =
        "function measure(){" +
        "var b=document.body;var root=document.querySelector('.wp-site-blocks')||b;" +
        "var height=Math.ceil(Math.max(root.scrollHeight,root.offsetHeight,b.scrollHeight,40));" +
        "parent.postMessage({type:'ab-chrome-height',id:window.name,height:height},'*');" +
        "}" +
        "function forceNav(){" +
        "document.querySelectorAll('.wp-block-navigation__responsive-container').forEach(function(el){" +
        "el.classList.add('is-menu-open','has-modal-open');el.classList.remove('hidden-by-default');" +
        "el.setAttribute('aria-hidden','false');" +
        "el.style.cssText='display:block!important;position:static!important;width:auto!important;height:auto!important;max-height:none!important;opacity:1!important;visibility:visible!important;overflow:visible!important;padding:0!important;background:transparent!important;inset:auto!important;';" +
        "});" +
        "document.querySelectorAll('.wp-block-navigation__responsive-container-open,.wp-block-navigation__responsive-container-close').forEach(function(el){el.style.display='none';});" +
        "document.querySelectorAll('.wp-block-navigation__responsive-container-content').forEach(function(el){" +
        "el.style.cssText='display:flex!important;flex-direction:row!important;flex-wrap:wrap!important;align-items:center!important;justify-content:flex-end!important;padding:0!important;width:auto!important;';" +
        "});" +
        "document.querySelectorAll('.wp-block-navigation__container,.wp-block-page-list').forEach(function(el){" +
        "el.style.cssText='display:flex!important;flex-direction:row!important;flex-wrap:wrap!important;align-items:center!important;gap:0.35rem 1.1rem!important;list-style:none!important;margin:0!important;padding:0!important;visibility:visible!important;opacity:1!important;';" +
        "});" +
        "document.querySelectorAll('.wp-block-navigation-item,.wp-block-pages-list-item').forEach(function(el){" +
        "el.style.cssText='display:inline-flex!important;width:auto!important;flex-direction:row!important;';" +
        "});" +
        "document.querySelectorAll('.ab-nav-menu__toggle').forEach(function(el){el.style.display='none';});" +
        "document.querySelectorAll('.ab-nav-menu').forEach(function(nav){nav.classList.remove('is-open');});" +
        "document.querySelectorAll('.ab-nav-menu__list').forEach(function(el){" +
        "el.style.cssText='display:flex!important;flex-direction:row!important;flex-wrap:wrap!important;align-items:center!important;gap:8px!important;list-style:none!important;margin:0!important;padding:0!important;position:static!important;width:auto!important;max-width:100%!important;background:transparent!important;border:0!important;box-shadow:none!important;visibility:visible!important;opacity:1!important;';" +
        "});" +
        "document.querySelectorAll('.ab-nav-menu__item').forEach(function(el){" +
        "el.style.cssText='display:block!important;width:auto!important;flex:0 0 auto!important;position:relative!important;list-style:none!important;';" +
        "});" +
        "document.querySelectorAll('.ab-nav-menu__link').forEach(function(el){" +
        "el.style.cssText=(el.getAttribute('style')||'')+';display:inline-flex!important;width:auto!important;white-space:nowrap!important;';" +
        "});" +
        "document.querySelectorAll('.ab-nav-menu__sub').forEach(function(el){" +
        "el.style.cssText='list-style:none!important;position:absolute!important;top:calc(100% + 6px)!important;left:0!important;min-width:220px!important;padding:6px 0!important;margin:0!important;background:#fff!important;border:1px solid #e8edf3!important;border-radius:10px!important;box-shadow:0 12px 32px rgba(15,23,42,.14)!important;z-index:300!important;opacity:0!important;visibility:hidden!important;pointer-events:none!important;';" +
        "});" +
        "document.querySelectorAll('.ab-nav-menu__item.has-children').forEach(function(li){" +
        "li.addEventListener('mouseenter',function(){var s=li.querySelector(':scope > .ab-nav-menu__sub');if(s){s.style.opacity='1';s.style.visibility='visible';s.style.pointerEvents='auto';}});" +
        "li.addEventListener('mouseleave',function(){var s=li.querySelector(':scope > .ab-nav-menu__sub');if(s){s.style.opacity='0';s.style.visibility='hidden';s.style.pointerEvents='none';}});" +
        "});" +
        "measure();" +
        "}" +
        "forceNav();" +
        "window.addEventListener('load',function(){forceNav();setTimeout(forceNav,80);setTimeout(forceNav,300);setTimeout(forceNav,800);});" +
        "if(window.ResizeObserver){try{new ResizeObserver(measure).observe(document.body);}catch(e){}}";
    }

    return (
      "<!DOCTYPE html><html><head><meta charset=\"utf-8\"/>" +
      '<meta name="viewport" content="width=' +
      vpW +
      ', initial-scale=1"/>' +
      '<base href="' +
      String(home).replace(/"/g, "&quot;") +
      '"/>' +
      links +
      "<style>" +
      inline +
      layoutCss +
      modeCss +
      "</style></head>" +
      '<body class="' +
      bodyClass +
      '">' +
      '<div class="wp-site-blocks">' +
      (bodyHtml || "") +
      "</div>" +
      "<script>(function(){" +
      scriptJs +
      "})();</script></body></html>"
    );
  }

  function mountThemeIframe(iframeId, html) {
    var iframe = document.getElementById(iframeId);
    if (!iframe) return;
    iframe.name = iframeId;
    iframe.setAttribute("sandbox", "allow-same-origin allow-scripts");
    // Match page canvas device (mobile must not force 1100px desktop layout).
    var viewBp = state.viewBp || "desktop";
    iframe.setAttribute("data-chrome-bp", viewBp);
    var docHtml = buildChromeDocument(html || "", viewBp);
    iframe.onload = null;
    try {
      iframe.src = "about:blank";
      var done = false;
      function writeDoc() {
        if (done) return;
        try {
          var doc = iframe.contentDocument;
          if (!doc) return;
          done = true;
          doc.open();
          doc.write(docHtml);
          doc.close();
        } catch (e) {
          try {
            iframe.srcdoc = docHtml;
          } catch (e2) {}
        }
      }
      // about:blank may load sync or async
      setTimeout(writeDoc, 0);
      iframe.onload = writeDoc;
    } catch (err) {
      try {
        iframe.srcdoc = docHtml;
      } catch (e3) {}
    }
  }

  // Auto-resize chrome iframes from postMessage.
  window.addEventListener("message", function (e) {
    var data = e.data;
    if (!data || data.type !== "ab-chrome-height" || !data.id) return;
    var iframe = document.getElementById(data.id);
    if (!iframe) return;
    var viewBp = state.viewBp || "desktop";
    var raw = parseInt(data.height, 10) || 80;
    // Mobile: need full logo (~100px) + Menu button + padding — never clip below ~160.
    var minH =
      viewBp === "mobile"
        ? data.id.indexOf("header") >= 0
          ? 160
          : 80
        : viewBp === "tablet"
          ? 90
          : 48;
    var maxH = viewBp === "mobile" ? 520 : viewBp === "tablet" ? 480 : 800;
    var h = Math.max(minH, Math.min(maxH, raw));
    // Only grow unless content shrank a lot (avoids flicker while images load).
    var cur = parseInt(iframe.style.height, 10) || 0;
    if (cur && h < cur - 8 && h < minH + 20) {
      /* keep current until a stable smaller measure */
    }
    iframe.style.height = h + "px";
  });

  function showHeader() {
    ensureDocSettings();
    return !!(state.document && state.document.settings.showHeader);
  }

  function showFooter() {
    ensureDocSettings();
    return !!(state.document && state.document.settings.showFooter);
  }

  function setChromeFlag(key, value) {
    ensureDocSettings();
    pushHistory();
    state.document.settings[key] = !!value;
    markDirty();
    syncChromeVisibility();
    syncTopToggles();
    paintProps();
  }

  /** URL do Theme Builder (header/footer), com return à página ou Single. */
  function themeEditUrl(mode) {
    var url =
      "admin.php?page=aura-builder&mode=" + encodeURIComponent(mode || "header");
    if (editorMode === "single") {
      url += "&return=single";
    } else if (postId) {
      url += "&return=" + encodeURIComponent(String(postId));
    }
    return url;
  }

  /** True when canvas shows site header/footer chrome (page or Single). */
  function showsSiteChrome() {
    return !isThemePart || editorMode === "single";
  }

  /** Abre o editor de Header/Footer a partir da página ou Single. */
  function openThemePartEditor(mode) {
    // Don't nest: already editing header/footer itself.
    if (editorMode === "header" || editorMode === "footer") return;
    if (state.dirty) {
      if (
        !confirm(
          "Há alterações não salvas. Continuar para editar o " +
            (mode === "footer" ? "footer" : "header") +
            "? (salve antes se quiser manter as alterações)"
        )
      ) {
        return;
      }
    }
    window.location.href = themeEditUrl(mode);
  }

  function bindChromeEditButtons() {
    if (!showsSiteChrome()) return;
    document.querySelectorAll("[data-ab-edit-chrome]").forEach(function (btn) {
      btn.addEventListener("click", function (e) {
        e.preventDefault();
        e.stopPropagation();
        openThemePartEditor(btn.getAttribute("data-ab-edit-chrome") || "header");
      });
    });
  }

  /** Hambúrguer no canvas do editor (mobile view). */
  function bindEditorNavToggles() {
    var canvas = document.getElementById("ab-canvas");
    if (!canvas || canvas._abNavToggleBound) return;
    canvas._abNavToggleBound = true;
    canvas.addEventListener("click", function (e) {
      var btn = e.target.closest("[data-ab-nav-toggle]");
      if (btn) {
        e.preventDefault();
        e.stopPropagation();
        var nav = btn.closest("[data-ab-nav-menu]");
        if (!nav) return;
        canvas.querySelectorAll("[data-ab-nav-menu].is-open").forEach(function (other) {
          if (other === nav) return;
          other.classList.remove("is-open");
          var ot = other.querySelector("[data-ab-nav-toggle]");
          if (ot) ot.setAttribute("aria-expanded", "false");
        });
        var open = nav.classList.toggle("is-open");
        btn.setAttribute("aria-expanded", open ? "true" : "false");
        return;
      }
      if (!e.target.closest("[data-ab-nav-menu]")) {
        canvas.querySelectorAll("[data-ab-nav-menu].is-open").forEach(function (nav) {
          nav.classList.remove("is-open");
          var t = nav.querySelector("[data-ab-nav-toggle]");
          if (t) t.setAttribute("aria-expanded", "false");
        });
      }
    });
  }

  function syncChromeVisibility() {
    var h = document.getElementById("ab-theme-header");
    var f = document.getElementById("ab-theme-footer");
    if (h) h.classList.toggle("ab-theme-chrome--off", !showHeader());
    if (f) f.classList.toggle("ab-theme-chrome--off", !showFooter());
  }

  function syncTopToggles() {
    var th = document.getElementById("ab-toggle-header");
    var tf = document.getElementById("ab-toggle-footer");
    if (th) {
      th.checked = showHeader();
      th.closest(".ab-toggle") &&
        th.closest(".ab-toggle").classList.toggle("is-on", showHeader());
    }
    if (tf) {
      tf.checked = showFooter();
      tf.closest(".ab-toggle") &&
        tf.closest(".ab-toggle").classList.toggle("is-on", showFooter());
    }
  }

  /** Full rebuild (structure change). Avoid during text editing. */
  function paint() {
    var canvas = document.getElementById("ab-canvas");
    if (!canvas || !state.document) return;
    ensureDocSettings();

    var banner = "";
    if (state.imported && state.source && state.source !== "saved") {
      banner =
        '<div class="ab-import-banner" style="margin:16px 20px 0"><span>Conteúdo importado de <strong>' +
        esc(sourceLabel(state.source)) +
        "</strong>. Salve para gravar no Aura Builder.</span>" +
        '<button type="button" id="ab-reimport">Reimportar</button></div>';
    }

    var headerOff = showHeader() ? "" : " ab-theme-chrome--off";
    var footerOff = showFooter() ? "" : " ab-theme-chrome--off";
    // Page + Single: show chrome + edit buttons (like Home). Header/Footer editors: no chrome.
    var useChrome = showsSiteChrome();
    var chromeEditH = useChrome
      ? '<button type="button" class="ab-theme-chrome-edit" data-ab-edit-chrome="header" title="Editar header no Theme Builder">✎ Editar header</button>'
      : "";
    var chromeEditF = useChrome
      ? '<button type="button" class="ab-theme-chrome-edit" data-ab-edit-chrome="footer" title="Editar footer no Theme Builder">✎ Editar footer</button>'
      : "";

    canvas.innerHTML =
      '<div class="ab-page-preview">' +
      (useChrome
        ? '<div id="ab-theme-header" class="ab-theme-chrome ab-theme-chrome--header' +
          headerOff +
          '"><span class="ab-theme-chrome-label">Header</span>' +
          chromeEditH +
          '<iframe id="ab-iframe-header" class="ab-theme-iframe" title="Header do tema" scrolling="no"></iframe>' +
          "</div>"
        : "") +
      banner +
      '<div class="ab-page-body ab-theme-surface entry-content wp-block-post-content' +
      (isThemePart && editorMode !== "single" ? " ab-page-body--theme-part" : "") +
      '" id="ab-page-body">' +
      renderNode(state.document.rootId) +
      "</div>" +
      (useChrome
        ? '<div id="ab-theme-footer" class="ab-theme-chrome ab-theme-chrome--footer' +
          footerOff +
          '"><span class="ab-theme-chrome-label">Footer</span>' +
          chromeEditF +
          '<iframe id="ab-iframe-footer" class="ab-theme-iframe" title="Footer do tema" scrolling="no"></iframe>' +
          "</div>"
        : "") +
      "</div>";

    // Load real theme CSS inside isolated iframes (matches front).
    if (useChrome) {
      mountThemeIframe("ab-iframe-header", (state.theme && state.theme.header) || "");
      mountThemeIframe("ab-iframe-footer", (state.theme && state.theme.footer) || "");
      bindChromeEditButtons();
    }
    bindEditorNavToggles();

    // Site background from Site Settings (color / image / carousel)
    applyPageBackgroundPreview();

    // Re-apply section/container backgrounds via CSSOM (belt-and-suspenders after
    // HTML style="..." parse — protects bg_image URLs with query strings).
    // Buttons: face (.ab-btn) only — never paint the full-width wrapper.
    var bodyEl = document.getElementById("ab-page-body");
    if (bodyEl && state.document && state.document.nodes) {
      bodyEl.querySelectorAll("[data-node-id]").forEach(function (el) {
        var nid = el.getAttribute("data-node-id");
        var n = getNode(nid);
        if (!n) return;
        var stn = stylesFor(n, "view");
        if (n.type === "button") {
          el.style.setProperty("background", "transparent", "important");
          el.style.setProperty("background-color", "transparent", "important");
          el.style.setProperty("background-image", "none", "important");
          el.style.setProperty("border", "0", "important");
          el.style.setProperty("padding", "0", "important");
          var face = el.querySelector(".ab-btn");
          if (face) {
            applyNodeBackground(face, stn);
            if (stn.color) {
              face.style.setProperty("color", stn.color, "important");
            }
            if (stn.padding) face.style.setProperty("padding", stn.padding, "important");
            if (stn.radius) face.style.setProperty("border-radius", stn.radius, "important");
            if (stn.border) face.style.setProperty("border", stn.border, "important");
          }
        } else if (stn.bg_image || stn.background || resolveBgGradient(stn)) {
          applyNodeBackground(el, stn);
        }
        // Align media content with node box (gallery/image height, handles).
        syncMediaFill(el, n, stn);
      });
    }

    var layers = document.getElementById("ab-layers");
    if (layers) {
      layers.innerHTML = layersHtml(state.document.rootId, 0);
      bindLayersDrag(layers);
    }
    paintProps();
    syncTopToggles();
    if (state.leftPanel === "settings") paintSiteSettings();
    hydrateLoopGrids();
    // Contadores: anima no canvas (efeito de números subindo).
    setTimeout(function () {
      runCounterAnimations();
    }, 80);
    // Gallery slider (same behaviour as front-widgets.js).
    setTimeout(function () {
      initGallerySliders(bodyEl || document.getElementById("ab-page-body"));
    }, 40);
    syncBreakpointUI();
    var t = document.getElementById("ab-title");
    if (t) t.textContent = state.title || "Página #" + postId;

    // Responsive canvas width (view only)
    applyCanvasView();

    var re = document.getElementById("ab-reimport");
    if (re) {
      re.addEventListener("click", function () {
        if (
          !confirm(
            "Reimportar descarta alterações não salvas e lê de novo Elementor + HTML da página. Continuar?"
          )
        ) {
          return;
        }
        api("/documents/" + postId + "/import", { method: "POST", body: { force: true } })
          .then(function (res) {
            state.document = res.document;
            ensureDocSettings();
            state.imported = true;
            state.source = res.source || "import";
            state.dirty = true;
            setStatus("Reimportado — salve para gravar", "is-dirty");
            paint();
          })
          .catch(function (err) {
            setStatus("Reimport falhou: " + err.message, "is-err");
          });
      });
    }
  }

  /** Normalize multi-select list (primary = selectedId). */
  function getSelectedIds() {
    var ids = Array.isArray(state.selectedIds) ? state.selectedIds.slice() : [];
    if (!ids.length && state.selectedId) ids = [state.selectedId];
    // Drop missing / root
    ids = ids.filter(function (id) {
      var n = getNode(id);
      return n && n.type !== "root";
    });
    return ids;
  }

  function isIdSelected(id) {
    if (!id) return false;
    if (state.selectedId === id) return true;
    return (state.selectedIds || []).indexOf(id) >= 0;
  }

  /** Selection chrome only — keeps contenteditable focus/caret. */
  function updateSelectionUI() {
    var canvas = document.getElementById("ab-canvas");
    if (!canvas) return;

    canvas.querySelectorAll(".ab-node.is-selected, .ab-node.is-multi-selected").forEach(function (el) {
      el.classList.remove("is-selected");
      el.classList.remove("is-multi-selected");
    });
    canvas.querySelectorAll(".ab-overlay-chrome, .ab-resize-handle, .ab-multi-bar").forEach(function (el) {
      el.remove();
    });
    // Also remove floating multi bar on body
    var oldBar = document.getElementById("ab-multi-bar");
    if (oldBar) oldBar.remove();

    var ids = getSelectedIds();
    if (!ids.length) {
      state.selectedId = null;
      state.selectedIds = [];
      paintProps();
      refreshLayersActive();
      return;
    }

    // Sync primary id
    if (!state.selectedId || ids.indexOf(state.selectedId) < 0) {
      state.selectedId = ids[ids.length - 1];
    }
    state.selectedIds = ids;

    ids.forEach(function (sid) {
      var el = canvas.querySelector('[data-node-id="' + sid + '"]');
      var node = getNode(sid);
      if (!el || !node || node.type === "root") return;
      if (ids.length > 1) {
        el.classList.add("is-multi-selected");
        if (sid === state.selectedId) el.classList.add("is-selected");
      } else {
        el.classList.add("is-selected");
        el.insertAdjacentHTML("afterbegin", chromeHtml(node));
      }
    });

    if (ids.length > 1) {
      renderMultiSelectBar(ids);
    }
    paintProps();
    refreshLayersActive();
  }

  function renderMultiSelectBar(ids) {
    ids = ids || getSelectedIds();
    var old = document.getElementById("ab-multi-bar");
    if (old) old.remove();
    if (ids.length < 2) return;
    var bar = document.createElement("div");
    bar.id = "ab-multi-bar";
    bar.className = "ab-multi-bar";
    bar.innerHTML =
      '<span class="ab-multi-bar__count">' +
      ids.length +
      " selecionados</span>" +
      '<button type="button" class="ab-btn ab-btn--primary ab-multi-bar__btn" data-multi-act="row" title="Coloca os elementos na mesma linha (container horizontal)">' +
      "⟷ Mesma linha</button>" +
      '<button type="button" class="ab-btn ab-multi-bar__btn" data-multi-act="columns" title="Cria colunas iguais na grade (pai seção) ou em linha">' +
      "▥ Em colunas</button>" +
      '<button type="button" class="ab-btn ab-multi-bar__btn" data-multi-act="wrap" title="Envolve a seleção num container">' +
      "▢ Em container</button>" +
      '<button type="button" class="ab-btn ab-btn--ghost ab-multi-bar__btn" data-multi-act="clear">Limpar</button>' +
      '<span class="ab-multi-bar__hint">Ctrl/⌘+clique para adicionar · Esc limpa</span>';
    document.body.appendChild(bar);
    bar.querySelectorAll("[data-multi-act]").forEach(function (btn) {
      btn.addEventListener("click", function (e) {
        e.preventDefault();
        e.stopPropagation();
        var act = btn.getAttribute("data-multi-act");
        if (act === "clear") {
          state.selectedIds = state.selectedId ? [state.selectedId] : [];
          updateSelectionUI();
          return;
        }
        if (act === "row") {
          execute(function () {
            wrapSelectionInRow(ids);
          });
        }
        if (act === "columns") {
          execute(function () {
            wrapSelectionInColumns(ids);
          });
        }
        if (act === "wrap") {
          execute(function () {
            wrapSelectionInContainer(ids);
          });
        }
      });
    });
  }

  /**
   * Document order among siblings (or flat tree order if different parents).
   * @param {string[]} ids
   * @return {{parentId:string|null, ordered:string[]}|null}
   */
  function orderedSiblingsSelection(ids) {
    ids = (ids || []).filter(function (id) {
      return !!getNode(id);
    });
    if (ids.length < 2) return null;
    var parents = {};
    ids.forEach(function (id) {
      var n = getNode(id);
      var p = n && n.parentId ? n.parentId : "__none__";
      parents[p] = true;
    });
    var parentKeys = Object.keys(parents);
    if (parentKeys.length !== 1 || parentKeys[0] === "__none__") {
      return { parentId: null, ordered: ids, multiParent: true };
    }
    var parentId = parentKeys[0];
    var parent = getNode(parentId);
    var ordered = [];
    (parent.children || []).forEach(function (cid) {
      if (ids.indexOf(cid) >= 0) ordered.push(cid);
    });
    // Include any missing (shouldn't happen)
    ids.forEach(function (id) {
      if (ordered.indexOf(id) < 0) ordered.push(id);
    });
    return { parentId: parentId, ordered: ordered, multiParent: false };
  }

  /**
   * Wrap 1+ selected siblings in a new container (column by default).
   * Use to gain layout controls (align, gap, row) around image/text/button.
   */
  function wrapSelectionInContainer(ids) {
    ids = (ids || getSelectedIds()).filter(function (id) {
      var n = getNode(id);
      return n && n.type !== "root" && n.type !== "section";
    });
    if (!ids.length) {
      setStatus("Selecione um ou mais elementos (texto, imagem, botão…).", "is-err");
      return;
    }

    var parentId = null;
    var ordered = [];
    if (ids.length === 1) {
      var only = getNode(ids[0]);
      if (!only || !only.parentId) {
        setStatus("Não foi possível envolver este elemento.", "is-err");
        return;
      }
      parentId = only.parentId;
      ordered = [ids[0]];
    } else {
      var info = orderedSiblingsSelection(ids);
      if (!info || info.multiParent) {
        setStatus("Os itens precisam estar no mesmo container/seção.", "is-err");
        return;
      }
      parentId = info.parentId;
      ordered = info.ordered;
    }

    var parent = getNode(parentId);
    if (!parent) return;
    var firstIdx = (parent.children || []).indexOf(ordered[0]);
    if (firstIdx < 0) firstIdx = (parent.children || []).length;

    var wrapId = uid();
    var n = ordered.length;
    var child0 = getNode(ordered[0]);
    var label =
      n === 1 && child0
        ? "Container · " + (TYPE_LABELS[child0.type] || child0.type)
        : "Container (" + n + ")";

    state.document.nodes[wrapId] = {
      id: wrapId,
      type: "container",
      parentId: parentId,
      children: [],
      settings: {
        display: "flex",
        direction: "column",
        gap: "8px",
        width: "100%",
        justify_content: "flex-start",
        align_items: "stretch",
        col_span: parent.type === "section" ? 12 : (parent.settings && parent.settings.col_span) || 12,
        display_name: label,
      },
      styles: {
        desktop: { height: "auto", min_height: "0" },
        tablet: {},
        mobile: {},
      },
      attributes: {},
      classes: [],
      locked: false,
      hidden: false,
    };

    parent.children = (parent.children || []).filter(function (cid) {
      return ordered.indexOf(cid) < 0;
    });
    ordered.forEach(function (cid) {
      var child = getNode(cid);
      if (!child) return;
      child.parentId = wrapId;
      state.document.nodes[wrapId].children.push(cid);
    });
    parent.children.splice(Math.min(firstIdx, parent.children.length), 0, wrapId);

    state.selectedId = wrapId;
    state.selectedIds = [wrapId];
    setStatus(
      n === 1
        ? "Elemento envolvido em container. Use alinhamento / linha no painel."
        : n + " elementos no novo container.",
      ""
    );
  }

  /** Put 2+ selected nodes into a horizontal flex row container (only those selected). */
  function wrapSelectionInRow(ids) {
    var info = orderedSiblingsSelection(ids || getSelectedIds());
    if (!info || info.ordered.length < 2) {
      setStatus("Selecione 2 ou mais elementos (Ctrl/⌘+clique em cada um).", "is-err");
      return;
    }
    if (info.multiParent) {
      setStatus(
        "Os " +
          info.ordered.length +
          " itens precisam estar no mesmo pai. Clique só nos botões/widgets (não no container).",
        "is-err"
      );
      return;
    }
    var parent = getNode(info.parentId);
    if (!parent) return;
    var ordered = info.ordered;
    var firstIdx = (parent.children || []).indexOf(ordered[0]);
    if (firstIdx < 0) firstIdx = (parent.children || []).length;

    var rowId = uid();
    var n = ordered.length;
    state.document.nodes[rowId] = {
      id: rowId,
      type: "container",
      parentId: info.parentId,
      children: [],
      settings: {
        display: "flex",
        direction: "row",
        gap: "12px",
        width: "100%",
        justify_content: "flex-start",
        align_items: "center",
        col_span: parent.type === "section" ? 12 : parent.settings && parent.settings.col_span || 12,
        display_name: "Linha (" + n + ")",
      },
      // No min_height / height — row must hug the buttons, not stretch.
      styles: {
        desktop: { height: "auto", min_height: "0" },
        tablet: {},
        mobile: {},
      },
      attributes: {},
      classes: [],
      locked: false,
      hidden: false,
    };

    // Remove ONLY selected from parent (siblings stay); attach selected to row
    parent.children = (parent.children || []).filter(function (cid) {
      return ordered.indexOf(cid) < 0;
    });
    ordered.forEach(function (cid) {
      var child = getNode(cid);
      if (!child) return;
      child.parentId = rowId;
      child.settings = child.settings || {};
      if (child.type === "container") {
        // keep container children as-is; flex share space
        child.settings.width = child.settings.width || "auto";
      } else {
        // Leaves (button, text…): shrink-wrap in the row
        child.settings.width = "auto";
        normalizeNodeStyles(child);
        child.styles.desktop = child.styles.desktop || {};
        child.styles.desktop.width = "auto";
        child.styles.desktop.max_width = "100%";
      }
      state.document.nodes[rowId].children.push(cid);
    });
    parent.children.splice(Math.min(firstIdx, parent.children.length), 0, rowId);

    state.selectedId = rowId;
    state.selectedIds = [rowId];
    setStatus(n + " itens na mesma linha (resto do container intacto).", "");
  }

  /**
   * Put selection as equal columns:
   * - Parent section → grid + column containers
   * - Else → row container with column wrappers
   */
  function wrapSelectionInColumns(ids) {
    var info = orderedSiblingsSelection(ids || getSelectedIds());
    if (!info || info.ordered.length < 2) {
      setStatus("Selecione 2 ou mais elementos (Ctrl/⌘+clique).", "is-err");
      return;
    }
    if (info.multiParent) {
      setStatus("Os elementos precisam estar no mesmo container/seção.", "is-err");
      return;
    }
    var parent = getNode(info.parentId);
    if (!parent) return;
    var ordered = info.ordered;
    var n = ordered.length;
    var span = Math.max(1, Math.floor(12 / n));
    var lastSpan = 12 - span * (n - 1);
    var firstIdx = (parent.children || []).indexOf(ordered[0]);
    if (firstIdx < 0) firstIdx = (parent.children || []).length;

    // Detach selected
    parent.children = (parent.children || []).filter(function (cid) {
      return ordered.indexOf(cid) < 0;
    });

    var newColIds = [];
    ordered.forEach(function (cid, i) {
      var sp = i === n - 1 ? lastSpan : span;
      var colId = createColumnContainer(info.parentId, i + 1, sp);
      var col = getNode(colId);
      var child = getNode(cid);
      if (child && col) {
        child.parentId = colId;
        col.children = [cid];
      }
      newColIds.push(colId);
    });

    if (parent.type === "section") {
      parent.settings = parent.settings || {};
      parent.settings.direction = "grid";
      parent.settings.columns = n;
      if (!parent.settings.gap) parent.settings.gap = "16px";
    } else {
      // Non-section parent: wrap columns in a row container
      var rowId = uid();
      state.document.nodes[rowId] = {
        id: rowId,
        type: "container",
        parentId: info.parentId,
        children: newColIds.slice(),
        settings: {
          display: "flex",
          direction: "row",
          gap: "16px",
          width: "100%",
          col_span: 12,
          justify_content: "flex-start",
          align_items: "stretch",
          display_name: "Colunas (" + n + ")",
        },
        styles: { desktop: {}, tablet: {}, mobile: {} },
        attributes: {},
        classes: [],
        locked: false,
        hidden: false,
      };
      newColIds.forEach(function (cid) {
        var c = getNode(cid);
        if (c) {
          c.parentId = rowId;
          // Equal flex columns: keep col_span; width % is for grid only.
          c.settings = c.settings || {};
          c.settings.width = spanToWidthPct(c.settings.col_span || 12);
        }
      });
      parent.children.splice(Math.min(firstIdx, parent.children.length), 0, rowId);
      state.selectedId = rowId;
      state.selectedIds = [rowId];
      setStatus(n + " colunas criadas.", "");
      return;
    }

    // Insert columns into section at first index
    newColIds.forEach(function (cid, i) {
      parent.children.splice(Math.min(firstIdx + i, parent.children.length), 0, cid);
    });
    state.selectedId = newColIds[0] || null;
    state.selectedIds = newColIds.slice();
    setStatus(n + " colunas na grade.", "");
  }

  /** Convert a container: column ↔ row, or distribute children as columns. */
  function convertContainerLayout(id, mode) {
    var node = getNode(id);
    if (!node || node.type !== "container") {
      setStatus("Selecione um container.", "is-err");
      return;
    }
    node.settings = node.settings || {};
    var kids = (node.children || []).slice();

    if (mode === "row") {
      node.settings.direction = "row";
      node.settings.display = "flex";
      if (!node.settings.gap) node.settings.gap = "16px";
      if (!node.settings.justify_content) node.settings.justify_content = "flex-start";
      if (!node.settings.align_items) node.settings.align_items = "center";
      // Equal flex widths for children
      var n = kids.length || 1;
      var span = Math.max(1, Math.floor(12 / n));
      kids.forEach(function (cid, i) {
        var ch = getNode(cid);
        if (!ch) return;
        ch.settings = ch.settings || {};
        var sp = i === n - 1 ? 12 - span * (n - 1) : span;
        if (ch.type === "container") {
          ch.settings.col_span = sp;
          ch.settings.width = spanToWidthPct(sp);
        } else {
          ch.settings.width = "auto";
          normalizeNodeStyles(ch);
          ch.styles.desktop = ch.styles.desktop || {};
          ch.styles.desktop.width = "auto";
        }
      });
      if (!node.settings.display_name || /coluna/i.test(node.settings.display_name)) {
        node.settings.display_name = "Linha";
      }
      setStatus("Container em linha — use Principal → Alinhar itens da linha.", "");
      return;
    }

    if (mode === "column") {
      node.settings.direction = "column";
      node.settings.display = "flex";
      kids.forEach(function (cid) {
        var ch = getNode(cid);
        if (!ch) return;
        ch.settings = ch.settings || {};
        ch.settings.width = "100%";
        if (ch.type === "container") {
          ch.settings.col_span = 12;
        }
        normalizeNodeStyles(ch);
        if (ch.styles && ch.styles.desktop) {
          delete ch.styles.desktop.width;
        }
      });
      if (!node.settings.display_name || /linha/i.test(node.settings.display_name)) {
        node.settings.display_name = "Container";
      }
      setStatus("Container empilhado (vertical).", "");
      return;
    }

    if (mode === "columns") {
      // Each direct child becomes its own column container inside this node (row).
      if (kids.length < 2) {
        setStatus("O container precisa de 2+ itens para colunas.", "is-err");
        return;
      }
      node.settings.direction = "row";
      node.settings.display = "flex";
      if (!node.settings.gap) node.settings.gap = "16px";
      var nn = kids.length;
      var sp0 = Math.max(1, Math.floor(12 / nn));
      var newKids = [];
      kids.forEach(function (cid, i) {
        var ch = getNode(cid);
        if (!ch) return;
        // Already a column-like container with single purpose → keep
        if (ch.type === "container" && (ch.children || []).length) {
          ch.settings = ch.settings || {};
          ch.settings.direction = "column";
          ch.settings.col_span = i === nn - 1 ? 12 - sp0 * (nn - 1) : sp0;
          ch.settings.width = spanToWidthPct(ch.settings.col_span);
          newKids.push(cid);
          return;
        }
        var colId = createColumnContainer(id, i + 1, i === nn - 1 ? 12 - sp0 * (nn - 1) : sp0);
        var col = getNode(colId);
        if (col && ch) {
          ch.parentId = colId;
          col.children = [cid];
          col.parentId = id;
          newKids.push(colId);
        }
      });
      node.children = newKids;
      node.settings.display_name = "Colunas (" + nn + ")";
      setStatus(nn + " colunas no container.", "");
    }
  }

  function refreshLayersActive() {
    var layers = document.getElementById("ab-layers");
    if (!layers) return;
    var ids = getSelectedIds();
    layers.querySelectorAll("li").forEach(function (li) {
      var id = li.getAttribute("data-node-id");
      var on = ids.indexOf(id) >= 0;
      li.classList.toggle("is-active", on);
      li.classList.toggle("is-multi", on && ids.length > 1);
    });
  }

  function selectNode(id, opts) {
    opts = opts || {};
    var multi = !!(opts.multi || opts.toggle);

    if (!id) {
      state.selectedId = null;
      state.selectedIds = [];
      updateSelectionUI();
      return;
    }

    var n = getNode(id);
    if (!n || n.type === "root") {
      return;
    }

    if (multi) {
      var ids = getSelectedIds();
      var idx = ids.indexOf(id);
      if (idx >= 0) {
        ids.splice(idx, 1);
      } else {
        ids.push(id);
      }
      state.selectedIds = ids;
      state.selectedId = ids.length ? ids[ids.length - 1] : null;
      updateSelectionUI();
      return;
    }

    // Single select
    var same = state.selectedId === id && getSelectedIds().length <= 1;
    state.selectedId = id;
    state.selectedIds = [id];
    // Seção / coluna (container): abrir aba Camadas e destacar o item.
    if (n && (n.type === "section" || n.type === "container") && !opts.skipLayers) {
      if (state.leftPanel !== "layers") {
        setLeftPanel("layers");
      }
      var layers = document.getElementById("ab-layers");
      if (layers && !layers.querySelector("li") && state.document) {
        layers.innerHTML = layersHtml(state.document.rootId, 0);
        bindLayersDrag(layers);
      }
      refreshLayersActive();
      requestAnimationFrame(function () {
        var li = document.querySelector('#ab-layers li[data-node-id="' + id + '"]');
        if (li) {
          li.classList.add("is-active");
          if (li.scrollIntoView) {
            li.scrollIntoView({ block: "nearest", behavior: "smooth" });
          }
        }
      });
    }
    if (!same) {
      updateSelectionUI();
    } else {
      refreshLayersActive();
      paintProps();
    }
    if (opts.focusEdit && id && getSelectedIds().length <= 1) {
      requestAnimationFrame(function () {
        var nodeEl = canvasNodeEl(id);
        var field = nodeEl ? nodeEl.querySelector('[contenteditable="true"]') : null;
        if (field) {
          field.focus();
        }
      });
    }
  }

  function isEditingText(e) {
    var t = e && e.target;
    if (!t) {
      t = document.activeElement;
    }
    if (!t) return false;
    if (t.nodeType === 3) t = t.parentElement;
    if (!t) return false;
    if (t.isContentEditable) return true;
    if (t.closest && t.closest('[contenteditable="true"]')) return true;
    var tag = t.tagName;
    if (tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT") return true;
    return false;
  }

  /** Single active device badge (not clickable — avoids clutter and prop thrashing). */
  function deviceBadgeHtml(bp) {
    bp = bp || state.breakpoint || "desktop";
    var icon = bp === "tablet" ? "tablet" : bp === "mobile" ? "mobile" : "desktop";
    return (
      '<span class="ab-dev-badge is-on" title="' +
      esc(BP_LABEL[bp] || bp) +
      ' (estilos)">' +
      svgIcon(icon) +
      "</span>"
    );
  }

  /** Clickable style-breakpoint switcher (only in Estilo section). */
  function styleBreakpointRowHtml() {
    var active = state.breakpoint || "desktop";
    return (
      '<div class="ab-style-bp-row">' +
      '<span class="ab-style-bp-label">Editando estilos:</span>' +
      '<button type="button" class="ab-style-bp-btn' +
      (active === "desktop" ? " is-on" : "") +
      '" data-style-bp="desktop" title="Desktop">' +
      svgIcon("desktop") +
      "</button>" +
      '<button type="button" class="ab-style-bp-btn' +
      (active === "tablet" ? " is-on" : "") +
      '" data-style-bp="tablet" title="Tablet">' +
      svgIcon("tablet") +
      "</button>" +
      '<button type="button" class="ab-style-bp-btn' +
      (active === "mobile" ? " is-on" : "") +
      '" data-style-bp="mobile" title="Mobile">' +
      svgIcon("mobile") +
      "</button>" +
      '<strong class="ab-style-bp-name">' +
      esc(BP_LABEL[active] || active) +
      "</strong></div>"
    );
  }

  function fieldLabel(label, withDevices) {
    // Content props: no device icons. Style props: only the active style-bp badge.
    var badge = "";
    if (withDevices) {
      badge = deviceBadgeHtml(state.breakpoint);
    }
    return (
      '<div class="ab-field-label-row"><label>' +
      esc(label) +
      "</label>" +
      badge +
      "</div>"
    );
  }

  function fieldSelect(name, label, value, options, attr, withDevices) {
    attr = attr || "data-prop";
    // Only style fields show device badge (active icon only).
    if (withDevices === undefined || withDevices === null) {
      withDevices = attr === "data-style";
    }
    var opts = (options || [])
      .map(function (o) {
        var v = typeof o === "string" ? o : o.value;
        var lab = typeof o === "string" ? o : o.label;
        return (
          '<option value="' +
          esc(v) +
          '"' +
          (String(value) === String(v) ? " selected" : "") +
          ">" +
          esc(lab) +
          "</option>"
        );
      })
      .join("");
    return (
      '<div class="ab-field" data-style-field="' +
      esc(name) +
      '">' +
      fieldLabel(label, withDevices) +
      "<select " +
      attr +
      '="' +
      esc(name) +
      '">' +
      opts +
      "</select></div>"
    );
  }

  function fieldInput(name, label, value, opts) {
    opts = opts || {};
    var type = opts.type || "text";
    var attr = opts.attr || "data-prop";
    // Badge only for style fields (unless forced off).
    var withDevices =
      opts.devices === true || (opts.devices !== false && attr === "data-style" && !opts.readonly);
    var ph = opts.placeholder ? ' placeholder="' + esc(opts.placeholder) + '"' : "";
    var ro = opts.readonly ? " readonly" : "";
    var tag =
      type === "textarea"
        ? "<textarea " +
          attr +
          '="' +
          esc(name) +
          '"' +
          ph +
          ro +
          ">" +
          esc(value == null ? "" : value) +
          "</textarea>"
        : "<input type=\"" +
          esc(type) +
          '" ' +
          attr +
          '="' +
          esc(name) +
          '" value="' +
          esc(value == null ? "" : value) +
          '"' +
          ph +
          ro +
          "/>";
    return (
      '<div class="ab-field" data-style-field="' +
      esc(name) +
      '">' +
      fieldLabel(label, withDevices) +
      tag +
      "</div>"
    );
  }

  /** Image URL + WordPress media library button */
  function fieldMedia(name, label, value, attr) {
    attr = attr || "data-prop";
    var url = value == null ? "" : String(value);
    var preview = url
      ? '<div class="ab-media-preview"><img src="' + esc(url) + '" alt=""/></div>'
      : '<div class="ab-media-preview ab-media-preview--empty">Nenhuma imagem</div>';
    return (
      '<div class="ab-field ab-field-media" data-style-field="' +
      esc(name) +
      '">' +
      fieldLabel(label, false) +
      preview +
      '<div class="ab-media-row">' +
      '<input type="url" class="ab-media-url" ' +
      attr +
      '="' +
      esc(name) +
      '" value="' +
      esc(url) +
      '" placeholder="https://… ou escolha na galeria"/>' +
      '<button type="button" class="ab-btn ab-btn--primary ab-media-pick" data-media-for="' +
      esc(name) +
      '" data-media-attr="' +
      esc(attr) +
      '" title="Galeria do WordPress">' +
      svgIcon("gallery") +
      " Galeria</button>" +
      "</div>" +
      '<span class="ab-field-hint">Galeria do WordPress ou cole a URL.</span>' +
      "</div>"
    );
  }

  /**
   * WordPress media frame.
   * opts.multiple → select many images; callback receives array when multiple.
   * opts.selectedIds → pre-select attachment IDs already in the gallery.
   * opts.selectedUrls → resolve URLs to attachments when IDs are missing.
   */
  function openMediaLibrary(onSelect, opts) {
    opts = opts || {};
    if (!window.wp || !wp.media) {
      alert("Galeria do WordPress indisponível. Cole a URL da imagem.");
      return;
    }
    var multi = !!opts.multiple;
    var frame = wp.media({
      title: multi ? "Selecionar imagens" : "Selecionar imagem",
      button: { text: multi ? "Usar imagens" : "Usar esta imagem" },
      multiple: multi ? "add" : false,
      library: { type: "image" },
    });

    function attachmentToItem(att) {
      var j = att && att.toJSON ? att.toJSON() : att || {};
      return {
        url: j.url || "",
        alt: j.alt || j.title || "",
        id: j.id || 0,
        width: j.width || 0,
        height: j.height || 0,
      };
    }

    frame.on("open", function () {
      var selection = frame.state().get("selection");
      if (!selection) return;
      selection.reset();

      var ids = (opts.selectedIds || []).map(function (id) {
        return parseInt(id, 10);
      }).filter(function (id) {
        return id > 0;
      });

      // De-dupe ids
      var seen = {};
      ids = ids.filter(function (id) {
        if (seen[id]) return false;
        seen[id] = true;
        return true;
      });

      function addById(id) {
        var attachment = wp.media.attachment(id);
        if (!attachment) return;
        attachment.fetch({
          success: function () {
            selection.add(attachment);
          },
        });
        // Also add immediately if already in cache
        if (attachment.get("url")) {
          selection.add(attachment);
        }
      }

      ids.forEach(addById);

      // Resolve remaining URLs via REST (when we only stored the URL)
      var urls = (opts.selectedUrls || []).filter(Boolean);
      if (urls.length && (!ids.length || urls.length > ids.length)) {
        resolveMediaIdsByUrls(urls, function (resolvedIds) {
          resolvedIds.forEach(function (id) {
            if (!seen[id]) {
              seen[id] = true;
              addById(id);
            }
          });
        });
      }
    });

    frame.on("select", function () {
      var selection = frame.state().get("selection");
      if (!selection) return;
      if (multi) {
        var list = [];
        selection.each(function (att) {
          var item = attachmentToItem(att);
          if (item.url) list.push(item);
        });
        onSelect(list);
      } else {
        var att = selection.first();
        if (!att) return;
        onSelect(attachmentToItem(att));
      }
    });
    frame.open();
  }

  /** Best-effort: map image URLs → attachment IDs via WP REST. */
  function resolveMediaIdsByUrls(urls, done) {
    if (!urls || !urls.length) {
      done([]);
      return;
    }
    var base = (CFG.restUrl || "").replace(/aura-builder\/v1\/?$/, "").replace(/\/$/, "");
    if (!base || base.indexOf("wp-json") < 0) {
      base = window.location.origin + "/wp-json";
    } else {
      var idx = base.indexOf("/wp-json");
      base = base.slice(0, idx + "/wp-json".length);
    }
    // Query recent media and match by URL (reliable enough for gallery re-open).
    var url =
      base +
      "/wp/v2/media?per_page=100&media_type=image&_fields=id,source_url,guid,media_details";
    fetch(url, { credentials: "same-origin", headers: { "X-WP-Nonce": nonce || "" } })
      .then(function (r) {
        return r.json();
      })
      .then(function (list) {
        if (!Array.isArray(list)) {
          done([]);
          return;
        }
        var want = {};
        urls.forEach(function (u) {
          want[normalizeMediaUrl(u)] = true;
        });
        var ids = [];
        list.forEach(function (m) {
          var candidates = [
            m.source_url,
            m.guid && m.guid.rendered,
            m.media_details && m.media_details.sizes && m.media_details.sizes.full && m.media_details.sizes.full.source_url,
            m.media_details && m.media_details.sizes && m.media_details.sizes.large && m.media_details.sizes.large.source_url,
          ];
          candidates.forEach(function (c) {
            if (c && want[normalizeMediaUrl(c)] && ids.indexOf(m.id) < 0) {
              ids.push(m.id);
            }
          });
        });
        done(ids);
      })
      .catch(function () {
        done([]);
      });
  }

  function normalizeMediaUrl(u) {
    return String(u || "")
      .split("?")[0]
      .replace(/-\d+x\d+(\.[a-zA-Z]+)$/, "$1") // strip WP size suffix
      .replace(/\/$/, "")
      .toLowerCase();
  }

  /**
   * Parse gallery setting → list of {id,url}
   * Accepts: JSON string[], JSON {id,url}[], newline URLs
   */
  function parseGalleryItems(raw) {
    var out = [];
    if (raw == null || raw === "") return out;
    var s = String(raw).trim();
    if (!s) return out;
    if (s.charAt(0) === "[") {
      try {
        var dec = JSON.parse(s);
        if (Array.isArray(dec)) {
          dec.forEach(function (item) {
            if (typeof item === "string" && item) {
              out.push({ id: 0, url: item });
            } else if (item && item.url) {
              out.push({
                id: parseInt(item.id, 10) || 0,
                url: String(item.url),
              });
            }
          });
          return out;
        }
      } catch (e) {}
    }
    s.split(/[\n,]+/).forEach(function (p) {
      p = p.trim();
      if (!p) return;
      var u = p.split("|")[0].trim();
      if (u) out.push({ id: 0, url: u });
    });
    return out;
  }

  /** Parse images setting → string[] (URLs only, for render) */
  function parseImagesValue(raw) {
    return parseGalleryItems(raw)
      .map(function (it) {
        return it.url;
      })
      .filter(Boolean);
  }

  /** Serialize gallery items for settings (prefer {id,url} when id known). */
  function serializeGalleryItems(items) {
    var out = (items || []).map(function (it) {
      if (typeof it === "string") return it;
      if (it && it.id) return { id: it.id, url: it.url };
      return (it && it.url) || "";
    }).filter(function (x) {
      return x && (typeof x === "string" ? x : x.url);
    });
    return JSON.stringify(out);
  }

  /** Multi-image field: opens WP media dialog (multiple). */
  function fieldGalleryMedia(name, label, value) {
    var urls = parseImagesValue(value);
    var thumbs = urls
      .map(function (u, i) {
        return (
          '<div class="ab-gallery-thumb" data-g-idx="' +
          i +
          '">' +
          '<img src="' +
          esc(u) +
          '" alt=""/>' +
          '<button type="button" class="ab-gallery-thumb__rm" data-g-rm="' +
          i +
          '" title="Remover">×</button>' +
          "</div>"
        );
      })
      .join("");
    return (
      '<div class="ab-field ab-field-gallery-media" data-gallery-field="' +
      esc(name) +
      '">' +
      fieldLabel(label, false) +
      '<div class="ab-gallery-thumbs">' +
      (thumbs ||
        '<div class="ab-gallery-thumbs--empty">Nenhuma imagem — abra a mídia do WordPress</div>') +
      "</div>" +
      '<input type="hidden" data-prop="' +
      esc(name) +
      '" value="' +
      esc(urls.length ? JSON.stringify(urls) : "[]") +
      '"/>' +
      '<div class="ab-media-row">' +
      '<button type="button" class="ab-btn ab-btn--primary ab-gallery-media-pick" data-gallery-for="' +
      esc(name) +
      '">' +
      svgIcon("gallery") +
      " Abrir mídia</button>" +
      '<button type="button" class="ab-btn ab-btn--ghost ab-gallery-media-clear" data-gallery-clear="' +
      esc(name) +
      '"' +
      (urls.length ? "" : " disabled") +
      ">Limpar</button>" +
      "</div>" +
      '<span class="ab-field-hint">' +
      urls.length +
      " imagem(ns) · diálogo de mídias do WordPress (seleção múltipla).</span>" +
      "</div>"
    );
  }

  /** Accordion items repeater: title + content + image per item. */
  /**
   * Repair stripslashes damage on JSON \uXXXX escapes:
   * "matru00edcula" → "matrícula", "ud83dudccd" → 📍
   */
  function repairStrippedUnicode(s) {
    s = String(s == null ? "" : s);
    if (!s || (s.indexOf("u00") < 0 && !/u[dD][89abABc-fC-F]/.test(s))) return s;
    // Surrogate pairs (emoji) first
    s = s.replace(/u([dD][89abAB][0-9a-fA-F]{2})u([dD][c-fC-F][0-9a-fA-F]{2})/g, function (_, hi, lo) {
      var h = parseInt(hi, 16);
      var l = parseInt(lo, 16);
      var code = (h - 0xd800) * 0x400 + (l - 0xdc00) + 0x10000;
      try {
        return String.fromCodePoint(code);
      } catch (e) {
        return _;
      }
    });
    // BMP latin / punctuation
    s = s.replace(/u([0-9a-fA-F]{4})/g, function (m, hex) {
      var code = parseInt(hex, 16);
      if (
        (code >= 0x00a0 && code <= 0x024f) ||
        (code >= 0x1e00 && code <= 0x1eff) ||
        (code >= 0x2000 && code <= 0x206f) ||
        (code >= 0x20a0 && code <= 0x20cf) ||
        (code >= 0x2100 && code <= 0x214f) ||
        (code >= 0x2190 && code <= 0x21ff) ||
        (code >= 0x2600 && code <= 0x27bf)
      ) {
        return String.fromCharCode(code);
      }
      return m;
    });
    return s;
  }

  function parseAccordionItems(raw) {
    var out = [];
    if (raw == null || raw === "") return out;
    var s = repairStrippedUnicode(String(raw).trim());
    if (!s) return out;
    if (s.charAt(0) === "[") {
      try {
        var dec = JSON.parse(s);
        if (Array.isArray(dec)) {
          dec.forEach(function (row) {
            if (typeof row === "string") {
              var p = row.split("|");
              out.push({
                title: repairStrippedUnicode((p[0] || "").trim()),
                content: repairStrippedUnicode((p[1] || "").trim()),
                image: (p[2] || "").trim(),
              });
            } else if (row && typeof row === "object") {
              out.push({
                title: repairStrippedUnicode(String(row.title || "")),
                content: repairStrippedUnicode(String(row.content || "")),
                image: String(row.image || row.url || ""),
              });
            }
          });
          return out;
        }
      } catch (e) {}
    }
    s.split(/\r?\n/).forEach(function (ln) {
      ln = ln.trim();
      if (!ln) return;
      var p = ln.split("|");
      out.push({
        title: repairStrippedUnicode((p[0] || "").trim()),
        content: repairStrippedUnicode((p[1] || "").trim()),
        image: (p[2] || "").trim(),
      });
    });
    return out;
  }

  /** Icon list repeater: icon (library) + text per row. */
  function parseIconListItems(raw) {
    var out = [];
    if (raw == null || raw === "") return out;
    var s = repairStrippedUnicode(String(raw).trim());
    if (!s) return out;
    if (s.charAt(0) === "[") {
      try {
        var dec = JSON.parse(s);
        if (Array.isArray(dec)) {
          dec.forEach(function (row) {
            if (typeof row === "string") {
              var p = row.split("|");
              if (p.length >= 2)
                out.push({
                  icon: repairStrippedUnicode(p[0].trim()),
                  text: repairStrippedUnicode(p[1].trim()),
                });
              else out.push({ icon: "✓", text: repairStrippedUnicode(row.trim()) });
            } else if (row && typeof row === "object") {
              out.push({
                icon: repairStrippedUnicode(String(row.icon || "✓")),
                text: repairStrippedUnicode(String(row.text || row.label || "")),
              });
            }
          });
          return out;
        }
      } catch (e) {}
    }
    s.split(/\r?\n/).forEach(function (ln) {
      ln = ln.trim();
      if (!ln) return;
      if (ln.indexOf("|") >= 0) {
        var p2 = ln.split("|");
        out.push({
          icon: repairStrippedUnicode((p2[0] || "✓").trim()),
          text: repairStrippedUnicode((p2[1] || "").trim()),
        });
      } else {
        out.push({ icon: "✓", text: repairStrippedUnicode(ln) });
      }
    });
    return out;
  }

  function fieldIconListItems(name, label, value) {
    var items = parseIconListItems(value);
    if (!items.length) {
      items = [
        { icon: "✓", text: "Item um" },
        { icon: "✓", text: "Item dois" },
      ];
    }
    var rows = items
      .map(function (it, i) {
        return (
          '<div class="ab-il-item" data-il-idx="' +
          i +
          '">' +
          '<span class="ab-il-item__preview">' +
          esc(it.icon || "✓") +
          "</span>" +
          '<button type="button" class="ab-btn ab-btn--ghost ab-il-item__icon-btn" data-il-icon="' +
          i +
          '" title="Biblioteca de ícones">' +
          svgIcon("icon") +
          "</button>" +
          '<input type="hidden" class="ab-il-item__icon" data-il-field="icon" data-il-idx="' +
          i +
          '" value="' +
          esc(it.icon || "✓") +
          '"/>' +
          '<input type="text" class="ab-il-item__text" data-il-field="text" data-il-idx="' +
          i +
          '" value="' +
          esc(it.text || "") +
          '" placeholder="Texto do item"/>' +
          '<button type="button" class="ab-btn ab-btn--ghost ab-il-item__rm" data-il-rm="' +
          i +
          '" title="Remover">×</button>' +
          "</div>"
        );
      })
      .join("");
    return (
      '<div class="ab-field ab-field-icon-list-items" data-il-field="' +
      esc(name) +
      '">' +
      fieldLabel(label, false) +
      '<input type="hidden" data-prop="' +
      esc(name) +
      '" value="' +
      esc(JSON.stringify(items)) +
      '"/>' +
      '<div class="ab-il-list">' +
      rows +
      "</div>" +
      '<button type="button" class="ab-btn ab-btn--primary ab-il-add" data-il-add="' +
      esc(name) +
      '" style="width:100%;margin-top:8px">+ Adicionar item</button>' +
      '<span class="ab-field-hint">Clique no botão de ícone para abrir a biblioteca.</span>' +
      "</div>"
    );
  }

  function fieldAccordionItems(name, label, value) {
    var items = parseAccordionItems(value);
    if (!items.length) {
      items = [
        { title: "Item 1", content: "Conteúdo…", image: "" },
        { title: "Item 2", content: "Conteúdo…", image: "" },
      ];
    }
    var rows = items
      .map(function (it, i) {
        return (
          '<div class="ab-acc-item" data-acc-idx="' +
          i +
          '">' +
          '<div class="ab-acc-item__head">' +
          "<strong>Item " +
          (i + 1) +
          "</strong>" +
          '<button type="button" class="ab-btn ab-btn--ghost ab-acc-item__rm" data-acc-rm="' +
          i +
          '" title="Remover item">Remover</button>' +
          "</div>" +
          '<label class="ab-acc-item__lab">Título</label>' +
          '<input type="text" class="ab-acc-item__title" data-acc-field="title" data-acc-idx="' +
          i +
          '" value="' +
          esc(it.title) +
          '" placeholder="Título do item"/>' +
          '<label class="ab-acc-item__lab">Conteúdo</label>' +
          '<textarea class="ab-acc-item__content" data-acc-field="content" data-acc-idx="' +
          i +
          '" rows="3" placeholder="Texto do conteúdo">' +
          esc(it.content) +
          "</textarea>" +
          '<label class="ab-acc-item__lab">Imagem (opcional — galeria WP)</label>' +
          '<div class="ab-acc-item__media">' +
          (it.image
            ? '<img class="ab-acc-item__thumb" src="' + esc(it.image) + '" alt=""/>'
            : '<span class="ab-acc-item__noimg">Sem imagem</span>') +
          '<input type="hidden" class="ab-acc-item__image" data-acc-field="image" data-acc-idx="' +
          i +
          '" value="' +
          esc(it.image) +
          '"/>' +
          '<button type="button" class="ab-btn ab-btn--primary ab-acc-item__pick" data-acc-pick="' +
          i +
          '">' +
          svgIcon("gallery") +
          " Mídia</button>" +
          (it.image
            ? '<button type="button" class="ab-btn ab-btn--ghost ab-acc-item__img-clear" data-acc-img-clear="' +
              i +
              '">Limpar</button>'
            : "") +
          "</div>" +
          "</div>"
        );
      })
      .join("");

    return (
      '<div class="ab-field ab-field-accordion-items" data-acc-items-field="' +
      esc(name) +
      '">' +
      fieldLabel(label, false) +
      '<input type="hidden" data-prop="' +
      esc(name) +
      '" value="' +
      esc(JSON.stringify(items)) +
      '"/>' +
      '<div class="ab-acc-items-list">' +
      rows +
      "</div>" +
      '<button type="button" class="ab-btn ab-btn--primary ab-acc-item-add" data-acc-add="' +
      esc(name) +
      '" style="width:100%;margin-top:8px">+ Adicionar item</button>' +
      '<span class="ab-field-hint">Cada item: título, texto e imagem opcional (diálogo de mídias).</span>' +
      "</div>"
    );
  }

  var SOCIAL_NETWORK_OPTS = [
    { value: "facebook", label: "Facebook", glyph: "f", color: "#1877f2" },
    { value: "instagram", label: "Instagram", glyph: "ig", color: "#e4405f" },
    { value: "youtube", label: "YouTube", glyph: "▶", color: "#ff0000" },
    { value: "whatsapp", label: "WhatsApp", glyph: "wa", color: "#25d366" },
    { value: "linkedin", label: "LinkedIn", glyph: "in", color: "#0a66c2" },
    { value: "x", label: "X (Twitter)", glyph: "𝕏", color: "#111" },
    { value: "tiktok", label: "TikTok", glyph: "♪", color: "#010101" },
    { value: "telegram", label: "Telegram", glyph: "tg", color: "#229ed9" },
    { value: "pinterest", label: "Pinterest", glyph: "p", color: "#e60023" },
    { value: "email", label: "E-mail", glyph: "@", color: "#64748b" },
    { value: "website", label: "Site", glyph: "🌐", color: "#0f172a" },
  ];

  function parseSocialItems(raw) {
    var out = [];
    if (raw == null || raw === "") return out;
    var s = String(raw).trim();
    if (!s) return out;
    if (s.charAt(0) === "[") {
      try {
        var dec = JSON.parse(s);
        if (Array.isArray(dec)) {
          dec.forEach(function (row) {
            if (typeof row === "string") {
              var p = row.split("|");
              out.push({
                network: (p[0] || "website").toLowerCase().trim(),
                url: (p[1] || "#").trim(),
                label: "",
              });
            } else if (row && typeof row === "object") {
              out.push({
                network: String(row.network || row.name || "website").toLowerCase(),
                url: String(row.url || "#"),
                label: String(row.label || ""),
              });
            }
          });
          return out;
        }
      } catch (e) {}
    }
    s.split(/\r?\n/).forEach(function (ln) {
      ln = ln.trim();
      if (!ln) return;
      var p = ln.split("|");
      out.push({
        network: (p[0] || "website").toLowerCase().trim(),
        url: (p[1] || "#").trim(),
        label: "",
      });
    });
    return out;
  }

  function fieldSocialItems(name, label, value) {
    var items = parseSocialItems(value);
    if (!items.length) {
      items = [
        { network: "facebook", url: "https://facebook.com", label: "" },
        { network: "instagram", url: "https://instagram.com", label: "" },
      ];
    }
    var netOpts = SOCIAL_NETWORK_OPTS.map(function (n) {
      return '<option value="' + esc(n.value) + '">' + esc(n.label) + "</option>";
    }).join("");
    var rows = items
      .map(function (it, i) {
        var meta =
          SOCIAL_NETWORK_OPTS.find(function (n) {
            return n.value === it.network;
          }) || SOCIAL_NETWORK_OPTS[SOCIAL_NETWORK_OPTS.length - 1];
        var opts = SOCIAL_NETWORK_OPTS.map(function (n) {
          return (
            '<option value="' +
            esc(n.value) +
            '"' +
            (n.value === it.network ? " selected" : "") +
            ">" +
            esc(n.label) +
            "</option>"
          );
        }).join("");
        return (
          '<div class="ab-soc-item" data-soc-idx="' +
          i +
          '">' +
          '<span class="ab-soc-item__badge" style="background:' +
          esc(meta.color) +
          '">' +
          esc(meta.glyph) +
          "</span>" +
          '<select class="ab-soc-item__net" data-soc-field="network" data-soc-idx="' +
          i +
          '">' +
          opts +
          "</select>" +
          '<input type="url" class="ab-soc-item__url" data-soc-field="url" data-soc-idx="' +
          i +
          '" value="' +
          esc(it.url) +
          '" placeholder="https://…"/>' +
          '<button type="button" class="ab-btn ab-btn--ghost ab-soc-item__rm" data-soc-rm="' +
          i +
          '">×</button>' +
          "</div>"
        );
      })
      .join("");
    return (
      '<div class="ab-field ab-field-social-items" data-soc-field="' +
      esc(name) +
      '">' +
      fieldLabel(label, false) +
      '<input type="hidden" data-prop="' +
      esc(name) +
      '" value="' +
      esc(JSON.stringify(items)) +
      '"/>' +
      '<div class="ab-soc-list">' +
      rows +
      "</div>" +
      '<button type="button" class="ab-btn ab-btn--primary ab-soc-add" style="width:100%;margin-top:8px">+ Adicionar rede</button>' +
      '<span class="ab-field-hint">Escolha a rede na lista e informe o link.</span>' +
      "</div>"
    );
  }

  /** Parse Label|URL lines (with > for children) → tree. */
  function parseNavItemsRaw(raw) {
    var tree = [];
    var last = -1;
    String(raw || "")
      .split(/\r?\n/)
      .forEach(function (ln) {
        if (!String(ln).trim()) return;
        var child = false;
        if (/^\s*>\s*/.test(ln) || /^\s{2,}/.test(ln)) {
          child = true;
          ln = ln.replace(/^\s*>\s*/, "").replace(/^\s+/, "");
        }
        ln = ln.trim();
        var p = ln.split("|");
        var lab = (p[0] || "").trim();
        if (!lab) return;
        var node = { label: lab, url: (p[1] || "#").trim() || "#", children: [] };
        if (child && last >= 0) {
          tree[last].children = tree[last].children || [];
          tree[last].children.push(node);
        } else {
          tree.push(node);
          last = tree.length - 1;
        }
      });
    return tree;
  }

  /** Serialize tree → Label|URL lines (submenu with >). */
  function serializeNavItemsTree(tree) {
    var lines = [];
    (tree || []).forEach(function (it) {
      if (!it) return;
      lines.push((it.label || "Item") + "|" + (it.url || "#"));
      (it.children || []).forEach(function (ch) {
        if (!ch) return;
        lines.push("> " + (ch.label || "Subitem") + "|" + (ch.url || "#"));
      });
    });
    return lines.join("\n");
  }

  /** Swap two indices in an array (mutates). */
  function swapArrayItems(arr, a, b) {
    if (!arr || a === b || a < 0 || b < 0 || a >= arr.length || b >= arr.length) return false;
    var tmp = arr[a];
    arr[a] = arr[b];
    arr[b] = tmp;
    return true;
  }

  /**
   * Move top-level items (by index) under another top-level parent as subitems.
   * Their former children are flattened as siblings under the same parent
   * (nav format only supports 1 level of nesting).
   * Returns new tree or null if invalid.
   */
  function moveNavItemsToSubmenu(tree, selectedIdxs, parentIdx) {
    tree = tree || [];
    parentIdx = parseInt(parentIdx, 10);
    if (isNaN(parentIdx) || parentIdx < 0 || parentIdx >= tree.length) return null;
    var selected = (selectedIdxs || [])
      .map(function (i) {
        return parseInt(i, 10);
      })
      .filter(function (i) {
        return !isNaN(i) && i >= 0 && i < tree.length && i !== parentIdx;
      });
    // unique, keep order
    var seen = {};
    selected = selected.filter(function (i) {
      if (seen[i]) return false;
      seen[i] = true;
      return true;
    });
    if (!selected.length) return null;

    var moveSet = {};
    selected.forEach(function (i) {
      moveSet[i] = true;
    });

    var toMove = [];
    selected
      .slice()
      .sort(function (a, b) {
        return a - b;
      })
      .forEach(function (i) {
        var it = tree[i];
        if (!it) return;
        toMove.push({
          label: it.label || "Item",
          url: it.url || "#",
          children: [],
        });
        (it.children || []).forEach(function (ch) {
          if (!ch) return;
          toMove.push({
            label: ch.label || "Subitem",
            url: ch.url || "#",
            children: [],
          });
        });
      });

    var newTree = [];
    tree.forEach(function (it, i) {
      if (moveSet[i]) return;
      if (i === parentIdx) {
        var kids = (it.children || []).map(function (ch) {
          return {
            label: ch.label || "Subitem",
            url: ch.url || "#",
            children: [],
          };
        });
        toMove.forEach(function (m) {
          kids.push(m);
        });
        newTree.push({
          label: it.label || "Item",
          url: it.url || "#",
          children: kids,
        });
      } else {
        newTree.push(it);
      }
    });
    return newTree;
  }

  /** Resolve nav menu items for editor list (WP tree or custom raw). */
  function resolveNavMenuItemsForEdit(settings) {
    settings = settings || {};
    var source = settings.source || "wp";
    if (source === "custom") {
      return parseNavItemsRaw(settings.items || "");
    }
    // WP menu — use CFG.navMenus[].items
    var menus = Array.isArray(CFG.navMenus) ? CFG.navMenus : [];
    var mid = String(settings.menu_id || "0");
    var picked = null;
    menus.forEach(function (m) {
      if (String(m.id) === mid) picked = m;
    });
    if (!picked && mid === "0" && menus.length) picked = menus[0];
    if (picked && Array.isArray(picked.items) && picked.items.length) {
      return picked.items.map(function (it) {
        return {
          label: it.label || "",
          url: it.url || "#",
          children: (it.children || []).map(function (ch) {
            return { label: ch.label || "", url: ch.url || "#", children: [] };
          }),
        };
      });
    }
    // Fallback to stored custom string if WP empty
    return parseNavItemsRaw(settings.items || "");
  }

  /**
   * Gutenberg-like menu items editor: list of pages/links with expand, + add.
   * When source=wp, list is read-only until user clicks "Personalizar itens".
   */
  function fieldNavMenuItems(name, label, value, settings) {
    settings = settings || {};
    var source = settings.source || "wp";
    var editable = source === "custom";
    var items = resolveNavMenuItemsForEdit(settings);
    if (!items.length && editable) {
      items = [
        { label: "Início", url: "/", children: [] },
        { label: "Sobre", url: "#", children: [] },
      ];
      // Seed so the list is real (not a phantom preview).
      var seedNode = state.selectedId ? getNode(state.selectedId) : null;
      if (seedNode && seedNode.type === "nav_menu") {
        seedNode.settings = seedNode.settings || {};
        if (!String(seedNode.settings.items || "").trim()) {
          seedNode.settings.items = serializeNavItemsTree(items);
        }
      }
    }
    var openIdx = state.navMenuOpenIdx != null ? state.navMenuOpenIdx : -1;
    // Sanitize selection against current length
    var selectedSet = {};
    var selectedList = Array.isArray(state.navMenuSelected) ? state.navMenuSelected : [];
    selectedList = selectedList.filter(function (i) {
      return typeof i === "number" && i >= 0 && i < items.length;
    });
    state.navMenuSelected = selectedList;
    selectedList.forEach(function (i) {
      selectedSet[i] = true;
    });
    var selCount = selectedList.length;

    var rows = "";
    items.forEach(function (it, i) {
      var kids = Array.isArray(it.children) ? it.children : [];
      var isOpen = openIdx === i;
      var isSel = !!selectedSet[i];
      rows +=
        '<div class="ab-nav-item' +
        (isOpen ? " is-open" : "") +
        (isSel ? " is-selected" : "") +
        (editable ? "" : " is-readonly") +
        '" data-nav-idx="' +
        i +
        '">' +
        '<div class="ab-nav-item__row">' +
        (editable
          ? '<label class="ab-nav-item__check" title="Selecionar para mover">' +
            '<input type="checkbox" data-nav-sel="' +
            i +
            '"' +
            (isSel ? " checked" : "") +
            "/>" +
            '<span class="ab-check-ui" aria-hidden="true"></span>' +
            "</label>"
          : "") +
        '<button type="button" class="ab-nav-item__toggle" data-nav-toggle="' +
        i +
        '">' +
        '<span class="ab-nav-item__ico" aria-hidden="true">' +
        svgIcon("menuitem") +
        "</span>" +
        '<span class="ab-nav-item__label">' +
        esc(it.label || "Item") +
        (kids.length ? ' <span class="ab-nav-item__subn">(' + kids.length + ")</span>" : "") +
        "</span>" +
        "</button>" +
        (editable
          ? '<span class="ab-nav-item__move">' +
            '<button type="button" class="ab-nav-item__up" data-nav-up="' +
            i +
            '" title="Subir" aria-label="Subir"' +
            (i === 0 ? " disabled" : "") +
            ">▲</button>" +
            '<button type="button" class="ab-nav-item__down" data-nav-down="' +
            i +
            '" title="Descer" aria-label="Descer"' +
            (i >= items.length - 1 ? " disabled" : "") +
            ">▼</button>" +
            "</span>" +
            '<button type="button" class="ab-nav-item__rm" data-nav-rm="' +
            i +
            '" title="Remover">×</button>'
          : "") +
        "</div>";
      if (isOpen) {
        rows +=
          '<div class="ab-nav-item__edit">' +
          '<div class="ab-field"><label>Rótulo</label>' +
          '<input type="text" class="ab-nav-item__lab" data-nav-field="label" data-nav-idx="' +
          i +
          '" value="' +
          esc(it.label || "") +
          '"' +
          (editable ? "" : " readonly") +
          "/></div>" +
          '<div class="ab-field"><label>URL / link</label>' +
          '<input type="text" class="ab-nav-item__url" data-nav-field="url" data-nav-idx="' +
          i +
          '" value="' +
          esc(it.url || "#") +
          '" placeholder="https://… ou /pagina"' +
          (editable ? "" : " readonly") +
          "/></div>";
        // Sub-items
        rows +=
          '<div class="ab-nav-item__children"><div class="ab-field-hint">Subitens — use ▲ ▼ para reordenar</div>';
        kids.forEach(function (ch, ci) {
          rows +=
            '<div class="ab-nav-sub" data-nav-parent="' +
            i +
            '" data-nav-cidx="' +
            ci +
            '">' +
            '<input type="text" class="ab-nav-sub__lab" data-nav-cfield="label" data-nav-parent="' +
            i +
            '" data-nav-cidx="' +
            ci +
            '" value="' +
            esc(ch.label || "") +
            '" placeholder="Rótulo"' +
            (editable ? "" : " readonly") +
            "/>" +
            '<input type="text" class="ab-nav-sub__url" data-nav-cfield="url" data-nav-parent="' +
            i +
            '" data-nav-cidx="' +
            ci +
            '" value="' +
            esc(ch.url || "#") +
            '" placeholder="URL"' +
            (editable ? "" : " readonly") +
            "/>" +
            (editable
              ? '<span class="ab-nav-sub__move">' +
                '<button type="button" class="ab-nav-sub__up" data-nav-cup="' +
                i +
                ":" +
                ci +
                '" title="Subir"' +
                (ci === 0 ? " disabled" : "") +
                ">▲</button>" +
                '<button type="button" class="ab-nav-sub__down" data-nav-cdown="' +
                i +
                ":" +
                ci +
                '" title="Descer"' +
                (ci >= kids.length - 1 ? " disabled" : "") +
                ">▼</button>" +
                "</span>" +
                '<button type="button" class="ab-btn ab-btn--ghost ab-nav-sub__rm" data-nav-crm="' +
                i +
                ":" +
                ci +
                '">×</button>'
              : "") +
            "</div>";
        });
        if (editable) {
          rows +=
            '<button type="button" class="ab-btn ab-btn--ghost ab-nav-sub-add" data-nav-cadd="' +
            i +
            '" style="width:100%;margin-top:4px">+ Subitem</button>';
        }
        rows += "</div></div>";
      }
      rows += "</div>";
    });

    var html =
      '<div class="ab-field ab-field-nav-items" data-nav-field="' +
      esc(name) +
      '" data-nav-editable="' +
      (editable ? "1" : "0") +
      '">' +
      fieldLabel(label || "Itens do menu", false);

    if (!editable) {
      html +=
        '<div class="ab-field-hint">Itens do menu WordPress (somente leitura). Clique em <strong>Personalizar itens</strong> para editar rótulos e links aqui.</div>';
    } else {
      html +=
        '<div class="ab-field-hint">▲ ▼ reordena a posição. Marque vários e envie para submenu de outro. Clique no nome para editar.</div>';
    }

    // Multi-select bulk actions
    if (editable && items.length > 1) {
      html +=
        '<div class="ab-nav-bulk' +
        (selCount ? " is-active" : "") +
        '">' +
        '<div class="ab-nav-bulk__top">' +
        '<button type="button" class="ab-btn ab-btn--ghost ab-nav-sel-all" title="Selecionar todos">☑ Todos</button>' +
        '<button type="button" class="ab-btn ab-btn--ghost ab-nav-sel-none" title="Limpar seleção">☐ Limpar</button>' +
        '<span class="ab-nav-bulk__count">' +
        (selCount
          ? selCount + " selecionado" + (selCount > 1 ? "s" : "")
          : "Nenhum selecionado") +
        "</span>" +
        "</div>";
      if (selCount > 0) {
        var parentOpts = items
          .map(function (it, i) {
            if (selectedSet[i]) return ""; // can't be parent of itself if selected
            return (
              '<option value="' +
              i +
              '">' +
              esc(it.label || "Item " + (i + 1)) +
              "</option>"
            );
          })
          .join("");
        // If all selected, no parent options — still show message
        html +=
          '<div class="ab-nav-bulk__move">' +
          '<label class="ab-nav-bulk__lbl">Enviar para submenu de</label>' +
          '<select class="ab-nav-bulk__parent" id="ab-nav-bulk-parent"' +
          (parentOpts ? "" : " disabled") +
          ">" +
          (parentOpts || '<option value="">— Selecione um pai —</option>') +
          "</select>" +
          '<button type="button" class="ab-btn ab-btn--primary ab-nav-bulk-move" id="ab-nav-bulk-move"' +
          (parentOpts ? "" : " disabled") +
          ">Mover</button>" +
          "</div>";
      }
      html += "</div>";
    }

    html +=
      '<div class="ab-nav-list">' +
      (rows || '<div class="ab-props-empty">Nenhum item.</div>') +
      "</div>";

    if (editable) {
      html +=
        '<button type="button" class="ab-btn ab-nav-add" style="width:100%;margin-top:8px" title="Adicionar item">' +
        '<span class="ab-nav-add__plus">+</span> Adicionar item</button>';
    } else {
      html +=
        '<button type="button" class="ab-btn ab-btn--primary ab-nav-make-custom" style="width:100%;margin-top:8px">✎ Personalizar itens</button>';
    }

    // Import from WP when custom
    if (editable && Array.isArray(CFG.navMenus) && CFG.navMenus.length) {
      html +=
        '<button type="button" class="ab-btn ab-btn--ghost ab-nav-import-wp" style="width:100%;margin-top:6px">Importar do menu WP…</button>';
    }

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

  function fieldPageSearch(name, label, value) {
    var id = String(value || "").trim();
    return (
      '<div class="ab-field ab-field-page-search" data-page-field="' +
      esc(name) +
      '">' +
      fieldLabel(label, false) +
      '<input type="search" class="ab-page-search-input" placeholder="Buscar página ou post…" autocomplete="off"/>' +
      '<select class="ab-page-search-select" data-prop="' +
      esc(name) +
      '" size="6">' +
      '<option value="">— Selecione —</option>' +
      (id
        ? '<option value="' + esc(id) + '" selected>ID ' + esc(id) + " (carregando…)</option>"
        : "") +
      "</select>" +
      '<span class="ab-field-hint">Digite para buscar. O conteúdo da página escolhida é inserido neste elemento.</span>' +
      "</div>"
    );
  }

  /**
   * Parse CSS box shorthand (margin/padding/radius) → 4 sides [T,R,B,L].
   * Accepts "10px", "10px 20px", "10 20 30", "auto", mixed units.
   */
  function parseCssBoxSides(raw) {
    var s = String(raw == null ? "" : raw).trim().replace(/\s+/g, " ");
    if (!s) {
      return { sides: ["", "", "", ""], linked: true, empty: true };
    }
    var parts = s.split(" ").filter(Boolean);
    var t, r, b, l;
    if (parts.length === 1) {
      t = r = b = l = parts[0];
    } else if (parts.length === 2) {
      t = b = parts[0];
      r = l = parts[1];
    } else if (parts.length === 3) {
      t = parts[0];
      r = l = parts[1];
      b = parts[2];
    } else {
      t = parts[0];
      r = parts[1];
      b = parts[2];
      l = parts[3];
    }
    var linked = t === r && r === b && b === l;
    return { sides: [t, r, b, l], linked: linked, empty: false };
  }

  /** Join 4 sides into compact CSS shorthand (empty → ""). */
  function joinCssBoxSides(sides) {
    var arr = (sides || []).map(function (v) {
      return String(v == null ? "" : v).trim();
    });
    while (arr.length < 4) arr.push("");
    arr = arr.slice(0, 4);
    // Treat blank as "0" only if sibling has value? Prefer omit empty entirely.
    var any = arr.some(function (v) {
      return v !== "";
    });
    if (!any) return "";
    var filled = arr.map(function (v) {
      return v === "" ? "0" : v;
    });
    var t = filled[0];
    var r = filled[1];
    var b = filled[2];
    var l = filled[3];
    if (t === r && r === b && b === l) return t;
    if (t === b && r === l) return t + " " + r;
    if (r === l) return t + " " + r + " " + b;
    return t + " " + r + " " + b + " " + l;
  }

  /**
   * Normalize a single side input: bare number → number+unit; keep auto/keywords.
   * @param {boolean} [allowAuto] — false for padding (auto is invalid).
   */
  function normalizeDimSide(raw, unit, allowAuto) {
    var v = String(raw == null ? "" : raw).trim();
    if (v === "") return "";
    if (/^(auto|initial|inherit|unset|0)$/i.test(v)) {
      var kw = v.toLowerCase() === "0" ? "0" : v.toLowerCase();
      if (kw === "auto" && allowAuto === false) return "0";
      return kw;
    }
    // already has unit or calc/var
    if (/[a-z%)]$/i.test(v) || /\d/.test(v) === false) return v;
    // pure number
    if (/^-?\d+(\.\d+)?$/.test(v)) {
      var u = unit || "px";
      return u === "none" ? v : v + u;
    }
    return v;
  }

  /**
   * Elementor-like 4-side dimensions control (margin / padding / radius).
   * opts.mode: "box" (T R B L) | "corners" (TL TR BR BL)
   * opts.allowAuto: show hint for "auto" (margin)
   * opts.units: array of unit strings
   */
  function fieldDimensions(name, label, value, opts) {
    opts = opts || {};
    var mode = opts.mode === "corners" ? "corners" : "box";
    var units = opts.units || ["px", "%", "em", "rem", "vw", "vh"];
    var parsed = parseCssBoxSides(value);
    var sides = parsed.sides.slice();
    var linked = parsed.linked;
    // Detect unit from first side with a unit
    var unit = "px";
    for (var i = 0; i < sides.length; i++) {
      var m = String(sides[i] || "").match(/^-?[\d.]+([a-z%]+)$/i);
      if (m) {
        unit = m[1].toLowerCase();
        break;
      }
    }
    if (units.indexOf(unit) < 0) units = [unit].concat(units);
    var labels =
      mode === "corners"
        ? [
            { k: "tl", title: "Superior esquerdo", icon: "⌜" },
            { k: "tr", title: "Superior direito", icon: "⌝" },
            { k: "br", title: "Inferior direito", icon: "⌟" },
            { k: "bl", title: "Inferior esquerdo", icon: "⌞" },
          ]
        : [
            { k: "t", title: "Superior", icon: "↑" },
            { k: "r", title: "Direita", icon: "→" },
            { k: "b", title: "Inferior", icon: "↓" },
            { k: "l", title: "Esquerda", icon: "←" },
          ];
    // Display numeric part without unit for cleaner inputs
    function displayNum(side) {
      var v = String(side || "");
      if (!v) return "";
      if (/^(auto|initial|inherit|unset)$/i.test(v)) return v.toLowerCase();
      var mm = v.match(/^(-?[\d.]+)([a-z%]+)?$/i);
      if (mm) return mm[1];
      return v;
    }
    var unitOpts = units
      .map(function (u) {
        return (
          '<option value="' +
          esc(u) +
          '"' +
          (u === unit ? " selected" : "") +
          ">" +
          esc(u) +
          "</option>"
        );
      })
      .join("");
    var cells = labels
      .map(function (lab, idx) {
        return (
          '<div class="ab-dim__cell">' +
          '<input type="text" class="ab-dim__input" data-dim-side="' +
          idx +
          '" value="' +
          esc(displayNum(sides[idx])) +
          '" placeholder="—" inputmode="decimal" autocomplete="off" title="' +
          esc(lab.title) +
          '"/>' +
          '<span class="ab-dim__side" title="' +
          esc(lab.title) +
          '">' +
          esc(lab.icon) +
          "</span>" +
          "</div>"
        );
      })
      .join("");
    return (
      '<div class="ab-field ab-field-dim" data-style-field="' +
      esc(name) +
      '" data-dim-box="' +
      esc(name) +
      '" data-dim-linked="' +
      (linked ? "1" : "0") +
      '" data-dim-unit="' +
      esc(unit) +
      '">' +
      fieldLabel(label, true) +
      '<div class="ab-dim__toolbar">' +
      '<select class="ab-dim__unit" title="Unidade" aria-label="Unidade">' +
      unitOpts +
      "</select>" +
      '<button type="button" class="ab-dim__link' +
      (linked ? " is-linked" : "") +
      '" title="' +
      (linked ? "Desvincular lados" : "Vincular lados") +
      '" aria-pressed="' +
      (linked ? "true" : "false") +
      '">' +
      (linked ? "🔗" : "🔓") +
      "</button>" +
      "</div>" +
      '<div class="ab-dim__grid">' +
      cells +
      "</div>" +
      (opts.allowAuto
        ? '<span class="ab-field-hint">Use <code>auto</code> para centralizar (ex.: esquerda/direita = auto).</span>'
        : "") +
      '<input type="hidden" data-style="' +
      esc(name) +
      '" value="' +
      esc(value == null ? "" : String(value)) +
      '"/>' +
      "</div>"
    );
  }

  /**
   * Border control: 4 widths + style + color (stored as border_width / border_style / border_color,
   * or compact border shorthand when linked + solid).
   */
  function parseBorderStyles(st) {
    st = st || {};
    var widthRaw = st.border_width || "";
    var style = st.border_style || "";
    var color = st.border_color || "";
    // Fall back to full border shorthand
    if (!widthRaw && st.border) {
      var raw = String(st.border).trim();
      // e.g. "1px solid #ccc" | "2px dashed rgba(...)"
      var m = raw.match(
        /^((?:[\d.]+(?:px|em|rem|%)?\s+){0,3}[\d.]+(?:px|em|rem|%)?|0|thin|medium|thick)(?:\s+(none|hidden|dotted|dashed|solid|double|groove|ridge|inset|outset))?(?:\s+(.+))?$/i
      );
      if (m) {
        widthRaw = m[1].trim();
        style = style || (m[2] || "solid");
        color = color || (m[3] || "").trim();
      } else if (raw && raw !== "0" && raw !== "none") {
        widthRaw = raw;
      }
    }
    if (!style) style = "solid";
    var parsed = parseCssBoxSides(widthRaw);
    return {
      sides: parsed.sides,
      linked: parsed.linked,
      style: style,
      color: color,
      empty: parsed.empty && !color,
    };
  }

  function fieldBorderBox(st) {
    var pb = parseBorderStyles(st);
    var unit = "px";
    for (var i = 0; i < pb.sides.length; i++) {
      var m = String(pb.sides[i] || "").match(/^-?[\d.]+([a-z%]+)$/i);
      if (m) {
        unit = m[1].toLowerCase();
        break;
      }
    }
    function displayNum(side) {
      var v = String(side || "");
      if (!v) return "";
      var mm = v.match(/^(-?[\d.]+)([a-z%]+)?$/i);
      if (mm) return mm[1];
      return v;
    }
    var labels = [
      { title: "Superior", icon: "↑" },
      { title: "Direita", icon: "→" },
      { title: "Inferior", icon: "↓" },
      { title: "Esquerda", icon: "←" },
    ];
    var cells = labels
      .map(function (lab, idx) {
        return (
          '<div class="ab-dim__cell">' +
          '<input type="text" class="ab-dim__input" data-dim-side="' +
          idx +
          '" value="' +
          esc(displayNum(pb.sides[idx])) +
          '" placeholder="—" inputmode="decimal" autocomplete="off" title="' +
          esc(lab.title) +
          '"/>' +
          '<span class="ab-dim__side" title="' +
          esc(lab.title) +
          '">' +
          esc(lab.icon) +
          "</span>" +
          "</div>"
        );
      })
      .join("");
    var styles = [
      { value: "solid", label: "Sólida" },
      { value: "dashed", label: "Tracejada" },
      { value: "dotted", label: "Pontilhada" },
      { value: "double", label: "Dupla" },
      { value: "none", label: "Nenhuma" },
    ];
    var styleOpts = styles
      .map(function (o) {
        return (
          '<option value="' +
          esc(o.value) +
          '"' +
          (pb.style === o.value ? " selected" : "") +
          ">" +
          esc(o.label) +
          "</option>"
        );
      })
      .join("");
    var widthJoined = joinCssBoxSides(pb.sides);
    return (
      '<div class="ab-field ab-field-dim ab-field-border-box" data-style-field="border" data-border-box="1" data-dim-linked="' +
      (pb.linked ? "1" : "0") +
      '" data-dim-unit="' +
      esc(unit) +
      '">' +
      fieldLabel("Borda", true) +
      '<div class="ab-dim__toolbar">' +
      '<select class="ab-dim__unit" title="Unidade">' +
      ["px", "em", "rem"].map(function (u) {
        return (
          '<option value="' +
          esc(u) +
          '"' +
          (u === unit ? " selected" : "") +
          ">" +
          esc(u) +
          "</option>"
        );
      }).join("") +
      "</select>" +
      '<button type="button" class="ab-dim__link' +
      (pb.linked ? " is-linked" : "") +
      '" title="' +
      (pb.linked ? "Desvincular lados" : "Vincular lados") +
      '">' +
      (pb.linked ? "🔗" : "🔓") +
      "</button>" +
      "</div>" +
      '<div class="ab-dim__grid">' +
      cells +
      "</div>" +
      '<div class="ab-border-meta">' +
      '<div class="ab-border-meta__row">' +
      "<label>Estilo</label>" +
      '<select class="ab-border-style">' +
      styleOpts +
      "</select>" +
      "</div>" +
      fieldColor("border_color", "Cor da borda", pb.color || "", "data-style") +
      "</div>" +
      '<input type="hidden" data-style="border_width" value="' +
      esc(widthJoined) +
      '"/>' +
      '<input type="hidden" data-style="border_style" value="' +
      esc(pb.style || "solid") +
      '"/>' +
      "</div>"
    );
  }

  /**
   * Wire 4-side dimension controls + border box after props paint.
   */
  function bindDimensionsControls(box) {
    if (!box) return;

    function readSides(wrap) {
      var unit = (wrap.querySelector(".ab-dim__unit") || {}).value || wrap.getAttribute("data-dim-unit") || "px";
      var linked = wrap.getAttribute("data-dim-linked") === "1";
      var keyName = wrap.getAttribute("data-dim-box") || "";
      var allowAuto = keyName === "margin";
      var inputs = wrap.querySelectorAll(".ab-dim__input[data-dim-side]");
      var sides = ["", "", "", ""];
      inputs.forEach(function (inp) {
        var idx = parseInt(inp.getAttribute("data-dim-side"), 10);
        if (isNaN(idx) || idx < 0 || idx > 3) return;
        sides[idx] = normalizeDimSide(inp.value, unit, allowAuto);
      });
      if (linked) {
        // Prefer the first non-empty; else first field
        var master = sides[0];
        for (var i = 0; i < 4; i++) {
          if (sides[i] !== "") {
            master = sides[i];
            break;
          }
        }
        sides = [master, master, master, master];
      }
      return { sides: sides, unit: unit, linked: linked, joined: joinCssBoxSides(sides) };
    }

    function syncLinkedInputs(wrap, value) {
      var unit = (wrap.querySelector(".ab-dim__unit") || {}).value || "px";
      var display = value;
      var mm = String(value || "").match(/^(-?[\d.]+)([a-z%]+)?$/i);
      if (mm && !/^(auto)$/i.test(value)) display = mm[1];
      wrap.querySelectorAll(".ab-dim__input[data-dim-side]").forEach(function (inp) {
        inp.value = display === "0" && value === "" ? "" : display || "";
      });
    }

    function setLinkedUi(wrap, linked) {
      wrap.setAttribute("data-dim-linked", linked ? "1" : "0");
      var btn = wrap.querySelector(".ab-dim__link");
      if (btn) {
        btn.classList.toggle("is-linked", linked);
        btn.setAttribute("aria-pressed", linked ? "true" : "false");
        btn.title = linked ? "Desvincular lados" : "Vincular lados";
        btn.textContent = linked ? "🔗" : "🔓";
      }
    }

    // Margin / padding / radius boxes
    box.querySelectorAll("[data-dim-box]").forEach(function (wrap) {
      if (wrap._abDimBound) return;
      wrap._abDimBound = true;
      var key = wrap.getAttribute("data-dim-box");
      var hidden = wrap.querySelector('input[data-style="' + key + '"]');

      function commit(fromIdx, opts) {
        opts = opts || {};
        var id = state.selectedId;
        if (!id || !key) return;
        var linked = wrap.getAttribute("data-dim-linked") === "1";
        if (linked && fromIdx != null) {
          var unit = (wrap.querySelector(".ab-dim__unit") || {}).value || "px";
          var src = wrap.querySelector('.ab-dim__input[data-dim-side="' + fromIdx + '"]');
          var master = normalizeDimSide(src ? src.value : "", unit);
          syncLinkedInputs(wrap, master);
        }
        var data = readSides(wrap);
        if (hidden) hidden.value = data.joined;
        if (!opts.soft) pushHistory();
        updateStyle(id, key, data.joined);
        markDirty();
        refreshNodeVisual(id);
      }

      wrap.querySelectorAll(".ab-dim__input[data-dim-side]").forEach(function (inp) {
        var liveT = null;
        inp.addEventListener("input", function () {
          // Live preview while typing (no history spam).
          clearTimeout(liveT);
          liveT = setTimeout(function () {
            commit(parseInt(inp.getAttribute("data-dim-side"), 10), { soft: true });
          }, 80);
        });
        inp.addEventListener("change", function () {
          clearTimeout(liveT);
          commit(parseInt(inp.getAttribute("data-dim-side"), 10));
        });
        inp.addEventListener("keydown", function (e) {
          if (e.key === "Enter") {
            e.preventDefault();
            clearTimeout(liveT);
            commit(parseInt(inp.getAttribute("data-dim-side"), 10));
            inp.blur();
          }
        });
      });
      var unitSel = wrap.querySelector(".ab-dim__unit");
      if (unitSel) {
        unitSel.addEventListener("change", function () {
          wrap.setAttribute("data-dim-unit", unitSel.value || "px");
          // Re-apply unit to current numeric values
          wrap.querySelectorAll(".ab-dim__input[data-dim-side]").forEach(function (inp) {
            var v = String(inp.value || "").trim();
            if (v && /^-?[\d.]+$/.test(v)) {
              /* keep number; unit applied on commit */
            } else if (v) {
              // strip old unit, keep number
              var mm = v.match(/^(-?[\d.]+)/);
              if (mm) inp.value = mm[1];
            }
          });
          commit(0);
        });
      }
      var linkBtn = wrap.querySelector(".ab-dim__link");
      if (linkBtn) {
        linkBtn.addEventListener("click", function (e) {
          e.preventDefault();
          var next = wrap.getAttribute("data-dim-linked") !== "1";
          setLinkedUi(wrap, next);
          if (next) {
            // copy first non-empty to all
            var data = readSides(wrap);
            var master = data.sides[0];
            for (var i = 0; i < 4; i++) {
              if (data.sides[i] !== "") {
                master = data.sides[i];
                break;
              }
            }
            syncLinkedInputs(wrap, master);
            commit(0);
          }
        });
      }
    });

    // Border width + style (+ color via color handlers)
    box.querySelectorAll("[data-border-box]").forEach(function (wrap) {
      if (wrap._abBorderBound) return;
      wrap._abBorderBound = true;
      var hiddenW = wrap.querySelector('input[data-style="border_width"]');
      var hiddenS = wrap.querySelector('input[data-style="border_style"]');

      function commitBorder(fromIdx) {
        var id = state.selectedId;
        if (!id) return;
        var linked = wrap.getAttribute("data-dim-linked") === "1";
        if (linked && fromIdx != null) {
          var unit = (wrap.querySelector(".ab-dim__unit") || {}).value || "px";
          var src = wrap.querySelector('.ab-dim__input[data-dim-side="' + fromIdx + '"]');
          var master = normalizeDimSide(src ? src.value : "", unit);
          syncLinkedInputs(wrap, master);
        }
        var data = readSides(wrap);
        var styleEl = wrap.querySelector(".ab-border-style");
        var bStyle = styleEl ? styleEl.value : "solid";
        // Color from the nested color field
        var colorInp = wrap.querySelector('input.ab-color-hex[data-style="border_color"]');
        var bColor = colorInp ? String(colorInp.value || "").trim() : "";

        if (hiddenW) hiddenW.value = data.joined;
        if (hiddenS) hiddenS.value = bStyle;

        pushHistory();
        if (!data.joined || bStyle === "none") {
          updateStyle(id, "border", "");
          updateStyle(id, "border_width", "");
          updateStyle(id, "border_style", "");
          updateStyle(id, "border_color", "");
        } else if (data.linked) {
          // Single width on all sides → compact border shorthand.
          var one = data.sides[0] || data.joined;
          updateStyle(id, "border", one + " " + bStyle + (bColor ? " " + bColor : ""));
          updateStyle(id, "border_width", "");
          updateStyle(id, "border_style", "");
          updateStyle(id, "border_color", bColor || "");
        } else {
          // Per-side widths → longhands.
          updateStyle(id, "border", "");
          updateStyle(id, "border_width", data.joined);
          updateStyle(id, "border_style", bStyle || "solid");
          updateStyle(id, "border_color", bColor || "");
        }
        markDirty();
        refreshNodeVisual(id);
      }

      wrap.querySelectorAll(".ab-dim__input[data-dim-side]").forEach(function (inp) {
        inp.addEventListener("change", function () {
          commitBorder(parseInt(inp.getAttribute("data-dim-side"), 10));
        });
        inp.addEventListener("keydown", function (e) {
          if (e.key === "Enter") {
            e.preventDefault();
            commitBorder(parseInt(inp.getAttribute("data-dim-side"), 10));
            inp.blur();
          }
        });
      });
      var unitSel = wrap.querySelector(".ab-dim__unit");
      if (unitSel) {
        unitSel.addEventListener("change", function () {
          wrap.setAttribute("data-dim-unit", unitSel.value || "px");
          commitBorder(0);
        });
      }
      var styleSel = wrap.querySelector(".ab-border-style");
      if (styleSel) {
        styleSel.addEventListener("change", function () {
          commitBorder(null);
        });
      }
      var linkBtn = wrap.querySelector(".ab-dim__link");
      if (linkBtn) {
        linkBtn.addEventListener("click", function (e) {
          e.preventDefault();
          var next = wrap.getAttribute("data-dim-linked") !== "1";
          setLinkedUi(wrap, next);
          if (next) {
            var data = readSides(wrap);
            var master = data.sides[0];
            for (var i = 0; i < 4; i++) {
              if (data.sides[i] !== "") {
                master = data.sides[i];
                break;
              }
            }
            syncLinkedInputs(wrap, master);
          }
          commitBorder(0);
        });
      }
      // When border color changes, re-commit widths/style together
      var colorHex = wrap.querySelector('input.ab-color-hex[data-style="border_color"]');
      if (colorHex) {
        colorHex.addEventListener("change", function () {
          commitBorder(null);
        });
      }
    });
  }

  /** Select with optional custom value fallback. */
  function fieldSelectOrCustom(name, label, value, options, attr) {
    attr = attr || "data-prop";
    var str = value == null ? "" : String(value);
    var known = (options || []).some(function (o) {
      return o.value !== "custom" && String(o.value) === str;
    });
    var forced = isForceCustom(name, attr);
    var selectVal;
    if (forced) {
      // User chose "Personalizado…" — always show free input (even if current value is a preset).
      selectVal = "custom";
    } else if (known) {
      selectVal = str;
    } else if (str === "" && (options || []).some(function (o) { return o.value === ""; })) {
      selectVal = "";
    } else if (str === "") {
      selectVal = (options[0] && options[0].value) || "";
    } else {
      selectVal = "custom";
    }
    var html = fieldSelect(name, label, selectVal, options, attr);
    if (selectVal === "custom") {
      var customAttr = attr === "data-prop" ? "data-prop-custom" : "data-style-custom";
      var display = str === "custom" || str === "" ? "" : str;
      // Font family: open Google Fonts picker popup.
      if (name === "font_family") {
        var bare = display.replace(/^["']|["']$/g, "").split(",")[0].trim();
        html +=
          '<div class="ab-field ab-field-custom ab-field-google-font">' +
          '<label class="ab-custom-label">Google Fonts / personalizado</label>' +
          '<div class="ab-gf-row">' +
          '<input type="text" class="ab-custom-input ab-gf-value" ' +
          customAttr +
          '="' +
          esc(name) +
          '" value="' +
          esc(bare) +
          '" placeholder="Nome da fonte…" autocomplete="off"/>' +
          '<button type="button" class="ab-btn ab-btn--primary ab-gf-open" title="Abrir biblioteca Google Fonts">' +
          "Biblioteca</button>" +
          "</div>" +
          '<span class="ab-field-hint">Clique em <strong>Biblioteca</strong> para ver e escolher fontes com preview.</span>' +
          "</div>";
      } else {
        html +=
          '<div class="ab-field ab-field-custom">' +
          '<label class="ab-custom-label">Valor personalizado</label>' +
          '<input type="text" class="ab-custom-input" ' +
          customAttr +
          '="' +
          esc(name) +
          '" value="' +
          esc(display) +
          '" placeholder="ex: 320px · 40% · 1.5rem" autocomplete="off"/>' +
          '<span class="ab-field-hint">Digite e pressione Enter ou saia do campo para aplicar.</span>' +
          "</div>";
      }
    }
    return html;
  }

  /** Alignment control with icons only (Elementor-like) — no select. */
  /**
   * Parse object-position into {x,y} percentages 0–100.
   * Accepts "50% 30%", "center top", "left", etc.
   */
  function parseObjectPosition(raw) {
    var s = String(raw == null ? "center center" : raw).trim().toLowerCase().replace(/\s+/g, " ");
    if (!s) s = "center center";
    var map = {
      left: 0,
      center: 50,
      right: 100,
      top: 0,
      bottom: 100,
    };
    function one(tok, axis) {
      tok = String(tok || "").trim();
      if (tok === "") return 50;
      if (map[tok] != null) return map[tok];
      var m = tok.match(/^(-?\d+(?:\.\d+)?)\s*%?$/);
      if (m) {
        var n = parseFloat(m[1]);
        if (isNaN(n)) return 50;
        return Math.max(0, Math.min(100, n));
      }
      return 50;
    }
    var parts = s.split(" ");
    if (parts.length === 1) {
      // single keyword: top/bottom → y; left/right → x; center → both
      if (parts[0] === "top" || parts[0] === "bottom") return { x: 50, y: one(parts[0]) };
      if (parts[0] === "left" || parts[0] === "right") return { x: one(parts[0]), y: 50 };
      return { x: one(parts[0]), y: one(parts[0]) };
    }
    // CSS object-position: first is X, second is Y (keywords or %)
    return { x: one(parts[0], "x"), y: one(parts[1], "y") };
  }

  function formatObjectPosition(x, y) {
    x = Math.round(Math.max(0, Math.min(100, Number(x) || 0)));
    y = Math.round(Math.max(0, Math.min(100, Number(y) || 0)));
    return x + "% " + y + "%";
  }

  /** Resolve a preview URL for the focal pad from various widget settings. */
  function resolveFocalPreviewUrl(settings) {
    if (!settings || typeof settings !== "object") return "";
    if (settings.url) return String(settings.url);
    if (settings.image) return String(settings.image);
    if (settings.bg_image) return String(settings.bg_image);
    // Accordion / tabs / toggle: first item image
    try {
      var raw = settings.items || "[]";
      var items = typeof raw === "string" ? JSON.parse(raw) : raw;
      if (Array.isArray(items)) {
        for (var i = 0; i < items.length; i++) {
          if (items[i] && items[i].image) return String(items[i].image);
        }
      }
    } catch (err) {}
    // Gallery / carousel: first URL in images list
    try {
      var gRaw = settings.images || "[]";
      var gItems = typeof gRaw === "string" ? JSON.parse(gRaw) : gRaw;
      if (Array.isArray(gItems) && gItems.length) {
        var first = gItems[0];
        if (typeof first === "string") return first;
        if (first && first.url) return String(first.url);
      }
    } catch (err2) {}
    return "";
  }

  /** Visual focal-point picker: drag the ball inside a square; image follows. */
  function fieldFocalPoint(name, label, value, settings) {
    var xy = parseObjectPosition(value);
    var url = resolveFocalPreviewUrl(settings);
    var fit =
      (settings && (settings.object_fit || settings.image_object_fit)) || "cover";
    var bg = url
      ? "background-image:url('" +
        String(url).replace(/\\/g, "\\\\").replace(/'/g, "\\'") +
        "');background-size:" +
        (fit === "contain" ? "contain" : "cover") +
        ";background-position:" +
        xy.x +
        "% " +
        xy.y +
        "%;background-repeat:no-repeat;background-color:#0f172a;"
      : "background:repeating-conic-gradient(#1e293b 0% 25%,#0f172a 0% 50%) 50%/16px 16px;";
    return (
      '<div class="ab-field ab-field-focal" data-focal-field="' +
      esc(name) +
      '">' +
      '<div class="ab-field-label">' +
      esc(label || "Posição no quadro") +
      "</div>" +
      '<p class="ab-field-hint" style="margin:0 0 8px">Arraste a <strong>bolinha</strong> para escolher o foco do recorte (topo, base, centro…).</p>' +
      '<div class="ab-focal" data-focal-pad="1" style="' +
      bg +
      '">' +
      '<button type="button" class="ab-focal__knob" data-focal-knob="1" style="left:' +
      xy.x +
      "%;top:" +
      xy.y +
      '%" aria-label="Arrastar posição da imagem" title="Arraste"></button>' +
      "</div>" +
      '<div class="ab-focal__readout"><span data-focal-readout="1">' +
      esc(formatObjectPosition(xy.x, xy.y)) +
      "</span></div>" +
      '<div class="ab-focal__presets" role="group" aria-label="Atalhos">' +
      [
        ["50% 0%", "Topo"],
        ["50% 50%", "Centro"],
        ["50% 100%", "Base"],
        ["0% 50%", "Esq."],
        ["100% 50%", "Dir."],
      ]
        .map(function (p) {
          return (
            '<button type="button" class="ab-focal__preset" data-focal-preset="' +
            esc(p[0]) +
            '">' +
            esc(p[1]) +
            "</button>"
          );
        })
        .join("") +
      "</div>" +
      '<input type="hidden" data-prop="' +
      esc(name) +
      '" value="' +
      esc(formatObjectPosition(xy.x, xy.y)) +
      '" data-focal-input="1"/>' +
      "</div>"
    );
  }

  function bindFocalPointPickers(root) {
    if (!root) return;
    root.querySelectorAll("[data-focal-field]").forEach(function (field) {
      if (field._abFocalBound) return;
      field._abFocalBound = true;
      var propKey = field.getAttribute("data-focal-field") || "object_position";
      var pad = field.querySelector("[data-focal-pad]");
      var knob = field.querySelector("[data-focal-knob]");
      var input = field.querySelector("[data-focal-input]");
      var readout = field.querySelector("[data-focal-readout]");
      if (!pad || !knob || !input) return;

      function applyXY(x, y, commit) {
        x = Math.max(0, Math.min(100, x));
        y = Math.max(0, Math.min(100, y));
        var val = formatObjectPosition(x, y);
        knob.style.left = x + "%";
        knob.style.top = y + "%";
        pad.style.backgroundPosition = x + "% " + y + "%";
        input.value = val;
        if (readout) readout.textContent = val;
        var id = state.selectedId;
        var n = getNode(id);
        if (!n) return;
        n.settings = n.settings || {};
        n.settings[propKey] = val;
        // Live canvas without full paint (keeps props panel / drag state).
        refreshNodeVisual(id);
        if (commit) markDirty();
      }

      function fromEvent(e) {
        var rect = pad.getBoundingClientRect();
        if (!rect.width || !rect.height) return;
        var cx = (e.clientX != null ? e.clientX : e.touches && e.touches[0] && e.touches[0].clientX) || 0;
        var cy = (e.clientY != null ? e.clientY : e.touches && e.touches[0] && e.touches[0].clientY) || 0;
        var x = ((cx - rect.left) / rect.width) * 100;
        var y = ((cy - rect.top) / rect.height) * 100;
        applyXY(x, y, false);
      }

      var dragging = false;
      function onMove(e) {
        if (!dragging) return;
        e.preventDefault();
        fromEvent(e.touches ? e.touches[0] : e);
      }
      function onUp() {
        if (!dragging) return;
        dragging = false;
        document.removeEventListener("mousemove", onMove);
        document.removeEventListener("mouseup", onUp);
        document.removeEventListener("touchmove", onMove);
        document.removeEventListener("touchend", onUp);
        markDirty();
        document.body.classList.remove("ab-focal-dragging");
      }
      function startDrag(e) {
        e.preventDefault();
        e.stopPropagation();
        dragging = true;
        document.body.classList.add("ab-focal-dragging");
        document.addEventListener("mousemove", onMove);
        document.addEventListener("mouseup", onUp);
        document.addEventListener("touchmove", onMove, { passive: false });
        document.addEventListener("touchend", onUp);
        fromEvent(e.touches ? e.touches[0] : e);
      }
      pad.addEventListener("mousedown", startDrag);
      pad.addEventListener("touchstart", startDrag, { passive: false });
      knob.addEventListener("mousedown", startDrag);
      knob.addEventListener("touchstart", startDrag, { passive: false });

      field.querySelectorAll("[data-focal-preset]").forEach(function (btn) {
        btn.addEventListener("click", function (e) {
          e.preventDefault();
          e.stopPropagation();
          var raw = btn.getAttribute("data-focal-preset") || "50% 50%";
          var xy = parseObjectPosition(raw);
          pushHistory();
          applyXY(xy.x, xy.y, true);
        });
      });
    });
  }

  function fieldAlignIcons(name, label, value, attr, opts) {
    attr = attr || "data-prop";
    opts = opts || {};
    var cur = value || "left";
    // Simple SVG bars for align icons
    var icons = {
      left:
        '<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true"><rect x="1" y="2" width="14" height="1.5"/><rect x="1" y="6" width="10" height="1.5"/><rect x="1" y="10" width="12" height="1.5"/><rect x="1" y="14" width="8" height="1.5"/></svg>',
      center:
        '<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true"><rect x="1" y="2" width="14" height="1.5"/><rect x="3" y="6" width="10" height="1.5"/><rect x="2" y="10" width="12" height="1.5"/><rect x="4" y="14" width="8" height="1.5"/></svg>',
      right:
        '<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true"><rect x="1" y="2" width="14" height="1.5"/><rect x="5" y="6" width="10" height="1.5"/><rect x="3" y="10" width="12" height="1.5"/><rect x="7" y="14" width="8" height="1.5"/></svg>',
      justify:
        '<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true"><rect x="1" y="2" width="14" height="1.5"/><rect x="1" y="6" width="14" height="1.5"/><rect x="1" y="10" width="14" height="1.5"/><rect x="1" y="14" width="14" height="1.5"/></svg>',
    };
    var titles = { left: "Esquerda", center: "Centro", right: "Direita", justify: "Justificado" };
    // Default: 3 buttons (like Elementor). Justify only when explicitly requested (text blocks).
    var keys = opts.justify ? ["left", "center", "right", "justify"] : ["left", "center", "right"];
    if (cur === "justify" && keys.indexOf("justify") < 0) cur = "left";
    var btns = keys
      .map(function (v) {
        return (
          '<button type="button" class="ab-align-btn' +
          (cur === v ? " is-on" : "") +
          '" data-align-set="' +
          esc(v) +
          '" data-align-key="' +
          esc(name) +
          '" data-align-attr="' +
          esc(attr) +
          '" title="' +
          esc(titles[v] || v) +
          '">' +
          icons[v] +
          "</button>"
        );
      })
      .join("");
    return (
      '<div class="ab-field ab-field-align" data-style-field="' +
      esc(name) +
      '">' +
      fieldLabel(label, attr === "data-style") +
      '<div class="ab-align-group">' +
      btns +
      "</div>" +
      '<input type="hidden" ' +
      attr +
      '="' +
      esc(name) +
      '" value="' +
      esc(cur) +
      '"/>' +
      "</div>"
    );
  }

  /**
   * Icon library — commonly used glyphs (Elementor-like picker).
   * value is the character / short emoji used in the canvas.
   */
  var ICON_LIBRARY = [
    { cat: "UI", items: [
      { v: "★", n: "Star" }, { v: "☆", n: "Star outline" }, { v: "♥", n: "Heart" }, { v: "♡", n: "Heart outline" },
      { v: "●", n: "Circle" }, { v: "○", n: "Circle outline" }, { v: "■", n: "Square" }, { v: "□", n: "Square outline" },
      { v: "▲", n: "Triangle" }, { v: "◆", n: "Diamond" }, { v: "◇", n: "Diamond outline" }, { v: "✦", n: "Sparkle" },
      { v: "✓", n: "Check" }, { v: "✔", n: "Check heavy" }, { v: "✕", n: "Close" }, { v: "✖", n: "X heavy" },
      { v: "+", n: "Plus" }, { v: "−", n: "Minus" }, { v: "•", n: "Bullet" }, { v: "◉", n: "Bullseye" },
      { v: "☰", n: "Menu" }, { v: "⚙", n: "Settings" }, { v: "🔍", n: "Search" }, { v: "⌂", n: "Home" },
    ]},
    { cat: "Setas", items: [
      { v: "→", n: "Right" }, { v: "←", n: "Left" }, { v: "↑", n: "Up" }, { v: "↓", n: "Down" },
      { v: "⇒", n: "Double right" }, { v: "⇐", n: "Double left" }, { v: "↗", n: "NE" }, { v: "↘", n: "SE" },
      { v: "◀", n: "Play left" }, { v: "▶", n: "Play" }, { v: "▲", n: "Caret up" }, { v: "▼", n: "Caret down" },
      { v: "«", n: "Chevrons left" }, { v: "»", n: "Chevrons right" }, { v: "‹", n: "Chevron left" }, { v: "›", n: "Chevron right" },
    ]},
    { cat: "Comunicação", items: [
      { v: "✉", n: "Mail" }, { v: "📞", n: "Phone" }, { v: "📱", n: "Mobile" }, { v: "💬", n: "Chat" },
      { v: "📢", n: "Megaphone" }, { v: "🔔", n: "Bell" }, { v: "📍", n: "Pin" }, { v: "🗺", n: "Map" },
      { v: "🌐", n: "Globe" }, { v: "🔗", n: "Link" }, { v: "📎", n: "Clip" }, { v: "📅", n: "Calendar" },
    ]},
    { cat: "Educação", items: [
      { v: "📚", n: "Books" }, { v: "📖", n: "Book open" }, { v: "✏️", n: "Pencil" }, { v: "📝", n: "Memo" },
      { v: "🎓", n: "Graduation" }, { v: "🏫", n: "School" }, { v: "🧑‍🏫", n: "Teacher" }, { v: "🧒", n: "Child" },
      { v: "🔬", n: "Science" }, { v: "🧪", n: "Lab" }, { v: "🧮", n: "Math" }, { v: "🎨", n: "Art" },
      { v: "🎵", n: "Music" }, { v: "⚽", n: "Sports" }, { v: "🏆", n: "Trophy" }, { v: "⭐", n: "Star" },
    ]},
    { cat: "Pessoas", items: [
      { v: "👤", n: "User" }, { v: "👥", n: "Users" }, { v: "👨‍👩‍👧", n: "Family" }, { v: "🤝", n: "Handshake" },
      { v: "👋", n: "Wave" }, { v: "👍", n: "Like" }, { v: "👏", n: "Clap" }, { v: "💡", n: "Idea" },
    ]},
    { cat: "Social", items: [
      { v: "f", n: "Facebook" }, { v: "𝕏", n: "X/Twitter" }, { v: "▶", n: "YouTube" }, { v: "in", n: "LinkedIn" },
      { v: "ig", n: "Instagram" }, { v: "wa", n: "WhatsApp" }, { v: "tg", n: "Telegram" }, { v: "@", n: "Email" },
    ]},
    { cat: "Negócios", items: [
      { v: "💼", n: "Briefcase" }, { v: "📊", n: "Chart" }, { v: "📈", n: "Growth" }, { v: "💰", n: "Money" },
      { v: "🛒", n: "Cart" }, { v: "🏷", n: "Tag" }, { v: "🔒", n: "Lock" }, { v: "🔓", n: "Unlock" },
      { v: "⏱", n: "Timer" }, { v: "📁", n: "Folder" }, { v: "📄", n: "Document" }, { v: "🖥", n: "Desktop" },
    ]},
    { cat: "Natureza", items: [
      { v: "☀", n: "Sun" }, { v: "☁", n: "Cloud" }, { v: "🌧", n: "Rain" }, { v: "❄", n: "Snow" },
      { v: "🌳", n: "Tree" }, { v: "🌸", n: "Flower" }, { v: "🍀", n: "Clover" }, { v: "🌊", n: "Wave" },
    ]},
  ];

  function fieldIconPicker(name, label, value) {
    var cur = value == null ? "" : String(value);
    return (
      '<div class="ab-field ab-field-icon" data-style-field="' +
      esc(name) +
      '">' +
      fieldLabel(label, false) +
      '<div class="ab-icon-pick-row">' +
      '<span class="ab-icon-pick-preview" data-icon-preview="' +
      esc(name) +
      '">' +
      (cur ? esc(cur) : "—") +
      "</span>" +
      '<input type="text" class="ab-icon-pick-input" data-prop="' +
      esc(name) +
      '" value="' +
      esc(cur) +
      '" placeholder="emoji ou caractere" maxlength="8"/>' +
      '<button type="button" class="ab-btn ab-btn--primary ab-icon-pick-open" data-icon-for="' +
      esc(name) +
      '" title="Abrir biblioteca de ícones">' +
      svgIcon("icon") +
      " Biblioteca</button>" +
      "</div>" +
      '<span class="ab-field-hint">Escolha na biblioteca ou digite um emoji/símbolo.</span>' +
      "</div>"
    );
  }

  /**
   * @param {string} forName data-prop key OR empty when using onPick
   * @param {{onPick?: function(string):void}|null} opts
   */
  function openIconLibrary(forName, opts) {
    opts = opts || {};
    var existing = document.getElementById("ab-icon-modal");
    if (existing) existing.remove();
    var modal = document.createElement("div");
    modal.id = "ab-icon-modal";
    modal.className = "ab-icon-modal";
    modal.innerHTML =
      '<div class="ab-icon-modal__panel" role="dialog" aria-label="Biblioteca de ícones">' +
      '<div class="ab-icon-modal__head">' +
      "<strong>Biblioteca de ícones</strong>" +
      '<input type="search" class="ab-icon-modal__search" placeholder="Filtrar por nome…" autocomplete="off"/>' +
      '<button type="button" class="ab-icon-modal__close" aria-label="Fechar">×</button>' +
      "</div>" +
      '<div class="ab-icon-modal__body">' +
      '<aside class="ab-icon-modal__cats"></aside>' +
      '<div class="ab-icon-modal__grid-wrap"><div class="ab-icon-modal__grid"></div></div>' +
      "</div>" +
      '<div class="ab-icon-modal__foot"><button type="button" class="ab-btn ab-btn--primary ab-icon-modal__insert" disabled>Inserir</button></div>' +
      "</div>";
    document.body.appendChild(modal);

    var catsEl = modal.querySelector(".ab-icon-modal__cats");
    var gridEl = modal.querySelector(".ab-icon-modal__grid");
    var searchEl = modal.querySelector(".ab-icon-modal__search");
    var insertBtn = modal.querySelector(".ab-icon-modal__insert");
    var activeCat = "all";
    var selected = null;

    function allItems() {
      var out = [];
      ICON_LIBRARY.forEach(function (g) {
        g.items.forEach(function (it) {
          out.push({ v: it.v, n: it.n, cat: g.cat });
        });
      });
      return out;
    }

    function renderCats() {
      var html = '<button type="button" class="ab-icon-cat' + (activeCat === "all" ? " is-on" : "") + '" data-cat="all">Todos</button>';
      ICON_LIBRARY.forEach(function (g) {
        html +=
          '<button type="button" class="ab-icon-cat' +
          (activeCat === g.cat ? " is-on" : "") +
          '" data-cat="' +
          esc(g.cat) +
          '">' +
          esc(g.cat) +
          "</button>";
      });
      catsEl.innerHTML = html;
      catsEl.querySelectorAll("[data-cat]").forEach(function (btn) {
        btn.addEventListener("click", function () {
          activeCat = btn.getAttribute("data-cat");
          renderCats();
          renderGrid();
        });
      });
    }

    function renderGrid() {
      var q = (searchEl.value || "").toLowerCase().trim();
      var items = allItems().filter(function (it) {
        if (activeCat !== "all" && it.cat !== activeCat) return false;
        if (!q) return true;
        return it.n.toLowerCase().indexOf(q) >= 0 || it.v.indexOf(q) >= 0 || it.cat.toLowerCase().indexOf(q) >= 0;
      });
      gridEl.innerHTML = items
        .map(function (it) {
          return (
            '<button type="button" class="ab-icon-tile' +
            (selected === it.v ? " is-on" : "") +
            '" data-icon-val="' +
            esc(it.v) +
            '" title="' +
            esc(it.n) +
            '"><span class="ab-icon-tile__glyph">' +
            esc(it.v) +
            '</span><span class="ab-icon-tile__name">' +
            esc(it.n) +
            "</span></button>"
          );
        })
        .join("");
      gridEl.querySelectorAll("[data-icon-val]").forEach(function (tile) {
        tile.addEventListener("click", function () {
          selected = tile.getAttribute("data-icon-val");
          gridEl.querySelectorAll(".ab-icon-tile").forEach(function (t) {
            t.classList.toggle("is-on", t.getAttribute("data-icon-val") === selected);
          });
          insertBtn.disabled = !selected;
        });
        tile.addEventListener("dblclick", function () {
          selected = tile.getAttribute("data-icon-val");
          applyIcon();
        });
      });
    }

    function applyIcon() {
      if (!selected) return;
      if (typeof opts.onPick === "function") {
        opts.onPick(selected);
        close();
        return;
      }
      var inp = document.querySelector('#ab-props [data-prop="' + forName + '"]');
      if (inp) {
        inp.value = selected;
        inp.dispatchEvent(new Event("input", { bubbles: true }));
        inp.dispatchEvent(new Event("change", { bubbles: true }));
      }
      var prev = document.querySelector('[data-icon-preview="' + forName + '"]');
      if (prev) prev.textContent = selected;
      // Direct apply if listeners not enough
      if (state.selectedId && forName) {
        pushHistory();
        updateSettings(state.selectedId, forName, selected);
        markDirty();
        refreshNodeVisual(state.selectedId);
      }
      close();
    }

    function close() {
      modal.remove();
    }

    modal.addEventListener("click", function (e) {
      if (e.target === modal) close();
    });
    modal.querySelector(".ab-icon-modal__close").addEventListener("click", close);
    insertBtn.addEventListener("click", applyIcon);
    searchEl.addEventListener("input", renderGrid);
    renderCats();
    renderGrid();
    setTimeout(function () {
      searchEl.focus();
    }, 30);
  }

  /** Which style controls make sense for each widget (avoid image props on button/icon). */
  function widgetStyleProfile(type) {
    var layout = {
      section: 1,
      container: 1,
      root: 1,
      loop_grid: 1,
      creative_cards: 1,
      gallery: 1,
      tabs: 1,
      accordion: 1,
      advanced_accordion: 1,
      toggle: 1,
    };
    var media = {
      image: 1,
      image_box: 1,
      video: 1,
      embed: 1,
      carousel: 1,
      image_float: 1,
      image_overlay: 1,
      image_carousel_pro: 1,
      preview_card: 1,
      gallery: 1,
    };
    var texty = {
      heading: 1,
      text: 1,
      button: 1,
      dynamic: 1,
      alert: 1,
      testimonial: 1,
      counter: 1,
      text_path: 1,
      text_animation: 1,
      info_box: 1,
      preview_card: 1,
      icon_box: 1,
      image_box: 1,
      nav_menu: 1,
      advanced_accordion: 1,
      comments_form: 1,
    };
    var iconish = { icon: 1, star_rating: 1, social_icons: 1, social_bar: 1, icon_list: 1 };
    var noBg = {
      image: 1,
      divider: 1,
      spacer: 1,
      video: 1,
      embed: 1,
      carousel: 1,
      image_float: 1,
      image_overlay: 1,
      image_carousel_pro: 1,
      google_maps: 1,
      menu_anchor: 1,
      shortcode: 1,
    };
    var heightOk = {
      section: 1,
      container: 1,
      image: 1,
      video: 1,
      spacer: 1,
      google_maps: 1,
      image_float: 1,
      image_overlay: 1,
      image_carousel_pro: 1,
      carousel: 1,
      gallery: 1,
      preview_card: 1,
      loop_grid: 1,
      root: 1,
    };
    // Layout containers: full typography cascades to child texts/headings (StyleEngine deep selector).
    var layoutTypo = type === "container" || type === "section";
    return {
      width: type !== "container" && type !== "root" && type !== "spacer" && type !== "divider" && type !== "menu_anchor",
      typography: !!texty[type] || layoutTypo,
      // Icon-like: color + size only (no full typography suite)
      iconColor: !!iconish[type] || type === "progress_bar",
      // Plain color only (no font suite) for remaining widgets
      simpleColor:
        !texty[type] &&
        !layoutTypo &&
        !iconish[type] &&
        type !== "image" &&
        type !== "gallery" &&
        type !== "carousel" &&
        type !== "video" &&
        type !== "embed" &&
        type !== "image_float" &&
        type !== "image_overlay" &&
        type !== "image_carousel_pro" &&
        type !== "google_maps" &&
        type !== "spacer" &&
        type !== "divider" &&
        type !== "menu_anchor",
      background: !noBg[type],
      // Background image only for layout surfaces — NOT button/icon/text leaves
      backgroundImage: !!layout[type] || type === "info_box" || type === "preview_card",
      height: !!heightOk[type],
      spacing: type !== "menu_anchor" && type !== "divider",
      // Border radius useful for button, cards, images, media, layout
      radius:
        type === "button" ||
        type === "image" ||
        type === "gallery" ||
        type === "carousel" ||
        type === "image_carousel_pro" ||
        type === "image_float" ||
        type === "image_overlay" ||
        type === "preview_card" ||
        type === "info_box" ||
        type === "icon" ||
        type === "image_box" ||
        type === "icon_box" ||
        type === "section" ||
        type === "container" ||
        type === "video" ||
        type === "embed",
      // Motion effects (efeitos.txt) — almost all visual widgets.
      effects: widgetSupportsEffects(type),
    };
  }

  function widgetSupportsEffects(type) {
    var no = {
      root: 1,
      spacer: 1,
      shortcode: 1,
      html: 1,
      menu_anchor: 1,
      sidebar: 1,
      google_maps: 1,
    };
    return !no[type];
  }

  var FX_ENTRANCE_OPTS = [
    { value: "", label: "Nenhum" },
    { value: "fadein", label: "Fade" },
    { value: "fadeinup", label: "Fade ↑" },
    { value: "fadeindown", label: "Fade ↓" },
    { value: "fadeinleft", label: "Fade ←" },
    { value: "fadeinright", label: "Fade →" },
    { value: "zoomin", label: "Zoom in" },
    { value: "zoominup", label: "Zoom in ↑" },
    { value: "slideinup", label: "Slide ↑" },
    { value: "slideindown", label: "Slide ↓" },
    { value: "slideinleft", label: "Slide ←" },
    { value: "slideinright", label: "Slide →" },
    { value: "bouncein", label: "Bounce" },
    { value: "flipinx", label: "Flip X" },
    { value: "flipiny", label: "Flip Y" },
    { value: "rotatein", label: "Rotate in" },
    { value: "lightspeedin", label: "Light speed" },
  ];
  var FX_HOVER_OPTS = [
    { value: "", label: "Nenhum" },
    { value: "grow", label: "Crescer" },
    { value: "shrink", label: "Encolher" },
    { value: "lift", label: "Elevar" },
    { value: "sink", label: "Afundar" },
    { value: "float", label: "Flutuar" },
    { value: "pulse", label: "Pulso" },
    { value: "wobble", label: "Balançar" },
    { value: "buzz", label: "Vibrar" },
    { value: "glow", label: "Brilho" },
    { value: "brightness", label: "Brilho +" },
    { value: "grayscale", label: "P&B → cor" },
    { value: "blur", label: "Desfoque" },
    { value: "rotate", label: "Girar" },
    { value: "skew", label: "Inclinar" },
    { value: "borderglow", label: "Borda brilho" },
  ];
  var FX_SHADOW_OPTS = [
    { value: "", label: "Nenhuma" },
    { value: "sm", label: "Suave" },
    { value: "md", label: "Média" },
    { value: "lg", label: "Grande" },
    { value: "xl", label: "Extra" },
    { value: "colored", label: "Colorida (roxa)" },
  ];

  /** Build effects classes + inline vars from style bag (editor). */
  function fxClassList(st) {
    st = st || {};
    var cls = [];
    var ent = String(st.fx_entrance || "").toLowerCase().replace(/[^a-z]/g, "");
    var hov = String(st.fx_hover || "").toLowerCase().replace(/_/g, "-").replace(/[^a-z-]/g, "");
    var sh = String(st.fx_shadow || "").toLowerCase();
    if (ent && ent !== "none") {
      cls.push("ab-fx", "ab-fx-entrance", "ab-fx-entrance--" + ent);
    }
    if (hov && hov !== "none") {
      if (hov === "border-glow" || hov === "borderglow") hov = "borderglow";
      cls.push("ab-fx", "ab-fx-hover", "ab-fx-hover--" + hov);
    }
    if (sh && sh !== "none") cls.push("ab-fx-shadow--" + sh);
    if (st.fx_rotate || st.fx_scale || st.fx_translate_x || st.fx_translate_y || st.fx_opacity) {
      cls.push("ab-fx", "ab-fx-static");
    }
    // unique
    var seen = {};
    return cls.filter(function (c) {
      if (seen[c]) return false;
      seen[c] = true;
      return true;
    });
  }

  function fxInlineStyle(st) {
    st = st || {};
    var parts = [];
    if (st.fx_entrance_duration) parts.push("--ab-fx-duration:" + parseInt(st.fx_entrance_duration, 10) + "ms");
    if (st.fx_entrance_delay) parts.push("--ab-fx-delay:" + parseInt(st.fx_entrance_delay, 10) + "ms");
    if (st.fx_hover_duration) parts.push("--ab-fx-hover-duration:" + parseInt(st.fx_hover_duration, 10) + "ms");
    if (st.fx_transition) parts.push("--ab-fx-transition:" + parseInt(st.fx_transition, 10) + "ms");
    var tf = [];
    if (st.fx_translate_x) {
      var x = String(st.fx_translate_x);
      if (/^-?[\d.]+$/.test(x)) x += "px";
      tf.push("translateX(" + x + ")");
    }
    if (st.fx_translate_y) {
      var y = String(st.fx_translate_y);
      if (/^-?[\d.]+$/.test(y)) y += "px";
      tf.push("translateY(" + y + ")");
    }
    if (st.fx_rotate !== undefined && st.fx_rotate !== "" && st.fx_rotate != null) {
      var r = String(st.fx_rotate);
      if (/^-?[\d.]+$/.test(r)) r += "deg";
      tf.push("rotate(" + r + ")");
    }
    if (st.fx_scale !== undefined && st.fx_scale !== "" && st.fx_scale != null) {
      tf.push("scale(" + st.fx_scale + ")");
    }
    if (tf.length) parts.push("transform:" + tf.join(" "));
    if (st.fx_opacity !== undefined && st.fx_opacity !== "" && st.fx_opacity != null) {
      var op = parseFloat(st.fx_opacity);
      if (!isNaN(op)) {
        if (op > 1) op = op / 100;
        parts.push("opacity:" + Math.max(0, Math.min(1, op)));
      }
    }
    return parts.join(";");
  }

  function effectsPanelHtml(st) {
    var html = "";
    html +=
      '<div class="ab-props-section-title">Efeitos · ' +
      esc(BP_LABEL[state.breakpoint]) +
      "</div>";
    html +=
      '<div class="ab-field-hint">Entrada ao rolar, hover, transform e sombra — por dispositivo. Ver <code>efeitos.txt</code>.</div>';
    html += fieldSelect("fx_entrance", "Animação de entrada", st.fx_entrance || "", FX_ENTRANCE_OPTS, "data-style");
    html += fieldInput("fx_entrance_duration", "Duração entrada (ms)", st.fx_entrance_duration || "", {
      type: "number",
      attr: "data-style",
      devices: true,
      placeholder: "800",
    });
    html += fieldInput("fx_entrance_delay", "Atraso entrada (ms)", st.fx_entrance_delay || "", {
      type: "number",
      attr: "data-style",
      devices: true,
      placeholder: "0",
    });
    html += fieldSelect("fx_hover", "Efeito hover", st.fx_hover || "", FX_HOVER_OPTS, "data-style");
    html += fieldInput("fx_hover_duration", "Duração hover (ms)", st.fx_hover_duration || "", {
      type: "number",
      attr: "data-style",
      devices: true,
      placeholder: "300",
    });
    html += fieldInput("fx_rotate", "Rotação (°)", st.fx_rotate || "", {
      type: "text",
      attr: "data-style",
      devices: true,
      placeholder: "ex: 3 ou -5",
    });
    html += fieldInput("fx_scale", "Escala", st.fx_scale || "", {
      type: "text",
      attr: "data-style",
      devices: true,
      placeholder: "ex: 1.05",
    });
    html += fieldInput("fx_translate_x", "Deslocar X", st.fx_translate_x || "", {
      type: "text",
      attr: "data-style",
      devices: true,
      placeholder: "ex: 10px",
    });
    html += fieldInput("fx_translate_y", "Deslocar Y", st.fx_translate_y || "", {
      type: "text",
      attr: "data-style",
      devices: true,
      placeholder: "ex: -8px",
    });
    html += fieldInput("fx_opacity", "Opacidade", st.fx_opacity || "", {
      type: "text",
      attr: "data-style",
      devices: true,
      placeholder: "0–1 ou %",
    });
    html += fieldSelect("fx_shadow", "Sombra", st.fx_shadow || "", FX_SHADOW_OPTS, "data-style");
    html += fieldInput("fx_transition", "Transição base (ms)", st.fx_transition || "", {
      type: "number",
      attr: "data-style",
      devices: true,
      placeholder: "300",
    });
    html +=
      '<button type="button" class="ab-btn ab-btn--primary ab-fx-preview" style="width:100%;margin-top:4px">▶ Pré-visualizar entrada</button>';
    return html;
  }

  /** Searchable category picker (site categories from AuraBuilderConfig). */
  /**
   * Category picker. multi=true → several IDs as "12,34,56".
   */
  function fieldCategorySearch(name, label, value, multi) {
    multi = !!multi;
    var cats = Array.isArray(CFG.categories) ? CFG.categories : [];
    var selected = String(value || "")
      .split(/[\s,;|]+/)
      .map(function (s) {
        return s.trim();
      })
      .filter(Boolean);
    var selectedSet = {};
    selected.forEach(function (id) {
      selectedSet[id] = true;
    });
    var names = [];
    cats.forEach(function (c) {
      if (selectedSet[String(c.id)] || selectedSet[c.slug]) {
        names.push(c.name);
      }
    });
    var opts = cats
      .map(function (c) {
        var sel = selectedSet[String(c.id)] || selectedSet[c.slug] ? " selected" : "";
        return (
          '<option value="' +
          esc(String(c.id)) +
          '" data-slug="' +
          esc(c.slug) +
          '"' +
          sel +
          ">" +
          esc(c.name + " · " + c.count + " posts") +
          "</option>"
        );
      })
      .join("");
    return (
      '<div class="ab-field ab-field-cat-search" data-style-field="' +
      esc(name) +
      '" data-cat-multi="' +
      (multi ? "1" : "0") +
      '">' +
      fieldLabel(label, false) +
      '<div class="ab-cat-search-box">' +
      '<input type="search" class="ab-cat-filter" placeholder="Pesquisar categoria…" autocomplete="off" data-cat-filter="' +
      esc(name) +
      '"/>' +
      '<select class="ab-cat-select" data-prop="' +
      esc(name) +
      '" size="6" data-cat-select="' +
      esc(name) +
      '"' +
      (multi ? " multiple" : "") +
      ">" +
      (multi ? "" : '<option value="">— Todas as categorias —</option>') +
      opts +
      "</select>" +
      '<div class="ab-field-hint">' +
      (multi ? "Segure Ctrl/Cmd para várias · " : "") +
      "Selecionada(s): <strong data-cat-label=\"" +
      esc(name) +
      '">' +
      esc(names.length ? names.join(", ") : multi ? "Nenhuma (todas)" : "Todas") +
      "</strong></div>" +
      "</div></div>"
    );
  }

  function fieldSwitch(name, label, value) {
    var on = value === true || value === 1 || value === "1" || value === "yes" || value === "on";
    return (
      '<div class="ab-field ab-field-switch" data-style-field="' +
      esc(name) +
      '">' +
      '<label class="ab-switch-row">' +
      '<span class="ab-switch-label">' +
      esc(label) +
      "</span>" +
      '<span class="ab-switch' +
      (on ? " is-on" : "") +
      '">' +
      '<input type="checkbox" data-prop="' +
      esc(name) +
      '" data-switch="1" value="1"' +
      (on ? " checked" : "") +
      "/>" +
      '<span class="ab-switch-ui" aria-hidden="true"></span>' +
      '<span class="ab-switch-text">' +
      (on ? "Sim" : "Não") +
      "</span>" +
      "</span></label></div>"
    );
  }

  /** Resolve content setting key for active style breakpoint (desktop / tablet / mobile). */
  function contentKeyForBp(baseKey, bp) {
    bp = bp || state.breakpoint || "desktop";
    if (bp === "tablet") return baseKey + "_tablet";
    if (bp === "mobile") return baseKey + "_mobile";
    return baseKey;
  }

  /**
   * Read responsive content value with cascade: mobile → tablet → desktop.
   * Empty string on tablet/mobile means "inherit".
   */
  function contentValForBp(settings, baseKey, bp) {
    settings = settings || {};
    bp = bp || state.breakpoint || "desktop";
    var k = contentKeyForBp(baseKey, bp);
    var v = settings[k];
    if (bp !== "desktop" && (v === undefined || v === null || v === "")) {
      if (bp === "mobile") {
        var vt = settings[baseKey + "_tablet"];
        if (vt !== undefined && vt !== null && vt !== "") return vt;
      }
      return settings[baseKey] != null ? settings[baseKey] : "";
    }
    return v != null ? v : "";
  }

  function renderSchemaField(field, settings) {
    if (field.showIf) {
      var depKey = field.showIf.key;
      var dep = field.responsive
        ? contentValForBp(settings, depKey, state.breakpoint)
        : settings[depKey];
      // also check desktop dep when responsive showIf
      if (field.responsive && (dep === undefined || dep === null || dep === "")) {
        dep = settings[depKey];
      }
      var want = field.showIf.value;
      if (String(dep == null ? "" : dep) !== String(want)) return "";
    }
    var storeKey = field.key;
    var val = settings[field.key];
    var bp = state.breakpoint || "desktop";
    if (field.responsive) {
      storeKey = contentKeyForBp(field.key, bp);
      // For display: cascade so tablet/mobile show inherited value in the control.
      val = contentValForBp(settings, field.key, bp);
      // But if explicit empty-override for switch we need raw store for "inherit"
      // Switches: show cascade value; saving writes to storeKey.
    }
    if (field.plain && (field.type === "text" || field.type === "textarea")) {
      val = plainText(val);
    }
    // Responsive content: alignment stored in styles.text_align for active device.
    if (field.key === "align") {
      var stAlign = stylesFor(getNode(state.selectedId) || { styles: {} });
      if (stAlign.text_align) val = stAlign.text_align;
    }
    var label = field.label;
    if (field.responsive && bp !== "desktop") {
      label = field.label + " · " + (BP_LABEL[bp] || bp);
    }
    if (field.type === "switch") {
      return fieldSwitch(storeKey, label, val != null && val !== "" ? val : "0");
    }
    if (field.type === "icon") {
      return fieldIconPicker(storeKey, label, val || "");
    }
    if (field.type === "gallery_media") {
      return fieldGalleryMedia(storeKey, label, val || "[]");
    }
    if (field.type === "accordion_items") {
      return fieldAccordionItems(storeKey, label, val || "[]");
    }
    if (field.type === "icon_list_items") {
      return fieldIconListItems(storeKey, label, val || "[]");
    }
    if (field.type === "social_items") {
      return fieldSocialItems(storeKey, label, val || "[]");
    }
    if (field.type === "nav_menu_items") {
      return fieldNavMenuItems(storeKey, label, val || "", settings);
    }
    if (field.type === "page_search") {
      return fieldPageSearch(storeKey, label, val || "");
    }
    if (field.type === "align") {
      // Content align: 3 icons only (no justify)
      return fieldAlignIcons(storeKey, label, val || "left", "data-prop", { justify: false });
    }
    if (field.type === "focal_point") {
      return fieldFocalPoint(storeKey, label, val || "center center", settings);
    }
    if (field.type === "category_search") {
      return fieldCategorySearch(storeKey, label, val || "", !!field.multi);
    }
    if (field.type === "select") {
      var opts = field.options || [];
      // Tablet/mobile: allow "inherit" empty for responsive selects
      if (field.responsive && bp !== "desktop") {
        opts = [{ value: "", label: "— Herdar (desktop) —" }].concat(opts);
        // Raw stored value (not cascaded) so inherit shows correctly
        var rawStore = settings[storeKey];
        if (rawStore === undefined || rawStore === null || rawStore === "") {
          val = "";
        } else {
          val = rawStore;
        }
      }
      var hasCustom = opts.some(function (o) {
        return o.value === "custom";
      });
      if (hasCustom) {
        return fieldSelectOrCustom(storeKey, label, val, opts, "data-prop");
      }
      return fieldSelect(storeKey, label, val, opts, "data-prop", false);
    }
    if (field.type === "media") {
      return fieldMedia(storeKey, label, val, "data-prop");
    }
    if (field.type === "textarea") {
      return fieldInput(storeKey, label, val, { type: "textarea", devices: false });
    }
    if (field.type === "url") {
      return fieldInput(storeKey, label, val, {
        type: "url",
        placeholder: field.placeholder || "https://…",
        devices: false,
      });
    }
    if (field.type === "number") {
      // Tablet/mobile: empty = inherit
      var numVal = val;
      if (field.responsive && bp !== "desktop") {
        var rawN = settings[storeKey];
        numVal = rawN === undefined || rawN === null ? "" : rawN;
      }
      return fieldInput(storeKey, label, numVal, {
        type: "number",
        devices: false,
        placeholder: field.responsive && bp !== "desktop" ? "Herdar" : field.placeholder || "",
      });
    }
    if (field.type === "color") {
      return fieldColor(storeKey, label, val, field.attr || "data-prop");
    }
    return fieldInput(storeKey, label, val, {
      type: "text",
      placeholder: field.placeholder || "",
      devices: false,
    });
  }

  /** Parse #rgb/#rrggbb/#rrggbbaa or rgb()/rgba() → { hex, a:0-100, empty } */
  function parseColorValue(raw) {
    raw = String(raw == null ? "" : raw).trim();
    if (!raw) return { hex: "#111111", a: 100, empty: true };
    if (/^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/.test(raw)) {
      var h = raw;
      if (h.length === 4) {
        h = "#" + h[1] + h[1] + h[2] + h[2] + h[3] + h[3];
      }
      if (h.length === 9) {
        var aa = parseInt(h.slice(7, 9), 16);
        return {
          hex: h.slice(0, 7).toLowerCase(),
          a: Math.round((aa / 255) * 100),
          empty: false,
        };
      }
      return { hex: h.slice(0, 7).toLowerCase(), a: 100, empty: false };
    }
    // rgb(r,g,b) / rgba(r,g,b,a)  or modern rgb(r g b / a)
    var m = raw.match(
      /^rgba?\(\s*([\d.]+)\s*[, ]\s*([\d.]+)\s*[, ]\s*([\d.]+)(?:\s*[,/]\s*([\d.]+%?))?\s*\)$/i
    );
    if (m) {
      var r = Math.max(0, Math.min(255, Math.round(parseFloat(m[1]))));
      var g = Math.max(0, Math.min(255, Math.round(parseFloat(m[2]))));
      var b = Math.max(0, Math.min(255, Math.round(parseFloat(m[3]))));
      var a = 100;
      if (m[4] != null && m[4] !== "") {
        var av = String(m[4]);
        if (av.indexOf("%") >= 0) a = Math.round(parseFloat(av));
        else {
          var af = parseFloat(av);
          a = af <= 1 ? Math.round(af * 100) : Math.round(af);
        }
      }
      a = Math.max(0, Math.min(100, a));
      var hx =
        "#" +
        [r, g, b]
          .map(function (n) {
            return n.toString(16).padStart(2, "0");
          })
          .join("");
      return { hex: hx, a: a, empty: false };
    }
    return { hex: "#111111", a: 100, empty: true };
  }

  /** Compose CSS color from hex + alpha % (0–100). */
  function composeColorValue(hex, alphaPct) {
    var p = parseColorValue(hex);
    var h = p.hex || "#111111";
    var a = alphaPct == null ? 100 : Math.max(0, Math.min(100, Math.round(Number(alphaPct))));
    if (a >= 100) return h;
    var r = parseInt(h.slice(1, 3), 16);
    var g = parseInt(h.slice(3, 5), 16);
    var b = parseInt(h.slice(5, 7), 16);
    var af = (a / 100).toFixed(2).replace(/\.?0+$/, "");
    if (af === "") af = "0";
    return "rgba(" + r + ", " + g + ", " + b + ", " + af + ")";
  }

  /**
   * Background control: solid color (with alpha) OR multi-stop gradient.
   * Writes styles: bg_type, background, bg_grad_kind, bg_grad_angle, bg_grad_stops, bg_gradient.
   */
  function fieldBackground(st) {
    st = st || {};
    var mode = st.bg_type === "gradient" ? "gradient" : "solid";
    var solid = st.background || "";
    var kind = st.bg_grad_kind === "radial" ? "radial" : "linear";
    var angle = st.bg_grad_angle != null && st.bg_grad_angle !== "" ? String(st.bg_grad_angle) : "135";
    var stops = parseGradStops(st.bg_grad_stops, solid || "#1e3a8a");
    var previewCss = mode === "gradient" ? buildGradientCss(kind, angle, stops) : solid || "transparent";
    var html =
      '<div class="ab-field ab-field-bg" data-bg-panel="1">' +
      fieldLabel("Fundo", true) +
      '<div class="ab-bg-mode-tabs">' +
      '<button type="button" class="ab-bg-mode-tab' +
      (mode === "solid" ? " is-on" : "") +
      '" data-bg-mode="solid">Sólida</button>' +
      '<button type="button" class="ab-bg-mode-tab' +
      (mode === "gradient" ? " is-on" : "") +
      '" data-bg-mode="gradient">Degradê</button>' +
      "</div>" +
      '<div class="ab-bg-preview" style="background:' +
      esc(previewCss) +
      '" title="Prévia do fundo"></div>';

    // Solid panel
    html +=
      '<div class="ab-bg-solid-pane"' +
      (mode === "solid" ? "" : " hidden") +
      ">" +
      fieldColor("background", "Cor de fundo", solid, "data-style") +
      "</div>";

    // Gradient panel
    html +=
      '<div class="ab-bg-grad-pane"' +
      (mode === "gradient" ? "" : " hidden") +
      ">" +
      '<div class="ab-field"><label>Tipo</label>' +
      '<select class="ab-bg-grad-kind" data-style="bg_grad_kind">' +
      '<option value="linear"' +
      (kind === "linear" ? " selected" : "") +
      ">Linear</option>" +
      '<option value="radial"' +
      (kind === "radial" ? " selected" : "") +
      ">Radial</option>" +
      "</select></div>" +
      '<div class="ab-field ab-bg-angle-row"' +
      (kind === "radial" ? " hidden" : "") +
      ">" +
      "<label>Ângulo (" +
      esc(angle) +
      "°)</label>" +
      '<input type="range" class="ab-bg-grad-angle" data-style="bg_grad_angle" min="0" max="360" step="1" value="' +
      esc(angle) +
      '"/>' +
      '<span class="ab-bg-angle-val">' +
      esc(angle) +
      "°</span>" +
      "</div>" +
      '<div class="ab-bg-stops" data-bg-stops="1">';
    stops.forEach(function (stop, i) {
      var pc = parseColorValue(stop.c);
      html +=
        '<div class="ab-bg-stop" data-stop-i="' +
        i +
        '">' +
        '<input type="color" class="ab-bg-stop-swatch" value="' +
        esc(pc.hex) +
        '" title="Cor do ponto"/>' +
        '<input type="text" class="ab-bg-stop-color" value="' +
        esc(stop.c) +
        '" placeholder="#hex ou rgba"/>' +
        '<input type="number" class="ab-bg-stop-pos" min="0" max="100" value="' +
        esc(String(stop.p)) +
        '" title="Posição %"/>' +
        '<span class="ab-bg-stop-pct">%</span>' +
        '<button type="button" class="ab-btn ab-btn--ghost ab-bg-stop-rm" title="Remover ponto"' +
        (stops.length <= 2 ? " disabled" : "") +
        ">×</button>" +
        "</div>";
    });
    html +=
      "</div>" +
      '<button type="button" class="ab-btn ab-bg-stop-add" style="width:100%;margin-top:6px">+ Adicionar cor</button>' +
      '<span class="ab-field-hint">Degradê com 2 ou mais cores. Ajuste posição (0–100%) e transparência em cada cor (rgba).</span>' +
      "</div>" +
      // hidden sync fields
      '<input type="hidden" data-style="bg_type" value="' +
      esc(mode) +
      '"/>' +
      '<input type="hidden" data-style="bg_grad_stops" value="' +
      esc(JSON.stringify(stops)) +
      '"/>' +
      '<input type="hidden" data-style="bg_gradient" value="' +
      esc(mode === "gradient" ? buildGradientCss(kind, angle, stops) : "") +
      '"/>' +
      "</div>";
    return html;
  }

  function fieldColor(name, label, value, attr) {
    attr = attr || "data-style";
    var raw = value != null && value !== "" ? String(value) : "";
    var parsed = parseColorValue(raw);
    var hex = parsed.hex;
    var alpha = parsed.empty ? 100 : parsed.a;
    var previewBg = raw
      ? "background-image:linear-gradient(45deg,#cbd5e1 25%,transparent 25%),linear-gradient(-45deg,#cbd5e1 25%,transparent 25%),linear-gradient(45deg,transparent 75%,#cbd5e1 75%),linear-gradient(-45deg,transparent 75%,#cbd5e1 75%);background-size:10px 10px;background-position:0 0,0 5px,5px -5px,-5px 0px;"
      : "";
    return (
      '<div class="ab-field ab-field-color" data-style-field="' +
      esc(name) +
      '">' +
      fieldLabel(label, true) +
      '<div class="ab-color-row">' +
      '<span class="ab-color-preview" style="' +
      previewBg +
      (raw ? "background-color:" + esc(raw) + ";" : "") +
      '" title="Prévia com transparência"></span>' +
      '<input type="color" class="ab-color-swatch" value="' +
      esc(hex) +
      '" data-color-for="' +
      esc(name) +
      '" data-color-attr="' +
      esc(attr) +
      '"/>' +
      '<input type="text" class="ab-color-hex" ' +
      attr +
      '="' +
      esc(name) +
      '" value="' +
      esc(raw) +
      '" placeholder="#1c27c4 ou rgba(…)"/>' +
      '<button type="button" class="ab-btn ab-btn--ghost ab-color-clear" data-clear="' +
      esc(name) +
      '" data-clear-attr="' +
      esc(attr) +
      '">Limpar</button>' +
      "</div>" +
      '<div class="ab-color-alpha-row">' +
      '<label class="ab-color-alpha-label">Transparência</label>' +
      '<input type="range" class="ab-color-alpha" min="0" max="100" step="1" value="' +
      esc(String(alpha)) +
      '" data-alpha-for="' +
      esc(name) +
      '" data-color-attr="' +
      esc(attr) +
      '"/>' +
      '<span class="ab-color-alpha-val" data-alpha-val="' +
      esc(name) +
      '">' +
      esc(String(alpha)) +
      "%</span>" +
      "</div>" +
      '<span class="ab-field-hint">' +
      (raw
        ? "Valor: <code>" +
          esc(raw) +
          "</code> · 100% = opaco, 0% = invisível · " +
          esc(BP_LABEL[state.breakpoint])
        : "Escolha a cor e use o controle <strong>Transparência</strong> (0–100%).") +
      "</span>" +
      "</div>"
    );
  }

  function syncBreakpointUI() {
    // Top bar = view only
    var view = state.viewBp || "desktop";
    document.querySelectorAll("[data-bp]").forEach(function (btn) {
      btn.classList.toggle("is-active", btn.getAttribute("data-bp") === view);
    });
    var badge = document.getElementById("ab-bp-badge");
    if (badge) badge.textContent = BP_LABEL[view] || view;
  }

  /** Top bar: changes canvas preview width + re-renders (hamburger, colunas mobile). */
  function setViewBreakpoint(bp) {
    if (!BP_WIDTH[bp]) return;
    state.viewBp = bp;
    syncBreakpointUI();
    // Full paint so nav_menu hamburger / column stack match the device view.
    // Props panel stays on the same selected node after paintProps().
    paint();
  }

  function applyCanvasView() {
    var bp = state.viewBp || "desktop";
    var preview = document.querySelector(".ab-page-preview");
    var focus = !!state.hideSidebars;
    if (preview) {
      // Always mark device class — CSS (incl. fullscreen) depends on these.
      preview.classList.toggle("ab-bp-desktop", bp === "desktop");
      preview.classList.toggle("ab-bp-tablet", bp === "tablet");
      preview.classList.toggle("ab-bp-mobile", bp === "mobile");
      // Clear previous inline width rules then re-apply for current device.
      preview.style.removeProperty("max-width");
      preview.style.removeProperty("width");
      preview.style.removeProperty("margin");
      // Full width only for desktop (incl. fullscreen). Tablet/mobile keep frame.
      if (bp === "desktop") {
        preview.style.maxWidth = "100%";
        preview.style.width = "100%";
        preview.style.margin = "0";
      } else {
        // !important so .ab-body--focus rules cannot force 100% width
        preview.style.setProperty("max-width", BP_WIDTH[bp] || "100%", "important");
        preview.style.setProperty("width", "100%", "important");
        preview.style.setProperty("margin", "12px auto", "important");
      }
    }
    var body = document.getElementById("ab-page-body");
    if (body) body.dataset.viewBp = bp;
    var canvas = document.getElementById("ab-canvas");
    if (canvas) {
      canvas.style.maxWidth = "";
      canvas.style.width = "";
    }
  }

  /** Style panel only: switch which style bag (desktop/tablet/mobile) is edited. */
  function setStyleBreakpoint(bp) {
    if (!BP_WIDTH[bp] && bp !== "desktop" && bp !== "tablet" && bp !== "mobile") return;
    if (bp !== "desktop" && bp !== "tablet" && bp !== "mobile") return;
    state.breakpoint = bp;
    paintProps();
  }

  // Legacy alias used by older handlers
  function setBreakpoint(bp) {
    setViewBreakpoint(bp);
  }

  /* ═══════════════════════════════════════════════════════════════════════════
   * Props panel: PRINCIPAL | ESTILO | AVANÇADO | RESPONSIVO
   * Each property has an internal category (tab + accordion group).
   * ═══════════════════════════════════════════════════════════════════════════ */
  /** Icon-only tabs (Gutenberg-style). Labels stay for title/aria + search. */
  var PROP_TABS = [
    { id: "principal", label: "Principal", icon: "list" },
    { id: "estilo", label: "Estilo", icon: "color" },
    { id: "avancado", label: "Avançado", icon: "settings" },
    { id: "responsivo", label: "Responsivo", icon: "responsive" },
  ];

  var PROP_GROUP_LABELS = {
    elemento: "Elemento",
    conteudo: "Conteúdo",
    layout: "Layout",
    midia: "Mídia",
    link: "Link & navegação",
    widget: "Opções do widget",
    dispositivo: "Dispositivo",
    tipografia: "Tipografia",
    fundo: "Fundo",
    borda: "Borda",
    sombra: "Sombra & opacidade",
    efeitos: "Efeitos",
    tamanho: "Tamanho",
    espacamento: "Espaçamento",
    posicionamento: "Posicionamento",
    acoes: "Ações",
    resp_layout: "Layout responsivo",
    resp_visibilidade: "Visibilidade",
  };

  /** Default open accordion groups per tab (first paint). */
  var PROP_GROUP_DEFAULT_OPEN = {
    elemento: true,
    conteudo: true,
    layout: true,
    midia: true,
    link: true,
    widget: true,
    dispositivo: true,
    tipografia: true,
    fundo: true,
    borda: true,
    sombra: false,
    efeitos: false,
    tamanho: true,
    espacamento: true,
    posicionamento: true,
    acoes: false,
    resp_layout: true,
    resp_visibilidade: true,
  };

  /**
   * Style property → { tab, group, keywords }.
   * Adding a new style key only needs an entry here — UI groups automatically.
   */
  var STYLE_PROP_META = {
    color: { tab: "estilo", group: "tipografia", keywords: "cor texto color" },
    font_family: { tab: "estilo", group: "tipografia", keywords: "fonte família tipografia font family" },
    font_size: { tab: "estilo", group: "tipografia", keywords: "tamanho fonte font size" },
    font_weight: { tab: "estilo", group: "tipografia", keywords: "peso weight bold" },
    font_style: { tab: "estilo", group: "tipografia", keywords: "itálico italic estilo" },
    line_height: { tab: "estilo", group: "tipografia", keywords: "altura linha line height" },
    letter_spacing: { tab: "estilo", group: "tipografia", keywords: "espaçamento letras letter spacing" },
    text_transform: { tab: "estilo", group: "tipografia", keywords: "maiúsculas transform" },
    text_decoration: { tab: "estilo", group: "tipografia", keywords: "sublinhado decoração underline" },
    text_align: { tab: "estilo", group: "tipografia", keywords: "alinhamento texto align" },
    background: { tab: "estilo", group: "fundo", keywords: "fundo cor background" },
    bg_type: { tab: "estilo", group: "fundo", keywords: "tipo fundo" },
    bg_gradient: { tab: "estilo", group: "fundo", keywords: "gradiente gradient" },
    bg_grad_kind: { tab: "estilo", group: "fundo", keywords: "gradiente linear radial" },
    bg_grad_angle: { tab: "estilo", group: "fundo", keywords: "ângulo gradiente" },
    bg_grad_stops: { tab: "estilo", group: "fundo", keywords: "cores gradiente" },
    bg_image: { tab: "estilo", group: "fundo", keywords: "imagem fundo background image" },
    bg_size: { tab: "estilo", group: "fundo", keywords: "tamanho fundo cover contain" },
    bg_position: { tab: "estilo", group: "fundo", keywords: "posição fundo" },
    bg_repeat: { tab: "estilo", group: "fundo", keywords: "repetição fundo repeat" },
    bg_attachment: { tab: "estilo", group: "fundo", keywords: "fixação parallax scroll fixed" },
    radius: { tab: "estilo", group: "borda", keywords: "cantos arredondados border-radius raio quatro lados" },
    border: { tab: "estilo", group: "borda", keywords: "borda border contorno" },
    border_width: { tab: "estilo", group: "borda", keywords: "borda espessura border-width lados" },
    border_style: { tab: "estilo", group: "borda", keywords: "borda estilo solid dashed" },
    border_color: { tab: "estilo", group: "borda", keywords: "borda cor border-color" },
    fx_shadow: { tab: "estilo", group: "sombra", keywords: "sombra shadow box-shadow" },
    fx_opacity: { tab: "estilo", group: "sombra", keywords: "opacidade opacity" },
    fx_entrance: { tab: "estilo", group: "efeitos", keywords: "animação entrada entrance" },
    fx_entrance_duration: { tab: "estilo", group: "efeitos", keywords: "duração animação" },
    fx_entrance_delay: { tab: "estilo", group: "efeitos", keywords: "atraso animação delay" },
    fx_hover: { tab: "estilo", group: "efeitos", keywords: "hover efeito" },
    fx_hover_duration: { tab: "estilo", group: "efeitos", keywords: "duração hover" },
    fx_rotate: { tab: "estilo", group: "efeitos", keywords: "rotação transform rotate" },
    fx_scale: { tab: "estilo", group: "efeitos", keywords: "escala scale transform" },
    fx_translate_x: { tab: "estilo", group: "efeitos", keywords: "deslocar x translate" },
    fx_translate_y: { tab: "estilo", group: "efeitos", keywords: "deslocar y translate" },
    fx_transition: { tab: "estilo", group: "efeitos", keywords: "transição transition" },
    width: { tab: "avancado", group: "tamanho", keywords: "largura width" },
    height: { tab: "avancado", group: "tamanho", keywords: "altura height" },
    min_height: { tab: "avancado", group: "tamanho", keywords: "altura mínima min-height" },
    max_width: { tab: "avancado", group: "tamanho", keywords: "largura máxima max-width" },
    margin: { tab: "avancado", group: "espacamento", keywords: "margem margin externa quatro lados" },
    padding: { tab: "avancado", group: "espacamento", keywords: "padding recuo espaçamento interno quatro lados" },
    z_index: { tab: "avancado", group: "posicionamento", keywords: "z-index ordem stacking" },
    hide: { tab: "responsivo", group: "resp_visibilidade", keywords: "ocultar esconder visibilidade hide display" },
  };

  /**
   * Setting/schema property → { tab, group, keywords }.
   * Unlisted keys fall back to resolveSettingMeta() heuristics.
   */
  var SETTING_PROP_META = {
    display_name: { tab: "principal", group: "elemento", keywords: "nome elemento display" },
    text: { tab: "principal", group: "conteudo", keywords: "texto conteúdo" },
    title: { tab: "principal", group: "conteudo", keywords: "título" },
    submit_label: { tab: "principal", group: "conteudo", keywords: "botão texto publicar" },
    btn_bg: { tab: "principal", group: "conteudo", keywords: "botão cor fundo" },
    btn_color: { tab: "principal", group: "conteudo", keywords: "botão cor texto" },
    btn_full_width: { tab: "principal", group: "conteudo", keywords: "botão largura total" },
    btn_radius: { tab: "principal", group: "conteudo", keywords: "botão cantos radius" },
    show_title: { tab: "principal", group: "conteudo", keywords: "mostrar título" },
    show_list: { tab: "principal", group: "conteudo", keywords: "listar comentários" },
    list_title: { tab: "principal", group: "conteudo", keywords: "título lista" },
    justify_content: { tab: "principal", group: "layout", keywords: "alinhar justify horizontal esquerda centro direita" },
    align_items: { tab: "principal", group: "layout", keywords: "alinhar align vertical topo meio base" },
    direction: { tab: "principal", group: "layout", keywords: "direção linha coluna row column" },
    gap: { tab: "principal", group: "layout", keywords: "espaço gap espaçamento" },
    content: { tab: "principal", group: "conteudo", keywords: "conteúdo texto" },
    description: { tab: "principal", group: "conteudo", keywords: "descrição" },
    subtitle: { tab: "principal", group: "conteudo", keywords: "subtítulo" },
    prefix: { tab: "principal", group: "conteudo", keywords: "prefixo" },
    suffix: { tab: "principal", group: "conteudo", keywords: "sufixo" },
    words: { tab: "principal", group: "conteudo", keywords: "palavras" },
    code: { tab: "principal", group: "conteudo", keywords: "código html shortcode" },
    number: { tab: "principal", group: "conteudo", keywords: "número contador" },
    name: { tab: "principal", group: "conteudo", keywords: "nome" },
    role: { tab: "principal", group: "conteudo", keywords: "cargo" },
    address: { tab: "principal", group: "conteudo", keywords: "endereço" },
    rating: { tab: "principal", group: "conteudo", keywords: "nota avaliação" },
    message: { tab: "principal", group: "conteudo", keywords: "mensagem" },
    button_text: { tab: "principal", group: "conteudo", keywords: "texto botão" },
    read_more_text: { tab: "principal", group: "conteudo", keywords: "leia mais" },
    submit_label: { tab: "principal", group: "conteudo", keywords: "botão enviar" },
    list_title: { tab: "principal", group: "conteudo", keywords: "título lista" },
    toggle_label: { tab: "principal", group: "conteudo", keywords: "texto menu mobile" },
    url: { tab: "principal", group: "midia", keywords: "url link imagem vídeo" },
    image: { tab: "principal", group: "midia", keywords: "imagem foto" },
    images: { tab: "principal", group: "midia", keywords: "imagens galeria" },
    icon: { tab: "principal", group: "midia", keywords: "ícone icon" },
    alt: { tab: "principal", group: "midia", keywords: "alt texto alternativo" },
    link: { tab: "principal", group: "link", keywords: "link url" },
    button_url: { tab: "principal", group: "link", keywords: "link botão url" },
    align: { tab: "principal", group: "layout", keywords: "alinhamento" },
    layout: { tab: "principal", group: "layout", keywords: "layout modelo" },
    direction: { tab: "principal", group: "layout", keywords: "direção flex grid" },
    columns: { tab: "principal", group: "layout", keywords: "colunas grid" },
    gap: { tab: "principal", group: "layout", keywords: "espaço gap" },
    width: { tab: "principal", group: "layout", keywords: "largura" },
    height: { tab: "principal", group: "layout", keywords: "altura" },
    col_span: { tab: "principal", group: "layout", keywords: "span coluna" },
    content_width: { tab: "principal", group: "layout", keywords: "largura conteúdo" },
    max_width: { tab: "principal", group: "layout", keywords: "largura máxima" },
    size: { tab: "principal", group: "layout", keywords: "tamanho" },
    tag: { tab: "principal", group: "layout", keywords: "tag html h1 h2" },
    per_view: { tab: "principal", group: "layout", keywords: "visíveis slider" },
    ratio: { tab: "principal", group: "layout", keywords: "proporção aspect" },
    image_position: { tab: "principal", group: "layout", keywords: "posição imagem" },
    media_width: { tab: "principal", group: "layout", keywords: "largura mídia" },
    object_fit: { tab: "principal", group: "layout", keywords: "encaixe cover contain recorte quadro" },
    object_position: {
      tab: "principal",
      group: "layout",
      keywords: "posição foco recorte topo base centro object-position quadro imagem bolinha",
    },
    image_object_position: {
      tab: "principal",
      group: "layout",
      keywords: "posição foco recorte topo base centro object-position quadro imagem bolinha loop",
    },
    image_object_fit: {
      tab: "principal",
      group: "layout",
      keywords: "encaixe cover contain recorte imagem loop",
    },
    image_height: { tab: "principal", group: "layout", keywords: "altura imagem" },
    radius: { tab: "estilo", group: "borda", keywords: "cantos arredondados raio" },
    shadow: { tab: "estilo", group: "sombra", keywords: "sombra" },
    border: { tab: "estilo", group: "borda", keywords: "borda contorno" },
    border_color: { tab: "estilo", group: "borda", keywords: "cor borda" },
    accent: { tab: "estilo", group: "tipografia", keywords: "cor destaque accent" },
    hover_color: { tab: "estilo", group: "tipografia", keywords: "hover cor mouse" },
    color: { tab: "estilo", group: "tipografia", keywords: "cor" },
    opacity: { tab: "estilo", group: "sombra", keywords: "opacidade" },
    shape: { tab: "estilo", group: "borda", keywords: "forma círculo" },
    style: { tab: "principal", group: "widget", keywords: "estilo visual" },
    items: { tab: "principal", group: "widget", keywords: "itens lista" },
    source: { tab: "principal", group: "widget", keywords: "fonte origem" },
    provider: { tab: "principal", group: "widget", keywords: "provedor" },
    autoplay: { tab: "principal", group: "widget", keywords: "autoplay" },
    dots: { tab: "principal", group: "widget", keywords: "pontos" },
    arrows: { tab: "principal", group: "widget", keywords: "setas" },
    enabled: { tab: "principal", group: "widget", keywords: "ativo" },
    mobile_style: { tab: "responsivo", group: "resp_layout", keywords: "mobile menu drawer" },
    show_toggle: { tab: "responsivo", group: "resp_layout", keywords: "botão menu mobile" },
    columns_tablet: { tab: "responsivo", group: "resp_layout", keywords: "colunas tablet" },
    columns_mobile: { tab: "responsivo", group: "resp_layout", keywords: "colunas mobile" },
  };

  function resolveSettingMeta(key, label) {
    var base = String(key || "").replace(/__(desktop|tablet|mobile)$/, "");
    if (SETTING_PROP_META[base]) {
      var m = SETTING_PROP_META[base];
      return {
        tab: m.tab,
        group: m.group,
        keywords: (m.keywords || "") + " " + base + " " + (label || ""),
        label: label || base,
      };
    }
    var k = base.toLowerCase();
    var tab = "principal";
    var group = "widget";
    if (
      /^(text|title|content|description|subtitle|prefix|suffix|words|code|number|name|role|address|rating|message|submit_label|list_title|read_more_text|button_text|toggle_label|anchor)$/.test(
        k
      ) ||
      /_text$/.test(k)
    ) {
      group = "conteudo";
    } else if (
      /^(url|image|images|icon|alt|media|src)$/.test(k) ||
      /_(url|image|icon)$/.test(k)
    ) {
      group = "midia";
    } else if (/^(link|button_url|href)$/.test(k)) {
      group = "link";
    } else if (
      /^(align|layout|direction|columns|gap|width|height|col_span|content_width|max_width|size|tag|per_view|ratio|zoom)$/.test(
        k
      ) ||
      /_position$|_align$|_size$|_span$/.test(k) ||
      /^columns_/.test(k)
    ) {
      group = "layout";
      if (/tablet|mobile/.test(k)) {
        tab = "responsivo";
        group = "resp_layout";
      }
    } else if (/radius|shadow|border|opacity|accent|shape|font_/.test(k) && !/source|query|template/.test(k)) {
      tab = "estilo";
      if (/radius|border|shape/.test(k)) group = "borda";
      else if (/shadow|opacity/.test(k)) group = "sombra";
      else group = "tipografia";
    } else if (/mobile|tablet|drawer/.test(k)) {
      tab = "responsivo";
      group = "resp_layout";
    }
    return {
      tab: tab,
      group: group,
      keywords: (label || "") + " " + base + " " + k,
      label: label || base,
    };
  }

  function resolveStyleMeta(key, label) {
    var m = STYLE_PROP_META[key];
    if (m) {
      return {
        tab: m.tab,
        group: m.group,
        keywords: (m.keywords || "") + " " + key + " " + (label || ""),
        label: label || key,
      };
    }
    return {
      tab: "avancado",
      group: "posicionamento",
      keywords: (label || "") + " " + key,
      label: label || key,
    };
  }

  function createPropsCollector() {
    var bags = { principal: {}, estilo: {}, avancado: {}, responsivo: {} };
    var order = { principal: [], estilo: [], avancado: [], responsivo: [] };
    function add(meta, fieldHtml) {
      if (!fieldHtml) return;
      var tab = (meta && meta.tab) || "principal";
      var group = (meta && meta.group) || "widget";
      if (!bags[tab]) tab = "principal";
      if (!bags[tab][group]) {
        bags[tab][group] = [];
        order[tab].push(group);
      }
      var search =
        ((meta && meta.keywords) || "") +
        " " +
        ((meta && meta.label) || "") +
        " " +
        ((meta && meta.key) || "");
      bags[tab][group].push({
        html: fieldHtml,
        search: String(search).toLowerCase(),
        key: (meta && meta.key) || "",
        label: (meta && meta.label) || "",
      });
    }
    function addStyle(key, label, fieldHtml) {
      var meta = resolveStyleMeta(key, label);
      meta.key = key;
      meta.label = label || key;
      add(meta, fieldHtml);
    }
    function addSetting(key, label, fieldHtml) {
      var meta = resolveSettingMeta(key, label);
      meta.key = key;
      meta.label = label || key;
      add(meta, fieldHtml);
    }
    function addRaw(tab, group, key, label, fieldHtml, keywords) {
      add(
        {
          tab: tab,
          group: group,
          key: key || "",
          label: label || "",
          keywords: keywords || label || key || "",
        },
        fieldHtml
      );
    }
    return { add: add, addStyle: addStyle, addSetting: addSetting, addRaw: addRaw, bags: bags, order: order };
  }

  function isPropsGroupOpen(groupId) {
    if (state.propsOpenGroups && Object.prototype.hasOwnProperty.call(state.propsOpenGroups, groupId)) {
      return !!state.propsOpenGroups[groupId];
    }
    return PROP_GROUP_DEFAULT_OPEN[groupId] !== false;
  }

  function wrapPropsField(item) {
    return (
      '<div class="ab-props-field" data-search="' +
      esc(item.search || "") +
      '" data-prop-key="' +
      esc(item.key || "") +
      '">' +
      item.html +
      "</div>"
    );
  }

  function renderPropsAccordion(tabId, col) {
    var groups = col.order[tabId] || [];
    var bags = col.bags[tabId] || {};
    var q = String(state.propsSearch || "")
      .toLowerCase()
      .trim();
    var html = "";
    var anyVisible = false;
    groups.forEach(function (gid) {
      var items = bags[gid] || [];
      if (!items.length) return;
      var visibleItems = items;
      if (q) {
        visibleItems = items.filter(function (it) {
          return (it.search || "").indexOf(q) >= 0 || (it.label || "").toLowerCase().indexOf(q) >= 0;
        });
      }
      if (!visibleItems.length) return;
      anyVisible = true;
      var open = q ? true : isPropsGroupOpen(gid);
      var gLabel = PROP_GROUP_LABELS[gid] || gid;
      html +=
        '<div class="ab-props-acc' +
        (open ? " is-open" : "") +
        '" data-props-group="' +
        esc(gid) +
        '">' +
        '<button type="button" class="ab-props-acc__hd" data-props-acc-toggle="' +
        esc(gid) +
        '" aria-expanded="' +
        (open ? "true" : "false") +
        '">' +
        '<span class="ab-props-acc__chev" aria-hidden="true">▾</span>' +
        '<span class="ab-props-acc__title">' +
        esc(gLabel) +
        "</span>" +
        '<span class="ab-props-acc__count">' +
        visibleItems.length +
        "</span>" +
        "</button>" +
        '<div class="ab-props-acc__bd">' +
        visibleItems.map(wrapPropsField).join("") +
        "</div></div>";
    });
    if (!anyVisible) {
      html =
        '<div class="ab-props-empty">' +
        (q
          ? 'Nenhuma propriedade para “' + esc(q) + '” nesta aba.'
          : "Nenhuma propriedade nesta aba para este elemento.") +
        "</div>";
    }
    return html;
  }

  function renderPropsShell(col, typeLabel) {
    var active = state.propsTab || "principal";
    if (!PROP_TABS.some(function (t) {
      return t.id === active;
    })) {
      active = "principal";
      state.propsTab = active;
    }
    // When searching, prefer the tab that has the most matches (or keep current if it has any).
    var q = String(state.propsSearch || "")
      .toLowerCase()
      .trim();
    var tabCounts = {};
    PROP_TABS.forEach(function (t) {
      var n = 0;
      (col.order[t.id] || []).forEach(function (gid) {
        (col.bags[t.id][gid] || []).forEach(function (it) {
          if (
            !q ||
            (it.search || "").indexOf(q) >= 0 ||
            (it.label || "").toLowerCase().indexOf(q) >= 0
          ) {
            n++;
          }
        });
      });
      tabCounts[t.id] = n;
    });
    if (q && tabCounts[active] === 0) {
      var best = active;
      var bestN = 0;
      PROP_TABS.forEach(function (t) {
        if (tabCounts[t.id] > bestN) {
          bestN = tabCounts[t.id];
          best = t.id;
        }
      });
      if (bestN > 0) {
        active = best;
        state.propsTab = active;
      }
    }

    var tabsHtml = PROP_TABS.map(function (t) {
      var cnt = tabCounts[t.id] || 0;
      return (
        '<button type="button" class="ab-props-tab' +
        (t.id === active ? " is-active" : "") +
        (cnt === 0 && q ? " is-empty" : "") +
        '" data-props-tab="' +
        esc(t.id) +
        '" title="' +
        esc(t.label) +
        '"' +
        ' aria-label="' +
        esc(t.label) +
        '" role="tab" aria-selected="' +
        (t.id === active ? "true" : "false") +
        '">' +
        '<span class="ab-props-tab__ico" aria-hidden="true">' +
        svgIcon(t.icon || "list") +
        "</span>" +
        (q && cnt ? '<span class="ab-props-tab__n">' + cnt + "</span>" : "") +
        "</button>"
      );
    }).join("");

    return (
      '<div class="ab-props-head">' +
      '<div class="ab-props-type">' +
      esc(typeLabel || "Elemento") +
      "</div>" +
      '<div class="ab-props-tabs" role="tablist">' +
      tabsHtml +
      "</div>" +
      '<div class="ab-props-search-wrap">' +
      '<span class="ab-props-search-ico" aria-hidden="true">🔍</span>' +
      '<input type="search" class="ab-props-search" id="ab-props-search" placeholder="Buscar propriedade…" value="' +
      esc(state.propsSearch || "") +
      '" autocomplete="off"/>' +
      (state.propsSearch
        ? '<button type="button" class="ab-props-search-clear" id="ab-props-search-clear" title="Limpar">×</button>'
        : "") +
      "</div>" +
      "</div>" +
      '<div class="ab-props-tab-panel" data-active-tab="' +
      esc(active) +
      '">' +
      renderPropsAccordion(active, col) +
      "</div>"
    );
  }

  function effectsFieldsOnly(st) {
    // Same controls as effectsPanelHtml but without section title (accordion provides it).
    var html = "";
    html +=
      '<div class="ab-field-hint">Entrada ao rolar, hover, transform e sombra — por dispositivo.</div>';
    html += fieldSelect("fx_entrance", "Animação de entrada", st.fx_entrance || "", FX_ENTRANCE_OPTS, "data-style");
    html += fieldInput("fx_entrance_duration", "Duração entrada (ms)", st.fx_entrance_duration || "", {
      type: "number",
      attr: "data-style",
      devices: true,
      placeholder: "800",
    });
    html += fieldInput("fx_entrance_delay", "Atraso entrada (ms)", st.fx_entrance_delay || "", {
      type: "number",
      attr: "data-style",
      devices: true,
      placeholder: "0",
    });
    html += fieldSelect("fx_hover", "Efeito hover", st.fx_hover || "", FX_HOVER_OPTS, "data-style");
    html += fieldInput("fx_hover_duration", "Duração hover (ms)", st.fx_hover_duration || "", {
      type: "number",
      attr: "data-style",
      devices: true,
      placeholder: "300",
    });
    html += fieldInput("fx_rotate", "Rotação (°)", st.fx_rotate || "", {
      type: "text",
      attr: "data-style",
      devices: true,
      placeholder: "ex: 3 ou -5",
    });
    html += fieldInput("fx_scale", "Escala", st.fx_scale || "", {
      type: "text",
      attr: "data-style",
      devices: true,
      placeholder: "ex: 1.05",
    });
    html += fieldInput("fx_translate_x", "Deslocar X", st.fx_translate_x || "", {
      type: "text",
      attr: "data-style",
      devices: true,
      placeholder: "ex: 10px",
    });
    html += fieldInput("fx_translate_y", "Deslocar Y", st.fx_translate_y || "", {
      type: "text",
      attr: "data-style",
      devices: true,
      placeholder: "ex: -8px",
    });
    html += fieldInput("fx_opacity", "Opacidade", st.fx_opacity || "", {
      type: "text",
      attr: "data-style",
      devices: true,
      placeholder: "0–1 ou %",
    });
    html += fieldSelect("fx_shadow", "Sombra", st.fx_shadow || "", FX_SHADOW_OPTS, "data-style");
    html += fieldInput("fx_transition", "Transição base (ms)", st.fx_transition || "", {
      type: "number",
      attr: "data-style",
      devices: true,
      placeholder: "300",
    });
    html +=
      '<button type="button" class="ab-btn ab-btn--primary ab-fx-preview" style="width:100%;margin-top:4px">▶ Pré-visualizar entrada</button>';
    return html;
  }

  function paintProps() {
    var box = document.getElementById("ab-props");
    if (!box) return;

    // Multi-select panel (2+ nodes)
    var multiIds = getSelectedIds();
    if (multiIds.length > 1) {
      box.innerHTML =
        '<div class="ab-props-section-title">Seleção múltipla</div>' +
        '<p class="ab-field-hint" style="margin:0 0 12px"><strong>' +
        multiIds.length +
        "</strong> elementos · Ctrl/⌘+clique para ajustar</p>" +
        '<div style="display:flex;flex-direction:column;gap:8px">' +
        '<button type="button" class="ab-btn ab-btn--primary" id="ab-multi-row">⟷ Colocar na mesma linha</button>' +
        '<button type="button" class="ab-btn" id="ab-multi-cols">▥ Distribuir em colunas</button>' +
        '<button type="button" class="ab-btn" id="ab-multi-wrap">▢ Envolver em container</button>' +
        '<button type="button" class="ab-btn ab-btn--ghost" id="ab-multi-clear">Limpar seleção</button>' +
        "</div>" +
        '<p class="ab-field-hint" style="margin-top:12px">Os itens precisam estar no <strong>mesmo</strong> container ou seção.</p>';
      var br = document.getElementById("ab-multi-row");
      var bc = document.getElementById("ab-multi-cols");
      var bw = document.getElementById("ab-multi-wrap");
      var bcl = document.getElementById("ab-multi-clear");
      if (br)
        br.addEventListener("click", function () {
          execute(function () {
            wrapSelectionInRow(multiIds);
          });
        });
      if (bc)
        bc.addEventListener("click", function () {
          execute(function () {
            wrapSelectionInColumns(multiIds);
          });
        });
      if (bw)
        bw.addEventListener("click", function () {
          execute(function () {
            wrapSelectionInContainer(multiIds);
          });
        });
      if (bcl)
        bcl.addEventListener("click", function () {
          state.selectedIds = state.selectedId ? [state.selectedId] : [];
          updateSelectionUI();
        });
      return;
    }

    if (!state.selectedId) {
      ensureDocSettings();
      if (isThemePart) {
        box.innerHTML =
          '<div class="ab-props-section-title">' +
          (editorMode === "footer" ? "Footer" : "Header") +
          "</div>" +
          '<div class="ab-props-empty">Clique num elemento do ' +
          (editorMode === "footer" ? "footer" : "header") +
          " para editar. Use ⚙ para condições de exibição.</div>";
        return;
      }
      box.innerHTML =
        '<div class="ab-props-section-title">Página</div>' +
        '<div class="ab-props-empty">Clique em um elemento no miolo para editar.<br/><span class="ab-field-hint">Dica: clique <strong>direto no botão/texto</strong> (não no container), depois <strong>Ctrl/⌘+clique</strong> nos outros para a mesma linha.</span></div>' +
        '<div class="ab-props-section-title" style="margin-top:14px">Header &amp; Footer</div>' +
        '<p class="ab-field-hint" style="margin:0 0 8px">Edite o layout global no Theme Builder (mesmo editor visual).</p>' +
        '<div class="ab-chrome-edit-row">' +
        '<button type="button" class="ab-btn ab-btn--primary" id="ab-prop-edit-header">✎ Editar Header</button>' +
        '<button type="button" class="ab-btn" id="ab-prop-edit-footer">✎ Editar Footer</button>' +
        "</div>" +
        '<label class="ab-toggle' +
        (showHeader() ? " is-on" : "") +
        '" style="width:100%;margin:12px 0 8px">' +
        '<span class="ab-check-ui" aria-hidden="true"></span>' +
        '<input type="checkbox" id="ab-prop-header"' +
        (showHeader() ? " checked" : "") +
        "/>" +
        '<span class="ab-toggle-label">Exibir header nesta página</span></label>' +
        '<label class="ab-toggle' +
        (showFooter() ? " is-on" : "") +
        '" style="width:100%">' +
        '<span class="ab-check-ui" aria-hidden="true"></span>' +
        '<input type="checkbox" id="ab-prop-footer"' +
        (showFooter() ? " checked" : "") +
        "/>" +
        '<span class="ab-toggle-label">Exibir footer nesta página</span></label>';
      var ph = document.getElementById("ab-prop-header");
      var pf = document.getElementById("ab-prop-footer");
      var peh = document.getElementById("ab-prop-edit-header");
      var pef = document.getElementById("ab-prop-edit-footer");
      if (ph)
        ph.addEventListener("change", function () {
          var lab = ph.closest(".ab-toggle");
          if (lab) lab.classList.toggle("is-on", ph.checked);
          setChromeFlag("showHeader", ph.checked);
        });
      if (pf)
        pf.addEventListener("change", function () {
          var lab = pf.closest(".ab-toggle");
          if (lab) lab.classList.toggle("is-on", pf.checked);
          setChromeFlag("showFooter", pf.checked);
        });
      if (peh)
        peh.addEventListener("click", function () {
          openThemePartEditor("header");
        });
      if (pef)
        pef.addEventListener("click", function () {
          openThemePartEditor("footer");
        });
      return;
    }
    var node = getNode(state.selectedId);
    if (!node) {
      box.innerHTML = '<div class="ab-props-empty">Elemento não encontrado.</div>';
      return;
    }
    normalizeNodeStyles(node);
    // Edit styles for active breakpoint only.
    var st = ensureStyles(node, state.breakpoint);
    var settings = node.settings || {};
    // Ensure root has settings defaults
    if (node.type === "root") {
      settings = node.settings = node.settings || {};
      if (!settings.content_width) settings.content_width = "full";
      if (!settings.max_width) settings.max_width = "1200px";
    }
    var typeLabel = TYPE_LABELS[node.type] || node.type;
    var schema = PROP_SCHEMA[node.type] || [];
    var sp = widgetStyleProfile(node.type);
    var col = createPropsCollector();

    // ── PRINCIPAL: elemento + schema settings ──────────────────────────────
    col.addSetting(
      "display_name",
      "Nome no editor",
      '<div class="ab-field ab-field-name-inline">' +
        '<span class="ab-name-type" title="Tipo do widget">' +
        esc(typeLabel) +
        "</span>" +
        '<input type="text" class="ab-name-input" data-prop="display_name" value="' +
        esc(settings.display_name || "") +
        '" placeholder="Nome no editor" autocomplete="off"/>' +
        "</div>" +
        '<span class="ab-field-hint">O tipo fica à esquerda; o nome aparece nas camadas e na barra roxa.</span>'
    );

    if (node.type === "loop_grid") {
      col.addRaw(
        "principal",
        "layout",
        "bp_hint",
        "Breakpoint",
        styleBreakpointRowHtml() +
          '<div class="ab-field-hint">Colunas, espaçamento, imagem, meta e card mudam por dispositivo. Consulta (categoria, ordem…) é global.</div>',
        "desktop tablet mobile breakpoint dispositivo"
      );
    }

    if (!schema.length && node.type === "divider") {
      col.addRaw(
        "principal",
        "conteudo",
        "divider_empty",
        "Divisor",
        '<div class="ab-props-empty">Divisor não tem opções de conteúdo.</div>',
        "divisor"
      );
    }

    schema.forEach(function (field) {
      var fh = renderSchemaField(field, settings);
      if (!fh) return;
      col.addSetting(field.key, field.label, fh);
    });

    // Wrap any leaf (or single container) in a layout container
    if (node.type !== "root" && node.type !== "section") {
      col.addRaw(
        "principal",
        "layout",
        "wrap_in_container",
        "Container",
        '<div style="display:flex;flex-direction:column;gap:8px">' +
          '<button type="button" class="ab-btn ab-btn--primary" id="ab-wrap-container">▢ Envolver em container</button>' +
          '<span class="ab-field-hint">Cria um container em volta deste elemento. Depois use <strong>Em linha</strong>, alinhamento e gap no container — ideal para posicionar imagens e textos.</span>' +
          "</div>",
        "envolver container wrap layout posição"
      );
    }

    if (node.type === "image") {
      col.addRaw(
        "principal",
        "layout",
        "image_pos_hint",
        "Dica de recorte",
        '<p class="ab-field-hint">Com <strong>altura fixa</strong> no quadro (Estilo → Altura), use o <strong>quadrado com bolinha</strong> para escolher o foco (rosto, topo, base). ' +
          "Encaixe = <em>Cobrir o quadro</em> para recortar. Alinhamento E/C/D só move o bloco na página.</p>",
        "posição imagem foco recorte bolinha object-position"
      );
    }

    // Container layout convert shortcuts + quick align for rows
    if (node.type === "container") {
      var dirNow = (settings.direction || "column") === "row" ? "row" : "column";
      var jNow = settings.justify_content || "flex-start";
      var aNow = settings.align_items || (dirNow === "row" ? "flex-start" : "stretch");
      col.addRaw(
        "principal",
        "layout",
        "container_convert",
        "Converter layout",
        '<div style="display:flex;flex-direction:column;gap:8px">' +
          '<button type="button" class="ab-btn' +
          (dirNow === "row" ? " ab-btn--primary" : "") +
          '" id="ab-ctr-to-row">⟷ Em linha (horizontal)</button>' +
          '<button type="button" class="ab-btn' +
          (dirNow === "column" ? " ab-btn--primary" : "") +
          '" id="ab-ctr-to-col">⬇ Empilhar (coluna)</button>' +
          '<button type="button" class="ab-btn" id="ab-ctr-to-cols">▥ Filhos em colunas iguais</button>' +
          '<span class="ab-field-hint">Linha = itens lado a lado. Empilhar = um abaixo do outro. Colunas = cada filho vira uma coluna.</span>' +
          "</div>",
        "converter linha coluna colunas layout container"
      );
      if (dirNow === "row") {
        col.addRaw(
          "principal",
          "layout",
          "container_row_align",
          "Alinhar itens da linha",
          '<div class="ab-row-align">' +
            '<div class="ab-row-align__lab">Horizontal</div>' +
            '<div class="ab-row-align__btns" data-row-align="justify_content">' +
            '<button type="button" class="ab-row-align__btn' +
            (jNow === "flex-start" ? " is-on" : "") +
            '" data-val="flex-start" title="Esquerda">⟸</button>' +
            '<button type="button" class="ab-row-align__btn' +
            (jNow === "center" ? " is-on" : "") +
            '" data-val="center" title="Centro">⇔</button>' +
            '<button type="button" class="ab-row-align__btn' +
            (jNow === "flex-end" ? " is-on" : "") +
            '" data-val="flex-end" title="Direita">⟹</button>' +
            '<button type="button" class="ab-row-align__btn' +
            (jNow === "space-between" ? " is-on" : "") +
            '" data-val="space-between" title="Espaçar">▤</button>' +
            "</div>" +
            '<div class="ab-row-align__lab">Vertical</div>' +
            '<div class="ab-row-align__btns" data-row-align="align_items">' +
            '<button type="button" class="ab-row-align__btn' +
            (aNow === "flex-start" ? " is-on" : "") +
            '" data-val="flex-start" title="Topo">⬆</button>' +
            '<button type="button" class="ab-row-align__btn' +
            (aNow === "center" ? " is-on" : "") +
            '" data-val="center" title="Meio">⬤</button>' +
            '<button type="button" class="ab-row-align__btn' +
            (aNow === "flex-end" ? " is-on" : "") +
            '" data-val="flex-end" title="Base">⬇</button>' +
            "</div>" +
            '<span class="ab-field-hint">Também em <strong>Alinhamento na linha</strong> abaixo (selects).</span>' +
            "</div>",
          "alinhar linha esquerda centro direita topo base justify"
        );
      }
    }

    if (node.type === "root") {
      var cwHint = settings.content_width || "full";
      if (cwHint === "full") {
        col.addRaw(
          "principal",
          "layout",
          "root_width_hint",
          "Largura",
          '<p class="ab-field-hint">Modo <strong>Largura total (100%)</strong>: a Raiz ocupa 100% da área. O valor 1200px de Wide não se aplica.</p>',
          "largura wide full"
        );
      } else if (cwHint === "wide") {
        col.addRaw(
          "principal",
          "layout",
          "root_width_hint",
          "Largura",
          '<p class="ab-field-hint">Modo <strong>Wide</strong>: use “Largura máxima (só no modo Wide)” para definir o teto (ex.: 1200px).</p>',
          "largura wide"
        );
      }
      col.addRaw(
        "principal",
        "layout",
        "root_hint",
        "Raiz",
        '<p class="ab-field-hint">A <strong>Raiz</strong> controla fundo, largura e o <strong>espaço entre seções</strong>. Margem/padding da Raiz envolvem a página.</p>',
        "raiz seções"
      );
    }

    // ── ESTILO: tipografia, fundo, borda, efeitos ──────────────────────────
    col.addRaw(
      "estilo",
      "dispositivo",
      "bp_estilo",
      "Dispositivo",
      styleBreakpointRowHtml() +
        '<div class="ab-field-hint">Editando estilos de <strong>' +
        esc(BP_LABEL[state.breakpoint] || "Desktop") +
        "</strong>. Troque o dispositivo aqui ou na aba Responsivo.</div>",
      "desktop tablet mobile breakpoint dispositivo"
    );

    if (sp.typography) {
      if (node.type === "container" || node.type === "section") {
        col.addRaw(
          "estilo",
          "tipografia",
          "layout_typo_hint",
          "Tipografia do container",
          '<p class="ab-field-hint">Estes estilos aplicam-se a <strong>todos os textos e títulos</strong> dentro deste ' +
            (node.type === "section" ? "seção" : "container") +
            ". Use “Aplicar nos filhos” se algum texto já tiver fonte própria e não herdar.</p>" +
            '<button type="button" class="ab-btn" id="ab-typo-apply-children" style="width:100%;margin-top:6px">Aplicar tipografia nos textos filhos</button>',
          "fonte tipografia filhos cascade herdar"
        );
      }
      col.addStyle(
        "font_family",
        "Família da fonte",
        fieldSelectOrCustom("font_family", "Família da fonte", st.font_family || "", FONT_FAMILY_OPTS, "data-style")
      );
      col.addStyle(
        "font_family",
        "Google Fonts",
        '<div class="ab-field ab-field-gf-launch">' +
          '<button type="button" class="ab-btn ab-btn--primary ab-gf-open" style="width:100%">' +
          "🔤 Biblioteca Google Fonts</button>" +
          '<span class="ab-field-hint">Popup com preview de cada fonte e texto de exemplo editável.</span>' +
          "</div>"
      );
      col.addStyle(
        "font_size",
        "Tamanho da fonte",
        fieldSelectOrCustom("font_size", "Tamanho da fonte", st.font_size || "", FONT_SIZE_OPTS, "data-style")
      );
      col.addStyle(
        "font_weight",
        "Peso",
        fieldSelect("font_weight", "Peso (weight)", st.font_weight || "", FONT_WEIGHT_OPTS, "data-style")
      );
      col.addStyle(
        "font_style",
        "Estilo",
        fieldSelect("font_style", "Estilo", st.font_style || "", FONT_STYLE_OPTS, "data-style")
      );
      col.addStyle(
        "line_height",
        "Altura da linha",
        fieldSelectOrCustom("line_height", "Altura da linha", st.line_height || "", LINE_HEIGHT_OPTS, "data-style")
      );
      col.addStyle(
        "letter_spacing",
        "Espaçamento entre letras",
        fieldSelectOrCustom(
          "letter_spacing",
          "Espaçamento entre letras",
          st.letter_spacing || "",
          LETTER_SPACING_OPTS,
          "data-style"
        )
      );
      col.addStyle(
        "text_transform",
        "Transformação",
        fieldSelect("text_transform", "Transformação", st.text_transform || "", TEXT_TRANSFORM_OPTS, "data-style")
      );
      col.addStyle(
        "text_decoration",
        "Decoração",
        fieldSelect("text_decoration", "Decoração", st.text_decoration || "", TEXT_DECORATION_OPTS, "data-style")
      );
      var allowJustify = node.type === "heading" || node.type === "text" || node.type === "dynamic";
      col.addStyle(
        "text_align",
        "Alinhamento",
        fieldAlignIcons(
          "text_align",
          "Alinhamento",
          st.text_align || settings.align || "left",
          "data-style",
          { justify: allowJustify }
        )
      );
      col.addStyle("color", "Cor do texto", fieldColor("color", "Cor do texto", st.color || "", "data-style"));
    } else if (sp.iconColor) {
      col.addStyle(
        "font_size",
        "Tamanho",
        fieldSelectOrCustom("font_size", "Tamanho", st.font_size || "", FONT_SIZE_OPTS, "data-style")
      );
      col.addStyle("color", "Cor", fieldColor("color", "Cor", st.color || "", "data-style"));
    } else if (sp.simpleColor) {
      col.addStyle("color", "Cor", fieldColor("color", "Cor", st.color || "", "data-style"));
    }

    if (sp.background) {
      col.addStyle("background", "Fundo", fieldBackground(st));
    }

    if (sp.backgroundImage) {
      col.addStyle(
        "bg_image",
        "Imagem de fundo",
        fieldMedia("bg_image", "Imagem de fundo", st.bg_image || "", "data-style")
      );
      col.addStyle(
        "bg_size",
        "Tamanho do fundo",
        fieldSelect("bg_size", "Tamanho do fundo", st.bg_size || "cover", BG_SIZE_OPTS, "data-style")
      );
      col.addStyle(
        "bg_position",
        "Posição do fundo",
        fieldSelect("bg_position", "Posição do fundo", st.bg_position || "center center", BG_POS_OPTS, "data-style")
      );
      col.addStyle(
        "bg_repeat",
        "Repetição",
        fieldSelect("bg_repeat", "Repetição", st.bg_repeat || "no-repeat", BG_REPEAT_OPTS, "data-style")
      );
      col.addStyle(
        "bg_attachment",
        "Fixação",
        fieldSelect(
          "bg_attachment",
          "Fixação",
          st.bg_attachment || "",
          [
            { value: "", label: "Padrão (scroll)" },
            { value: "scroll", label: "Scroll" },
            { value: "fixed", label: "Fixo (parallax leve)" },
          ],
          "data-style"
        )
      );
    }

    if (sp.radius) {
      col.addStyle(
        "radius",
        "Raio dos cantos",
        fieldDimensions("radius", "Raio dos cantos", st.radius || "", {
          mode: "corners",
          units: ["px", "%", "em", "rem"],
        })
      );
    }
    // Border: same surfaces as radius + any spaced layout leaf (heading/text keep margin/padding only).
    if (sp.radius || (sp.spacing && (node.type === "section" || node.type === "container" || node.type === "button" || node.type === "image" || node.type === "image_box" || node.type === "icon_box" || node.type === "info_box" || node.type === "preview_card" || node.type === "loop_grid"))) {
      col.addStyle("border", "Borda", fieldBorderBox(st));
    }

    if (sp.effects) {
      col.addStyle("fx_entrance", "Efeitos", effectsFieldsOnly(st));
    }

    // ── AVANÇADO: tamanho, espaçamento, z-index ────────────────────────────
    col.addRaw(
      "avancado",
      "dispositivo",
      "bp_avancado",
      "Dispositivo",
      styleBreakpointRowHtml() +
        '<div class="ab-field-hint">Largura, altura, margem e padding são por dispositivo (<strong>' +
        esc(BP_LABEL[state.breakpoint] || "Desktop") +
        "</strong>).</div>",
      "desktop tablet mobile breakpoint"
    );

    if (sp.width && node.type !== "root") {
      col.addStyle(
        "width",
        "Largura",
        fieldSelectOrCustom("width", "Largura", st.width || "", WIDTH_OPTS, "data-style") +
          (st.max_width && st.max_width !== "none" && st.max_width !== "100%"
            ? '<span class="ab-field-hint" style="color:#fbbf24">⚠ Largura máx. ativa (<strong>' +
              esc(String(st.max_width)) +
              "</strong>) — o bloco não passa desse teto mesmo com 100%.</span>"
            : "")
      );
      col.addStyle(
        "max_width",
        "Largura máxima",
        fieldSelectOrCustom(
          "max_width",
          "Largura máxima",
          st.max_width === "none" ? "" : st.max_width || "",
          MAX_WIDTH_OPTS,
          "data-style"
        ) +
          '<span class="ab-field-hint">Para <strong>borda a borda</strong>: Largura = 100% e Largura máxima = <em>Nenhum</em>.</span>'
      );
    }

    if (sp.height) {
      col.addStyle(
        "height",
        "Altura mínima / altura",
        fieldSelectOrCustom(
          "height",
          "Altura mínima / altura",
          st.height || st.min_height || "",
          [
            { value: "", label: "Automático" },
            { value: "auto", label: "Auto" },
            { value: "120px", label: "120 px" },
            { value: "200px", label: "200 px" },
            { value: "300px", label: "300 px" },
            { value: "50vh", label: "50% da tela" },
            { value: "100vh", label: "Altura da tela" },
            { value: "custom", label: "Personalizado…" },
          ],
          "data-style"
        )
      );
    }

    if (sp.spacing) {
      col.addStyle(
        "margin",
        "Margem externa",
        fieldDimensions("margin", "Margem externa", st.margin || "", {
          allowAuto: true,
          units: ["px", "%", "em", "rem", "vw", "vh"],
        })
      );
      col.addStyle(
        "padding",
        "Recuo (interno)",
        fieldDimensions("padding", "Recuo (interno)", st.padding || "", {
          allowAuto: false,
          units: ["px", "%", "em", "rem", "vw", "vh"],
        })
      );
    }

    if (node.type !== "root") {
      col.addStyle(
        "z_index",
        "Ordem (z-index)",
        '<div class="ab-field"><label>Ordem (z-index)</label>' +
          '<input type="number" data-style="z_index" value="' +
          esc(st.z_index != null && st.z_index !== "" ? String(st.z_index) : "") +
          '" placeholder="auto" step="1" style="width:100%;box-sizing:border-box"/>' +
          '<span class="ab-field-hint">Maior número fica na frente. Ordem das camadas também define quem cobre quem.</span></div>'
      );
    }

    // Copy / paste style + duplicate / delete → Avançado > Ações
    if (node.type !== "root") {
      var hasClip = !!(state.styleClipboard && Object.keys(state.styleClipboard).length);
      col.addRaw(
        "avancado",
        "acoes",
        "copy_style",
        "Copiar estilo",
        '<div style="display:flex;flex-direction:column;gap:8px">' +
          '<button type="button" class="ab-btn" id="ab-copy-style" title="Copia cores, fontes, fundo e borda deste breakpoint">📋 Copiar estilo</button>' +
          '<button type="button" class="ab-btn' +
          (hasClip ? "" : " ab-btn--ghost") +
          '" id="ab-paste-style"' +
          (hasClip ? "" : " disabled") +
          ' title="Cola o estilo copiado neste elemento">' +
          (hasClip
            ? "📌 Colar estilo (" + Object.keys(state.styleClipboard).length + ")"
            : "📌 Colar estilo") +
          "</button>" +
          '<span class="ab-field-hint">Copia fontes, cores, fundo, borda, padding e margem do breakpoint atual.</span>' +
          "</div>",
        "copiar colar estilo clipboard"
      );
      col.addRaw(
        "avancado",
        "acoes",
        "dup_del",
        "Duplicar / Remover",
        '<div style="display:flex;flex-direction:column;gap:8px">' +
          '<button type="button" class="ab-btn" id="ab-dup">Duplicar</button>' +
          '<button type="button" class="ab-btn ab-btn--danger" id="ab-del">Remover</button>' +
          "</div>",
        "duplicar remover excluir"
      );
    }

    // ── RESPONSIVO: device switcher + hide + key responsive props ──────────
    col.addRaw(
      "responsivo",
      "dispositivo",
      "style_bp",
      "Editando estilos",
      styleBreakpointRowHtml() +
        '<div class="ab-field-hint">Os ícones do <strong>topo</strong> só mudam a visualização. Aqui você escolhe em qual dispositivo as propriedades abaixo se aplicam.</div>',
      "desktop tablet mobile dispositivo breakpoint responsivo"
    );

    col.addStyle(
      "hide",
      "Visibilidade neste dispositivo",
      fieldSelect(
        "hide",
        "Visibilidade neste dispositivo",
        st.hide === true || st.hide === "1" || st.hide === 1 ? "1" : "",
        [
          { value: "", label: "Visível" },
          { value: "1", label: "Ocultar neste tamanho" },
        ],
        "data-style"
      )
    );

    // Mirror key layout/size props on responsive tab for quick per-device edits
    if (sp.width && node.type !== "root") {
      col.addRaw(
        "responsivo",
        "resp_layout",
        "width_r",
        "Largura",
        fieldSelectOrCustom(
          "width",
          "Largura · " + (BP_LABEL[state.breakpoint] || ""),
          st.width || "",
          WIDTH_OPTS,
          "data-style"
        ),
        "largura width responsivo"
      );
    }
    if (sp.height) {
      col.addRaw(
        "responsivo",
        "resp_layout",
        "height_r",
        "Altura",
        fieldSelectOrCustom(
          "height",
          "Altura · " + (BP_LABEL[state.breakpoint] || ""),
          st.height || st.min_height || "",
          [
            { value: "", label: "Automático" },
            { value: "auto", label: "Auto" },
            { value: "120px", label: "120 px" },
            { value: "200px", label: "200 px" },
            { value: "300px", label: "300 px" },
            { value: "50vh", label: "50% da tela" },
            { value: "100vh", label: "Altura da tela" },
            { value: "custom", label: "Personalizado…" },
          ],
          "data-style"
        ),
        "altura height responsivo"
      );
    }
    if (sp.spacing) {
      col.addRaw(
        "responsivo",
        "resp_layout",
        "margin_r",
        "Margem",
        fieldDimensions(
          "margin",
          "Margem · " + (BP_LABEL[state.breakpoint] || ""),
          st.margin || "",
          { allowAuto: true, units: ["px", "%", "em", "rem", "vw", "vh"] }
        ),
        "margem margin responsivo"
      );
      col.addRaw(
        "responsivo",
        "resp_layout",
        "padding_r",
        "Recuo",
        fieldDimensions(
          "padding",
          "Recuo · " + (BP_LABEL[state.breakpoint] || ""),
          st.padding || "",
          { units: ["px", "%", "em", "rem", "vw", "vh"] }
        ),
        "padding recuo espaçamento responsivo"
      );
    }
    if (sp.typography) {
      col.addRaw(
        "responsivo",
        "resp_layout",
        "font_size_r",
        "Fonte",
        fieldSelectOrCustom(
          "font_size",
          "Tamanho da fonte · " + (BP_LABEL[state.breakpoint] || ""),
          st.font_size || "",
          FONT_SIZE_OPTS,
          "data-style"
        ),
        "fonte font size responsivo"
      );
      var allowJustifyR = node.type === "heading" || node.type === "text" || node.type === "dynamic";
      col.addRaw(
        "responsivo",
        "resp_layout",
        "text_align_r",
        "Alinhamento",
        fieldAlignIcons(
          "text_align",
          "Alinhamento · " + (BP_LABEL[state.breakpoint] || ""),
          st.text_align || settings.align || "left",
          "data-style",
          { justify: allowJustifyR }
        ),
        "alinhamento align responsivo"
      );
    }

    box.innerHTML = renderPropsShell(col, typeLabel);
    bindFocalPointPickers(box);

    // ── Props chrome: tabs, search, accordion ──────────────────────────────
    box.querySelectorAll("[data-props-tab]").forEach(function (btn) {
      btn.addEventListener("click", function (e) {
        e.preventDefault();
        var tab = btn.getAttribute("data-props-tab");
        if (!tab || tab === state.propsTab) return;
        state.propsTab = tab;
        paintProps();
      });
    });
    var searchInp = document.getElementById("ab-props-search");
    if (searchInp) {
      // Keep focus/cursor when re-painting after search
      if (state.propsSearch && document.activeElement === searchInp) {
        /* already focused via re-render */
      }
      searchInp.addEventListener("input", function () {
        state.propsSearch = String(searchInp.value || "");
        paintProps();
        var again = document.getElementById("ab-props-search");
        if (again) {
          again.focus();
          var len = again.value.length;
          try {
            again.setSelectionRange(len, len);
          } catch (err) {}
        }
      });
      searchInp.addEventListener("keydown", function (e) {
        if (e.key === "Escape") {
          state.propsSearch = "";
          paintProps();
        }
      });
    }
    var searchClear = document.getElementById("ab-props-search-clear");
    if (searchClear) {
      searchClear.addEventListener("click", function (e) {
        e.preventDefault();
        state.propsSearch = "";
        paintProps();
      });
    }
    box.querySelectorAll("[data-props-acc-toggle]").forEach(function (btn) {
      btn.addEventListener("click", function (e) {
        e.preventDefault();
        var gid = btn.getAttribute("data-props-acc-toggle");
        if (!gid) return;
        if (!state.propsOpenGroups) state.propsOpenGroups = {};
        var next = !isPropsGroupOpen(gid);
        state.propsOpenGroups[gid] = next;
        var acc = btn.closest(".ab-props-acc");
        if (acc) {
          acc.classList.toggle("is-open", next);
          btn.setAttribute("aria-expanded", next ? "true" : "false");
        }
      });
    });

    // Style-breakpoint switcher (does not change canvas view).
    box.querySelectorAll("[data-style-bp]").forEach(function (btn) {
      btn.addEventListener("click", function (e) {
        e.preventDefault();
        e.stopPropagation();
        var bp = btn.getAttribute("data-style-bp");
        if (!bp) return;
        setStyleBreakpoint(bp);
      });
    });

    // Wrap selected element(s) in a container
    var wrapBtn = document.getElementById("ab-wrap-container");
    if (wrapBtn)
      wrapBtn.addEventListener("click", function () {
        execute(function () {
          wrapSelectionInContainer(getSelectedIds());
        });
      });

    // Copy container/section typography onto every texty descendant (override leaf styles).
    var typoKids = document.getElementById("ab-typo-apply-children");
    if (typoKids)
      typoKids.addEventListener("click", function () {
        execute(function () {
          var rootN = getNode(state.selectedId);
          if (!rootN || (rootN.type !== "container" && rootN.type !== "section")) return;
          var stSrc = stylesFor(rootN, "edit") || {};
          var keys = [
            "font_family",
            "font_size",
            "font_weight",
            "font_style",
            "line_height",
            "letter_spacing",
            "text_transform",
            "text_decoration",
            "color",
            "text_align",
          ];
          var textyTypes = {
            heading: 1,
            text: 1,
            button: 1,
            dynamic: 1,
            alert: 1,
            testimonial: 1,
            info_box: 1,
            icon_box: 1,
            image_box: 1,
            text_path: 1,
            text_animation: 1,
            comments_form: 1,
          };
          var count = 0;
          function walk(nid) {
            var n = getNode(nid);
            if (!n) return;
            if (textyTypes[n.type]) {
              normalizeNodeStyles(n);
              var bag = ensureStyles(n, state.breakpoint || "desktop");
              keys.forEach(function (k) {
                if (stSrc[k] !== undefined && stSrc[k] !== null && stSrc[k] !== "") {
                  bag[k] = stSrc[k];
                }
              });
              count++;
            }
            (n.children || []).forEach(walk);
          }
          (rootN.children || []).forEach(walk);
          setStatus(
            count
              ? "Tipografia copiada para " + count + " elemento(s)."
              : "Nenhum texto filho encontrado.",
            count ? "" : "is-err"
          );
        });
      });

    // Container layout convert
    var cRow = document.getElementById("ab-ctr-to-row");
    var cCol = document.getElementById("ab-ctr-to-col");
    var cCols = document.getElementById("ab-ctr-to-cols");
    if (cRow)
      cRow.addEventListener("click", function () {
        execute(function () {
          convertContainerLayout(state.selectedId, "row");
        });
      });
    if (cCol)
      cCol.addEventListener("click", function () {
        execute(function () {
          convertContainerLayout(state.selectedId, "column");
        });
      });
    if (cCols)
      cCols.addEventListener("click", function () {
        execute(function () {
          convertContainerLayout(state.selectedId, "columns");
        });
      });
    // Quick align buttons for row containers
    box.querySelectorAll("[data-row-align]").forEach(function (group) {
      var key = group.getAttribute("data-row-align");
      group.querySelectorAll(".ab-row-align__btn").forEach(function (btn) {
        btn.addEventListener("click", function () {
          var val = btn.getAttribute("data-val");
          if (!key || !val || !state.selectedId) return;
          execute(function () {
            var n = getNode(state.selectedId);
            if (!n) return;
            n.settings = n.settings || {};
            n.settings[key] = val;
          });
        });
      });
    });

    function applyProp(key, v) {
      var id = state.selectedId;
      if (key === "height" && getNode(id) && getNode(id).type === "spacer") {
        v = parseInt(v, 10) || 40;
      }
      if (key === "columns" || key === "col_span") {
        v = parseInt(v, 10) || (key === "columns" ? 12 : 12);
        v = Math.max(1, Math.min(12, v));
      }
      if (key === "text" || key === "code") {
        if (key === "text") v = plainText(v) || v;
        updateSettings(id, key, v);
        markDirty();
        var nodeEl = canvasNodeEl(id);
        var field = nodeEl ? nodeEl.querySelector('[data-field="text"]') : null;
        if (field && key === "text") {
          field.textContent = v;
        }
        return;
      }
      // Nome no editor — never full paint (would steal focus every keystroke).
      if (key === "display_name") {
        var dn = getNode(id);
        if (!dn) return;
        dn.settings = dn.settings || {};
        if (v === "" || v == null) delete dn.settings.display_name;
        else dn.settings.display_name = String(v);
        markDirty();
        return;
      }
      // Alignment is per-device (desktop / tablet / mobile).
      if (key === "align") {
        pushHistory();
        updateSettings(id, "align", v);
        updateStyle(id, "text_align", v);
        markDirty();
        refreshNodeVisual(id);
        return;
      }
      // Image crop focus / fit inside fixed frame — live preview.
      if (
        key === "object_position" ||
        key === "object_fit" ||
        key === "image_object_position" ||
        key === "image_object_fit"
      ) {
        pushHistory();
        updateSettings(id, key, v);
        markDirty();
        refreshNodeVisual(id);
        return;
      }
      // Heading size preset → font_size style for current breakpoint
      if (key === "size") {
        pushHistory();
        updateSettings(id, "size", v);
        if (v && HEADING_SIZE_MAP[v]) {
          updateStyle(id, "font_size", HEADING_SIZE_MAP[v]);
        } else if (v === "" || v === "default") {
          // leave explicit font_size if set; only clear when empty default
        }
        markDirty();
        refreshNodeVisual(id);
        paintProps();
        return;
      }
      // Image / embed URL live update.
      if (
        key === "url" ||
        key === "alt" ||
        key === "source" ||
        key === "provider" ||
        key === "link" ||
        key === "tag" ||
        key === "text"
      ) {
        pushHistory();
        updateSettings(id, key, v);
        markDirty();
        refreshNodeVisual(id);
        return;
      }
      execute(function () {
        var nodeNow = getNode(id);
        if (key === "width" && nodeNow && nodeNow.type === "container") {
          var parW = nodeNow.parentId ? getNode(nodeNow.parentId) : null;
          if (parW && parW.type === "section" && parW.settings && parW.settings.direction === "grid") {
            var spW = widthPctToSpan(v || "100%");
            nodeNow.settings = nodeNow.settings || {};
            nodeNow.settings.col_span = spW;
            nodeNow.settings.width = spanToWidthPct(spW);
            rebalanceColumnSpans(parW.id, id);
          } else {
            updateSettings(id, "width", v);
            updateStyle(id, "width", v);
            if (v && v !== "100%") ensureParentRowForColumns(id);
          }
        } else if (key === "col_span" && nodeNow && nodeNow.type === "container") {
          var spC = Math.max(1, Math.min(12, parseInt(v, 10) || 12));
          nodeNow.settings = nodeNow.settings || {};
          nodeNow.settings.col_span = spC;
          nodeNow.settings.width = spanToWidthPct(spC);
          var parC = nodeNow.parentId ? getNode(nodeNow.parentId) : null;
          if (parC && parC.type === "section" && parC.settings && parC.settings.direction === "grid") {
            rebalanceColumnSpans(parC.id, id);
          }
        } else {
          updateSettings(id, key, v);
        }
        nodeNow = getNode(id);
        // Raiz: ao escolher Largura total, limpa max_width residual do estilo
        // (senão o canvas / save parece “voltar” para 1200px).
        if (key === "content_width" && nodeNow && nodeNow.type === "root") {
          nodeNow.settings = nodeNow.settings || {};
          nodeNow.settings.content_width = v || "full";
          if (v === "full" || v === "content") {
            normalizeNodeStyles(nodeNow);
            ["desktop", "tablet", "mobile"].forEach(function (bp) {
              if (nodeNow.styles && nodeNow.styles[bp]) {
                delete nodeNow.styles[bp].max_width;
                delete nodeNow.styles[bp].width;
              }
            });
          }
          if (v === "wide" && !nodeNow.settings.max_width) {
            nodeNow.settings.max_width = "1200px";
          }
        }
        // Número de colunas → Elementor-like: conteúdo na 1ª, vazias com área pontilhada.
        if (key === "columns" && nodeNow && nodeNow.type === "section") {
          syncSectionColumns(id, v);
        }
        // Switching section to grid: if columns already set, sync column containers.
        if (key === "direction" && v === "grid" && nodeNow && nodeNow.type === "section") {
          var nCols = parseInt((nodeNow.settings && nodeNow.settings.columns) || 1, 10) || 1;
          if (nCols >= 1 && nCols <= 12) {
            syncSectionColumns(id, nCols);
          }
        }
        // Layout da raiz: re-pintar props (showIf max_width some/some some) após o paint do execute.
        if (key === "content_width" || key === "max_width" || key === "gap") {
          // execute() já chama paint(); só atualiza painel de props (showIf).
          setTimeout(function () {
            paintProps();
          }, 0);
        }
      });
    }

    // Category searchable select
    box.querySelectorAll("[data-cat-select]").forEach(function (sel) {
      var key = sel.getAttribute("data-prop") || "category";
      var multi = sel.multiple || sel.closest("[data-cat-multi='1']");
      sel.addEventListener("change", function () {
        var v = "";
        var lab = "Todas";
        if (multi) {
          var ids = [];
          var names = [];
          Array.prototype.forEach.call(sel.selectedOptions || [], function (opt) {
            if (opt.value) {
              ids.push(opt.value);
              names.push(opt.textContent.replace(/\s*·.*$/, "").trim());
            }
          });
          v = ids.join(",");
          lab = names.length ? names.join(", ") : "Nenhuma (todas)";
        } else {
          v = sel.value;
          lab = sel.options[sel.selectedIndex]
            ? sel.options[sel.selectedIndex].textContent
            : "Todas";
          if (!v) lab = "Todas";
        }
        var labelEl = box.querySelector('[data-cat-label="' + key + '"]');
        if (labelEl) labelEl.textContent = lab;
        pushHistory();
        updateSettings(state.selectedId, key, v);
        markDirty();
        refreshNodeVisual(state.selectedId);
      });
    });
    box.querySelectorAll("[data-cat-filter]").forEach(function (inp) {
      var key = inp.getAttribute("data-cat-filter");
      var sel = box.querySelector('[data-cat-select="' + key + '"]');
      if (!sel) return;
      inp.addEventListener("input", function () {
        var q = String(inp.value || "").toLowerCase().trim();
        Array.prototype.forEach.call(sel.options, function (opt, idx) {
          if (idx === 0) {
            opt.hidden = false;
            return;
          }
          var text = (opt.textContent || "").toLowerCase();
          var slug = (opt.getAttribute("data-slug") || "").toLowerCase();
          opt.hidden = q !== "" && text.indexOf(q) < 0 && slug.indexOf(q) < 0 && opt.value.indexOf(q) < 0;
        });
      });
    });

    // Alignment icon buttons (content + style)
    box.querySelectorAll("[data-align-set]").forEach(function (btn) {
      btn.addEventListener("click", function (e) {
        e.preventDefault();
        var v = btn.getAttribute("data-align-set");
        var key = btn.getAttribute("data-align-key") || "align";
        var attr = btn.getAttribute("data-align-attr") || "data-prop";
        var group = btn.closest(".ab-field-align");
        if (group) {
          group.querySelectorAll(".ab-align-btn").forEach(function (b) {
            b.classList.toggle("is-on", b === btn);
          });
          var hidden = group.querySelector("input[" + attr + '="' + key + '"]');
          if (hidden) hidden.value = v;
        }
        if (attr === "data-style") {
          pushHistory();
          updateStyle(state.selectedId, key, v);
          // Keep settings.align in sync for leaf defaults
          if (key === "text_align") {
            updateSettings(state.selectedId, "align", v);
          }
          markDirty();
          refreshNodeVisual(state.selectedId);
        } else {
          applyProp(key === "align" ? "align" : key, v);
        }
      });
    });

    // WordPress media library picker (single).
    box.querySelectorAll(".ab-media-pick").forEach(function (btn) {
      btn.addEventListener("click", function (e) {
        e.preventDefault();
        var key = btn.getAttribute("data-media-for") || "url";
        var attr = btn.getAttribute("data-media-attr") || "data-prop";
        openMediaLibrary(function (att) {
          if (!att || !att.url) return;
          pushHistory();
          if (attr === "data-style") {
            updateStyle(state.selectedId, key, att.url);
            if (key === "bg_image" && !styleValAt(getNode(state.selectedId), "bg_size", state.breakpoint)) {
              updateStyle(state.selectedId, "bg_size", "cover");
            }
          } else {
            updateSettings(state.selectedId, key, att.url);
            if (att.alt) updateSettings(state.selectedId, "alt", att.alt);
          }
          markDirty();
          refreshNodeVisual(state.selectedId);
          paintProps();
        });
      });
    });

    // Multi-image gallery / carousel — WP media dialog (multiple + preselect).
    function setGalleryItems(key, items) {
      pushHistory();
      updateSettings(state.selectedId, key, serializeGalleryItems(items || []));
      markDirty();
      refreshNodeVisual(state.selectedId);
      paintProps();
    }
    box.querySelectorAll(".ab-gallery-media-pick").forEach(function (btn) {
      btn.addEventListener("click", function (e) {
        e.preventDefault();
        var key = btn.getAttribute("data-gallery-for") || "images";
        var current = parseGalleryItems(
          (getNode(state.selectedId).settings && getNode(state.selectedId).settings[key]) || "[]"
        );
        var selectedIds = current.map(function (it) { return it.id; }).filter(Boolean);
        var selectedUrls = current.map(function (it) { return it.url; }).filter(Boolean);
        openMediaLibrary(
          function (list) {
            // Replace with current dialog selection (pre-selected + user changes).
            // Empty selection → keep current (user cancelled effectively).
            if (!Array.isArray(list)) return;
            if (!list.length) {
              // User cleared all in dialog
              setGalleryItems(key, []);
              return;
            }
            setGalleryItems(
              key,
              list.map(function (att) {
                return { id: att.id || 0, url: att.url };
              })
            );
          },
          { multiple: true, selectedIds: selectedIds, selectedUrls: selectedUrls }
        );
      });
    });
    box.querySelectorAll(".ab-gallery-media-clear").forEach(function (btn) {
      btn.addEventListener("click", function (e) {
        e.preventDefault();
        var key = btn.getAttribute("data-gallery-clear") || "images";
        setGalleryItems(key, []);
      });
    });
    box.querySelectorAll("[data-g-rm]").forEach(function (btn) {
      btn.addEventListener("click", function (e) {
        e.preventDefault();
        e.stopPropagation();
        var field = btn.closest("[data-gallery-field]");
        if (!field) return;
        var key = field.getAttribute("data-gallery-field") || "images";
        var idx = parseInt(btn.getAttribute("data-g-rm"), 10);
        var items = parseGalleryItems(
          (getNode(state.selectedId).settings && getNode(state.selectedId).settings[key]) || "[]"
        );
        if (isNaN(idx) || idx < 0 || idx >= items.length) return;
        items.splice(idx, 1);
        setGalleryItems(key, items);
      });
    });

    // Accordion items repeater
    function readAccItemsFromDom(wrap) {
      var items = [];
      wrap.querySelectorAll(".ab-acc-item").forEach(function (row) {
        items.push({
          title: (row.querySelector(".ab-acc-item__title") || {}).value || "",
          content: (row.querySelector(".ab-acc-item__content") || {}).value || "",
          image: (row.querySelector(".ab-acc-item__image") || {}).value || "",
        });
      });
      return items;
    }
    function commitAccItems(wrap) {
      var key = wrap.getAttribute("data-acc-items-field") || "items";
      var items = readAccItemsFromDom(wrap);
      var hidden = wrap.querySelector('input[data-prop="' + key + '"]');
      if (hidden) hidden.value = JSON.stringify(items);
      pushHistory();
      updateSettings(state.selectedId, key, JSON.stringify(items));
      markDirty();
      refreshNodeVisual(state.selectedId);
    }
    box.querySelectorAll(".ab-field-accordion-items").forEach(function (wrap) {
      wrap.querySelectorAll(".ab-acc-item__title, .ab-acc-item__content").forEach(function (el) {
        var ev = el.tagName === "TEXTAREA" ? "input" : "input";
        el.addEventListener(ev, function () {
          commitAccItems(wrap);
        });
      });
      wrap.querySelectorAll(".ab-acc-item__pick").forEach(function (btn) {
        btn.addEventListener("click", function (e) {
          e.preventDefault();
          var idx = parseInt(btn.getAttribute("data-acc-pick"), 10);
          openMediaLibrary(function (att) {
            if (!att || !att.url) return;
            var row = wrap.querySelector('.ab-acc-item[data-acc-idx="' + idx + '"]');
            if (!row) return;
            var imgInp = row.querySelector(".ab-acc-item__image");
            if (imgInp) imgInp.value = att.url;
            commitAccItems(wrap);
            paintProps();
          });
        });
      });
      wrap.querySelectorAll("[data-acc-img-clear]").forEach(function (btn) {
        btn.addEventListener("click", function (e) {
          e.preventDefault();
          var idx = parseInt(btn.getAttribute("data-acc-img-clear"), 10);
          var row = wrap.querySelector('.ab-acc-item[data-acc-idx="' + idx + '"]');
          if (!row) return;
          var imgInp = row.querySelector(".ab-acc-item__image");
          if (imgInp) imgInp.value = "";
          commitAccItems(wrap);
          paintProps();
        });
      });
      wrap.querySelectorAll("[data-acc-rm]").forEach(function (btn) {
        btn.addEventListener("click", function (e) {
          e.preventDefault();
          var idx = parseInt(btn.getAttribute("data-acc-rm"), 10);
          var items = readAccItemsFromDom(wrap);
          if (items.length <= 1) {
            alert("Mantenha pelo menos um item (ou mude a fonte para Posts).");
            return;
          }
          items.splice(idx, 1);
          pushHistory();
          updateSettings(state.selectedId, wrap.getAttribute("data-acc-items-field") || "items", JSON.stringify(items));
          markDirty();
          refreshNodeVisual(state.selectedId);
          paintProps();
        });
      });
      var addBtn = wrap.querySelector(".ab-acc-item-add");
      if (addBtn) {
        addBtn.addEventListener("click", function (e) {
          e.preventDefault();
          var items = readAccItemsFromDom(wrap);
          items.push({
            title: "Item " + (items.length + 1),
            content: "",
            image: "",
          });
          pushHistory();
          updateSettings(state.selectedId, wrap.getAttribute("data-acc-items-field") || "items", JSON.stringify(items));
          markDirty();
          refreshNodeVisual(state.selectedId);
          paintProps();
        });
      }
    });

    // Icon library modal
    box.querySelectorAll(".ab-icon-pick-open").forEach(function (btn) {
      btn.addEventListener("click", function (e) {
        e.preventDefault();
        var key = btn.getAttribute("data-icon-for") || "icon";
        openIconLibrary(key);
      });
    });

    // Preview entrance animation on selected node
    box.querySelectorAll(".ab-fx-preview").forEach(function (btn) {
      btn.addEventListener("click", function (e) {
        e.preventDefault();
        var el = canvasNodeEl(state.selectedId);
        if (!el) return;
        el.classList.remove("ab-fx-in");
        // reflow
        void el.offsetWidth;
        requestAnimationFrame(function () {
          el.classList.add("ab-fx-in");
        });
      });
    });

    // Google Fonts library popup
    box.querySelectorAll(".ab-gf-open").forEach(function (btn) {
      btn.addEventListener("click", function (e) {
        e.preventDefault();
        var curInp =
          box.querySelector('[data-style-custom="font_family"]') ||
          box.querySelector(".ab-gf-value");
        var cur = "";
        if (curInp && curInp.value) {
          cur = String(curInp.value).replace(/^["']|["']$/g, "").split(",")[0].trim();
        } else {
          var n = getNode(state.selectedId);
          var st = n ? stylesFor(n) : {};
          if (st.font_family) {
            cur = String(st.font_family).replace(/^["']|["']$/g, "").split(",")[0].trim();
          }
        }
        openGoogleFontsPicker(cur, function (css, name) {
          pushHistory();
          setForceCustom("font_family", "data-style", true);
          updateStyle(state.selectedId, "font_family", css);
          markDirty();
          refreshNodeVisual(state.selectedId);
          paintProps();
        });
      });
    });

    // Icon list items repeater
    function readIlItems(wrap) {
      var items = [];
      wrap.querySelectorAll(".ab-il-item").forEach(function (row) {
        items.push({
          icon: (row.querySelector(".ab-il-item__icon") || {}).value || "✓",
          text: (row.querySelector(".ab-il-item__text") || {}).value || "",
        });
      });
      return items;
    }
    function commitIlItems(wrap) {
      var key = wrap.getAttribute("data-il-field") || "items";
      var items = readIlItems(wrap);
      pushHistory();
      updateSettings(state.selectedId, key, JSON.stringify(items));
      markDirty();
      refreshNodeVisual(state.selectedId);
    }
    box.querySelectorAll(".ab-field-icon-list-items").forEach(function (wrap) {
      wrap.querySelectorAll(".ab-il-item__text").forEach(function (el) {
        el.addEventListener("input", function () {
          commitIlItems(wrap);
        });
      });
      wrap.querySelectorAll("[data-il-icon]").forEach(function (btn) {
        btn.addEventListener("click", function (e) {
          e.preventDefault();
          var idx = parseInt(btn.getAttribute("data-il-icon"), 10);
          openIconLibrary("", {
            onPick: function (glyph) {
              var row = wrap.querySelector('.ab-il-item[data-il-idx="' + idx + '"]');
              if (!row) return;
              var hid = row.querySelector(".ab-il-item__icon");
              var prev = row.querySelector(".ab-il-item__preview");
              if (hid) hid.value = glyph;
              if (prev) prev.textContent = glyph;
              commitIlItems(wrap);
              paintProps();
            },
          });
        });
      });
      wrap.querySelectorAll("[data-il-rm]").forEach(function (btn) {
        btn.addEventListener("click", function (e) {
          e.preventDefault();
          var idx = parseInt(btn.getAttribute("data-il-rm"), 10);
          var items = readIlItems(wrap);
          if (items.length <= 1) return;
          items.splice(idx, 1);
          pushHistory();
          updateSettings(state.selectedId, wrap.getAttribute("data-il-field") || "items", JSON.stringify(items));
          markDirty();
          refreshNodeVisual(state.selectedId);
          paintProps();
        });
      });
      var addIl = wrap.querySelector(".ab-il-add");
      if (addIl) {
        addIl.addEventListener("click", function (e) {
          e.preventDefault();
          var items = readIlItems(wrap);
          items.push({ icon: "✓", text: "Novo item" });
          pushHistory();
          updateSettings(state.selectedId, wrap.getAttribute("data-il-field") || "items", JSON.stringify(items));
          markDirty();
          refreshNodeVisual(state.selectedId);
          paintProps();
        });
      }
    });

    // Social items repeater
    function readSocItems(wrap) {
      var items = [];
      wrap.querySelectorAll(".ab-soc-item").forEach(function (row) {
        items.push({
          network: (row.querySelector(".ab-soc-item__net") || {}).value || "website",
          url: (row.querySelector(".ab-soc-item__url") || {}).value || "#",
          label: "",
        });
      });
      return items;
    }
    function commitSocItems(wrap) {
      var key = wrap.getAttribute("data-soc-field") || "items";
      pushHistory();
      updateSettings(state.selectedId, key, JSON.stringify(readSocItems(wrap)));
      markDirty();
      refreshNodeVisual(state.selectedId);
    }
    box.querySelectorAll(".ab-field-social-items").forEach(function (wrap) {
      wrap.querySelectorAll(".ab-soc-item__net, .ab-soc-item__url").forEach(function (el) {
        el.addEventListener(el.tagName === "SELECT" ? "change" : "input", function () {
          commitSocItems(wrap);
          if (el.classList.contains("ab-soc-item__net")) paintProps();
        });
      });
      wrap.querySelectorAll("[data-soc-rm]").forEach(function (btn) {
        btn.addEventListener("click", function (e) {
          e.preventDefault();
          var idx = parseInt(btn.getAttribute("data-soc-rm"), 10);
          var items = readSocItems(wrap);
          if (items.length <= 1) return;
          items.splice(idx, 1);
          pushHistory();
          updateSettings(state.selectedId, wrap.getAttribute("data-soc-field") || "items", JSON.stringify(items));
          markDirty();
          refreshNodeVisual(state.selectedId);
          paintProps();
        });
      });
      var addSoc = wrap.querySelector(".ab-soc-add");
      if (addSoc) {
        addSoc.addEventListener("click", function (e) {
          e.preventDefault();
          var items = readSocItems(wrap);
          items.push({ network: "facebook", url: "https://", label: "" });
          pushHistory();
          updateSettings(state.selectedId, wrap.getAttribute("data-soc-field") || "items", JSON.stringify(items));
          markDirty();
          refreshNodeVisual(state.selectedId);
          paintProps();
        });
      }
    });

    // Nav menu items editor (Gutenberg-like list)
    (function bindNavMenuItems() {
      var wrap = box.querySelector(".ab-field-nav-items");
      if (!wrap) return;
      var id = state.selectedId;
      var node = getNode(id);
      if (!node) return;

      function currentTree() {
        return resolveNavMenuItemsForEdit(node.settings || {});
      }
      function saveTree(tree, forceCustom) {
        pushHistory();
        node.settings = node.settings || {};
        if (forceCustom || node.settings.source === "custom") {
          node.settings.source = "custom";
          node.settings.items = serializeNavItemsTree(tree);
        }
        markDirty();
        refreshNodeVisual(id);
        paintProps();
      }

      function setSelection(list) {
        state.navMenuSelected = (list || []).map(function (i) {
          return parseInt(i, 10);
        }).filter(function (i) {
          return !isNaN(i) && i >= 0;
        });
        paintProps();
      }

      // Multi-select checkboxes
      wrap.querySelectorAll("[data-nav-sel]").forEach(function (cb) {
        cb.addEventListener("click", function (e) {
          e.stopPropagation();
        });
        cb.addEventListener("change", function (e) {
          e.stopPropagation();
          var idx = parseInt(cb.getAttribute("data-nav-sel"), 10);
          if (isNaN(idx)) return;
          var cur = Array.isArray(state.navMenuSelected) ? state.navMenuSelected.slice() : [];
          var pos = cur.indexOf(idx);
          if (cb.checked) {
            if (pos < 0) cur.push(idx);
          } else if (pos >= 0) {
            cur.splice(pos, 1);
          }
          cur.sort(function (a, b) {
            return a - b;
          });
          setSelection(cur);
        });
      });

      var selAll = wrap.querySelector(".ab-nav-sel-all");
      if (selAll) {
        selAll.addEventListener("click", function (e) {
          e.preventDefault();
          var tree = currentTree();
          var all = tree.map(function (_it, i) {
            return i;
          });
          setSelection(all);
        });
      }
      var selNone = wrap.querySelector(".ab-nav-sel-none");
      if (selNone) {
        selNone.addEventListener("click", function (e) {
          e.preventDefault();
          setSelection([]);
        });
      }

      var bulkMove = wrap.querySelector(".ab-nav-bulk-move") || wrap.querySelector("#ab-nav-bulk-move");
      if (bulkMove) {
        bulkMove.addEventListener("click", function (e) {
          e.preventDefault();
          var parentSel =
            wrap.querySelector(".ab-nav-bulk__parent") || wrap.querySelector("#ab-nav-bulk-parent");
          if (!parentSel) return;
          var parentIdx = parseInt(parentSel.value, 10);
          var selected = Array.isArray(state.navMenuSelected) ? state.navMenuSelected.slice() : [];
          if (!selected.length) {
            setStatus("Selecione ao menos um item", "is-err");
            return;
          }
          if (isNaN(parentIdx)) {
            setStatus("Escolha o item pai do submenu", "is-err");
            return;
          }
          if (selected.indexOf(parentIdx) >= 0) {
            setStatus("O item pai não pode estar entre os selecionados", "is-err");
            return;
          }
          var tree = currentTree();
          var next = moveNavItemsToSubmenu(tree, selected, parentIdx);
          if (!next) {
            setStatus("Não foi possível mover os itens", "is-err");
            return;
          }
          // Find new parent index (after removal of selected before parent, index shifts)
          var newParentIdx = -1;
          var parentLabel = (tree[parentIdx] && tree[parentIdx].label) || "";
          next.forEach(function (it, i) {
            if (it.label === parentLabel && newParentIdx < 0) newParentIdx = i;
          });
          // Safer: count how many selected were before parentIdx
          var shift = selected.filter(function (i) {
            return i < parentIdx;
          }).length;
          newParentIdx = parentIdx - shift;
          if (newParentIdx < 0 || newParentIdx >= next.length) newParentIdx = 0;
          state.navMenuSelected = [];
          state.navMenuOpenIdx = newParentIdx;
          saveTree(next, true);
          setStatus(
            selected.length +
              " item" +
              (selected.length > 1 ? "s" : "") +
              " movido" +
              (selected.length > 1 ? "s" : "") +
              " para o submenu",
            "is-ok"
          );
        });
      }

      wrap.querySelectorAll("[data-nav-toggle]").forEach(function (btn) {
        btn.addEventListener("click", function (e) {
          e.preventDefault();
          var idx = parseInt(btn.getAttribute("data-nav-toggle"), 10);
          if (isNaN(idx)) return;
          state.navMenuOpenIdx = state.navMenuOpenIdx === idx ? -1 : idx;
          paintProps();
        });
      });

      /** Remap selection/open indices after a top-level swap a↔b. */
      function remapAfterSwap(a, b) {
        function mapIdx(i) {
          if (i === a) return b;
          if (i === b) return a;
          return i;
        }
        if (state.navMenuOpenIdx === a || state.navMenuOpenIdx === b) {
          state.navMenuOpenIdx = mapIdx(state.navMenuOpenIdx);
        }
        state.navMenuSelected = (state.navMenuSelected || []).map(mapIdx);
      }

      wrap.querySelectorAll("[data-nav-up]").forEach(function (btn) {
        btn.addEventListener("click", function (e) {
          e.preventDefault();
          e.stopPropagation();
          var idx = parseInt(btn.getAttribute("data-nav-up"), 10);
          if (isNaN(idx) || idx <= 0) return;
          var tree = currentTree();
          if (!swapArrayItems(tree, idx, idx - 1)) return;
          remapAfterSwap(idx, idx - 1);
          saveTree(tree, true);
        });
      });
      wrap.querySelectorAll("[data-nav-down]").forEach(function (btn) {
        btn.addEventListener("click", function (e) {
          e.preventDefault();
          e.stopPropagation();
          var idx = parseInt(btn.getAttribute("data-nav-down"), 10);
          var tree = currentTree();
          if (isNaN(idx) || idx < 0 || idx >= tree.length - 1) return;
          if (!swapArrayItems(tree, idx, idx + 1)) return;
          remapAfterSwap(idx, idx + 1);
          saveTree(tree, true);
        });
      });

      wrap.querySelectorAll("[data-nav-cup]").forEach(function (btn) {
        btn.addEventListener("click", function (e) {
          e.preventDefault();
          e.stopPropagation();
          var raw = btn.getAttribute("data-nav-cup") || "";
          var parts = raw.split(":");
          var p = parseInt(parts[0], 10);
          var c = parseInt(parts[1], 10);
          if (isNaN(p) || isNaN(c) || c <= 0) return;
          var tree = currentTree();
          if (!tree[p] || !tree[p].children) return;
          if (!swapArrayItems(tree[p].children, c, c - 1)) return;
          state.navMenuOpenIdx = p;
          saveTree(tree, true);
        });
      });
      wrap.querySelectorAll("[data-nav-cdown]").forEach(function (btn) {
        btn.addEventListener("click", function (e) {
          e.preventDefault();
          e.stopPropagation();
          var raw = btn.getAttribute("data-nav-cdown") || "";
          var parts = raw.split(":");
          var p = parseInt(parts[0], 10);
          var c = parseInt(parts[1], 10);
          var tree = currentTree();
          if (!tree[p] || !tree[p].children) return;
          if (isNaN(p) || isNaN(c) || c >= tree[p].children.length - 1) return;
          if (!swapArrayItems(tree[p].children, c, c + 1)) return;
          state.navMenuOpenIdx = p;
          saveTree(tree, true);
        });
      });

      wrap.querySelectorAll("[data-nav-rm]").forEach(function (btn) {
        btn.addEventListener("click", function (e) {
          e.preventDefault();
          e.stopPropagation();
          var idx = parseInt(btn.getAttribute("data-nav-rm"), 10);
          var tree = currentTree();
          if (isNaN(idx) || idx < 0 || idx >= tree.length) return;
          tree.splice(idx, 1);
          if (state.navMenuOpenIdx === idx) state.navMenuOpenIdx = -1;
          else if (state.navMenuOpenIdx > idx) state.navMenuOpenIdx--;
          // Drop removed index from multi-select
          state.navMenuSelected = (state.navMenuSelected || [])
            .filter(function (i) {
              return i !== idx;
            })
            .map(function (i) {
              return i > idx ? i - 1 : i;
            });
          saveTree(tree, true);
        });
      });

      function commitTopField(el) {
        var idx = parseInt(el.getAttribute("data-nav-idx"), 10);
        var field = el.getAttribute("data-nav-field");
        var tree = currentTree();
        if (!tree[idx] || !field) return;
        // Ensure we're editing custom source
        if ((node.settings || {}).source !== "custom") return;
        tree[idx][field] = String(el.value || "").trim() || (field === "url" ? "#" : "Item");
        node.settings = node.settings || {};
        node.settings.items = serializeNavItemsTree(tree);
        markDirty();
        refreshNodeVisual(id);
        // Update label in row without full repaint for smoother typing
        if (field === "label") {
          var lab = wrap.querySelector(
            '.ab-nav-item[data-nav-idx="' + idx + '"] .ab-nav-item__label'
          );
          if (lab) {
            var kids = tree[idx].children || [];
            lab.innerHTML =
              esc(tree[idx].label || "Item") +
              (kids.length
                ? ' <span class="ab-nav-item__subn">(' + kids.length + ")</span>"
                : "");
          }
        }
      }
      wrap.querySelectorAll("[data-nav-field]").forEach(function (el) {
        el.addEventListener("input", function () {
          commitTopField(el);
        });
        el.addEventListener("change", function () {
          commitTopField(el);
        });
      });

      function commitChildField(el) {
        var p = parseInt(el.getAttribute("data-nav-parent"), 10);
        var c = parseInt(el.getAttribute("data-nav-cidx"), 10);
        var field = el.getAttribute("data-nav-cfield");
        var tree = currentTree();
        if (!tree[p] || !tree[p].children || !tree[p].children[c] || !field) return;
        if ((node.settings || {}).source !== "custom") return;
        tree[p].children[c][field] =
          String(el.value || "").trim() || (field === "url" ? "#" : "Subitem");
        node.settings.items = serializeNavItemsTree(tree);
        markDirty();
        refreshNodeVisual(id);
      }
      wrap.querySelectorAll("[data-nav-cfield]").forEach(function (el) {
        el.addEventListener("input", function () {
          commitChildField(el);
        });
        el.addEventListener("change", function () {
          commitChildField(el);
        });
      });

      wrap.querySelectorAll("[data-nav-crm]").forEach(function (btn) {
        btn.addEventListener("click", function (e) {
          e.preventDefault();
          var raw = btn.getAttribute("data-nav-crm") || "";
          var parts = raw.split(":");
          var p = parseInt(parts[0], 10);
          var c = parseInt(parts[1], 10);
          var tree = currentTree();
          if (!tree[p] || !tree[p].children) return;
          tree[p].children.splice(c, 1);
          saveTree(tree, true);
        });
      });

      wrap.querySelectorAll("[data-nav-cadd]").forEach(function (btn) {
        btn.addEventListener("click", function (e) {
          e.preventDefault();
          var p = parseInt(btn.getAttribute("data-nav-cadd"), 10);
          var tree = currentTree();
          if (!tree[p]) return;
          tree[p].children = tree[p].children || [];
          tree[p].children.push({ label: "Subitem", url: "#", children: [] });
          state.navMenuOpenIdx = p;
          saveTree(tree, true);
        });
      });

      var addBtn = wrap.querySelector(".ab-nav-add");
      if (addBtn) {
        addBtn.addEventListener("click", function (e) {
          e.preventDefault();
          var tree = currentTree();
          tree.push({ label: "Novo item", url: "#", children: [] });
          state.navMenuOpenIdx = tree.length - 1;
          saveTree(tree, true);
        });
      }

      var makeCustom = wrap.querySelector(".ab-nav-make-custom");
      if (makeCustom) {
        makeCustom.addEventListener("click", function (e) {
          e.preventDefault();
          var tree = currentTree();
          if (!tree.length) {
            tree = [
              { label: "Início", url: "/", children: [] },
              { label: "Sobre", url: "#", children: [] },
            ];
          }
          pushHistory();
          node.settings = node.settings || {};
          node.settings.source = "custom";
          node.settings.items = serializeNavItemsTree(tree);
          state.navMenuOpenIdx = 0;
          markDirty();
          refreshNodeVisual(id);
          paintProps();
        });
      }

      var importWp = wrap.querySelector(".ab-nav-import-wp");
      if (importWp) {
        importWp.addEventListener("click", function (e) {
          e.preventDefault();
          var menus = Array.isArray(CFG.navMenus) ? CFG.navMenus : [];
          if (!menus.length) {
            alert("Nenhum menu WordPress encontrado.");
            return;
          }
          var names = menus
            .map(function (m, i) {
              return i + 1 + ". " + m.name + " (" + m.count + " itens)";
            })
            .join("\n");
          var pick = window.prompt("Importar qual menu?\n" + names + "\n\nDigite o número:", "1");
          var n = parseInt(pick, 10);
          if (!n || n < 1 || n > menus.length) return;
          var m = menus[n - 1];
          var tree = (m.items || []).map(function (it) {
            return {
              label: it.label || "",
              url: it.url || "#",
              children: (it.children || []).map(function (ch) {
                return { label: ch.label || "", url: ch.url || "#", children: [] };
              }),
            };
          });
          if (!tree.length) {
            alert("Este menu não tem itens.");
            return;
          }
          state.navMenuOpenIdx = 0;
          saveTree(tree, true);
        });
      }
    })();

    // Page search (REST wp/v2/pages or posts)
    box.querySelectorAll(".ab-field-page-search").forEach(function (wrap) {
      var inp = wrap.querySelector(".ab-page-search-input");
      var sel = wrap.querySelector(".ab-page-search-select");
      if (!inp || !sel) return;
      var timer = null;
      function loadPages(q) {
        var typeSel = document.querySelector('#ab-props [data-prop="source_type"]');
        var ptype = (typeSel && typeSel.value) || "page";
        var endpoint = ptype === "post" ? "posts" : "pages";
        var base = (CFG.restUrl || "").replace(/aura-builder\/v1\/?$/, "").replace(/\/$/, "");
        if (!base || base.indexOf("wp-json") < 0) {
          base = window.location.origin + "/wp-json";
        } else {
          var idx = base.indexOf("/wp-json");
          base = base.slice(0, idx + "/wp-json".length);
        }
        var url =
          base +
          "/wp/v2/" +
          endpoint +
          "?per_page=20&status=publish&_fields=id,title,link&search=" +
          encodeURIComponent(q || "");
        fetch(url, { credentials: "same-origin", headers: { "X-WP-Nonce": nonce || "" } })
          .then(function (r) {
            return r.json();
          })
          .then(function (list) {
            if (!Array.isArray(list)) return;
            var cur = sel.value;
            sel.innerHTML = '<option value="">— Selecione —</option>';
            list.forEach(function (p) {
              var t =
                (p.title && (p.title.rendered || p.title)) ||
                "ID " + p.id;
              t = String(t).replace(/<[^>]+>/g, "");
              var o = document.createElement("option");
              o.value = String(p.id);
              o.textContent = t + " (#" + p.id + ")";
              if (String(p.id) === String(cur)) o.selected = true;
              sel.appendChild(o);
            });
          })
          .catch(function () {});
      }
      // Initial load for current id
      loadPages("");
      inp.addEventListener("input", function () {
        clearTimeout(timer);
        timer = setTimeout(function () {
          loadPages(inp.value);
        }, 280);
      });
      sel.addEventListener("change", function () {
        pushHistory();
        updateSettings(state.selectedId, "page_id", sel.value || "");
        markDirty();
        refreshNodeVisual(state.selectedId);
      });
    });
    // Keep icon preview glyph in sync while typing
    box.querySelectorAll(".ab-icon-pick-input").forEach(function (inp) {
      inp.addEventListener("input", function () {
        var key = inp.getAttribute("data-prop") || "icon";
        var prev = box.querySelector('[data-icon-preview="' + key + '"]');
        if (prev) prev.textContent = inp.value || "—";
      });
    });
    // Style URL inputs for bg_image (media field with data-style)
    box.querySelectorAll('input.ab-media-url[data-style]').forEach(function (inp) {
      var key = inp.getAttribute("data-style");
      if (!key) return;
      inp.addEventListener("change", function () {
        pushHistory();
        updateStyle(state.selectedId, key, String(inp.value || "").trim());
        markDirty();
        refreshNodeVisual(state.selectedId);
      });
    });

    box.querySelectorAll("[data-prop]").forEach(function (el) {
      var key = el.getAttribute("data-prop");
      // Hidden JSON fields managed by specialty UIs — skip generic bind.
      if (el.type === "hidden" && (key === "items" || key === "images")) {
        return;
      }
      // Switch toggles (Sim/Não)
      if (el.getAttribute("data-switch") === "1" || el.type === "checkbox") {
        el.addEventListener("change", function () {
          var v = el.checked ? "1" : "0";
          var row = el.closest(".ab-switch");
          if (row) {
            row.classList.toggle("is-on", el.checked);
            var t = row.querySelector(".ab-switch-text");
            if (t) t.textContent = el.checked ? "Sim" : "Não";
          }
          pushHistory();
          updateSettings(state.selectedId, key, v);
          markDirty();
          // showIf fields (ex.: posição da imagem) need full props rebuild
          paintProps();
          refreshNodeVisual(state.selectedId);
          // refresh layers label if renamed via other fields
          var layers = document.getElementById("ab-layers");
          if (layers && state.document) {
            layers.innerHTML = layersHtml(state.document.rootId, 0);
          }
        });
        return;
      }
      var ev = el.tagName === "SELECT" || el.type === "number" ? "change" : "input";
      el.addEventListener(ev, function () {
        var v = el.value;
        if (el.tagName === "SELECT" && v === "custom") {
          setForceCustom(key, "data-prop", true);
          paintProps();
          requestAnimationFrame(function () {
            var inp = document.querySelector('#ab-props [data-prop-custom="' + key + '"]');
            if (inp) {
              inp.focus();
              inp.select();
            }
          });
          return;
        }
        if (el.tagName === "SELECT") setForceCustom(key, "data-prop", false);
        if (key === "display_name") {
          // Light update only — keep focus in the input.
          applyProp(key, v);
          var n = getNode(state.selectedId);
          var layers = document.getElementById("ab-layers");
          if (layers && state.document) {
            layers.innerHTML = layersHtml(state.document.rootId, 0);
            bindLayersDrag(layers);
          }
          var ch = document.querySelector(
            '.ab-overlay-chrome[data-chrome="' + state.selectedId + '"] .ab-overlay-label'
          );
          if (ch) {
            var handle = ch.querySelector(".ab-drag-handle");
            ch.innerHTML =
              (handle ? handle.outerHTML : "") + esc(nodeDisplayName(n));
          }
          return;
        }
        applyProp(key, v);
        // image_position etc. refresh loop
        if (getNode(state.selectedId) && getNode(state.selectedId).type === "loop_grid") {
          refreshNodeVisual(state.selectedId);
        }
        // Rebuild props / canvas when layout model or source changes
        if (
          el.tagName === "SELECT" &&
          (key === "source" || key === "layout" || key === "style" || key === "columns" || key === "per_view" || key === "ratio" || key === "menu_id")
        ) {
          // Nav menu: switching to custom copies current WP items into editable list
          if (key === "source" && v === "custom") {
            var nNav = getNode(state.selectedId);
            if (nNav && nNav.type === "nav_menu") {
              nNav.settings = nNav.settings || {};
              var treeCopy = resolveNavMenuItemsForEdit(
                Object.assign({}, nNav.settings, { source: "wp" })
              );
              if (treeCopy.length) {
                nNav.settings.items = serializeNavItemsTree(treeCopy);
              }
              state.navMenuOpenIdx = 0;
            }
          }
          refreshNodeVisual(state.selectedId);
          if (key === "source" || key === "layout" || key === "menu_id") paintProps();
        }
      });
    });
    box.querySelectorAll("[data-prop-custom]").forEach(function (el) {
      var key = el.getAttribute("data-prop-custom");
      function commitPropCustom() {
        var v = String(el.value || "").trim();
        setForceCustom(key, "data-prop", true);
        applyProp(key, v);
      }
      el.addEventListener("keydown", function (e) {
        if (e.key === "Enter") {
          e.preventDefault();
          commitPropCustom();
          el.blur();
        }
      });
      el.addEventListener("change", commitPropCustom);
    });
    box.querySelectorAll("[data-style]").forEach(function (el) {
      if (el.classList && el.classList.contains("ab-color-hex")) return; // handled above
      // Hidden sync fields inside dimensions/border controls are driven by bindDimensionsControls.
      if (el.closest && el.closest("[data-dim-box], [data-border-box]")) return;
      var key = el.getAttribute("data-style");
      el.addEventListener("change", function () {
        var v = el.value;
        if (v === "custom") {
          setForceCustom(key, "data-style", true);
          paintProps();
          requestAnimationFrame(function () {
            var inp = document.querySelector('#ab-props [data-style-custom="' + key + '"]');
            if (inp) {
              inp.focus();
              inp.select();
            }
          });
          return;
        }
        setForceCustom(key, "data-style", false);
        if (key === "hide") {
          pushHistory();
          if (v === "1") updateStyle(state.selectedId, "hide", true);
          else updateStyle(state.selectedId, "hide", "");
          markDirty();
          refreshNodeVisual(state.selectedId);
          return;
        }
        pushHistory();
        updateStyle(state.selectedId, key, v);
        if (key === "width") {
          var n = getNode(state.selectedId);
          if (n && n.type === "container" && v && v !== "100%") {
            ensureParentRowForColumns(state.selectedId);
          }
          // Re-paint props so max_width field / warning updates after full-width choice.
          if (v === "100%" || v === "100") {
            markDirty();
            refreshNodeVisual(state.selectedId);
            paintProps();
            return;
          }
        }
        if (key === "max_width") {
          markDirty();
          refreshNodeVisual(state.selectedId);
          paintProps();
          return;
        }
        markDirty();
        refreshNodeVisual(state.selectedId);
      });
    });

    bindDimensionsControls(box);
    box.querySelectorAll("[data-style-custom]").forEach(function (el) {
      var key = el.getAttribute("data-style-custom");
      function commitStyleCustom() {
        var v = String(el.value || "").trim();
        setForceCustom(key, "data-style", true);
        pushHistory();
        updateStyle(state.selectedId, key, v);
        if (key === "width") {
          var n = getNode(state.selectedId);
          if (n && n.type === "container" && v && v !== "100%") {
            ensureParentRowForColumns(state.selectedId);
          }
          if (v === "100%" || v === "100") {
            markDirty();
            refreshNodeVisual(state.selectedId);
            paintProps();
            return;
          }
        }
        if (key === "max_width") {
          markDirty();
          refreshNodeVisual(state.selectedId);
          paintProps();
          return;
        }
        markDirty();
        refreshNodeVisual(state.selectedId);
      }
      el.addEventListener("keydown", function (e) {
        if (e.key === "Enter") {
          e.preventDefault();
          commitStyleCustom();
          el.blur();
        }
      });
      el.addEventListener("change", commitStyleCustom);
    });
    function applyColorLive(key, val, attr) {
      attr = attr || "data-style";
      var node = getNode(state.selectedId);
      if (!node) return;
      if (attr === "data-prop") {
        updateSettings(state.selectedId, key, val);
      } else {
        normalizeNodeStyles(node);
        updateStyle(state.selectedId, key, val);
      }
      markDirty();
      refreshNodeVisual(state.selectedId);
    }

    function colorFieldEls(key, attr) {
      attr = attr || "data-style";
      return {
        sw: box.querySelector(
          '.ab-color-swatch[data-color-for="' + key + '"][data-color-attr="' + attr + '"]'
        ) || box.querySelector('.ab-color-swatch[data-color-for="' + key + '"]'),
        text: box.querySelector('input.ab-color-hex[' + attr + '="' + key + '"]'),
        alpha: box.querySelector(
          '.ab-color-alpha[data-alpha-for="' + key + '"][data-color-attr="' + attr + '"]'
        ) || box.querySelector('.ab-color-alpha[data-alpha-for="' + key + '"]'),
        alphaVal: box.querySelector('.ab-color-alpha-val[data-alpha-val="' + key + '"]'),
        preview: box.querySelector(
          '.ab-field-color[data-style-field="' + key + '"] .ab-color-preview'
        ),
      };
    }
    function readAlphaFor(key, attr) {
      attr = attr || "data-style";
      var els = colorFieldEls(key, attr);
      if (!els.alpha) return 100;
      var n = parseInt(els.alpha.value, 10);
      return isNaN(n) ? 100 : Math.max(0, Math.min(100, n));
    }
    function syncColorUi(key, cssVal, attr) {
      attr = attr || "data-style";
      var els = colorFieldEls(key, attr);
      var p = parseColorValue(cssVal);
      if (els.sw && !p.empty) els.sw.value = p.hex;
      if (els.text) els.text.value = cssVal || "";
      if (els.alpha && !p.empty) els.alpha.value = String(p.a);
      if (els.alphaVal) els.alphaVal.textContent = (p.empty ? 100 : p.a) + "%";
      if (els.preview) {
        if (cssVal) {
          els.preview.style.backgroundColor = cssVal;
          els.preview.style.backgroundImage =
            "linear-gradient(45deg,#cbd5e1 25%,transparent 25%),linear-gradient(-45deg,#cbd5e1 25%,transparent 25%),linear-gradient(45deg,transparent 75%,#cbd5e1 75%),linear-gradient(-45deg,transparent 75%,#cbd5e1 75%)";
          els.preview.style.backgroundSize = "10px 10px";
          els.preview.style.backgroundPosition = "0 0,0 5px,5px -5px,-5px 0px";
        } else {
          els.preview.style.backgroundColor = "transparent";
          els.preview.style.backgroundImage = "none";
        }
      }
    }
    function commitColor(key, cssVal, pushHist, attr) {
      attr = attr || "data-style";
      if (pushHist) pushHistory();
      if (key) applyColorLive(key, cssVal, attr);
    }

    box.querySelectorAll(".ab-color-swatch").forEach(function (sw) {
      // Skip globals panel swatches
      if (sw.getAttribute("data-g-color")) return;
      sw.addEventListener("input", function () {
        var key = sw.getAttribute("data-color-for");
        var attr = sw.getAttribute("data-color-attr") || "data-style";
        if (!key) return;
        var composed = composeColorValue(sw.value, readAlphaFor(key, attr));
        // Prefer solid hex for settings colors (PHP sanitize_hex_color)
        if (attr === "data-prop" && parseInt(String(readAlphaFor(key, attr)), 10) >= 100) {
          composed = sw.value;
        }
        syncColorUi(key, composed, attr);
        commitColor(key, composed, false, attr);
      });
      sw.addEventListener("change", function () {
        var key = sw.getAttribute("data-color-for");
        var attr = sw.getAttribute("data-color-attr") || "data-style";
        if (!key) return;
        var a = readAlphaFor(key, attr);
        var composed = a >= 100 && attr === "data-prop" ? sw.value : composeColorValue(sw.value, a);
        syncColorUi(key, composed, attr);
        commitColor(key, composed, true, attr);
      });
    });
    box.querySelectorAll(".ab-color-alpha").forEach(function (rng) {
      if (rng.getAttribute("data-g-color")) return;
      rng.addEventListener("input", function () {
        var key = rng.getAttribute("data-alpha-for");
        var attr = rng.getAttribute("data-color-attr") || "data-style";
        if (!key) return;
        var els = colorFieldEls(key, attr);
        var hex = (els.sw && els.sw.value) || "#111111";
        var composed = composeColorValue(hex, rng.value);
        syncColorUi(key, composed, attr);
        commitColor(key, composed, false, attr);
      });
      rng.addEventListener("change", function () {
        var key = rng.getAttribute("data-alpha-for");
        var attr = rng.getAttribute("data-color-attr") || "data-style";
        if (!key) return;
        var els = colorFieldEls(key, attr);
        var hex = (els.sw && els.sw.value) || "#111111";
        var composed = composeColorValue(hex, rng.value);
        syncColorUi(key, composed, attr);
        commitColor(key, composed, true, attr);
      });
    });
    box.querySelectorAll("input.ab-color-hex").forEach(function (inp) {
      // Globals panel uses data-g-color-text — skip
      if (inp.getAttribute("data-g-color-text")) return;
      function colorAttrOf(el) {
        if (el.hasAttribute("data-prop")) return "data-prop";
        if (el.hasAttribute("data-style")) return "data-style";
        return "data-style";
      }
      function colorKeyOf(el) {
        return el.getAttribute("data-prop") || el.getAttribute("data-style") || "";
      }
      function syncHex(pushHist) {
        var key = colorKeyOf(inp);
        var attr = colorAttrOf(inp);
        if (!key) return;
        var val = String(inp.value || "").trim();
        // Accept hex, rgba, transparent keyword
        var ok =
          val === "" ||
          val === "transparent" ||
          /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/.test(val) ||
          /^rgba?\(/i.test(val) ||
          /^hsla?\(/i.test(val);
        if (!ok && !pushHist) return;
        if (pushHist) pushHistory();
        applyColorLive(key, val, attr);
        if (val && val !== "transparent") {
          var p = parseColorValue(val);
          if (!p.empty) syncColorUi(key, val, attr);
        } else {
          syncColorUi(key, val, attr);
        }
      }
      // Live while typing (valid color) — effect must show without re-clicking device icons.
      inp.addEventListener("input", function () {
        var val = String(inp.value || "").trim();
        if (
          val === "" ||
          val === "transparent" ||
          /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/.test(val) ||
          /^rgba?\([^)]+\)$/i.test(val)
        ) {
          syncHex(false);
        }
      });
      inp.addEventListener("change", function () {
        syncHex(true);
      });
    });
    box.querySelectorAll(".ab-color-clear").forEach(function (btn) {
      btn.addEventListener("click", function () {
        var key = btn.getAttribute("data-clear");
        var attr = btn.getAttribute("data-clear-attr") || "data-style";
        if (!key) return;
        pushHistory();
        if (attr === "data-prop") {
          updateSettings(state.selectedId, key, "");
        } else {
          updateStyle(state.selectedId, key, "");
        }
        markDirty();
        refreshNodeVisual(state.selectedId);
        paintProps();
      });
    });

    // —— Background solid / gradient panel ——
    (function bindBgPanel() {
      var panel = box.querySelector("[data-bg-panel]");
      if (!panel) return;

      function readStopsFromDom() {
        var rows = panel.querySelectorAll(".ab-bg-stop");
        var stops = [];
        rows.forEach(function (row) {
          var cInp = row.querySelector(".ab-bg-stop-color");
          var pInp = row.querySelector(".ab-bg-stop-pos");
          var c = cInp ? String(cInp.value || "").trim() : "";
          var p = pInp ? parseInt(pInp.value, 10) : 0;
          if (!c) return;
          if (isNaN(p)) p = 0;
          p = Math.max(0, Math.min(100, p));
          stops.push({ c: c, p: p });
        });
        if (stops.length < 2) {
          stops = [
            { c: "#1e3a8a", p: 0 },
            { c: "#c41e3a", p: 100 },
          ];
        }
        return stops;
      }

      function currentMode() {
        var on = panel.querySelector(".ab-bg-mode-tab.is-on");
        return on && on.getAttribute("data-bg-mode") === "gradient" ? "gradient" : "solid";
      }

      function syncHiddenAndApply(pushHist) {
        var mode = currentMode();
        var kindEl = panel.querySelector(".ab-bg-grad-kind");
        var angEl = panel.querySelector(".ab-bg-grad-angle");
        var kind = kindEl && kindEl.value === "radial" ? "radial" : "linear";
        var angle = angEl ? angEl.value : "135";
        var stops = readStopsFromDom();
        var css = mode === "gradient" ? buildGradientCss(kind, angle, stops) : "";
        var hidType = panel.querySelector('input[data-style="bg_type"]');
        var hidStops = panel.querySelector('input[data-style="bg_grad_stops"]');
        var hidGrad = panel.querySelector('input[data-style="bg_gradient"]');
        if (hidType) hidType.value = mode;
        if (hidStops) hidStops.value = JSON.stringify(stops);
        if (hidGrad) hidGrad.value = css;
        var prev = panel.querySelector(".ab-bg-preview");
        if (prev) {
          prev.style.background =
            mode === "gradient" ? css : panel.querySelector('.ab-bg-solid-pane input.ab-color-hex[data-style="background"]')
              ? panel.querySelector('.ab-bg-solid-pane input.ab-color-hex[data-style="background"]').value || "transparent"
              : "transparent";
        }
        var angLab = panel.querySelector(".ab-bg-angle-row label");
        var angVal = panel.querySelector(".ab-bg-angle-val");
        if (angLab) angLab.textContent = "Ângulo (" + angle + "°)";
        if (angVal) angVal.textContent = angle + "°";
        var angRow = panel.querySelector(".ab-bg-angle-row");
        if (angRow) angRow.hidden = kind === "radial";

        if (pushHist) pushHistory();
        var id = state.selectedId;
        normalizeNodeStyles(getNode(id));
        updateStyle(id, "bg_type", mode);
        updateStyle(id, "bg_grad_kind", kind);
        updateStyle(id, "bg_grad_angle", String(angle));
        updateStyle(id, "bg_grad_stops", JSON.stringify(stops));
        updateStyle(id, "bg_gradient", css);
        markDirty();
        refreshNodeVisual(id);
      }

      panel.querySelectorAll(".ab-bg-mode-tab").forEach(function (tab) {
        tab.addEventListener("click", function () {
          var mode = tab.getAttribute("data-bg-mode") || "solid";
          panel.querySelectorAll(".ab-bg-mode-tab").forEach(function (t) {
            t.classList.toggle("is-on", t === tab);
          });
          var solid = panel.querySelector(".ab-bg-solid-pane");
          var grad = panel.querySelector(".ab-bg-grad-pane");
          if (solid) solid.hidden = mode !== "solid";
          if (grad) grad.hidden = mode !== "gradient";
          syncHiddenAndApply(true);
          // rebuild solid color bindings when switching
          if (mode === "solid") paintProps();
        });
      });

      var kindSel = panel.querySelector(".ab-bg-grad-kind");
      if (kindSel) {
        kindSel.addEventListener("change", function () {
          syncHiddenAndApply(true);
        });
      }
      var ang = panel.querySelector(".ab-bg-grad-angle");
      if (ang) {
        ang.addEventListener("input", function () {
          syncHiddenAndApply(false);
        });
        ang.addEventListener("change", function () {
          syncHiddenAndApply(true);
        });
      }

      function bindStopRow(row) {
        var sw = row.querySelector(".ab-bg-stop-swatch");
        var col = row.querySelector(".ab-bg-stop-color");
        var pos = row.querySelector(".ab-bg-stop-pos");
        var rm = row.querySelector(".ab-bg-stop-rm");
        if (sw && col) {
          sw.addEventListener("input", function () {
            var a = parseColorValue(col.value).a;
            col.value = composeColorValue(sw.value, a);
            syncHiddenAndApply(false);
          });
          sw.addEventListener("change", function () {
            var a = parseColorValue(col.value).a;
            col.value = composeColorValue(sw.value, a);
            syncHiddenAndApply(true);
          });
        }
        if (col) {
          col.addEventListener("input", function () {
            var p = parseColorValue(col.value);
            if (!p.empty && sw) sw.value = p.hex;
            syncHiddenAndApply(false);
          });
          col.addEventListener("change", function () {
            syncHiddenAndApply(true);
          });
        }
        if (pos) {
          pos.addEventListener("input", function () {
            syncHiddenAndApply(false);
          });
          pos.addEventListener("change", function () {
            syncHiddenAndApply(true);
          });
        }
        if (rm) {
          rm.addEventListener("click", function () {
            var rows = panel.querySelectorAll(".ab-bg-stop");
            if (rows.length <= 2) return;
            row.remove();
            syncHiddenAndApply(true);
            paintProps();
          });
        }
      }
      panel.querySelectorAll(".ab-bg-stop").forEach(bindStopRow);

      var addBtn = panel.querySelector(".ab-bg-stop-add");
      if (addBtn) {
        addBtn.addEventListener("click", function () {
          var stops = readStopsFromDom();
          var last = stops[stops.length - 1];
          stops.push({
            c: last ? last.c : "#7c3aed",
            p: Math.min(100, (last ? last.p : 50) + 10),
          });
          pushHistory();
          updateStyle(state.selectedId, "bg_type", "gradient");
          updateStyle(state.selectedId, "bg_grad_stops", JSON.stringify(stops));
          updateStyle(
            state.selectedId,
            "bg_gradient",
            buildGradientCss(
              panel.querySelector(".ab-bg-grad-kind")
                ? panel.querySelector(".ab-bg-grad-kind").value
                : "linear",
              panel.querySelector(".ab-bg-grad-angle")
                ? panel.querySelector(".ab-bg-grad-angle").value
                : "135",
              stops
            )
          );
          markDirty();
          paintProps();
          refreshNodeVisual(state.selectedId);
        });
      }
    })();
    var del = document.getElementById("ab-del");
    if (del)
      del.addEventListener("click", function () {
        execute(function () {
          removeNode(state.selectedId);
        });
      });
    var dup = document.getElementById("ab-dup");
    if (dup)
      dup.addEventListener("click", function () {
        execute(function () {
          duplicateNode(state.selectedId);
        });
      });
    var copyStyleBtn = document.getElementById("ab-copy-style");
    if (copyStyleBtn)
      copyStyleBtn.addEventListener("click", function () {
        if (state.selectedId) copyNodeStyle(state.selectedId);
      });
    var pasteStyleBtn = document.getElementById("ab-paste-style");
    if (pasteStyleBtn)
      pasteStyleBtn.addEventListener("click", function () {
        if (state.selectedId) pasteNodeStyle(state.selectedId);
      });
  }

  function targetParentId() {
    if (state.selectedId) {
      var n = getNode(state.selectedId);
      if (!n) return state.document.rootId;
      if (n.type === "container" || n.type === "section" || n.type === "root") return n.id;
      return n.parentId || state.document.rootId;
    }
    var root = getNode(state.document.rootId);
    if (root && root.children && root.children[0]) {
      var sec = getNode(root.children[0]);
      if (sec && sec.children && sec.children[0]) return sec.children[0];
      return sec ? sec.id : root.id;
    }
    return state.document.rootId;
  }

  /* ── interactions: select, resize, drag ── */
  function bindCanvas(canvasWrap) {
    // Click inside text field: select the leaf (heading/text/button), not parent container.
    // Critical: always sync selectedIds — otherwise updateSelectionUI reverts to old container.
    canvasWrap.addEventListener(
      "mousedown",
      function (e) {
        if (e.target.closest(".ab-resize-handle") || e.target.closest(".ab-drag-handle")) return;
        if (e.target.closest("[data-act]")) return;
        if (e.target.closest(".ab-overlay-chrome")) return;

        var field = e.target.closest('[contenteditable="true"]');
        if (field) {
          var nodeEl = field.closest("[data-node-id]");
          if (nodeEl) {
            var id = nodeEl.getAttribute("data-node-id");
            var nField = getNode(id);
            // Prefer leaf if somehow we hit a container wrapper first.
            if (nField && (nField.type === "container" || nField.type === "section")) {
              var deeper = pickDeepestLeafUnder(e);
              if (deeper) id = deeper;
            }
            var cur = getSelectedIds();
            var needs =
              state.selectedId !== id ||
              cur.length !== 1 ||
              cur[0] !== id ||
              !!(e.ctrlKey || e.metaKey);
            if (needs) {
              if (e.ctrlKey || e.metaKey) {
                // Multi-select even from contenteditable.
                selectNode(id, { multi: true });
              } else {
                state.selectedId = id;
                state.selectedIds = [id];
                // Defer chrome so browser can place caret first.
                setTimeout(function () {
                  updateSelectionUI();
                  if (document.activeElement !== field) {
                    field.focus();
                  }
                }, 0);
              }
            }
          }
          return; // allow default caret placement (unless multi)
        }
      },
      true
    );

    /** Walk from target: first [data-node-id] is deepest. Prefer leaf over container. */
    function pickDeepestLeafUnder(e) {
      var start = e && e.target;
      if (start && start.nodeType === 3) start = start.parentElement;
      if (!start || !start.closest) return null;
      if (start.closest(".ab-overlay-chrome") || start.closest(".ab-resize-handle")) return null;
      if (start.closest(".ab-multi-bar") || start.closest("[data-act]")) return null;

      // Deepest node under pointer via closest, then elementsFromPoint for padding hits.
      var list = [];
      try {
        list = document.elementsFromPoint(e.clientX, e.clientY) || [];
      } catch (err) {
        list = [];
      }
      if (!list.length && start) list = [start];

      var fallback = null;
      for (var i = 0; i < list.length; i++) {
        var el = list[i];
        if (!el || !el.closest) continue;
        if (el.closest(".ab-overlay-chrome") || el.closest(".ab-resize-handle")) continue;
        if (el.closest(".ab-multi-bar")) continue;
        // Find deepest data-node-id on this element chain
        var walk = el;
        var found = null;
        while (walk && walk !== canvasWrap) {
          if (walk.getAttribute && walk.getAttribute("data-node-id")) {
            found = walk;
            break;
          }
          walk = walk.parentElement;
        }
        if (!found) continue;
        var nid = found.getAttribute("data-node-id");
        var n = getNode(nid);
        if (!n || n.type === "root") continue;
        if (n.type !== "section" && n.type !== "container") {
          return nid;
        }
        if (!fallback) fallback = nid;
      }
      // Also try e.target.closest — deepest leaf if click is on leaf content
      var near = start.closest("[data-node-id]");
      if (near) {
        var nearId = near.getAttribute("data-node-id");
        var nearN = getNode(nearId);
        if (nearN && nearN.type !== "root" && nearN.type !== "section" && nearN.type !== "container") {
          return nearId;
        }
        if (!fallback && nearN && nearN.type !== "root") fallback = nearId;
      }
      return fallback;
    }

    /** Prefer leaf node under cursor (not parent container). */
    function pickNodeIdFromEvent(e) {
      return pickDeepestLeafUnder(e);
    }

    canvasWrap.addEventListener("click", function (e) {
      var act = e.target.closest("[data-act]");
      if (act) {
        e.preventDefault();
        e.stopPropagation();
        var id = act.getAttribute("data-id");
        var a = act.getAttribute("data-act");
        if (a === "del") {
          execute(function () {
            removeNode(id);
          });
        }
        if (a === "dup") {
          execute(function () {
            duplicateNode(id);
          });
        }
        if (a === "move-up" || a === "move-down") {
          execute(function () {
            if (moveNodeSibling(id, a === "move-up" ? "up" : "down")) {
              state.selectedId = id;
              state.selectedIds = [id];
            }
          });
        }
        if (a === "copy-style") {
          copyNodeStyle(id);
        }
        if (a === "paste-style") {
          pasteNodeStyle(id);
        }
        return;
      }
      if (e.target.closest(".ab-resize-handle") || e.target.closest(".ab-drag-handle")) return;
      if (e.target.closest(".ab-multi-bar")) return;
      // Text editing: mousedown already selected the leaf — don't re-select parent.
      if (e.target.closest('[contenteditable="true"]')) {
        // Ensure selection stuck on the leaf (safety if mousedown was skipped).
        var fieldEl = e.target.closest('[contenteditable="true"]');
        var leafEl = fieldEl && fieldEl.closest("[data-node-id]");
        if (leafEl && !(e.ctrlKey || e.metaKey)) {
          var leafId = leafEl.getAttribute("data-node-id");
          var curIds = getSelectedIds();
          if (state.selectedId !== leafId || curIds.length !== 1 || curIds[0] !== leafId) {
            state.selectedId = leafId;
            state.selectedIds = [leafId];
            updateSelectionUI();
          }
        }
        return;
      }

      var nid = pickNodeIdFromEvent(e);
      if (nid) {
        e.preventDefault();
        e.stopPropagation();
        var n = getNode(nid);
        var multi = !!(e.ctrlKey || e.metaKey);
        // Leaf with editable text → select + focus field (no full paint) unless multi.
        if (!multi && n && (n.type === "heading" || n.type === "text" || n.type === "button")) {
          selectNode(nid, { focusEdit: true });
        } else {
          selectNode(nid, { focusEdit: false, multi: multi });
        }
        return;
      }
      // Click empty canvas
      if (!e.target.closest(".ab-node")) {
        state.selectedId = null;
        state.selectedIds = [];
        updateSelectionUI();
      }
    });

    canvasWrap.addEventListener("input", function (e) {
      var field = e.target.closest("[contenteditable][data-field]");
      if (!field) return;
      var nodeEl = field.closest("[data-node-id]");
      if (!nodeEl) return;
      var id = nodeEl.getAttribute("data-node-id");
      var key = field.getAttribute("data-field");
      var node = getNode(id);
      if (!node) return;
      if (node.type === "text") node.settings.text = field.innerHTML;
      else node.settings[key] = field.innerText || field.textContent || "";
      markDirty();
      // Sync props panel field without full paint if possible.
      var prop = document.querySelector('#ab-props [data-prop="' + key + '"]');
      if (prop && prop !== document.activeElement) {
        prop.value = node.settings[key] || "";
      }
    });

    // Stop keyboard from bubbling delete while typing.
    canvasWrap.addEventListener(
      "keydown",
      function (e) {
        if (!isEditingText(e)) return;
        // Never let global shortcuts steal Backspace/Delete while typing.
        if (e.key === "Backspace" || e.key === "Delete") {
          e.stopPropagation();
        }
      },
      true
    );

    // Resize
    canvasWrap.addEventListener("mousedown", function (e) {
      var handle = e.target.closest("[data-resize]");
      if (handle) {
        e.preventDefault();
        e.stopPropagation();
        var id = handle.getAttribute("data-id");
        var dir = handle.getAttribute("data-resize");
        var el = canvasWrap.querySelector('[data-node-id="' + id + '"]');
        if (!el) return;
        var rect = el.getBoundingClientRect();
        pushHistory();
        resizeState = {
          id: id,
          dir: dir,
          startX: e.clientX,
          startY: e.clientY,
          startW: rect.width,
          startH: rect.height,
        };
        el.classList.add("is-resizing");
        document.body.style.userSelect = "none";
        return;
      }

      var dragH = e.target.closest("[data-drag]");
      if (dragH) {
        e.preventDefault();
        e.stopPropagation();
        var did = dragH.getAttribute("data-drag");
        pushHistory();
        dragState = { id: did, overParent: null, overIndex: null };
        var nel = canvasWrap.querySelector('[data-node-id="' + did + '"]');
        if (nel) nel.classList.add("is-dragging");
        document.body.classList.add("ab-is-dragging");
        document.body.style.userSelect = "none";
        document.body.style.cursor = "grabbing";
      }
    });
  }

  /** True if nodeId is the same as or a descendant of ancestorId. */
  function isNodeInside(nodeId, ancestorId) {
    if (!nodeId || !ancestorId) return false;
    if (nodeId === ancestorId) return true;
    var walk = getNode(nodeId);
    var guard = 0;
    while (walk && walk.parentId && guard++ < 40) {
      if (walk.parentId === ancestorId) return true;
      walk = getNode(walk.parentId);
    }
    return false;
  }

  /** Clear drop UI markers. */
  function clearDropUI() {
    document.querySelectorAll(".ab-drop-indicator").forEach(function (n) {
      n.remove();
    });
    document.querySelectorAll(".ab-drop-target, .ab-drop-target-col").forEach(function (el) {
      el.classList.remove("ab-drop-target", "ab-drop-target-col");
    });
  }

  /**
   * Resolve which column container under a section is under the cursor (X).
   * Falls back to first container.
   */
  function columnUnderPoint(sectionId, clientX, clientY) {
    var sec = getNode(sectionId);
    if (!sec || !sec.children || !sec.children.length) return null;
    var cols = sec.children.filter(function (cid) {
      var c = getNode(cid);
      return c && c.type === "container";
    });
    if (!cols.length) return null;
    var best = null;
    var bestDist = Infinity;
    cols.forEach(function (cid) {
      var el = canvasNodeEl(cid);
      if (!el) return;
      var r = el.getBoundingClientRect();
      // Prefer column that contains the point.
      if (clientX >= r.left && clientX <= r.right && clientY >= r.top - 8 && clientY <= r.bottom + 8) {
        best = cid;
        bestDist = -1;
        return;
      }
      if (bestDist < 0) return;
      var cx = (r.left + r.right) / 2;
      var dist = Math.abs(clientX - cx);
      if (dist < bestDist) {
        bestDist = dist;
        best = cid;
      }
    });
    return best || cols[0];
  }

  /**
   * Compute drop target from mouse position while dragging a node.
   * Returns { parentId, index, highlightId, siblingId?, before? } or null.
   */
  function resolveDropTarget(clientX, clientY, dragId) {
    var wrap =
      document.getElementById("ab-canvas-wrap") ||
      document.getElementById("ab-page-body") ||
      document.getElementById("ab-canvas");
    if (!wrap) return null;

    // Hit-test with dragged node ignored (CSS already sets pointer-events:none).
    var target = document.elementFromPoint(clientX, clientY);
    if (!target) return null;
    // If we hit chrome outside canvas, bail.
    if (!wrap.contains(target) && target !== wrap) {
      // Still allow if inside page body / document tree under canvas.
      var pageBody = document.getElementById("ab-page-body");
      if (!pageBody || !pageBody.contains(target)) return null;
    }

    function okParent(pid) {
      return pid && pid !== dragId && !isNodeInside(pid, dragId);
    }

    // 1) Empty column drop zone.
    var empty = target.closest(".ab-drop-empty");
    if (empty) {
      var ep = empty.getAttribute("data-parent");
      if (okParent(ep)) {
        return { parentId: ep, index: 9999, highlightId: ep };
      }
    }

    // 2) Hit a node.
    var nodeEl = target.closest(".ab-node[data-node-id]");
    if (nodeEl) {
      var hitId = nodeEl.getAttribute("data-node-id");
      // Walk up if we somehow hit the dragged node itself.
      if (hitId === dragId) {
        nodeEl = nodeEl.parentElement
          ? nodeEl.parentElement.closest(".ab-node[data-node-id]")
          : null;
        hitId = nodeEl ? nodeEl.getAttribute("data-node-id") : null;
      }
      if (hitId && hitId !== dragId && !isNodeInside(hitId, dragId)) {
        var hit = getNode(hitId);
        if (hit) {
          // Column container → drop inside.
          if (hit.type === "container") {
            var kids = hit.children || [];
            var childHit = null;
            for (var i = 0; i < kids.length; i++) {
              if (kids[i] === dragId) continue;
              var cel = canvasNodeEl(kids[i]);
              if (!cel) continue;
              var cr = cel.getBoundingClientRect();
              if (
                clientY >= cr.top &&
                clientY <= cr.bottom &&
                clientX >= cr.left &&
                clientX <= cr.right
              ) {
                childHit = kids[i];
                break;
              }
            }
            if (childHit) {
              var cEl = canvasNodeEl(childHit);
              var cRect = cEl.getBoundingClientRect();
              var beforeC = clientY < cRect.top + cRect.height / 2;
              var cIdx = kids.indexOf(childHit);
              return {
                parentId: hitId,
                index: beforeC ? cIdx : cIdx + 1,
                highlightId: hitId,
                siblingId: childHit,
                before: beforeC,
              };
            }
            return { parentId: hitId, index: 9999, highlightId: hitId };
          }

          // Section → column under cursor.
          if (hit.type === "section") {
            var colId = columnUnderPoint(hitId, clientX, clientY);
            if (okParent(colId)) {
              return { parentId: colId, index: 9999, highlightId: colId };
            }
          }

          // Leaf / other → sibling of this node in its parent.
          if (hit.type !== "root" && hit.parentId && okParent(hit.parentId)) {
            var parent = getNode(hit.parentId);
            if (parent) {
              var rect = nodeEl.getBoundingClientRect();
              var before = clientY < rect.top + rect.height / 2;
              var idx = parent.children ? parent.children.indexOf(hitId) : 0;
              if (idx < 0) idx = 0;
              return {
                parentId: hit.parentId,
                index: before ? idx : idx + 1,
                highlightId: hit.parentId,
                siblingId: hitId,
                before: before,
              };
            }
          }
        }
      }
    }

    // 3) Fallback by class.
    var contEl = target.closest(".ab-type-container");
    if (contEl) {
      var cid = contEl.getAttribute("data-node-id");
      if (okParent(cid)) {
        return { parentId: cid, index: 9999, highlightId: cid };
      }
    }
    var secEl = target.closest(".ab-type-section");
    if (secEl) {
      var sid = secEl.getAttribute("data-node-id");
      var col2 = columnUnderPoint(sid, clientX, clientY);
      if (okParent(col2)) {
        return { parentId: col2, index: 9999, highlightId: col2 };
      }
    }
    return null;
  }

  function onMouseMove(e) {
    if (resizeState) {
      var dx = e.clientX - resizeState.startX;
      var dy = e.clientY - resizeState.startY;
      var dir = resizeState.dir;
      var w = resizeState.startW;
      var h = resizeState.startH;
      if (dir.indexOf("e") >= 0) w = Math.max(40, resizeState.startW + dx);
      if (dir.indexOf("w") >= 0) w = Math.max(40, resizeState.startW - dx);
      if (dir.indexOf("s") >= 0) h = Math.max(20, resizeState.startH + dy);
      if (dir.indexOf("n") >= 0) h = Math.max(20, resizeState.startH - dy);
      var node = getNode(resizeState.id);
      if (!node) return;
      var st = ensureStyles(node);
      var el = canvasNodeEl(resizeState.id);

      // Clamp leaf widgets to parent width so the slide never spills out of the section.
      var isLeaf =
        node.type !== "root" &&
        node.type !== "section" &&
        node.type !== "container";
      if (el && el.parentElement && isLeaf) {
        var pBox = el.parentElement;
        // Prefer content box of the layout parent (container/section), not chrome
        var layoutParent = el.parentElement.closest
          ? el.parentElement.closest(".ab-type-container, .ab-type-section, .ab-type-root") ||
            el.parentElement
          : el.parentElement;
        var pW = layoutParent.clientWidth || pBox.clientWidth || 0;
        if (pW > 40 && w > pW) w = pW;
      }

      if (dir.indexOf("e") >= 0 || dir.indexOf("w") >= 0) {
        st.width = Math.round(w) + "px";
        if (node.type === "container") {
          node.settings = node.settings || {};
          node.settings.width = st.width;
        }
      }
      // Height only for structural/media — never fix height on text/heading
      // (creates empty tall box and makes side handles look huge).
      var allowH = {
        section: 1,
        container: 1,
        spacer: 1,
        image: 1,
        image_box: 1,
        video: 1,
        gallery: 1,
        embed: 1,
        maps: 1,
        carousel: 1,
        image_carousel_pro: 1,
        image_float: 1,
        image_overlay: 1,
        preview_card: 1,
      };
      if ((dir.indexOf("s") >= 0 || dir.indexOf("n") >= 0) && allowH[node.type]) {
        st.height = Math.round(h) + "px";
        st.min_height = st.height;
        if (node.type === "spacer") {
          node.settings.height = Math.round(h);
        }
      }
      // live update DOM without full paint (canvas only — not layers list)
      if (el) {
        if (st.width) {
          el.style.width = st.width;
          // Leaves stay inside parent; never force maxWidth = fixed px (that overflowed the section).
          if (isLeaf) {
            el.style.maxWidth = "100%";
            el.style.boxSizing = "border-box";
          } else {
            el.style.maxWidth = st.width;
            el.style.flex = "0 0 " + st.width;
          }
          el.classList.add("ab-has-w");
        }
        if (allowH[node.type] && st.height) {
          el.style.height = st.height;
          el.style.minHeight = st.height;
          el.classList.add("ab-has-h");
          // Grow parent container/section so the slide stays "inside" the layout box.
          if (isLeaf && el.parentElement) {
            var pEl =
              el.parentElement.closest(".ab-type-container, .ab-type-section") ||
              el.parentElement;
            if (pEl && pEl.classList && pEl.classList.contains("ab-node")) {
              var need = Math.round(h) + 8;
              var pH = pEl.clientHeight || 0;
              if (need > pH) {
                pEl.style.minHeight = need + "px";
                var pId = pEl.getAttribute("data-node-id");
                var pNode = pId ? getNode(pId) : null;
                if (pNode && allowH[pNode.type]) {
                  var pst = ensureStyles(pNode);
                  if (
                    !pst.height ||
                    parseInt(pst.height, 10) < need ||
                    !pst.min_height ||
                    parseInt(pst.min_height, 10) < need
                  ) {
                    pst.min_height = need + "px";
                  }
                }
              }
            }
          }
        }
        // Keep media + handles in sync while dragging (no full paint).
        syncMediaFill(el, node, st);
      }
      markDirty();
      return;
    }

    if (dragState) {
      try {
        clearDropUI();
        var drop = resolveDropTarget(e.clientX, e.clientY, dragState.id);
        if (!drop || !drop.parentId) {
          dragState.overParent = null;
          dragState.overIndex = null;
          return;
        }
        dragState.overParent = drop.parentId;
        dragState.overIndex =
          typeof drop.index === "number" ? drop.index : 9999;

        // Highlight target column/container (dashed purple).
        var hl = canvasNodeEl(drop.highlightId || drop.parentId);
        if (hl) {
          hl.classList.add("ab-drop-target");
          if (hl.classList.contains("ab-type-container")) {
            hl.classList.add("ab-drop-target-col");
          }
        }

        // Line indicator before/after sibling.
        if (drop.siblingId) {
          var sibling = canvasNodeEl(drop.siblingId);
          if (sibling && sibling.parentNode) {
            var ind = document.createElement("div");
            ind.className = "ab-drop-indicator";
            if (drop.before) sibling.parentNode.insertBefore(ind, sibling);
            else sibling.parentNode.insertBefore(ind, sibling.nextSibling);
          }
        } else if (hl) {
          var lastChild = null;
          var pNode = getNode(drop.parentId);
          var kids = (pNode && pNode.children) || [];
          for (var k = kids.length - 1; k >= 0; k--) {
            if (kids[k] === dragState.id) continue;
            var ke = canvasNodeEl(kids[k]);
            if (ke) {
              lastChild = ke;
              break;
            }
          }
          var ind2 = document.createElement("div");
          ind2.className = "ab-drop-indicator ab-drop-indicator--in";
          if (lastChild && lastChild.parentNode) {
            lastChild.parentNode.insertBefore(ind2, lastChild.nextSibling);
          } else {
            var emptyZ = hl.querySelector(".ab-drop-empty");
            if (emptyZ) emptyZ.appendChild(ind2);
            else hl.appendChild(ind2);
          }
        }
      } catch (err) {
        // Never break drag mid-flight.
        if (typeof console !== "undefined" && console.warn) {
          console.warn("ab drag move", err);
        }
      }
    }
  }

  function onMouseUp(e) {
    if (resizeState) {
      var rid = resizeState.id;
      var rel = rid ? canvasNodeEl(rid) : null;
      if (rel) rel.classList.remove("is-resizing");
      resizeState = null;
      document.body.style.userSelect = "";
      // Prefer live refresh over full paint — keeps selection/handles stable.
      if (rid) {
        refreshNodeVisual(rid);
        updateSelectionUI();
      } else {
        paint();
      }
      return;
    }
    if (dragState) {
      var ds = dragState;
      dragState = null;
      document.body.style.userSelect = "";
      document.body.style.cursor = "";
      document.body.classList.remove("ab-is-dragging");
      clearDropUI();
      var dragEl2 = canvasNodeEl(ds.id);
      if (dragEl2) dragEl2.classList.remove("is-dragging");

      // Final resolve on mouseup so we always use the release position.
      var finalDrop = null;
      try {
        if (e && typeof e.clientX === "number") {
          finalDrop = resolveDropTarget(e.clientX, e.clientY, ds.id);
        }
      } catch (err2) {}
      if (finalDrop && finalDrop.parentId) {
        ds.overParent = finalDrop.parentId;
        ds.overIndex =
          typeof finalDrop.index === "number" ? finalDrop.index : 9999;
      }

      if (ds.overParent && ds.overParent !== ds.id && !isNodeInside(ds.overParent, ds.id)) {
        var parent = getNode(ds.overParent);
        if (parent && parent.type === "section") {
          var col = columnUnderPoint(
            parent.id,
            e && e.clientX ? e.clientX : 0,
            e && e.clientY ? e.clientY : 0
          );
          if (col) {
            ds.overParent = col;
            ds.overIndex = 9999;
          }
        }
        // Skip no-op moves (same parent + same effective index).
        var cur = getNode(ds.id);
        var sameParent = cur && cur.parentId === ds.overParent;
        execute(function () {
          moveNode(ds.id, ds.overParent, ds.overIndex);
          state.selectedId = ds.id;
        });
      } else {
        paint();
      }
    }
  }

  function setLeftPanel(panel) {
    state.leftPanel = panel || "widgets";
    document.querySelectorAll("[data-left-panel]").forEach(function (btn) {
      btn.classList.toggle("is-active", btn.getAttribute("data-left-panel") === state.leftPanel);
    });
    document.querySelectorAll("[data-left-view]").forEach(function (view) {
      view.hidden = view.getAttribute("data-left-view") !== state.leftPanel;
    });
    var hd = document.getElementById("ab-left-hd");
    if (hd) {
      var titles = { widgets: "Elementos", layers: "Camadas", settings: "Config. do site" };
      hd.textContent = titles[state.leftPanel] || "Painel";
    }
    if (state.leftPanel === "settings") paintSiteSettings();
  }

  function paintWidgetCatalog() {
    var cat = document.getElementById("ab-cat");
    if (!cat) return;
    if (!state.widgetTab || state.widgetTab === "embed") state.widgetTab = "layout";
    if (state.widgetSearch == null) state.widgetSearch = "";
    var q = String(state.widgetSearch || "")
      .toLowerCase()
      .trim();

    var html =
      '<div class="ab-elem-search-wrap">' +
      '<input type="search" class="ab-elem-search" id="ab-elem-search" placeholder="Pesquisar elementos…" value="' +
      esc(state.widgetSearch || "") +
      '" autocomplete="off"/>' +
      "</div>";

    html += '<div class="ab-elem-tabs" role="tablist">';
    CATALOG_GROUPS.forEach(function (g) {
      html +=
        '<button type="button" class="ab-elem-tab' +
        (state.widgetTab === g.id && !q ? " is-active" : "") +
        '" data-widget-tab="' +
        esc(g.id) +
        '" title="' +
        esc(g.title) +
        '" aria-label="' +
        esc(g.title) +
        '" role="tab">' +
        '<span class="ab-elem-tab__ico" aria-hidden="true">' +
        svgIcon(g.icon || g.id) +
        "</span>" +
        "</button>";
    });
    html += "</div>";

    function matchWidget(w) {
      if (!q) return true;
      var hay =
        (w.title || "") +
        " " +
        (w.type || "") +
        " " +
        (w.group || "") +
        " " +
        (w.icon || "") +
        " " +
        JSON.stringify(w.defaults || {});
      return hay.toLowerCase().indexOf(q) >= 0;
    }

    function widgetBtn(w) {
      var defJson = encodeURIComponent(JSON.stringify(w.defaults || {}));
      return (
        '<button type="button" class="ab-cat-item" data-add-type="' +
        esc(w.type) +
        '" data-add-defaults="' +
        defJson +
        '" title="' +
        esc(w.title) +
        '">' +
        '<span class="ab-cat-icon">' +
        svgIcon(w.icon || w.type) +
        "</span>" +
        '<span class="ab-cat-label">' +
        esc(w.title) +
        "</span></button>"
      );
    }

    if (q) {
      // Search across ALL tabs
      html += '<div class="ab-widget-group-title">Resultados</div><div class="ab-cat">';
      var found = 0;
      CATALOG.forEach(function (w) {
        if (!matchWidget(w)) return;
        found++;
        html += widgetBtn(w);
      });
      if (!found) {
        html += '<div class="ab-props-empty" style="grid-column:1/-1">Nenhum elemento encontrado.</div>';
      }
      html += "</div>";
    } else {
      CATALOG_GROUPS.forEach(function (g) {
        html +=
          '<div class="ab-elem-pane" data-widget-pane="' +
          esc(g.id) +
          '"' +
          (state.widgetTab === g.id ? "" : " hidden") +
          ">";
        if (g.id === "layout") {
          // Layout: subgrupos com linha divisória (Seções | Seção interna | Containers | Barra lateral)
          LAYOUT_SUBGROUPS.forEach(function (sg, sgi) {
            if (sgi > 0) html += '<div class="ab-layout-divider" role="separator"></div>';
            html +=
              '<div class="ab-widget-group-title">' + esc(sg.title) + "</div>";
            html += '<div class="ab-cat">';
            CATALOG.filter(function (w) {
              return w.group === "layout" && (w.subgroup || "sections") === sg.id;
            }).forEach(function (w) {
              html += widgetBtn(w);
            });
            html += "</div>";
            // Presets de colunas dentro da seção Containers
            if (sg.id === "containers") {
              html +=
                '<div class="ab-widget-group-title" style="margin-top:12px">Colunas rápidas</div>' +
                '<div class="ab-preset-row" id="ab-presets"></div>' +
                '<p class="ab-field-hint" style="margin:6px 0 0">Cria uma seção com colunas já configuradas (grid 12).</p>';
            }
          });
        } else {
          html += '<div class="ab-widget-group-title">' + esc(g.title) + "</div>";
          html += '<div class="ab-cat">';
          CATALOG.filter(function (w) {
            return w.group === g.id;
          }).forEach(function (w) {
            html += widgetBtn(w);
          });
          html += "</div>";
        }
        html += "</div>";
      });
    }

    cat.innerHTML = html;

    var searchInp = document.getElementById("ab-elem-search");
    if (searchInp) {
      searchInp.addEventListener("input", function () {
        state.widgetSearch = searchInp.value;
        paintWidgetCatalog();
        var again = document.getElementById("ab-elem-search");
        if (again) {
          again.focus();
          var len = again.value.length;
          try {
            again.setSelectionRange(len, len);
          } catch (e) {}
        }
      });
    }

    cat.querySelectorAll("[data-widget-tab]").forEach(function (btn) {
      btn.addEventListener("click", function () {
        state.widgetTab = btn.getAttribute("data-widget-tab") || "layout";
        state.widgetSearch = "";
        paintWidgetCatalog();
      });
    });
    cat.querySelectorAll("[data-add-type]").forEach(function (btn) {
      btn.addEventListener("click", function () {
        var t = btn.getAttribute("data-add-type");
        var defRaw = btn.getAttribute("data-add-defaults") || "";
        var extra = {};
        try {
          extra = defRaw ? JSON.parse(decodeURIComponent(defRaw)) : {};
        } catch (err) {
          extra = {};
        }
        execute(function () {
          addNode(t, targetParentId(), extra);
        });
      });
    });
    var presets = document.getElementById("ab-presets");
    if (presets) {
      [
        { label: "2 colunas", ratios: [50, 50], hint: "50 / 50" },
        { label: "3 colunas", ratios: [33, 34, 33], hint: "33×3" },
        { label: "4 colunas", ratios: [25, 25, 25, 25], hint: "25×4" },
        { label: "25 / 75", ratios: [25, 75], hint: "Sidebar + conteúdo" },
        { label: "75 / 25", ratios: [75, 25], hint: "Conteúdo + sidebar" },
        { label: "33 / 67", ratios: [33, 67], hint: "1/3 + 2/3" },
      ].forEach(function (p) {
        var b = document.createElement("button");
        b.type = "button";
        b.className = "ab-preset-btn";
        b.textContent = p.label;
        b.title = "Criar seção com colunas " + (p.hint || p.label);
        b.addEventListener("click", function () {
          addColumnsPreset(p.ratios);
        });
        presets.appendChild(b);
      });
    }
  }

  function paintSiteSettings() {
    var box = document.getElementById("ab-site-settings");
    if (!box) return;
    if (!state.document) return;
    ensureDocSettings();
    if (!state.settingsTab) state.settingsTab = "page";

    // Theme Builder: only part conditions (no page tabs).
    if (isThemePart) {
      paintThemePartSettings(box);
      return;
    }

    // Tabs: esta página vs globais do site
    var tabs =
      '<div class="ab-settings-tabs">' +
      '<button type="button" class="ab-settings-tab' +
      (state.settingsTab === "page" ? " is-active" : "") +
      '" data-settings-tab="page">Desta página</button>' +
      '<button type="button" class="ab-settings-tab' +
      (state.settingsTab === "global" ? " is-active" : "") +
      '" data-settings-tab="global">Globais do site</button>' +
      "</div>";

    if (state.settingsTab === "global") {
      paintGlobalSettings(box, tabs);
      return;
    }

    var bp = getBreakpoints();
    var s = state.document.settings;
    var siteBg = (s.site_bg || "") + "";
    var mode = s.site_bg_mode || "color";
    var sizeMode = s.site_bg_size || "cover";
    // If stored as "50%" treat as percent mode
    var sizePct = s.site_bg_size_pct || "100";
    if (/^[\d.]+%$/.test(sizeMode) && sizeMode !== "100% 100%") {
      sizePct = String(parseInt(sizeMode, 10) || 100);
      sizeMode = "percent";
    }
    var bgItems = parseGalleryItems(s.site_bg_images || "[]");
    var html = tabs;
    var isHome = !!(s.is_home_page === true || s.is_home_page === 1 || s.is_home_page === "1");
    var pageRole = s.page_role || (isHome ? "home" : "singular");
    html += '<div class="ab-settings-section">';
    html += '<div class="ab-settings-section-title">' + svgIcon("layout") + " Página</div>";
    html +=
      '<div class="ab-field"><label>Tipo / papel da página</label><select id="ab-ss-page-role">' +
      [
        { v: "home", l: "Principal (home)" },
        { v: "singular", l: "Página única" },
        { v: "archive", l: "Arquivo (listagem)" },
        { v: "category", l: "Categorias" },
      ]
        .map(function (o) {
          return (
            '<option value="' +
            o.v +
            '"' +
            (pageRole === o.v ? " selected" : "") +
            ">" +
            o.l +
            "</option>"
          );
        })
        .join("") +
      "</select></div>";
    html +=
      '<label class="ab-settings-toggle' +
      (isHome || pageRole === "home" ? " is-on" : "") +
      '"><span class="ab-check-ui" aria-hidden="true"></span>' +
      '<input type="checkbox" id="ab-ss-home"' +
      (isHome || pageRole === "home" ? " checked" : "") +
      "/>" +
      svgIcon("home") +
      '<span class="ab-settings-toggle-label">Definir como Página Inicial</span></label>';
    html +=
      '<span class="ab-field-hint">Principal = home do site + <strong>Aparência → Editor</strong> abre no Aura. Arquivo/Categorias marcam o uso da página (consulta atual / filtros).</span>';
    html +=
      '<div class="ab-field" id="ab-ss-page-cats-wrap" style="' +
      (pageRole === "category" ? "" : "display:none") +
      '"><label>Categorias desta página</label>' +
      '<input type="text" id="ab-ss-page-cats" value="' +
      esc(s.page_categories || "") +
      '" placeholder="IDs: 12,34 ou slugs separados por vírgula"/>' +
      '<span class="ab-field-hint">Usado com Loop em “consulta atual do arquivo” ou filtros manuais.</span></div>';
    var hSrc = s.header_source || "global";
    var fSrc = s.footer_source || "global";
    html +=
      '<div class="ab-field"><label>Header nesta página</label><select id="ab-ss-header-src">' +
      [
        { v: "global", l: "Padrão (Theme Builder / tema)" },
        { v: "builder", l: "Header do Builder" },
        { v: "theme", l: "Header do tema" },
        { v: "none", l: "Ocultar header" },
      ]
        .map(function (o) {
          return (
            '<option value="' +
            o.v +
            '"' +
            (hSrc === o.v ? " selected" : "") +
            ">" +
            o.l +
            "</option>"
          );
        })
        .join("") +
      "</select></div>";
    html +=
      '<div class="ab-field"><label>Footer nesta página</label><select id="ab-ss-footer-src">' +
      [
        { v: "global", l: "Padrão (Theme Builder / tema)" },
        { v: "builder", l: "Footer do Builder" },
        { v: "theme", l: "Footer do tema" },
        { v: "none", l: "Ocultar footer" },
      ]
        .map(function (o) {
          return (
            '<option value="' +
            o.v +
            '"' +
            (fSrc === o.v ? " selected" : "") +
            ">" +
            o.l +
            "</option>"
          );
        })
        .join("") +
      "</select></div>";
    html +=
      '<div class="ab-chrome-edit-row" style="margin:8px 0 4px">' +
      '<button type="button" class="ab-btn ab-btn--primary" id="ab-ss-edit-header">✎ Editar Header</button>' +
      '<button type="button" class="ab-btn" id="ab-ss-edit-footer">✎ Editar Footer</button>' +
      "</div>";
    html +=
      '<p class="ab-field-hint"><a href="admin.php?page=aura-builder-theme">Painel Header &amp; Footer</a> · “Ocultar” vale só nesta página. Voltar no editor de H/F retorna a esta página.</p>';
    // Legacy toggles map to theme/none when source global.
    html +=
      '<label class="ab-settings-toggle' +
      (showHeader() ? " is-on" : "") +
      '"><span class="ab-check-ui" aria-hidden="true"></span>' +
      '<input type="checkbox" id="ab-ss-header"' +
      (showHeader() ? " checked" : "") +
      "/>" +
      svgIcon("header") +
      '<span class="ab-settings-toggle-label">Exibir header (legado)</span></label>';
    html +=
      '<label class="ab-settings-toggle' +
      (showFooter() ? " is-on" : "") +
      '"><span class="ab-check-ui" aria-hidden="true"></span>' +
      '<input type="checkbox" id="ab-ss-footer"' +
      (showFooter() ? " checked" : "") +
      "/>" +
      svgIcon("footer") +
      '<span class="ab-settings-toggle-label">Exibir footer (legado)</span></label>';
    html += "</div>";

    html += '<div class="ab-settings-section">';
    html += '<div class="ab-settings-section-title">' + svgIcon("color") + " Fundo da página</div>";
    html +=
      '<p class="ab-field-hint" style="margin-bottom:10px">Cor, <strong>imagem</strong> ou <strong>carrossel</strong>. Tamanho em % disponível abaixo.</p>';
    html += '<div class="ab-ss-mode-row" role="radiogroup" aria-label="Tipo de fundo">';
    [
      { v: "color", l: "Cor sólida" },
      { v: "image", l: "Imagem" },
      { v: "carousel", l: "Carrossel" },
    ].forEach(function (o) {
      html +=
        '<label class="ab-ss-mode' +
        (mode === o.v ? " is-on" : "") +
        '"><input type="radio" name="ab-ss-bg-mode" value="' +
        o.v +
        '"' +
        (mode === o.v ? " checked" : "") +
        "/> " +
        o.l +
        "</label>";
    });
    html += "</div>";
    // Hidden select kept for commitBgSettings compatibility
    html +=
      '<select id="ab-ss-bg-mode" hidden aria-hidden="true">' +
      '<option value="color"' +
      (mode === "color" ? " selected" : "") +
      ">color</option>" +
      '<option value="image"' +
      (mode === "image" ? " selected" : "") +
      ">image</option>" +
      '<option value="carousel"' +
      (mode === "carousel" ? " selected" : "") +
      ">carousel</option></select>";

    html +=
      '<div class="ab-field"><label>Cor de fundo</label><div class="ab-color-row"><input type="color" id="ab-ss-bg-swatch" class="ab-color-swatch" value="' +
      esc(/^#[0-9a-fA-F]{6}$/.test(siteBg) ? siteBg : "#ffffff") +
      '"/><input type="text" id="ab-ss-bg" class="ab-color-hex" value="' +
      esc(siteBg) +
      '" placeholder="#ffffff ou vazio"/></div></div>';

    // Size — always useful for image/carousel
    html +=
      '<div class="ab-field" id="ab-ss-size-wrap"><label>Tamanho do fundo</label><select id="ab-ss-bg-size">' +
      '<option value="cover"' +
      (sizeMode === "cover" ? " selected" : "") +
      ">Cobrir (cover)</option>" +
      '<option value="contain"' +
      (sizeMode === "contain" ? " selected" : "") +
      ">Conter (contain)</option>" +
      '<option value="auto"' +
      (sizeMode === "auto" ? " selected" : "") +
      ">Automático</option>" +
      '<option value="100% 100%"' +
      (sizeMode === "100% 100%" ? " selected" : "") +
      ">Esticar 100% × 100%</option>" +
      '<option value="percent"' +
      (sizeMode === "percent" ? " selected" : "") +
      ">Porcentagem (%)</option>" +
      "</select></div>";
    html +=
      '<div class="ab-field" id="ab-ss-pct-wrap" style="' +
      (sizeMode === "percent" ? "" : "display:none") +
      '"><label>Tamanho em % (ex.: 50 = metade)</label><input type="number" id="ab-ss-bg-pct" min="1" max="300" value="' +
      esc(String(sizePct)) +
      '"/><span class="ab-field-hint">Valores típicos: 50, 80, 100, 150…</span></div>';

    html +=
      '<div class="ab-field" id="ab-ss-pos-wrap"><label>Posição</label><select id="ab-ss-bg-pos">' +
      ["center center", "top center", "bottom center", "center left", "center right", "top left", "top right", "bottom left", "bottom right"]
        .map(function (p) {
          return (
            '<option value="' +
            p +
            '"' +
            ((s.site_bg_position || "center center") === p ? " selected" : "") +
            ">" +
            p +
            "</option>"
          );
        })
        .join("") +
      "</select></div>";
    html +=
      '<div class="ab-field" id="ab-ss-rep-wrap"><label>Repetir</label><select id="ab-ss-bg-rep">' +
      [
        { v: "no-repeat", l: "Não repetir" },
        { v: "repeat", l: "Repetir" },
        { v: "repeat-x", l: "Repetir X" },
        { v: "repeat-y", l: "Repetir Y" },
      ]
        .map(function (o) {
          return (
            '<option value="' +
            o.v +
            '"' +
            ((s.site_bg_repeat || "no-repeat") === o.v ? " selected" : "") +
            ">" +
            o.l +
            "</option>"
          );
        })
        .join("") +
      "</select></div>";

    // Single image
    html +=
      '<div class="ab-field" id="ab-ss-img-wrap" style="' +
      (mode === "image" ? "" : "display:none") +
      '"><label>Imagem de fundo</label>';
    if (s.site_bg_image) {
      html +=
        '<div class="ab-ss-bg-preview"><img src="' +
        esc(s.site_bg_image) +
        '" alt=""/></div>';
    }
    html +=
      '<div class="ab-ss-btn-row">' +
      '<button type="button" class="ab-btn ab-btn--primary" id="ab-ss-bg-pick">' +
      svgIcon("image") +
      " Escolher imagem</button>" +
      (s.site_bg_image
        ? '<button type="button" class="ab-btn ab-btn--ghost" id="ab-ss-bg-clear">Remover</button>'
        : "") +
      "</div></div>";

    // Carousel gallery
    html +=
      '<div class="ab-field" id="ab-ss-car-wrap" style="' +
      (mode === "carousel" ? "" : "display:none") +
      '"><label>Imagens do carrossel</label>';
    html += '<div class="ab-gallery-thumbs" id="ab-ss-car-thumbs">';
    if (bgItems.length) {
      bgItems.forEach(function (it, i) {
        html +=
          '<div class="ab-gallery-thumb" data-ss-g-idx="' +
          i +
          '"><img src="' +
          esc(it.url) +
          '" alt=""/><button type="button" class="ab-gallery-thumb__rm" data-ss-g-rm="' +
          i +
          '" title="Remover">×</button></div>';
      });
    } else {
      html += '<div class="ab-gallery-thumbs--empty">Nenhuma imagem — abra a mídia</div>';
    }
    html += "</div>";
    html +=
      '<div class="ab-ss-btn-row">' +
      '<button type="button" class="ab-btn ab-btn--primary" id="ab-ss-car-pick">' +
      svgIcon("gallery") +
      " Selecionar imagens</button>" +
      (bgItems.length
        ? '<button type="button" class="ab-btn ab-btn--ghost" id="ab-ss-car-clear">Limpar</button>'
        : "") +
      "</div>";
    html +=
      '<div class="ab-field"><label>Tempo entre slides (ms)</label><input type="number" id="ab-ss-car-speed" min="1500" max="30000" step="100" value="' +
      esc(String(s.site_bg_carousel_speed || 5000)) +
      '"/></div>';
    html +=
      '<div class="ab-field"><label>Efeito</label><select id="ab-ss-car-fx">' +
      [
        { v: "fade", l: "Fade" },
        { v: "slide", l: "Deslizar" },
        { v: "zoom", l: "Zoom" },
      ]
        .map(function (o) {
          return (
            '<option value="' +
            o.v +
            '"' +
            ((s.site_bg_carousel_fx || "fade") === o.v ? " selected" : "") +
            ">" +
            o.l +
            "</option>"
          );
        })
        .join("") +
      "</select></div></div>";

    html +=
      '<span class="ab-field-hint">Salve a página para aplicar o fundo no site público.</span>';
    html += "</div>";

    html += '<div class="ab-settings-section">';
    html += '<div class="ab-settings-section-title">' + svgIcon("desktop") + " Breakpoints</div>";
    html += '<p class="ab-field-hint">Larguras máximas (px) para CSS tablet/mobile — como no Elementor.</p>';
    html +=
      '<div class="ab-field"><label>Mobile (max-width)</label><input type="number" id="ab-ss-bp-mobile" min="320" max="1200" value="' +
      esc(String(bp.mobile)) +
      '"/></div>';
    html +=
      '<div class="ab-field"><label>Tablet (max-width)</label><input type="number" id="ab-ss-bp-tablet" min="480" max="1600" value="' +
      esc(String(bp.tablet)) +
      '"/></div>';
    html +=
      '<div class="ab-field"><label>Laptop (referência)</label><input type="number" id="ab-ss-bp-laptop" min="1024" max="2560" value="' +
      esc(String(bp.laptop)) +
      '"/></div>';
    html += "</div>";

    html +=
      '<p class="ab-field-hint" style="margin-top:16px">Salve a página para aplicar breakpoints no site público.</p>';
    box.innerHTML = html;

    var sh = document.getElementById("ab-ss-header");
    var sf = document.getElementById("ab-ss-footer");
    var shome = document.getElementById("ab-ss-home");
    if (sh)
      sh.addEventListener("change", function () {
        setChromeFlag("showHeader", sh.checked);
        paintSiteSettings();
      });
    if (sf)
      sf.addEventListener("change", function () {
        setChromeFlag("showFooter", sf.checked);
        paintSiteSettings();
      });
    if (shome)
      shome.addEventListener("change", function () {
        ensureDocSettings();
        state.document.settings.is_home_page = !!shome.checked;
        if (shome.checked) state.document.settings.page_role = "home";
        else if (state.document.settings.page_role === "home") {
          state.document.settings.page_role = "singular";
        }
        markDirty();
        paintSiteSettings();
      });
    var roleSel = document.getElementById("ab-ss-page-role");
    if (roleSel)
      roleSel.addEventListener("change", function () {
        ensureDocSettings();
        var role = roleSel.value || "singular";
        state.document.settings.page_role = role;
        state.document.settings.is_home_page = role === "home";
        markDirty();
        paintSiteSettings();
      });
    var pageCats = document.getElementById("ab-ss-page-cats");
    if (pageCats)
      pageCats.addEventListener("change", function () {
        ensureDocSettings();
        state.document.settings.page_categories = String(pageCats.value || "").trim();
        markDirty();
      });
    var hSrcEl = document.getElementById("ab-ss-header-src");
    if (hSrcEl)
      hSrcEl.addEventListener("change", function () {
        ensureDocSettings();
        state.document.settings.header_source = hSrcEl.value || "global";
        if (hSrcEl.value === "none") state.document.settings.showHeader = false;
        else if (hSrcEl.value === "theme" || hSrcEl.value === "builder") state.document.settings.showHeader = true;
        markDirty();
        paint();
      });
    var fSrcEl = document.getElementById("ab-ss-footer-src");
    if (fSrcEl)
      fSrcEl.addEventListener("change", function () {
        ensureDocSettings();
        state.document.settings.footer_source = fSrcEl.value || "global";
        if (fSrcEl.value === "none") state.document.settings.showFooter = false;
        else if (fSrcEl.value === "theme" || fSrcEl.value === "builder") state.document.settings.showFooter = true;
        markDirty();
        paint();
      });
    var ssEditH = document.getElementById("ab-ss-edit-header");
    var ssEditF = document.getElementById("ab-ss-edit-footer");
    if (ssEditH)
      ssEditH.addEventListener("click", function () {
        openThemePartEditor("header");
      });
    if (ssEditF)
      ssEditF.addEventListener("click", function () {
        openThemePartEditor("footer");
      });

    function saveBp() {
      ensureDocSettings();
      state.document.settings.breakpoints = {
        mobile: parseInt(document.getElementById("ab-ss-bp-mobile").value, 10) || 767,
        tablet: parseInt(document.getElementById("ab-ss-bp-tablet").value, 10) || 1024,
        laptop: parseInt(document.getElementById("ab-ss-bp-laptop").value, 10) || 1400,
      };
      markDirty();
    }
    ["ab-ss-bp-mobile", "ab-ss-bp-tablet", "ab-ss-bp-laptop"].forEach(function (id) {
      var el = document.getElementById(id);
      if (el) el.addEventListener("change", saveBp);
    });

    function toggleBgModeUI(m) {
      var imgW = document.getElementById("ab-ss-img-wrap");
      var carW = document.getElementById("ab-ss-car-wrap");
      if (imgW) imgW.style.display = m === "image" ? "" : "none";
      if (carW) carW.style.display = m === "carousel" ? "" : "none";
    }
    function togglePctUI() {
      var sm = document.getElementById("ab-ss-bg-size");
      var pw = document.getElementById("ab-ss-pct-wrap");
      if (pw && sm) pw.style.display = sm.value === "percent" ? "" : "none";
    }
    function commitBgSettings(repaint) {
      ensureDocSettings();
      var st = state.document.settings;
      var modeEl = document.getElementById("ab-ss-bg-mode");
      var sizeEl = document.getElementById("ab-ss-bg-size");
      var pctEl = document.getElementById("ab-ss-bg-pct");
      var posEl = document.getElementById("ab-ss-bg-pos");
      var repEl = document.getElementById("ab-ss-bg-rep");
      var speedEl = document.getElementById("ab-ss-car-speed");
      var fxEl = document.getElementById("ab-ss-car-fx");
      if (modeEl) st.site_bg_mode = modeEl.value || "color";
      if (sizeEl) {
        if (sizeEl.value === "percent") {
          var pct = parseInt(pctEl && pctEl.value, 10);
          if (isNaN(pct) || pct < 1) pct = 100;
          if (pct > 300) pct = 300;
          st.site_bg_size = pct + "%";
          st.site_bg_size_pct = String(pct);
        } else {
          st.site_bg_size = sizeEl.value || "cover";
        }
      }
      if (posEl) st.site_bg_position = posEl.value || "center center";
      if (repEl) st.site_bg_repeat = repEl.value || "no-repeat";
      if (speedEl) st.site_bg_carousel_speed = Math.max(1500, Math.min(30000, parseInt(speedEl.value, 10) || 5000));
      if (fxEl) st.site_bg_carousel_fx = fxEl.value || "fade";
      markDirty();
      applyPageBackgroundPreview();
      if (repaint) paintSiteSettings();
    }

    var modeSel = document.getElementById("ab-ss-bg-mode");
    box.querySelectorAll('input[name="ab-ss-bg-mode"]').forEach(function (radio) {
      radio.addEventListener("change", function () {
        if (!radio.checked) return;
        if (modeSel) modeSel.value = radio.value;
        toggleBgModeUI(radio.value);
        commitBgSettings(true);
      });
    });
    if (modeSel)
      modeSel.addEventListener("change", function () {
        toggleBgModeUI(modeSel.value);
        commitBgSettings(true);
      });
    var sizeSel = document.getElementById("ab-ss-bg-size");
    if (sizeSel)
      sizeSel.addEventListener("change", function () {
        togglePctUI();
        commitBgSettings(false);
      });
    ["ab-ss-bg-pct", "ab-ss-bg-pos", "ab-ss-bg-rep", "ab-ss-car-speed", "ab-ss-car-fx"].forEach(function (id) {
      var el = document.getElementById(id);
      if (el) el.addEventListener("change", function () {
        commitBgSettings(false);
      });
    });

    var bg = document.getElementById("ab-ss-bg");
    var bgSw = document.getElementById("ab-ss-bg-swatch");
    function applySiteBgColor(val) {
      ensureDocSettings();
      state.document.settings.site_bg = val || "";
      markDirty();
      applyPageBackgroundPreview();
    }
    if (bg)
      bg.addEventListener("change", function () {
        applySiteBgColor(String(bg.value || "").trim());
        if (bgSw && /^#([0-9a-fA-F]{6})$/.test(bg.value)) bgSw.value = bg.value;
      });
    if (bgSw)
      bgSw.addEventListener("input", function () {
        if (bg) bg.value = bgSw.value;
        applySiteBgColor(bgSw.value);
      });

    var pickImg = document.getElementById("ab-ss-bg-pick");
    if (pickImg)
      pickImg.addEventListener("click", function () {
        openMediaLibrary(function (att) {
          ensureDocSettings();
          state.document.settings.site_bg_image = (att && att.url) || "";
          if (state.document.settings.site_bg_mode !== "image") {
            state.document.settings.site_bg_mode = "image";
          }
          markDirty();
          applyPageBackgroundPreview();
          paintSiteSettings();
        });
      });
    var clearImg = document.getElementById("ab-ss-bg-clear");
    if (clearImg)
      clearImg.addEventListener("click", function () {
        ensureDocSettings();
        state.document.settings.site_bg_image = "";
        markDirty();
        applyPageBackgroundPreview();
        paintSiteSettings();
      });

    var pickCar = document.getElementById("ab-ss-car-pick");
    if (pickCar)
      pickCar.addEventListener("click", function () {
        var cur = parseGalleryItems((state.document.settings && state.document.settings.site_bg_images) || "[]");
        openMediaLibrary(
          function (atts) {
            ensureDocSettings();
            var list = Array.isArray(atts) ? atts : atts ? [atts] : [];
            state.document.settings.site_bg_images = serializeGalleryItems(
              list.map(function (a) {
                return { id: a.id || 0, url: a.url || "" };
              })
            );
            state.document.settings.site_bg_mode = "carousel";
            markDirty();
            applyPageBackgroundPreview();
            paintSiteSettings();
          },
          {
            multiple: true,
            selectedIds: cur.map(function (it) {
              return it.id;
            }),
            selectedUrls: cur.map(function (it) {
              return it.url;
            }),
          }
        );
      });
    var clearCar = document.getElementById("ab-ss-car-clear");
    if (clearCar)
      clearCar.addEventListener("click", function () {
        ensureDocSettings();
        state.document.settings.site_bg_images = "[]";
        markDirty();
        applyPageBackgroundPreview();
        paintSiteSettings();
      });
    box.querySelectorAll("[data-ss-g-rm]").forEach(function (btn) {
      btn.addEventListener("click", function (e) {
        e.preventDefault();
        e.stopPropagation();
        var idx = parseInt(btn.getAttribute("data-ss-g-rm"), 10);
        ensureDocSettings();
        var items = parseGalleryItems(state.document.settings.site_bg_images || "[]");
        if (!isNaN(idx) && idx >= 0 && idx < items.length) {
          items.splice(idx, 1);
          state.document.settings.site_bg_images = serializeGalleryItems(items);
          markDirty();
          applyPageBackgroundPreview();
          paintSiteSettings();
        }
      });
    });

    bindSettingsTabs(box);
  }

  function bindSettingsTabs(box) {
    box.querySelectorAll("[data-settings-tab]").forEach(function (btn) {
      btn.addEventListener("click", function () {
        state.settingsTab = btn.getAttribute("data-settings-tab") || "page";
        paintSiteSettings();
      });
    });
  }

  /** Settings panel when editing Header/Footer theme parts. */
  function paintThemePartSettings(box) {
    var title =
      editorMode === "footer" ? "Footer" : editorMode === "single" ? "Single Post" : "Header";
    var m = themePartMeta;
    var html = "";
    html += '<div class="ab-settings-section">';
    html +=
      '<div class="ab-settings-section-title">' +
      svgIcon(editorMode === "footer" ? "footer" : editorMode === "single" ? "text" : "header") +
      " " +
      title +
      " · Theme Builder</div>";
    html +=
      '<label class="ab-settings-toggle' +
      (m.enabled ? " is-on" : "") +
      '"><span class="ab-check-ui" aria-hidden="true"></span>' +
      '<input type="checkbox" id="ab-tp-enabled"' +
      (m.enabled ? " checked" : "") +
      "/>" +
      '<span class="ab-settings-toggle-label">Ativar ' +
      title +
      (editorMode === "single"
        ? " (layout dinâmico de posts no site)"
        : " do Builder (substitui o tema)") +
      "</span></label>";
    if (editorMode === "single" && !m.enabled) {
      html +=
        '<div class="ab-field-hint" style="margin:8px 0;padding:10px;border-radius:8px;background:#422006;border:1px solid #b45309;color:#fde68a">' +
        "<strong>Single inativo.</strong> Marque “Ativar” acima e <strong>Salve</strong>. " +
        "Só então o layout substitui o conteúdo nos posts do site. " +
        "Uma página normal com widget “Dinâmico” <em>não</em> vira template de post — use " +
        "Theme Builder → Single Post." +
        "</div>";
    }
    html +=
      '<div class="ab-field"><label>Exibir em</label><select id="ab-tp-display">' +
      (editorMode === "single"
        ? [
            { v: "singular", l: "Posts e páginas (recomendado)" },
            { v: "all", l: "Tudo (singular + arquivos)" },
            { v: "none", l: "Nenhuma (desligado)" },
          ]
        : [
            { v: "all", l: "Todas as páginas" },
            { v: "home", l: "Só na Home" },
            { v: "singular", l: "Só páginas/posts (singular)" },
            { v: "none", l: "Nenhuma (desligado por condição)" },
          ]
      )
        .map(function (o) {
          var cur =
            editorMode === "single" ? m.display || "singular" : m.display || "all";
          return (
            '<option value="' +
            o.v +
            '"' +
            (cur === o.v ? " selected" : "") +
            ">" +
            o.l +
            "</option>"
          );
        })
        .join("") +
      "</select></div>";
    html +=
      '<div class="ab-field"><label>Incluir só estes IDs</label><input type="text" id="ab-tp-include" value="' +
      esc(m.include_ids || "") +
      '" placeholder="ex: 12,34 — vazio = usar “Exibir em”"/></div>';
    html +=
      '<div class="ab-field"><label>Excluir IDs</label><input type="text" id="ab-tp-exclude" value="' +
      esc(m.exclude_ids || "") +
      '" placeholder="ex: 56"/></div>';
    html +=
      editorMode === "single"
        ? '<span class="ab-field-hint">Use widgets <strong>Dinâmicos</strong> (título, conteúdo, imagem…). No site, cada página/post preenche esses campos.</span>'
        : '<span class="ab-field-hint">Por página: no editor da página, ⚙ → Header/Footer nesta página (global / builder / tema / ocultar).</span>';
    html += "</div>";

    // Single: header/footer chrome (default Theme Builder vs theme vs hide).
    if (editorMode === "single") {
      var hSrc = m.header_source || "global";
      var fSrc = m.footer_source || "global";
      var chromeOpts = [
        { v: "global", l: "Padrão (Theme Builder Header/Footer)" },
        { v: "builder", l: "Forçar Header/Footer do Builder" },
        { v: "theme", l: "Header/Footer do tema WordPress" },
        { v: "none", l: "Ocultar" },
      ];
      function chromeSelect(id, label, cur) {
        return (
          '<div class="ab-field"><label>' +
          label +
          '</label><select id="' +
          id +
          '">' +
          chromeOpts
            .map(function (o) {
              return (
                '<option value="' +
                o.v +
                '"' +
                (cur === o.v ? " selected" : "") +
                ">" +
                o.l +
                "</option>"
              );
            })
            .join("") +
          "</select></div>"
        );
      }
      html += '<div class="ab-settings-section">';
      html +=
        '<div class="ab-settings-section-title">' +
        svgIcon("header") +
        " Header &amp; Footer neste Single</div>";
      html +=
        '<span class="ab-field-hint">O Single define só o <strong>conteúdo</strong>. Escolha se usa o header/footer padrão do site ou oculta.</span>';
      html += chromeSelect("ab-tp-header-src", "Header", hSrc);
      html += chromeSelect("ab-tp-footer-src", "Footer", fSrc);
      html +=
        '<div class="ab-chrome-edit-row" style="margin:8px 0 4px">' +
        '<button type="button" class="ab-btn ab-btn--primary" id="ab-tp-edit-header">✎ Editar Header</button>' +
        '<button type="button" class="ab-btn" id="ab-tp-edit-footer">✎ Editar Footer</button>' +
        "</div>";
      html +=
        '<label class="ab-settings-toggle' +
        (showHeader() ? " is-on" : "") +
        '"><span class="ab-check-ui" aria-hidden="true"></span>' +
        '<input type="checkbox" id="ab-tp-show-header"' +
        (showHeader() ? " checked" : "") +
        "/>" +
        svgIcon("header") +
        '<span class="ab-settings-toggle-label">Exibir header</span></label>';
      html +=
        '<label class="ab-settings-toggle' +
        (showFooter() ? " is-on" : "") +
        '"><span class="ab-check-ui" aria-hidden="true"></span>' +
        '<input type="checkbox" id="ab-tp-show-footer"' +
        (showFooter() ? " checked" : "") +
        "/>" +
        svgIcon("footer") +
        '<span class="ab-settings-toggle-label">Exibir footer</span></label>';
      html += "</div>";
    }

    if (editorMode === "header") {
      html += '<div class="ab-settings-section">';
      html += '<div class="ab-settings-section-title">Sticky &amp; transparente</div>';
      html +=
        '<label class="ab-settings-toggle' +
        (m.sticky === "1" ? " is-on" : "") +
        '"><span class="ab-check-ui" aria-hidden="true"></span>' +
        '<input type="checkbox" id="ab-tp-sticky"' +
        (m.sticky === "1" ? " checked" : "") +
        "/>" +
        '<span class="ab-settings-toggle-label">Header sticky</span></label>';
      html +=
        '<div class="ab-field"><label>Fundo sticky</label><input type="text" id="ab-tp-sticky-bg" value="' +
        esc(m.sticky_bg || "#ffffff") +
        '" placeholder="#ffffff"/></div>';
      html +=
        '<label class="ab-settings-toggle' +
        (m.transparent === "1" ? " is-on" : "") +
        '"><span class="ab-check-ui" aria-hidden="true"></span>' +
        '<input type="checkbox" id="ab-tp-transparent"' +
        (m.transparent === "1" ? " checked" : "") +
        "/>" +
        '<span class="ab-settings-toggle-label">Header transparente (sobre o hero)</span></label>';
      html += "</div>";
    }

    html +=
      '<p class="ab-field-hint">Monte o layout no canvas (Logo dinâmico + Menu de navegação + botões). <strong>Salve</strong> no topo para publicar o ' +
      title +
      ".</p>";
    html +=
      '<p class="ab-field-hint"><a href="nav-menus.php" target="_blank" rel="noopener">Aparência → Menus</a> · widgets no catálogo Básicos.</p>';

    box.innerHTML = html;

    function markTp() {
      markDirty();
    }
    var en = document.getElementById("ab-tp-enabled");
    if (en)
      en.addEventListener("change", function () {
        themePartMeta.enabled = !!en.checked;
        markTp();
        paintThemePartSettings(box);
      });
    var disp = document.getElementById("ab-tp-display");
    if (disp)
      disp.addEventListener("change", function () {
        themePartMeta.display = disp.value || "all";
        markTp();
      });
    ["ab-tp-include", "ab-tp-exclude", "ab-tp-sticky-bg", "ab-tp-header-src", "ab-tp-footer-src"].forEach(
      function (id) {
        var el = document.getElementById(id);
        if (!el) return;
        el.addEventListener("change", function () {
          if (id === "ab-tp-include") themePartMeta.include_ids = el.value.trim();
          if (id === "ab-tp-exclude") themePartMeta.exclude_ids = el.value.trim();
          if (id === "ab-tp-sticky-bg") themePartMeta.sticky_bg = el.value.trim() || "#ffffff";
          if (id === "ab-tp-header-src") {
            themePartMeta.header_source = el.value || "global";
            if (state.document && state.document.settings) {
              state.document.settings.header_source = themePartMeta.header_source;
              state.document.settings.showHeader = themePartMeta.header_source !== "none";
            }
            syncChromeVisibility();
            paint();
          }
          if (id === "ab-tp-footer-src") {
            themePartMeta.footer_source = el.value || "global";
            if (state.document && state.document.settings) {
              state.document.settings.footer_source = themePartMeta.footer_source;
              state.document.settings.showFooter = themePartMeta.footer_source !== "none";
            }
            syncChromeVisibility();
            paint();
          }
          markTp();
        });
      }
    );
    var tpH = document.getElementById("ab-tp-show-header");
    if (tpH)
      tpH.addEventListener("change", function () {
        var on = !!tpH.checked;
        themePartMeta.header_source = on
          ? themePartMeta.header_source === "none"
            ? "global"
            : themePartMeta.header_source || "global"
          : "none";
        if (state.document && state.document.settings) {
          state.document.settings.showHeader = on;
          state.document.settings.header_source = themePartMeta.header_source;
        }
        markTp();
        paintThemePartSettings(box);
        paint();
      });
    var tpF = document.getElementById("ab-tp-show-footer");
    if (tpF)
      tpF.addEventListener("change", function () {
        var on = !!tpF.checked;
        themePartMeta.footer_source = on
          ? themePartMeta.footer_source === "none"
            ? "global"
            : themePartMeta.footer_source || "global"
          : "none";
        if (state.document && state.document.settings) {
          state.document.settings.showFooter = on;
          state.document.settings.footer_source = themePartMeta.footer_source;
        }
        markTp();
        paintThemePartSettings(box);
        paint();
      });
    var tpeh = document.getElementById("ab-tp-edit-header");
    if (tpeh)
      tpeh.addEventListener("click", function () {
        openThemePartEditor("header");
      });
    var tpef = document.getElementById("ab-tp-edit-footer");
    if (tpef)
      tpef.addEventListener("click", function () {
        openThemePartEditor("footer");
      });
    var st = document.getElementById("ab-tp-sticky");
    if (st)
      st.addEventListener("change", function () {
        themePartMeta.sticky = st.checked ? "1" : "0";
        markTp();
        paintThemePartSettings(box);
      });
    var tr = document.getElementById("ab-tp-transparent");
    if (tr)
      tr.addEventListener("change", function () {
        themePartMeta.transparent = tr.checked ? "1" : "0";
        markTp();
        paintThemePartSettings(box);
      });
  }

  function paintGlobalSettings(box, tabsHtml) {
    ensureGlobals();
    var g = state.globals;
    var c = g.colors;
    var t = g.typography;
    var l = g.layout;
    var r = g.radius;

    function colorField(key, label) {
      var val = c[key] || "#000000";
      var hex = /^#[0-9a-fA-F]{6}$/.test(val) ? val : "#7c3aed";
      return (
        '<div class="ab-field"><label>' +
        esc(label) +
        '</label><div class="ab-color-row">' +
        '<input type="color" class="ab-color-swatch" data-g-color="' +
        esc(key) +
        '" value="' +
        esc(hex) +
        '"/>' +
        '<input type="text" class="ab-color-hex" data-g-color-text="' +
        esc(key) +
        '" value="' +
        esc(val) +
        '" placeholder="#hex"/>' +
        "</div></div>"
      );
    }

    function textField(key, label, group, placeholder) {
      var bag = group === "layout" ? l : group === "typography" ? t : group === "radius" ? r : {};
      return (
        '<div class="ab-field"><label>' +
        esc(label) +
        '</label><input type="text" data-g-field="' +
        esc(group) +
        "." +
        esc(key) +
        '" value="' +
        esc(String(bag[key] != null ? bag[key] : "")) +
        '" placeholder="' +
        esc(placeholder || "") +
        '"/></div>'
      );
    }

    function fontSelectField(key, label) {
      var cur = String(t[key] || "");
      var opts = FONT_FAMILY_OPTS.filter(function (o) {
        return o.value !== "custom";
      });
      // If current value is not in list, add it as selected option
      var known = opts.some(function (o) {
        return o.value === cur || (cur && o.value && o.value.indexOf(cur.split(",")[0].replace(/['"]/g, "").trim()) >= 0);
      });
      var html =
        '<div class="ab-field ab-field--font-global"><label>' +
        esc(label) +
        "</label>" +
        '<select data-g-field="typography.' +
        esc(key) +
        '" class="ab-g-font-select">';
      opts.forEach(function (o) {
        var sel =
          o.value === cur ||
          (cur &&
            o.value &&
            o.label &&
            cur.toLowerCase().indexOf(String(o.label).replace(/^Google · /i, "").toLowerCase()) >= 0 &&
            o.value !== "")
            ? " selected"
            : "";
        // Prefer exact match
        if (o.value === cur) sel = " selected";
        html +=
          '<option value="' +
          esc(o.value) +
          '"' +
          sel +
          ">" +
          esc(o.label) +
          "</option>";
      });
      if (cur && !opts.some(function (o) { return o.value === cur; })) {
        html +=
          '<option value="' +
          esc(cur) +
          '" selected>' +
          esc(cur) +
          " (atual)</option>";
      }
      html += "</select>";
      html +=
        '<button type="button" class="ab-btn ab-btn--primary ab-g-gf-open" data-g-font-key="' +
        esc(key) +
        '" style="width:100%;margin-top:8px">🔤 Biblioteca Google Fonts</button>';
      html +=
        '<span class="ab-field-hint">Lista rápida ou popup com preview das Google Fonts.</span></div>';
      return html;
    }

    function sizeSelectField(key, label, defaults) {
      var cur = String(t[key] || "");
      var opts = defaults.slice();
      var html =
        '<div class="ab-field"><label>' +
        esc(label) +
        '</label><div class="ab-g-size-row">' +
        '<select data-g-field="typography.' +
        esc(key) +
        '" class="ab-g-size-select">';
      var matched = false;
      opts.forEach(function (o) {
        var sel = o.value === cur ? " selected" : "";
        if (sel) matched = true;
        html +=
          '<option value="' +
          esc(o.value) +
          '"' +
          sel +
          ">" +
          esc(o.label) +
          "</option>";
      });
      if (cur && !matched) {
        html +=
          '<option value="' +
          esc(cur) +
          '" selected>' +
          esc(cur) +
          "</option>";
      }
      html +=
        '</select><input type="number" min="0.5" max="8" step="0.05" class="ab-g-size-num" data-g-size-num="' +
        esc(key) +
        '" value="' +
        esc(String(parseFloat(cur) || "")) +
        '" title="Valor numérico (rem)" placeholder="rem"/>' +
        '<span class="ab-g-size-unit">rem</span></div></div>';
      return html;
    }

    var SIZE_H_OPTS = [
      { value: "1.5rem", label: "1.5 rem" },
      { value: "1.75rem", label: "1.75 rem" },
      { value: "2rem", label: "2 rem" },
      { value: "2.25rem", label: "2.25 rem" },
      { value: "2.5rem", label: "2.5 rem" },
      { value: "2.75rem", label: "2.75 rem" },
      { value: "3rem", label: "3 rem" },
      { value: "3.5rem", label: "3.5 rem" },
      { value: "4rem", label: "4 rem" },
      { value: "48px", label: "48 px" },
      { value: "40px", label: "40 px" },
      { value: "32px", label: "32 px" },
    ];
    var SIZE_BODY_OPTS = [
      { value: "0.875rem", label: "0.875 rem (14px)" },
      { value: "1rem", label: "1 rem (16px)" },
      { value: "1.05rem", label: "1.05 rem" },
      { value: "1.125rem", label: "1.125 rem" },
      { value: "1.25rem", label: "1.25 rem" },
      { value: "14px", label: "14 px" },
      { value: "16px", label: "16 px" },
      { value: "18px", label: "18 px" },
    ];
    var RADIUS_OPTS = [
      { value: "0", label: "0 (quadrado)" },
      { value: "4px", label: "4 px" },
      { value: "6px", label: "6 px" },
      { value: "8px", label: "8 px" },
      { value: "10px", label: "10 px" },
      { value: "12px", label: "12 px" },
      { value: "16px", label: "16 px" },
      { value: "999px", label: "Pílula" },
    ];

    var html = tabsHtml || "";
    html +=
      '<p class="ab-field-hint" style="margin-bottom:12px">Tokens do <strong>site inteiro</strong>. A página em edição <strong>atualiza ao vivo</strong>. Salve para gravar no site.</p>';

    html += '<div class="ab-settings-section">';
    html += '<div class="ab-settings-section-title">' + svgIcon("layout") + " Layout</div>";
    html += textField("content_max", "Largura máx. do conteúdo", "layout", "1200px");
    html += textField("content_width", "Largura relativa", "layout", "90%");
    html += textField("column_gap", "Espaço entre colunas", "layout", "24px");
    html +=
      '<span class="ab-field-hint">Usado por seções boxed e como referência de gap.</span>';
    html += "</div>";

    html += '<div class="ab-settings-section">';
    html += '<div class="ab-settings-section-title">' + svgIcon("color") + " Cores</div>";
    html += colorField("primary", "Primary (botões)");
    html += colorField("secondary", "Secondary");
    html += colorField("text", "Texto");
    html += colorField("text_muted", "Texto suave");
    html += colorField("bg", "Fundo");
    html += colorField("border", "Borda");
    html += colorField("btn_text", "Texto do botão");
    html +=
      '<span class="ab-field-hint">Altere e veja botões, textos e fundo do canvas mudarem na hora.</span>';
    html += "</div>";

    html += '<div class="ab-settings-section">';
    html += '<div class="ab-settings-section-title">' + svgIcon("text") + " Tipografia</div>";
    html += fontSelectField("font_body", "Fonte do corpo");
    html += fontSelectField("font_heading", "Fonte dos títulos");
    html += sizeSelectField("h1", "Tamanho H1", SIZE_H_OPTS);
    html += sizeSelectField("h2", "Tamanho H2", SIZE_H_OPTS);
    html += sizeSelectField("h3", "Tamanho H3", SIZE_H_OPTS);
    html += sizeSelectField("body", "Tamanho body", SIZE_BODY_OPTS);
    html += "</div>";

    html += '<div class="ab-settings-section">';
    html += '<div class="ab-settings-section-title">' + svgIcon("layout") + " Raio</div>";
    var radCur = String(r.md || "8px");
    html +=
      '<div class="ab-field"><label>Border radius (botões)</label><select data-g-field="radius.md">';
    RADIUS_OPTS.forEach(function (o) {
      html +=
        '<option value="' +
        esc(o.value) +
        '"' +
        (o.value === radCur ? " selected" : "") +
        ">" +
        esc(o.label) +
        "</option>";
    });
    if (radCur && !RADIUS_OPTS.some(function (o) { return o.value === radCur; })) {
      html +=
        '<option value="' + esc(radCur) + '" selected>' + esc(radCur) + "</option>";
    }
    html += "</select></div>";
    html += "</div>";

    // Header / Footer (mínimo)
    var ch = g.chrome || {};
    html += '<div class="ab-settings-section">';
    html += '<div class="ab-settings-section-title">' + svgIcon("header") + " Header & Footer</div>";
    html +=
      '<label class="ab-settings-toggle' +
      (ch.header_sticky === "1" ? " is-on" : "") +
      '"><span class="ab-check-ui" aria-hidden="true"></span>' +
      '<input type="checkbox" data-g-chrome="header_sticky"' +
      (ch.header_sticky === "1" ? " checked" : "") +
      "/>" +
      '<span class="ab-settings-toggle-label">Header sticky (fix)</span></label>';
    html +=
      '<div class="ab-field"><label>Fundo do sticky</label><div class="ab-color-row">' +
      '<input type="color" class="ab-color-swatch" data-g-chrome-color="header_sticky_bg" value="' +
      esc(/^#[0-9a-fA-F]{6}$/.test(ch.header_sticky_bg || "") ? ch.header_sticky_bg : "#ffffff") +
      '"/>' +
      '<input type="text" data-g-chrome-text="header_sticky_bg" value="' +
      esc(ch.header_sticky_bg || "#ffffff") +
      '"/></div></div>';
    html +=
      '<label class="ab-settings-toggle' +
      (ch.header_transparent === "1" ? " is-on" : "") +
      '"><span class="ab-check-ui" aria-hidden="true"></span>' +
      '<input type="checkbox" data-g-chrome="header_transparent"' +
      (ch.header_transparent === "1" ? " checked" : "") +
      "/>" +
      '<span class="ab-settings-toggle-label">Header transparente (sobre o hero)</span></label>';
    html +=
      '<div class="ab-field"><label>Footer — fundo (opcional)</label><input type="text" data-g-chrome-text="footer_bg" value="' +
      esc(ch.footer_bg || "") +
      '" placeholder="#0f172a ou vazio"/></div>';
    html +=
      '<div class="ab-field"><label>Footer — cor do texto</label><input type="text" data-g-chrome-text="footer_text" value="' +
      esc(ch.footer_text || "") +
      '" placeholder="#ffffff ou vazio"/></div>';
    html +=
      '<div class="ab-settings-subsection-title" style="margin:14px 0 8px;font-size:12px;font-weight:600;color:#64748b">Crédito “Powered by Auranet Builder”</div>';
    html +=
      '<div class="ab-field"><label>Cor do texto do crédito</label><div class="ab-color-row">' +
      '<input type="color" class="ab-color-swatch" data-g-chrome-color="credit_text_color" value="' +
      esc(/^#[0-9a-fA-F]{6}$/.test(ch.credit_text_color || "") ? ch.credit_text_color : "#4f46e5") +
      '"/>' +
      '<input type="text" data-g-chrome-text="credit_text_color" value="' +
      esc(ch.credit_text_color || "#4f46e5") +
      '" placeholder="#4f46e5"/></div></div>';
    html +=
      '<div class="ab-field"><label>Fundo do crédito</label><div class="ab-color-row">' +
      '<input type="color" class="ab-color-swatch" data-g-chrome-color="credit_bg_color" value="' +
      esc(/^#[0-9a-fA-F]{6}$/.test(ch.credit_bg_color || "") ? ch.credit_bg_color : "#ffffff") +
      '"/>' +
      '<input type="text" data-g-chrome-text="credit_bg_color" value="' +
      esc(ch.credit_bg_color || "#ffffff") +
      '" placeholder="#ffffff ou transparent"/></div></div>';
    html +=
      '<div class="ab-field-hint" style="margin:4px 0 8px;padding:10px 12px;border-radius:6px;text-align:center;font-size:12px;font-weight:600;background:' +
      esc(ch.credit_bg_color || "#ffffff") +
      ";color:" +
      esc(ch.credit_text_color || "#4f46e5") +
      '">Powered by Auranet Builder</div>';
    html +=
      '<span class="ab-field-hint">Sticky/transparente aqui afetam o header do tema quando o Builder não o substitui. Preferível editar no Theme Builder.</span>';
    html +=
      '<div class="ab-chrome-edit-row" style="margin-top:10px">' +
      '<button type="button" class="ab-btn ab-btn--primary" id="ab-g-edit-header">✎ Editar Header</button>' +
      '<button type="button" class="ab-btn" id="ab-g-edit-footer">✎ Editar Footer</button>' +
      "</div>";
    html +=
      '<p class="ab-field-hint" style="margin-top:6px"><a href="admin.php?page=aura-builder-theme">Painel Header &amp; Footer</a></p>';
    html += "</div>";

    html +=
      '<p class="ab-field-hint" style="margin-top:4px">CSS livre: use <strong>Aparência → Personalizar → CSS adicional</strong> do WordPress (o builder não injeta CSS arbitrário).</p>';

    html +=
      '<button type="button" class="ab-btn ab-btn--primary" id="ab-globals-save" style="width:100%;margin-top:8px">' +
      (state.globalsDirty ? "Salvar globais *" : "Salvar globais") +
      "</button>";
    html +=
      '<p class="ab-field-hint" style="margin-top:8px">Alterações de globais valem para todo o site após salvar.</p>';

    box.innerHTML = html;
    bindSettingsTabs(box);

    var gEditH = document.getElementById("ab-g-edit-header");
    var gEditF = document.getElementById("ab-g-edit-footer");
    if (gEditH)
      gEditH.addEventListener("click", function () {
        openThemePartEditor("header");
      });
    if (gEditF)
      gEditF.addEventListener("click", function () {
        openThemePartEditor("footer");
      });

    function markG() {
      state.globalsDirty = true;
      applyGlobalsCss("");
      var b = document.getElementById("ab-globals-save");
      if (b) b.textContent = "Salvar globais *";
    }

    box.querySelectorAll("[data-g-color]").forEach(function (inp) {
      inp.addEventListener("input", function () {
        var key = inp.getAttribute("data-g-color");
        ensureGlobals();
        state.globals.colors[key] = inp.value;
        var text = box.querySelector('[data-g-color-text="' + key + '"]');
        if (text) text.value = inp.value;
        markG();
      });
    });
    box.querySelectorAll("[data-g-color-text]").forEach(function (inp) {
      var applyHex = function () {
        var key = inp.getAttribute("data-g-color-text");
        ensureGlobals();
        state.globals.colors[key] = String(inp.value || "").trim();
        var sw = box.querySelector('[data-g-color="' + key + '"]');
        if (sw && /^#[0-9a-fA-F]{6}$/.test(inp.value)) sw.value = inp.value;
        markG();
      };
      inp.addEventListener("input", function () {
        if (/^#[0-9a-fA-F]{6}$/.test(String(inp.value || "").trim())) applyHex();
      });
      inp.addEventListener("change", applyHex);
    });
    box.querySelectorAll("[data-g-field]").forEach(function (inp) {
      var apply = function () {
        var path = inp.getAttribute("data-g-field") || "";
        ensureGlobals();
        if (path === "custom_css") {
          // Ignored — arbitrary CSS not stored (use WP Customizer).
          return;
        } else {
          var parts = path.split(".");
          if (parts.length === 2 && state.globals[parts[0]]) {
            state.globals[parts[0]][parts[1]] = String(inp.value || "").trim();
            if (parts[0] === "typography" && (parts[1] === "font_body" || parts[1] === "font_heading")) {
              ensureGoogleFont(state.globals.typography[parts[1]]);
            }
          }
        }
        // Sync numeric size input when select changes
        if (inp.classList.contains("ab-g-size-select")) {
          var sk = path.split(".")[1];
          var num = box.querySelector('[data-g-size-num="' + sk + '"]');
          if (num) {
            var pf = parseFloat(inp.value);
            num.value = isNaN(pf) ? "" : String(pf);
          }
        }
        markG();
      };
      inp.addEventListener("change", apply);
      inp.addEventListener("input", function () {
        // Live for text layout fields
        if (inp.tagName === "SELECT") return;
        if (inp.tagName === "TEXTAREA") return;
        apply();
      });
      if (inp.tagName === "TEXTAREA") inp.addEventListener("blur", apply);
    });

    // Numeric rem input next to size selects
    box.querySelectorAll("[data-g-size-num]").forEach(function (inp) {
      inp.addEventListener("input", function () {
        var key = inp.getAttribute("data-g-size-num");
        var n = parseFloat(inp.value);
        if (!key || isNaN(n) || n <= 0) return;
        ensureGlobals();
        var unit = "rem";
        var sel = box.querySelector('select[data-g-field="typography.' + key + '"]');
        var cur = sel ? String(sel.value || "") : "";
        if (/px$/i.test(cur)) unit = "px";
        var val = (Math.round(n * 100) / 100) + unit;
        state.globals.typography[key] = val;
        if (sel) {
          // Ensure option exists
          var found = false;
          Array.prototype.forEach.call(sel.options, function (opt) {
            if (opt.value === val) found = true;
          });
          if (!found) {
            var o = document.createElement("option");
            o.value = val;
            o.textContent = val;
            sel.appendChild(o);
          }
          sel.value = val;
        }
        markG();
      });
    });

    // Google Fonts popup from global settings
    box.querySelectorAll(".ab-g-gf-open").forEach(function (btn) {
      btn.addEventListener("click", function () {
        var key = btn.getAttribute("data-g-font-key") || "font_body";
        ensureGlobals();
        var cur = state.globals.typography[key] || "";
        var curName = String(cur)
          .split(",")[0]
          .replace(/["']/g, "")
          .trim();
        openGoogleFontsPicker(curName, function (cssFamily) {
          if (!cssFamily) return;
          ensureGlobals();
          state.globals.typography[key] = cssFamily;
          ensureGoogleFont(cssFamily);
          markG();
          paintSiteSettings();
        });
      });
    });

    box.querySelectorAll("[data-g-chrome]").forEach(function (inp) {
      inp.addEventListener("change", function () {
        ensureGlobals();
        state.globals.chrome = state.globals.chrome || {};
        state.globals.chrome[inp.getAttribute("data-g-chrome")] = inp.checked ? "1" : "0";
        markG();
        paintSiteSettings();
      });
    });
    box.querySelectorAll("[data-g-chrome-color]").forEach(function (inp) {
      inp.addEventListener("input", function () {
        ensureGlobals();
        state.globals.chrome = state.globals.chrome || {};
        var k = inp.getAttribute("data-g-chrome-color");
        state.globals.chrome[k] = inp.value;
        var t = box.querySelector('[data-g-chrome-text="' + k + '"]');
        if (t) t.value = inp.value;
        markG();
      });
    });
    box.querySelectorAll("[data-g-chrome-text]").forEach(function (inp) {
      inp.addEventListener("change", function () {
        ensureGlobals();
        state.globals.chrome = state.globals.chrome || {};
        state.globals.chrome[inp.getAttribute("data-g-chrome-text")] = String(inp.value || "").trim();
        markG();
      });
    });

    var saveBtn = document.getElementById("ab-globals-save");
    if (saveBtn)
      saveBtn.addEventListener("click", function () {
        saveBtn.disabled = true;
        saveBtn.textContent = "Salvando…";
        saveGlobals()
          .then(function () {
            saveBtn.disabled = false;
            paintSiteSettings();
          })
          .catch(function (err) {
            saveBtn.disabled = false;
            saveBtn.textContent = "Salvar globais *";
            setStatus(String(err.message || err), "is-err");
          });
      });
  }

  function syncSidebarsVisibility() {
    var body = document.querySelector(".ab-body");
    var btn = document.getElementById("ab-toggle-sidebars");
    if (body) {
      body.classList.toggle("ab-body--focus", !!state.hideSidebars);
      // Header/footer editor: full-bleed canvas (no side gutters).
      body.classList.toggle("ab-body--theme-part", !!isThemePart);
      body.classList.toggle("ab-body--left-collapsed", !!state.leftCollapsed && !state.hideSidebars);
      body.classList.toggle("ab-body--right-collapsed", !!state.rightCollapsed && !state.hideSidebars);
    }
    if (btn) {
      btn.classList.toggle("is-active", !!state.hideSidebars);
      btn.title = state.hideSidebars
        ? "Mostrar painéis laterais"
        : "Ocultar painéis (ver página inteira)";
    }
    var leftRail = document.getElementById("ab-rail-left");
    var rightRail = document.getElementById("ab-rail-right");
    if (leftRail) {
      leftRail.setAttribute("aria-pressed", state.leftCollapsed ? "true" : "false");
      leftRail.title = state.leftCollapsed ? "Mostrar painel esquerdo" : "Minimizar painel esquerdo";
      leftRail.textContent = state.leftCollapsed ? "›" : "‹";
    }
    if (rightRail) {
      rightRail.setAttribute("aria-pressed", state.rightCollapsed ? "true" : "false");
      rightRail.title = state.rightCollapsed ? "Mostrar painel direito" : "Minimizar painel direito";
      rightRail.textContent = state.rightCollapsed ? "‹" : "›";
    }
    // Re-apply canvas width so focus mode expands to full page width.
    applyCanvasView();
  }

  function toggleSidebar(side) {
    if (side === "left") state.leftCollapsed = !state.leftCollapsed;
    else if (side === "right") state.rightCollapsed = !state.rightCollapsed;
    // Leaving focus mode when expanding a single panel.
    if (state.hideSidebars && ((side === "left" && !state.leftCollapsed) || (side === "right" && !state.rightCollapsed))) {
      state.hideSidebars = false;
    }
    try {
      localStorage.setItem(
        "ab_sidebar_collapse",
        JSON.stringify({ left: !!state.leftCollapsed, right: !!state.rightCollapsed })
      );
    } catch (e) {}
    syncSidebarsVisibility();
  }

  function restoreSidebarCollapse() {
    try {
      var raw = localStorage.getItem("ab_sidebar_collapse");
      if (!raw) return;
      var o = JSON.parse(raw);
      if (o && typeof o === "object") {
        state.leftCollapsed = !!o.left;
        state.rightCollapsed = !!o.right;
      }
    } catch (e) {}
  }

  function buildShell(root) {
    root.innerHTML =
      '<div class="ab-top">' +
      '<div class="ab-top-left">' +
      '<a class="ab-icon-btn ab-back-btn" id="ab-back" href="#" title="Voltar ao WordPress">' +
      svgIcon("back") +
      "</a>" +
      (CFG.pluginLogo
        ? '<img class="ab-brand ab-brand--img" src="' +
          esc(CFG.pluginLogo) +
          '" alt="Aura Builder" title="Aura Builder" width="32" height="32"/>'
        : '<span class="ab-brand" title="Aura Builder">AB</span>') +
      '<div class="ab-rail-tabs" role="tablist">' +
      '<button type="button" class="ab-icon-btn is-active" data-left-panel="widgets" title="Elementos / Widgets">' +
      svgIcon("widgets") +
      "</button>" +
      '<button type="button" class="ab-icon-btn" data-left-panel="layers" title="Camadas">' +
      svgIcon("layers") +
      "</button>" +
      '<button type="button" class="ab-icon-btn" id="ab-library" title="Modelos e biblioteca">' +
      svgIcon("library") +
      "</button>" +
      '<button type="button" class="ab-icon-btn" data-left-panel="settings" title="Configurações do site">' +
      svgIcon("settings") +
      "</button>" +
      "</div></div>" +
      '<div class="ab-title" id="ab-title"></div>' +
      '<div class="ab-status" id="ab-status" title="Status">…</div>' +
      '<div class="ab-bp-group" title="Visualizar por dispositivo">' +
      '<button type="button" class="ab-bp-btn is-active" data-bp="desktop" title="Desktop">' +
      svgIcon("desktop") +
      "</button>" +
      '<button type="button" class="ab-bp-btn" data-bp="tablet" title="Tablet">' +
      svgIcon("tablet") +
      "</button>" +
      '<button type="button" class="ab-bp-btn" data-bp="mobile" title="Mobile">' +
      svgIcon("mobile") +
      "</button>" +
      '<span class="ab-bp-badge" id="ab-bp-badge">Desktop</span>' +
      "</div>" +
      '<button type="button" class="ab-icon-btn" id="ab-toggle-sidebars" title="Ocultar painéis (ver página inteira)">' +
      svgIcon("panels") +
      "</button>" +
      '<div class="ab-top-actions">' +
      '<button type="button" class="ab-icon-btn" id="ab-save-tpl" title="Salvar como template">' +
      svgIcon("plus") +
      "</button>" +
      '<button type="button" class="ab-icon-btn" id="ab-undo" title="Desfazer (Ctrl+Z)">' +
      svgIcon("undo") +
      "</button>" +
      '<button type="button" class="ab-icon-btn" id="ab-redo" title="Refazer (Ctrl+Y)">' +
      svgIcon("redo") +
      "</button>" +
      '<a class="ab-icon-btn" id="ab-view" href="#" target="_blank" rel="noopener" title="Ver página">' +
      svgIcon("view") +
      "</a>" +
      '<button type="button" class="ab-icon-btn ab-icon-btn--primary" id="ab-save" title="Salvar (Ctrl+S)">' +
      svgIcon("save") +
      "</button>" +
      "</div></div>" +
      '<div class="ab-body">' +
      '<aside class="ab-panel ab-panel--left">' +
      '<div class="ab-panel-hd" id="ab-left-hd">Elementos</div>' +
      '<div class="ab-panel-bd">' +
      '<div data-left-view="widgets" id="ab-view-widgets"><div id="ab-cat"></div></div>' +
      '<div data-left-view="layers" id="ab-view-layers" hidden>' +
      '<p class="ab-field-hint">Arraste para reordenar.</p>' +
      '<ul class="ab-layers" id="ab-layers"></ul></div>' +
      '<div data-left-view="settings" id="ab-view-settings" hidden>' +
      '<div id="ab-site-settings"></div></div>' +
      "</div></aside>" +
      '<button type="button" class="ab-panel-rail ab-panel-rail--left" id="ab-rail-left" title="Minimizar painel esquerdo" aria-label="Minimizar painel esquerdo">‹</button>' +
      '<main class="ab-canvas-wrap" id="ab-canvas-wrap"><div class="ab-canvas" id="ab-canvas"></div></main>' +
      '<button type="button" class="ab-panel-rail ab-panel-rail--right" id="ab-rail-right" title="Minimizar painel direito" aria-label="Minimizar painel direito">›</button>' +
      '<aside class="ab-panel ab-panel--right">' +
      '<div class="ab-panel-hd" title="Propriedades">Propriedades</div>' +
      '<div class="ab-panel-bd" id="ab-props"></div></aside>' +
      "</div>";

    paintWidgetCatalog();
    setLeftPanel("widgets");
    restoreSidebarCollapse();
    syncSidebarsVisibility();

    document.querySelectorAll("[data-left-panel]").forEach(function (btn) {
      btn.addEventListener("click", function () {
        // Opening a panel exits focus mode so sidebars return.
        if (state.hideSidebars) {
          state.hideSidebars = false;
          syncSidebarsVisibility();
        }
        if (state.leftCollapsed) {
          state.leftCollapsed = false;
          syncSidebarsVisibility();
        }
        setLeftPanel(btn.getAttribute("data-left-panel"));
      });
    });

    var sbToggle = document.getElementById("ab-toggle-sidebars");
    if (sbToggle) {
      sbToggle.addEventListener("click", function () {
        state.hideSidebars = !state.hideSidebars;
        // Focus mode overrides individual collapse; restoring focus off keeps collapse flags.
        syncSidebarsVisibility();
      });
    }
    var railL = document.getElementById("ab-rail-left");
    var railR = document.getElementById("ab-rail-right");
    if (railL)
      railL.addEventListener("click", function (e) {
        e.preventDefault();
        e.stopPropagation();
        toggleSidebar("left");
      });
    if (railR)
      railR.addEventListener("click", function (e) {
        e.preventDefault();
        e.stopPropagation();
        toggleSidebar("right");
      });

    document.getElementById("ab-save").addEventListener("click", save);
    document.getElementById("ab-undo").addEventListener("click", undo);
    document.getElementById("ab-redo").addEventListener("click", redo);
    document.getElementById("ab-back").href = backUrl || "admin.php?page=aura-builder-pages";
    document.getElementById("ab-view").href = state.viewUrl || "#";
    var libBtn = document.getElementById("ab-library");
    var saveTplBtn = document.getElementById("ab-save-tpl");
    if (libBtn)
      libBtn.addEventListener("click", function () {
        // Um único atalho: páginas (aplicar layout) no editor de página; header/footer no Theme Builder.
        if (isThemePart) {
          openTemplateLibrary({ tab: editorMode });
        } else {
          openTemplateLibrary({ tab: "page", applyPage: true });
        }
      });
    if (saveTplBtn) saveTplBtn.addEventListener("click", saveAsTemplate);

    document.querySelectorAll("[data-bp]").forEach(function (btn) {
      btn.addEventListener("click", function () {
        // Top bar = preview viewport only (does not change which styles you edit).
        setViewBreakpoint(btn.getAttribute("data-bp"));
      });
    });

    bindCanvas(document.getElementById("ab-canvas-wrap"));
    document.addEventListener("mousemove", onMouseMove);
    document.addEventListener("mouseup", onMouseUp);

    document.getElementById("ab-layers").addEventListener("click", function (e) {
      var li = e.target.closest("li[data-node-id]");
      if (!li) return;
      var id = li.getAttribute("data-node-id");
      var multi = !!(e.ctrlKey || e.metaKey);
      if (multi) {
        selectNode(id, { multi: true, skipLayers: true });
        return;
      }
      var n = getNode(id);
      if (n && (n.type === "heading" || n.type === "text" || n.type === "button")) {
        selectNode(id, { focusEdit: true });
      } else {
        selectNode(id, { focusEdit: false });
      }
    });
  }

  /** Merge styles from local doc into server doc (never lose colors on save). */
  function mergeStylesFromLocal(serverDoc, localDoc) {
    if (!serverDoc || !localDoc || !serverDoc.nodes || !localDoc.nodes) return serverDoc;
    Object.keys(localDoc.nodes).forEach(function (id) {
      var loc = localDoc.nodes[id];
      var srv = serverDoc.nodes[id];
      if (!loc || !srv || !loc.styles) return;
      if (!srv.styles) srv.styles = { desktop: {}, tablet: {}, mobile: {} };
      ["desktop", "tablet", "mobile"].forEach(function (bp) {
        var ls = loc.styles[bp] || {};
        if (!srv.styles[bp] || typeof srv.styles[bp] !== "object" || Array.isArray(srv.styles[bp])) {
          srv.styles[bp] = {};
        }
        Object.keys(ls).forEach(function (k) {
          if (ls[k] !== undefined && ls[k] !== null && ls[k] !== "") {
            // Prefer local if server missing this key.
            if (srv.styles[bp][k] === undefined || srv.styles[bp][k] === null || srv.styles[bp][k] === "") {
              srv.styles[bp][k] = ls[k];
            }
          }
        });
      });
    });
    return serverDoc;
  }

  function save() {
    if (state.saving || !state.document) return;
    state.saving = true;
    setStatus("Salvando…", "");
    normalizeDocumentStyles(state.document);
    var snapshot = cloneDoc(state.document);

    // Renova nonce antes do POST (evita cookie/nonce stale em abas abertas há horas).
    refreshNonce()
      .catch(function () {
        /* se falhar, tenta salvar com o nonce atual; api() ainda retenta 1x */
        return null;
      })
      .then(function () {
        if (isThemePart) {
          // Single Post: always activate on save (creating the model means enabling it).
          var enabledFlag =
            themePartMeta.enabled === true ||
            themePartMeta.enabled === 1 ||
            themePartMeta.enabled === "1" ||
            themePartMeta.enabled === "true";
          if (editorMode === "single") {
            enabledFlag = true;
            themePartMeta.enabled = true;
            if (
              !themePartMeta.display ||
              themePartMeta.display === "all" ||
              themePartMeta.display === "home"
            ) {
              themePartMeta.display = "singular";
            }
            // Clear starter flag after first save
            if (state.document.metadata) {
              delete state.document.metadata.starter;
            }
          }
          var partBody = Object.assign({}, themePartMeta, {
            enabled: enabledFlag,
            document: state.document,
          });
          return api("/theme-parts/" + editorMode, {
            method: "POST",
            body: { part: partBody, document: state.document },
          }).then(function (res) {
            if (res.document) {
              state.document = mergeStylesFromLocal(res.document, snapshot);
              normalizeDocumentStyles(state.document);
            }
            if (res.part) {
              themePartMeta = Object.assign(themePartMeta, res.part);
              // Normalize enabled to real boolean after save
              themePartMeta.enabled =
                res.part.enabled === true ||
                res.part.enabled === 1 ||
                res.part.enabled === "1";
              delete themePartMeta.document;
            }
            if (res.nonce) {
              nonce = res.nonce;
              CFG.nonce = res.nonce;
            }
            state.dirty = false;
            state.imported = false;
            state.source = "saved";
            state.saving = false;
            setStatus(
              editorMode === "single"
                ? "Single Post ativo ✓ — abra um post no site para ver"
                : "Salvo ✓",
              "is-ok"
            );
            paint();
          });
        }

        return api("/documents/" + postId, {
          method: "POST",
          body: { document: state.document, enabled: true },
        }).then(function (res) {
          if (res.document) {
            state.document = mergeStylesFromLocal(res.document, snapshot);
            normalizeDocumentStyles(state.document);
          }
          if (res.nonce) {
            nonce = res.nonce;
            CFG.nonce = res.nonce;
          }
          state.dirty = false;
          state.imported = false;
          state.source = "saved";
          state.saving = false;
          setStatus("Salvo ✓", "is-ok");
          paint();
        });
      })
      .catch(function (err) {
        state.saving = false;
        setStatus("Erro: " + (err.message || err), "is-err");
      });
  }

  /**
   * One-shot: old defaults (root wide + section boxed) put the whole page
   * inside a ~1140px “container”. Upgrade to full width once per document.
   */
  function maybeUpgradeFullWidthLayout(doc) {
    if (!doc || !doc.nodes) return false;
    ensureDocSettings();
    if (doc.settings._ab_layout_v2) return false;
    doc.settings._ab_layout_v2 = 1;
    var changed = false;
    var root = doc.nodes[doc.rootId];
    if (root) {
      root.settings = root.settings || {};
      if (root.settings.content_width !== "full") {
        root.settings.content_width = "full";
        changed = true;
      }
      if (!root.settings.max_width) root.settings.max_width = "1200px";
    }
    Object.keys(doc.nodes).forEach(function (id) {
      var n = doc.nodes[id];
      if (!n || n.type !== "section") return;
      n.settings = n.settings || {};
      if (!n.settings.layout || n.settings.layout === "boxed") {
        n.settings.layout = "full";
        changed = true;
      }
    });
    return changed;
  }

  /* ── Template library / aplicar layout na página ── */

  /**
   * @param {{tab?:string, applyPage?:boolean}} opts
   */
  function openTemplateLibrary(opts) {
    opts = opts || {};
    var applyPageMode = !!opts.applyPage || (!isThemePart && opts.tab === "page");
    var existing = document.getElementById("ab-tpl-lib-modal");
    if (existing) existing.remove();
    var modal = document.createElement("div");
    modal.id = "ab-tpl-lib-modal";
    modal.className = "ab-tpl-lib-modal";
    var title = applyPageMode
      ? "Escolher modelo para esta página"
      : "Biblioteca de templates";
    var subtitle = applyPageMode
      ? "Clique no modelo para ver a confirmação e aplicar. Título e URL do WordPress não mudam."
      : "Clique em um card para aplicar ou inserir. Título e URL da página não mudam.";
    modal.innerHTML =
      '<div class="ab-tpl-lib-panel ab-tpl-lib-panel--wide">' +
      '<div class="ab-tpl-lib-hd">' +
      "<div><strong>" +
      esc(title) +
      "</strong>" +
      '<p class="ab-tpl-lib-sub">' +
      esc(subtitle) +
      "</p></div>" +
      '<button type="button" class="ab-icon-btn" id="ab-tpl-lib-close" title="Fechar">' +
      svgIcon("close") +
      "</button></div>" +
      '<div class="ab-tpl-lib-toolbar">' +
      '<input type="search" id="ab-tpl-lib-search" class="ab-tpl-lib-search" placeholder="Buscar modelo…"/>' +
      "</div>" +
      '<div class="ab-tpl-lib-tabs" id="ab-tpl-lib-tabs">' +
      '<button type="button" class="ab-tpl-lib-tab' +
      (applyPageMode || opts.tab === "page" ? " is-on" : "") +
      '" data-tpl-type="page">Páginas / Layouts</button>' +
      '<button type="button" class="ab-tpl-lib-tab' +
      (!applyPageMode && opts.tab !== "page" && opts.tab !== "header" && opts.tab !== "footer" && opts.tab !== "user" ? " is-on" : "") +
      '" data-tpl-type="component">Seções</button>' +
      '<button type="button" class="ab-tpl-lib-tab' +
      (opts.tab === "header" ? " is-on" : "") +
      '" data-tpl-type="header">Headers</button>' +
      '<button type="button" class="ab-tpl-lib-tab' +
      (opts.tab === "footer" ? " is-on" : "") +
      '" data-tpl-type="footer">Footers</button>' +
      '<button type="button" class="ab-tpl-lib-tab' +
      (opts.tab === "user" ? " is-on" : "") +
      '" data-tpl-type="user">Meus</button>' +
      "</div>" +
      '<div class="ab-tpl-lib-grid" id="ab-tpl-lib-list"><p class="ab-field-hint">Carregando…</p></div>' +
      '<p class="ab-field-hint ab-tpl-lib-foot">Admin: <a href="admin.php?page=aura-builder-templates" target="_blank" rel="noopener">Templates &amp; Kits</a></p>' +
      "</div>";
    document.body.appendChild(modal);
    modal.addEventListener("click", function (e) {
      if (e.target === modal) modal.remove();
    });
    document.getElementById("ab-tpl-lib-close").addEventListener("click", function () {
      modal.remove();
    });

    var currentType =
      opts.tab ||
      (applyPageMode ? "page" : isThemePart ? editorMode : "component");
    if (currentType !== "page" && currentType !== "component" && currentType !== "header" && currentType !== "footer" && currentType !== "user") {
      currentType = "page";
    }
    // Sync tab active state
    modal.querySelectorAll("[data-tpl-type]").forEach(function (tab) {
      tab.classList.toggle("is-on", tab.getAttribute("data-tpl-type") === currentType);
    });

    var searchQ = "";
    var searchTimer = null;

    function loadList() {
      var list = document.getElementById("ab-tpl-lib-list");
      if (!list) return;
      list.innerHTML = '<p class="ab-field-hint" style="grid-column:1/-1">Carregando…</p>';
      var qs =
        currentType === "user"
          ? "?source=user"
          : "?type=" + encodeURIComponent(currentType);
      if (searchQ) qs += (qs.indexOf("?") === 0 ? "&" : "?") + "q=" + encodeURIComponent(searchQ);
      // fix double ?
      qs = qs.replace("?&", "?").replace(/\?q=/, function (m, offset, str) {
        return str.indexOf("?type=") >= 0 || str.indexOf("?source=") >= 0 ? "&q=" : m;
      });
      // simpler rebuild
      var params = [];
      if (currentType === "user") params.push("source=user");
      else params.push("type=" + encodeURIComponent(currentType));
      if (searchQ) params.push("q=" + encodeURIComponent(searchQ));
      api("/templates?" + params.join("&"))
        .then(function (res) {
          var items = res.templates || [];
          // Page tab: also show user page templates mixed from source filter
          if (currentType === "page") {
            // include user pages if any were not returned — API already can list type=page with both sources if no source filter
          }
          if (!items.length) {
            list.innerHTML =
              '<p class="ab-field-hint" style="grid-column:1/-1">Nenhum modelo nesta categoria. Salve um com “+” ou importe um kit em Templates.</p>';
            return;
          }
          list.className = "ab-tpl-lib-grid";

          function tplCardHtml(t) {
            var type = t.type || currentType;
            var thumb = t.thumbnail || "";
            var letter = esc((t.name || "?").charAt(0).toUpperCase());
            var actionHint =
              type === "page" || applyPageMode
                ? "Clique para aplicar nesta página"
                : type === "header" || type === "footer"
                  ? "Clique para aplicar"
                  : "Clique para inserir seção";
            var thumbHtml = thumb
              ? '<img class="ab-tpl-lib-card__img" src="' +
                esc(thumb) +
                '" alt="' +
                esc(t.name || "") +
                '" loading="lazy" onerror="this.style.display=\'none\';var p=this.nextElementSibling;if(p)p.hidden=false;"/>' +
                '<div class="ab-tpl-lib-card__ph" hidden>' +
                letter +
                "</div>"
              : '<div class="ab-tpl-lib-card__ph">' + letter + "</div>";
            var catKey = String(t.category || "general").toLowerCase();
            var catLabel = TPL_CATEGORY_LABELS[catKey] || t.category || "Geral";
            return (
              '<article class="ab-tpl-lib-card" data-tpl-id="' +
              esc(t.id) +
              '" data-tpl-type="' +
              esc(type) +
              '" title="' +
              esc(actionHint) +
              '">' +
              '<div class="ab-tpl-lib-card__thumb">' +
              thumbHtml +
              '<span class="ab-tpl-lib-card__overlay">' +
              esc(actionHint) +
              "</span>" +
              "</div>" +
              '<div class="ab-tpl-lib-card__body">' +
              "<strong>" +
              esc(t.name || t.id) +
              "</strong>" +
              '<span class="ab-tpl-lib-card__meta">' +
              esc(catLabel) +
              " · " +
              esc(t.source || "core") +
              "</span>" +
              "</div></article>"
            );
          }

          // Agrupa por categoria com títulos: Escolar, Empresa, Blog…
          var byCat = {};
          items.forEach(function (t) {
            var key = String(t.category || "general").toLowerCase() || "general";
            if (!byCat[key]) byCat[key] = [];
            byCat[key].push(t);
          });
          var orderedKeys = [];
          TPL_CATEGORY_ORDER.forEach(function (k) {
            if (byCat[k] && byCat[k].length) orderedKeys.push(k);
          });
          Object.keys(byCat).forEach(function (k) {
            if (orderedKeys.indexOf(k) < 0) orderedKeys.push(k);
          });

          var htmlParts = [];
          orderedKeys.forEach(function (catKey) {
            var label = TPL_CATEGORY_LABELS[catKey] || catKey;
            htmlParts.push(
              '<div class="ab-tpl-cat-title" role="heading" aria-level="3">' +
                esc(label) +
                "</div>"
            );
            byCat[catKey].forEach(function (t) {
              htmlParts.push(tplCardHtml(t));
            });
          });
          list.innerHTML = htmlParts.join("");

          // Clique no card → confirmação e aplica
          list.querySelectorAll(".ab-tpl-lib-card").forEach(function (card) {
            card.addEventListener("click", function () {
              var id = card.getAttribute("data-tpl-id");
              var forcePage =
                applyPageMode || card.getAttribute("data-tpl-type") === "page";
              insertTemplateById(id, modal, { forcePage: forcePage });
            });
          });
        })
        .catch(function (err) {
          list.innerHTML =
            '<p class="ab-field-hint" style="grid-column:1/-1;color:#fca5a5">' +
            esc(err.message) +
            "</p>";
        });
    }

    modal.querySelectorAll("[data-tpl-type]").forEach(function (tab) {
      tab.addEventListener("click", function () {
        modal.querySelectorAll("[data-tpl-type]").forEach(function (t) {
          t.classList.remove("is-on");
        });
        tab.classList.add("is-on");
        currentType = tab.getAttribute("data-tpl-type") || "page";
        applyPageMode = currentType === "page" && !isThemePart;
        var hd = modal.querySelector(".ab-tpl-lib-hd strong");
        var sub = modal.querySelector(".ab-tpl-lib-sub");
        if (hd)
          hd.textContent = applyPageMode
            ? "Escolher modelo para esta página"
            : "Biblioteca de templates";
        if (sub)
          sub.textContent = applyPageMode
            ? "Clique no modelo para ver a confirmação e aplicar. Título e URL do WordPress não mudam."
            : "Clique em um card para aplicar ou inserir.";
        loadList();
      });
    });
    var searchEl = document.getElementById("ab-tpl-lib-search");
    if (searchEl) {
      searchEl.addEventListener("input", function () {
        clearTimeout(searchTimer);
        searchTimer = setTimeout(function () {
          searchQ = String(searchEl.value || "").trim();
          loadList();
        }, 220);
      });
    }
    loadList();
  }

  /**
   * Apply template. opts.forcePage = replace entire page document.
   */
  function insertTemplateById(id, modal, opts) {
    opts = opts || {};
    if (!id) return;
    setStatus("Aplicando modelo…", "");
    api("/templates/" + encodeURIComponent(id) + "/insert", { method: "POST", body: {} })
      .then(function (res) {
        var doc = res.document;
        var type = res.type || "component";
        if (!doc || !doc.nodes) throw new Error("Documento vazio");

        // Page layout apply (main request)
        if (type === "page" || opts.forcePage) {
          if (isThemePart) {
            throw new Error("Modelos de página só podem ser aplicados no editor de página.");
          }
          if (
            !confirm(
              "Aplicar o modelo “" +
                (res.name || id) +
                "” nesta página?\n\n" +
                "• O conteúdo atual do Builder será substituído\n" +
                "• Título e endereço da página no WordPress permanecem\n" +
                "• Você pode desfazer com Ctrl+Z"
            )
          ) {
            setStatus("Cancelado", "");
            return;
          }
          applyPageLayoutDocument(doc, res.name || id);
          if (modal) modal.remove();
          return;
        }

        if (type === "header" || type === "footer") {
          if (!isThemePart || editorMode !== type) {
            if (
              !confirm(
                "Abrir o editor de " +
                  type +
                  " e aplicar este template? (salve a página antes se necessário)"
              )
            ) {
              return;
            }
            window.location.href =
              "admin.php?page=aura-builder&mode=" +
              encodeURIComponent(type) +
              (postId ? "&return=" + encodeURIComponent(String(postId)) : "");
            return;
          }
          pushHistory();
          state.document = doc;
          normalizeDocumentStyles(state.document);
          ensureDocSettings();
          state.document.settings.showHeader = false;
          state.document.settings.showFooter = false;
          markDirty();
          paint();
          setStatus("Template de " + type + " aplicado", "is-dirty");
        } else {
          // Component: merge section children under current root (or selected section parent).
          pushHistory();
          mergeTemplateFragment(doc);
          markDirty();
          paint();
          setStatus("Seção inserida do template", "is-dirty");
        }
        if (modal) modal.remove();
      })
      .catch(function (err) {
        setStatus("Erro template: " + (err.message || err), "is-err");
        alert(err.message || "Falha ao aplicar modelo");
      });
  }

  /** Replace page body with template; keep page-level settings. */
  function applyPageLayoutDocument(doc, name) {
    pushHistory();
    var prev = state.document || {};
    var prevSettings = (prev.settings && typeof prev.settings === "object"
      ? prev.settings
      : {}) || {};
    // Fresh document from template
    state.document = doc;
    normalizeDocumentStyles(state.document);
    ensureDocSettings();
    // Preserve page-specific prefs
    var keep = [
      "showHeader",
      "showFooter",
      "header_source",
      "footer_source",
      "is_home_page",
      "page_role",
      "page_categories",
      "site_bg",
      "site_bg_mode",
      "site_bg_image",
      "site_bg_images",
      "site_bg_size",
      "site_bg_size_pct",
      "site_bg_position",
      "site_bg_repeat",
      "site_bg_carousel_speed",
      "site_bg_carousel_fx",
      "breakpoints",
    ];
    keep.forEach(function (k) {
      if (prevSettings[k] !== undefined && prevSettings[k] !== null && prevSettings[k] !== "") {
        state.document.settings[k] = prevSettings[k];
      }
    });
    state.selectedId = null;
    state.imported = false;
    state.source = "template";
    markDirty();
    paint();
    setStatus("Modelo “" + (name || "layout") + "” aplicado — salve a página", "is-dirty");
  }

  /** Insert component template sections into the current document. */
  function mergeTemplateFragment(frag) {
    if (!state.document || !frag || !frag.nodes) return;
    var rootId = state.document.rootId;
    var root = state.document.nodes[rootId];
    if (!root) return;
    // Parent: selected section/container or root.
    var parentId = rootId;
    if (state.selectedId) {
      var sel = getNode(state.selectedId);
      if (sel && (sel.type === "section" || sel.type === "container" || sel.type === "root")) {
        parentId = sel.id;
      } else if (sel && sel.parentId) {
        parentId = sel.parentId;
      }
    }
    var parent = state.document.nodes[parentId] || root;
    parentId = parent.id;

    var fragRoot = frag.rootId;
    var fragRootNode = frag.nodes[fragRoot];
    var toAdd = (fragRootNode && fragRootNode.children) || [];
    // If fragment is a single section tree, add those sections.
    Object.keys(frag.nodes).forEach(function (nid) {
      if (nid === fragRoot) return;
      var n = frag.nodes[nid];
      if (!n) return;
      // Deep clone node into document.
      state.document.nodes[nid] = JSON.parse(JSON.stringify(n));
    });
    toAdd.forEach(function (cid) {
      var child = state.document.nodes[cid];
      if (!child) return;
      child.parentId = parentId;
      if (!parent.children) parent.children = [];
      parent.children.push(cid);
    });
    // Orphan any non-root nodes that weren't under root children (still attached via tree).
    // Clean: remove fragment root if accidentally copied.
    if (state.document.nodes[fragRoot] && fragRoot !== rootId) {
      delete state.document.nodes[fragRoot];
    }
  }

  function saveAsTemplate() {
    var name = prompt("Nome do template:", "");
    if (name == null) return;
    name = String(name).trim();
    if (!name) {
      alert("Informe um nome.");
      return;
    }
    var scope = "page";
    var body = { name: name, document: state.document };
    if (isThemePart) {
      scope =
        editorMode === "footer" ? "footer" : editorMode === "single" ? "page" : "header";
      body.scope = scope;
    } else if (state.selectedId) {
      var sel = getNode(state.selectedId);
      if (sel && (sel.type === "section" || sel.type === "container")) {
        scope = "section";
        body.scope = "section";
        body.nodeId = sel.id;
        body.subtype = sel.type;
      } else {
        body.scope = "page";
        if (postId) body.postId = postId;
      }
    } else {
      body.scope = "page";
      if (postId) body.postId = postId;
    }
    setStatus("Salvando template…", "");
    api("/templates/export", { method: "POST", body: body })
      .then(function (res) {
        setStatus("Template salvo: " + (res.template && res.template.name), "is-ok");
        alert(
          "Template “" +
            ((res.template && res.template.name) || name) +
            "” salvo em Meus templates.\nAbra Biblioteca (ícone livro) para reutilizar."
        );
      })
      .catch(function (err) {
        setStatus("Erro ao salvar template", "is-err");
        alert(err.message || "Falha");
      });
  }

  /**
   * Header Theme Builder layout fix:
   * A) Leaves (logo+menu) soltos na seção → flex row.
   * B) Colunas (containers) com 25%/75% → forçar direction=grid e spans.
   */
  function maybeUpgradeHeaderRowLayout(doc) {
    if (!doc || !doc.nodes) return false;
    if (!isThemePart || editorMode !== "header") return false;
    ensureDocSettings();
    var changed = false;
    Object.keys(doc.nodes).forEach(function (id) {
      var n = doc.nodes[id];
      if (!n || n.type !== "section") return;
      n.settings = n.settings || {};
      var kids = n.children || [];
      if (!kids.length) return;
      var hasLeaf = false;
      var colIds = [];
      kids.forEach(function (cid) {
        var c = doc.nodes[cid];
        if (!c) return;
        if (c.type === "container") colIds.push(cid);
        else if (c.type !== "section") hasLeaf = true;
      });

      // B) Multi-column header: keep grid so 25%/75% (col_span) works.
      if (colIds.length >= 2) {
        if (n.settings.direction !== "grid") {
          n.settings.direction = "grid";
          changed = true;
        }
        n.settings.columns = colIds.length;
        // Only auto-span when every column still claims full width (default).
        // Never overwrite an intentional 25/75 (3+9) or 50/50 (6+6).
        var spans = colIds.map(function (cid) {
          var c = doc.nodes[cid];
          return resolveColumnSpan(c);
        });
        var allFull = spans.every(function (sp) {
          return sp >= 12;
        });
        if (allFull) {
          if (colIds.length === 2) {
            // Header típico: logo 25% | menu 75%
            var c0 = doc.nodes[colIds[0]];
            var c1 = doc.nodes[colIds[1]];
            if (c0) {
              c0.settings = c0.settings || {};
              c0.settings.col_span = 3;
              c0.settings.width = "25%";
            }
            if (c1) {
              c1.settings = c1.settings || {};
              c1.settings.col_span = 9;
              c1.settings.width = "75%";
            }
            changed = true;
          } else {
            var base = Math.floor(12 / colIds.length);
            var rem = 12 - base * colIds.length;
            colIds.forEach(function (cid, i) {
              var c = doc.nodes[cid];
              if (!c) return;
              var sp = base + (i < rem ? 1 : 0);
              c.settings = c.settings || {};
              c.settings.col_span = sp;
              c.settings.width = spanToWidthPct(sp);
            });
            changed = true;
          }
        }
        n.styles = n.styles || {};
        n.styles.desktop = n.styles.desktop || {};
        if (!n.styles.desktop.align_items) {
          n.styles.desktop.align_items = "center";
          changed = true;
        }
        if (n.styles.desktop.display) {
          delete n.styles.desktop.display;
          changed = true;
        }
        return;
      }

      // A) Only auto-fix when leaves live directly in the section (header pattern).
      if (!hasLeaf) return;
      if (n.settings.direction === "grid" || !n.settings.direction) {
        n.settings.direction = "row";
        n.settings.columns = Math.max(2, kids.length);
        changed = true;
      }
      n.styles = n.styles || {};
      n.styles.desktop = n.styles.desktop || {};
      if (!n.styles.desktop.align_items) {
        n.styles.desktop.align_items = "center";
        changed = true;
      }
      if (!n.styles.desktop.justify_content) {
        n.styles.desktop.justify_content = "space-between";
        changed = true;
      }
      // Remove stale display:flex from bag (layout comes from direction).
      if (n.styles.desktop.display) {
        delete n.styles.desktop.display;
        changed = true;
      }
      // Drop fixed widths on nav that break flex (e.g. 973px from resize handle).
      kids.forEach(function (cid) {
        var c = doc.nodes[cid];
        if (!c) return;
        c.styles = c.styles || {};
        c.styles.desktop = c.styles.desktop || {};
        if (c.type === "nav_menu" && c.styles.desktop.width) {
          delete c.styles.desktop.width;
          changed = true;
        }
        if (c.type === "nav_menu") {
          c.settings = c.settings || {};
          if ((c.settings.layout || "horizontal") !== "horizontal") {
            c.settings.layout = "horizontal";
            changed = true;
          }
        }
      });
    });
    if (changed) {
      doc.settings = doc.settings || {};
      doc.settings._ab_header_row_v1 = 1;
    }
    return changed;
  }

  function load() {
    if (isThemePart) {
      return Promise.all([api("/theme-parts/" + editorMode), loadGlobals()]).then(function (pair) {
        var res = pair[0];
        var part = res.part || {};
        var enRaw = part.enabled;
        var enabledNow =
          enRaw === true ||
          enRaw === 1 ||
          enRaw === "1" ||
          enRaw === "true" ||
          enRaw === "yes" ||
          enRaw === "on";
        themePartMeta = {
          enabled: enabledNow,
          display:
            part.display ||
            (editorMode === "single" ? "singular" : "all"),
          include_ids: part.include_ids || "",
          exclude_ids: part.exclude_ids || "",
          source: part.source || "builder",
          sticky: part.sticky || "0",
          sticky_bg: part.sticky_bg || "#ffffff",
          transparent: part.transparent || "0",
          header_source: part.header_source || "global",
          footer_source: part.footer_source || "global",
        };
        state.document = res.document || part.document;
        if (!state.document) {
          throw new Error("Documento do " + editorMode + " vazio");
        }
        normalizeDocumentStyles(state.document);
        ensureDocSettings();
        var headerFixed = maybeUpgradeHeaderRowLayout(state.document);
        // Single: mark starter layout dirty so first Save activates it on the site.
        var singleStarter =
          editorMode === "single" &&
          state.document.metadata &&
          state.document.metadata.starter &&
          !enabledNow;
        if (editorMode === "single" && !themePartMeta.display) {
          themePartMeta.display = "singular";
        }
        state.title =
          editorMode === "footer"
            ? "Footer (Theme Builder)"
            : editorMode === "single"
              ? "Single Post (Theme Builder)"
              : "Header (Theme Builder)";
        state.viewUrl = CFG.viewUrl || CFG.homeUrl || "/";
        state.editUrl = "";
        state.imported = false;
        state.source = "theme_part";
        // Single: same chrome preview as Home (ThemeChrome HTML).
        if (editorMode === "single" && res.theme) {
          state.theme = res.theme;
          if (!state.theme.styles) state.theme.styles = { links: [], inline: "" };
          if (!state.theme.homeUrl) state.theme.homeUrl = CFG.homeUrl || "/";
        } else {
          state.theme = {
            header: "",
            footer: "",
            hasHeader: false,
            hasFooter: false,
            styles: { links: [], inline: "" },
            homeUrl: CFG.homeUrl || "/",
          };
        }
        // Header/Footer editors: no nested chrome. Single: show + configure like Home.
        state.document.settings = state.document.settings || {};
        if (editorMode === "header" || editorMode === "footer") {
          state.document.settings.showHeader = false;
          state.document.settings.showFooter = false;
        } else if (editorMode === "single") {
          var hs = themePartMeta.header_source || "global";
          var fs = themePartMeta.footer_source || "global";
          state.document.settings.header_source = hs;
          state.document.settings.footer_source = fs;
          state.document.settings.showHeader = hs !== "none";
          state.document.settings.showFooter = fs !== "none";
        }
        if (res.nonce) {
          nonce = res.nonce;
          CFG.nonce = res.nonce;
        }
        state.dirty = !!headerFixed || !!singleStarter;
        if (headerFixed) {
          setStatus("Layout do header ajustado — salve para gravar", "is-dirty");
        } else if (singleStarter) {
          setStatus(
            "Modelo Single pronto — clique em Salvar para ativar nos posts",
            "is-dirty"
          );
        } else if (editorMode === "single" && !enabledNow) {
          setStatus(
            "Single inativo — Salvar ativa o modelo nos posts",
            "is-dirty"
          );
          state.dirty = true;
        } else {
          setStatus("Theme Builder · " + editorMode, "is-ok");
        }
        var t = document.getElementById("ab-title");
        if (t) t.textContent = state.title;
        var view = document.getElementById("ab-view");
        var back = document.getElementById("ab-back");
        if (view) view.href = state.viewUrl;
        if (back) back.href = backUrl || "admin.php?page=aura-builder-theme";
        paint();
      });
    }

    return Promise.all([api("/documents/" + postId), loadGlobals()]).then(function (pair) {
      var res = pair[0];
      state.document = res.document;
      normalizeDocumentStyles(state.document);
      ensureDocSettings();
      // Ensure root has editable settings object
      var root = state.document && state.document.nodes && state.document.nodes[state.document.rootId];
      if (root) {
        root.settings = root.settings || {};
        if (!root.settings.content_width) root.settings.content_width = "full";
        if (!root.settings.max_width) root.settings.max_width = "1200px";
        normalizeNodeStyles(root);
      }
      var upgraded = maybeUpgradeFullWidthLayout(state.document);
      state.title = res.title || "";
      state.viewUrl = res.viewUrl || state.viewUrl;
      state.editUrl = res.editUrl || state.editUrl;
      state.imported = !!res.imported;
      state.source = res.source || (res.imported ? "import" : "saved");
      state.theme = res.theme || {
        header: "",
        footer: "",
        hasHeader: false,
        hasFooter: false,
        styles: { links: [], inline: "" },
        homeUrl: "",
      };
      if (!state.theme.styles) state.theme.styles = { links: [], inline: "" };
      if (res.nonce) nonce = res.nonce;
      state.dirty = !!res.imported || upgraded;
      if (res.imported) {
        setStatus("Importado — salve para gravar", "is-dirty");
      } else if (upgraded) {
        setStatus("Largura total aplicada — salve a página", "is-dirty");
      } else {
        setStatus("Pronto", "is-ok");
      }
      var view = document.getElementById("ab-view");
      var back = document.getElementById("ab-back");
      if (view && state.viewUrl) view.href = state.viewUrl;
      if (back) back.href = backUrl || "admin.php?page=aura-builder-pages";
      paint();
    });
  }

  function boot() {
    var root = document.getElementById("aura-builder-root");
    if (!root) return;
    // data-ab-mode from PHP for theme parts
    var modeAttr = root.getAttribute("data-ab-mode");
    if (modeAttr === "header" || modeAttr === "footer" || modeAttr === "single") {
      editorMode = modeAttr;
      isThemePart = true;
    }
    if (!postId && !isThemePart) {
      root.innerHTML = '<div class="ab-error-box">ID da página não informado.</div>';
      return;
    }
    document.documentElement.classList.add("ab-editor-open");
    document.body.classList.add("ab-editor-open");
    if (isThemePart) document.body.classList.add("ab-editor-theme-part");
    buildShell(root);
    load().catch(function (err) {
      document.getElementById("ab-canvas").innerHTML =
        '<div class="ab-error-box"><strong>Falha ao carregar</strong><p>' +
        esc(err.message) +
        "</p></div>";
      setStatus("Erro", "is-err");
    });

    // Mantém nonce REST válido em sessões longas (evita falha de cookie no save).
    setInterval(function () {
      refreshNonce().catch(function () {});
    }, 10 * 60 * 1000);

    window.addEventListener("beforeunload", function (e) {
      if (state.dirty) {
        e.preventDefault();
        e.returnValue = "";
      }
    });
    document.addEventListener("keydown", function (e) {
      // While typing text, only allow Ctrl+S; never remove widgets with Backspace.
      if (isEditingText(e)) {
        if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === "s") {
          e.preventDefault();
          save();
        }
        return;
      }

      if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === "s") {
        e.preventDefault();
        save();
      }
      if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === "z" && !e.shiftKey) {
        e.preventDefault();
        undo();
      }
      if (
        (e.ctrlKey || e.metaKey) &&
        (e.key.toLowerCase() === "y" || (e.shiftKey && e.key.toLowerCase() === "z"))
      ) {
        e.preventDefault();
        redo();
      }
      // Esc clears multi-select
      if (e.key === "Escape") {
        if (getSelectedIds().length > 1) {
          e.preventDefault();
          state.selectedIds = state.selectedId ? [state.selectedId] : [];
          updateSelectionUI();
          return;
        }
      }
      // Only Delete removes a widget (never Backspace — that is for text).
      if (e.key === "Delete" && state.selectedId) {
        e.preventDefault();
        var delIds = getSelectedIds();
        execute(function () {
          // Delete deepest-first / reverse so parents aren't wiped before kids if mixed
          delIds.slice().reverse().forEach(function (did) {
            removeNode(did);
          });
          state.selectedIds = [];
          state.selectedId = null;
        });
      }
      if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === "d" && state.selectedId) {
        e.preventDefault();
        execute(function () {
          duplicateNode(state.selectedId);
        });
      }
    });
  }

  if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", boot);
  else boot();
})();
