import React, { useState, useRef, useEffect } from 'react';
import ReactDOM from 'react-dom';
import type { CBNode, StyleMap } from '../../types';
import type { EditorStore } from '../../store/editorStore';
import { BlockPicker } from '../canvas/BlockPicker';
import { ICONS } from '../../utils/icons';
import { DynamicTagPicker } from './DynamicTagPicker';
import { IconInput, IconPickerModal, RenderIconValue } from './IconPickerModal';
import type { IconValue, TokenCategory, DesignToken, JalpiClass, BgData, BorderData, InteractionRule, InteractionTrigger, InteractionAction, InteractionSettings } from '../../types';
import { tokenCategoryFor } from '../../utils/designTokens';
import { useApi } from '../../hooks/useApi';
import { buildWaapiPreviewKeyframes, type AnimationPreset } from '../../utils/animationPresets';
import AnimationPresetPicker from './AnimationPresetPicker';
import { GenericContent, GenericStyleExtras } from '../../widgets/GenericContent';
import { isSchemaWidget } from '../../widgets/registry';

declare const jalpiEditorConfig: {
  colors:  Array<{ name: string; color: string; slug: string }>;
  fonts:   Array<{ label: string; value: string }>;
  postId:  number;
  apiUrl:  string;
  nonce:   string;
};

interface Props { store: EditorStore }

const TAB_LABELS: Record<string, string> = {
  properties: 'Content',
  style:      'Style',
  animation:  'Animation',
  responsive: 'Advanced',
};

const ELEMENT_META: Record<string, { label: string; icon: React.ReactElement }> = {
  section:       { label: 'Section',       icon: <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><rect x="2" y="3" width="20" height="18" rx="2"/><line x1="2" y1="9" x2="22" y2="9"/></svg> },
  container:     { label: 'Container',     icon: <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><rect x="2" y="4" width="20" height="16" rx="2"/><line x1="12" y1="4" x2="12" y2="20"/></svg> },
  column:        { label: 'Column',        icon: <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><rect x="3" y="3" width="7" height="18" rx="1"/><rect x="14" y="3" width="7" height="18" rx="1" opacity=".6"/></svg> },
  heading:       { label: 'Heading',       icon: <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M4 12h16M4 6h16M4 18h8"/></svg> },
  text:          { label: 'Text',          icon: <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><line x1="17" y1="10" x2="3" y2="10"/><line x1="21" y1="6" x2="3" y2="6"/><line x1="21" y1="14" x2="3" y2="14"/><line x1="17" y1="18" x2="3" y2="18"/></svg> },
  image:         { label: 'Image',         icon: <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="8.5" cy="8.5" r="1.5"/><polyline points="21 15 16 10 5 21"/></svg> },
  button:        { label: 'Button',        icon: <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><rect x="2" y="7" width="20" height="10" rx="5"/></svg> },
  spacer:        { label: 'Spacer',        icon: <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><line x1="12" y1="5" x2="12" y2="19"/><polyline points="8 9 12 5 16 9"/><polyline points="16 15 12 19 8 15"/></svg> },
  divider:       { label: 'Divider',       icon: <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><line x1="3" y1="12" x2="21" y2="12"/></svg> },
  video:         { label: 'Video',         icon: <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><polygon points="23 7 16 12 23 17 23 7"/><rect x="1" y="5" width="15" height="14" rx="2"/></svg> },
  maps:          { label: 'Map',           icon: <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"/><circle cx="12" cy="10" r="3"/></svg> },
  html:          { label: 'HTML',          icon: <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><polyline points="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 18"/></svg> },
  icon:          { label: 'Icon',          icon: <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/></svg> },
  shortcode:     { label: 'Shortcode',     icon: <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><polyline points="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 18"/><line x1="12" y1="3" x2="12" y2="21"/></svg> },
  logo:          { label: 'Logo',          icon: <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><rect x="3" y="7" width="18" height="10" rx="2"/><path d="M9 7V5a2 2 0 0 1 4 0v2"/></svg> },
  form:          { label: 'Form',          icon: <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"/><line x1="7" y1="8" x2="17" y2="8"/><line x1="7" y1="12" x2="17" y2="12"/><line x1="7" y1="16" x2="13" y2="16"/></svg> },
  alert:         { label: 'Alert',         icon: <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round"><path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg> },
  counter:       { label: 'Counter',       icon: <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round"><circle cx="12" cy="12" r="10"/><path d="M12 8v4l3 3"/></svg> },
  progress:      { label: 'Progress',      icon: <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round"><rect x="2" y="10" width="20" height="4" rx="2"/><rect x="2" y="10" width="14" height="4" rx="2" fill="currentColor" stroke="none"/></svg> },
  testimonial:   { label: 'Testimonial',   icon: <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round"><path d="M3 21c3 0 7-1 7-8V5c0-1.25-.756-2.017-2-2H4c-1.25 0-2 .75-2 1.972V11c0 1.25.75 2 2 2 1 0 1 0 1 1v1c0 1-1 2-2 2s-1 .008-1 1.031V20c0 1 0 1 1 1z"/><path d="M15 21c3 0 7-1 7-8V5c0-1.25-.757-2.017-2-2h-4c-1.25 0-2 .75-2 1.972V11c0 1.25.75 2 2 2h.75c0 2.25.25 4-2.75 4v3c0 1 0 1 1 1z"/></svg> },
  accordion:     { label: 'Accordion',     icon: <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round"><rect x="2" y="4" width="20" height="4" rx="1"/><rect x="2" y="10" width="20" height="4" rx="1" opacity=".5"/><rect x="2" y="16" width="20" height="4" rx="1" opacity=".3"/></svg> },
  badge:         { label: 'Badge',         icon: <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round"><circle cx="12" cy="8" r="6"/><path d="M15.477 12.89L17 22l-5-3-5 3 1.523-9.11"/></svg> },
  'icon-list':   { label: 'Icon List',     icon: <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round"><polyline points="9 11 12 14 22 4"/><path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"/></svg> },
  'pricing-table':{ label: 'Pricing Table', icon: <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round"><rect x="2" y="3" width="20" height="18" rx="2"/><line x1="2" y1="9" x2="22" y2="9"/><line x1="12" y1="9" x2="12" y2="21"/></svg> },
  'team-member': { label: 'Team Member',   icon: <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round"><circle cx="12" cy="8" r="4"/><path d="M4 20c0-4 3.6-7 8-7s8 3 8 7"/></svg> },
  faq:           { label: 'FAQ',           icon: <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round"><circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg> },
  table:         { label: 'Table',         icon: <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round"><rect x="2" y="3" width="20" height="18" rx="2"/><line x1="2" y1="9" x2="22" y2="9"/><line x1="8" y1="9" x2="8" y2="21"/></svg> },
  'feature-card':{ label: 'Feature Card',  icon: <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round"><rect x="2" y="3" width="20" height="18" rx="2"/><path d="M8 10h8M8 14h5"/></svg> },
  cta:           { label: 'Call To Action', icon: <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round"><path d="M5 12h14M12 5l7 7-7 7"/></svg> },
  'stats-card':  { label: 'Stats Card',    icon: <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round"><line x1="18" y1="20" x2="18" y2="10"/><line x1="12" y1="20" x2="12" y2="4"/><line x1="6" y1="20" x2="6" y2="14"/></svg> },
  timeline:      { label: 'Timeline',      icon: <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round"><line x1="12" y1="2" x2="12" y2="22"/><circle cx="12" cy="6" r="2" fill="currentColor" stroke="none"/><circle cx="12" cy="12" r="2" fill="currentColor" stroke="none" opacity=".6"/><circle cx="12" cy="18" r="2" fill="currentColor" stroke="none" opacity=".4"/></svg> },
  testimonials:  { label: 'Testimonials',  icon: <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round"><path d="M3 21c3 0 7-1 7-8V5c0-1.25-.756-2.017-2-2H4c-1.25 0-2 .75-2 1.972V11c0 1.25.75 2 2 2 1 0 1 0 1 1v1c0 1-1 2-2 2s-1 .008-1 1.031V20c0 1 0 1 1 1z"/><path d="M15 21c3 0 7-1 7-8V5c0-1.25-.757-2.017-2-2h-4c-1.25 0-2 .75-2 1.972V11c0 1.25.75 2 2 2h.75c0 2.25.25 4-2.75 4v3c0 1 0 1 1 1z"/></svg> },
};

/* =========================================================
   Accordion default-open maps (per tab × element type)
   ========================================================= */

const CONTENT_FIRST_GROUP: Record<string, string> = {
  section:        'Layout',
  container:      'Layout',
  column:         'Column',
  heading:        'Heading',
  text:           'Text Content',
  image:          'Image',
  button:         'Button',
  spacer:         'Spacer',
  video:          'Video Source',
  maps:           'Google Maps',
  html:           'HTML Code',
  icon:           'Pick Icon',
  shortcode:      'Shortcode',
  logo:           'Logo Image',
  form:           'Form',
  alert:          'Alert',
  counter:        'Counter',
  progress:       'Progress Bars',
  testimonial:    'Testimonial',
  accordion:      'Accordion Panels',
  badge:          'Badge',
  'icon-list':    'Icon List',
  'pricing-table':'Pricing',
  'team-member':  'Team Member',
  faq:            'FAQ Items',
  table:          'Table',
  'feature-card': 'Feature Card',
  cta:            'Call To Action',
  'stats-card':   'Stats',
  timeline:       'Timeline',
  testimonials:   'Testimonials',
};

const STYLE_FIRST_GROUP: Record<string, string> = {
  section:        'Background',
  container:      'Background',
  column:         'Background',
  heading:        'Text Color',
  text:           'Text Color',
  button:         'Typography',
  html:           'Typography',
  icon:           'Typography',
  accordion:      'Accordion',
  counter:        'Counter',
  progress:       'Progress Bar',
  video:          'Video',
  'feature-card': 'Box',
  cta:            'Wrapper',
  faq:             'Layout',
  'image-carousel': 'Layout',
};

/* =========================================================
   Device Banner
   ========================================================= */

function DeviceBanner({ bp, onBack }: { bp: 'tablet' | 'mobile'; onBack: () => void }) {
  const isTablet = bp === 'tablet';
  return (
    <div className={`jalpi-dev-banner jalpi-dev-banner--${bp}`}>
      {isTablet ? (
        <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
          <rect x="4" y="2" width="16" height="20" rx="2"/><line x1="12" y1="18" x2="12" y2="18"/>
        </svg>
      ) : (
        <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
          <rect x="5" y="2" width="14" height="20" rx="2"/><line x1="12" y1="18" x2="12" y2="18"/>
        </svg>
      )}
      <span>Editing <strong>{isTablet ? 'Tablet' : 'Mobile'}</strong> — overrides only</span>
      <button className="jalpi-dev-banner__back" onClick={onBack}>↩ Desktop</button>
    </div>
  );
}

/* =========================================================
   Root panel
   ========================================================= */

export function PropertiesPanel({ store }: Props) {
  const { state } = store;
  const node = store.getSelectedNode();
  const meta = node ? ELEMENT_META[node.type] : null;
  const isSection   = node?.type === 'section';
  const isContainer = node?.type === 'container';
  const isColumn    = node?.type === 'column';

  const tabLabel = (tab: string) => {
    if (tab === 'responsive' && (isSection || isContainer || isColumn)) return 'Settings';
    return TAB_LABELS[tab];
  };

  return (
    <div className="jalpi-props-panel">

      {/* Element type badge */}
      {node && meta && (
        <div className="jalpi-element-badge">
          <span className="jalpi-element-badge__icon">{meta.icon}</span>
          <span className="jalpi-element-badge__label">{meta.label}</span>
        </div>
      )}

      <div className="jalpi-props-panel__tabs">
        {(['properties', 'style', 'animation', 'responsive'] as const).map((tab) => (
          <button
            key={tab}
            className={`jalpi-props-panel__tab${state.panelTab === tab ? ' is-active' : ''}`}
            onClick={() => store.setPanelTab(tab)}
          >
            {tabLabel(tab)}
          </button>
        ))}
      </div>

      {state.breakpoint !== 'desktop' && (
        <DeviceBanner bp={state.breakpoint} onBack={() => store.setBreakpoint('desktop')} />
      )}

      <div className="jalpi-props-panel__body">
        {!node ? (
          <NothingSelected store={store} />
        ) : (
          <>
            {state.panelTab === 'properties' && (
              <AccordionScope key={node.id + '-content'} defaultGroup={CONTENT_FIRST_GROUP[node.type] ?? null}>
                <ContentTab node={node} store={store} />
              </AccordionScope>
            )}
            {state.panelTab === 'style' && (
              <AccordionScope key={node.id + '-style'} defaultGroup={STYLE_FIRST_GROUP[node.type] ?? 'Background'}>
                <StyleTab node={node} store={store} />
              </AccordionScope>
            )}
            {state.panelTab === 'animation' && (
              <AccordionScope key={node.id + '-animation'} defaultGroup="Interaction">
                <AnimationTab node={node} store={store} />
              </AccordionScope>
            )}
            {state.panelTab === 'responsive' && (
              <AccordionScope key={node.id + '-advanced'} defaultGroup="Margin">
                <AdvancedTab node={node} store={store} />
              </AccordionScope>
            )}
          </>
        )}
      </div>
    </div>
  );
}

/* =========================================================
   Empty state
   ========================================================= */

function NothingSelected({ store }: { store: EditorStore }) {
  return (
    <div className="jalpi-empty-state">
      <div className="jalpi-empty-state__icon">
        <svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.75" strokeLinecap="round" strokeLinejoin="round">
          <path d="M3 3l7.07 16.97 2.51-7.39 7.39-2.51L3 3z"/>
          <path d="m13 13 6 6"/>
        </svg>
      </div>
      <p className="jalpi-empty-state__title">Nothing selected</p>
      <p className="jalpi-empty-state__desc">Click any element on the canvas to edit it, or add a new section to get started.</p>
      <button className="jalpi-btn jalpi-btn--teal" onClick={() => store.addSection()}>
        + Add Section
      </button>
    </div>
  );
}

/* =========================================================
   CONTENT TAB
   ========================================================= */

function ContentTab({ node, store }: { node: CBNode; store: EditorStore }) {
  switch (node.type) {
    case 'section':   return <SectionContent   node={node} store={store} />;
    case 'container': return <ContainerContent node={node} store={store} />;
    case 'column':    return null;
    case 'heading':   return <HeadingContent   node={node} store={store} />;
    case 'text':      return <TextContent      node={node} store={store} />;
    case 'image':     return <ImageContent     node={node} store={store} />;
    case 'button':    return <ButtonContent    node={node} store={store} />;
    case 'spacer':    return <SpacerContent    node={node} store={store} />;
    case 'divider':   return <DividerContent   node={node} store={store} />;
    case 'video':     return <VideoContent     node={node} store={store} />;
    case 'maps':      return <MapsContent      node={node} store={store} />;
    case 'html':      return <HtmlContent      node={node} store={store} />;
    case 'icon':      return <IconContent      node={node} store={store} />;
    case 'shortcode': return <ShortcodeContent node={node} store={store} />;
    case 'logo':          return <LogoContent         node={node} store={store} />;
    case 'form':          return <FormContent         node={node} store={store} />;
    case 'alert':         return <AlertContent        node={node} store={store} />;
    case 'counter':       return <CounterContent      node={node} store={store} />;
    case 'progress':      return <ProgressContent     node={node} store={store} />;
    case 'testimonial':   return <TestimonialContent  node={node} store={store} />;
    case 'accordion':     return <AccordionElContent  node={node} store={store} />;
    case 'badge':         return <BadgeContent        node={node} store={store} />;
    case 'icon-list':     return <IconListContent     node={node} store={store} />;
    case 'pricing-table': return <PricingTableContent node={node} store={store} />;
    case 'team-member':   return <TeamMemberContent   node={node} store={store} />;
    case 'faq':           return <FaqContent          node={node} store={store} />;
    case 'table':         return <TableContent        node={node} store={store} />;
    case 'feature-card':  return <FeatureCardContent  node={node} store={store} />;
    case 'cta':           return <CtaContent          node={node} store={store} />;
    case 'stats-card':    return <StatsCardContent    node={node} store={store} />;
    case 'timeline':      return <TimelineContent         node={node} store={store} />;
    case 'testimonials':   return <TestimonialsCarouselContent node={node} store={store} />;
    case 'tabs':           return <TabsContent          node={node} store={store} />;
    case 'tabs-adv':       return <TabsAdvContent       node={node} store={store} />;
    case 'menu':           return <MenuContent          node={node} store={store} />;
    case 'adv-menu':       return <AdvMenuContent       node={node} store={store} />;
    case 'breadcrumbs':    return <BreadcrumbsContent   node={node} store={store} />;
    case 'image-box':      return <ImageBoxContent       node={node} store={store} />;
    case 'social-icons':   return <SocialIconsContent    node={node} store={store} />;
    case 'gallery':        return <GalleryContent        node={node} store={store} />;
    case 'image-carousel': return <ImageCarouselContent  node={node} store={store} />;
    case 'countdown':      return <CountdownContent      node={node} store={store} />;
    case 'flip-box':       return <FlipBoxContent        node={node} store={store} />;
    case 'star-rating':    return <StarRatingContent     node={node} store={store} />;
    case 'products-grid':  return <ProductsGridContent   node={node} store={store} />;
    case 'product-categories': return <ProductCategoriesContent node={node} store={store} />;
    case 'single-product':     return <SingleProductContent     node={node} store={store} />;
    default:
      // Schema-driven registry widgets render their content controls from the
      // field schema. Unknown types (none in practice) still return null.
      if (isSchemaWidget(node.type)) return <GenericContent node={node} store={store} />;
      return null;
  }
}

/* ─────────────────────────────────────
   SECTION — Content Tab (Layout)
   ──────────────────────────────────── */
function SectionContent({ node, store }: { node: CBNode; store: EditorStore }) {
  const { state } = store;
  const bp         = state.breakpoint;
  const settings   = (node.settings ?? {}) as Record<string, unknown>;
  const styles     = (node.styles ?? {}) as Record<string, string>;

  const upSettings = (key: string, val: unknown) =>
    store.updateNode(node.id, { settings: { ...settings, [key]: val } });
  const upStyle = (updates: Record<string, string>) =>
    store.updateNodeStyles(node.id, updates);

  // Responsive: Width and Height are stored per-breakpoint so desktop / tablet / mobile
  // each remember their own value independently.
  const respKey: 'tabletStyles' | 'mobileStyles' | null =
    bp === 'tablet' ? 'tabletStyles' : bp === 'mobile' ? 'mobileStyles' : null;
  const respStyles = (respKey ? (node[respKey] ?? {}) : {}) as Record<string, string>;
  const updateResp = (updates: Record<string, string>) =>
    store.updateNode(node.id, { [respKey!]: { ...respStyles, ...updates } });

  // Width displayed in the control — tablet/mobile use tabletStyles.maxWidth with desktop fallback
  const widthVal = respKey
    ? (respStyles.maxWidth || (settings.boxedWidth as string) || '1200px')
    : ((settings.boxedWidth as string) || '1200px');

  // Height displayed in the control — tablet/mobile use tabletStyles.height with desktop fallback
  const heightVal = respKey
    ? (respStyles.height || styles.height || '')
    : (styles.height || '');

  const contentWidth = (settings.contentWidth as string) || 'full';
  const alignItems   = (settings.alignItems   as string) || '';

  const ALIGN_OPTS = [
    {
      val: 'flex-start', title: 'Top',
      icon: <svg width="14" height="14" viewBox="0 0 20 20" fill="currentColor"><rect x="3" y="2" width="14" height="1.5" rx=".75" opacity=".5"/><rect x="5" y="4" width="4" height="7" rx="1"/><rect x="11" y="4" width="4" height="5" rx="1" opacity=".6"/></svg>,
    },
    {
      val: 'center', title: 'Center',
      icon: <svg width="14" height="14" viewBox="0 0 20 20" fill="currentColor"><rect x="3" y="9.25" width="14" height="1.5" rx=".75" opacity=".5"/><rect x="5" y="3" width="4" height="14" rx="1"/><rect x="11" y="6" width="4" height="8" rx="1" opacity=".6"/></svg>,
    },
    {
      val: 'flex-end', title: 'Bottom',
      icon: <svg width="14" height="14" viewBox="0 0 20 20" fill="currentColor"><rect x="5" y="9" width="4" height="7" rx="1"/><rect x="11" y="11" width="4" height="5" rx="1" opacity=".6"/><rect x="3" y="17.5" width="14" height="1.5" rx=".75" opacity=".5"/></svg>,
    },
    {
      val: 'stretch', title: 'Stretch',
      icon: <svg width="14" height="14" viewBox="0 0 20 20" fill="currentColor"><rect x="3" y="2" width="14" height="1.5" rx=".75" opacity=".5"/><rect x="3" y="16.5" width="14" height="1.5" rx=".75" opacity=".5"/><rect x="5" y="4" width="4" height="12" rx="1"/><rect x="11" y="4" width="4" height="12" rx="1" opacity=".6"/></svg>,
    },
  ];

  return (
    <div className="jalpi-props-section">
      <PropGroup label="Layout">
        <PropRow label="Content Width" stack>
          <div className="jalpi-toggle-group">
            <button
              className={`jalpi-toggle-group__btn${contentWidth === 'full' ? ' is-active' : ''}`}
              onClick={() => upSettings('contentWidth', 'full')}
            >Full Width</button>
            <button
              className={`jalpi-toggle-group__btn${contentWidth === 'boxed' ? ' is-active' : ''}`}
              onClick={() => upSettings('contentWidth', 'boxed')}
            >Boxed</button>
          </div>
        </PropRow>
        {contentWidth === 'boxed' && (
          <PropRow label="Width" stack>
            <SliderDimInput
              value={widthVal}
              onChange={(v) => respKey ? updateResp({ maxWidth: v }) : upSettings('boxedWidth', v)}
              max={2560} step={10}
              units={['px', '%', 'vw', 'rem', 'em']}
              placeholder="1200"
            />
          </PropRow>
        )}
        <PropRow label="Height" stack>
          <SliderDimInput
            value={heightVal}
            onChange={(v) => respKey ? updateResp({ height: v }) : upStyle({ height: v })}
            max={1200} step={10}
            units={['px', '%', 'vh', 'rem', 'em', 'vw']}
            placeholder="auto"
          />
        </PropRow>
        <PropRow label="Align Items" stack>
          <div className="jalpi-icon-btn-group">
            {ALIGN_OPTS.map((o) => (
              <button key={o.val} type="button"
                className={`jalpi-icon-btn${alignItems === o.val ? ' is-active' : ''}`}
                title={o.title}
                onClick={() => upSettings('alignItems', alignItems === o.val ? '' : o.val)}
              >{o.icon}</button>
            ))}
          </div>
        </PropRow>
      </PropGroup>
    </div>
  );
}

/* — Container — */
function SliderDimInput({
  value, onChange, min = 0, max = 2000, step = 10, units = ['px', '%', 'vh'], placeholder = 'auto',
}: {
  value: string; onChange: (v: string) => void;
  min?: number; max?: number; step?: number; units?: string[]; placeholder?: string;
}) {
  const extractUnit = (v: string) => {
    for (const u of ['dvh', 'vw', 'vh', 'rem', 'em', 'px', '%', 'deg']) {
      if (v.endsWith(u) && units.includes(u)) return u;
    }
    return units[0];
  };
  const isEmpty   = !value || value === 'auto';
  const activeUnit = isEmpty ? units[0] : extractUnit(value);
  const numVal    = isEmpty ? 0 : parseFloat(value) || 0;
  const rangeMax  = (activeUnit === '%' || activeUnit === 'vh' || activeUnit === 'dvh') ? 100
                  : activeUnit === 'deg' ? 360 : max;
  const rangeStep = (activeUnit === '%' || activeUnit === 'vh' || activeUnit === 'dvh' || activeUnit === 'deg') ? 1 : step;
  const pct       = Math.min(100, (numVal / rangeMax) * 100);

  const cycleUnit = () => {
    const next = units[(units.indexOf(activeUnit) + 1) % units.length];
    onChange(numVal + next);
  };

  return (
    <div className="jalpi-slider-dim">
      <input
        type="range" className="jalpi-slider-dim__track"
        min={min} max={rangeMax} step={rangeStep} value={numVal}
        style={{ '--pct': `${pct}%` } as React.CSSProperties}
        onChange={(e) => onChange(Number(e.target.value) + activeUnit)}
      />
      <div className="jalpi-slider-dim__row">
        <input
          type="number" className="jalpi-slider-dim__num"
          value={isEmpty ? '' : numVal}
          placeholder={placeholder}
          min={min}
          onChange={(e) => {
            const n = parseFloat(e.target.value);
            if (isNaN(n) || e.target.value === '') { onChange(''); return; }
            onChange(n + activeUnit);
          }}
        />
        <button type="button" className="jalpi-slider-dim__unit" onClick={cycleUnit}>{activeUnit}</button>
      </div>
    </div>
  );
}

function ContainerContent({ node, store }: { node: CBNode; store: EditorStore }) {
  const { state } = store;
  const bp         = state.breakpoint;
  const settings   = (node.settings ?? {}) as Record<string, unknown>;
  const styles     = (node.styles ?? {}) as Record<string, string>;

  const upSettings = (key: string, val: unknown) =>
    store.updateNode(node.id, { settings: { ...settings, [key]: val } });
  const upStyle = (updates: Record<string, string>) =>
    store.updateNodeStyles(node.id, updates);

  // Responsive: Width and Height stored per-breakpoint
  const respKey: 'tabletStyles' | 'mobileStyles' | null =
    bp === 'tablet' ? 'tabletStyles' : bp === 'mobile' ? 'mobileStyles' : null;
  const respStyles = (respKey ? (node[respKey] ?? {}) : {}) as Record<string, string>;
  const updateResp = (updates: Record<string, string>) =>
    store.updateNode(node.id, { [respKey!]: { ...respStyles, ...updates } });

  const widthVal = respKey
    ? (respStyles.maxWidth || (settings.boxedWidth as string) || '1200px')
    : ((settings.boxedWidth as string) || '1200px');
  const heightVal = respKey
    ? (respStyles.height || styles.height || '')
    : (styles.height || '');

  const contentWidth = (settings.contentWidth as string) || 'full';
  const layoutMode   = (settings.layoutMode   as string) || 'block';

  const children   = node.children ?? [];
  const hasColumns = children.length > 0 && children.every((c) => c.type === 'column');
  const columnCount = hasColumns ? children.length : 1;

  // ── Flex state ──────────────────────────────────────────
  const flexDir    = (settings.flexDirection as string) || 'row';
  const flexWrapM  = (settings.flexWrapMode  as string) || 'nowrap';
  const flexJust   = (settings.flexJustify   as string) || 'flex-start';
  const flexAlign  = (settings.flexAlign     as string) || 'stretch';
  const flexGap    = (settings.flexGap       as string) || '0';

  // ── Grid state ──────────────────────────────────────────
  const gridAutoMode    = (settings.gridAutoMode    as string) || 'none';
  const gridColumns     = (settings.gridColumns     as string) || '3';
  const gridRows        = (settings.gridRows        as string) || '';
  const gridGap         = (settings.gridGap         as string) || '20px';
  const gridMinColSize  = (settings.gridMinColSize  as string) || '200px';

  const FLEX_DIR_OPTS = [
    { val: 'row',            title: 'Row →',     icon: <svg width="14" height="14" viewBox="0 0 20 20" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round"><rect x="2" y="6" width="5" height="8" rx="1"/><rect x="8" y="6" width="5" height="8" rx="1" opacity=".5"/><rect x="14" y="6" width="4" height="8" rx="1" opacity=".3"/><path d="M2 10h16M16 8l2 2-2 2"/></svg> },
    { val: 'column',         title: 'Column ↓',  icon: <svg width="14" height="14" viewBox="0 0 20 20" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round"><rect x="5" y="2" width="10" height="4" rx="1"/><rect x="5" y="8" width="10" height="4" rx="1" opacity=".5"/><rect x="5" y="14" width="10" height="4" rx="1" opacity=".3"/><path d="M10 2v16M8 16l2 2 2-2"/></svg> },
    { val: 'row-reverse',    title: 'Row ←',     icon: <svg width="14" height="14" viewBox="0 0 20 20" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round"><rect x="2" y="6" width="4" height="8" rx="1" opacity=".3"/><rect x="7" y="6" width="5" height="8" rx="1" opacity=".5"/><rect x="13" y="6" width="5" height="8" rx="1"/><path d="M18 10H2M4 8 2 10l2 2"/></svg> },
    { val: 'column-reverse', title: 'Column ↑',  icon: <svg width="14" height="14" viewBox="0 0 20 20" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round"><rect x="5" y="2" width="10" height="4" rx="1" opacity=".3"/><rect x="5" y="8" width="10" height="4" rx="1" opacity=".5"/><rect x="5" y="14" width="10" height="4" rx="1"/><path d="M10 18V2M8 4l2-2 2 2"/></svg> },
  ];

  const FLEX_WRAP_OPTS = [
    { val: 'nowrap',       title: 'No Wrap',      icon: <svg width="14" height="14" viewBox="0 0 20 20" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round"><rect x="2" y="7" width="4" height="6" rx="1"/><rect x="8" y="7" width="4" height="6" rx="1"/><rect x="14" y="7" width="4" height="6" rx="1"/></svg> },
    { val: 'wrap',         title: 'Wrap',          icon: <svg width="14" height="14" viewBox="0 0 20 20" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round"><rect x="2" y="3" width="4" height="5" rx="1"/><rect x="8" y="3" width="4" height="5" rx="1"/><rect x="14" y="3" width="4" height="5" rx="1"/><rect x="2" y="11" width="4" height="5" rx="1"/><rect x="8" y="11" width="4" height="5" rx="1"/></svg> },
    { val: 'wrap-reverse', title: 'Wrap Reverse',  icon: <svg width="14" height="14" viewBox="0 0 20 20" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round"><rect x="2" y="11" width="4" height="5" rx="1"/><rect x="8" y="11" width="4" height="5" rx="1"/><rect x="14" y="11" width="4" height="5" rx="1"/><rect x="2" y="3" width="4" height="5" rx="1"/><rect x="8" y="3" width="4" height="5" rx="1"/></svg> },
  ];

  const JUSTIFY_OPTS = [
    { val: 'flex-start',    title: 'Start',         icon: <svg width="14" height="14" viewBox="0 0 20 20" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round"><line x1="2" y1="2" x2="2" y2="18"/><rect x="4" y="6" width="4" height="8" rx="1"/><rect x="10" y="6" width="4" height="8" rx="1"/></svg> },
    { val: 'center',        title: 'Center',        icon: <svg width="14" height="14" viewBox="0 0 20 20" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round"><line x1="10" y1="2" x2="10" y2="18"/><rect x="3" y="6" width="4" height="8" rx="1"/><rect x="13" y="6" width="4" height="8" rx="1"/></svg> },
    { val: 'flex-end',      title: 'End',           icon: <svg width="14" height="14" viewBox="0 0 20 20" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round"><line x1="18" y1="2" x2="18" y2="18"/><rect x="8" y="6" width="4" height="8" rx="1"/><rect x="14" y="6" width="4" height="8" rx="1"/></svg> },
    { val: 'space-between', title: 'Space Between', icon: <svg width="14" height="14" viewBox="0 0 20 20" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round"><line x1="2" y1="2" x2="2" y2="18"/><line x1="18" y1="2" x2="18" y2="18"/><rect x="4" y="6" width="4" height="8" rx="1"/><rect x="12" y="6" width="4" height="8" rx="1"/></svg> },
    { val: 'space-around',  title: 'Space Around',  icon: <svg width="14" height="14" viewBox="0 0 20 20" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round"><rect x="2" y="6" width="4" height="8" rx="1"/><rect x="8" y="6" width="4" height="8" rx="1"/><rect x="14" y="6" width="4" height="8" rx="1"/></svg> },
    { val: 'space-evenly',  title: 'Space Evenly',  icon: <svg width="14" height="14" viewBox="0 0 20 20" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round"><line x1="2" y1="10" x2="18" y2="10" opacity=".3"/><rect x="3" y="6" width="4" height="8" rx="1"/><rect x="8.5" y="6" width="3" height="8" rx="1" opacity=".6"/><rect x="13" y="6" width="4" height="8" rx="1"/></svg> },
  ];

  const ALIGN_OPTS = [
    { val: 'flex-start', title: 'Start',   icon: <svg width="14" height="14" viewBox="0 0 20 20" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round"><line x1="2" y1="2" x2="18" y2="2"/><rect x="4" y="4" width="5" height="12" rx="1"/><rect x="11" y="4" width="5" height="8" rx="1" opacity=".5"/></svg> },
    { val: 'center',     title: 'Center',  icon: <svg width="14" height="14" viewBox="0 0 20 20" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round"><line x1="2" y1="10" x2="18" y2="10"/><rect x="4" y="3" width="5" height="14" rx="1"/><rect x="11" y="6" width="5" height="8" rx="1" opacity=".5"/></svg> },
    { val: 'flex-end',   title: 'End',     icon: <svg width="14" height="14" viewBox="0 0 20 20" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round"><line x1="2" y1="18" x2="18" y2="18"/><rect x="4" y="4" width="5" height="12" rx="1"/><rect x="11" y="8" width="5" height="8" rx="1" opacity=".5"/></svg> },
    { val: 'stretch',    title: 'Stretch', icon: <svg width="14" height="14" viewBox="0 0 20 20" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round"><line x1="2" y1="2" x2="18" y2="2"/><line x1="2" y1="18" x2="18" y2="18"/><rect x="4" y="4" width="5" height="12" rx="1"/><rect x="11" y="4" width="5" height="12" rx="1" opacity=".5"/></svg> },
    { val: 'baseline',   title: 'Baseline',icon: <svg width="14" height="14" viewBox="0 0 20 20" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round"><line x1="2" y1="15" x2="18" y2="15" strokeDasharray="2 1"/><rect x="4" y="4" width="5" height="11" rx="1"/><rect x="11" y="8" width="5" height="7" rx="1" opacity=".5"/></svg> },
  ];

  return (
    <div className="jalpi-props-section">

      {/* ── Container frame ── */}
      <PropGroup label="Container">
        <PropRow label="Content Width" stack>
          <div className="jalpi-toggle-group">
            <button className={`jalpi-toggle-group__btn${contentWidth === 'full' ? ' is-active' : ''}`}
              onClick={() => upSettings('contentWidth', 'full')}>Full</button>
            <button className={`jalpi-toggle-group__btn${contentWidth === 'boxed' ? ' is-active' : ''}`}
              onClick={() => upSettings('contentWidth', 'boxed')}>Boxed</button>
          </div>
        </PropRow>
        {contentWidth === 'boxed' && (
          <PropRow label="Max Width" stack>
            <SliderDimInput value={widthVal}
              onChange={(v) => respKey ? updateResp({ maxWidth: v }) : upSettings('boxedWidth', v)}
              max={2560} step={10} units={['px', '%', 'vw', 'rem']} placeholder="1200" />
          </PropRow>
        )}
        <PropRow label="Height" stack>
          <SliderDimInput value={heightVal}
            onChange={(v) => respKey ? updateResp({ height: v }) : upStyle({ height: v })}
            max={1200} step={10} units={['px', '%', 'vh']} placeholder="auto" />
        </PropRow>
      </PropGroup>

      {/* ── Layout Mode ── */}
      <PropGroup label="Layout Mode">
        <PropRow label="Type" stack>
          <div className="jalpi-toggle-group">
            <button className={`jalpi-toggle-group__btn${layoutMode === 'block' ? ' is-active' : ''}`}
              onClick={() => upSettings('layoutMode', 'block')}>Block</button>
            <button className={`jalpi-toggle-group__btn${layoutMode === 'flex' ? ' is-active' : ''}`}
              onClick={() => upSettings('layoutMode', 'flex')}>Flex</button>
            <button className={`jalpi-toggle-group__btn${layoutMode === 'grid' ? ' is-active' : ''}`}
              onClick={() => upSettings('layoutMode', 'grid')}>Grid</button>
          </div>
        </PropRow>
      </PropGroup>

      {/* ── Flexbox controls ── */}
      {layoutMode === 'flex' && (
        <PropGroup label="Flexbox">
          <PropRow label="Direction" stack>
            <div className="jalpi-icon-btn-group">
              {FLEX_DIR_OPTS.map((o) => (
                <button key={o.val} type="button"
                  className={`jalpi-icon-btn${flexDir === o.val ? ' is-active' : ''}`}
                  title={o.title} onClick={() => upSettings('flexDirection', o.val)}>{o.icon}</button>
              ))}
            </div>
          </PropRow>
          <PropRow label="Wrap" stack>
            <div className="jalpi-icon-btn-group">
              {FLEX_WRAP_OPTS.map((o) => (
                <button key={o.val} type="button"
                  className={`jalpi-icon-btn${flexWrapM === o.val ? ' is-active' : ''}`}
                  title={o.title} onClick={() => upSettings('flexWrapMode', o.val)}>{o.icon}</button>
              ))}
            </div>
          </PropRow>
          <PropRow label="Justify" stack>
            <div className="jalpi-icon-btn-group jalpi-icon-btn-group--6">
              {JUSTIFY_OPTS.map((o) => (
                <button key={o.val} type="button"
                  className={`jalpi-icon-btn${flexJust === o.val ? ' is-active' : ''}`}
                  title={o.title} onClick={() => upSettings('flexJustify', o.val)}>{o.icon}</button>
              ))}
            </div>
          </PropRow>
          <PropRow label="Align" stack>
            <div className="jalpi-icon-btn-group">
              {ALIGN_OPTS.map((o) => (
                <button key={o.val} type="button"
                  className={`jalpi-icon-btn${flexAlign === o.val ? ' is-active' : ''}`}
                  title={o.title} onClick={() => upSettings('flexAlign', o.val)}>{o.icon}</button>
              ))}
            </div>
          </PropRow>
          <PropRow label="Gap" stack>
            <SliderDimInput value={flexGap} onChange={(v) => upSettings('flexGap', v)}
              max={200} step={4} units={['px', 'em', 'rem', '%']} placeholder="0" />
          </PropRow>
        </PropGroup>
      )}

      {/* ── CSS Grid controls ── */}
      {layoutMode === 'grid' && (
        <PropGroup label="CSS Grid">
          <PropRow label="Auto Mode" stack>
            <div className="jalpi-toggle-group">
              {(['none', 'fill', 'fit'] as const).map((m) => (
                <button key={m} className={`jalpi-toggle-group__btn${gridAutoMode === m ? ' is-active' : ''}`}
                  onClick={() => upSettings('gridAutoMode', m)}>
                  {m === 'none' ? 'Fixed' : m === 'fill' ? 'Auto Fill' : 'Auto Fit'}
                </button>
              ))}
            </div>
          </PropRow>
          {gridAutoMode === 'none' ? (
            <PropRow label="Columns" stack>
              <div className="jalpi-toggle-group">
                {['1','2','3','4','5','6'].map((n) => (
                  <button key={n} className={`jalpi-toggle-group__btn${gridColumns === n ? ' is-active' : ''}`}
                    onClick={() => upSettings('gridColumns', n)}>{n}</button>
                ))}
              </div>
            </PropRow>
          ) : (
            <PropRow label="Min Col Size" stack>
              <SliderDimInput value={gridMinColSize}
                onChange={(v) => upSettings('gridMinColSize', v)}
                min={60} max={600} step={10} units={['px', 'em', 'rem']} placeholder="200" />
            </PropRow>
          )}
          <PropRow label="Rows" stack>
            <div className="jalpi-toggle-group">
              <button className={`jalpi-toggle-group__btn${!gridRows || gridRows === 'auto' ? ' is-active' : ''}`}
                onClick={() => upSettings('gridRows', 'auto')}>Auto</button>
              {['2','3','4'].map((n) => (
                <button key={n} className={`jalpi-toggle-group__btn${gridRows === n ? ' is-active' : ''}`}
                  onClick={() => upSettings('gridRows', n)}>{n}</button>
              ))}
            </div>
          </PropRow>
          <PropRow label="Gap" stack>
            <SliderDimInput value={gridGap} onChange={(v) => upSettings('gridGap', v)}
              max={200} step={4} units={['px', 'em', 'rem', '%']} placeholder="20" />
          </PropRow>
        </PropGroup>
      )}

      {/* ── Block mode: column children ── */}
      {layoutMode === 'block' && (
        <PropGroup label="Columns">
          <PropRow label="Layout" stack>
            <div className="jalpi-toggle-group">
              <button className={`jalpi-toggle-group__btn${!hasColumns ? ' is-active' : ''}`}
                onClick={() => store.setContainerColumns(node.id, 1)}>Block</button>
              {[2, 3, 4].map((n) => (
                <button key={n}
                  className={`jalpi-toggle-group__btn${columnCount === n ? ' is-active' : ''}`}
                  onClick={() => store.setContainerColumns(node.id, n)}>{n}</button>
              ))}
            </div>
          </PropRow>
          {hasColumns && (
            <>
              <PropRow label="Gap" stack>
                <SliderDimInput value={(settings.columnGap as string) || '20px'}
                  onChange={(v) => upSettings('columnGap', v)} max={200} step={4}
                  units={['px', 'em', '%']} placeholder="20" />
              </PropRow>
              {children.map((col, i) => {
                const cw = ((col.settings ?? {}) as Record<string, unknown>).width as string
                  || `${Math.round(100 / columnCount)}%`;
                return (
                  <PropRow key={col.id} label={`Col ${i + 1}`} stack>
                    <SliderDimInput value={cw}
                      onChange={(v) => store.updateNode(col.id, {
                        settings: { ...(col.settings ?? {}), width: v },
                      })}
                      min={5} max={100} step={1} units={['%', 'px']} placeholder="50%" />
                  </PropRow>
                );
              })}
            </>
          )}
        </PropGroup>
      )}
    </div>
  );
}

/* — Column — */
function ColumnContent({ node, store }: { node: CBNode; store: EditorStore }) {
  const settings   = (node.settings ?? {}) as Record<string, unknown>;

  const upSettings = (key: string, val: unknown) =>
    store.updateNode(node.id, { settings: { ...settings, [key]: val } });

  const ALIGN_OPTS = [
    {
      val: 'flex-start', title: 'Top',
      icon: <svg width="14" height="14" viewBox="0 0 20 20" fill="currentColor"><rect x="3" y="2" width="14" height="1.5" rx=".75" opacity=".5"/><rect x="5" y="4" width="4" height="7" rx="1"/><rect x="11" y="4" width="4" height="5" rx="1" opacity=".6"/></svg>,
    },
    {
      val: 'center', title: 'Center',
      icon: <svg width="14" height="14" viewBox="0 0 20 20" fill="currentColor"><rect x="3" y="9.25" width="14" height="1.5" rx=".75" opacity=".5"/><rect x="5" y="3" width="4" height="14" rx="1"/><rect x="11" y="6" width="4" height="8" rx="1" opacity=".6"/></svg>,
    },
    {
      val: 'flex-end', title: 'Bottom',
      icon: <svg width="14" height="14" viewBox="0 0 20 20" fill="currentColor"><rect x="5" y="9" width="4" height="7" rx="1"/><rect x="11" y="11" width="4" height="5" rx="1" opacity=".6"/><rect x="3" y="17.5" width="14" height="1.5" rx=".75" opacity=".5"/></svg>,
    },
    {
      val: 'stretch', title: 'Stretch',
      icon: <svg width="14" height="14" viewBox="0 0 20 20" fill="currentColor"><rect x="3" y="2" width="14" height="1.5" rx=".75" opacity=".5"/><rect x="3" y="16.5" width="14" height="1.5" rx=".75" opacity=".5"/><rect x="5" y="4" width="4" height="12" rx="1"/><rect x="11" y="4" width="4" height="12" rx="1" opacity=".6"/></svg>,
    },
  ];

  return (
    <div className="jalpi-props-section">
      <PropGroup label="Column">
        <PropRow label="Width" stack>
          <SliderDimInput
            value={(settings.width as string) || '50%'}
            onChange={(v) => upSettings('width', v)}
            min={5} max={100} step={1}
            units={['%', 'px']}
            placeholder="50%"
          />
        </PropRow>
        <PropRow label="Align Self" stack>
          <div className="jalpi-icon-btn-group">
            {ALIGN_OPTS.map((o) => (
              <button key={o.val} type="button"
                className={`jalpi-icon-btn${settings.alignSelf === o.val ? ' is-active' : ''}`}
                title={o.title}
                onClick={() => upSettings('alignSelf', settings.alignSelf === o.val ? '' : o.val)}
              >{o.icon}</button>
            ))}
          </div>
        </PropRow>
      </PropGroup>
    </div>
  );
}

/* — Heading — */
function HeadingContent({ node, store }: { node: CBNode; store: EditorStore }) {
  const content  = (node.content ?? {}) as { text?: string; tag?: string };
  const textaRef = useRef<HTMLTextAreaElement>(null);
  const update   = (key: string, val: string) =>
    store.updateNodeContent(node.id, { [key]: val });

  const insertTag = (tag: string) => {
    const el = textaRef.current;
    if (!el) {
      update('text', (content.text || '') + tag);
      return;
    }
    const start = el.selectionStart ?? (content.text || '').length;
    const end   = el.selectionEnd   ?? start;
    const cur   = content.text || '';
    update('text', cur.slice(0, start) + tag + cur.slice(end));
    requestAnimationFrame(() => {
      el.focus();
      const pos = start + tag.length;
      el.setSelectionRange(pos, pos);
    });
  };

  return (
    <div className="jalpi-props-section">
      <PropGroup label="Heading">
        <PropRow label="Tag">
          <div className="jalpi-toggle-group">
            {['h1','h2','h3','h4','h5','h6'].map((t) => (
              <button
                key={t}
                className={`jalpi-toggle-group__btn${(content.tag || 'h2') === t ? ' is-active' : ''}`}
                onClick={() => update('tag', t)}
              >
                {t.toUpperCase()}
              </button>
            ))}
          </div>
        </PropRow>
        <PropRow label="Text" stack>
          <div className="jalpi-field-with-tag">
            <textarea
              ref={textaRef}
              className="jalpi-textarea"
              rows={3}
              value={content.text || ''}
              onChange={(e) => update('text', e.target.value)}
              placeholder="Enter heading text…"
            />
            <DynamicTagPicker postId={jalpiEditorConfig.postId} onInsert={insertTag} />
          </div>
        </PropRow>
      </PropGroup>
    </div>
  );
}

/* ── Rich Text Editor (used by TextContent panel) ─────────────────────────── */

function RteBtn({ label, title, onMouseDown }: {
  label: React.ReactNode;
  title: string;
  onMouseDown: (e: React.MouseEvent) => void;
}) {
  return (
    <button type="button" className="jalpi-rte-btn" title={title} onMouseDown={onMouseDown}>
      {label}
    </button>
  );
}
function RteSep() { return <span className="jalpi-rte-sep" />; }

const RTE_ICONS = {
  bulletList: (<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round"><line x1="9" y1="6" x2="20" y2="6"/><line x1="9" y1="12" x2="20" y2="12"/><line x1="9" y1="18" x2="20" 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>),
  numberedList: (<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round"><line x1="10" y1="6" x2="21" y2="6"/><line x1="10" y1="12" x2="21" y2="12"/><line x1="10" y1="18" x2="21" y2="18"/><path d="M4 6h1V10M4 10h2M4 14h1.5a1 1 0 0 1 0 2H4.5a1 1 0 0 1 0 2H6" stroke="currentColor" strokeWidth="1.5"/></svg>),
  link: (<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></svg>),
  quote: (<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor"><path d="M3 21c3 0 7-1 7-8V5c0-1.25-.756-2.017-2-2H4c-1.25 0-2 .75-2 1.972V11c0 1.25.75 2 2 2 1 0 1 0 1 1v1c0 1-1 2-2 2s-1 .008-1 1.031V20c0 1 0 1 1 1z"/><path d="M15 21c3 0 7-1 7-8V5c0-1.25-.757-2.017-2-2h-4c-1.25 0-2 .75-2 1.972V11c0 1.25.75 2 2 2h.75c0 2.25.25 4-2.75 4v3c0 1 0 1 1 1z"/></svg>),
  alignLeft:    (<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round"><line x1="3" y1="6" x2="21" y2="6"/><line x1="3" y1="10" x2="15" y2="10"/><line x1="3" y1="14" x2="21" y2="14"/><line x1="3" y1="18" x2="15" y2="18"/></svg>),
  alignCenter:  (<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round"><line x1="3" y1="6" x2="21" y2="6"/><line x1="6" y1="10" x2="18" y2="10"/><line x1="3" y1="14" x2="21" y2="14"/><line x1="6" y1="18" x2="18" y2="18"/></svg>),
  alignRight:   (<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round"><line x1="3" y1="6" x2="21" y2="6"/><line x1="9" y1="10" x2="21" y2="10"/><line x1="3" y1="14" x2="21" y2="14"/><line x1="9" y1="18" x2="21" y2="18"/></svg>),
  alignJustify: (<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round"><line x1="3" y1="6" x2="21" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/><line x1="3" y1="14" x2="21" y2="14"/><line x1="3" y1="18" x2="21" y2="18"/></svg>),
  indent:   (<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><line x1="3" y1="6" x2="21" y2="6"/><line x1="3" y1="18" x2="21" y2="18"/><polyline points="7 8 11 12 7 16"/><line x1="11" y1="12" x2="21" y2="12"/></svg>),
  outdent:  (<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><line x1="3" y1="6" x2="21" y2="6"/><line x1="3" y1="18" x2="21" y2="18"/><polyline points="11 8 7 12 11 16"/><line x1="7" y1="12" x2="21" y2="12"/></svg>),
};

/* — Text — */
function TextContent({ node, store }: { node: CBNode; store: EditorStore }) {
  const editorRef = useRef<HTMLDivElement>(null);
  const [isSource,   setIsSource]   = useState(false);
  const [sourceHtml, setSourceHtml] = useState('');

  const content = (node.content ?? {}) as { html?: string };
  const html    = content.html || '';

  // Re-populate the visual editor when the node changes OR when switching back to visual mode
  useEffect(() => {
    if (!isSource && editorRef.current) {
      editorRef.current.innerHTML = html;
    }
  // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [node.id, isSource]);

  const saveContent = () => {
    if (editorRef.current) {
      const raw = editorRef.current.innerHTML;
      store.updateNodeContent(node.id, { html: raw === '<br>' ? '' : raw });
    }
  };

  // execCommand does NOT always fire onInput, so we save explicitly after every command
  const exec = (cmd: string, val?: string) => (e: React.MouseEvent) => {
    e.preventDefault();
    editorRef.current?.focus();
    document.execCommand(cmd, false, val ?? undefined);
    requestAnimationFrame(saveContent);
  };

  const insertLink = (e: React.MouseEvent) => {
    e.preventDefault();
    // eslint-disable-next-line no-alert
    const url = window.prompt('Enter URL:', 'https://');
    if (url) {
      editorRef.current?.focus();
      document.execCommand('createLink', false, url);
      requestAnimationFrame(saveContent);
    }
  };

  return (
    <div className="jalpi-props-section">
      <PropGroup label="Text Content">
        <div className="jalpi-rich-editor">

          {/* ── Toolbar ──────────────────────────────────────── */}
          <div className="jalpi-rte-toolbar">

            {/* Row 1 */}
            <div className="jalpi-rte-row">
              <select
                className="jalpi-rte-format"
                defaultValue="p"
                onMouseDown={(e) => e.stopPropagation()}
                onChange={(e) => {
                  editorRef.current?.focus();
                  document.execCommand('formatBlock', false, e.target.value);
                  requestAnimationFrame(saveContent);
                }}
              >
                <option value="p">Paragraph</option>
                <option value="h1">H1</option>
                <option value="h2">H2</option>
                <option value="h3">H3</option>
                <option value="h4">H4</option>
              </select>

              <RteBtn title="Bold"          label={<b>B</b>}              onMouseDown={exec('bold')} />
              <RteBtn title="Italic"        label={<i>I</i>}              onMouseDown={exec('italic')} />
              <RteBtn title="Bullet list"   label={RTE_ICONS.bulletList}  onMouseDown={exec('insertUnorderedList')} />
              <RteBtn title="Numbered list" label={RTE_ICONS.numberedList} onMouseDown={exec('insertOrderedList')} />
              <RteBtn title="Insert link"   label={RTE_ICONS.link}        onMouseDown={insertLink} />
              <RteBtn title="Blockquote"    label={RTE_ICONS.quote}       onMouseDown={exec('formatBlock', 'blockquote')} />
              <RteSep />
              <RteBtn title="Align left"    label={RTE_ICONS.alignLeft}    onMouseDown={exec('justifyLeft')} />
              <RteBtn title="Align center"  label={RTE_ICONS.alignCenter}  onMouseDown={exec('justifyCenter')} />
              <RteBtn title="Align right"   label={RTE_ICONS.alignRight}   onMouseDown={exec('justifyRight')} />
              <RteBtn title="Justify"       label={RTE_ICONS.alignJustify} onMouseDown={exec('justifyFull')} />
              <RteSep />
              <RteBtn title="Strikethrough" label={<s>S</s>}              onMouseDown={exec('strikeThrough')} />
              <RteSep />
              <DynamicTagPicker
                postId={jalpiEditorConfig.postId}
                onInsert={(tag) => {
                  editorRef.current?.focus();
                  document.execCommand('insertText', false, tag);
                  requestAnimationFrame(saveContent);
                }}
              />

              {/* Visual / Text tabs */}
              <div className="jalpi-rte-tabs">
                <button
                  type="button"
                  className={`jalpi-rte-tab${!isSource ? ' is-active' : ''}`}
                  onMouseDown={(e) => {
                    e.preventDefault();
                    // Save source edits to store before switching to visual
                    store.updateNodeContent(node.id, { html: sourceHtml });
                    setIsSource(false);
                    // useEffect([node.id, isSource]) will re-init the contentEditable
                  }}
                >Visual</button>
                <button
                  type="button"
                  className={`jalpi-rte-tab${isSource ? ' is-active' : ''}`}
                  onMouseDown={(e) => {
                    e.preventDefault();
                    // Capture current visual HTML before unmounting the contentEditable
                    setSourceHtml(editorRef.current?.innerHTML || html);
                    setIsSource(true);
                  }}
                >Text</button>
              </div>
            </div>

            {/* Row 2 */}
            <div className="jalpi-rte-row jalpi-rte-row--2">
              <RteBtn title="Underline" label={<u>U</u>} onMouseDown={exec('underline')} />

              {/* Text colour */}
              <label className="jalpi-rte-btn jalpi-rte-color-lbl" title="Text color" onMouseDown={(e) => e.preventDefault()}>
                <span className="jalpi-rte-color-a">A</span>
                <input
                  type="color"
                  className="jalpi-rte-color-input"
                  defaultValue="#000000"
                  onMouseDown={(e) => e.stopPropagation()}
                  onChange={(e) => {
                    editorRef.current?.focus();
                    document.execCommand('foreColor', false, e.target.value);
                    requestAnimationFrame(saveContent);
                  }}
                />
              </label>

              <RteSep />
              <RteBtn title="Indent"  label={RTE_ICONS.indent}  onMouseDown={exec('indent')} />
              <RteBtn title="Outdent" label={RTE_ICONS.outdent} onMouseDown={exec('outdent')} />
              <RteSep />
              <RteBtn title="Undo" label="↩" onMouseDown={exec('undo')} />
              <RteBtn title="Redo" label="↪" onMouseDown={exec('redo')} />
            </div>
          </div>

          {/* ── Content area ─────────────────────────────────── */}
          {isSource ? (
            <textarea
              className="jalpi-rte-source"
              value={sourceHtml}
              onChange={(e) => {
                setSourceHtml(e.target.value);
                store.updateNodeContent(node.id, { html: e.target.value });
              }}
              autoFocus
            />
          ) : (
            <div
              ref={editorRef}
              contentEditable
              suppressContentEditableWarning
              className="jalpi-rte-content"
              onInput={saveContent}
            />
          )}
        </div>
      </PropGroup>
    </div>
  );
}

const IMAGE_SIZE_OPTIONS = [
  { value: '',             label: '— Full Size —' },
  { value: 'thumbnail',    label: 'Thumbnail  ·  150 × 150' },
  { value: 'medium',       label: 'Medium  ·  300 × 300' },
  { value: 'medium_large', label: 'Medium Large  ·  768px wide' },
  { value: 'large',        label: 'Large  ·  1024 × 1024' },
  { value: 'custom',       label: 'Custom' },
];

/* — Image — */
function ImageContent({ node, store }: { node: CBNode; store: EditorStore }) {
  const content = (node.content ?? {}) as {
    url?: string; alt?: string; caption?: string; linkUrl?: string; newTab?: boolean;
    width?: string; height?: string; imageSize?: string;
  };
  const update     = (key: string, val: unknown) =>
    store.updateNodeContent(node.id, { [key]: val });
  const imageSize  = content.imageSize || '';
  const isCustom   = imageSize === 'custom';

  return (
    <div className="jalpi-props-section">
      <PropGroup label="Image">
        <MediaPickerInput value={content.url || ''} onChange={(v) => update('url', v)} />
        <PropRow label="Caption" stack>
          <TextInput value={content.caption || ''} onChange={(v) => update('caption', v)} placeholder="Image caption…" />
        </PropRow>
        <PropRow label="Image Size" stack>
          <Select
            value={imageSize}
            options={IMAGE_SIZE_OPTIONS}
            onChange={(v) => update('imageSize', v)}
          />
        </PropRow>
        {isCustom && (
          <>
            <PropRow label="Width" stack>
              <DimensionInput value={content.width || ''} onChange={(v) => update('width', v)} placeholder="auto" units={['px', '%', 'vw']} />
            </PropRow>
            <PropRow label="Height" stack>
              <DimensionInput value={content.height || ''} onChange={(v) => update('height', v)} placeholder="auto" units={['px', '%', 'vh']} />
            </PropRow>
          </>
        )}
      </PropGroup>

      <PropGroup label="Link" defaultOpen={false}>
        <PropRow label="URL" stack>
          <TextInput value={content.linkUrl || ''} onChange={(v) => update('linkUrl', v)} placeholder="https://…" />
        </PropRow>
        <PropRow label="New Tab">
          <Toggle checked={!!content.newTab} onChange={(v) => update('newTab', v)} />
        </PropRow>
      </PropGroup>
    </div>
  );
}

/* — Button — */
const BUTTON_TYPES = [
  { value: 'default', label: 'Default' },
  { value: 'info',    label: 'Info'    },
  { value: 'success', label: 'Success' },
  { value: 'warning', label: 'Warning' },
  { value: 'danger',  label: 'Danger'  },
];

function ButtonIconPicker({
  value,
  onChange,
}: {
  value:    string | IconValue;
  onChange: (v: string | IconValue) => void;
}) {
  return (
    <IconInput
      value={value}
      onChange={(v) => onChange(v)}
      placeholder="No icon"
    />
  );
}

function ButtonContent({ node, store }: { node: CBNode; store: EditorStore }) {
  const content  = (node.content ?? {}) as {
    text?:         string;
    url?:          string;
    newTab?:       boolean;
    buttonType?:   string;
    icon?:         string | IconValue;
    iconPosition?: string;
    iconSpacing?:  number;
    buttonId?:     string;
  };
  const labelRef = useRef<HTMLInputElement>(null);
  const update   = (key: string, val: unknown) =>
    store.updateNodeContent(node.id, { [key]: val });

  const insertIntoLabel = (tag: string) => {
    const el = labelRef.current;
    if (!el) { update('text', (content.text || '') + tag); return; }
    const start = el.selectionStart ?? (content.text || '').length;
    const end   = el.selectionEnd   ?? start;
    const cur   = content.text || '';
    update('text', cur.slice(0, start) + tag + cur.slice(end));
    requestAnimationFrame(() => { el.focus(); const p = start + tag.length; el.setSelectionRange(p, p); });
  };

  const hasIcon = !!content.icon;
  const isBefore = !content.iconPosition || content.iconPosition === 'before';

  return (
    <div className="jalpi-props-section">
      <PropGroup label="Button">

        {/* Type */}
        <PropRow label="Type">
          <Select
            value={content.buttonType || 'default'}
            options={BUTTON_TYPES}
            onChange={(v) => update('buttonType', v)}
          />
        </PropRow>

        {/* Text — label has dyntag badge, input + picker inline */}
        <div className="jalpi-prop-row jalpi-btn-text-row">
          <span className="jalpi-prop-row__label">
            Text <span className="jalpi-dyntag-badge">✦</span>
          </span>
          <div className="jalpi-prop-row__control jalpi-ctrl-row">
            <TextInput inputRef={labelRef} value={content.text || ''} onChange={(v) => update('text', v)} placeholder="Click here" />
          </div>
        </div>

        {/* Link — stacked, URL + gear + dyntag, new-tab checkbox */}
        <div className="jalpi-prop-row jalpi-prop-row--stack jalpi-link-field">
          <span className="jalpi-prop-row__label">Link</span>
          <div className="jalpi-ctrl-row">
            <TextInput value={content.url || '#'} onChange={(v) => update('url', v)} placeholder="https://…" />
          </div>
          <label className="jalpi-link-newtab">
            <input type="checkbox" checked={!!content.newTab} onChange={(e) => update('newTab', e.target.checked)} />
            Open in new tab
          </label>
        </div>

        {/* Icon */}
        <PropRow label="Icon">
          <ButtonIconPicker value={content.icon || ''} onChange={(v) => update('icon', v)} />
        </PropRow>

        {/* Icon Position — toggle buttons (only when icon is set) */}
        {hasIcon && (
          <PropRow label="Icon Position">
            <div className="jalpi-toggle-group">
              <button
                type="button"
                className={`jalpi-toggle-btn${isBefore ? ' is-active' : ''}`}
                title="Before text"
                onClick={() => update('iconPosition', 'before')}
              >
                <svg width="14" height="14" viewBox="0 0 20 14" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round">
                  <line x1="1" y1="1" x2="1" y2="13"/>
                  <line x1="4" y1="7" x2="16" y2="7"/>
                  <polyline points="11,3 16,7 11,11"/>
                </svg>
              </button>
              <button
                type="button"
                className={`jalpi-toggle-btn${!isBefore ? ' is-active' : ''}`}
                title="After text"
                onClick={() => update('iconPosition', 'after')}
              >
                <svg width="14" height="14" viewBox="0 0 20 14" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round">
                  <line x1="19" y1="1" x2="19" y2="13"/>
                  <line x1="4" y1="7" x2="16" y2="7"/>
                  <polyline points="9,3 4,7 9,11"/>
                </svg>
              </button>
            </div>
          </PropRow>
        )}

        {/* Icon Spacing — slider + px number (only when icon is set) */}
        {hasIcon && (
          <PropRow label="Icon Spacing">
            <div className="jalpi-slider-row">
              <input
                type="range"
                className="jalpi-range"
                min="0" max="40" step="1"
                value={content.iconSpacing ?? 6}
                onChange={(e) => update('iconSpacing', Number(e.target.value))}
              />
              <div className="jalpi-dim-wrap">
                <input
                  type="number"
                  className="jalpi-input jalpi-dim-num"
                  min="0" max="100"
                  value={content.iconSpacing ?? 6}
                  onChange={(e) => update('iconSpacing', Number(e.target.value))}
                />
                <span className="jalpi-dim-unit">px</span>
              </div>
            </div>
          </PropRow>
        )}

        {/* Button ID */}
        <div className="jalpi-prop-row">
          <span className="jalpi-prop-row__label">Button ID</span>
          <div className="jalpi-prop-row__control jalpi-ctrl-row">
            <TextInput value={content.buttonId || ''} onChange={(v) => update('buttonId', v)} placeholder="my-btn" />
          </div>
        </div>
        <p className="jalpi-field-note jalpi-field-note--pad">Please make sure the ID is unique and not used elsewhere.</p>

      </PropGroup>
    </div>
  );
}

/* — Spacer — */
function SpacerContent({ node, store }: { node: CBNode; store: EditorStore }) {
  const settings = (node.settings ?? {}) as { height?: string };
  return (
    <div className="jalpi-props-section">
      <PropGroup label="Spacer">
        <SliderPropRow
          label="Space"
          responsive
          value={settings.height || ''}
          onChange={(v) => store.updateNode(node.id, { settings: { ...settings, height: v } })}
          min={0}
          max={500}
          step={1}
          units={['px', 'em', 'vh', '%']}
          placeholder="40"
        />
      </PropGroup>
    </div>
  );
}

/* — Divider — */
function DividerContent({ node, store }: { node: CBNode; store: EditorStore }) {
  const settings = (node.settings ?? {}) as { borderStyle?: string; width?: string; align?: string };
  const styles   = (node.styles   ?? {}) as Record<string, string>;

  // Prefer the explicit longhand values set by the Style tab; fall back to
  // parsing the borderTop shorthand so the two panels stay in sync.
  const lineColor = styles.borderTopColor || (() => {
    const m = (styles.borderTop ?? '').match(/#[0-9a-fA-F]{3,8}|rgba?\([^)]+\)/);
    return m ? m[0] : '#e5e7eb';
  })();
  const lineWidth = styles.borderTopWidth || (() => {
    const m = (styles.borderTop ?? '').match(/(\d+(?:\.\d+)?px)/);
    return m ? m[1] : '1px';
  })();

  const borderStyle = settings.borderStyle ?? 'solid';
  const width       = settings.width       ?? '100%';
  const align       = settings.align       ?? 'center';

  const update = (patch: { borderStyle?: string; width?: string; align?: string }) => {
    const merged = { ...settings, ...patch };
    const bStyle = merged.borderStyle ?? 'solid';
    const w      = merged.width       ?? '100%';
    const a      = merged.align       ?? 'center';
    const ml     = a === 'left'  ? '0'    : 'auto';
    const mr     = a === 'right' ? '0'    : 'auto';

    store.updateNode(node.id, {
      settings: { ...node.settings, ...merged },
      styles: {
        ...node.styles,
        border: 'none',
        borderTop: `${lineWidth} ${bStyle} ${lineColor}`,
        width: w,
        display: 'block',
        marginLeft:  ml,
        marginRight: mr,
      },
    });
  };

  return (
    <div className="jalpi-props-section">
      <PropGroup label="Divider">
        <PropRow label="Style">
          <Select
            value={borderStyle}
            options={[
              { value: 'solid',  label: 'Solid'  },
              { value: 'dashed', label: 'Dashed' },
              { value: 'dotted', label: 'Dotted' },
              { value: 'double', label: 'Double' },
            ]}
            onChange={(v) => update({ borderStyle: v })}
          />
        </PropRow>
        <PropRow label="Width" stack responsive>
          <SliderDimInput
            value={width}
            onChange={(v) => update({ width: v })}
            units={['%']}
            min={1}
            max={100}
            step={1}
          />
        </PropRow>
        <PropRow label="Alignment" responsive>
          <AlignButtons
            value={align}
            onChange={(v) => update({ align: v })}
            options={['left', 'center', 'right']}
          />
        </PropRow>
      </PropGroup>
    </div>
  );
}

/* — Video — */
function VideoContent({ node, store }: { node: CBNode; store: EditorStore }) {
  const content = (node.content ?? {}) as {
    source?: string; url?: string;
    startTime?: string; endTime?: string;
    autoplay?: boolean; muted?: boolean; loop?: boolean; controls?: boolean;
    captions?: boolean; privacyMode?: boolean; lazyLoad?: boolean;
    suggestedVideos?: string;
  };
  const upd = (key: string, val: unknown) => store.updateNodeContent(node.id, { [key]: val });

  // Auto-detect source from URL if not set
  const source = content.source || (
    /youtube\.com|youtu\.be/.test(content.url || '') ? 'youtube' :
    /vimeo\.com/.test(content.url || '')             ? 'vimeo'   : 'self'
  );
  const isYT    = source === 'youtube';
  const isVimeo = source === 'vimeo';

  return (
    <div className="jalpi-props-section">
      <PropGroup label="Video">
        <PropRow label="Source" stack>
          <Select
            value={source}
            options={[
              { value: 'youtube', label: 'YouTube'     },
              { value: 'vimeo',   label: 'Vimeo'       },
              { value: 'self',    label: 'Self Hosted'  },
            ]}
            onChange={(v) => upd('source', v)}
          />
        </PropRow>
        <PropRow label="Link" stack>
          <TextInput
            value={content.url || ''}
            onChange={(v) => upd('url', v)}
            placeholder={isYT ? 'https://www.youtube.com/watch?v=…' : isVimeo ? 'https://vimeo.com/…' : '.mp4 / .webm URL'}
          />
        </PropRow>
        {(isYT || isVimeo) && (
          <>
            <PropRow label="Start Time" stack>
              <TextInput value={content.startTime || ''} onChange={(v) => upd('startTime', v)} placeholder="0" />
              <span className="jalpi-prop-hint">Specify a start time (in seconds)</span>
            </PropRow>
            <PropRow label="End Time" stack>
              <TextInput value={content.endTime || ''} onChange={(v) => upd('endTime', v)} placeholder="" />
              <span className="jalpi-prop-hint">Specify an end time (in seconds)</span>
            </PropRow>
          </>
        )}
      </PropGroup>

      <PropGroup label="Video Options" defaultOpen={false}>
        <PropRow label="Autoplay">
          <Toggle checked={!!content.autoplay} onChange={(v) => upd('autoplay', v)} />
        </PropRow>
        <PropRow label="Mute">
          <Toggle checked={!!content.muted} onChange={(v) => upd('muted', v)} />
        </PropRow>
        <PropRow label="Loop">
          <Toggle checked={!!content.loop} onChange={(v) => upd('loop', v)} />
        </PropRow>
        <PropRow label="Player Controls">
          <Toggle checked={content.controls !== false} onChange={(v) => upd('controls', v)} />
        </PropRow>
        {isYT && (
          <PropRow label="Captions">
            <Toggle checked={!!content.captions} onChange={(v) => upd('captions', v)} />
          </PropRow>
        )}
        {(isYT || isVimeo) && (
          <PropRow label="Privacy Mode">
            <Toggle checked={!!content.privacyMode} onChange={(v) => upd('privacyMode', v)} />
          </PropRow>
        )}
        <PropRow label="Lazy Load">
          <Toggle checked={content.lazyLoad !== false} onChange={(v) => upd('lazyLoad', v)} />
        </PropRow>
        {isYT && (
          <PropRow label="Suggested Videos" stack>
            <Select
              value={content.suggestedVideos || 'current'}
              options={[
                { value: 'current', label: 'Current Video Channel' },
                { value: 'any',     label: 'Any Video'             },
              ]}
              onChange={(v) => upd('suggestedVideos', v)}
            />
          </PropRow>
        )}
      </PropGroup>
    </div>
  );
}

/* — Maps — */
function MapsContent({ node, store }: { node: CBNode; store: EditorStore }) {
  const content = (node.content ?? {}) as { address?: string; height?: string };
  const update  = (key: string, val: string) =>
    store.updateNodeContent(node.id, { [key]: val });

  return (
    <div className="jalpi-props-section">
      <PropGroup label="Google Maps">
        <PropRow label="Address">
          <textarea
            className="jalpi-textarea"
            rows={2}
            value={content.address || ''}
            onChange={(e) => update('address', e.target.value)}
            placeholder="e.g. Times Square, New York"
          />
        </PropRow>
        <PropRow label="Height">
          <DimensionInput
            value={content.height || '400px'}
            onChange={(v) => update('height', v)}
            placeholder="400px"
            units={['px', 'vh']}
          />
        </PropRow>
      </PropGroup>
    </div>
  );
}

/* — HTML — */
function HtmlContent({ node, store }: { node: CBNode; store: EditorStore }) {
  const content = (node.content ?? {}) as { html?: string };
  return (
    <div className="jalpi-props-section">
      <PropGroup label="HTML Code">
        <textarea
          className="jalpi-textarea"
          rows={10}
          value={content.html || ''}
          onChange={(e) => store.updateNodeContent(node.id, { html: e.target.value })}
          placeholder="<!-- Enter raw HTML here -->"
          style={{ fontFamily: 'monospace', fontSize: '11px', lineHeight: '1.6' }}
        />
      </PropGroup>
    </div>
  );
}

/* — Icon — */
function IconContent({ node, store }: { node: CBNode; store: EditorStore }) {
  const content = (node.content ?? {}) as {
    icon?: string | IconValue;
    view?: string; borderRadius?: string;
    link?: string; newTab?: boolean;
  };
  // Read style-tab values for the live preview (colours/size live in styles, not content)
  const st           = (node.styles ?? {}) as Record<string, string>;
  const update       = (key: string, val: unknown) => store.updateNodeContent(node.id, { [key]: val });
  const [pickerOpen, setPickerOpen] = useState(false);

  const view         = content.view || 'default';
  const iconColor    = st.color      || 'currentColor';
  // Secondary Color (style tab) is the authoritative source; fall back to legacy content.viewColor
  const boxColor     = st.iconBoxColor || (content as any).viewColor || '#e8e8e8';
  const iconSizePx   = parseInt(st.iconSize  || '48', 10);
  const innerPad     = st.iconPadding || '10px';
  const borderRadius = content.borderRadius ?? '50%';

  // Preview reflects live style-tab values
  const previewWrapStyle: React.CSSProperties =
    view === 'stacked' ? { display:'inline-flex', alignItems:'center', justifyContent:'center', background: boxColor, borderRadius, padding: innerPad } :
    view === 'framed'  ? { display:'inline-flex', alignItems:'center', justifyContent:'center', border: `2px solid ${boxColor}`, borderRadius, padding: innerPad } :
    {};

  return (
    <div className="jalpi-props-section">

      {/* Large icon preview box — click to open picker */}
      <div className="jalpi-icon-preview-panel" onClick={() => setPickerOpen(true)} title="Click to change icon">
        <div className="jalpi-icon-preview-panel__box">
          {content.icon ? (
            <div style={previewWrapStyle}>
              <span style={{ fontSize: iconSizePx, color: iconColor, display:'inline-flex', lineHeight:1 }}>
                <RenderIconValue value={content.icon} />
              </span>
            </div>
          ) : (
            <div className="jalpi-icon-preview-panel__empty">
              <svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round"><circle cx="12" cy="8" r="6"/><path d="M3 21a9 9 0 0 1 18 0"/></svg>
              <span>Click to choose icon</span>
            </div>
          )}
        </div>
        {content.icon && (
          <button type="button" className="jalpi-icon-preview-panel__remove"
            onClick={(e) => { e.stopPropagation(); update('icon', undefined); }}
            title="Remove icon">✕</button>
        )}
      </div>

      {pickerOpen && (
        <IconPickerModal
          value={content.icon}
          onChange={(v) => { update('icon', v); setPickerOpen(false); }}
          onClear={() => { update('icon', undefined); setPickerOpen(false); }}
          onClose={() => setPickerOpen(false)}
        />
      )}

      {/* View — structural choice (Default / Stacked / Framed) */}
      <PropGroup label="View" defaultOpen={false}>
        <PropRow label="Style">
          <Select
            value={view}
            options={[
              { value: 'default', label: 'Default' },
              { value: 'stacked', label: 'Stacked' },
              { value: 'framed',  label: 'Framed'  },
            ]}
            onChange={(v) => update('view', v)}
          />
        </PropRow>
        {(view === 'stacked' || view === 'framed') && (
          <PropRow label="Shape">
            <Select
              value={content.borderRadius === '0' ? 'square' : 'circle'}
              options={[
                { value: 'circle', label: 'Circle' },
                { value: 'square', label: 'Square' },
              ]}
              onChange={(v) => update('borderRadius', v === 'square' ? '0' : '50%')}
            />
          </PropRow>
        )}
      </PropGroup>

      {/* Link */}
      <PropGroup label="Link" defaultOpen={false}>
        <PropRow label="URL" stack>
          <TextInput value={content.link || ''} onChange={(v) => update('link', v)} placeholder="https://…" />
        </PropRow>
        <PropRow label="New Tab">
          <input type="checkbox" checked={!!content.newTab} onChange={(e) => update('newTab', e.target.checked)} />
        </PropRow>
      </PropGroup>
    </div>
  );
}

/* — Shortcode — */
function ShortcodeContent({ node, store }: { node: CBNode; store: EditorStore }) {
  const content = (node.content ?? {}) as { shortcode?: string };
  return (
    <div className="jalpi-props-section">
      <PropGroup label="Shortcode">
        <TextInput
          value={content.shortcode || ''}
          onChange={(v) => store.updateNodeContent(node.id, { shortcode: v })}
          placeholder='[my_shortcode attr="value"]'
        />
      </PropGroup>
    </div>
  );
}

/* — Logo — */
function LogoContent({ node, store }: { node: CBNode; store: EditorStore }) {
  const content = (node.content ?? {}) as { url?: string; alt?: string; width?: string; height?: string; linkUrl?: string; newTab?: boolean };
  const update  = (key: string, val: unknown) =>
    store.updateNodeContent(node.id, { [key]: val });

  return (
    <div className="jalpi-props-section">
      <PropGroup label="Logo Image">
        <PropRow label="Source URL">
          <TextInput value={content.url || ''} onChange={(v) => update('url', v)} placeholder="https://…" />
        </PropRow>
        <PropRow label="Alt Text">
          <TextInput value={content.alt || ''} onChange={(v) => update('alt', v)} placeholder="Company logo" />
        </PropRow>
      </PropGroup>

      <PropGroup label="Dimensions" defaultOpen={false}>
        <PropRow label="Width">
          <DimensionInput value={content.width || ''} onChange={(v) => update('width', v)} placeholder="auto" units={['px', '%']} />
        </PropRow>
        <PropRow label="Height">
          <DimensionInput value={content.height || ''} onChange={(v) => update('height', v)} placeholder="auto" units={['px', '%']} />
        </PropRow>
      </PropGroup>

      <PropGroup label="Link" defaultOpen={false}>
        <PropRow label="URL">
          <TextInput value={content.linkUrl || ''} onChange={(v) => update('linkUrl', v)} placeholder="https://…" />
        </PropRow>
        <PropRow label="New Tab">
          <Toggle checked={!!content.newTab} onChange={(v) => update('newTab', v)} />
        </PropRow>
      </PropGroup>
    </div>
  );
}

/* — Form — */
type FormPlugin = { value: string; label: string };

function FormContent({ node, store }: { node: CBNode; store: EditorStore }) {
  const content = (node.content ?? {}) as { plugin?: string; form_id?: string };
  const update  = (key: string, val: unknown) => store.updateNodeContent(node.id, { [key]: val });

  const [plugins,        setPlugins]        = useState<FormPlugin[] | null>(null);
  const [forms,          setForms]          = useState<FormPlugin[]>([]);
  const [loadingPlugins, setLoadingPlugins] = useState(false);
  const [loadingForms,   setLoadingForms]   = useState(false);
  const [pluginError,    setPluginError]    = useState('');

  // Fetch active form plugins once on mount.
  useEffect(() => {
    if (plugins !== null) return;
    setLoadingPlugins(true);
    fetch(`${jalpiEditorConfig.apiUrl}/forms/plugins`, {
      headers: { 'X-WP-Nonce': jalpiEditorConfig.nonce },
    })
      .then((r) => r.json())
      .then((data: FormPlugin[]) => { setPlugins(Array.isArray(data) ? data : []); setLoadingPlugins(false); })
      .catch(() => { setPlugins([]); setLoadingPlugins(false); setPluginError('Could not load plugin list.'); });
  }, [plugins]);

  // Fetch forms whenever the selected plugin changes.
  useEffect(() => {
    if (!content.plugin) { setForms([]); return; }
    setLoadingForms(true);
    fetch(`${jalpiEditorConfig.apiUrl}/forms?plugin=${encodeURIComponent(content.plugin)}`, {
      headers: { 'X-WP-Nonce': jalpiEditorConfig.nonce },
    })
      .then((r) => r.json())
      .then((data: FormPlugin[]) => { setForms(Array.isArray(data) ? data : []); setLoadingForms(false); })
      .catch(() => { setForms([]); setLoadingForms(false); });
  }, [content.plugin]);

  const pluginOptions: FormPlugin[] = [
    { value: '', label: '— Choose plugin —' },
    ...(plugins ?? []),
  ];

  const formOptions: FormPlugin[] = [
    { value: '', label: loadingForms ? 'Loading…' : forms.length === 0 ? '— No forms found —' : '— Choose form —' },
    ...forms,
  ];

  const noPlugins = plugins !== null && plugins.length === 0 && !loadingPlugins;

  return (
    <div className="jalpi-props-section">
      <PropGroup label="Form">
        {loadingPlugins && (
          <div className="jalpi-form-picker-loading">Detecting form plugins…</div>
        )}
        {pluginError && (
          <div className="jalpi-form-picker-error">{pluginError}</div>
        )}
        {noPlugins && (
          <div className="jalpi-form-picker-notice">
            No supported form plugin is active. Install and activate{' '}
            <strong>Contact Form 7</strong>, <strong>WPForms</strong>,{' '}
            <strong>Forminator</strong>, or <strong>Gravity Forms</strong>.
          </div>
        )}
        {!loadingPlugins && !noPlugins && plugins !== null && (
          <>
            <PropRow label="Plugin">
              <Select
                value={content.plugin || ''}
                options={pluginOptions}
                onChange={(v) => { update('plugin', v); update('form_id', ''); setForms([]); }}
              />
            </PropRow>
            {content.plugin && (
              <PropRow label="Form">
                <Select
                  value={content.form_id || ''}
                  options={formOptions}
                  onChange={(v) => update('form_id', v)}
                />
              </PropRow>
            )}
            {content.plugin && content.form_id && (
              <div className="jalpi-form-picker-badge">
                <span className="jalpi-form-picker-badge__dot" />
                {pluginOptions.find((p) => p.value === content.plugin)?.label ?? content.plugin}
                {' · '}
                {forms.find((f) => f.value === content.form_id)?.label ?? `Form #${content.form_id}`}
              </div>
            )}
          </>
        )}
      </PropGroup>
    </div>
  );
}

/* =========================================================
   Shared: simple item repeater helper
   ========================================================= */

function ItemRepeater<T extends Record<string, unknown>>({
  items,
  onChange,
  newItem,
  renderItem,
  addLabel = '+ Add item',
  renderAdd,
}: {
  items:      T[];
  onChange:   (items: T[]) => void;
  newItem:    T;
  renderItem: (item: T, idx: number, update: (key: keyof T, val: unknown) => void) => React.ReactNode;
  addLabel?:  string;
  /** Optional custom add control. Receives an `append` fn to push a fully-built
   * item (e.g. one whose icon was chosen from the library before adding).
   * When provided, it replaces the default "+ Add item" button. */
  renderAdd?: (append: (item: T) => void) => React.ReactNode;
}) {
  const update = (idx: number, key: keyof T, val: unknown) => {
    const next = items.map((item, i) => i === idx ? { ...item, [key]: val } : item);
    onChange(next);
  };
  const remove = (idx: number) => onChange(items.filter((_, i) => i !== idx));
  const add    = () => onChange([...items, { ...newItem }]);
  const append = (item: T) => onChange([...items, item]);

  return (
    <div className="jalpi-repeater">
      {items.map((item, idx) => (
        <div key={idx} className="jalpi-repeater__item">
          <div className="jalpi-repeater__item-header">
            <span className="jalpi-repeater__item-num">{idx + 1}</span>
            <button type="button" className="jalpi-repeater__remove" title="Remove" onClick={() => remove(idx)}>
              <svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
            </button>
          </div>
          <div className="jalpi-repeater__item-body">
            {renderItem(item, idx, (key, val) => update(idx, key, val))}
          </div>
        </div>
      ))}
      {renderAdd
        ? renderAdd(append)
        : <button type="button" className="jalpi-repeater__add" onClick={add}>{addLabel}</button>}
    </div>
  );
}

/* =========================================================
   New element content panels
   ========================================================= */

/* — Alert — */
function AlertContent({ node, store }: { node: CBNode; store: EditorStore }) {
  const content  = (node.content ?? {}) as { variant?: string; title?: string; text?: string };
  const update   = (key: string, val: string) => store.updateNodeContent(node.id, { [key]: val });
  return (
    <div className="jalpi-props-section">
      <PropGroup label="Alert">
        <PropRow label="Type" stack>
          <Select
            value={content.variant || 'info'}
            options={[
              { value: 'info',    label: 'Info (blue)'    },
              { value: 'success', label: 'Success (green)' },
              { value: 'warning', label: 'Warning (amber)' },
              { value: 'error',   label: 'Error (red)'    },
            ]}
            onChange={(v) => update('variant', v)}
          />
        </PropRow>
        <PropRow label="Title" stack>
          <TextInput value={content.title || ''} onChange={(v) => update('title', v)} placeholder="Alert title…" />
        </PropRow>
        <PropRow label="Message" stack>
          <textarea className="jalpi-textarea" rows={3}
            value={content.text || ''}
            onChange={(e) => update('text', e.target.value)}
            placeholder="Alert message…"
          />
        </PropRow>
      </PropGroup>
    </div>
  );
}

/* — Counter — */
function CounterContent({ node, store }: { node: CBNode; store: EditorStore }) {
  type CounterItem = {
    startNumber: string; endNumber: string;
    prefix: string; suffix: string;
    duration: string; thousandSep: boolean; separator: string;
    title: string;
  };

  const raw = (node.content ?? {}) as any;

  // Normalise: if saved as a legacy single-counter, wrap it in items[]
  const items: CounterItem[] = raw.items?.length
    ? raw.items.map((it: any) => ({
        startNumber: it.startNumber ?? '0',
        endNumber:   it.endNumber ?? it.number ?? '100',
        prefix:      it.prefix    ?? '',
        suffix:      it.suffix    ?? '',
        duration:    String(it.duration ?? '2000'),
        thousandSep: it.thousandSep ?? false,
        separator:   it.separator  ?? 'dot',
        title:       it.title ?? it.label ?? '',
      }))
    : [{
        startNumber: raw.startNumber ?? '0',
        endNumber:   raw.endNumber ?? raw.number ?? '100',
        prefix:      raw.prefix    ?? '',
        suffix:      raw.suffix    ?? '',
        duration:    String(raw.duration ?? '2000'),
        thousandSep: raw.thousandSep ?? false,
        separator:   raw.separator  ?? 'dot',
        title:       raw.title ?? raw.label ?? '',
      }];

  const [openIdx, setOpenIdx] = React.useState<number>(0);

  const saveItems = (next: CounterItem[]) => store.updateNodeContent(node.id, { items: next });
  const setField  = (idx: number, key: keyof CounterItem, v: unknown) =>
    saveItems(items.map((it, i) => i === idx ? { ...it, [key]: v } : it));
  const removeAt  = (idx: number) => {
    saveItems(items.filter((_, i) => i !== idx));
    if (openIdx === idx) setOpenIdx(-1);
    else if (openIdx > idx) setOpenIdx(openIdx - 1);
  };
  const dupAt     = (idx: number) =>
    saveItems([...items.slice(0, idx + 1), { ...items[idx] }, ...items.slice(idx + 1)]);
  const addItem   = () => {
    saveItems([...items, { startNumber: '0', endNumber: '100', prefix: '', suffix: '+', duration: '2000', thousandSep: false, separator: 'dot', title: `Counter ${items.length + 1}` }]);
    setOpenIdx(items.length);
  };

  const SEP_OPTIONS = [
    { value: 'dot',   label: 'Dot (.)' },
    { value: 'comma', label: 'Comma (,)' },
    { value: 'space', label: 'Space' },
  ];

  return (
    <div className="jalpi-props-section">
      <PropGroup label="Counters">
        <div className="jalpi-acc-items">
          {items.map((item, idx) => {
            const isOpen = openIdx === idx;
            const label  = item.prefix + (item.endNumber || '0') + item.suffix + (item.title ? ` — ${item.title}` : '');
            return (
              <div key={idx} className={`jalpi-acc-item-row${isOpen ? ' is-open' : ''}`}>
                <div className="jalpi-acc-item-row__head">
                  <button type="button" className="jalpi-acc-item-row__toggle"
                    onClick={() => setOpenIdx(isOpen ? -1 : idx)}
                    title={isOpen ? 'Collapse' : 'Expand'}>
                    <svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round"
                      style={{ transform: isOpen ? 'rotate(180deg)' : 'rotate(0deg)', transition: 'transform .15s' }}>
                      <polyline points="6 9 12 15 18 9"/>
                    </svg>
                  </button>
                  <span className="jalpi-acc-item-row__label">{label || `Counter #${idx + 1}`}</span>
                  <button type="button" className="jalpi-acc-item-row__btn" title="Duplicate" onClick={() => dupAt(idx)}>
                    <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
                      <rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/>
                    </svg>
                  </button>
                  <button type="button" className="jalpi-acc-item-row__btn jalpi-acc-item-row__btn--del" title="Remove" onClick={() => removeAt(idx)}>
                    <svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round">
                      <line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/>
                    </svg>
                  </button>
                </div>
                {isOpen && (
                  <div className="jalpi-acc-item-row__body">
                    <PropRow label="Title" stack>
                      <TextInput value={item.title} onChange={(v) => setField(idx, 'title', v)} placeholder="Projects Completed" />
                    </PropRow>
                    <PropRow label="Starting Number" stack>
                      <TextInput value={item.startNumber} onChange={(v) => setField(idx, 'startNumber', v)} placeholder="0" />
                    </PropRow>
                    <PropRow label="Ending Number" stack>
                      <TextInput value={item.endNumber} onChange={(v) => setField(idx, 'endNumber', v)} placeholder="100" />
                    </PropRow>
                    <PropRow label="Prefix" stack>
                      <TextInput value={item.prefix} onChange={(v) => setField(idx, 'prefix', v)} placeholder="$" />
                    </PropRow>
                    <PropRow label="Suffix" stack>
                      <TextInput value={item.suffix} onChange={(v) => setField(idx, 'suffix', v)} placeholder="+" />
                    </PropRow>
                    <PropRow label="Duration (ms)" stack>
                      <TextInput value={item.duration} onChange={(v) => setField(idx, 'duration', v)} placeholder="2000" />
                    </PropRow>
                    <PropRow label="Thousand Separator">
                      <Toggle checked={item.thousandSep} onChange={(v) => setField(idx, 'thousandSep', v)} />
                    </PropRow>
                    {item.thousandSep && (
                      <PropRow label="Separator">
                        <Select value={item.separator} options={SEP_OPTIONS} onChange={(v) => setField(idx, 'separator', v)} />
                      </PropRow>
                    )}
                  </div>
                )}
              </div>
            );
          })}
        </div>
        <button type="button" className="jalpi-add-item-btn" onClick={addItem}>+ Add Item</button>
      </PropGroup>
    </div>
  );
}

/* — Progress — */
function ProgressContent({ node, store }: { node: CBNode; store: EditorStore }) {
  type ProgressItem = {
    title: string; titleTag: string; showTitle: boolean;
    percentage: number; showPercentage: boolean;
    innerText: string; color: string;
  };
  const content = (node.content ?? {}) as { items?: ProgressItem[] };
  const items   = (content.items ?? []).map((it) => ({
    title:           it.title           ?? (it as any).label ?? '',
    titleTag:        it.titleTag        ?? 'h3',
    showTitle:       it.showTitle       ?? true,
    percentage:      it.percentage      ?? 0,
    showPercentage:  it.showPercentage  ?? true,
    innerText:       it.innerText       ?? '',
    color:           it.color           ?? '#1a1a1a',
  }));

  const [openIdx, setOpenIdx] = React.useState<number>(0);

  const saveItems = (next: ProgressItem[]) => store.updateNodeContent(node.id, { items: next });
  const setField  = (idx: number, key: keyof ProgressItem, v: unknown) =>
    saveItems(items.map((it, i) => i === idx ? { ...it, [key]: v } : it));
  const removeAt  = (idx: number) => {
    saveItems(items.filter((_, i) => i !== idx));
    if (openIdx === idx) setOpenIdx(-1);
    else if (openIdx > idx) setOpenIdx(openIdx - 1);
  };
  const dupAt     = (idx: number) =>
    saveItems([...items.slice(0, idx + 1), { ...items[idx] }, ...items.slice(idx + 1)]);
  const addItem   = () => {
    saveItems([...items, { title: `Skill #${items.length + 1}`, titleTag: 'h3', showTitle: true, percentage: 75, showPercentage: true, innerText: '', color: '#1a1a1a' }]);
    setOpenIdx(items.length);
  };

  const TAG_OPTIONS = ['h1','h2','h3','h4','h5','h6','div','span'].map((t) => ({ value: t, label: t.toUpperCase() }));

  return (
    <div className="jalpi-props-section">
      <PropGroup label="Progress Bars">
        <div className="jalpi-acc-items">
          {items.map((item, idx) => {
            const isOpen = openIdx === idx;
            return (
              <div key={idx} className={`jalpi-acc-item-row${isOpen ? ' is-open' : ''}`}>
                <div className="jalpi-acc-item-row__head">
                  <button type="button" className="jalpi-acc-item-row__toggle"
                    onClick={() => setOpenIdx(isOpen ? -1 : idx)}
                    title={isOpen ? 'Collapse' : 'Expand'}>
                    <svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round"
                      style={{ transform: isOpen ? 'rotate(180deg)' : 'rotate(0deg)', transition: 'transform .15s' }}>
                      <polyline points="6 9 12 15 18 9"/>
                    </svg>
                  </button>
                  <span className="jalpi-acc-item-row__label">{item.title || `Skill #${idx + 1}`}</span>
                  <button type="button" className="jalpi-acc-item-row__btn" title="Duplicate" onClick={() => dupAt(idx)}>
                    <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
                      <rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/>
                    </svg>
                  </button>
                  <button type="button" className="jalpi-acc-item-row__btn jalpi-acc-item-row__btn--del" title="Remove" onClick={() => removeAt(idx)}>
                    <svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round">
                      <line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/>
                    </svg>
                  </button>
                </div>
                {isOpen && (
                  <div className="jalpi-acc-item-row__body">
                    <PropRow label="Title" stack>
                      <TextInput value={item.title} onChange={(v) => setField(idx, 'title', v)} placeholder="Skill name" />
                    </PropRow>
                    <PropRow label="Title HTML Tag">
                      <Select value={item.titleTag} options={TAG_OPTIONS} onChange={(v) => setField(idx, 'titleTag', v)} />
                    </PropRow>
                    <PropRow label="Display Title">
                      <Toggle checked={item.showTitle} onChange={(v) => setField(idx, 'showTitle', v)} />
                    </PropRow>
                    <PropRow label="Percentage" stack>
                      <div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
                        <input type="range" className="jalpi-range" style={{ flex: 1 }} min={0} max={100} value={item.percentage}
                          onChange={(e) => setField(idx, 'percentage', Number(e.target.value))} />
                        <input type="number" className="jalpi-num-input" min={0} max={100} value={item.percentage}
                          onChange={(e) => setField(idx, 'percentage', Math.min(100, Math.max(0, Number(e.target.value))))}
                          style={{ width: 54 }} />
                      </div>
                    </PropRow>
                    <PropRow label="Display Percentage">
                      <Toggle checked={item.showPercentage} onChange={(v) => setField(idx, 'showPercentage', v)} />
                    </PropRow>
                    <PropRow label="Inner Text" stack>
                      <TextInput value={item.innerText} onChange={(v) => setField(idx, 'innerText', v)} placeholder="e.g. Web Designer" />
                    </PropRow>
                    <PropRow label="Bar Color">
                      <ColorInput value={item.color} onChange={(v) => setField(idx, 'color', v)} />
                    </PropRow>
                  </div>
                )}
              </div>
            );
          })}
        </div>
        <button type="button" className="jalpi-add-item-btn" onClick={addItem}>+ Add Item</button>
      </PropGroup>
    </div>
  );
}

/* — Testimonial — */
function TestimonialContent({ node, store }: { node: CBNode; store: EditorStore }) {
  const content = (node.content ?? {}) as { text?: string; name?: string; role?: string; avatarUrl?: string; rating?: number };
  const update  = (key: string, val: unknown) => store.updateNodeContent(node.id, { [key]: val });
  return (
    <div className="jalpi-props-section">
      <PropGroup label="Testimonial">
        <PropRow label="Quote" stack>
          <textarea className="jalpi-textarea" rows={4}
            value={content.text || ''}
            onChange={(e) => update('text', e.target.value)}
            placeholder="Enter testimonial quote…"
          />
        </PropRow>
        <PropRow label="Rating" stack>
          <Select
            value={String(content.rating ?? 5)}
            options={[5,4,3,2,1].map((n) => ({ value: String(n), label: '★'.repeat(n) + '☆'.repeat(5 - n) }))}
            onChange={(v) => update('rating', Number(v))}
          />
        </PropRow>
      </PropGroup>
      <PropGroup label="Author" defaultOpen={false}>
        <PropRow label="Avatar" stack>
          <MediaPickerInput value={content.avatarUrl || ''} onChange={(v) => update('avatarUrl', v)} />
        </PropRow>
        <PropRow label="Name" stack>
          <TextInput value={content.name || ''} onChange={(v) => update('name', v)} placeholder="Jane Smith" />
        </PropRow>
        <PropRow label="Role" stack>
          <TextInput value={content.role || ''} onChange={(v) => update('role', v)} placeholder="CEO, Acme Corp" />
        </PropRow>
      </PropGroup>
    </div>
  );
}

/* — Accordion — */
function AccordionElContent({ node, store }: { node: CBNode; store: EditorStore }) {
  type AccItem = { title: string; body: string };
  const content = (node.content ?? {}) as {
    items?: AccItem[];
    itemPosition?: string;
    iconPosition?: string;
    expandIcon?: string;
    collapseIcon?: string;
  };

  const items        = content.items        ?? [{ title: 'Item #1', body: '' }, { title: 'Item #2', body: '' }, { title: 'Item #3', body: '' }];
  const itemPosition = content.itemPosition ?? 'left';
  const iconPosition = content.iconPosition ?? 'right';
  const expandIcon   = content.expandIcon   ?? 'default';
  const collapseIcon = content.collapseIcon ?? 'default';

  // Which item's edit-tab is open (-1 = none)
  const [openIdx, setOpenIdx] = React.useState<number>(-1);

  const saveItems = (next: AccItem[]) => store.updateNodeContent(node.id, { items: next });
  const update    = (key: string, val: unknown) => store.updateNodeContent(node.id, { [key]: val });

  const setField  = (idx: number, key: keyof AccItem, v: string) =>
    saveItems(items.map((it, i) => i === idx ? { ...it, [key]: v } : it));
  const removeAt  = (idx: number) => {
    saveItems(items.filter((_, i) => i !== idx));
    if (openIdx === idx) setOpenIdx(-1);
    else if (openIdx > idx) setOpenIdx(openIdx - 1);
  };
  const dupAt     = (idx: number) => {
    saveItems([...items.slice(0, idx + 1), { ...items[idx] }, ...items.slice(idx + 1)]);
  };
  const addItem   = () => {
    saveItems([...items, { title: `Item #${items.length + 1}`, body: '' }]);
    setOpenIdx(items.length); // auto-open the new item
  };

  const iconOptsList = (type: 'expand' | 'collapse') => [
    { value: 'none',
      label: (
        <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
          <circle cx="12" cy="12" r="10"/><line x1="4.93" y1="4.93" x2="19.07" y2="19.07"/>
        </svg>
      ),
    },
    { value: 'default',
      label: (
        <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round">
          <polyline points="6 9 12 15 18 9"/>
        </svg>
      ),
    },
    { value: type === 'expand' ? 'plus' : 'minus',
      label: <span style={{ fontSize: 14, lineHeight: 1, fontWeight: 700 }}>{type === 'expand' ? '+' : '−'}</span>,
    },
  ];

  return (
    <div className="jalpi-props-section">
      <PropGroup label="Layout">

        {/* Compact item list with expand-to-edit */}
        <div className="jalpi-acc-items-label">Items</div>
        <div className="jalpi-acc-items">
          {items.map((item, idx) => {
            const isOpen = openIdx === idx;
            return (
              <div key={idx} className={`jalpi-acc-item-row${isOpen ? ' is-open' : ''}`}>
                {/* Header row */}
                <div className="jalpi-acc-item-row__head">
                  {/* Toggle arrow */}
                  <button
                    type="button"
                    className="jalpi-acc-item-row__toggle"
                    onClick={() => setOpenIdx(isOpen ? -1 : idx)}
                    title={isOpen ? 'Collapse' : 'Expand'}
                  >
                    <svg
                      width="10" height="10" viewBox="0 0 24 24" fill="none"
                      stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round"
                      style={{ transform: isOpen ? 'rotate(180deg)' : 'rotate(0deg)', transition: 'transform .15s' }}
                    >
                      <polyline points="6 9 12 15 18 9"/>
                    </svg>
                  </button>
                  <span className="jalpi-acc-item-row__label">{item.title || `Item #${idx + 1}`}</span>
                  <button type="button" className="jalpi-acc-item-row__btn" title="Duplicate" onClick={() => dupAt(idx)}>
                    <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
                      <rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/>
                    </svg>
                  </button>
                  <button type="button" className="jalpi-acc-item-row__btn jalpi-acc-item-row__btn--del" title="Remove" onClick={() => removeAt(idx)}>
                    <svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round">
                      <line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/>
                    </svg>
                  </button>
                </div>

                {/* Expanded edit panel */}
                {isOpen && (
                  <div className="jalpi-acc-item-row__body">
                    <PropRow label="Title" stack>
                      <TextInput
                        value={item.title}
                        onChange={(v) => setField(idx, 'title', v)}
                        placeholder="Panel title…"
                      />
                    </PropRow>
                    <PropRow label="Content" stack>
                      <textarea
                        className="jalpi-textarea"
                        rows={4}
                        value={item.body}
                        onChange={(e) => setField(idx, 'body', e.target.value)}
                        placeholder="Panel content…"
                      />
                    </PropRow>
                  </div>
                )}
              </div>
            );
          })}
        </div>
        <button type="button" className="jalpi-add-item-btn" onClick={addItem}>+ Add Item</button>

        {/* Separator between items list and layout settings */}
        <div className="jalpi-acc-section-sep" />

        {/* Item Position */}
        <PropRow label="Item Position" responsive>
          <AlignButtons
            value={itemPosition}
            onChange={(v) => update('itemPosition', v)}
            options={['left', 'center', 'right', 'justify']}
          />
        </PropRow>

        {/* Icon sub-section */}
        <div className="jalpi-acc-icon-label">Icon</div>
        <PropRow label="Position" responsive>
          <AlignButtons
            value={iconPosition}
            onChange={(v) => update('iconPosition', v)}
            options={['left', 'right']}
          />
        </PropRow>
        <PropRow label="Expand">
          <div className="jalpi-align-btns">
            {iconOptsList('expand').map((opt) => (
              <button key={opt.value} type="button"
                className={`jalpi-align-btn${expandIcon === opt.value ? ' is-active' : ''}`}
                onClick={() => update('expandIcon', opt.value)} title={opt.value}
              >{opt.label}</button>
            ))}
          </div>
        </PropRow>
        <PropRow label="Collapse">
          <div className="jalpi-align-btns">
            {iconOptsList('collapse').map((opt) => (
              <button key={opt.value} type="button"
                className={`jalpi-align-btn${collapseIcon === opt.value ? ' is-active' : ''}`}
                onClick={() => update('collapseIcon', opt.value)} title={opt.value}
              >{opt.label}</button>
            ))}
          </div>
        </PropRow>

      </PropGroup>
    </div>
  );
}

/* — Badge — */
function BadgeContent({ node, store }: { node: CBNode; store: EditorStore }) {
  const content = (node.content ?? {}) as { text?: string; variant?: string };
  const update  = (key: string, val: string) => store.updateNodeContent(node.id, { [key]: val });
  return (
    <div className="jalpi-props-section">
      <PropGroup label="Badge">
        <PropRow label="Text" stack>
          <TextInput value={content.text || ''} onChange={(v) => update('text', v)} placeholder="Badge label" />
        </PropRow>
        <PropRow label="Color" stack>
          <Select
            value={content.variant || 'primary'}
            options={[
              { value: 'primary',   label: 'Primary (purple)'  },
              { value: 'secondary', label: 'Secondary (grey)'  },
              { value: 'success',   label: 'Success (green)'   },
              { value: 'warning',   label: 'Warning (amber)'   },
              { value: 'error',     label: 'Error (red)'       },
              { value: 'info',      label: 'Info (blue)'       },
            ]}
            onChange={(v) => update('variant', v)}
          />
        </PropRow>
      </PropGroup>
    </div>
  );
}

/* — Icon List — */
function IconListContent({ node, store }: { node: CBNode; store: EditorStore }) {
  type ILItem = { text: string; icon: string | IconValue; link: string; newTab: boolean };

  const raw         = (node.content ?? {}) as Record<string, unknown>;
  const layout      = (raw.layout      ?? 'list')  as string;
  const defaultIcon = (raw.defaultIcon ?? '') as string | IconValue;
  const applyLinkOn = (raw.applyLinkOn ?? 'full')  as string;

  const rawItems = (raw.items ?? []) as unknown[];
  const items: ILItem[] = rawItems.map((it: unknown) =>
    typeof it === 'string'
      ? { text: it, icon: '', link: '', newTab: false }
      : { text: (it as any).text ?? '', icon: (it as any).icon ?? '', link: (it as any).link ?? '', newTab: !!(it as any).newTab }
  );

  const [openIdx, setOpenIdx] = useState<number | null>(null);

  const save = (nextItems: ILItem[], extra?: Record<string, unknown>) =>
    store.updateNodeContent(node.id, { ...raw, items: nextItems, ...extra });

  const setField = (idx: number, key: keyof ILItem, val: unknown) =>
    save(items.map((it, i) => i === idx ? { ...it, [key]: val } : it));

  const addItem = () => {
    const next = [...items, { text: `List Item #${items.length + 1}`, icon: (defaultIcon || 'check') as string | IconValue, link: '', newTab: false }];
    save(next);
    setOpenIdx(next.length - 1);
  };

  const removeAt = (idx: number) => {
    save(items.filter((_, i) => i !== idx));
    if (openIdx === idx) setOpenIdx(null);
  };

  const dupAt = (idx: number) => {
    const copy = { ...items[idx] };
    const next = [...items.slice(0, idx + 1), copy, ...items.slice(idx + 1)];
    save(next);
    setOpenIdx(idx + 1);
  };

  return (
    <div className="jalpi-props-section">
      <PropGroup label="Icon List" defaultOpen>

        <PropRow label="Layout">
          <div className="jalpi-align-btns">
            {([
              { val: 'list',   title: 'List',
                icon: <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round"><line x1="9" y1="6" x2="21" y2="6"/><line x1="9" y1="12" x2="21" y2="12"/><line x1="9" y1="18" x2="21" y2="18"/><circle cx="3" cy="6" r="1.5" fill="currentColor" stroke="none"/><circle cx="3" cy="12" r="1.5" fill="currentColor" stroke="none"/><circle cx="3" cy="18" r="1.5" fill="currentColor" stroke="none"/></svg> },
              { val: 'inline', title: 'Inline',
                icon: <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round"><circle cx="5" cy="12" r="2"/><circle cx="12" cy="12" r="2"/><circle cx="19" cy="12" r="2"/></svg> },
            ] as { val: string; title: string; icon: React.ReactNode }[]).map(opt => (
              <button key={opt.val} type="button"
                className={`jalpi-align-btn${layout === opt.val ? ' is-active' : ''}`}
                onClick={() => save(items, { layout: opt.val })}
                title={opt.title}>{opt.icon}</button>
            ))}
          </div>
        </PropRow>

        <div style={{ fontSize: 11, fontWeight: 600, color: '#6b7280', textTransform: 'uppercase' as const, letterSpacing: '0.06em', margin: '12px 0 6px' }}>Items</div>

        {items.map((item, idx) => (
          <div key={idx} className={`jalpi-acc-item-row${openIdx === idx ? ' is-open' : ''}`}>
            <div className="jalpi-acc-item-row__head">
              <button type="button" className="jalpi-acc-item-row__toggle"
                onClick={() => setOpenIdx(openIdx === idx ? null : idx)}
                title={openIdx === idx ? 'Collapse' : 'Expand'}>
                <svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round"
                  style={{ transform: openIdx === idx ? 'rotate(180deg)' : 'rotate(0deg)', transition: 'transform .15s' }}>
                  <polyline points="6 9 12 15 18 9"/>
                </svg>
              </button>
              <span style={{ display: 'inline-flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0, width: 14, opacity: 0.35, cursor: 'grab' }}>
                <svg width="10" height="14" viewBox="0 0 10 14" fill="currentColor">
                  <circle cx="3" cy="2.5" r="1.2"/><circle cx="7" cy="2.5" r="1.2"/>
                  <circle cx="3" cy="7"   r="1.2"/><circle cx="7" cy="7"   r="1.2"/>
                  <circle cx="3" cy="11.5" r="1.2"/><circle cx="7" cy="11.5" r="1.2"/>
                </svg>
              </span>
              <span className="jalpi-acc-item-row__label">{item.text || `List Item #${idx + 1}`}</span>
              <button type="button" className="jalpi-acc-item-row__btn" onClick={() => dupAt(idx)} title="Duplicate">
                <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>
              </button>
              <button type="button" className="jalpi-acc-item-row__btn jalpi-acc-item-row__btn--del" onClick={() => removeAt(idx)} title="Remove">
                <svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
              </button>
            </div>
            {openIdx === idx && (
              <div className="jalpi-acc-item-row__body">
                <PropRow label="Text" stack>
                  <TextInput value={item.text} onChange={(v) => setField(idx, 'text', v)} placeholder="List item text…" />
                </PropRow>
                <PropRow label="Icon" stack>
                  <IconInput value={item.icon as IconValue | ''} onChange={(v) => setField(idx, 'icon', v)} placeholder="Choose icon…" />
                  {item.icon && (
                    <div className="jalpi-icon-large-preview">
                      <RenderIconValue value={item.icon as IconValue} />
                    </div>
                  )}
                </PropRow>
                <PropRow label="Link" stack>
                  <TextInput value={item.link} onChange={(v) => setField(idx, 'link', v)} placeholder="https://…" />
                </PropRow>
                {item.link && (
                  <PropRow label="New Tab">
                    <input type="checkbox" checked={!!item.newTab} onChange={(e) => setField(idx, 'newTab', e.target.checked)} />
                  </PropRow>
                )}
              </div>
            )}
          </div>
        ))}

        <button type="button" className="jalpi-add-item-btn" style={{ marginTop: 8 }} onClick={addItem}>+ Add Item</button>

        <PropRow label="Apply Link On" stack>
          <Select
            value={applyLinkOn}
            options={[
              { value: 'full', label: 'Full Width' },
              { value: 'icon', label: 'Icon Only'  },
              { value: 'text', label: 'Text Only'  },
            ]}
            onChange={(v) => save(items, { applyLinkOn: v })}
          />
        </PropRow>

      </PropGroup>
    </div>
  );
}

/* — Pricing Table — */
function PricingTableContent({ node, store }: { node: CBNode; store: EditorStore }) {
  const content  = (node.content ?? {}) as {
    name?: string; tagline?: string; currency?: string; amount?: string; period?: string;
    features?: string[]; btnText?: string; btnUrl?: string; featured?: boolean;
  };
  const update   = (key: string, val: unknown) => store.updateNodeContent(node.id, { [key]: val });
  const features = content.features ?? [];
  const saveFeats = (next: string[]) => update('features', next);

  return (
    <div className="jalpi-props-section">
      <PropGroup label="Pricing">
        <PropRow label="Plan name" stack>
          <TextInput value={content.name || ''} onChange={(v) => update('name', v)} placeholder="Pro" />
        </PropRow>
        <PropRow label="Tagline" stack>
          <TextInput value={content.tagline || ''} onChange={(v) => update('tagline', v)} placeholder="Best for growing teams" />
        </PropRow>
        <PropRow label="Featured">
          <Toggle checked={!!content.featured} onChange={(v) => update('featured', v)} />
        </PropRow>
      </PropGroup>

      <PropGroup label="Price" defaultOpen={false}>
        <PropRow label="Currency" stack>
          <TextInput value={content.currency || '$'} onChange={(v) => update('currency', v)} placeholder="$" />
        </PropRow>
        <PropRow label="Amount" stack>
          <TextInput value={content.amount || '0'} onChange={(v) => update('amount', v)} placeholder="49" />
        </PropRow>
        <PropRow label="Period" stack>
          <TextInput value={content.period || 'mo'} onChange={(v) => update('period', v)} placeholder="mo" />
        </PropRow>
      </PropGroup>

      <PropGroup label="Features" defaultOpen={false}>
        <div className="jalpi-repeater">
          {features.map((f, idx) => (
            <div key={idx} className="jalpi-repeater__item">
              <div className="jalpi-repeater__item-header">
                <span className="jalpi-repeater__item-num">{idx + 1}</span>
                <button type="button" className="jalpi-repeater__remove"
                  onClick={() => saveFeats(features.filter((_, i) => i !== idx))}>
                  <svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
                </button>
              </div>
              <div className="jalpi-repeater__item-body">
                <TextInput value={f} onChange={(v) => saveFeats(features.map((t, i) => i === idx ? v : t))} placeholder="Feature…" />
              </div>
            </div>
          ))}
          <button type="button" className="jalpi-repeater__add" onClick={() => saveFeats([...features, ''])}>+ Add feature</button>
        </div>
      </PropGroup>

      <PropGroup label="Button" defaultOpen={false}>
        <PropRow label="Label" stack>
          <TextInput value={content.btnText || ''} onChange={(v) => update('btnText', v)} placeholder="Get Started" />
        </PropRow>
        <PropRow label="URL" stack>
          <TextInput value={content.btnUrl || '#'} onChange={(v) => update('btnUrl', v)} placeholder="https://…" />
        </PropRow>
      </PropGroup>
    </div>
  );
}

/* — Team Member — */
function TeamMemberContent({ node, store }: { node: CBNode; store: EditorStore }) {
  const content = (node.content ?? {}) as { photoUrl?: string; name?: string; role?: string; bio?: string; linkedin?: string; twitter?: string };
  const update  = (key: string, val: unknown) => store.updateNodeContent(node.id, { [key]: val });
  return (
    <div className="jalpi-props-section">
      <PropGroup label="Team Member">
        <PropRow label="Photo" stack>
          <MediaPickerInput value={content.photoUrl || ''} onChange={(v) => update('photoUrl', v)} />
        </PropRow>
        <PropRow label="Name" stack>
          <TextInput value={content.name || ''} onChange={(v) => update('name', v)} placeholder="Full name" />
        </PropRow>
        <PropRow label="Role" stack>
          <TextInput value={content.role || ''} onChange={(v) => update('role', v)} placeholder="Job title" />
        </PropRow>
        <PropRow label="Bio" stack>
          <textarea className="jalpi-textarea" rows={3}
            value={content.bio || ''}
            onChange={(e) => update('bio', e.target.value)}
            placeholder="Short bio…"
          />
        </PropRow>
      </PropGroup>
      <PropGroup label="Social Links" defaultOpen={false}>
        <PropRow label="LinkedIn" stack>
          <TextInput value={content.linkedin || ''} onChange={(v) => update('linkedin', v)} placeholder="https://linkedin.com/in/…" />
        </PropRow>
        <PropRow label="Twitter / X" stack>
          <TextInput value={content.twitter || ''} onChange={(v) => update('twitter', v)} placeholder="https://twitter.com/…" />
        </PropRow>
      </PropGroup>
    </div>
  );
}

/* — FAQ — */
type FaqItemData = { question: string; answer: string; defaultOpen?: boolean };

function FaqContent({ node, store }: { node: CBNode; store: EditorStore }) {
  const content = (node.content ?? {}) as { items?: FaqItemData[] };
  const items   = (content.items ?? []) as FaqItemData[];
  const save    = (next: FaqItemData[]) => store.updateNodeContent(node.id, { items: next });

  return (
    <div className="jalpi-props-section">
      <PropGroup label="FAQ Items">
        <ItemRepeater
          items={items}
          onChange={save}
          newItem={{ question: '', answer: '', defaultOpen: false }}
          addLabel="+ Add FAQ Item"
          renderItem={(item, _, upd) => (
            <>
              <PropRow label="Question" stack>
                <TextInput value={item.question} onChange={(v) => upd('question', v)} placeholder="Question…" />
              </PropRow>
              <PropRow label="Answer" stack>
                <textarea className="jalpi-textarea" rows={3}
                  value={item.answer}
                  onChange={(e) => upd('answer', e.target.value)}
                  placeholder="Answer…"
                />
              </PropRow>
              <PropRow label="Open by default">
                <label style={{ display: 'flex', alignItems: 'center', gap: 6, cursor: 'pointer', fontSize: 12 }}>
                  <input type="checkbox" checked={!!item.defaultOpen} onChange={(e) => upd('defaultOpen', e.target.checked)} />
                  Expanded
                </label>
              </PropRow>
            </>
          )}
        />
      </PropGroup>
    </div>
  );
}

/* ── FAQ Style Tab ──────────────────────────────────────────────────────────── */
function FaqStyleTab({ node, store }: { node: CBNode; store: EditorStore }) {
  const s   = (node.settings ?? {}) as Record<string, unknown>;
  const str = (k: string) => (s[k] as string) || '';
  const upS = (patch: Record<string, unknown>) => store.updateNode(node.id, { settings: { ...s, ...patch } });

  const [qTypoOpen, setQTypoOpen] = useState(false);
  const [aTypoOpen, setATypoOpen] = useState(false);
  const qTypoRef = useRef<HTMLButtonElement>(null);
  const aTypoRef = useRef<HTMLButtonElement>(null);

  const qTypoStyles: Record<string, string> = {
    fontFamily: str('faq_q_fontFamily'), fontSize: str('faq_q_fontSize'),
    fontWeight: str('faq_q_fontWeight'), fontStyle: str('faq_q_fontStyle'),
    textTransform: str('faq_q_textTransform'), textDecoration: str('faq_q_textDecoration'),
    lineHeight: str('faq_q_lineHeight'), letterSpacing: str('faq_q_letterSpacing'),
  };
  const updateQTypo = (patch: Record<string, string>) => {
    const mapped: Record<string, unknown> = {};
    for (const [k, v] of Object.entries(patch)) mapped[`faq_q_${k}`] = v;
    upS(mapped);
  };

  const aTypoStyles: Record<string, string> = {
    fontFamily: str('faq_a_fontFamily'), fontSize: str('faq_a_fontSize'),
    fontWeight: str('faq_a_fontWeight'), fontStyle: str('faq_a_fontStyle'),
    textTransform: str('faq_a_textTransform'), textDecoration: str('faq_a_textDecoration'),
    lineHeight: str('faq_a_lineHeight'), letterSpacing: str('faq_a_letterSpacing'),
  };
  const updateATypo = (patch: Record<string, string>) => {
    const mapped: Record<string, unknown> = {};
    for (const [k, v] of Object.entries(patch)) mapped[`faq_a_${k}`] = v;
    upS(mapped);
  };

  const hasBorder = str('faq_item_bd_type') && str('faq_item_bd_type') !== 'none';

  return (
    <div className="jalpi-props-section">

      {/* ── Layout ── */}
      <PropGroup label="Layout" defaultOpen>
        <PropRow label="Style">
          <Select value={str('faq_layout') || 'boxed'}
            options={[
              { value: 'boxed',     label: 'Boxed'     },
              { value: 'classic',   label: 'Classic'   },
              { value: 'minimal',   label: 'Minimal'   },
              { value: 'separated', label: 'Separated' },
              { value: 'framed',    label: 'Framed'    },
            ]}
            onChange={(v) => upS({ faq_layout: v })} />
        </PropRow>
        <SliderPropRow label="Item Gap" responsive
          value={str('faq_item_gap')} onChange={(v) => upS({ faq_item_gap: v })}
          min={0} max={40} step={1} units={['px']} placeholder="10" />
        <PropRow label="Multiple Open">
          <label style={{ display: 'flex', alignItems: 'center', gap: 6, cursor: 'pointer', fontSize: 12 }}>
            <input type="checkbox" checked={str('faq_multiple_open') === '1'} onChange={(e) => upS({ faq_multiple_open: e.target.checked ? '1' : '0' })} />
            Allow multiple items open
          </label>
        </PropRow>
      </PropGroup>

      {/* ── Icon ── */}
      <PropGroup label="Icon" defaultOpen={false}>
        <PropRow label="Style">
          <Select value={str('faq_icon') || 'chevron'}
            options={[
              { value: 'chevron', label: 'Chevron'    },
              { value: 'plus',    label: 'Plus/Minus' },
              { value: 'arrow',   label: 'Arrow'      },
              { value: 'circle',  label: 'Circle +'   },
              { value: 'none',    label: 'None'       },
            ]}
            onChange={(v) => upS({ faq_icon: v })} />
        </PropRow>
        <PropRow label="Position">
          <Select value={str('faq_icon_pos') || 'right'}
            options={[{ value: 'right', label: 'Right' }, { value: 'left', label: 'Left' }]}
            onChange={(v) => upS({ faq_icon_pos: v })} />
        </PropRow>
        <SliderPropRow label="Size"
          value={str('faq_icon_size')} onChange={(v) => upS({ faq_icon_size: v })}
          min={12} max={40} step={1} units={['px']} placeholder="20" />
        <ColorPropRow label="Color"        value={str('faq_icon_color')}        onChange={(v) => upS({ faq_icon_color: v })}        placeholder="#9ca3af" />
        <ColorPropRow label="Active Color" value={str('faq_icon_active_color')} onChange={(v) => upS({ faq_icon_active_color: v })} placeholder="#6366f1" />
      </PropGroup>

      {/* ── Animation ── */}
      <PropGroup label="Animation" defaultOpen={false}>
        <SliderPropRow label="Duration"
          value={str('faq_anim_dur')} onChange={(v) => upS({ faq_anim_dur: v })}
          min={0.1} max={1} step={0.05} units={['s']} placeholder="0.3" />
        <PropRow label="Easing">
          <Select value={str('faq_anim_ease') || 'ease'}
            options={[
              { value: 'ease',        label: 'Ease'        },
              { value: 'ease-in-out', label: 'Ease In-Out' },
              { value: 'ease-in',     label: 'Ease In'     },
              { value: 'ease-out',    label: 'Ease Out'    },
              { value: 'linear',      label: 'Linear'      },
            ]}
            onChange={(v) => upS({ faq_anim_ease: v })} />
        </PropRow>
      </PropGroup>

      {/* ── Question ── */}
      <PropGroup label="Question" defaultOpen={false}>
        <TypographyTriggerRow label="Typography" isOpen={qTypoOpen} triggerRef={qTypoRef} onToggle={() => setQTypoOpen((o) => !o)} />
        {qTypoOpen && (
          <TypographyPopup styles={qTypoStyles} update={updateQTypo} fonts={jalpiEditorConfig.fonts ?? []} respKey={null} onClose={() => setQTypoOpen(false)} anchorRef={qTypoRef} />
        )}
        <ColorPropRow label="Color"        value={str('faq_q_color')}        onChange={(v) => upS({ faq_q_color: v })}        placeholder="#111827" />
        <ColorPropRow label="Active Color" value={str('faq_q_active_color')} onChange={(v) => upS({ faq_q_active_color: v })} placeholder="#6366f1" />
        <ColorPropRow label="Hover Color"  value={str('faq_q_hover_color')}  onChange={(v) => upS({ faq_q_hover_color: v })}  placeholder="" />
        <PropRow label="Padding V / H" responsive>
          <div style={{ display: 'flex', gap: 6 }}>
            <SliderDimInput value={str('faq_q_pad_v')} onChange={(v) => upS({ faq_q_pad_v: v })} min={0} max={60} step={1} units={['px']} placeholder="18" />
            <SliderDimInput value={str('faq_q_pad_h')} onChange={(v) => upS({ faq_q_pad_h: v })} min={0} max={60} step={1} units={['px']} placeholder="20" />
          </div>
        </PropRow>
      </PropGroup>

      {/* ── Answer ── */}
      <PropGroup label="Answer" defaultOpen={false}>
        <TypographyTriggerRow label="Typography" isOpen={aTypoOpen} triggerRef={aTypoRef} onToggle={() => setATypoOpen((o) => !o)} />
        {aTypoOpen && (
          <TypographyPopup styles={aTypoStyles} update={updateATypo} fonts={jalpiEditorConfig.fonts ?? []} respKey={null} onClose={() => setATypoOpen(false)} anchorRef={aTypoRef} />
        )}
        <ColorPropRow label="Color" value={str('faq_a_color')} onChange={(v) => upS({ faq_a_color: v })} placeholder="#4b5563" />
        <PropRow label="Padding V / H" responsive>
          <div style={{ display: 'flex', gap: 6 }}>
            <SliderDimInput value={str('faq_a_pad_v')} onChange={(v) => upS({ faq_a_pad_v: v })} min={0} max={60} step={1} units={['px']} placeholder="20" />
            <SliderDimInput value={str('faq_a_pad_h')} onChange={(v) => upS({ faq_a_pad_h: v })} min={0} max={60} step={1} units={['px']} placeholder="20" />
          </div>
        </PropRow>
      </PropGroup>

      {/* ── Item Style ── */}
      <PropGroup label="Item Style" defaultOpen={false}>
        <ColorPropRow label="Background"        value={str('faq_item_bg')}        onChange={(v) => upS({ faq_item_bg: v })}        placeholder="" />
        <ColorPropRow label="Active Background" value={str('faq_item_active_bg')} onChange={(v) => upS({ faq_item_active_bg: v })} placeholder="" />
        <ColorPropRow label="Hover Background"  value={str('faq_item_hover_bg')}  onChange={(v) => upS({ faq_item_hover_bg: v })}  placeholder="" />
        <PropRow label="Border Type">
          <Select value={str('faq_item_bd_type') || 'none'}
            options={[
              { value: 'none',   label: 'None'   },
              { value: 'solid',  label: 'Solid'  },
              { value: 'dashed', label: 'Dashed' },
              { value: 'dotted', label: 'Dotted' },
            ]}
            onChange={(v) => upS({ faq_item_bd_type: v })} />
        </PropRow>
        {hasBorder && (
          <>
            <SliderPropRow label="Border Width"
              value={str('faq_item_bd_width')} onChange={(v) => upS({ faq_item_bd_width: v })}
              min={1} max={8} step={1} units={['px']} placeholder="1" />
            <ColorPropRow label="Border Color"        value={str('faq_item_bd_color')}        onChange={(v) => upS({ faq_item_bd_color: v })}        placeholder="#e5e7eb" />
            <ColorPropRow label="Active Border Color" value={str('faq_item_active_bd_color')} onChange={(v) => upS({ faq_item_active_bd_color: v })} placeholder="#6366f1" />
          </>
        )}
        <SliderPropRow label="Border Radius" responsive
          value={str('faq_item_br')} onChange={(v) => upS({ faq_item_br: v })}
          min={0} max={32} step={1} units={['px']} placeholder="10" />
        <PropRow label="Box Shadow">
          <TextInput value={str('faq_item_shadow')} onChange={(v) => upS({ faq_item_shadow: v })} placeholder="0 2px 8px rgba(0,0,0,.08)" />
        </PropRow>
        <PropRow label="Active Shadow">
          <TextInput value={str('faq_item_active_shadow')} onChange={(v) => upS({ faq_item_active_shadow: v })} placeholder="0 4px 16px rgba(99,102,241,.12)" />
        </PropRow>
      </PropGroup>

    </div>
  );
}

/* — Table — */
function TableContent({ node, store }: { node: CBNode; store: EditorStore }) {
  const content = (node.content ?? {}) as { headers?: string[]; rows?: string[][]; caption?: string };
  const headers = content.headers ?? ['Column 1', 'Column 2'];
  const rows    = content.rows    ?? [];
  const update  = (patch: Record<string, unknown>) => store.updateNodeContent(node.id, patch);

  const updateHeader = (idx: number, val: string) =>
    update({ headers: headers.map((h, i) => i === idx ? val : h) });

  const updateCell = (rowIdx: number, colIdx: number, val: string) =>
    update({ rows: rows.map((row, ri) => ri === rowIdx ? row.map((c, ci) => ci === colIdx ? val : c) : row) });

  const addCol = () => {
    update({
      headers: [...headers, `Column ${headers.length + 1}`],
      rows: rows.map((r) => [...r, '']),
    });
  };

  const addRow = () => update({ rows: [...rows, headers.map(() => '')] });

  const removeRow = (idx: number) => update({ rows: rows.filter((_, i) => i !== idx) });

  return (
    <div className="jalpi-props-section">
      <PropGroup label="Table">
        <PropRow label="Caption" stack>
          <TextInput value={content.caption || ''} onChange={(v) => update({ caption: v })} placeholder="Optional caption…" />
        </PropRow>
        <div className="jalpi-table-editor">
          <div className="jalpi-table-editor__head">
            <span className="jalpi-table-editor__label">Headers</span>
            <button type="button" className="jalpi-table-editor__add-col" onClick={addCol}>+ Col</button>
          </div>
          <div className="jalpi-table-editor__headers">
            {headers.map((h, i) => (
              <input key={i} type="text" className="jalpi-input jalpi-table-editor__cell"
                value={h} onChange={(e) => updateHeader(i, e.target.value)} placeholder={`Column ${i + 1}`} />
            ))}
          </div>
          <div className="jalpi-table-editor__rows">
            {rows.map((row, ri) => (
              <div key={ri} className="jalpi-table-editor__row">
                {row.map((cell, ci) => (
                  <input key={ci} type="text" className="jalpi-input jalpi-table-editor__cell"
                    value={cell} onChange={(e) => updateCell(ri, ci, e.target.value)} placeholder="…" />
                ))}
                <button type="button" className="jalpi-repeater__remove" title="Remove row" onClick={() => removeRow(ri)}>
                  <svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
                </button>
              </div>
            ))}
          </div>
          <button type="button" className="jalpi-repeater__add" onClick={addRow}>+ Add row</button>
        </div>
      </PropGroup>
    </div>
  );
}

/* — Feature Card — */
function FeatureCardContent({ node, store }: { node: CBNode; store: EditorStore }) {
  const content    = (node.content ?? {}) as { icon?: string | IconValue; title?: string; text?: string; view?: string; linkUrl?: string; linkNewTab?: boolean };
  const update     = (key: string, val: unknown) => store.updateNodeContent(node.id, { [key]: val });
  const [pickerOpen, setPickerOpen] = useState(false);
  return (
    <div className="jalpi-props-section">
      <PropGroup label="Icon Box">
        {/* Clickable preview — opens icon library */}
        <div
          className="jalpi-icon-box-preview"
          style={{ cursor: 'pointer' }}
          title="Click to choose icon"
          onClick={() => setPickerOpen(true)}
        >
          {content.icon
            ? <RenderIconValue value={content.icon as IconValue} />
            : <svg width="1em" height="1em" viewBox="0 0 24 24" fill="none" stroke="#bbb" strokeWidth="1.5" strokeLinecap="round"><circle cx="12" cy="8" r="5"/><path d="M3 21a9 9 0 0 1 18 0"/></svg>
          }
        </div>
        {pickerOpen && (
          <IconPickerModal
            value={content.icon}
            onChange={(v) => { update('icon', v); setPickerOpen(false); }}
            onClear={content.icon ? () => { update('icon', undefined); setPickerOpen(false); } : undefined}
            onClose={() => setPickerOpen(false)}
          />
        )}
        <IconInput value={content.icon} onChange={(v) => update('icon', v || undefined)} placeholder="Choose icon…" />

        <PropRow label="View" stack>
          <Select
            value={content.view || 'default'}
            options={[
              { value: 'default', label: 'Default' },
              { value: 'stacked', label: 'Stacked' },
              { value: 'framed',  label: 'Framed'  },
            ]}
            onChange={(v) => update('view', v)}
          />
        </PropRow>

        <PropRow label="Title" stack>
          <TextInput value={content.title || ''} onChange={(v) => update('title', v)} placeholder="This is the heading" />
        </PropRow>

        <PropRow label="Description" stack>
          <textarea className="jalpi-textarea" rows={4}
            value={content.text || ''}
            onChange={(e) => update('text', e.target.value)}
            placeholder="Lorem ipsum dolor sit amet…"
          />
        </PropRow>

        <PropRow label="Link" stack>
          <TextInput value={content.linkUrl || ''} onChange={(v) => update('linkUrl', v)} placeholder="Type or paste your URL" />
        </PropRow>
        {content.linkUrl && (
          <PropRow label="New Tab">
            <input type="checkbox" checked={!!content.linkNewTab} onChange={(e) => update('linkNewTab', e.target.checked)} />
          </PropRow>
        )}
      </PropGroup>
    </div>
  );
}

/* — Call To Action — */
function CtaContent({ node, store }: { node: CBNode; store: EditorStore }) {
  const content = (node.content ?? {}) as { title?: string; subtitle?: string; primaryText?: string; primaryUrl?: string; secondaryText?: string; secondaryUrl?: string };
  const update  = (key: string, val: string) => store.updateNodeContent(node.id, { [key]: val });
  return (
    <div className="jalpi-props-section">
      <PropGroup label="Call To Action">
        <PropRow label="Headline" stack>
          <TextInput value={content.title || ''} onChange={(v) => update('title', v)} placeholder="Ready to get started?" />
        </PropRow>
        <PropRow label="Subtext" stack>
          <textarea className="jalpi-textarea" rows={2}
            value={content.subtitle || ''}
            onChange={(e) => update('subtitle', e.target.value)}
            placeholder="Supporting description…"
          />
        </PropRow>
      </PropGroup>
      <PropGroup label="Primary Button" defaultOpen={false}>
        <PropRow label="Label" stack>
          <TextInput value={content.primaryText || ''} onChange={(v) => update('primaryText', v)} placeholder="Get Started" />
        </PropRow>
        <PropRow label="URL" stack>
          <TextInput value={content.primaryUrl || '#'} onChange={(v) => update('primaryUrl', v)} placeholder="https://…" />
        </PropRow>
      </PropGroup>
      <PropGroup label="Secondary Button" defaultOpen={false}>
        <PropRow label="Label" stack>
          <TextInput value={content.secondaryText || ''} onChange={(v) => update('secondaryText', v)} placeholder="Learn more" />
        </PropRow>
        <PropRow label="URL" stack>
          <TextInput value={content.secondaryUrl || '#'} onChange={(v) => update('secondaryUrl', v)} placeholder="https://…" />
        </PropRow>
      </PropGroup>
    </div>
  );
}

/* — Stats Card — */
function StatsCardContent({ node, store }: { node: CBNode; store: EditorStore }) {
  const content = (node.content ?? {}) as { value?: string; label?: string; sublabel?: string };
  const update  = (key: string, val: string) => store.updateNodeContent(node.id, { [key]: val });
  return (
    <div className="jalpi-props-section">
      <PropGroup label="Stats">
        <PropRow label="Value" stack>
          <TextInput value={content.value || ''} onChange={(v) => update('value', v)} placeholder="10K+" />
        </PropRow>
        <PropRow label="Label" stack>
          <TextInput value={content.label || ''} onChange={(v) => update('label', v)} placeholder="Happy Customers" />
        </PropRow>
        <PropRow label="Sub-label" stack>
          <TextInput value={content.sublabel || ''} onChange={(v) => update('sublabel', v)} placeholder="Worldwide" />
        </PropRow>
      </PropGroup>
    </div>
  );
}

/* — Timeline — */
function TimelineContent({ node, store }: { node: CBNode; store: EditorStore }) {
  const content = (node.content ?? {}) as { items?: Array<{ date: string; title: string; text: string }> };
  const items   = content.items ?? [];
  const save    = (next: typeof items) => store.updateNodeContent(node.id, { items: next });

  return (
    <div className="jalpi-props-section">
      <PropGroup label="Timeline">
        <ItemRepeater
          items={items}
          onChange={save}
          newItem={{ date: '', title: '', text: '' }}
          addLabel="+ Add event"
          renderItem={(item, _, upd) => (
            <>
              <PropRow label="Date" stack>
                <TextInput value={item.date} onChange={(v) => upd('date', v)} placeholder="2025" />
              </PropRow>
              <PropRow label="Title" stack>
                <TextInput value={item.title} onChange={(v) => upd('title', v)} placeholder="Event title…" />
              </PropRow>
              <PropRow label="Description" stack>
                <textarea className="jalpi-textarea" rows={2}
                  value={item.text}
                  onChange={(e) => upd('text', e.target.value)}
                  placeholder="Event details…"
                />
              </PropRow>
            </>
          )}
        />
      </PropGroup>
    </div>
  );
}

/* — Testimonials Carousel — */
type TestimonialItem = { text: string; name: string; role: string; avatarUrl: string; rating: number };

function StarRatingInput({ value, onChange }: { value: number; onChange: (v: number) => void }) {
  const [hov, setHov] = React.useState(0);
  return (
    <div style={{ display: 'flex', gap: 4 }}>
      {[1,2,3,4,5].map((n) => (
        <span key={n}
          style={{ fontSize: 22, cursor: 'pointer', color: n <= (hov || value) ? '#f59e0b' : '#d1d5db', transition: 'color .1s', lineHeight: 1 }}
          onMouseEnter={() => setHov(n)} onMouseLeave={() => setHov(0)}
          onClick={() => onChange(n)}
        >★</span>
      ))}
    </div>
  );
}

function TestimonialsCarouselContent({ node, store }: { node: CBNode; store: EditorStore }) {
  const content = (node.content ?? {}) as { layout?: string; cols?: number; autoplay?: number; items?: TestimonialItem[] };
  const items   = content.items ?? [];
  const autoplay = content.autoplay ?? 4000;
  const isCarousel = (content.layout || 'grid') === 'carousel';
  const [openIdx, setOpenIdx] = React.useState<number | null>(items.length > 0 ? 0 : null);
  const update = (patch: Record<string, unknown>) => store.updateNodeContent(node.id, patch);
  const save   = (next: TestimonialItem[]) => store.updateNodeContent(node.id, { items: next });
  const setField = (idx: number, key: keyof TestimonialItem, val: unknown) =>
    save(items.map((it, i) => i === idx ? { ...it, [key]: val } : it));
  const addItem = () => {
    const next = [...items, { text: '', name: '', role: '', avatarUrl: '', rating: 5 }];
    save(next);
    setOpenIdx(next.length - 1);
  };
  const removeAt = (idx: number) => {
    save(items.filter((_, i) => i !== idx));
    if (openIdx === idx) setOpenIdx(null);
    else if (openIdx !== null && openIdx > idx) setOpenIdx(openIdx - 1);
  };
  const moveUp = (idx: number) => {
    if (idx === 0) return;
    const next = [...items];
    [next[idx - 1], next[idx]] = [next[idx], next[idx - 1]];
    save(next);
    setOpenIdx(idx - 1);
  };

  return (
    <div className="jalpi-props-section">
      <PropGroup label="Display" defaultOpen>
        <PropRow label="Layout">
          <div style={{ display: 'flex', gap: 4 }}>
            {(['grid', 'carousel'] as const).map((v) => (
              <button key={v}
                className={'jalpi-btn-seg' + ((content.layout || 'grid') === v ? ' is-active' : '')}
                onClick={() => update({ layout: v })}
              >{v === 'grid' ? 'Grid' : 'Carousel'}</button>
            ))}
          </div>
        </PropRow>
        {!isCarousel && (
          <PropRow label="Columns">
            <div style={{ display: 'flex', gap: 4 }}>
              {[1, 2, 3].map((n) => (
                <button key={n}
                  className={'jalpi-btn-seg' + ((content.cols ?? 3) === n ? ' is-active' : '')}
                  onClick={() => update({ cols: n })}
                >{n}</button>
              ))}
            </div>
          </PropRow>
        )}
        {isCarousel && (
          <>
            <PropRow label="Autoplay">
              <Toggle checked={autoplay > 0} onChange={(v) => update({ autoplay: v ? 4000 : 0 })} />
            </PropRow>
            {autoplay > 0 && (
              <SliderPropRow label="Interval (s)"
                value={String(Math.round(autoplay / 1000))}
                onChange={(v) => update({ autoplay: Math.max(1, parseInt(v) || 4) * 1000 })}
                min={1} max={15} step={1} units={['']} placeholder="4"
              />
            )}
          </>
        )}
      </PropGroup>

      <PropGroup label="Items" defaultOpen>
        {items.length === 0 && (
          <div style={{ textAlign: 'center', color: '#9ca3af', fontSize: 11, padding: '8px 0' }}>No items yet — add one below</div>
        )}
        {items.map((item, idx) => {
          const isOpen = openIdx === idx;
          const stars  = Math.max(1, Math.min(5, item.rating ?? 5));
          return (
            <div key={idx} className={`jalpi-acc-item-row${isOpen ? ' is-open' : ''}`}>
              <div className="jalpi-acc-item-row__head">
                <button type="button" className="jalpi-acc-item-row__toggle"
                  onClick={() => setOpenIdx(isOpen ? null : idx)}
                  title={isOpen ? 'Collapse' : 'Expand'}>
                  <svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round"
                    style={{ transform: isOpen ? 'rotate(180deg)' : 'rotate(0deg)', transition: 'transform .15s' }}>
                    <polyline points="6 9 12 15 18 9"/>
                  </svg>
                </button>
                <span className="jalpi-acc-item-row__label">
                  <span style={{ color: '#f59e0b', letterSpacing: 1, fontSize: 10 }}>{'★'.repeat(stars)}</span>
                  {'  '}{item.name || `Testimonial ${idx + 1}`}
                </span>
                <button type="button" className="jalpi-acc-item-row__btn" title="Move up"
                  onClick={() => moveUp(idx)} disabled={idx === 0}
                  style={{ opacity: idx === 0 ? 0.3 : 1 }}>
                  <svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
                    <polyline points="18 15 12 9 6 15"/>
                  </svg>
                </button>
                <button type="button" className="jalpi-acc-item-row__btn jalpi-acc-item-row__btn--del" title="Remove"
                  onClick={() => removeAt(idx)}>
                  <svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round">
                    <line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/>
                  </svg>
                </button>
              </div>
              {isOpen && (
                <div className="jalpi-acc-item-row__body">
                  <PropRow label="Quote" stack>
                    <textarea className="jalpi-textarea" rows={3}
                      value={item.text}
                      onChange={(e) => setField(idx, 'text', e.target.value)}
                      placeholder="What did they say about you?"
                    />
                  </PropRow>
                  <PropRow label="Rating" stack>
                    <StarRatingInput value={item.rating ?? 5} onChange={(v) => setField(idx, 'rating', v)} />
                  </PropRow>
                  <PropRow label="Avatar" stack>
                    <MediaPickerInput value={item.avatarUrl} onChange={(v) => setField(idx, 'avatarUrl', v)} />
                  </PropRow>
                  <PropRow label="Name" stack>
                    <TextInput value={item.name} onChange={(v) => setField(idx, 'name', v)} placeholder="Full name" />
                  </PropRow>
                  <PropRow label="Role / Company" stack>
                    <TextInput value={item.role} onChange={(v) => setField(idx, 'role', v)} placeholder="CEO, Acme Corp" />
                  </PropRow>
                </div>
              )}
            </div>
          );
        })}
        <button className="jalpi-add-item-btn" onClick={addItem}>+ Add Testimonial</button>
      </PropGroup>
    </div>
  );
}

/* — Tabs — */
function TabsContent({ node, store }: { node: CBNode; store: EditorStore }) {
  type TabItem = { label: string; content: string };
  const content = (node.content ?? {}) as { items?: TabItem[]; tabStyle?: string };
  const items   = content.items ?? [];
  const update  = (patch: Record<string, unknown>) => store.updateNodeContent(node.id, patch);
  const save    = (next: TabItem[]) => update({ items: next });
  return (
    <div className="jalpi-props-section">
      <PropGroup label="Tabs">
        <PropRow label="Style" stack>
          <Select
            value={content.tabStyle || 'underline'}
            options={[
              { value: 'underline', label: 'Underline' },
              { value: 'boxed',     label: 'Boxed' },
              { value: 'pill',      label: 'Pill' },
            ]}
            onChange={(v) => update({ tabStyle: v })}
          />
        </PropRow>
        <ItemRepeater
          items={items}
          onChange={save}
          newItem={{ label: 'New Tab', content: '' }}
          addLabel="+ Add tab"
          renderItem={(item, _, upd) => (
            <>
              <PropRow label="Label" stack>
                <TextInput value={item.label} onChange={(v) => upd('label', v)} placeholder="Tab name" />
              </PropRow>
              <PropRow label="Content" stack>
                <textarea className="jalpi-textarea" rows={3} value={item.content}
                  onChange={(e) => upd('content', e.target.value)}
                  placeholder="Tab content…" />
              </PropRow>
            </>
          )}
        />
      </PropGroup>
    </div>
  );
}

/* — Tabs (Advanced) — each tab's widgets live in a container child; this panel
     edits ONLY tab metadata (title / icon / css id) and add/remove/duplicate.
     Add/remove/duplicate route through the store so items[] and children[] stay
     in lockstep. Editing title/icon/cssId is content-only (safe direct edit). */
/* =========================================================
   MENU — content controls.

   Source is either a WordPress menu (fetched from the REST endpoint so the
   picker lists what the site actually has) or a custom link tree edited here.
   ========================================================= */

/* =========================================================
   BREADCRUMBS — content + style.
   The trail itself is derived server-side from the queried page, so there is
   nothing to author here beyond presentation.
   ========================================================= */

function BreadcrumbsContent({ node, store }: { node: CBNode; store: EditorStore }) {
  const c = (node.content ?? {}) as Record<string, unknown>;
  const upd = (patch: Record<string, unknown>) => store.updateNodeContent(node.id, patch);

  return (
    <div className="jalpi-props-section">
      <PropGroup label="Breadcrumbs" defaultOpen standalone>
        <PropRow label="Separator">
          <Select
            value={(c.separator as string) || 'chevron'}
            options={[
              { value: 'chevron', label: '›  Chevron' },
              { value: 'slash', label: '/  Slash' },
              { value: 'arrow', label: '→  Arrow' },
              { value: 'dot', label: '·  Dot' },
              { value: 'pipe', label: '|  Pipe' },
            ]}
            onChange={(v) => upd({ separator: v })}
          />
        </PropRow>

        <PropRow label="Show Home">
          <Toggle checked={c.showHome !== false} onChange={(v) => upd({ showHome: v })} />
        </PropRow>

        {c.showHome !== false && (
          <PropRow label="Home Label" stack>
            <TextInput
              value={(c.homeLabel as string) ?? ''}
              onChange={(v) => upd({ homeLabel: v })}
              placeholder="Home"
            />
          </PropRow>
        )}

        <PropRow label="Show Current Page">
          <Toggle checked={c.showCurrent !== false} onChange={(v) => upd({ showCurrent: v })} />
        </PropRow>

        <PropRow label="ARIA Label" stack>
          <TextInput
            value={(c.ariaLabel as string) ?? ''}
            onChange={(v) => upd({ ariaLabel: v })}
            placeholder="Breadcrumb"
          />
        </PropRow>

        <p className="jalpi-ia-hint">
          The trail is built from the real page when published — the canvas shows a sample.
          Includes schema.org markup so search engines can display it.
        </p>
      </PropGroup>
    </div>
  );
}

function BreadcrumbsStyleTab({ node, store }: { node: CBNode; store: EditorStore }) {
  const s = (node.settings ?? {}) as Record<string, unknown>;
  const str = (k: string) => (s[k] as string) || '';
  const upd = (patch: Record<string, unknown>) =>
    store.updateNode(node.id, { settings: { ...s, ...patch } });

  return (
    <div className="jalpi-props-section">
      <PropGroup label="Colors" defaultOpen standalone>
        <PropRow label="Link">
          <ColorInput value={str('bc_link_color')} onChange={(v) => upd({ bc_link_color: v })} />
        </PropRow>
        <PropRow label="Link Hover">
          <ColorInput value={str('bc_link_hover')} onChange={(v) => upd({ bc_link_hover: v })} />
        </PropRow>
        <PropRow label="Current Page">
          <ColorInput value={str('bc_current_color')} onChange={(v) => upd({ bc_current_color: v })} />
        </PropRow>
        <PropRow label="Separator">
          <ColorInput value={str('bc_sep_color')} onChange={(v) => upd({ bc_sep_color: v })} />
        </PropRow>
      </PropGroup>

      <PropGroup label="Typography & Spacing" defaultOpen={false} standalone>
        <SliderPropRow
          label="Font Size"
          value={str('bc_font_size')}
          onChange={(v) => upd({ bc_font_size: v })}
          min={8}
          max={40}
        />
        <SliderPropRow
          label="Gap"
          value={str('bc_gap')}
          onChange={(v) => upd({ bc_gap: v })}
          min={0}
          max={40}
        />
      </PropGroup>
    </div>
  );
}

type WpMenuOption = { id: number; name: string; count: number };

/* =========================================================
   MENU — Content tab.
   One group holding exactly the widget's content controls.
   ========================================================= */

type AdvMenuItem = { id?: number; label: string; hasChildren: boolean };
type AdvMegaCfg = { enabled?: boolean; cols?: number; panelIndex?: number };

/**
 * Advanced Menu — Content tab.
 *
 * The mega-panel section lists the chosen menu's top-level items and lets each
 * one be given a panel. Panel CONTENT is authored on the canvas (the panel is a
 * real container child), so this only owns which items have panels and how many
 * columns each uses.
 */
function AdvMenuContent({ node, store }: { node: CBNode; store: EditorStore }) {
  const c = (node.content ?? {}) as Record<string, unknown>;
  const upd = (patch: Record<string, unknown>) => store.updateNodeContent(node.id, patch);

  const [menus, setMenus] = useState<WpMenuOption[]>([]);
  const [items, setItems] = useState<AdvMenuItem[]>([]);
  const [loading, setLoading] = useState(false);

  const menuId = Number(c.menuId ?? 0);
  const mega = (c.megaItems ?? {}) as Record<string, AdvMegaCfg>;

  useEffect(() => {
    setLoading(true);

    // The editor's config global is `jalpiEditorConfig` (see
    // Jalpi_Editor::enqueue_editor). Using the wrong name silently yields
    // undefined and the request never fires.
    const w = window as unknown as { jalpiEditorConfig?: { apiUrl?: string; nonce?: string } };
    const base = w.jalpiEditorConfig?.apiUrl;
    if (!base) { setLoading(false); return; }

    let cancelled = false;
    fetch(`${base}/nav-menus`, {
      credentials: 'same-origin',
      headers: w.jalpiEditorConfig?.nonce ? { 'X-WP-Nonce': w.jalpiEditorConfig.nonce } : {},
    })
      .then((r) => r.json())
      .then((d) => {
        if (cancelled) return;
        const list = Array.isArray(d?.menus) ? d.menus : [];
        setMenus(list);
        const found = list.find((m: { id: number }) => m.id === menuId);
        setItems(found?.items ?? []);
      })
      .catch(() => { if (!cancelled) { setMenus([]); setItems([]); } })
      .finally(() => { if (!cancelled) setLoading(false); });

    return () => { cancelled = true; };
  }, [menuId]);

  return (
    <div className="jalpi-props-section">
      <PropGroup label="Menu Source" defaultOpen standalone>
        <PropRow label="Navigation Menu">
          <Select
            value={String(menuId)}
            options={[
              { value: '0', label: loading ? 'Loading…' : 'Select a menu…' },
              ...menus.map((m) => ({ value: String(m.id), label: `${m.name} (${m.count})` })),
            ]}
            onChange={(v) => upd({ menuId: parseInt(v, 10) || 0 })}
          />
        </PropRow>
        {!loading && menus.length === 0 && (
          <p className="jalpi-ia-hint">
            No menus found. Create one under Appearance → Menus.
          </p>
        )}
      </PropGroup>

      <PropGroup label="Layout" defaultOpen standalone>
        <PropRow label="Menu Layout">
          <Select
            value={(c.layout as string) || 'horizontal'}
            options={[
              { value: 'horizontal', label: 'Horizontal' },
              { value: 'vertical', label: 'Vertical' },
            ]}
            onChange={(v) => upd({ layout: v })}
          />
        </PropRow>
        <PropRow label="Menu Alignment">
          <Select
            value={(c.align as string) || 'left'}
            options={[
              { value: 'left', label: 'Left' },
              { value: 'center', label: 'Center' },
              { value: 'right', label: 'Right' },
              { value: 'justify', label: 'Justify' },
            ]}
            onChange={(v) => upd({ align: v })}
          />
        </PropRow>
        <PropRow label="Submenu Indicator">
          <Select
            value={(c.submenuIndicator as string) || 'arrow'}
            options={[
              { value: 'arrow', label: 'Arrow' },
              { value: 'plus', label: 'Plus / Minus' },
              { value: 'none', label: 'None' },
            ]}
            onChange={(v) => upd({ submenuIndicator: v })}
          />
        </PropRow>
        <PropRow label="Menu Width">
          <TextInput
            value={(c.menuWidth as string) ?? ''}
            placeholder="e.g. 100%"
            onChange={(v) => upd({ menuWidth: v })}
          />
        </PropRow>
        <PropRow label="Item Spacing">
          <TextInput
            value={(c.itemSpacing as string) ?? ''}
            placeholder="e.g. 8px"
            onChange={(v) => upd({ itemSpacing: v })}
          />
        </PropRow>
      </PropGroup>

      {/* Appearance of the mega panels. Which ITEMS get one is set further
          down under Mega Navigation. */}
      <PropGroup label="Mega Panel Appearance" defaultOpen={false} standalone>
        <PropRow label="Trigger">
          <Select
            value={(c.megaTrigger as string) || 'hover'}
            options={[
              { value: 'hover', label: 'Hover' },
              { value: 'click', label: 'Click' },
            ]}
            onChange={(v) => upd({ megaTrigger: v })}
          />
        </PropRow>
        <PropRow label="Panel Width">
          <Select
            value={(c.panelWidth as string) || 'container'}
            options={[
              { value: 'container', label: 'Container Width' },
              { value: 'full', label: 'Full Width' },
              { value: 'custom', label: 'Custom Width' },
            ]}
            onChange={(v) => upd({ panelWidth: v })}
          />
        </PropRow>
        {(c.panelWidth as string) === 'custom' && (
          <PropRow label="Custom Width">
            <TextInput
              value={(c.panelCustomWidth as string) ?? ''}
              placeholder="e.g. 720px"
              onChange={(v) => upd({ panelCustomWidth: v })}
            />
          </PropRow>
        )}
        <PropRow label="Dropdown Position">
          <Select
            value={(c.dropdownPosition as string) || 'left'}
            options={[
              { value: 'left', label: 'Left' },
              { value: 'center', label: 'Center' },
              { value: 'right', label: 'Right' },
            ]}
            onChange={(v) => upd({ dropdownPosition: v })}
          />
        </PropRow>

      </PropGroup>

      {/* Per-item Mega Navigation switches. This menu renders as an ordinary
          WordPress navigation menu — normal dropdowns, unlimited nesting —
          until an item is switched on here. Panel CONTENT is authored on the
          canvas: enable an item, then click it to open its drop target. */}
      <PropGroup label="Mega Navigation" defaultOpen standalone>
        {menuId === 0 ? (
          <p className="jalpi-ia-hint">Choose a menu above to enable Mega Navigation on its items.</p>
        ) : items.length === 0 ? (
          <p className="jalpi-ia-hint">{loading ? 'Loading menu items…' : 'This menu has no top-level items.'}</p>
        ) : (
          <>
            <p className="jalpi-ia-hint">
              Off by default — every item behaves as a normal dropdown. Turn one on to
              replace its submenu with a mega panel, then click that item on the canvas
              to drop elements into it.
            </p>
            {items.map((item, i) => {
              const itemId = Number(item.id ?? 0);
              if (!itemId) return null;
              const cfg = mega[String(itemId)];
              const on = !!cfg?.enabled;
              return (
                <div
                  key={itemId || i}
                  style={{ padding: '6px 0', borderBottom: '1px solid #f0f0f1' }}
                >
                  <PropRow label={item.label || `Item ${i + 1}`}>
                    <Toggle
                      checked={on}
                      onChange={(v) => store.setAdvMenuMega(node.id, itemId, v)}
                    />
                  </PropRow>
                  {on ? (
                    <PropRow label="Columns">
                      <Select
                        value={String(cfg?.cols ?? 3)}
                        options={Array.from({ length: 12 }, (_, n) => ({
                          value: String(n + 1),
                          label: `${n + 1} Column${n === 0 ? '' : 's'}`,
                        }))}
                        onChange={(v) => store.setAdvMenuMegaCols(node.id, itemId, parseInt(v, 10) || 3)}
                      />
                    </PropRow>
                  ) : (
                    // Naming the fallback removes the main ambiguity: an item
                    // with children keeps its dropdown, one without stays a
                    // plain link.
                    <p className="jalpi-ia-hint" style={{ margin: '0 0 4px' }}>
                      {item.hasChildren ? 'Normal dropdown submenu' : 'Plain link (no submenu)'}
                    </p>
                  )}
                </div>
              );
            })}
          </>
        )}
      </PropGroup>

      <PropGroup label="Animation" defaultOpen={false} standalone>
        <PropRow label="Animation">
          <Select
            value={(c.animation as string) || 'fade'}
            options={[
              { value: 'fade', label: 'Fade' },
              { value: 'slide-down', label: 'Slide Down' },
              { value: 'slide-up', label: 'Slide Up' },
              { value: 'zoom', label: 'Zoom' },
              { value: 'scale', label: 'Scale' },
              { value: 'none', label: 'None' },
            ]}
            onChange={(v) => upd({ animation: v })}
          />
        </PropRow>
        <SliderPropRow
          label="Duration (ms)"
          value={String(c.animationDuration ?? 250)}
          onChange={(v) => upd({ animationDuration: parseInt(v, 10) || 0 })}
          min={0}
          max={1500}
          step={10}
        />
        <SliderPropRow
          label="Open Delay (ms)"
          value={String(c.openDelay ?? 100)}
          onChange={(v) => upd({ openDelay: parseInt(v, 10) || 0 })}
          min={0}
          max={1000}
          step={10}
        />
        <SliderPropRow
          label="Close Delay (ms)"
          value={String(c.closeDelay ?? 200)}
          onChange={(v) => upd({ closeDelay: parseInt(v, 10) || 0 })}
          min={0}
          max={1000}
          step={10}
        />
      </PropGroup>

      <PropGroup label="Mobile" defaultOpen={false} standalone>
        <SliderPropRow
          label="Breakpoint (px)"
          value={String(c.breakpoint ?? 1024)}
          onChange={(v) => upd({ breakpoint: parseInt(v, 10) || 1024 })}
          min={320}
          max={1440}
          step={1}
        />
        <PropRow label="Mobile Layout">
          <Select
            value={(c.mobileLayout as string) || 'accordion'}
            options={[
              { value: 'accordion', label: 'Accordion' },
              { value: 'offcanvas', label: 'Off Canvas' },
              { value: 'dropdown', label: 'Dropdown' },
            ]}
            onChange={(v) => upd({ mobileLayout: v })}
          />
        </PropRow>
        <PropRow label="Sticky Mobile Header">
          <Toggle
            checked={!!c.stickyMobileHeader}
            onChange={(v) => upd({ stickyMobileHeader: v })}
          />
        </PropRow>
      </PropGroup>

      <PropGroup label="Accessibility" defaultOpen={false} standalone>
        <PropRow label="ARIA Label">
          <TextInput
            value={(c.ariaLabel as string) ?? ''}
            placeholder="Main navigation"
            onChange={(v) => upd({ ariaLabel: v })}
          />
        </PropRow>
      </PropGroup>
    </div>
  );
}

function MenuContent({ node, store }: { node: CBNode; store: EditorStore }) {
  const c = (node.content ?? {}) as Record<string, unknown>;
  const upd = (patch: Record<string, unknown>) => store.updateNodeContent(node.id, patch);

  const [menus, setMenus] = useState<WpMenuOption[]>([]);
  const [loading, setLoading] = useState(false);

  useEffect(() => {
    setLoading(true);

    // The editor's config global is `jalpiEditorConfig` (see
    // Jalpi_Editor::enqueue_editor). Using the wrong name silently yields
    // undefined and the request never fires.
    const w = window as unknown as { jalpiEditorConfig?: { apiUrl?: string; nonce?: string } };
    const base = w.jalpiEditorConfig?.apiUrl;
    if (!base) { setLoading(false); return; }

    let cancelled = false;
    fetch(`${base}/nav-menus`, {
      credentials: 'same-origin',
      headers: w.jalpiEditorConfig?.nonce ? { 'X-WP-Nonce': w.jalpiEditorConfig.nonce } : {},
    })
      .then((r) => r.json())
      .then((d) => { if (!cancelled) setMenus(Array.isArray(d?.menus) ? d.menus : []); })
      .catch(() => { if (!cancelled) setMenus([]); })
      .finally(() => { if (!cancelled) setLoading(false); });

    return () => { cancelled = true; };
  }, []);

  return (
    <div className="jalpi-props-section">
      <PropGroup label="Menu" defaultOpen standalone>
        <PropRow label="Navigation Menu">
          <Select
            value={String(c.menuId ?? 0)}
            options={[
              { value: '0', label: loading ? 'Loading…' : 'Select a menu…' },
              ...menus.map((m) => ({ value: String(m.id), label: `${m.name} (${m.count})` })),
            ]}
            onChange={(v) => upd({ menuId: parseInt(v, 10) || 0 })}
          />
        </PropRow>
        {!loading && menus.length === 0 && (
          <p className="jalpi-ia-hint">
            No menus found. Create one under Appearance → Menus.
          </p>
        )}

        <PropRow label="Menu Layout">
          <Select
            value={(c.layout as string) || 'horizontal'}
            options={[
              { value: 'horizontal', label: 'Horizontal' },
              { value: 'vertical', label: 'Vertical' },
            ]}
            onChange={(v) => upd({ layout: v })}
          />
        </PropRow>

        <PropRow label="Dropdown Menu">
          <Select
            value={(c.submenuIcon as string) || 'chevron'}
            options={[
              { value: 'chevron', label: 'Arrow' },
              { value: 'caret', label: 'Caret' },
              { value: 'plus', label: 'Plus / Minus' },
              { value: 'none', label: 'No Indicator' },
            ]}
            onChange={(v) => upd({ submenuIcon: v })}
          />
        </PropRow>

        <PropRow label="Show Submenus">
          <Toggle
            checked={c.showSubmenus !== false}
            onChange={(v) => upd({ showSubmenus: v })}
          />
        </PropRow>

        <PropRow label="Dropdown Trigger">
          <Select
            value={(c.dropdownTrigger as string) || 'hover'}
            options={[
              { value: 'hover', label: 'Hover' },
              { value: 'click', label: 'Click' },
            ]}
            onChange={(v) => upd({ dropdownTrigger: v })}
          />
        </PropRow>

        <PropRow label="Responsive Menu">
          <Select
            value={(c.responsiveMenu as string) || 'dropdown'}
            options={[
              { value: 'dropdown', label: 'Collapse to Toggle' },
              { value: 'none', label: 'Always Show Full Menu' },
            ]}
            onChange={(v) => upd({ responsiveMenu: v })}
          />
        </PropRow>

        <SliderPropRow
          label="Mobile Breakpoint"
          value={String(c.mobileBreakpoint ?? 768)}
          onChange={(v) => upd({ mobileBreakpoint: parseInt(v, 10) || 768 })}
          min={320}
          max={1440}
          step={1}
          units={['px']}
        />

        <PropRow label="Menu Alignment">
          <Select
            value={(c.align as string) || 'left'}
            options={[
              { value: 'left', label: 'Left' },
              { value: 'center', label: 'Center' },
              { value: 'right', label: 'Right' },
              { value: 'space-between', label: 'Space Between' },
            ]}
            onChange={(v) => upd({ align: v })}
          />
        </PropRow>

        <SliderPropRow
          label="Menu Width"
          value={(c.menuWidth as string) ?? ''}
          onChange={(v) => upd({ menuWidth: v })}
          min={0}
          max={1600}
        />

        <SliderPropRow
          label="Item Spacing"
          value={(c.itemSpacing as string) ?? ''}
          onChange={(v) => upd({ itemSpacing: v })}
          min={0}
          max={80}
        />

        <PropRow label="ARIA Label" stack>
          <TextInput
            value={(c.ariaLabel as string) ?? ''}
            onChange={(v) => upd({ ariaLabel: v })}
            placeholder="Main navigation"
          />
        </PropRow>
      </PropGroup>
    </div>
  );
}

function TabsAdvContent({ node, store }: { node: CBNode; store: EditorStore }) {
  type TabItem = { id?: string; title?: string; icon?: IconValue | ''; cssId?: string };
  const content = (node.content ?? {}) as { items?: TabItem[]; tabStyle?: string };
  const items   = content.items ?? [];
  const setMeta = (i: number, key: string, val: unknown) => {
    const next = items.map((it, idx) => (idx === i ? { ...it, [key]: val } : it));
    store.updateNodeContent(node.id, { items: next });
  };
  return (
    <div className="jalpi-props-section">
      <PropGroup label="Tabs">
        <PropRow label="Style" stack>
          <Select
            value={content.tabStyle || 'underline'}
            options={[
              { value: 'underline', label: 'Underline' },
              { value: 'boxed',     label: 'Boxed' },
              { value: 'pill',      label: 'Pill' },
            ]}
            onChange={(v) => store.updateNodeContent(node.id, { tabStyle: v })}
          />
        </PropRow>

        <div className="jalpi-repeater">
          {items.map((item, i) => (
            <div key={item.id ?? i} className="jalpi-repeater__item">
              <div className="jalpi-repeater__item-header">
                <span className="jalpi-repeater__item-num">{item.title || `Tab #${i + 1}`}</span>
                <button type="button" className="jalpi-repeater__remove" title="Duplicate" onClick={() => store.duplicateTab(node.id, i)}>
                  <svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>
                </button>
                <button type="button" className="jalpi-repeater__remove" title="Delete" onClick={() => store.removeTab(node.id, i)}>
                  <svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
                </button>
              </div>
              <div className="jalpi-repeater__item-body">
                <PropRow label="Title" stack>
                  <TextInput value={item.title || ''} onChange={(v) => setMeta(i, 'title', v)} placeholder="Tab title" />
                </PropRow>
                <PropRow label="Icon" stack>
                  <IconInput value={(item.icon as IconValue | '') || ''} onChange={(v) => setMeta(i, 'icon', v || '')} placeholder="Choose icon…" />
                </PropRow>
                <PropRow label="CSS ID" stack>
                  <TextInput value={item.cssId || ''} onChange={(v) => setMeta(i, 'cssId', v)} placeholder="" />
                </PropRow>
              </div>
            </div>
          ))}
          <button type="button" className="jalpi-repeater__add" onClick={() => store.addTab(node.id)}>+ Add Tab</button>
        </div>
      </PropGroup>
    </div>
  );
}

/* — Image Box — */
function ImageBoxContent({ node, store }: { node: CBNode; store: EditorStore }) {
  const content = (node.content ?? {}) as { url?: string; alt?: string; title?: string; description?: string; titleTag?: string; imagePosition?: string; linkUrl?: string };
  const update  = (key: string, val: string) => store.updateNodeContent(node.id, { [key]: val });
  return (
    <div className="jalpi-props-section">
      <PropGroup label="Image">
        <PropRow label="Image" stack>
          <MediaPickerInput value={content.url || ''} onChange={(v) => update('url', v)} />
        </PropRow>
        <PropRow label="Alt Text" stack>
          <TextInput value={content.alt || ''} onChange={(v) => update('alt', v)} placeholder="Alt text" />
        </PropRow>
        <PropRow label="Position" stack>
          <Select
            value={content.imagePosition || 'top'}
            options={[
              { value: 'top',   label: 'Top' },
              { value: 'left',  label: 'Left' },
              { value: 'right', label: 'Right' },
            ]}
            onChange={(v) => update('imagePosition', v)}
          />
        </PropRow>
        <PropRow label="Link URL" stack>
          <TextInput value={content.linkUrl || ''} onChange={(v) => update('linkUrl', v)} placeholder="https://…" />
        </PropRow>
      </PropGroup>
      <PropGroup label="Text">
        <PropRow label="Title" stack>
          <TextInput value={content.title || ''} onChange={(v) => update('title', v)} placeholder="Image title" />
        </PropRow>
        <PropRow label="Title Tag" stack>
          <Select
            value={content.titleTag || 'h3'}
            options={['h2','h3','h4','h5','h6'].map((t) => ({ value: t, label: t.toUpperCase() }))}
            onChange={(v) => update('titleTag', v)}
          />
        </PropRow>
        <PropRow label="Description" stack>
          <textarea className="jalpi-textarea" rows={3} value={content.description || ''}
            onChange={(e) => update('description', e.target.value)}
            placeholder="Image description…" />
        </PropRow>
      </PropGroup>
    </div>
  );
}

/* — Social Icons — */
const PLATFORM_OPTIONS = [
  { value: 'facebook',  label: 'Facebook'   },
  { value: 'instagram', label: 'Instagram'  },
  { value: 'twitter',   label: 'Twitter / X' },
  { value: 'linkedin',  label: 'LinkedIn'   },
  { value: 'youtube',   label: 'YouTube'    },
  { value: 'tiktok',    label: 'TikTok'    },
  { value: 'pinterest', label: 'Pinterest'  },
  { value: 'github',    label: 'GitHub'     },
  { value: 'email',     label: 'Email'      },
  { value: 'whatsapp',  label: 'WhatsApp'   },
];

function SocialIconsContent({ node, store }: { node: CBNode; store: EditorStore }) {
  type SocialItem = { platform: string; url: string };
  const content = (node.content ?? {}) as { items?: SocialItem[]; iconStyle?: string; iconSize?: string; align?: string; color?: string };
  const items   = content.items ?? [];
  const update  = (patch: Record<string, unknown>) => store.updateNodeContent(node.id, patch);
  const save    = (next: SocialItem[]) => update({ items: next });
  return (
    <div className="jalpi-props-section">
      <PropGroup label="Display">
        <PropRow label="Style" stack>
          <Select
            value={content.iconStyle || 'circle'}
            options={[
              { value: 'circle',  label: 'Circle'  },
              { value: 'square',  label: 'Square'  },
              { value: 'minimal', label: 'Minimal' },
            ]}
            onChange={(v) => update({ iconStyle: v })}
          />
        </PropRow>
        <PropRow label="Size" stack>
          <SliderDimInput value={content.iconSize || '40px'} onChange={(v) => update({ iconSize: v })} max={80} step={2} units={['px']} placeholder="40" />
        </PropRow>
        <PropRow label="Align" stack>
          <Select
            value={content.align || 'center'}
            options={[
              { value: 'left',   label: 'Left'   },
              { value: 'center', label: 'Center' },
              { value: 'right',  label: 'Right'  },
            ]}
            onChange={(v) => update({ align: v })}
          />
        </PropRow>
        <PropRow label="Override Color">
          <ColorInput value={content.color || ''} onChange={(v) => update({ color: v })} placeholder="(platform default)" />
        </PropRow>
      </PropGroup>
      <PropGroup label="Icons">
        <ItemRepeater
          items={items}
          onChange={save}
          newItem={{ platform: 'facebook', url: '' }}
          addLabel="+ Add icon"
          renderItem={(item, _, upd) => (
            <>
              <PropRow label="Platform" stack>
                <Select value={item.platform} options={PLATFORM_OPTIONS} onChange={(v) => upd('platform', v)} />
              </PropRow>
              <PropRow label="URL" stack>
                <TextInput value={item.url} onChange={(v) => upd('url', v)} placeholder="https://…" />
              </PropRow>
            </>
          )}
        />
      </PropGroup>
    </div>
  );
}

/* — Gallery — */
/* ── WooCommerce: Single Product ────────────────────────────────────────────
 * Content tab, Elementor order: Product Source → Layout → Product Structure →
 * Image → Title → Rating → Price → Add To Cart → Meta → Share → Tabs →
 * Related → Upsells. Sub-groups only appear when their section is enabled, so
 * the panel stays navigable despite the widget's size. */
function SingleProductContent({ node, store }: { node: CBNode; store: EditorStore }) {
  const c   = (node.content  ?? {}) as Record<string, unknown>;
  const s   = (node.settings ?? {}) as Record<string, string>;
  const up  = (patch: Record<string, unknown>) => store.updateNodeContent(node.id, patch);
  const upS = (patch: Record<string, string>)  => store.updateNodeSettings(node.id, patch);

  const str  = (k: string, d = '') => (c[k] == null || c[k] === '' ? d : String(c[k]));
  const sstr = (k: string, d = '') => s[k] || d;
  const on   = (k: string, d: boolean) => (k in c ? !!c[k] : d);

  const source = str('source', 'current');

  // Share networks are stored as a comma string; toggle one on/off in place.
  const nets = str('shareNetworks', '').split(',').map((n) => n.trim()).filter(Boolean);
  const toggleNet = (net: string) => {
    const next = nets.includes(net) ? nets.filter((n) => n !== net) : [...nets, net];
    up({ shareNetworks: next.join(',') });
  };

  return (
    <div className="jalpi-props-section">

      {/* ── Product Source ── */}
      <PropGroup label="Product Source" defaultOpen>
        <PropRow label="Source" stack>
          <Select
            value={source}
            options={[
              { value: 'current', label: 'Current Product' },
              { value: 'select',  label: 'Select Product' },
              { value: 'custom',  label: 'Custom Product ID' },
            ]}
            onChange={(v) => up({ source: v })}
          />
        </PropRow>

        {source === 'current' && (
          <>
            <div style={{ fontSize: 11, opacity: 0.7, padding: '4px 0 8px' }}>
              Uses the product being viewed — ideal for a Theme Builder single-product template.
            </div>
            <PropRow label="Preview Product ID">
              <TextInput value={str('previewProduct', '0')} onChange={(v) => up({ previewProduct: v })} placeholder="0" />
            </PropRow>
            <div style={{ fontSize: 11, opacity: 0.7, padding: '0 0 8px' }}>
              Shown in the editor and anywhere there is no current product.
            </div>
          </>
        )}
        {source === 'select' && (
          <PropRow label="Product ID">
            <TextInput value={str('productId', '0')} onChange={(v) => up({ productId: v })} placeholder="123" />
          </PropRow>
        )}
        {source === 'custom' && (
          <PropRow label="Custom Product ID">
            <TextInput value={str('customProductId', '0')} onChange={(v) => up({ customProductId: v })} placeholder="123" />
          </PropRow>
        )}
      </PropGroup>

      {/* ── Layout ── */}
      <PropGroup label="Layout" defaultOpen>
        <PropRow label="Product Layout" stack>
          <Select
            value={str('layout', 'default')}
            options={[
              { value: 'default', label: 'Default' },
              { value: 'modern',  label: 'Modern' },
              { value: 'classic', label: 'Classic' },
              { value: 'custom',  label: 'Custom' },
            ]}
            onChange={(v) => up({ layout: v })}
          />
        </PropRow>
        <PropRow label="Gallery Position" stack>
          <Select
            value={str('galleryPosition', 'left')}
            options={[
              { value: 'left',  label: 'Left' },
              { value: 'right', label: 'Right' },
              { value: 'top',   label: 'Top' },
            ]}
            onChange={(v) => up({ galleryPosition: v })}
          />
        </PropRow>
        <PropRow label="Gallery Width (%)">
          <SliderDimInput value={sstr('sp_gallery_width', '50')} onChange={(v) => upS({ sp_gallery_width: v })} min={20} max={80} step={5} units={['']} placeholder="50" />
        </PropRow>
        <PropRow label="Column Gap">
          <SliderDimInput value={sstr('sp_col_gap', '40')} onChange={(v) => upS({ sp_col_gap: v })} max={120} step={4} units={['px']} placeholder="40" />
        </PropRow>
        <PropRow label="Column Gap (Tablet)">
          <SliderDimInput value={sstr('sp_col_gap_tablet')} onChange={(v) => upS({ sp_col_gap_tablet: v })} max={120} step={4} units={['px']} placeholder="inherit" />
        </PropRow>
      </PropGroup>

      {/* ── Product Structure ── */}
      <PropGroup label="Product Structure" defaultOpen>
        {([
          ['showGallery',   'Product Gallery',   true],
          ['showTitle',     'Product Title',     true],
          ['showRating',    'Product Rating',    true],
          ['showPrice',     'Product Price',     true],
          ['showExcerpt',   'Short Description', true],
          ['showAddToCart', 'Add To Cart',       true],
          ['showMeta',      'Product Meta',      true],
          ['showShare',     'Product Share',     false],
          ['showTabs',      'Product Tabs',      true],
          ['showRelated',   'Related Products',  true],
          ['showUpsells',   'Upsells',           false],
        ] as Array<[string, string, boolean]>).map(([key, label, def]) => (
          <PropRow key={key} label={label}>
            <Toggle checked={on(key, def)} onChange={(v) => up({ [key]: v })} />
          </PropRow>
        ))}
      </PropGroup>

      {/* ── Image ── */}
      {on('showGallery', true) && (
        <PropGroup label="Product Image" defaultOpen={false}>
          <PropRow label="Show Thumbnails">
            <Toggle checked={on('showThumbs', true)} onChange={(v) => up({ showThumbs: v })} />
          </PropRow>
          {on('showThumbs', true) && (
            <PropRow label="Thumbnail Position" stack>
              <Select
                value={str('thumbPosition', 'horizontal')}
                options={[
                  { value: 'horizontal', label: 'Horizontal' },
                  { value: 'vertical',   label: 'Vertical' },
                ]}
                onChange={(v) => up({ thumbPosition: v })}
              />
            </PropRow>
          )}
          <PropRow label="Image Size" stack>
            <Select
              value={sstr('sp_img_size', 'woocommerce_single')}
              options={[
                { value: 'woocommerce_single',    label: 'Single (Large)' },
                { value: 'woocommerce_thumbnail', label: 'Shop Thumbnail' },
                { value: 'medium',                label: 'Medium' },
                { value: 'large',                 label: 'Large' },
                { value: 'full',                  label: 'Full' },
              ]}
              onChange={(v) => upS({ sp_img_size: v })}
            />
          </PropRow>
          <PropRow label="Image Link" stack>
            <Select
              value={str('imageLink', 'lightbox')}
              options={[
                { value: 'lightbox', label: 'Lightbox' },
                { value: 'product',  label: 'Product URL' },
                { value: 'none',     label: 'None' },
              ]}
              onChange={(v) => up({ imageLink: v })}
            />
          </PropRow>
          <PropRow label="Lazy Load">
            <Toggle checked={on('lazyLoad', true)} onChange={(v) => up({ lazyLoad: v })} />
          </PropRow>
        </PropGroup>
      )}

      {/* ── Title ── */}
      {on('showTitle', true) && (
        <PropGroup label="Product Title" defaultOpen={false}>
          <PropRow label="HTML Tag" stack>
            <Select
              value={str('titleTag', 'h1')}
              options={[
                { value: 'h1',  label: 'H1' }, { value: 'h2',  label: 'H2' },
                { value: 'h3',  label: 'H3' }, { value: 'div', label: 'Div' },
              ]}
              onChange={(v) => up({ titleTag: v })}
            />
          </PropRow>
          <PropRow label="Alignment" stack>
            <Select
              value={sstr('sp_title_align', 'inherit')}
              options={[
                { value: 'inherit', label: 'Inherit' }, { value: 'left', label: 'Left' },
                { value: 'center', label: 'Center' }, { value: 'right', label: 'Right' },
              ]}
              onChange={(v) => upS({ sp_title_align: v })}
            />
          </PropRow>
        </PropGroup>
      )}

      {/* ── Rating ── */}
      {on('showRating', true) && (
        <PropGroup label="Rating" defaultOpen={false}>
          <PropRow label="Show Review Count">
            <Toggle checked={on('showReviewCount', true)} onChange={(v) => up({ showReviewCount: v })} />
          </PropRow>
          <PropRow label="Link To Reviews">
            <Toggle checked={on('linkToReviews', false)} onChange={(v) => up({ linkToReviews: v })} />
          </PropRow>
          <PropRow label="Show When No Reviews">
            <Toggle checked={on('showEmptyRating', false)} onChange={(v) => up({ showEmptyRating: v })} />
          </PropRow>
        </PropGroup>
      )}

      {/* ── Price ── */}
      {on('showPrice', true) && (
        <PropGroup label="Price" defaultOpen={false}>
          <PropRow label="Show Regular Price">
            <Toggle checked={on('showRegularPrice', true)} onChange={(v) => up({ showRegularPrice: v })} />
          </PropRow>
          <PropRow label="Show Sale Price">
            <Toggle checked={on('showSalePrice', true)} onChange={(v) => up({ showSalePrice: v })} />
          </PropRow>
          <PropRow label="Sale Badge">
            <Toggle checked={on('showSaleBadge', true)} onChange={(v) => up({ showSaleBadge: v })} />
          </PropRow>
        </PropGroup>
      )}

      {/* ── Add To Cart ── */}
      {on('showAddToCart', true) && (
        <PropGroup label="Add To Cart" defaultOpen={false}>
          <PropRow label="Show Quantity">
            <Toggle checked={on('showQuantity', true)} onChange={(v) => up({ showQuantity: v })} />
          </PropRow>
          {on('showQuantity', true) && (
            <PropRow label="Quantity Type" stack>
              <Select
                value={str('quantityType', 'input')}
                options={[
                  { value: 'input',     label: 'Input' },
                  { value: 'plusminus', label: 'Plus / Minus' },
                ]}
                onChange={(v) => up({ quantityType: v })}
              />
            </PropRow>
          )}
          <PropRow label="Button Text" stack>
            <TextInput value={str('buttonText')} onChange={(v) => up({ buttonText: v })} placeholder="Add to cart" />
          </PropRow>
          <PropRow label="Icon" stack>
            <Select
              value={sstr('sp_btn_icon', 'none')}
              options={[
                { value: 'none',  label: 'None' },  { value: 'cart',  label: 'Cart 🛒' },
                { value: 'bag',   label: 'Bag 🛍' }, { value: 'plus',  label: 'Plus ＋' },
                { value: 'arrow', label: 'Arrow →' },
              ]}
              onChange={(v) => upS({ sp_btn_icon: v })}
            />
          </PropRow>
          {sstr('sp_btn_icon', 'none') !== 'none' && (
            <PropRow label="Icon Position" stack>
              <Select
                value={sstr('sp_btn_icon_pos', 'before')}
                options={[{ value: 'before', label: 'Before' }, { value: 'after', label: 'After' }]}
                onChange={(v) => upS({ sp_btn_icon_pos: v })}
              />
            </PropRow>
          )}
          <PropRow label="AJAX Add To Cart">
            <Toggle checked={on('ajaxAddToCart', true)} onChange={(v) => up({ ajaxAddToCart: v })} />
          </PropRow>
          <div style={{ fontSize: 11, opacity: 0.7, padding: '4px 0 0' }}>
            Variable products use WooCommerce's own variation form, so AJAX and quantity apply to simple products.
          </div>
        </PropGroup>
      )}

      {/* ── Meta ── */}
      {on('showMeta', true) && (
        <PropGroup label="Product Meta" defaultOpen={false}>
          <PropRow label="SKU">
            <Toggle checked={on('metaSku', true)} onChange={(v) => up({ metaSku: v })} />
          </PropRow>
          <PropRow label="Categories">
            <Toggle checked={on('metaCategories', true)} onChange={(v) => up({ metaCategories: v })} />
          </PropRow>
          <PropRow label="Tags">
            <Toggle checked={on('metaTags', true)} onChange={(v) => up({ metaTags: v })} />
          </PropRow>
          <PropRow label="Separator" stack>
            <TextInput value={str('metaSeparator')} onChange={(v) => up({ metaSeparator: v })} placeholder=", " />
          </PropRow>
          <PropRow label="Show Labels">
            <Toggle checked={on('metaShowLabels', true)} onChange={(v) => up({ metaShowLabels: v })} />
          </PropRow>
        </PropGroup>
      )}

      {/* ── Share ── */}
      {on('showShare', false) && (
        <PropGroup label="Share" defaultOpen={false}>
          {([
            ['facebook',  'Facebook'],
            ['twitter',   'Twitter / X'],
            ['linkedin',  'LinkedIn'],
            ['pinterest', 'Pinterest'],
            ['whatsapp',  'WhatsApp'],
          ] as Array<[string, string]>).map(([key, label]) => (
            <PropRow key={key} label={label}>
              <Toggle checked={nets.includes(key)} onChange={() => toggleNet(key)} />
            </PropRow>
          ))}
          <PropRow label="Icon Style" stack>
            <Select
              value={sstr('sp_share_style', 'circle')}
              options={[
                { value: 'circle',  label: 'Circle' },
                { value: 'rounded', label: 'Rounded' },
                { value: 'square',  label: 'Square' },
              ]}
              onChange={(v) => upS({ sp_share_style: v })}
            />
          </PropRow>
          <PropRow label="Icon Size">
            <SliderDimInput value={sstr('sp_share_size', '15')} onChange={(v) => upS({ sp_share_size: v })} max={40} units={['px']} placeholder="15" />
          </PropRow>
        </PropGroup>
      )}

      {/* ── Tabs ── */}
      {on('showTabs', true) && (
        <PropGroup label="Product Tabs" defaultOpen={false}>
          <PropRow label="Default Active Tab" stack>
            <Select
              value={str('defaultTab', 'description')}
              options={[
                { value: 'description',            label: 'Description' },
                { value: 'additional_information', label: 'Additional Information' },
                { value: 'reviews',                label: 'Reviews' },
              ]}
              onChange={(v) => up({ defaultTab: v })}
            />
          </PropRow>
          <PropRow label="Tab Position" stack>
            <Select
              value={str('tabPosition', 'top')}
              options={[{ value: 'top', label: 'Top' }, { value: 'left', label: 'Left' }]}
              onChange={(v) => up({ tabPosition: v })}
            />
          </PropRow>
          <div style={{ fontSize: 11, opacity: 0.7, padding: '4px 0 0' }}>
            Tabs render through WooCommerce, so tabs added by other plugins appear automatically.
          </div>
        </PropGroup>
      )}

      {/* ── Related ── */}
      {on('showRelated', true) && (
        <PropGroup label="Related Products" defaultOpen={false}>
          <PropRow label="Products Count">
            <TextInput value={str('relatedCount', '4')} onChange={(v) => up({ relatedCount: v })} placeholder="4" />
          </PropRow>
          <PropRow label="Columns">
            <Select
              value={str('relatedColumns', '4')}
              options={[1, 2, 3, 4, 5, 6].map((n) => ({ value: String(n), label: String(n) }))}
              onChange={(v) => up({ relatedColumns: v })}
            />
          </PropRow>
          <PropRow label="Order By" stack>
            <Select
              value={str('relatedOrderBy', 'rand')}
              options={[
                { value: 'rand',       label: 'Random' },
                { value: 'date',       label: 'Date' },
                { value: 'price',      label: 'Price' },
                { value: 'title',      label: 'Title' },
                { value: 'popularity', label: 'Popularity' },
              ]}
              onChange={(v) => up({ relatedOrderBy: v })}
            />
          </PropRow>
        </PropGroup>
      )}

      {/* ── Upsells ── */}
      {on('showUpsells', false) && (
        <PropGroup label="Upsells" defaultOpen={false}>
          <PropRow label="Products Count">
            <TextInput value={str('upsellsCount', '4')} onChange={(v) => up({ upsellsCount: v })} placeholder="4" />
          </PropRow>
          <PropRow label="Columns">
            <Select
              value={str('upsellsColumns', '4')}
              options={[1, 2, 3, 4, 5, 6].map((n) => ({ value: String(n), label: String(n) }))}
              onChange={(v) => up({ upsellsColumns: v })}
            />
          </PropRow>
        </PropGroup>
      )}

      {/* ── Empty State ── */}
      <PropGroup label="Empty State" defaultOpen={false}>
        <PropRow label="Message" stack>
          <TextInput value={str('emptyMessage')} onChange={(v) => up({ emptyMessage: v })} placeholder="No product selected…" />
        </PropRow>
      </PropGroup>
    </div>
  );
}

/* Single Product — Style tab. Groups mirror Elementor: Product Container,
 * Gallery, Title, Rating, Price, Description, Add To Cart, Quantity, Meta,
 * Share, Tabs, Sections. Every key is a `settings` value sanitized by the
 * 'single-product' case in Jalpi_Sanitizer::sanitize_settings(). */
function SingleProductStyleTab({ node, store }: { node: CBNode; store: EditorStore }) {
  const s   = (node.settings ?? {}) as Record<string, string>;
  const str = (k: string, d = '') => s[k] || d;
  const upS = (patch: Record<string, string>) => store.updateNodeSettings(node.id, patch);

  const bd    = str('sp_bd_type', 'none');
  const btnBd = str('sp_btn_bd_type', 'none');

  return (
    <div className="jalpi-props-section">

      {/* ── Container ── */}
      <PropGroup label="Product Container" defaultOpen>
        <ColorPropRow label="Background" value={str('sp_bg')} onChange={(v) => upS({ sp_bg: v })} placeholder="#ffffff" />
        <PropRow label="Border Type" stack>
          <Select
            value={bd}
            options={[
              { value: 'none', label: 'None' }, { value: 'solid', label: 'Solid' },
              { value: 'dashed', label: 'Dashed' }, { value: 'dotted', label: 'Dotted' },
              { value: 'double', label: 'Double' },
            ]}
            onChange={(v) => upS({ sp_bd_type: v })}
          />
        </PropRow>
        {bd !== 'none' && (
          <>
            <PropRow label="Border Width">
              <SliderDimInput value={str('sp_bd_width', '1')} onChange={(v) => upS({ sp_bd_width: v })} max={12} step={1} units={['px']} placeholder="1" />
            </PropRow>
            <ColorPropRow label="Border Color" value={str('sp_bd_color')} onChange={(v) => upS({ sp_bd_color: v })} placeholder="#e5e7eb" />
          </>
        )}
        <PropRow label="Border Radius">
          <SliderDimInput value={str('sp_radius')} onChange={(v) => upS({ sp_radius: v })} max={60} units={['px']} placeholder="0" />
        </PropRow>
        <PropRow label="Box Shadow" stack>
          <TextInput value={str('sp_shadow')} onChange={(v) => upS({ sp_shadow: v })} placeholder="0 1px 3px rgba(0,0,0,.1)" />
        </PropRow>
        <PropRow label="Padding" stack>
          <TextInput value={str('sp_pad')} onChange={(v) => upS({ sp_pad: v })} placeholder="0" />
        </PropRow>
        <PropRow label="Margin" stack>
          <TextInput value={str('sp_margin')} onChange={(v) => upS({ sp_margin: v })} placeholder="0" />
        </PropRow>
      </PropGroup>

      {/* ── Gallery ── */}
      <PropGroup label="Gallery" defaultOpen={false}>
        <PropRow label="Image Border Radius">
          <SliderDimInput value={str('sp_img_radius', '8')} onChange={(v) => upS({ sp_img_radius: v })} max={60} units={['px']} placeholder="8" />
        </PropRow>
        <PropRow label="Hover Effect" stack>
          <Select
            value={str('sp_img_hover', 'none')}
            options={[
              { value: 'none', label: 'None' }, { value: 'zoom', label: 'Zoom' },
              { value: 'lift', label: 'Lift' },
            ]}
            onChange={(v) => upS({ sp_img_hover: v })}
          />
        </PropRow>
        <PropRow label="Thumbnail Size">
          <SliderDimInput value={str('sp_thumb_size', '72')} onChange={(v) => upS({ sp_thumb_size: v })} max={200} units={['px']} placeholder="72" />
        </PropRow>
        <PropRow label="Thumbnail Gap">
          <SliderDimInput value={str('sp_thumb_gap', '10')} onChange={(v) => upS({ sp_thumb_gap: v })} max={40} units={['px']} placeholder="10" />
        </PropRow>
        <PropRow label="Thumbnail Radius">
          <SliderDimInput value={str('sp_thumb_radius', '6')} onChange={(v) => upS({ sp_thumb_radius: v })} max={40} units={['px']} placeholder="6" />
        </PropRow>
        <ColorPropRow label="Active Thumb Border" value={str('sp_thumb_active_color')} onChange={(v) => upS({ sp_thumb_active_color: v })} placeholder="#111827" />
      </PropGroup>

      {/* ── Title ── */}
      <PropGroup label="Title" defaultOpen={false}>
        <PropRow label="Font Family" stack>
          <TextInput value={str('sp_title_font')} onChange={(v) => upS({ sp_title_font: v })} placeholder="inherit" />
        </PropRow>
        <PropRow label="Font Size">
          <SliderDimInput value={str('sp_title_size', '30')} onChange={(v) => upS({ sp_title_size: v })} max={90} units={['px']} placeholder="30" />
        </PropRow>
        <PropRow label="Font Weight" stack>
          <Select
            value={str('sp_title_weight', '700')}
            options={['300', '400', '500', '600', '700', '800'].map((w) => ({ value: w, label: w }))}
            onChange={(v) => upS({ sp_title_weight: v })}
          />
        </PropRow>
        <PropRow label="Transform" stack>
          <Select
            value={str('sp_title_transform', 'none')}
            options={[
              { value: 'none', label: 'Default' }, { value: 'uppercase', label: 'Uppercase' },
              { value: 'lowercase', label: 'Lowercase' }, { value: 'capitalize', label: 'Capitalize' },
            ]}
            onChange={(v) => upS({ sp_title_transform: v })}
          />
        </PropRow>
        <PropRow label="Line Height">
          <SliderDimInput value={str('sp_title_lh', '1.25')} onChange={(v) => upS({ sp_title_lh: v })} min={0} max={3} step={0.05} units={['']} placeholder="1.25" />
        </PropRow>
        <PropRow label="Letter Spacing">
          <SliderDimInput value={str('sp_title_ls')} onChange={(v) => upS({ sp_title_ls: v })} min={-5} max={20} step={0.5} units={['px']} placeholder="0" />
        </PropRow>
        <ColorPropRow label="Color"       value={str('sp_title_color')}       onChange={(v) => upS({ sp_title_color: v })}       placeholder="#111827" />
        <ColorPropRow label="Hover Color" value={str('sp_title_hover_color')} onChange={(v) => upS({ sp_title_hover_color: v })} placeholder="#7f54b3" />
        <PropRow label="Spacing / Margin" stack>
          <TextInput value={str('sp_title_margin')} onChange={(v) => upS({ sp_title_margin: v })} placeholder="0 0 10px" />
        </PropRow>
      </PropGroup>

      {/* ── Rating ── */}
      <PropGroup label="Rating" defaultOpen={false}>
        <PropRow label="Star Size">
          <SliderDimInput value={str('sp_star_size', '16')} onChange={(v) => upS({ sp_star_size: v })} max={48} units={['px']} placeholder="16" />
        </PropRow>
        <ColorPropRow label="Active Star" value={str('sp_star_color')}       onChange={(v) => upS({ sp_star_color: v })}       placeholder="#f59e0b" />
        <ColorPropRow label="Empty Star"  value={str('sp_star_empty_color')} onChange={(v) => upS({ sp_star_empty_color: v })} placeholder="#d1d5db" />
        <ColorPropRow label="Review Text" value={str('sp_review_color')}     onChange={(v) => upS({ sp_review_color: v })}     placeholder="#6b7280" />
        <PropRow label="Review Font Size">
          <SliderDimInput value={str('sp_review_size', '13')} onChange={(v) => upS({ sp_review_size: v })} max={28} units={['px']} placeholder="13" />
        </PropRow>
        <PropRow label="Spacing">
          <SliderDimInput value={str('sp_rating_spacing', '10')} onChange={(v) => upS({ sp_rating_spacing: v })} max={60} units={['px']} placeholder="10" />
        </PropRow>
      </PropGroup>

      {/* ── Price ── */}
      <PropGroup label="Price" defaultOpen={false}>
        <PropRow label="Font Size">
          <SliderDimInput value={str('sp_price_size', '24')} onChange={(v) => upS({ sp_price_size: v })} max={72} units={['px']} placeholder="24" />
        </PropRow>
        <PropRow label="Font Weight" stack>
          <Select
            value={str('sp_price_weight', '700')}
            options={['300', '400', '500', '600', '700', '800'].map((w) => ({ value: w, label: w }))}
            onChange={(v) => upS({ sp_price_weight: v })}
          />
        </PropRow>
        <ColorPropRow label="Regular Price" value={str('sp_price_color')}      onChange={(v) => upS({ sp_price_color: v })}      placeholder="#111827" />
        <ColorPropRow label="Sale Price"    value={str('sp_sale_price_color')} onChange={(v) => upS({ sp_sale_price_color: v })} placeholder="#dc2626" />
        <PropRow label="Spacing">
          <SliderDimInput value={str('sp_price_spacing', '14')} onChange={(v) => upS({ sp_price_spacing: v })} max={60} units={['px']} placeholder="14" />
        </PropRow>
        <ColorPropRow label="Badge Background" value={str('sp_badge_bg')}    onChange={(v) => upS({ sp_badge_bg: v })}    placeholder="#dc2626" />
        <ColorPropRow label="Badge Text"       value={str('sp_badge_color')} onChange={(v) => upS({ sp_badge_color: v })} placeholder="#ffffff" />
        <PropRow label="Badge Radius">
          <SliderDimInput value={str('sp_badge_radius', '4')} onChange={(v) => upS({ sp_badge_radius: v })} max={40} units={['px']} placeholder="4" />
        </PropRow>
        <PropRow label="Badge Font Size">
          <SliderDimInput value={str('sp_badge_size', '12')} onChange={(v) => upS({ sp_badge_size: v })} max={28} units={['px']} placeholder="12" />
        </PropRow>
        <PropRow label="Badge Padding" stack>
          <TextInput value={str('sp_badge_pad')} onChange={(v) => upS({ sp_badge_pad: v })} placeholder="3px 9px" />
        </PropRow>
      </PropGroup>

      {/* ── Description ── */}
      <PropGroup label="Description" defaultOpen={false}>
        <PropRow label="Font Size">
          <SliderDimInput value={str('sp_desc_size', '15')} onChange={(v) => upS({ sp_desc_size: v })} max={40} units={['px']} placeholder="15" />
        </PropRow>
        <ColorPropRow label="Text Color" value={str('sp_desc_color')} onChange={(v) => upS({ sp_desc_color: v })} placeholder="#4b5563" />
        <PropRow label="Line Height">
          <SliderDimInput value={str('sp_desc_lh', '1.7')} onChange={(v) => upS({ sp_desc_lh: v })} min={0} max={3} step={0.05} units={['']} placeholder="1.7" />
        </PropRow>
        <PropRow label="Alignment" stack>
          <Select
            value={str('sp_desc_align', 'inherit')}
            options={[
              { value: 'inherit', label: 'Inherit' }, { value: 'left', label: 'Left' },
              { value: 'center', label: 'Center' }, { value: 'right', label: 'Right' },
              { value: 'justify', label: 'Justify' },
            ]}
            onChange={(v) => upS({ sp_desc_align: v })}
          />
        </PropRow>
        <PropRow label="Spacing">
          <SliderDimInput value={str('sp_desc_spacing', '18')} onChange={(v) => upS({ sp_desc_spacing: v })} max={80} units={['px']} placeholder="18" />
        </PropRow>
      </PropGroup>

      {/* ── Add To Cart ── */}
      <PropGroup label="Add To Cart Button" defaultOpen={false}>
        <ColorPropRow label="Text Color" value={str('sp_btn_color')} onChange={(v) => upS({ sp_btn_color: v })} placeholder="#ffffff" />
        <ColorPropRow label="Background" value={str('sp_btn_bg')}    onChange={(v) => upS({ sp_btn_bg: v })}    placeholder="#111827" />
        <PropRow label="Font Size">
          <SliderDimInput value={str('sp_btn_size', '15')} onChange={(v) => upS({ sp_btn_size: v })} max={32} units={['px']} placeholder="15" />
        </PropRow>
        <PropRow label="Font Weight" stack>
          <Select
            value={str('sp_btn_weight', '600')}
            options={['300', '400', '500', '600', '700', '800'].map((w) => ({ value: w, label: w }))}
            onChange={(v) => upS({ sp_btn_weight: v })}
          />
        </PropRow>
        <PropRow label="Padding" stack>
          <TextInput value={str('sp_btn_pad')} onChange={(v) => upS({ sp_btn_pad: v })} placeholder="12px 28px" />
        </PropRow>
        <PropRow label="Border Radius">
          <SliderDimInput value={str('sp_btn_radius', '8')} onChange={(v) => upS({ sp_btn_radius: v })} max={60} units={['px']} placeholder="8" />
        </PropRow>
        <PropRow label="Border Type" stack>
          <Select
            value={btnBd}
            options={[
              { value: 'none', label: 'None' }, { value: 'solid', label: 'Solid' },
              { value: 'dashed', label: 'Dashed' }, { value: 'dotted', label: 'Dotted' },
            ]}
            onChange={(v) => upS({ sp_btn_bd_type: v })}
          />
        </PropRow>
        {btnBd !== 'none' && (
          <>
            <PropRow label="Border Width">
              <SliderDimInput value={str('sp_btn_bd_width', '1')} onChange={(v) => upS({ sp_btn_bd_width: v })} max={10} step={1} units={['px']} placeholder="1" />
            </PropRow>
            <ColorPropRow label="Border Color" value={str('sp_btn_bd_color')} onChange={(v) => upS({ sp_btn_bd_color: v })} placeholder="#111827" />
          </>
        )}
        <ColorPropRow label="Hover Text"       value={str('sp_btn_hover_color')} onChange={(v) => upS({ sp_btn_hover_color: v })} placeholder="#ffffff" />
        <ColorPropRow label="Hover Background" value={str('sp_btn_hover_bg')}    onChange={(v) => upS({ sp_btn_hover_bg: v })}    placeholder="#7f54b3" />
        <ColorPropRow label="Hover Border"     value={str('sp_btn_hover_bd')}    onChange={(v) => upS({ sp_btn_hover_bd: v })}    placeholder="#7f54b3" />
        <PropRow label="Hover Animation" stack>
          <Select
            value={str('sp_btn_hover_anim', 'none')}
            options={[
              { value: 'none', label: 'None' }, { value: 'grow', label: 'Grow' },
              { value: 'shrink', label: 'Shrink' }, { value: 'float', label: 'Float' },
              { value: 'sink', label: 'Sink' },
            ]}
            onChange={(v) => upS({ sp_btn_hover_anim: v })}
          />
        </PropRow>
        <PropRow label="Cart Spacing">
          <SliderDimInput value={str('sp_cart_spacing', '20')} onChange={(v) => upS({ sp_cart_spacing: v })} max={80} units={['px']} placeholder="20" />
        </PropRow>
      </PropGroup>

      {/* ── Quantity ── */}
      <PropGroup label="Quantity" defaultOpen={false}>
        <PropRow label="Width">
          <SliderDimInput value={str('sp_qty_width', '110')} onChange={(v) => upS({ sp_qty_width: v })} max={300} units={['px']} placeholder="110" />
        </PropRow>
        <PropRow label="Height">
          <SliderDimInput value={str('sp_qty_height', '46')} onChange={(v) => upS({ sp_qty_height: v })} max={100} units={['px']} placeholder="46" />
        </PropRow>
        <ColorPropRow label="Background"   value={str('sp_qty_bg')}        onChange={(v) => upS({ sp_qty_bg: v })}        placeholder="transparent" />
        <ColorPropRow label="Border Color" value={str('sp_qty_bd_color')}  onChange={(v) => upS({ sp_qty_bd_color: v })}  placeholder="#e5e7eb" />
        <PropRow label="Border Radius">
          <SliderDimInput value={str('sp_qty_radius', '8')} onChange={(v) => upS({ sp_qty_radius: v })} max={40} units={['px']} placeholder="8" />
        </PropRow>
        <PropRow label="Font Size">
          <SliderDimInput value={str('sp_qty_size', '15')} onChange={(v) => upS({ sp_qty_size: v })} max={32} units={['px']} placeholder="15" />
        </PropRow>
        <ColorPropRow label="Text Color"   value={str('sp_qty_color')}     onChange={(v) => upS({ sp_qty_color: v })}     placeholder="#111827" />
        <ColorPropRow label="Button Color" value={str('sp_qty_btn_color')} onChange={(v) => upS({ sp_qty_btn_color: v })} placeholder="#111827" />
      </PropGroup>

      {/* ── Meta ── */}
      <PropGroup label="Meta" defaultOpen={false}>
        <ColorPropRow label="Label Color" value={str('sp_meta_label_color')} onChange={(v) => upS({ sp_meta_label_color: v })} placeholder="#111827" />
        <ColorPropRow label="Text Color"  value={str('sp_meta_color')}       onChange={(v) => upS({ sp_meta_color: v })}       placeholder="#6b7280" />
        <PropRow label="Font Size">
          <SliderDimInput value={str('sp_meta_size', '13')} onChange={(v) => upS({ sp_meta_size: v })} max={28} units={['px']} placeholder="13" />
        </PropRow>
        <PropRow label="Spacing">
          <SliderDimInput value={str('sp_meta_spacing', '6')} onChange={(v) => upS({ sp_meta_spacing: v })} max={40} units={['px']} placeholder="6" />
        </PropRow>
      </PropGroup>

      {/* ── Share ── */}
      <PropGroup label="Share" defaultOpen={false}>
        <ColorPropRow label="Icon Color"       value={str('sp_share_color')}    onChange={(v) => upS({ sp_share_color: v })}    placeholder="#111827" />
        <ColorPropRow label="Background"       value={str('sp_share_bg')}       onChange={(v) => upS({ sp_share_bg: v })}       placeholder="#f3f4f6" />
        <ColorPropRow label="Hover Background" value={str('sp_share_hover_bg')} onChange={(v) => upS({ sp_share_hover_bg: v })} placeholder="#e5e7eb" />
      </PropGroup>

      {/* ── Tabs ── */}
      <PropGroup label="Tabs" defaultOpen={false}>
        <PropRow label="Tab Font Size">
          <SliderDimInput value={str('sp_tab_size', '15')} onChange={(v) => upS({ sp_tab_size: v })} max={32} units={['px']} placeholder="15" />
        </PropRow>
        <PropRow label="Tab Font Weight" stack>
          <Select
            value={str('sp_tab_weight', '600')}
            options={['300', '400', '500', '600', '700', '800'].map((w) => ({ value: w, label: w }))}
            onChange={(v) => upS({ sp_tab_weight: v })}
          />
        </PropRow>
        <ColorPropRow label="Tab Color"    value={str('sp_tab_color')}        onChange={(v) => upS({ sp_tab_color: v })}        placeholder="#6b7280" />
        <ColorPropRow label="Active Color" value={str('sp_tab_active_color')} onChange={(v) => upS({ sp_tab_active_color: v })} placeholder="#111827" />
        <ColorPropRow label="Hover Color"  value={str('sp_tab_hover_color')}  onChange={(v) => upS({ sp_tab_hover_color: v })}  placeholder="#7f54b3" />
        <ColorPropRow label="Border Color" value={str('sp_tab_bd_color')}     onChange={(v) => upS({ sp_tab_bd_color: v })}     placeholder="#e5e7eb" />
        <PropRow label="Tab Gap">
          <SliderDimInput value={str('sp_tab_gap', '24')} onChange={(v) => upS({ sp_tab_gap: v })} max={80} units={['px']} placeholder="24" />
        </PropRow>
        <PropRow label="Section Spacing">
          <SliderDimInput value={str('sp_tabs_spacing', '48')} onChange={(v) => upS({ sp_tabs_spacing: v })} max={160} units={['px']} placeholder="48" />
        </PropRow>
        <ColorPropRow label="Content Color" value={str('sp_tab_content_color')} onChange={(v) => upS({ sp_tab_content_color: v })} placeholder="#4b5563" />
        <PropRow label="Content Font Size">
          <SliderDimInput value={str('sp_tab_content_size', '15')} onChange={(v) => upS({ sp_tab_content_size: v })} max={32} units={['px']} placeholder="15" />
        </PropRow>
        <PropRow label="Content Line Height">
          <SliderDimInput value={str('sp_tab_content_lh', '1.7')} onChange={(v) => upS({ sp_tab_content_lh: v })} min={0} max={3} step={0.05} units={['']} placeholder="1.7" />
        </PropRow>
      </PropGroup>

      {/* ── Section headings (related / upsells) ── */}
      <PropGroup label="Section Titles" defaultOpen={false}>
        <PropRow label="Font Size">
          <SliderDimInput value={str('sp_section_title_size', '22')} onChange={(v) => upS({ sp_section_title_size: v })} max={60} units={['px']} placeholder="22" />
        </PropRow>
        <ColorPropRow label="Color" value={str('sp_section_title_color')} onChange={(v) => upS({ sp_section_title_color: v })} placeholder="#111827" />
      </PropGroup>

      {/* ── Empty State ── */}
      <PropGroup label="Empty State" defaultOpen={false}>
        <ColorPropRow label="Text Color" value={str('sp_empty_color')} onChange={(v) => upS({ sp_empty_color: v })} placeholder="#6b7280" />
      </PropGroup>
    </div>
  );
}

/* ── WooCommerce: Product Categories ────────────────────────────────────────
 * Content tab, Elementor order: Layout → Query → Category Content → Image →
 * Button → Carousel (only when the Carousel layout is active) → Empty State.
 * Per-breakpoint values (columns, gaps, alignment) live in `settings`, matching
 * the Gallery / Products Grid convention. */
function ProductCategoriesContent({ node, store }: { node: CBNode; store: EditorStore }) {
  const c   = (node.content  ?? {}) as Record<string, unknown>;
  const s   = (node.settings ?? {}) as Record<string, string>;
  const up  = (patch: Record<string, unknown>) => store.updateNodeContent(node.id, patch);
  const upS = (patch: Record<string, string>)  => store.updateNodeSettings(node.id, patch);

  const str  = (k: string, d = '') => (c[k] == null || c[k] === '' ? d : String(c[k]));
  const sstr = (k: string, d = '') => s[k] || d;
  const on   = (k: string, d: boolean) => (k in c ? !!c[k] : d);

  const source = str('source', 'all');
  const layout = str('layout', 'grid');

  return (
    <div className="jalpi-props-section">

      {/* ── Layout ── */}
      <PropGroup label="Layout" defaultOpen>
        <PropRow label="Skin" stack>
          <Select
            value={str('skin', 'classic')}
            options={[
              { value: 'classic', label: 'Classic' },
              { value: 'cards',   label: 'Cards' },
              { value: 'overlay', label: 'Overlay' },
              { value: 'minimal', label: 'Minimal' },
            ]}
            onChange={(v) => up({ skin: v })}
          />
        </PropRow>
        <PropRow label="Layout" stack>
          <Select
            value={layout}
            options={[
              { value: 'grid',     label: 'Grid' },
              { value: 'carousel', label: 'Carousel' },
              { value: 'list',     label: 'List' },
            ]}
            onChange={(v) => up({ layout: v })}
          />
        </PropRow>

        {/* Columns drive the grid; in carousel mode Slides Per View takes over
            on desktop, but tablet/mobile still fall back to these. */}
        <PropRow label="Columns" responsive>
          <Select
            value={sstr('pc_cols_desktop', '4')}
            options={[1, 2, 3, 4, 5, 6].map((n) => ({ value: String(n), label: String(n) }))}
            onChange={(v) => upS({ pc_cols_desktop: v })}
          />
        </PropRow>
        <PropRow label="Columns (Tablet)">
          <Select
            value={sstr('pc_cols_tablet', '2')}
            options={[1, 2, 3, 4, 5, 6].map((n) => ({ value: String(n), label: String(n) }))}
            onChange={(v) => upS({ pc_cols_tablet: v })}
          />
        </PropRow>
        <PropRow label="Columns (Mobile)">
          <Select
            value={sstr('pc_cols_mobile', '1')}
            options={[1, 2, 3, 4, 5, 6].map((n) => ({ value: String(n), label: String(n) }))}
            onChange={(v) => upS({ pc_cols_mobile: v })}
          />
        </PropRow>

        <PropRow label="Column Gap">
          <SliderDimInput value={sstr('pc_col_gap', '24')} onChange={(v) => upS({ pc_col_gap: v })} max={100} step={2} units={['px']} placeholder="24" />
        </PropRow>
        <PropRow label="Row Gap">
          <SliderDimInput value={sstr('pc_row_gap', '24')} onChange={(v) => upS({ pc_row_gap: v })} max={100} step={2} units={['px']} placeholder="24" />
        </PropRow>
        <PropRow label="Equal Height">
          <Toggle checked={on('equalHeight', true)} onChange={(v) => up({ equalHeight: v })} />
        </PropRow>
        <PropRow label="Alignment" stack>
          <Select
            value={sstr('pc_align', 'center')}
            options={[{ value: 'left', label: 'Left' }, { value: 'center', label: 'Center' }, { value: 'right', label: 'Right' }]}
            onChange={(v) => upS({ pc_align: v })}
          />
        </PropRow>
      </PropGroup>

      {/* ── Query ── */}
      <PropGroup label="Query" defaultOpen>
        <PropRow label="Source" stack>
          <Select
            value={source}
            options={[
              { value: 'all',      label: 'All Categories' },
              { value: 'selected', label: 'Selected Categories' },
              { value: 'parent',   label: 'Parent Categories' },
              { value: 'child',    label: 'Child Categories' },
              { value: 'featured', label: 'Featured Categories' },
            ]}
            onChange={(v) => up({ source: v })}
          />
        </PropRow>

        {source === 'selected' && (
          <PropRow label="Include Categories" stack>
            <TextInput value={str('includeCategories')} onChange={(v) => up({ includeCategories: v })} placeholder="12, 34, 56" />
          </PropRow>
        )}
        {source === 'child' && (
          <PropRow label="Parent Category ID">
            <TextInput value={str('parentCategory', '0')} onChange={(v) => up({ parentCategory: v })} placeholder="0" />
          </PropRow>
        )}
        {source === 'featured' && (
          <div style={{ fontSize: 11, opacity: 0.7, padding: '4px 0 8px' }}>
            Uses the <code>featured</code> term meta — set it on a category to include it here.
          </div>
        )}

        <PropRow label="Exclude Categories" stack>
          <TextInput value={str('excludeCategories')} onChange={(v) => up({ excludeCategories: v })} placeholder="8, 9" />
        </PropRow>
        <PropRow label="Category IDs" stack>
          <TextInput value={str('categoryIds')} onChange={(v) => up({ categoryIds: v })} placeholder="3, 7" />
        </PropRow>
        <PropRow label="Hide Empty">
          <Toggle checked={on('hideEmpty', true)} onChange={(v) => up({ hideEmpty: v })} />
        </PropRow>
        <PropRow label="Number of Categories">
          <TextInput value={str('number', '12')} onChange={(v) => up({ number: v })} placeholder="12" />
        </PropRow>

        <PropRow label="Order By" stack>
          <Select
            value={str('orderBy', 'name')}
            options={[
              { value: 'name',       label: 'Name' },
              { value: 'id',         label: 'ID' },
              { value: 'count',      label: 'Count' },
              { value: 'menu_order', label: 'Menu Order' },
              { value: 'slug',       label: 'Slug' },
            ]}
            onChange={(v) => up({ orderBy: v })}
          />
        </PropRow>
        <PropRow label="Order" stack>
          <Select
            value={str('order', 'ASC')}
            options={[{ value: 'ASC', label: 'Ascending' }, { value: 'DESC', label: 'Descending' }]}
            onChange={(v) => up({ order: v })}
          />
        </PropRow>
      </PropGroup>

      {/* ── Category Content ── */}
      <PropGroup label="Category Content" defaultOpen={false}>
        <PropRow label="Category Image">
          <Toggle checked={on('showImage', true)} onChange={(v) => up({ showImage: v })} />
        </PropRow>
        <PropRow label="Category Name">
          <Toggle checked={on('showName', true)} onChange={(v) => up({ showName: v })} />
        </PropRow>
        <PropRow label="Product Count">
          <Toggle checked={on('showCount', true)} onChange={(v) => up({ showCount: v })} />
        </PropRow>
        <PropRow label="Description">
          <Toggle checked={on('showDescription', false)} onChange={(v) => up({ showDescription: v })} />
        </PropRow>
        <PropRow label="View Button">
          <Toggle checked={on('showButton', false)} onChange={(v) => up({ showButton: v })} />
        </PropRow>
      </PropGroup>

      {/* ── Image ── */}
      <PropGroup label="Image" defaultOpen={false}>
        <PropRow label="Image Size" stack>
          <Select
            value={sstr('pc_img_size', 'woocommerce_thumbnail')}
            options={[
              { value: 'woocommerce_thumbnail', label: 'Shop Thumbnail' },
              { value: 'woocommerce_single',    label: 'Single (Large)' },
              { value: 'thumbnail',             label: 'Thumbnail' },
              { value: 'medium',                label: 'Medium' },
              { value: 'large',                 label: 'Large' },
              { value: 'full',                  label: 'Full' },
            ]}
            onChange={(v) => upS({ pc_img_size: v })}
          />
        </PropRow>
        <PropRow label="Image Ratio" stack>
          <Select
            value={sstr('pc_img_ratio', '1/1')}
            options={[
              { value: '1/1',  label: 'Square (1:1)' },
              { value: '4/3',  label: 'Landscape (4:3)' },
              { value: '3/4',  label: 'Portrait (3:4)' },
              { value: '16/9', label: 'Widescreen (16:9)' },
              { value: 'auto', label: 'Custom / Auto' },
            ]}
            onChange={(v) => upS({ pc_img_ratio: v })}
          />
        </PropRow>
        <PropRow label="Image Fit" stack>
          <Select
            value={sstr('pc_img_fit', 'cover')}
            options={[
              { value: 'cover',      label: 'Cover' },
              { value: 'contain',    label: 'Contain' },
              { value: 'fill',       label: 'Fill' },
              { value: 'scale-down', label: 'Scale Down' },
            ]}
            onChange={(v) => upS({ pc_img_fit: v })}
          />
        </PropRow>
        <PropRow label="Hover Effect" stack>
          <Select
            value={sstr('pc_img_hover', 'zoom')}
            options={[
              { value: 'none',      label: 'None' },
              { value: 'zoom',      label: 'Zoom' },
              { value: 'fade',      label: 'Fade' },
              { value: 'overlay',   label: 'Overlay' },
              { value: 'grayscale', label: 'Grayscale' },
            ]}
            onChange={(v) => upS({ pc_img_hover: v })}
          />
        </PropRow>
        <PropRow label="Lazy Load">
          <Toggle checked={on('lazyLoad', true)} onChange={(v) => up({ lazyLoad: v })} />
        </PropRow>
      </PropGroup>

      {/* ── Button ── */}
      <PropGroup label="Button" defaultOpen={false}>
        <PropRow label="Button Text" stack>
          <TextInput value={str('buttonText')} onChange={(v) => up({ buttonText: v })} placeholder="View Products" />
        </PropRow>
        <PropRow label="Icon" stack>
          <Select
            value={sstr('pc_btn_icon', 'none')}
            options={[
              { value: 'none',    label: 'None' },
              { value: 'arrow',   label: 'Arrow →' },
              { value: 'chevron', label: 'Chevron ›' },
              { value: 'plus',    label: 'Plus ＋' },
              { value: 'cart',    label: 'Cart 🛒' },
            ]}
            onChange={(v) => upS({ pc_btn_icon: v })}
          />
        </PropRow>
        {sstr('pc_btn_icon', 'none') !== 'none' && (
          <PropRow label="Icon Position" stack>
            <Select
              value={sstr('pc_btn_icon_pos', 'after')}
              options={[{ value: 'before', label: 'Before' }, { value: 'after', label: 'After' }]}
              onChange={(v) => upS({ pc_btn_icon_pos: v })}
            />
          </PropRow>
        )}
        <PropRow label="Open in New Window">
          <Toggle checked={!!c.openNewWindow} onChange={(v) => up({ openNewWindow: v })} />
        </PropRow>
      </PropGroup>

      {/* ── Carousel — only meaningful for the carousel layout ── */}
      {layout === 'carousel' && (
        <PropGroup label="Carousel" defaultOpen>
          <PropRow label="Slides Per View">
            <Select
              value={str('slidesPerView', '4')}
              options={[1, 2, 3, 4, 5, 6].map((n) => ({ value: String(n), label: String(n) }))}
              onChange={(v) => up({ slidesPerView: v })}
            />
          </PropRow>
          <PropRow label="Slides To Scroll">
            <Select
              value={str('slidesToScroll', '1')}
              options={[1, 2, 3, 4, 5, 6].map((n) => ({ value: String(n), label: String(n) }))}
              onChange={(v) => up({ slidesToScroll: v })}
            />
          </PropRow>
          <PropRow label="Loop">
            <Toggle checked={on('carouselLoop', true)} onChange={(v) => up({ carouselLoop: v })} />
          </PropRow>
          <PropRow label="Autoplay (ms, 0 = off)">
            <TextInput value={str('autoplay', '0')} onChange={(v) => up({ autoplay: v })} placeholder="0" />
          </PropRow>
          <PropRow label="Speed (ms)">
            <TextInput value={str('carouselSpeed', '400')} onChange={(v) => up({ carouselSpeed: v })} placeholder="400" />
          </PropRow>
          <PropRow label="Pause On Hover">
            <Toggle checked={on('pauseOnHover', true)} onChange={(v) => up({ pauseOnHover: v })} />
          </PropRow>
          <PropRow label="Navigation Arrows">
            <Toggle checked={on('showArrows', true)} onChange={(v) => up({ showArrows: v })} />
          </PropRow>
          <PropRow label="Dots Pagination">
            <Toggle checked={on('showDots', true)} onChange={(v) => up({ showDots: v })} />
          </PropRow>
        </PropGroup>
      )}

      {/* ── Empty State ── */}
      <PropGroup label="Empty State" defaultOpen={false}>
        <PropRow label="Empty Message" stack>
          <TextInput value={str('emptyMessage')} onChange={(v) => up({ emptyMessage: v })} placeholder="No product categories found." />
        </PropRow>
        <PropRow label="Empty Icon" stack>
          <TextInput value={str('emptyIcon')} onChange={(v) => up({ emptyIcon: v })} placeholder="🗂" />
        </PropRow>
      </PropGroup>
    </div>
  );
}

/* Product Categories — Style tab. Groups mirror Elementor: Category Card,
 * Image, Title, Product Count, Description, Button, Carousel. Every key is a
 * `settings` value sanitized by the 'product-categories' case in
 * Jalpi_Sanitizer::sanitize_settings(). */
function ProductCategoriesStyleTab({ node, store }: { node: CBNode; store: EditorStore }) {
  const s   = (node.settings ?? {}) as Record<string, string>;
  const c   = (node.content  ?? {}) as Record<string, unknown>;
  const str = (k: string, d = '') => s[k] || d;
  const upS = (patch: Record<string, string>) => store.updateNodeSettings(node.id, patch);

  const cardBd = str('pc_card_bd_type', 'none');
  const btnBd  = str('pc_btn_bd_type', 'none');
  const isCar  = String(c.layout ?? 'grid') === 'carousel';

  return (
    <div className="jalpi-props-section">

      {/* ── Category Card ── */}
      <PropGroup label="Category Card" defaultOpen>
        <ColorPropRow label="Background"       value={str('pc_card_bg')}       onChange={(v) => upS({ pc_card_bg: v })}       placeholder="#ffffff" />
        <ColorPropRow label="Hover Background" value={str('pc_card_hover_bg')} onChange={(v) => upS({ pc_card_hover_bg: v })} placeholder="#f9fafb" />
        <PropRow label="Border Type" stack>
          <Select
            value={cardBd}
            options={[
              { value: 'none', label: 'None' }, { value: 'solid', label: 'Solid' },
              { value: 'dashed', label: 'Dashed' }, { value: 'dotted', label: 'Dotted' },
              { value: 'double', label: 'Double' },
            ]}
            onChange={(v) => upS({ pc_card_bd_type: v })}
          />
        </PropRow>
        {cardBd !== 'none' && (
          <>
            <PropRow label="Border Width">
              <SliderDimInput value={str('pc_card_bd_width', '1')} onChange={(v) => upS({ pc_card_bd_width: v })} max={12} step={1} units={['px']} placeholder="1" />
            </PropRow>
            <ColorPropRow label="Border Color" value={str('pc_card_bd_color')}       onChange={(v) => upS({ pc_card_bd_color: v })}       placeholder="#e5e7eb" />
            <ColorPropRow label="Hover Border" value={str('pc_card_hover_bd_color')} onChange={(v) => upS({ pc_card_hover_bd_color: v })} placeholder="#7f54b3" />
          </>
        )}
        <PropRow label="Border Radius">
          <SliderDimInput value={str('pc_card_radius', '12')} onChange={(v) => upS({ pc_card_radius: v })} max={60} units={['px']} placeholder="12" />
        </PropRow>
        <PropRow label="Box Shadow" stack>
          <TextInput value={str('pc_card_shadow')} onChange={(v) => upS({ pc_card_shadow: v })} placeholder="0 1px 3px rgba(0,0,0,.1)" />
        </PropRow>
        <PropRow label="Hover Shadow" stack>
          <TextInput value={str('pc_card_hover_shadow')} onChange={(v) => upS({ pc_card_hover_shadow: v })} placeholder="0 8px 24px rgba(0,0,0,.12)" />
        </PropRow>
        <PropRow label="Hover Lift">
          <SliderDimInput value={str('pc_card_hover_lift')} onChange={(v) => upS({ pc_card_hover_lift: v })} max={24} step={1} units={['px']} placeholder="0" />
        </PropRow>
        <PropRow label="Padding" stack>
          <TextInput value={str('pc_card_pad')} onChange={(v) => upS({ pc_card_pad: v })} placeholder="16px" />
        </PropRow>
        <PropRow label="Margin" stack>
          <TextInput value={str('pc_card_margin')} onChange={(v) => upS({ pc_card_margin: v })} placeholder="0" />
        </PropRow>
        <PropRow label="Body Padding" stack>
          <TextInput value={str('pc_body_pad')} onChange={(v) => upS({ pc_body_pad: v })} placeholder="14px 0 0" />
        </PropRow>
        <PropRow label="Transition (ms)">
          <SliderDimInput value={str('pc_card_transition', '250')} onChange={(v) => upS({ pc_card_transition: v })} max={1200} step={50} units={['']} placeholder="250" />
        </PropRow>
      </PropGroup>

      {/* ── Image ── */}
      <PropGroup label="Image" defaultOpen={false}>
        <PropRow label="Width">
          <SliderDimInput value={str('pc_img_width')} onChange={(v) => upS({ pc_img_width: v })} max={800} units={['px', '%']} placeholder="auto" />
        </PropRow>
        <PropRow label="Height">
          <SliderDimInput value={str('pc_img_height')} onChange={(v) => upS({ pc_img_height: v })} max={800} units={['px']} placeholder="auto" />
        </PropRow>
        <PropRow label="Border Radius">
          <SliderDimInput value={str('pc_img_radius')} onChange={(v) => upS({ pc_img_radius: v })} max={60} units={['px']} placeholder="0" />
        </PropRow>
        <ColorPropRow label="Hover Overlay" value={str('pc_img_overlay_color')} onChange={(v) => upS({ pc_img_overlay_color: v })} placeholder="rgba(0,0,0,0.35)" />
        <ColorPropRow label="Overlay Skin Scrim" value={str('pc_overlay_color')} onChange={(v) => upS({ pc_overlay_color: v })} placeholder="rgba(0,0,0,0.55)" />
      </PropGroup>

      {/* ── Title ── */}
      <PropGroup label="Category Title" defaultOpen={false}>
        <PropRow label="Font Family" stack>
          <TextInput value={str('pc_title_font')} onChange={(v) => upS({ pc_title_font: v })} placeholder="inherit" />
        </PropRow>
        <PropRow label="Font Size">
          <SliderDimInput value={str('pc_title_size', '17')} onChange={(v) => upS({ pc_title_size: v })} max={60} units={['px']} placeholder="17" />
        </PropRow>
        <PropRow label="Font Weight" stack>
          <Select
            value={str('pc_title_weight', '600')}
            options={['300', '400', '500', '600', '700', '800'].map((w) => ({ value: w, label: w }))}
            onChange={(v) => upS({ pc_title_weight: v })}
          />
        </PropRow>
        <PropRow label="Transform" stack>
          <Select
            value={str('pc_title_transform', 'none')}
            options={[
              { value: 'none',       label: 'Default' },
              { value: 'uppercase',  label: 'Uppercase' },
              { value: 'lowercase',  label: 'Lowercase' },
              { value: 'capitalize', label: 'Capitalize' },
            ]}
            onChange={(v) => upS({ pc_title_transform: v })}
          />
        </PropRow>
        <PropRow label="Line Height">
          <SliderDimInput value={str('pc_title_lh', '1.4')} onChange={(v) => upS({ pc_title_lh: v })} min={0} max={3} step={0.1} units={['']} placeholder="1.4" />
        </PropRow>
        <PropRow label="Letter Spacing">
          <SliderDimInput value={str('pc_title_ls')} onChange={(v) => upS({ pc_title_ls: v })} min={-5} max={20} step={0.5} units={['px']} placeholder="0" />
        </PropRow>
        <ColorPropRow label="Text Color"  value={str('pc_title_color')}       onChange={(v) => upS({ pc_title_color: v })}       placeholder="#111827" />
        <ColorPropRow label="Hover Color" value={str('pc_title_hover_color')} onChange={(v) => upS({ pc_title_hover_color: v })} placeholder="#7f54b3" />
        <PropRow label="Spacing / Margin" stack>
          <TextInput value={str('pc_title_margin')} onChange={(v) => upS({ pc_title_margin: v })} placeholder="0 0 4px" />
        </PropRow>
      </PropGroup>

      {/* ── Product Count ── */}
      <PropGroup label="Product Count" defaultOpen={false}>
        <PropRow label="Font Size">
          <SliderDimInput value={str('pc_count_size', '13')} onChange={(v) => upS({ pc_count_size: v })} max={32} units={['px']} placeholder="13" />
        </PropRow>
        <ColorPropRow label="Text Color" value={str('pc_count_color')} onChange={(v) => upS({ pc_count_color: v })} placeholder="#6b7280" />
        <ColorPropRow label="Background" value={str('pc_count_bg')}    onChange={(v) => upS({ pc_count_bg: v })}    placeholder="#f3f4f6" />
        <PropRow label="Border Radius">
          <SliderDimInput value={str('pc_count_radius', '999')} onChange={(v) => upS({ pc_count_radius: v })} max={999} units={['px']} placeholder="999" />
        </PropRow>
        <PropRow label="Padding" stack>
          <TextInput value={str('pc_count_pad')} onChange={(v) => upS({ pc_count_pad: v })} placeholder="2px 8px" />
        </PropRow>
      </PropGroup>

      {/* ── Description ── */}
      <PropGroup label="Description" defaultOpen={false}>
        <PropRow label="Font Size">
          <SliderDimInput value={str('pc_desc_size', '13')} onChange={(v) => upS({ pc_desc_size: v })} max={32} units={['px']} placeholder="13" />
        </PropRow>
        <ColorPropRow label="Color" value={str('pc_desc_color')} onChange={(v) => upS({ pc_desc_color: v })} placeholder="#6b7280" />
        <PropRow label="Alignment" stack>
          <Select
            value={str('pc_desc_align', 'inherit')}
            options={[
              { value: 'inherit', label: 'Inherit' }, { value: 'left', label: 'Left' },
              { value: 'center', label: 'Center' }, { value: 'right', label: 'Right' },
            ]}
            onChange={(v) => upS({ pc_desc_align: v })}
          />
        </PropRow>
        <PropRow label="Spacing">
          <SliderDimInput value={str('pc_desc_spacing', '6')} onChange={(v) => upS({ pc_desc_spacing: v })} max={40} units={['px']} placeholder="6" />
        </PropRow>
      </PropGroup>

      {/* ── Button ── */}
      <PropGroup label="Button" defaultOpen={false}>
        <ColorPropRow label="Text Color" value={str('pc_btn_color')} onChange={(v) => upS({ pc_btn_color: v })} placeholder="#ffffff" />
        <ColorPropRow label="Background" value={str('pc_btn_bg')}    onChange={(v) => upS({ pc_btn_bg: v })}    placeholder="#111827" />
        <PropRow label="Font Size">
          <SliderDimInput value={str('pc_btn_size', '13')} onChange={(v) => upS({ pc_btn_size: v })} max={32} units={['px']} placeholder="13" />
        </PropRow>
        <PropRow label="Font Weight" stack>
          <Select
            value={str('pc_btn_weight', '600')}
            options={['300', '400', '500', '600', '700', '800'].map((w) => ({ value: w, label: w }))}
            onChange={(v) => upS({ pc_btn_weight: v })}
          />
        </PropRow>
        <PropRow label="Padding" stack>
          <TextInput value={str('pc_btn_pad')} onChange={(v) => upS({ pc_btn_pad: v })} placeholder="8px 16px" />
        </PropRow>
        <PropRow label="Border Radius">
          <SliderDimInput value={str('pc_btn_radius', '8')} onChange={(v) => upS({ pc_btn_radius: v })} max={60} units={['px']} placeholder="8" />
        </PropRow>
        <PropRow label="Border Type" stack>
          <Select
            value={btnBd}
            options={[
              { value: 'none', label: 'None' }, { value: 'solid', label: 'Solid' },
              { value: 'dashed', label: 'Dashed' }, { value: 'dotted', label: 'Dotted' },
            ]}
            onChange={(v) => upS({ pc_btn_bd_type: v })}
          />
        </PropRow>
        {btnBd !== 'none' && (
          <>
            <PropRow label="Border Width">
              <SliderDimInput value={str('pc_btn_bd_width', '1')} onChange={(v) => upS({ pc_btn_bd_width: v })} max={10} step={1} units={['px']} placeholder="1" />
            </PropRow>
            <ColorPropRow label="Border Color" value={str('pc_btn_bd_color')} onChange={(v) => upS({ pc_btn_bd_color: v })} placeholder="#111827" />
          </>
        )}
        <ColorPropRow label="Hover Text"       value={str('pc_btn_hover_color')} onChange={(v) => upS({ pc_btn_hover_color: v })} placeholder="#ffffff" />
        <ColorPropRow label="Hover Background" value={str('pc_btn_hover_bg')}    onChange={(v) => upS({ pc_btn_hover_bg: v })}    placeholder="#7f54b3" />
        <ColorPropRow label="Hover Border"     value={str('pc_btn_hover_bd')}    onChange={(v) => upS({ pc_btn_hover_bd: v })}    placeholder="#7f54b3" />
        <PropRow label="Hover Animation" stack>
          <Select
            value={str('pc_btn_hover_anim', 'none')}
            options={[
              { value: 'none', label: 'None' }, { value: 'grow', label: 'Grow' },
              { value: 'shrink', label: 'Shrink' }, { value: 'float', label: 'Float' },
              { value: 'sink', label: 'Sink' },
            ]}
            onChange={(v) => upS({ pc_btn_hover_anim: v })}
          />
        </PropRow>
        <PropRow label="Spacing">
          <SliderDimInput value={str('pc_btn_spacing', '12')} onChange={(v) => upS({ pc_btn_spacing: v })} max={60} units={['px']} placeholder="12" />
        </PropRow>
      </PropGroup>

      {/* ── Carousel chrome — only when the carousel layout is active ── */}
      {isCar && (
        <PropGroup label="Carousel" defaultOpen={false}>
          <PropRow label="Arrow Size">
            <SliderDimInput value={str('pc_arrow_size', '18')} onChange={(v) => upS({ pc_arrow_size: v })} max={48} units={['px']} placeholder="18" />
          </PropRow>
          <ColorPropRow label="Arrow Color"      value={str('pc_arrow_color')}      onChange={(v) => upS({ pc_arrow_color: v })}      placeholder="#111827" />
          <ColorPropRow label="Arrow Background" value={str('pc_arrow_bg')}         onChange={(v) => upS({ pc_arrow_bg: v })}         placeholder="#ffffff" />
          <ColorPropRow label="Dot Color"        value={str('pc_dot_color')}        onChange={(v) => upS({ pc_dot_color: v })}        placeholder="#d1d5db" />
          <ColorPropRow label="Active Dot"       value={str('pc_dot_active_color')} onChange={(v) => upS({ pc_dot_active_color: v })} placeholder="#111827" />
          <PropRow label="Dot Spacing">
            <SliderDimInput value={str('pc_dot_spacing', '8')} onChange={(v) => upS({ pc_dot_spacing: v })} max={40} units={['px']} placeholder="8" />
          </PropRow>
        </PropGroup>
      )}

      {/* ── Empty State ── */}
      <PropGroup label="Empty State" defaultOpen={false}>
        <ColorPropRow label="Text Color" value={str('pc_empty_color')} onChange={(v) => upS({ pc_empty_color: v })} placeholder="#6b7280" />
      </PropGroup>
    </div>
  );
}

/* ── WooCommerce: Products Grid ─────────────────────────────────────────────
 * Content tab, organized like Elementor: Layout → Query → Product Elements →
 * Image → Button → Pagination → Empty State. Controls whose effect is purely
 * visual live in ProductsGridStyleTab instead.
 *
 * Column counts / gaps are stored in `settings` (not `content`) because they
 * are per-breakpoint style values — same convention the Gallery widget uses. */
function ProductsGridContent({ node, store }: { node: CBNode; store: EditorStore }) {
  const c   = (node.content  ?? {}) as Record<string, unknown>;
  const s   = (node.settings ?? {}) as Record<string, string>;
  const up  = (patch: Record<string, unknown>) => store.updateNodeContent(node.id, patch);
  const upS = (patch: Record<string, string>)  => store.updateNodeSettings(node.id, patch);

  const str  = (k: string, d = '') => (c[k] == null || c[k] === '' ? d : String(c[k]));
  const sstr = (k: string, d = '') => s[k] || d;
  // Absent key → documented default (matches the PHP element's fallbacks).
  const on   = (k: string, d: boolean) => (k in c ? !!c[k] : d);

  const source     = str('source', 'latest');
  const pagination = str('pagination', 'none');
  const isManual   = source === 'manual';

  /** One toggle row per product element. */
  const ElToggle = ({ k, label, def }: { k: string; label: string; def: boolean }) => (
    <PropRow label={label}>
      <Toggle checked={on(k, def)} onChange={(v) => up({ [k]: v })} />
    </PropRow>
  );

  return (
    <div className="jalpi-props-section">

      {/* ── Layout ── */}
      <PropGroup label="Layout" defaultOpen>
        <PropRow label="Skin" stack>
          <Select
            value={str('skin', 'classic')}
            options={[
              { value: 'classic', label: 'Classic' },
              { value: 'cards',   label: 'Cards' },
              { value: 'minimal', label: 'Minimal' },
            ]}
            onChange={(v) => up({ skin: v })}
          />
        </PropRow>
        <PropRow label="Columns" responsive>
          <Select
            value={sstr('pg_cols_desktop', '4')}
            options={[1, 2, 3, 4, 5, 6].map((n) => ({ value: String(n), label: String(n) }))}
            onChange={(v) => upS({ pg_cols_desktop: v })}
          />
        </PropRow>
        <PropRow label="Columns (Tablet)">
          <Select
            value={sstr('pg_cols_tablet', '2')}
            options={[1, 2, 3, 4, 5, 6].map((n) => ({ value: String(n), label: String(n) }))}
            onChange={(v) => upS({ pg_cols_tablet: v })}
          />
        </PropRow>
        <PropRow label="Columns (Mobile)">
          <Select
            value={sstr('pg_cols_mobile', '1')}
            options={[1, 2, 3, 4, 5, 6].map((n) => ({ value: String(n), label: String(n) }))}
            onChange={(v) => upS({ pg_cols_mobile: v })}
          />
        </PropRow>

        {/* Rows vs. Products Per Page are mutually exclusive, as in Elementor:
            with Rows on, the count is columns × rows. */}
        <PropRow label="Use Rows">
          <Toggle checked={!!c.useRows} onChange={(v) => up({ useRows: v })} />
        </PropRow>
        {c.useRows ? (
          <PropRow label="Rows">
            <TextInput value={str('rows', '1')} onChange={(v) => up({ rows: v })} placeholder="1" />
          </PropRow>
        ) : (
          <PropRow label="Products Per Page">
            <TextInput value={str('perPage', '8')} onChange={(v) => up({ perPage: v })} placeholder="8" />
          </PropRow>
        )}

        <PropRow label="Column Gap">
          <SliderDimInput value={sstr('pg_col_gap', '24')} onChange={(v) => upS({ pg_col_gap: v })} max={100} step={2} units={['px']} placeholder="24" />
        </PropRow>
        <PropRow label="Row Gap">
          <SliderDimInput value={sstr('pg_row_gap', '32')} onChange={(v) => upS({ pg_row_gap: v })} max={100} step={2} units={['px']} placeholder="32" />
        </PropRow>
        <PropRow label="Equal Height">
          <Toggle checked={on('equalHeight', true)} onChange={(v) => up({ equalHeight: v })} />
        </PropRow>
        <PropRow label="Masonry">
          <Toggle checked={!!c.masonry} onChange={(v) => up({ masonry: v })} />
        </PropRow>
      </PropGroup>

      {/* ── Query ── */}
      <PropGroup label="Query" defaultOpen>
        <PropRow label="Source" stack>
          <Select
            value={source}
            options={[
              { value: 'latest',       label: 'Latest Products' },
              { value: 'featured',     label: 'Featured Products' },
              { value: 'sale',         label: 'Sale Products' },
              { value: 'best-selling', label: 'Best Selling' },
              { value: 'top-rated',    label: 'Top Rated' },
              { value: 'related',      label: 'Related Products' },
              { value: 'upsells',      label: 'Upsells' },
              { value: 'cross-sells',  label: 'Cross Sells' },
              { value: 'manual',       label: 'Manual Selection' },
            ]}
            onChange={(v) => up({ source: v })}
          />
        </PropRow>

        {/* Related/upsells/cross-sells derive from the current product. */}
        {['related', 'upsells', 'cross-sells'].includes(source) && (
          <div className="jalpi-prop-hint" style={{ fontSize: 11, opacity: 0.7, padding: '4px 0 8px' }}>
            Pulls from the product being viewed — shows the empty state elsewhere.
          </div>
        )}

        {isManual && (
          <PropRow label="Include Products" stack>
            <TextInput value={str('includeProducts')} onChange={(v) => up({ includeProducts: v })} placeholder="12, 34, 56" />
          </PropRow>
        )}

        <PropRow label="Categories" stack>
          <TextInput value={str('categories')} onChange={(v) => up({ categories: v })} placeholder="hoodies, shoes" />
        </PropRow>
        <PropRow label="Tags" stack>
          <TextInput value={str('tags')} onChange={(v) => up({ tags: v })} placeholder="sale, new" />
        </PropRow>
        <PropRow label="Brands" stack>
          <TextInput value={str('brands')} onChange={(v) => up({ brands: v })} placeholder="acme" />
        </PropRow>
        <PropRow label="Exclude Products" stack>
          <TextInput value={str('excludeProducts')} onChange={(v) => up({ excludeProducts: v })} placeholder="12, 34" />
        </PropRow>
        <PropRow label="Include by ID" stack>
          <TextInput value={str('includeIds')} onChange={(v) => up({ includeIds: v })} placeholder="12, 34" />
        </PropRow>
        <PropRow label="Exclude by ID" stack>
          <TextInput value={str('excludeIds')} onChange={(v) => up({ excludeIds: v })} placeholder="56, 78" />
        </PropRow>

        <PropRow label="Stock Status" stack>
          <Select
            value={str('stockStatus', 'any')}
            options={[
              { value: 'any',         label: 'Any' },
              { value: 'instock',     label: 'In Stock' },
              { value: 'outofstock',  label: 'Out of Stock' },
              { value: 'onbackorder', label: 'On Backorder' },
            ]}
            onChange={(v) => up({ stockStatus: v })}
          />
        </PropRow>
        <PropRow label="Product Status" stack>
          <Select
            value={str('productStatus', 'publish')}
            options={[
              { value: 'publish', label: 'Published' },
              { value: 'private', label: 'Private' },
              { value: 'any',     label: 'Any' },
            ]}
            onChange={(v) => up({ productStatus: v })}
          />
        </PropRow>

        <PropRow label="Order By" stack>
          <Select
            value={str('orderBy', 'date')}
            options={[
              { value: 'date',       label: 'Date' },
              { value: 'title',      label: 'Title' },
              { value: 'price',      label: 'Price' },
              { value: 'popularity', label: 'Popularity' },
              { value: 'rating',     label: 'Rating' },
              { value: 'menu_order', label: 'Menu Order' },
              { value: 'modified',   label: 'Last Modified' },
              { value: 'rand',       label: 'Random' },
            ]}
            onChange={(v) => up({ orderBy: v })}
          />
        </PropRow>
        <PropRow label="Order" stack>
          <Select
            value={str('order', 'DESC')}
            options={[{ value: 'DESC', label: 'Descending' }, { value: 'ASC', label: 'Ascending' }]}
            onChange={(v) => up({ order: v })}
          />
        </PropRow>
        <PropRow label="Offset">
          <TextInput value={str('offset', '0')} onChange={(v) => up({ offset: v })} placeholder="0" />
        </PropRow>
        <PropRow label="Ignore Sticky">
          <Toggle checked={!!c.ignoreSticky} onChange={(v) => up({ ignoreSticky: v })} />
        </PropRow>
      </PropGroup>

      {/* ── Product Elements ── */}
      <PropGroup label="Product Elements" defaultOpen={false}>
        <ElToggle k="showImage"      label="Product Image"      def />
        <ElToggle k="showSaleBadge"  label="Sale Badge"         def />
        <ElToggle k="showTitle"      label="Product Title"      def />
        <ElToggle k="showRating"     label="Product Rating"     def />
        <ElToggle k="showPrice"      label="Product Price"      def />
        <ElToggle k="showExcerpt"    label="Short Description"  def={false} />
        <ElToggle k="showAddToCart"  label="Add To Cart"        def />
        <ElToggle k="showWishlist"   label="Wishlist"           def={false} />
        <ElToggle k="showCompare"    label="Compare"            def={false} />
        <ElToggle k="showQuickView"  label="Quick View"         def={false} />
        <ElToggle k="showCategories" label="Product Categories" def={false} />
        <ElToggle k="showSku"        label="Product SKU"        def={false} />
      </PropGroup>

      {/* ── Image Settings ── */}
      <PropGroup label="Image" defaultOpen={false}>
        <PropRow label="Image Size" stack>
          <Select
            value={sstr('pg_img_size', 'woocommerce_thumbnail')}
            options={[
              { value: 'woocommerce_thumbnail',    label: 'Shop Thumbnail' },
              { value: 'woocommerce_single',       label: 'Single (Large)' },
              { value: 'thumbnail',                label: 'Thumbnail' },
              { value: 'medium',                   label: 'Medium' },
              { value: 'large',                    label: 'Large' },
              { value: 'full',                     label: 'Full' },
            ]}
            onChange={(v) => upS({ pg_img_size: v })}
          />
        </PropRow>
        <PropRow label="Image Ratio" stack>
          <Select
            value={sstr('pg_img_ratio', '1/1')}
            options={[
              { value: '1/1',  label: 'Square (1:1)' },
              { value: '4/3',  label: 'Landscape (4:3)' },
              { value: '3/4',  label: 'Portrait (3:4)' },
              { value: '16/9', label: 'Widescreen (16:9)' },
              { value: 'auto', label: 'Auto' },
            ]}
            onChange={(v) => upS({ pg_img_ratio: v })}
          />
        </PropRow>
        <PropRow label="Image Fit" stack>
          <Select
            value={sstr('pg_img_fit', 'cover')}
            options={[
              { value: 'cover',      label: 'Cover' },
              { value: 'contain',    label: 'Contain' },
              { value: 'fill',       label: 'Fill' },
              { value: 'none',       label: 'None' },
              { value: 'scale-down', label: 'Scale Down' },
            ]}
            onChange={(v) => upS({ pg_img_fit: v })}
          />
        </PropRow>
        <PropRow label="Hover Effect" stack>
          <Select
            value={sstr('pg_img_hover', 'zoom')}
            options={[
              { value: 'none',      label: 'None' },
              { value: 'zoom',      label: 'Zoom In' },
              { value: 'zoom-out',  label: 'Zoom Out' },
              { value: 'grayscale', label: 'Grayscale' },
              { value: 'blur',      label: 'Blur' },
              { value: 'lift',      label: 'Lift' },
            ]}
            onChange={(v) => upS({ pg_img_hover: v })}
          />
        </PropRow>
        <PropRow label="Secondary Image on Hover">
          <Toggle checked={!!c.secondaryImage} onChange={(v) => up({ secondaryImage: v })} />
        </PropRow>
        <PropRow label="Lazy Load">
          <Toggle checked={on('lazyLoad', true)} onChange={(v) => up({ lazyLoad: v })} />
        </PropRow>
      </PropGroup>

      {/* ── Button Settings ── */}
      <PropGroup label="Button" defaultOpen={false}>
        <PropRow label="Button Text" stack>
          <TextInput value={str('buttonText')} onChange={(v) => up({ buttonText: v })} placeholder="Add to cart" />
        </PropRow>
        <PropRow label="Icon" stack>
          <Select
            value={sstr('pg_btn_icon', 'none')}
            options={[
              { value: 'none',  label: 'None' },
              { value: 'cart',  label: 'Cart 🛒' },
              { value: 'bag',   label: 'Bag 🛍' },
              { value: 'plus',  label: 'Plus ＋' },
              { value: 'arrow', label: 'Arrow →' },
            ]}
            onChange={(v) => upS({ pg_btn_icon: v })}
          />
        </PropRow>
        {sstr('pg_btn_icon', 'none') !== 'none' && (
          <PropRow label="Icon Position" stack>
            <Select
              value={sstr('pg_btn_icon_pos', 'before')}
              options={[{ value: 'before', label: 'Before' }, { value: 'after', label: 'After' }]}
              onChange={(v) => upS({ pg_btn_icon_pos: v })}
            />
          </PropRow>
        )}
        <PropRow label="Show Quantity">
          <Toggle checked={!!c.showQuantity} onChange={(v) => up({ showQuantity: v })} />
        </PropRow>
        <PropRow label="AJAX Add To Cart">
          <Toggle checked={on('ajaxAddToCart', true)} onChange={(v) => up({ ajaxAddToCart: v })} />
        </PropRow>
      </PropGroup>

      {/* ── Pagination ── */}
      <PropGroup label="Pagination" defaultOpen={false}>
        <PropRow label="Type" stack>
          <Select
            value={pagination}
            options={[
              { value: 'none',      label: 'None' },
              { value: 'numbers',   label: 'Numbers' },
              { value: 'prev-next', label: 'Previous / Next' },
              { value: 'load-more', label: 'Load More' },
              { value: 'infinite',  label: 'Infinite Scroll' },
            ]}
            onChange={(v) => up({ pagination: v })}
          />
        </PropRow>
        {pagination === 'load-more' && (
          <PropRow label="Load More Text" stack>
            <TextInput value={str('loadMoreText', 'Load More')} onChange={(v) => up({ loadMoreText: v })} placeholder="Load More" />
          </PropRow>
        )}
      </PropGroup>

      {/* ── Empty State ── */}
      <PropGroup label="Empty State" defaultOpen={false}>
        <PropRow label="Empty Message" stack>
          <TextInput value={str('emptyMessage')} onChange={(v) => up({ emptyMessage: v })} placeholder="No products found." />
        </PropRow>
        <PropRow label="Empty Icon" stack>
          <TextInput value={str('emptyIcon')} onChange={(v) => up({ emptyIcon: v })} placeholder="🛒" />
        </PropRow>
      </PropGroup>
    </div>
  );
}

/* Products Grid — Style tab. Groups mirror Elementor: Grid, Product Card,
 * Image, Sale Badge, Title, Price, Rating, Button, Wishlist/Compare,
 * Pagination. Every key here is a `settings` value sanitized by the
 * 'products-grid' case in Jalpi_Sanitizer::sanitize_settings(). */
function ProductsGridStyleTab({ node, store }: { node: CBNode; store: EditorStore }) {
  const s   = (node.settings ?? {}) as Record<string, string>;
  const str = (k: string, d = '') => s[k] || d;
  const upS = (patch: Record<string, string>) => store.updateNodeSettings(node.id, patch);

  const cardBd = str('pg_card_bd_type', 'none');
  const btnBd  = str('pg_btn_bd_type', 'none');

  return (
    <div className="jalpi-props-section">

      {/* ── Grid ── */}
      <PropGroup label="Grid" defaultOpen>
        <PropRow label="Columns Gap">
          <SliderDimInput value={str('pg_col_gap', '24')} onChange={(v) => upS({ pg_col_gap: v })} max={100} step={2} units={['px']} placeholder="24" />
        </PropRow>
        <PropRow label="Rows Gap">
          <SliderDimInput value={str('pg_row_gap', '32')} onChange={(v) => upS({ pg_row_gap: v })} max={100} step={2} units={['px']} placeholder="32" />
        </PropRow>
        <PropRow label="Card Alignment" stack>
          <Select
            value={str('pg_card_align', 'left')}
            options={[{ value: 'left', label: 'Left' }, { value: 'center', label: 'Center' }, { value: 'right', label: 'Right' }]}
            onChange={(v) => upS({ pg_card_align: v })}
          />
        </PropRow>
      </PropGroup>

      {/* ── Product Card ── */}
      <PropGroup label="Product Card" defaultOpen={false}>
        <ColorPropRow label="Background" value={str('pg_card_bg')} onChange={(v) => upS({ pg_card_bg: v })} placeholder="#ffffff" />
        <PropRow label="Padding" stack>
          <TextInput value={str('pg_card_pad')} onChange={(v) => upS({ pg_card_pad: v })} placeholder="16px" />
        </PropRow>
        <PropRow label="Border Radius">
          <SliderDimInput value={str('pg_card_radius', '12')} onChange={(v) => upS({ pg_card_radius: v })} max={60} units={['px']} placeholder="12" />
        </PropRow>
        <PropRow label="Border Type" stack>
          <Select
            value={cardBd}
            options={[
              { value: 'none', label: 'None' }, { value: 'solid', label: 'Solid' },
              { value: 'dashed', label: 'Dashed' }, { value: 'dotted', label: 'Dotted' },
              { value: 'double', label: 'Double' },
            ]}
            onChange={(v) => upS({ pg_card_bd_type: v })}
          />
        </PropRow>
        {cardBd !== 'none' && (
          <>
            <PropRow label="Border Width">
              <SliderDimInput value={str('pg_card_bd_width', '1')} onChange={(v) => upS({ pg_card_bd_width: v })} max={12} step={1} units={['px']} placeholder="1" />
            </PropRow>
            <ColorPropRow label="Border Color" value={str('pg_card_bd_color')} onChange={(v) => upS({ pg_card_bd_color: v })} placeholder="#e5e7eb" />
            <ColorPropRow label="Hover Border" value={str('pg_card_hover_bd_color')} onChange={(v) => upS({ pg_card_hover_bd_color: v })} placeholder="#111827" />
          </>
        )}
        <PropRow label="Shadow" stack>
          <TextInput value={str('pg_card_shadow')} onChange={(v) => upS({ pg_card_shadow: v })} placeholder="0 1px 3px rgba(0,0,0,.1)" />
        </PropRow>
        <PropRow label="Hover Shadow" stack>
          <TextInput value={str('pg_card_hover_shadow')} onChange={(v) => upS({ pg_card_hover_shadow: v })} placeholder="0 8px 24px rgba(0,0,0,.12)" />
        </PropRow>
        <PropRow label="Hover Lift">
          <SliderDimInput value={str('pg_card_hover_lift')} onChange={(v) => upS({ pg_card_hover_lift: v })} max={24} step={1} units={['px']} placeholder="0" />
        </PropRow>
        <PropRow label="Transition (ms)">
          <SliderDimInput value={str('pg_card_transition', '250')} onChange={(v) => upS({ pg_card_transition: v })} max={1200} step={50} units={['']} placeholder="250" />
        </PropRow>
        <PropRow label="Body Padding" stack>
          <TextInput value={str('pg_body_pad')} onChange={(v) => upS({ pg_body_pad: v })} placeholder="14px 0 0" />
        </PropRow>
      </PropGroup>

      {/* ── Image ── */}
      <PropGroup label="Image" defaultOpen={false}>
        <PropRow label="Border Radius">
          <SliderDimInput value={str('pg_img_radius')} onChange={(v) => upS({ pg_img_radius: v })} max={60} units={['px']} placeholder="0" />
        </PropRow>
        <PropRow label="Width">
          <SliderDimInput value={str('pg_img_width')} onChange={(v) => upS({ pg_img_width: v })} max={800} units={['px', '%']} placeholder="auto" />
        </PropRow>
        <PropRow label="Height">
          <SliderDimInput value={str('pg_img_height')} onChange={(v) => upS({ pg_img_height: v })} max={800} units={['px']} placeholder="auto" />
        </PropRow>
        <ColorPropRow label="Hover Overlay" value={str('pg_img_overlay_color')} onChange={(v) => upS({ pg_img_overlay_color: v })} placeholder="rgba(0,0,0,0.2)" />
      </PropGroup>

      {/* ── Sale Badge ── */}
      <PropGroup label="Sale Badge" defaultOpen={false}>
        <ColorPropRow label="Text Color" value={str('pg_badge_color')} onChange={(v) => upS({ pg_badge_color: v })} placeholder="#ffffff" />
        <ColorPropRow label="Background"  value={str('pg_badge_bg')}    onChange={(v) => upS({ pg_badge_bg: v })}    placeholder="#111827" />
        <PropRow label="Position" stack>
          <Select
            value={str('pg_badge_pos', 'top-left')}
            options={[
              { value: 'top-left',     label: 'Top Left' },
              { value: 'top-right',    label: 'Top Right' },
              { value: 'bottom-left',  label: 'Bottom Left' },
              { value: 'bottom-right', label: 'Bottom Right' },
            ]}
            onChange={(v) => upS({ pg_badge_pos: v })}
          />
        </PropRow>
        <PropRow label="Font Size">
          <SliderDimInput value={str('pg_badge_size', '12')} onChange={(v) => upS({ pg_badge_size: v })} max={32} units={['px']} placeholder="12" />
        </PropRow>
        <PropRow label="Font Weight" stack>
          <Select
            value={str('pg_badge_weight', '600')}
            options={['300', '400', '500', '600', '700', '800'].map((w) => ({ value: w, label: w }))}
            onChange={(v) => upS({ pg_badge_weight: v })}
          />
        </PropRow>
        <PropRow label="Border Radius">
          <SliderDimInput value={str('pg_badge_radius', '4')} onChange={(v) => upS({ pg_badge_radius: v })} max={40} units={['px']} placeholder="4" />
        </PropRow>
        <PropRow label="Padding" stack>
          <TextInput value={str('pg_badge_pad')} onChange={(v) => upS({ pg_badge_pad: v })} placeholder="4px 10px" />
        </PropRow>
      </PropGroup>

      {/* ── Title ── */}
      <PropGroup label="Title" defaultOpen={false}>
        <ColorPropRow label="Color"       value={str('pg_title_color')}       onChange={(v) => upS({ pg_title_color: v })}       placeholder="#111827" />
        <ColorPropRow label="Hover Color" value={str('pg_title_hover_color')} onChange={(v) => upS({ pg_title_hover_color: v })} placeholder="#7f54b3" />
        <PropRow label="Font Size">
          <SliderDimInput value={str('pg_title_size', '16')} onChange={(v) => upS({ pg_title_size: v })} max={48} units={['px']} placeholder="16" />
        </PropRow>
        <PropRow label="Font Weight" stack>
          <Select
            value={str('pg_title_weight', '600')}
            options={['300', '400', '500', '600', '700', '800'].map((w) => ({ value: w, label: w }))}
            onChange={(v) => upS({ pg_title_weight: v })}
          />
        </PropRow>
        <PropRow label="Margin" stack>
          <TextInput value={str('pg_title_margin')} onChange={(v) => upS({ pg_title_margin: v })} placeholder="0 0 6px" />
        </PropRow>
      </PropGroup>

      {/* ── Price ── */}
      <PropGroup label="Price" defaultOpen={false}>
        <ColorPropRow label="Regular Price" value={str('pg_price_color')}      onChange={(v) => upS({ pg_price_color: v })}      placeholder="#111827" />
        <ColorPropRow label="Sale Price"    value={str('pg_sale_price_color')} onChange={(v) => upS({ pg_sale_price_color: v })} placeholder="#dc2626" />
        <PropRow label="Font Size">
          <SliderDimInput value={str('pg_price_size', '15')} onChange={(v) => upS({ pg_price_size: v })} max={40} units={['px']} placeholder="15" />
        </PropRow>
        <PropRow label="Font Weight" stack>
          <Select
            value={str('pg_price_weight', '600')}
            options={['300', '400', '500', '600', '700', '800'].map((w) => ({ value: w, label: w }))}
            onChange={(v) => upS({ pg_price_weight: v })}
          />
        </PropRow>
        <PropRow label="Spacing">
          <SliderDimInput value={str('pg_price_spacing', '6')} onChange={(v) => upS({ pg_price_spacing: v })} max={40} units={['px']} placeholder="6" />
        </PropRow>
      </PropGroup>

      {/* ── Rating ── */}
      <PropGroup label="Rating" defaultOpen={false}>
        <PropRow label="Star Size">
          <SliderDimInput value={str('pg_star_size', '15')} onChange={(v) => upS({ pg_star_size: v })} max={40} units={['px']} placeholder="15" />
        </PropRow>
        <ColorPropRow label="Filled Color" value={str('pg_star_color')}       onChange={(v) => upS({ pg_star_color: v })}       placeholder="#f59e0b" />
        <ColorPropRow label="Empty Color"  value={str('pg_star_empty_color')} onChange={(v) => upS({ pg_star_empty_color: v })} placeholder="#d1d5db" />
        <PropRow label="Spacing">
          <SliderDimInput value={str('pg_rating_spacing', '4')} onChange={(v) => upS({ pg_rating_spacing: v })} max={40} units={['px']} placeholder="4" />
        </PropRow>
      </PropGroup>

      {/* ── Button ── */}
      <PropGroup label="Button" defaultOpen={false}>
        <ColorPropRow label="Text Color" value={str('pg_btn_color')} onChange={(v) => upS({ pg_btn_color: v })} placeholder="#ffffff" />
        <ColorPropRow label="Background" value={str('pg_btn_bg')}    onChange={(v) => upS({ pg_btn_bg: v })}    placeholder="#111827" />
        <PropRow label="Font Size">
          <SliderDimInput value={str('pg_btn_size', '14')} onChange={(v) => upS({ pg_btn_size: v })} max={32} units={['px']} placeholder="14" />
        </PropRow>
        <PropRow label="Font Weight" stack>
          <Select
            value={str('pg_btn_weight', '600')}
            options={['300', '400', '500', '600', '700', '800'].map((w) => ({ value: w, label: w }))}
            onChange={(v) => upS({ pg_btn_weight: v })}
          />
        </PropRow>
        <PropRow label="Padding" stack>
          <TextInput value={str('pg_btn_pad')} onChange={(v) => upS({ pg_btn_pad: v })} placeholder="10px 18px" />
        </PropRow>
        <PropRow label="Border Radius">
          <SliderDimInput value={str('pg_btn_radius', '8')} onChange={(v) => upS({ pg_btn_radius: v })} max={60} units={['px']} placeholder="8" />
        </PropRow>
        <PropRow label="Border Type" stack>
          <Select
            value={btnBd}
            options={[
              { value: 'none', label: 'None' }, { value: 'solid', label: 'Solid' },
              { value: 'dashed', label: 'Dashed' }, { value: 'dotted', label: 'Dotted' },
            ]}
            onChange={(v) => upS({ pg_btn_bd_type: v })}
          />
        </PropRow>
        {btnBd !== 'none' && (
          <>
            <PropRow label="Border Width">
              <SliderDimInput value={str('pg_btn_bd_width', '1')} onChange={(v) => upS({ pg_btn_bd_width: v })} max={10} step={1} units={['px']} placeholder="1" />
            </PropRow>
            <ColorPropRow label="Border Color" value={str('pg_btn_bd_color')} onChange={(v) => upS({ pg_btn_bd_color: v })} placeholder="#111827" />
          </>
        )}
        <ColorPropRow label="Hover Text"       value={str('pg_btn_hover_color')} onChange={(v) => upS({ pg_btn_hover_color: v })} placeholder="#ffffff" />
        <ColorPropRow label="Hover Background" value={str('pg_btn_hover_bg')}    onChange={(v) => upS({ pg_btn_hover_bg: v })}    placeholder="#7f54b3" />
        <ColorPropRow label="Hover Border"     value={str('pg_btn_hover_bd')}    onChange={(v) => upS({ pg_btn_hover_bd: v })}    placeholder="#7f54b3" />
        <PropRow label="Hover Animation" stack>
          <Select
            value={str('pg_btn_hover_anim', 'none')}
            options={[
              { value: 'none',   label: 'None' },
              { value: 'grow',   label: 'Grow' },
              { value: 'shrink', label: 'Shrink' },
              { value: 'float',  label: 'Float' },
              { value: 'sink',   label: 'Sink' },
            ]}
            onChange={(v) => upS({ pg_btn_hover_anim: v })}
          />
        </PropRow>
        <PropRow label="Spacing">
          <SliderDimInput value={str('pg_btn_spacing', '12')} onChange={(v) => upS({ pg_btn_spacing: v })} max={60} units={['px']} placeholder="12" />
        </PropRow>
      </PropGroup>

      {/* ── Wishlist / Compare / Quick View share one action-button style. ── */}
      <PropGroup label="Wishlist & Compare" defaultOpen={false}>
        <PropRow label="Icon Size">
          <SliderDimInput value={str('pg_action_size', '16')} onChange={(v) => upS({ pg_action_size: v })} max={40} units={['px']} placeholder="16" />
        </PropRow>
        <ColorPropRow label="Color"       value={str('pg_action_color')}       onChange={(v) => upS({ pg_action_color: v })}       placeholder="#111827" />
        <ColorPropRow label="Background"  value={str('pg_action_bg')}          onChange={(v) => upS({ pg_action_bg: v })}          placeholder="#ffffff" />
        <ColorPropRow label="Hover Color" value={str('pg_action_hover_color')} onChange={(v) => upS({ pg_action_hover_color: v })} placeholder="#dc2626" />
      </PropGroup>

      {/* ── Pagination ── */}
      <PropGroup label="Pagination" defaultOpen={false}>
        <PropRow label="Font Size">
          <SliderDimInput value={str('pg_pag_size', '14')} onChange={(v) => upS({ pg_pag_size: v })} max={28} units={['px']} placeholder="14" />
        </PropRow>
        <ColorPropRow label="Color"        value={str('pg_pag_color')}        onChange={(v) => upS({ pg_pag_color: v })}        placeholder="#111827" />
        <ColorPropRow label="Active Color" value={str('pg_pag_active_color')} onChange={(v) => upS({ pg_pag_active_color: v })} placeholder="#111827" />
        <ColorPropRow label="Hover Color"  value={str('pg_pag_hover_color')}  onChange={(v) => upS({ pg_pag_hover_color: v })}  placeholder="#7f54b3" />
        <ColorPropRow label="Border Color" value={str('pg_pag_bd_color')}     onChange={(v) => upS({ pg_pag_bd_color: v })}     placeholder="#e5e7eb" />
        <PropRow label="Border Radius">
          <SliderDimInput value={str('pg_pag_radius', '6')} onChange={(v) => upS({ pg_pag_radius: v })} max={40} units={['px']} placeholder="6" />
        </PropRow>
        <PropRow label="Spacing">
          <SliderDimInput value={str('pg_pag_spacing', '8')} onChange={(v) => upS({ pg_pag_spacing: v })} max={40} units={['px']} placeholder="8" />
        </PropRow>
        <PropRow label="Top Margin">
          <SliderDimInput value={str('pg_pag_margin', '28')} onChange={(v) => upS({ pg_pag_margin: v })} max={120} units={['px']} placeholder="28" />
        </PropRow>
      </PropGroup>

      {/* ── Empty State ── */}
      <PropGroup label="Empty State" defaultOpen={false}>
        <ColorPropRow label="Text Color" value={str('pg_empty_color')} onChange={(v) => upS({ pg_empty_color: v })} placeholder="#6b7280" />
      </PropGroup>
    </div>
  );
}

function GalleryContent({ node, store }: { node: CBNode; store: EditorStore }) {
  type GalleryItem = { id?: string; url: string; alt: string; caption: string; linkUrl?: string; linkTarget?: string; category?: string };
  const content = (node.content ?? {}) as { items?: GalleryItem[]; layout?: string; enableLightbox?: boolean; enableFilter?: boolean };
  const items   = content.items ?? [];
  const update  = (patch: Record<string, unknown>) => store.updateNodeContent(node.id, patch);
  const save    = (next: GalleryItem[]) => update({ items: next });
  const tog     = (k: string) => update({ [k]: !content[k as keyof typeof content] });

  const openMultiPicker = () => {
    const wpGlobal = (window as any).wp;
    if (!wpGlobal?.media) {
      const url = window.prompt('Image URL:');
      if (url?.trim()) save([...items, { id: `g${Date.now()}`, url: url.trim(), alt: '', caption: '', linkUrl: '', linkTarget: '_blank', category: '' }]);
      return;
    }
    const frame = wpGlobal.media({
      title:    'Add Images to Gallery',
      button:   { text: 'Add to Gallery' },
      multiple: true,
      library:  { type: 'image' },
    });
    frame.on('select', () => {
      const selected = frame.state().get('selection').toArray();
      const newItems: GalleryItem[] = selected.map((att: any) => {
        const j = att.toJSON();
        return { id: `g${j.id || Date.now()}`, url: j.url, alt: j.alt || '', caption: j.caption || '', linkUrl: '', linkTarget: '_blank', category: '' };
      });
      save([...items, ...newItems]);
    });
    frame.open();
  };

  const replaceSingle = (index: number) => {
    const wpGlobal = (window as any).wp;
    if (!wpGlobal?.media) {
      const url = window.prompt('Image URL:', items[index]?.url || '');
      if (url !== null) { const next = [...items]; next[index] = { ...next[index], url: url.trim() }; save(next); }
      return;
    }
    const frame = wpGlobal.media({ title: 'Replace Image', button: { text: 'Use Image' }, multiple: false, library: { type: 'image' } });
    frame.on('select', () => {
      const j = frame.state().get('selection').first().toJSON();
      const next = [...items];
      next[index] = { ...next[index], url: j.url, alt: j.alt || next[index].alt };
      save(next);
    });
    frame.open();
  };

  return (
    <div className="jalpi-props-section">
      <PropGroup label="Layout" defaultOpen>
        <PropRow label="Layout Type" stack>
          <Select
            value={content.layout || 'grid'}
            options={[{ value: 'grid', label: 'Grid' }, { value: 'masonry', label: 'Masonry' }]}
            onChange={(v) => update({ layout: v })}
          />
        </PropRow>
        <PropRow label="Lightbox">
          <label style={{ display: 'flex', alignItems: 'center', gap: 6, cursor: 'pointer', fontSize: 12 }}>
            <input type="checkbox" checked={!!content.enableLightbox} onChange={() => tog('enableLightbox')} />
            Enable lightbox
          </label>
        </PropRow>
        <PropRow label="Filter Bar">
          <label style={{ display: 'flex', alignItems: 'center', gap: 6, cursor: 'pointer', fontSize: 12 }}>
            <input type="checkbox" checked={!!content.enableFilter} onChange={() => tog('enableFilter')} />
            Enable filter
          </label>
        </PropRow>
      </PropGroup>

      {/* Thumbnail gallery grid — Elementor-style */}
      <PropGroup label="Images" defaultOpen>
        {items.length > 0 && (
          <div className="jalpi-ic-gallery">
            {items.map((item, i) => (
              <div key={item.id || i} className="jalpi-ic-gallery__item">
                <div className="jalpi-ic-gallery__thumb" onClick={() => replaceSingle(i)} title="Click to replace">
                  {item.url
                    ? <img src={item.url} alt={item.alt || ''} />
                    : <span className="jalpi-ic-gallery__empty">?</span>}
                </div>
                <button
                  type="button"
                  className="jalpi-ic-gallery__remove"
                  onClick={() => save(items.filter((_, idx) => idx !== i))}
                  title="Remove"
                >×</button>
              </div>
            ))}
            <button type="button" className="jalpi-ic-gallery__add-thumb" onClick={openMultiPicker} title="Add images">
              <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
                <line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/>
              </svg>
            </button>
          </div>
        )}
        {items.length === 0 && (
          <button type="button" className="jalpi-repeater__add" onClick={openMultiPicker} style={{ width: '100%' }}>
            + Add Images
          </button>
        )}
      </PropGroup>

      {/* Per-image details — collapsed by default */}
      {items.length > 0 && (
        <PropGroup label="Image Details" defaultOpen={false}>
          <ItemRepeater
            items={items}
            onChange={save}
            newItem={{ id: `g${Date.now()}`, url: '', alt: '', caption: '', linkUrl: '', linkTarget: '_blank', category: '' }}
            addLabel="+ Add Image Manually"
            renderItem={(item, _, upd) => (
              <>
                <PropRow label="Image" stack>
                  <MediaPickerInput value={item.url} onChange={(v) => upd('url', v)} />
                </PropRow>
                <PropRow label="Alt Text" stack>
                  <TextInput value={item.alt || ''} onChange={(v) => upd('alt', v)} placeholder="Alt text" />
                </PropRow>
                <PropRow label="Caption" stack>
                  <TextInput value={item.caption || ''} onChange={(v) => upd('caption', v)} placeholder="Caption (optional)" />
                </PropRow>
                <PropRow label="Link URL" stack>
                  <TextInput value={item.linkUrl || ''} onChange={(v) => upd('linkUrl', v)} placeholder="https://" />
                </PropRow>
                <PropRow label="Open in" stack>
                  <Select value={item.linkTarget || '_blank'} options={[{ value: '_blank', label: 'New Tab' }, { value: '_self', label: 'Same Tab' }]} onChange={(v) => upd('linkTarget', v)} />
                </PropRow>
                {content.enableFilter && (
                  <PropRow label="Category" stack>
                    <TextInput value={item.category || ''} onChange={(v) => upd('category', v)} placeholder="e.g. nature, travel" />
                  </PropRow>
                )}
              </>
            )}
          />
        </PropGroup>
      )}
    </div>
  );
}

/* ── Gallery Style Tab ───────────────────────────────────────────────────── */
function GalleryStyleTab({ node, store }: { node: CBNode; store: EditorStore }) {
  const s   = (node.settings ?? {}) as Record<string, string>;
  const str = (k: string) => s[k] || '';
  const upS = (patch: Record<string, string>) => store.updateNodeSettings(node.id, patch);

  const hover       = str('gal_hover') || 'zoom';
  const bdType      = str('gal_img_bd_type') || 'none';
  const capShow     = str('gal_caption_show') || 'hover';

  const [shadowOpen, setShadowOpen] = React.useState(false);
  const shadowRef = React.useRef<HTMLButtonElement>(null);

  return (
    <div className="jalpi-props-section">

      {/* ── Grid Settings ── */}
      <PropGroup label="Grid" defaultOpen>
        <PropRow label="Desktop Cols">
          <Select value={str('gal_cols_desktop') || '3'} options={[1,2,3,4,5,6].map((n) => ({ value: String(n), label: String(n) }))} onChange={(v) => upS({ gal_cols_desktop: v })} />
        </PropRow>
        <PropRow label="Tablet Cols">
          <Select value={str('gal_cols_tablet') || '2'} options={[1,2,3,4].map((n) => ({ value: String(n), label: String(n) }))} onChange={(v) => upS({ gal_cols_tablet: v })} />
        </PropRow>
        <PropRow label="Mobile Cols">
          <Select value={str('gal_cols_mobile') || '1'} options={[1,2].map((n) => ({ value: String(n), label: String(n) }))} onChange={(v) => upS({ gal_cols_mobile: v })} />
        </PropRow>
        <PropRow label="Gap">
          <SliderDimInput value={str('gal_gap') || '12'} onChange={(v) => upS({ gal_gap: v })} max={60} step={2} units={['px']} placeholder="12" />
        </PropRow>
        <PropRow label="Aspect Ratio" stack>
          <Select
            value={str('gal_img_ratio') || '1/1'}
            options={[
              { value: '1/1',  label: 'Square (1:1)' },
              { value: '4/3',  label: 'Landscape (4:3)' },
              { value: '16/9', label: 'Widescreen (16:9)' },
              { value: '3/4',  label: 'Portrait (3:4)' },
              { value: 'auto', label: 'Auto (Masonry)' },
            ]}
            onChange={(v) => upS({ gal_img_ratio: v })}
          />
        </PropRow>
      </PropGroup>

      {/* ── Hover Effects ── */}
      <PropGroup label="Hover Effect" defaultOpen={false}>
        <PropRow label="Effect" stack>
          <Select
            value={hover}
            options={[
              { value: 'none',    label: 'None' },
              { value: 'zoom',    label: 'Zoom' },
              { value: 'scale',   label: 'Scale' },
              { value: 'fade',    label: 'Fade' },
              { value: 'overlay', label: 'Overlay' },
            ]}
            onChange={(v) => upS({ gal_hover: v })}
          />
        </PropRow>
        {(hover === 'overlay' || hover === 'fade') && (
          <ColorPropRow label="Overlay Color" value={str('gal_overlay_color')} onChange={(v) => upS({ gal_overlay_color: v })} placeholder="#000000" />
        )}
        {(hover === 'overlay' || hover === 'fade') && (
          <PropRow label="Opacity">
            <SliderDimInput value={str('gal_overlay_opacity') || '0.5'} onChange={(v) => upS({ gal_overlay_opacity: v })} min={0} max={1} step={0.05} units={['']} placeholder="0.5" />
          </PropRow>
        )}
      </PropGroup>

      {/* ── Caption ── */}
      <PropGroup label="Caption" defaultOpen={false}>
        <PropRow label="Display" stack>
          <Select
            value={capShow}
            options={[
              { value: 'hover',  label: 'On Hover' },
              { value: 'always', label: 'Always' },
              { value: 'never',  label: 'Never' },
            ]}
            onChange={(v) => upS({ gal_caption_show: v })}
          />
        </PropRow>
        {capShow !== 'never' && (
          <>
            <ColorPropRow label="Text Color"  value={str('gal_caption_color')} onChange={(v) => upS({ gal_caption_color: v })} placeholder="#ffffff" />
            <ColorPropRow label="Background"  value={str('gal_caption_bg')}    onChange={(v) => upS({ gal_caption_bg: v })}    placeholder="rgba(0,0,0,0.6)" />
            <PropRow label="Font Size">
              <SliderDimInput value={str('gal_caption_size') || '13'} onChange={(v) => upS({ gal_caption_size: v })} max={24} units={['px']} placeholder="13" />
            </PropRow>
            <PropRow label="Alignment" stack>
              <Select value={str('gal_caption_align') || 'center'} options={[{ value: 'left', label: 'Left' }, { value: 'center', label: 'Center' }, { value: 'right', label: 'Right' }]} onChange={(v) => upS({ gal_caption_align: v })} />
            </PropRow>
          </>
        )}
      </PropGroup>

      {/* ── Image Border Radius ── */}
      <PropGroup label="Border Radius" defaultOpen={false}>
        <PropRow label="Radius">
          <BoxSpacingControl
            values={{ top: str('gal_img_br_top'), right: str('gal_img_br_right'), bottom: str('gal_img_br_bottom'), left: str('gal_img_br_left') }}
            onChange={(side, v) => upS({ [`gal_img_br_${side}`]: v })}
            onChangeAll={(v) => upS({ gal_img_br_top: v, gal_img_br_right: v, gal_img_br_bottom: v, gal_img_br_left: v })}
          />
        </PropRow>
      </PropGroup>

      {/* ── Image Border ── */}
      <PropGroup label="Image Border" defaultOpen={false}>
        <PropRow label="Type" stack>
          <Select
            value={bdType}
            options={[{ value: 'none', label: 'None' }, { value: 'solid', label: 'Solid' }, { value: 'dashed', label: 'Dashed' }, { value: 'dotted', label: 'Dotted' }]}
            onChange={(v) => upS({ gal_img_bd_type: v })}
          />
        </PropRow>
        {bdType !== 'none' && (
          <>
            <ColorPropRow label="Color" value={str('gal_img_bd_color')} onChange={(v) => upS({ gal_img_bd_color: v })} placeholder="#e5e7eb" />
            <PropRow label="Width">
              <BoxSpacingControl
                values={{ top: str('gal_img_bdw_top'), right: str('gal_img_bdw_right'), bottom: str('gal_img_bdw_bottom'), left: str('gal_img_bdw_left') }}
                onChange={(side, v) => upS({ [`gal_img_bdw_${side}`]: v })}
                onChangeAll={(v) => upS({ gal_img_bdw_top: v, gal_img_bdw_right: v, gal_img_bdw_bottom: v, gal_img_bdw_left: v })}
              />
            </PropRow>
          </>
        )}
      </PropGroup>

      {/* ── Box Shadow ── */}
      <PropGroup label="Box Shadow" defaultOpen={false}>
        <TypographyTriggerRow label="Shadow" isOpen={shadowOpen} triggerRef={shadowRef}
          onToggle={() => setShadowOpen((o) => !o)} />
        {shadowOpen && ReactDOM.createPortal(
          <ImgBoxShadowPopup value={str('gal_img_shadow')} onChange={(v) => upS({ gal_img_shadow: v })}
            onClose={() => setShadowOpen(false)} anchorRef={shadowRef} />,
          document.body
        )}
      </PropGroup>

      {/* ── Filter Bar Colors ── */}
      <PropGroup label="Filter Bar" defaultOpen={false}>
        <ColorPropRow label="Button Color"        value={str('gal_filter_color')}        onChange={(v) => upS({ gal_filter_color: v })}        placeholder="#374151" />
        <ColorPropRow label="Button BG"           value={str('gal_filter_bg')}           onChange={(v) => upS({ gal_filter_bg: v })}           placeholder="transparent" />
        <ColorPropRow label="Active Text"         value={str('gal_filter_active_color')} onChange={(v) => upS({ gal_filter_active_color: v })} placeholder="#ffffff" />
        <ColorPropRow label="Active BG"           value={str('gal_filter_active_bg')}    onChange={(v) => upS({ gal_filter_active_bg: v })}    placeholder="#6366f1" />
      </PropGroup>
    </div>
  );
}

/* — Image Carousel — */
function ImageCarouselContent({ node, store }: { node: CBNode; store: EditorStore }) {
  type ICItem = { url: string; alt: string; caption: string; description: string; linkUrl: string; linkNewTab: boolean };
  const content = (node.content ?? {}) as { items?: ICItem[] };
  const items   = content.items ?? [];
  const save    = (next: ICItem[]) => store.updateNodeContent(node.id, { items: next });

  const openMultiPicker = () => {
    const wpGlobal = (window as any).wp;
    if (!wpGlobal?.media) {
      const url = window.prompt('Image URL:');
      if (url?.trim()) save([...items, { url: url.trim(), alt: '', caption: '', description: '', linkUrl: '', linkNewTab: false }]);
      return;
    }
    const frame = wpGlobal.media({
      title:    'Add Images to Carousel',
      button:   { text: 'Add to Carousel' },
      multiple: true,
      library:  { type: 'image' },
    });
    frame.on('select', () => {
      const selected = frame.state().get('selection').toArray();
      const newItems: ICItem[] = selected.map((att: any) => {
        const j = att.toJSON();
        return { url: j.url, alt: j.alt || '', caption: j.caption || '', description: j.description || '', linkUrl: '', linkNewTab: false };
      });
      save([...items, ...newItems]);
    });
    frame.open();
  };

  const replaceSingle = (index: number) => {
    const wpGlobal = (window as any).wp;
    if (!wpGlobal?.media) {
      const url = window.prompt('Image URL:', items[index]?.url || '');
      if (url !== null) { const next = [...items]; next[index] = { ...next[index], url: url.trim() }; save(next); }
      return;
    }
    const frame = wpGlobal.media({ title: 'Replace Image', button: { text: 'Use Image' }, multiple: false, library: { type: 'image' } });
    frame.on('select', () => {
      const j = frame.state().get('selection').first().toJSON();
      const next = [...items];
      next[index] = { ...next[index], url: j.url, alt: j.alt || next[index].alt };
      save(next);
    });
    frame.open();
  };

  return (
    <div className="jalpi-props-section">
      <PropGroup label="Images">
        {/* Thumbnail gallery grid — Elementor-style */}
        {items.length > 0 && (
          <div className="jalpi-ic-gallery">
            {items.map((item, i) => (
              <div key={i} className="jalpi-ic-gallery__item">
                <div className="jalpi-ic-gallery__thumb" onClick={() => replaceSingle(i)} title="Click to replace">
                  {item.url
                    ? <img src={item.url} alt={item.alt || ''} />
                    : <span className="jalpi-ic-gallery__empty">?</span>}
                </div>
                <button
                  type="button"
                  className="jalpi-ic-gallery__remove"
                  onClick={() => save(items.filter((_, idx) => idx !== i))}
                  title="Remove"
                >×</button>
              </div>
            ))}
            {/* Inline add-more button */}
            <button type="button" className="jalpi-ic-gallery__add-thumb" onClick={openMultiPicker} title="Add images">
              <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
                <line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/>
              </svg>
            </button>
          </div>
        )}
        {/* Primary add button when empty */}
        {items.length === 0 && (
          <button type="button" className="jalpi-repeater__add" onClick={openMultiPicker} style={{ width: '100%' }}>
            + Add Images
          </button>
        )}
      </PropGroup>

      {/* Per-image details — collapsed by default, like Elementor's individual item settings */}
      {items.length > 0 && (
        <PropGroup label="Image Details" defaultOpen={false}>
          <ItemRepeater
            items={items}
            onChange={save}
            newItem={{ url: '', alt: '', caption: '', description: '', linkUrl: '', linkNewTab: false }}
            addLabel="+ Add Image Manually"
            renderItem={(item, _, upd) => (
              <>
                <PropRow label="Image" stack>
                  <MediaPickerInput value={item.url} onChange={(v) => upd('url', v)} />
                </PropRow>
                <PropRow label="Alt Text" stack>
                  <TextInput value={item.alt} onChange={(v) => upd('alt', v)} placeholder="Describe the image" />
                </PropRow>
                <PropRow label="Caption" stack>
                  <TextInput value={item.caption} onChange={(v) => upd('caption', v)} placeholder="Short caption" />
                </PropRow>
                <PropRow label="Description" stack>
                  <TextInput value={item.description} onChange={(v) => upd('description', v)} placeholder="Longer description" />
                </PropRow>
                <PropRow label="Link URL" stack>
                  <TextInput value={item.linkUrl} onChange={(v) => upd('linkUrl', v)} placeholder="https://" />
                </PropRow>
                <PropRow label="Open in new tab">
                  <label style={{ display: 'flex', alignItems: 'center', gap: 6, cursor: 'pointer', fontSize: 12 }}>
                    <input type="checkbox" checked={!!item.linkNewTab} onChange={(e) => upd('linkNewTab', e.target.checked)} />
                    New tab
                  </label>
                </PropRow>
              </>
            )}
          />
        </PropGroup>
      )}
    </div>
  );
}

/* — Image Carousel Style Tab — */
function ImageCarouselStyleTab({ node, store }: { node: CBNode; store: EditorStore }) {
  const s   = (node.settings ?? {}) as Record<string, unknown>;
  const str = (k: string) => { const v = s[k]; return v != null ? String(v) : ''; };
  const upS = (patch: Record<string, unknown>) => store.updateNode(node.id, { settings: { ...s, ...patch } });

  const layout   = str('ic_layout')     || 'classic';
  const hover    = str('ic_hover')      || 'none';
  const capPos   = str('ic_caption_pos') || 'below';
  const autoplay = parseInt(str('ic_autoplay') || '0', 10);

  return (
    <div className="jalpi-props-section">
      {/* ── Layout ──────────────────────────────────────── */}
      <PropGroup label="Layout">
        <PropRow label="Style" stack>
          <select className="jalpi-select" value={layout} onChange={(e) => upS({ ic_layout: e.target.value })}>
            <option value="classic">Classic</option>
            <option value="card">Card</option>
            <option value="coverflow">Coverflow</option>
            <option value="modern">Modern</option>
            <option value="masonry">Masonry</option>
          </select>
        </PropRow>
        <PropRow label="Columns (Desktop)">
          <select className="jalpi-select" value={str('ic_cols') || (layout === 'card' ? '3' : '1')} onChange={(e) => upS({ ic_cols: e.target.value })}>
            {[1,2,3,4,5,6].map(n => <option key={n} value={n}>{n}</option>)}
          </select>
        </PropRow>
        <PropRow label="Columns (Tablet)">
          <select className="jalpi-select" value={str('ic_cols_tablet') || '2'} onChange={(e) => upS({ ic_cols_tablet: e.target.value })}>
            {[1,2,3,4].map(n => <option key={n} value={n}>{n}</option>)}
          </select>
        </PropRow>
        <PropRow label="Columns (Mobile)">
          <select className="jalpi-select" value={str('ic_cols_mobile') || '1'} onChange={(e) => upS({ ic_cols_mobile: e.target.value })}>
            {[1,2].map(n => <option key={n} value={n}>{n}</option>)}
          </select>
        </PropRow>
        <SliderPropRow label="Gap" value={str('ic_gap') || '16px'} onChange={(v) => upS({ ic_gap: v })} min={0} max={80} step={2} units={['px']} />
      </PropGroup>

      {/* ── Image ───────────────────────────────────────── */}
      <PropGroup label="Image">
        <SliderPropRow label="Height" value={str('ic_img_height') || '260px'} onChange={(v) => upS({ ic_img_height: v })} min={100} max={700} step={10} units={['px', 'vh']} />
        <PropRow label="Object Fit" stack>
          <select className="jalpi-select" value={str('ic_img_fit') || 'cover'} onChange={(e) => upS({ ic_img_fit: e.target.value })}>
            <option value="cover">Cover</option>
            <option value="contain">Contain</option>
            <option value="fill">Fill</option>
          </select>
        </PropRow>
        <SliderPropRow label="Border Radius" value={str('ic_img_br') || '0px'} onChange={(v) => upS({ ic_img_br: v })} min={0} max={40} step={1} units={['px']} />
      </PropGroup>

      {/* ── Behaviour ───────────────────────────────────── */}
      {layout !== 'masonry' && (
        <PropGroup label="Behaviour">
          <PropRow label="Transition">
            <select className="jalpi-select" value={str('ic_effect') || 'slide'} onChange={(e) => upS({ ic_effect: e.target.value })}>
              <option value="slide">Slide</option>
              <option value="fade">Fade</option>
            </select>
          </PropRow>
          <SliderPropRow label="Speed (ms)" value={str('ic_speed') || '400px'} onChange={(v) => upS({ ic_speed: v })} min={100} max={2000} step={50} units={['px']} placeholder="400" />
          <PropRow label="Autoplay">
            <label style={{ display: 'flex', alignItems: 'center', gap: 6, cursor: 'pointer', fontSize: 12 }}>
              <input type="checkbox" checked={autoplay > 0} onChange={(e) => upS({ ic_autoplay: e.target.checked ? 4000 : 0 })} />
              Enable
            </label>
          </PropRow>
          {autoplay > 0 && (
            <PropRow label="Interval (ms)" stack>
              <input type="number" min={1000} max={15000} step={500} className="jalpi-input" value={autoplay}
                onChange={(e) => upS({ ic_autoplay: Math.max(1000, parseInt(e.target.value) || 4000) })} />
            </PropRow>
          )}
          <PropRow label="Loop">
            <label style={{ display: 'flex', alignItems: 'center', gap: 6, cursor: 'pointer', fontSize: 12 }}>
              <input type="checkbox" checked={str('ic_loop') !== '0'} onChange={(e) => upS({ ic_loop: e.target.checked ? '1' : '0' })} />
              Infinite loop
            </label>
          </PropRow>
        </PropGroup>
      )}

      {/* ── Navigation ──────────────────────────────────── */}
      {layout !== 'masonry' && (
        <PropGroup label="Navigation">
          <PropRow label="Arrows">
            <label style={{ display: 'flex', alignItems: 'center', gap: 6, cursor: 'pointer', fontSize: 12 }}>
              <input type="checkbox" checked={str('ic_arrows') !== '0'} onChange={(e) => upS({ ic_arrows: e.target.checked ? '1' : '0' })} />
              Show arrows
            </label>
          </PropRow>
          <PropRow label="Dots">
            <label style={{ display: 'flex', alignItems: 'center', gap: 6, cursor: 'pointer', fontSize: 12 }}>
              <input type="checkbox" checked={str('ic_dots') !== '0'} onChange={(e) => upS({ ic_dots: e.target.checked ? '1' : '0' })} />
              Show dots
            </label>
          </PropRow>
          <PropRow label="Lightbox">
            <label style={{ display: 'flex', alignItems: 'center', gap: 6, cursor: 'pointer', fontSize: 12 }}>
              <input type="checkbox" checked={str('ic_lightbox') === '1'} onChange={(e) => upS({ ic_lightbox: e.target.checked ? '1' : '0' })} />
              Open in lightbox
            </label>
          </PropRow>
        </PropGroup>
      )}

      {/* ── Hover Effect ────────────────────────────────── */}
      <PropGroup label="Hover Effect">
        <PropRow label="Effect" stack>
          <select className="jalpi-select" value={hover} onChange={(e) => upS({ ic_hover: e.target.value })}>
            <option value="none">None</option>
            <option value="zoom">Zoom</option>
            <option value="scale">Scale</option>
            <option value="fade">Fade</option>
            <option value="blur">Blur</option>
            <option value="overlay">Overlay</option>
          </select>
        </PropRow>
        {hover === 'overlay' && (
          <ColorPropRow label="Overlay Color" value={str('ic_hover_overlay_color') || 'rgba(0,0,0,.4)'} onChange={(v) => upS({ ic_hover_overlay_color: v })} />
        )}
      </PropGroup>

      {/* ── Caption ─────────────────────────────────────── */}
      <PropGroup label="Caption">
        <PropRow label="Position" stack>
          <select className="jalpi-select" value={capPos} onChange={(e) => upS({ ic_caption_pos: e.target.value })}>
            <option value="none">Hidden</option>
            <option value="below">Below Image</option>
            <option value="overlay">Overlay</option>
          </select>
        </PropRow>
        {capPos !== 'none' && (
          <>
            <ColorPropRow label="Caption Color" value={str('ic_caption_color')} onChange={(v) => upS({ ic_caption_color: v })} />
            <SliderPropRow label="Caption Size" value={str('ic_caption_size') || '14px'} onChange={(v) => upS({ ic_caption_size: v })} min={10} max={32} step={1} units={['px']} />
            <ColorPropRow label="Description Color" value={str('ic_desc_color')} onChange={(v) => upS({ ic_desc_color: v })} />
            <SliderPropRow label="Description Size" value={str('ic_desc_size') || '13px'} onChange={(v) => upS({ ic_desc_size: v })} min={10} max={24} step={1} units={['px']} />
            {capPos === 'overlay' && (
              <ColorPropRow label="Overlay Background" value={str('ic_caption_bg')} onChange={(v) => upS({ ic_caption_bg: v })} />
            )}
          </>
        )}
      </PropGroup>

      {/* ── Arrow Style ─────────────────────────────────── */}
      {layout !== 'masonry' && str('ic_arrows') !== '0' && (
        <PropGroup label="Arrow Style">
          <ColorPropRow label="Icon Color" value={str('ic_arrow_color') || '#fff'} onChange={(v) => upS({ ic_arrow_color: v })} />
          <ColorPropRow label="Background" value={str('ic_arrow_bg') || 'rgba(0,0,0,.45)'} onChange={(v) => upS({ ic_arrow_bg: v })} />
          <SliderPropRow label="Icon Size" value={str('ic_arrow_size') || '20px'} onChange={(v) => upS({ ic_arrow_size: v })} min={10} max={36} step={1} units={['px']} />
          <SliderPropRow label="Border Radius" value={str('ic_arrow_br') || '50px'} onChange={(v) => upS({ ic_arrow_br: v })} min={0} max={50} step={1} units={['px', '%']} />
        </PropGroup>
      )}

      {/* ── Dot Style ───────────────────────────────────── */}
      {layout !== 'masonry' && str('ic_dots') !== '0' && (
        <PropGroup label="Dot Style">
          <ColorPropRow label="Dot Color" value={str('ic_dot_color') || 'rgba(0,0,0,.22)'} onChange={(v) => upS({ ic_dot_color: v })} />
          <ColorPropRow label="Active Color" value={str('ic_dot_active_color') || '#6366f1'} onChange={(v) => upS({ ic_dot_active_color: v })} />
          <SliderPropRow label="Dot Size" value={str('ic_dot_size') || '8px'} onChange={(v) => upS({ ic_dot_size: v })} min={4} max={20} step={1} units={['px']} />
        </PropGroup>
      )}

      {/* ── Card Style ──────────────────────────────────── */}
      {layout === 'card' && (
        <PropGroup label="Card Style">
          <ColorPropRow label="Card Background" value={str('ic_card_bg') || '#fff'} onChange={(v) => upS({ ic_card_bg: v })} />
          <SliderPropRow label="Border Radius" value={str('ic_card_br') || '12px'} onChange={(v) => upS({ ic_card_br: v })} min={0} max={32} step={1} units={['px']} />
          <SliderPropRow label="Padding" value={str('ic_card_pad') || '0px'} onChange={(v) => upS({ ic_card_pad: v })} min={0} max={40} step={2} units={['px']} />
          <PropRow label="Box Shadow" stack>
            <input type="text" className="jalpi-input" placeholder="0 2px 14px rgba(0,0,0,.09)" value={str('ic_card_shadow') || ''} onChange={(e) => upS({ ic_card_shadow: e.target.value })} />
          </PropRow>
        </PropGroup>
      )}
    </div>
  );
}

/* — Countdown — */
function CountdownContent({ node, store }: { node: CBNode; store: EditorStore }) {
  const content = (node.content ?? {}) as { targetDate?: string; label?: string };
  const update  = (key: string, val: string) => store.updateNodeContent(node.id, { [key]: val });
  return (
    <div className="jalpi-props-section">
      <PropGroup label="Countdown">
        <PropRow label="Target Date" stack>
          <input type="datetime-local" className="jalpi-input"
            value={content.targetDate || ''}
            onChange={(e) => update('targetDate', e.target.value)} />
        </PropRow>
        <PropRow label="Label" stack>
          <TextInput value={content.label || ''} onChange={(v) => update('label', v)} placeholder="Until launch" />
        </PropRow>
      </PropGroup>
    </div>
  );
}

/* — Flip Box — */
function FlipBoxContent({ node, store }: { node: CBNode; store: EditorStore }) {
  const content = (node.content ?? {}) as {
    frontTitle?: string; frontDescription?: string; frontBg?: string; frontColor?: string;
    backTitle?: string;  backDescription?: string;  backBg?: string;  backColor?: string;
    btnText?: string; btnUrl?: string; height?: string;
  };
  const update = (key: string, val: string) => store.updateNodeContent(node.id, { [key]: val });
  return (
    <div className="jalpi-props-section">
      <PropGroup label="Size">
        <PropRow label="Height" stack>
          <SliderDimInput value={content.height || '220px'} onChange={(v) => update('height', v)} max={600} step={10} units={['px']} placeholder="220" />
        </PropRow>
      </PropGroup>
      <PropGroup label="Front Face">
        <PropRow label="Title" stack>
          <TextInput value={content.frontTitle || ''} onChange={(v) => update('frontTitle', v)} placeholder="Front Side" />
        </PropRow>
        <PropRow label="Description" stack>
          <textarea className="jalpi-textarea" rows={2} value={content.frontDescription || ''}
            onChange={(e) => update('frontDescription', e.target.value)}
            placeholder="Front description…" />
        </PropRow>
        <PropRow label="Background">
          <ColorInput value={content.frontBg || '#1a1a1a'} onChange={(v) => update('frontBg', v)} />
        </PropRow>
        <PropRow label="Text Color">
          <ColorInput value={content.frontColor || '#ffffff'} onChange={(v) => update('frontColor', v)} />
        </PropRow>
      </PropGroup>
      <PropGroup label="Back Face">
        <PropRow label="Title" stack>
          <TextInput value={content.backTitle || ''} onChange={(v) => update('backTitle', v)} placeholder="Back Side" />
        </PropRow>
        <PropRow label="Description" stack>
          <textarea className="jalpi-textarea" rows={2} value={content.backDescription || ''}
            onChange={(e) => update('backDescription', e.target.value)}
            placeholder="Back description…" />
        </PropRow>
        <PropRow label="Background">
          <ColorInput value={content.backBg || '#111827'} onChange={(v) => update('backBg', v)} />
        </PropRow>
        <PropRow label="Text Color">
          <ColorInput value={content.backColor || '#ffffff'} onChange={(v) => update('backColor', v)} />
        </PropRow>
        <PropRow label="Button Text" stack>
          <TextInput value={content.btnText || ''} onChange={(v) => update('btnText', v)} placeholder="Learn More" />
        </PropRow>
        <PropRow label="Button URL" stack>
          <TextInput value={content.btnUrl || ''} onChange={(v) => update('btnUrl', v)} placeholder="https://…" />
        </PropRow>
      </PropGroup>
    </div>
  );
}

/* — Star Rating — */
function StarRatingContent({ node, store }: { node: CBNode; store: EditorStore }) {
  const content = (node.content ?? {}) as { rating?: number; maxRating?: number; label?: string; starColor?: string; size?: string; align?: string };
  const update  = (patch: Record<string, unknown>) => store.updateNodeContent(node.id, patch);
  return (
    <div className="jalpi-props-section">
      <PropGroup label="Rating">
        <PropRow label="Rating" stack>
          <Select
            value={String(content.rating ?? 4)}
            options={Array.from({ length: content.maxRating ?? 5 }, (_, i) => i + 1).reverse()
              .map((n) => ({ value: String(n), label: `${n} star${n !== 1 ? 's' : ''}` }))}
            onChange={(v) => update({ rating: parseInt(v) })}
          />
        </PropRow>
        <PropRow label="Max Stars" stack>
          <Select
            value={String(content.maxRating ?? 5)}
            options={[3,4,5,6,7,10].map((n) => ({ value: String(n), label: String(n) }))}
            onChange={(v) => update({ maxRating: parseInt(v) })}
          />
        </PropRow>
        <PropRow label="Label" stack>
          <TextInput value={content.label || ''} onChange={(v) => update({ label: v })} placeholder="4 out of 5 stars" />
        </PropRow>
      </PropGroup>
      <PropGroup label="Style">
        <PropRow label="Star Color">
          <ColorInput value={content.starColor || '#f59e0b'} onChange={(v) => update({ starColor: v })} />
        </PropRow>
        <PropRow label="Size" stack>
          <SliderDimInput value={content.size || '24px'} onChange={(v) => update({ size: v })} max={64} step={2} units={['px']} placeholder="24" />
        </PropRow>
        <PropRow label="Align" stack>
          <Select
            value={content.align || 'left'}
            options={[
              { value: 'left',   label: 'Left'   },
              { value: 'center', label: 'Center' },
              { value: 'right',  label: 'Right'  },
            ]}
            onChange={(v) => update({ align: v })}
          />
        </PropRow>
      </PropGroup>
    </div>
  );
}

/* =========================================================
   SECTION — Style Tab
   ========================================================= */

type NHState = 'normal' | 'hover';

function NormalHoverTabs({ state, onChange }: { state: NHState; onChange: (s: NHState) => void }) {
  return (
    <div className="jalpi-nh-tabs">
      <button type="button" className={`jalpi-nh-tab${state === 'normal' ? ' is-active' : ''}`} onClick={() => onChange('normal')}>Normal</button>
      <button type="button" className={`jalpi-nh-tab${state === 'hover'  ? ' is-active' : ''}`} onClick={() => onChange('hover')}>Hover</button>
    </div>
  );
}

function MediaPickerInput({ value, onChange }: { value: string; onChange: (v: string) => void }) {
  const openPicker = () => {
    const wpGlobal = (window as any).wp;
    if (!wpGlobal?.media) {
      const url = window.prompt('Image URL:', value);
      if (url !== null) onChange(url);
      return;
    }
    const frame = wpGlobal.media({ title: 'Choose Image', button: { text: 'Use Image' }, multiple: false, library: { type: 'image' } });
    frame.on('select', () => { const a = frame.state().get('selection').first().toJSON(); onChange(a.url); });
    frame.open();
  };
  return (
    <div className="jalpi-media-picker">
      {value ? (
        <>
          <div className="jalpi-media-picker__preview" onClick={openPicker}>
            <img src={value} alt="" className="jalpi-media-picker__img" />
            <div className="jalpi-media-picker__hover-label">
              <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
                <path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/>
                <path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/>
              </svg>
              Change
            </div>
          </div>
          <button type="button" className="jalpi-media-picker__remove" onClick={() => onChange('')}>
            <svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
              <line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/>
            </svg>
            Remove image
          </button>
        </>
      ) : (
        <button type="button" className="jalpi-media-picker__add" onClick={openPicker}>
          <svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
            <rect x="3" y="3" width="18" height="18" rx="2"/>
            <circle cx="8.5" cy="8.5" r="1.5" fill="currentColor" stroke="none"/>
            <polyline points="21 15 16 10 5 21"/>
          </svg>
          <span>Choose Image</span>
        </button>
      )}
    </div>
  );
}

function ImageUploadInput({ value, onChange }: { value: string; onChange: (v: string) => void }) {
  return <MediaPickerInput value={value} onChange={onChange} />;
}

const BG_TYPE_OPTIONS = [
  { value: 'classic',   label: 'Classic' },
  { value: 'gradient',  label: 'Gradient' },
  { value: 'video',     label: 'Video' },
  { value: 'slideshow', label: 'Slideshow' },
];

function buildGradientCss(d: Record<string, unknown>): string {
  const c1    = (d.gradientColor1   as string) || '#667eea';
  const c2    = (d.gradientColor2   as string) || '#764ba2';
  const l1    = d.gradientLocation1 != null ? Number(d.gradientLocation1) : 0;
  const l2    = d.gradientLocation2 != null ? Number(d.gradientLocation2) : 100;
  const gtype = (d.gradientType     as string) || 'linear';
  const angle = d.gradientAngle     != null ? Number(d.gradientAngle)    : 180;
  return gtype === 'radial'
    ? `radial-gradient(circle, ${c1} ${l1}%, ${c2} ${l2}%)`
    : `linear-gradient(${angle}deg, ${c1} ${l1}%, ${c2} ${l2}%)`;
}

function SectionBgEditor({
  data, onChange, showOpacity = false,
}: {
  data: Record<string, unknown>;
  onChange: (d: Record<string, unknown>) => void;
  showOpacity?: boolean;
}) {
  const type   = (data.type as string) || 'classic';
  const upd    = (k: string, v: unknown) => onChange({ ...data, [k]: v });
  const opPct  = Math.round(parseFloat((data.opacity as string) ?? '0.5') * 100);

  const updGradient = (fields: Record<string, unknown>) => {
    const next = { ...data, ...fields };
    onChange({ ...next, gradient: buildGradientCss(next) });
  };

  return (
    <>
      <PropRow label="Type" stack>
        <Select value={type} options={BG_TYPE_OPTIONS} onChange={(v) => upd('type', v)} />
      </PropRow>

      {type === 'classic' && (
        <>
          <PropRow label="Color">
            <ColorInput value={(data.color as string) || ''} onChange={(v) => upd('color', v)} placeholder="transparent" />
          </PropRow>
          <PropRow label="Image" stack>
            <ImageUploadInput value={(data.imageUrl as string) || ''} onChange={(v) => upd('imageUrl', v)} />
          </PropRow>
          {!!(data.imageUrl as string) && (
            <>
              <PropRow label="Position" stack>
                <Select
                  value={(data.imagePosition as string) || 'center center'}
                  options={[
                    { value: 'center center', label: 'Center Center' },
                    { value: 'center top',    label: 'Center Top' },
                    { value: 'center bottom', label: 'Center Bottom' },
                    { value: 'left center',   label: 'Left Center' },
                    { value: 'right center',  label: 'Right Center' },
                  ]}
                  onChange={(v) => upd('imagePosition', v)}
                />
              </PropRow>
              <PropRow label="Size" stack>
                <Select
                  value={(data.imageSize as string) || 'cover'}
                  options={[
                    { value: 'cover',   label: 'Cover' },
                    { value: 'contain', label: 'Contain' },
                    { value: 'auto',    label: 'Auto' },
                  ]}
                  onChange={(v) => upd('imageSize', v)}
                />
              </PropRow>
            </>
          )}
        </>
      )}

      {type === 'gradient' && (
        <>
          <PropRow label="Color">
            <ColorInput
              value={(data.gradientColor1 as string) || '#667eea'}
              onChange={(v) => updGradient({ gradientColor1: v })}
            />
          </PropRow>
          <PropRow label="Location" stack>
            <SliderDimInput
              value={`${data.gradientLocation1 ?? 0}%`}
              onChange={(v) => updGradient({ gradientLocation1: parseFloat(v) || 0 })}
              min={0} max={100} step={1}
              units={['%']}
              placeholder="0"
            />
          </PropRow>
          <PropRow label="Second Color">
            <ColorInput
              value={(data.gradientColor2 as string) || '#764ba2'}
              onChange={(v) => updGradient({ gradientColor2: v })}
            />
          </PropRow>
          <PropRow label="Location" stack>
            <SliderDimInput
              value={`${data.gradientLocation2 ?? 100}%`}
              onChange={(v) => updGradient({ gradientLocation2: parseFloat(v) || 100 })}
              min={0} max={100} step={1}
              units={['%']}
              placeholder="100"
            />
          </PropRow>
          <PropRow label="Type" stack>
            <Select
              value={(data.gradientType as string) || 'linear'}
              options={[
                { value: 'linear', label: 'Linear' },
                { value: 'radial', label: 'Radial' },
              ]}
              onChange={(v) => updGradient({ gradientType: v })}
            />
          </PropRow>
          {((data.gradientType as string) || 'linear') === 'linear' && (
            <PropRow label="Angle" stack>
              <SliderDimInput
                value={`${data.gradientAngle ?? 180}deg`}
                onChange={(v) => updGradient({ gradientAngle: parseFloat(v) || 0 })}
                min={0} max={360} step={1}
                units={['deg']}
                placeholder="180"
              />
            </PropRow>
          )}
        </>
      )}

      {type === 'video' && (
        <PropRow label="Video URL" stack>
          <TextInput
            value={(data.videoUrl as string) || ''}
            onChange={(v) => upd('videoUrl', v)}
            placeholder="https://example.com/background.mp4"
          />
        </PropRow>
      )}

      {type === 'slideshow' && (
        <>
          <PropRow label="Image URLs" stack>
            <textarea
              className="jalpi-code-area"
              rows={4}
              value={(data.slideshowUrls as string) || ''}
              onChange={(e) => upd('slideshowUrls', e.target.value)}
              placeholder={"https://example.com/slide1.jpg\nhttps://example.com/slide2.jpg\nhttps://example.com/slide3.jpg"}
              spellCheck={false}
            />
          </PropRow>
          <PropRow label="Speed" stack>
            <SliderDimInput
              value={(data.slideshowSpeed as string) || '5s'}
              onChange={(v) => upd('slideshowSpeed', v)}
              min={1} max={30} step={1}
              units={['s']}
              placeholder="5"
            />
          </PropRow>
        </>
      )}

      {showOpacity && (
        <PropRow label="Opacity">
          <div className="jalpi-opacity-row">
            <input
              type="range" min="0" max="100" step="1" value={opPct}
              style={{ '--pct': `${opPct}%` } as React.CSSProperties}
              onChange={(e) => upd('opacity', String(Number(e.target.value) / 100))}
            />
            <span className="jalpi-opacity-val">{opPct}%</span>
          </div>
        </PropRow>
      )}
    </>
  );
}

function SectionStyleTab({ node, store }: { node: CBNode; store: EditorStore }) {
  const [bgNH,     setBgNH]     = useState<NHState>('normal');
  const [ovNH,     setOvNH]     = useState<NHState>('normal');
  const [borderNH, setBorderNH] = useState<NHState>('normal');

  const settings   = (node.settings ?? {}) as Record<string, unknown>;
  const upSettings = (updates: Record<string, unknown>) =>
    store.updateNode(node.id, { settings: { ...settings, ...updates } });

  const bgKey  = bgNH  === 'normal' ? 'bgNormal'     : 'bgHover';
  const ovKey  = ovNH  === 'normal' ? 'overlayNormal' : 'overlayHover';
  const bdKey  = borderNH === 'normal' ? 'borderNormal' : 'borderHover';

  const bgData = (settings[bgKey]  as Record<string, unknown>) ?? {};
  const ovData = (settings[ovKey]  as Record<string, unknown>) ?? {};
  const bdData = (settings[bdKey]  as Record<string, unknown>) ?? {};

  const upBd = (k: string, v: unknown) =>
    upSettings({ [bdKey]: { ...bdData, [k]: v } });

  return (
    <div className="jalpi-props-section">

      {/* ── Background ── */}
      <PropGroup label="Background" icon={
        <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
          <rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="8.5" cy="8.5" r="1.5" fill="currentColor" stroke="none"/><polyline points="21 15 16 10 5 21"/>
        </svg>
      }>
        <NormalHoverTabs state={bgNH} onChange={setBgNH} />
        <SectionBgEditor data={bgData} onChange={(d) => upSettings({ [bgKey]: d })} />
      </PropGroup>

      {/* ── Background Overlay ── */}
      <PropGroup label="Background Overlay" defaultOpen={false} icon={
        <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
          <rect x="3" y="3" width="18" height="18" rx="2" opacity=".4"/><rect x="6" y="6" width="12" height="12" rx="1"/>
        </svg>
      }>
        <NormalHoverTabs state={ovNH} onChange={setOvNH} />
        <SectionBgEditor data={ovData} onChange={(d) => upSettings({ [ovKey]: d })} showOpacity />
      </PropGroup>

      {/* ── Border ── */}
      <PropGroup label="Border" defaultOpen={false} icon={
        <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
          <rect x="3" y="3" width="18" height="18" rx="4"/>
        </svg>
      }>
        <NormalHoverTabs state={borderNH} onChange={setBorderNH} />
        <PropRow label="Border Type" stack>
          <Select
            value={(bdData.borderType as string) || 'none'}
            options={[
              { value: 'none',   label: 'None' },
              { value: 'solid',  label: 'Solid' },
              { value: 'dashed', label: 'Dashed' },
              { value: 'dotted', label: 'Dotted' },
              { value: 'double', label: 'Double' },
            ]}
            onChange={(v) => upBd('borderType', v)}
          />
        </PropRow>
        {(bdData.borderType as string) && (bdData.borderType as string) !== 'none' && (
          <>
            <PropRow label="Border Width">
              <BoxSpacingControl
                values={{
                  top:    (bdData.borderTopWidth    as string) || '',
                  right:  (bdData.borderRightWidth  as string) || '',
                  bottom: (bdData.borderBottomWidth as string) || '',
                  left:   (bdData.borderLeftWidth   as string) || '',
                }}
                onChange={(side, val) => {
                  const cap = side.charAt(0).toUpperCase() + side.slice(1);
                  upBd(`border${cap}Width`, val);
                }}
                onChangeAll={(val) =>
                  upSettings({ [bdKey]: { ...bdData, borderTopWidth: val, borderRightWidth: val, borderBottomWidth: val, borderLeftWidth: val } })
                }
              />
            </PropRow>
            <PropRow label="Border Color">
              <ColorInput value={(bdData.borderColor as string) || '#000000'} onChange={(v) => upBd('borderColor', v)} />
            </PropRow>
          </>
        )}
        <PropRow label="Border Radius">
          <BoxSpacingControl
            values={{
              top:    (bdData.borderTopLeftRadius     as string) || '',
              right:  (bdData.borderTopRightRadius    as string) || '',
              bottom: (bdData.borderBottomRightRadius as string) || '',
              left:   (bdData.borderBottomLeftRadius  as string) || '',
            }}
            onChange={(side, val) => {
              const cornerMap: Record<string, string> = {
                top:    'borderTopLeftRadius',
                right:  'borderTopRightRadius',
                bottom: 'borderBottomRightRadius',
                left:   'borderBottomLeftRadius',
              };
              upBd(cornerMap[side], val);
            }}
            onChangeAll={(val) =>
              upSettings({ [bdKey]: { ...bdData, borderTopLeftRadius: val, borderTopRightRadius: val, borderBottomRightRadius: val, borderBottomLeftRadius: val } })
            }
          />
        </PropRow>
        <PropRow label="Box Shadow">
          <TextInput value={(bdData.boxShadow as string) || ''} onChange={(v) => upBd('boxShadow', v)} placeholder="0 4px 16px rgba(0,0,0,0.1)" />
        </PropRow>
      </PropGroup>

    </div>
  );
}

/* =========================================================
   Shared: 3-device Visibility toggle
   ========================================================= */

function VisibilityDeviceRow({ node, store }: { node: CBNode; store: EditorStore }) {
  const DEVICES = ['desktop', 'tablet', 'mobile'] as const;
  const DEVICE_ICONS: Record<string, React.ReactElement> = {
    desktop: <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><rect x="2" y="3" width="20" height="14" rx="2"/><path d="M8 21h8M12 17v4"/></svg>,
    tablet:  <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><rect x="4" y="2" width="16" height="20" rx="2"/><line x1="12" y1="18" x2="12.01" y2="18"/></svg>,
    mobile:  <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><rect x="5" y="2" width="14" height="20" rx="2"/><line x1="12" y1="18" x2="12.01" y2="18"/></svg>,
  };
  return (
    <PropGroup label="Visibility" icon={
      <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
        <path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/>
      </svg>
    }>
      <div className="jalpi-vis-devices">
        {DEVICES.map((device) => {
          const hidden = !!node.hideOn?.[device];
          return (
            <button
              key={device}
              className={`jalpi-vis-device${hidden ? ' is-hidden' : ''}`}
              onClick={() => store.updateNodeHideOn(node.id, device, !hidden)}
              title={`${hidden ? 'Show' : 'Hide'} on ${device}`}
            >
              {DEVICE_ICONS[device]}
              <span className="jalpi-vis-device__name">{device.charAt(0).toUpperCase() + device.slice(1)}</span>
              <span className="jalpi-vis-device__state">{hidden ? 'Hidden' : 'Visible'}</span>
            </button>
          );
        })}
      </div>
    </PropGroup>
  );
}

/* =========================================================
   SECTION — Settings Tab (Margin / Padding / Attributes)
   ========================================================= */

function SectionSettingsTab({ node, store }: { node: CBNode; store: EditorStore }) {
  const { state } = store;
  const bp         = state.breakpoint;
  const styles     = node.styles ?? {};
  const settings   = (node.settings ?? {}) as Record<string, unknown>;
  const upStyle    = (updates: Record<string, string>) => store.updateNodeStyles(node.id, updates);
  const upSettings = (key: string, val: unknown) =>
    store.updateNode(node.id, { settings: { ...settings, [key]: val } });

  const respKey    = bp === 'tablet' ? 'tabletStyles' : 'mobileStyles';
  const respStyles = (node[respKey as keyof CBNode] as Record<string, string>) ?? {};
  const updateResp = (updates: Record<string, string>) =>
    store.updateNode(node.id, { [respKey]: { ...respStyles, ...updates } });

  return (
    <div className="jalpi-props-section">

      {/* ── Desktop spacing ── */}
      {bp === 'desktop' && (
        <>
          <PropGroup label="Margin" icon={<MarginIcon />}>
            <BoxSpacingControl
              values={{ top: styles.marginTop || '', right: styles.marginRight || '', bottom: styles.marginBottom || '', left: styles.marginLeft || '' }}
              onChange={(side, val) => upStyle({ [`margin${side.charAt(0).toUpperCase() + side.slice(1)}`]: val })}
            />
          </PropGroup>

          <PropGroup label="Padding" icon={<PaddingIcon />}>
            <BoxSpacingControl
              values={{ top: styles.paddingTop || '', right: styles.paddingRight || '', bottom: styles.paddingBottom || '', left: styles.paddingLeft || '' }}
              onChange={(side, val) => upStyle({ [`padding${side.charAt(0).toUpperCase() + side.slice(1)}`]: val })}
            />
          </PropGroup>

          <div className="jalpi-advanced-hint">
            <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
              <rect x="2" y="3" width="20" height="14" rx="2"/><path d="M8 21h8M12 17v4"/>
            </svg>
            Switch to Tablet or Mobile above to add responsive overrides.
          </div>
        </>
      )}

      {/* ── Responsive overrides (tablet / mobile) ── */}
      {bp !== 'desktop' && (
        <>
          <div className="jalpi-props-notice">
            Overrides for <strong>{bp}</strong> only — blank = inherit desktop.
          </div>

          <PropGroup label="Margin" icon={<MarginIcon />}>
            <BoxSpacingControl
              values={{ top: respStyles.marginTop || '', right: respStyles.marginRight || '', bottom: respStyles.marginBottom || '', left: respStyles.marginLeft || '' }}
              onChange={(side, val) => updateResp({ [`margin${side.charAt(0).toUpperCase() + side.slice(1)}`]: val })}
            />
          </PropGroup>

          <PropGroup label="Padding" icon={<PaddingIcon />}>
            <BoxSpacingControl
              values={{ top: respStyles.paddingTop || '', right: respStyles.paddingRight || '', bottom: respStyles.paddingBottom || '', left: respStyles.paddingLeft || '' }}
              onChange={(side, val) => updateResp({ [`padding${side.charAt(0).toUpperCase() + side.slice(1)}`]: val })}
            />
          </PropGroup>

          <PropGroup label="Responsive Size" icon={<ResponsiveIcon />} defaultOpen={false}>
            <PropRow label="Min Height">
              <DimensionInput value={respStyles.minHeight || ''} onChange={(v) => updateResp({ minHeight: v })} placeholder="inherit" units={['px', '%', 'vh']} />
            </PropRow>
          </PropGroup>

          {node.type === 'container' && (
            <PropGroup label="Column Layout" icon={
              <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
                <rect x="3" y="3" width="7" height="18" rx="1"/><rect x="14" y="3" width="7" height="18" rx="1" opacity=".6"/>
              </svg>
            }>
              <PropRow label="Stack vertically">
                <Toggle
                  checked={['column', 'column-reverse'].includes(respStyles.flexDirection || '')}
                  onChange={(v) => updateResp({ flexDirection: v ? 'column' : '' })}
                />
              </PropRow>
              <PropRow label="Reverse order">
                <Toggle
                  checked={['column-reverse', 'row-reverse'].includes(respStyles.flexDirection || '')}
                  onChange={(v) => {
                    const stacked = ['column', 'column-reverse'].includes(respStyles.flexDirection || '');
                    updateResp({ flexDirection: v ? (stacked ? 'column-reverse' : 'row-reverse') : (stacked ? 'column' : '') });
                  }}
                />
              </PropRow>
            </PropGroup>
          )}

          <PropGroup label="Order & Display" defaultOpen={false} icon={
            <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
              <line x1="3" y1="6" x2="21" y2="6"/><line x1="3" y1="12" x2="21" y2="12"/><line x1="3" y1="18" x2="21" y2="18"/>
            </svg>
          }>
            <PropRow label="Order">
              <TextInput
                value={respStyles.order || ''}
                onChange={(v) => updateResp({ order: v })}
                placeholder="0 (auto)"
              />
            </PropRow>
            <PropRow label="Display">
              <Select
                value={respStyles.display || ''}
                options={[
                  { value: '',      label: '— Inherit —' },
                  { value: 'block', label: 'Block' },
                  { value: 'flex',  label: 'Flex' },
                  { value: 'grid',  label: 'Grid' },
                  { value: 'none',  label: 'None (hide)' },
                ]}
                onChange={(v) => updateResp({ display: v })}
              />
            </PropRow>
          </PropGroup>
        </>
      )}

      {/* ── Visibility ── */}
      <VisibilityDeviceRow node={node} store={store} />

      {/* ── Position & Z-index (desktop only) ── */}
      {bp === 'desktop' && (
        <PropGroup label="Position" defaultOpen={false} icon={
          <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
            <circle cx="12" cy="12" r="3"/><line x1="12" y1="2" x2="12" y2="5"/><line x1="12" y1="19" x2="12" y2="22"/>
            <line x1="2" y1="12" x2="5" y2="12"/><line x1="19" y1="12" x2="22" y2="12"/>
          </svg>
        }>
          <PropRow label="Mode" stack>
            <div className="jalpi-toggle-group">
              {(['static', 'relative', 'absolute', 'sticky'] as const).map((p) => (
                <button key={p}
                  className={`jalpi-toggle-group__btn${(styles.position || 'static') === p ? ' is-active' : ''}`}
                  onClick={() => upStyle({ position: p === 'static' ? '' : p })}>
                  {p.charAt(0).toUpperCase() + p.slice(1)}
                </button>
              ))}
            </div>
          </PropRow>
          {styles.position && styles.position !== 'static' && (
            <>
              <PropRow label="Top">
                <DimensionInput value={styles.top || ''} onChange={(v) => upStyle({ top: v })} placeholder="auto" units={['px', '%', 'vh', 'rem']} />
              </PropRow>
              <PropRow label="Right">
                <DimensionInput value={styles.right || ''} onChange={(v) => upStyle({ right: v })} placeholder="auto" units={['px', '%', 'vw', 'rem']} />
              </PropRow>
              <PropRow label="Bottom">
                <DimensionInput value={styles.bottom || ''} onChange={(v) => upStyle({ bottom: v })} placeholder="auto" units={['px', '%', 'vh', 'rem']} />
              </PropRow>
              <PropRow label="Left">
                <DimensionInput value={styles.left || ''} onChange={(v) => upStyle({ left: v })} placeholder="auto" units={['px', '%', 'vw', 'rem']} />
              </PropRow>
            </>
          )}
          <PropRow label="Z-Index">
            <TextInput value={styles.zIndex || ''} onChange={(v) => upStyle({ zIndex: v })} placeholder="auto" />
          </PropRow>
        </PropGroup>
      )}

      <CssIdentityGroup node={node} store={store} />
    </div>
  );
}

/* =========================================================
   STYLE TAB  (Elementor-inspired: Typography → Background → Size → Border)
   ========================================================= */

/* ── Testimonials: dedicated style panel ──────────────── */
function TestimonialsStyleTab({ node, store }: { node: CBNode; store: EditorStore }) {
  const s   = (node.settings ?? {}) as Record<string, string>;
  const upS = (patch: Record<string, string>) =>
    store.updateNode(node.id, { settings: { ...s, ...patch } });
  const str = (k: string) => s[k] || '';

  const fonts = jalpiEditorConfig.fonts ?? [];
  const TYPO_KEYS = ['fontFamily', 'fontSize', 'fontWeight', 'lineHeight', 'letterSpacing', 'textTransform'];

  const prefixStyles = (prefix: string) =>
    Object.fromEntries(TYPO_KEYS.map((k) => [k, str(`${prefix}_${k}`)]));
  const prefixUpdate = (prefix: string) => (patch: Record<string, string>) =>
    upS(Object.fromEntries(Object.entries(patch).map(([k, v]) => [`${prefix}_${k}`, v])));

  const [quoteTypoOpen, setQuoteTypoOpen] = React.useState(false);
  const [nameTypoOpen,  setNameTypoOpen]  = React.useState(false);
  const [roleTypoOpen,  setRoleTypoOpen]  = React.useState(false);
  const quoteTypoRef = React.useRef<HTMLButtonElement>(null);
  const nameTypoRef  = React.useRef<HTMLButtonElement>(null);
  const roleTypoRef  = React.useRef<HTMLButtonElement>(null);
  const closeAll = () => { setQuoteTypoOpen(false); setNameTypoOpen(false); setRoleTypoOpen(false); };

  const bdType = str('ts_bd_type') || 'none';

  return (
    <div className="jalpi-props-section">

      <PropGroup label="Card" defaultOpen>
        <ColorPropRow label="Background" value={str('ts_card_bg')} onChange={(v) => upS({ ts_card_bg: v })} placeholder="#ffffff" />
        <PropRow label="Shadow">
          <Select
            value={str('ts_card_shadow') || 'sm'}
            options={[
              { value: 'none', label: 'None'    },
              { value: 'sm',   label: 'Small'   },
              { value: 'md',   label: 'Medium'  },
              { value: 'lg',   label: 'Large'   },
              { value: 'xl',   label: 'X-Large' },
            ]}
            onChange={(v) => upS({ ts_card_shadow: v })}
          />
        </PropRow>
        <SliderPropRow label="Border Radius" value={str('ts_card_radius')} onChange={(v) => upS({ ts_card_radius: v })} min={0} max={40} step={1} units={['px', 'rem']} placeholder="16" />
        <SliderPropRow label="Padding"       value={str('ts_card_pad')}    onChange={(v) => upS({ ts_card_pad: v })}    min={8} max={80} step={2} units={['px']}           placeholder="28" />
        <SliderPropRow label="Gap"           value={str('ts_gap')}         onChange={(v) => upS({ ts_gap: v })}         min={8} max={80} step={4} units={['px', 'rem']}    placeholder="24" />
        <PropRow label="Border Type">
          <Select
            value={bdType}
            options={[
              { value: 'none',   label: 'None'   },
              { value: 'solid',  label: 'Solid'  },
              { value: 'dashed', label: 'Dashed' },
              { value: 'dotted', label: 'Dotted' },
            ]}
            onChange={(v) => upS({ ts_bd_type: v })}
          />
        </PropRow>
        {bdType !== 'none' && (
          <>
            <SliderPropRow label="Border Width" value={str('ts_bd_width')} onChange={(v) => upS({ ts_bd_width: v })} min={1} max={8} step={1} units={['px']} placeholder="1" />
            <ColorPropRow  label="Border Color" value={str('ts_bd_color')} onChange={(v) => upS({ ts_bd_color: v })} placeholder="#e5e7eb" />
          </>
        )}
        <PropRow label="Alignment">
          <AlignButtons
            value={str('ts_align') || 'left'}
            options={['left', 'center', 'right']}
            onChange={(v) => upS({ ts_align: v })}
          />
        </PropRow>
      </PropGroup>

      <PropGroup label="Quote" defaultOpen={false}>
        <ColorPropRow label="Text Color" value={str('ts_quote_color')} onChange={(v) => upS({ ts_quote_color: v })} placeholder="#374151" />
        <TypographyTriggerRow label="Typography" isOpen={quoteTypoOpen} triggerRef={quoteTypoRef}
          onToggle={() => { closeAll(); setQuoteTypoOpen((o) => !o); }} />
        {quoteTypoOpen && (
          <TypographyPopup
            styles={prefixStyles('ts_q')}
            update={prefixUpdate('ts_q')}
            fonts={fonts}
            respKey={null}
            onClose={() => setQuoteTypoOpen(false)}
            anchorRef={quoteTypoRef}
          />
        )}
        <ColorPropRow  label="Quote Mark Color" value={str('ts_qmark_color')} onChange={(v) => upS({ ts_qmark_color: v })} placeholder="rgba(99,102,241,0.1)" />
        <SliderPropRow label="Quote Mark Size"  value={str('ts_qmark_size')}  onChange={(v) => upS({ ts_qmark_size: v })}  min={40} max={120} step={4} units={['px']} placeholder="72" />
      </PropGroup>

      <PropGroup label="Author" defaultOpen={false}>
        <SliderPropRow label="Avatar Size"   value={str('ts_avatar_size')}   onChange={(v) => upS({ ts_avatar_size: v })}   min={24} max={80} step={2}  units={['px']}      placeholder="44" />
        <SliderPropRow label="Avatar Radius" value={str('ts_avatar_radius')} onChange={(v) => upS({ ts_avatar_radius: v })} min={0}  max={50} step={2}  units={['px', '%']} placeholder="50" />
        <ColorPropRow label="Name Color" value={str('ts_name_color')} onChange={(v) => upS({ ts_name_color: v })} placeholder="#111827" />
        <TypographyTriggerRow label="Name Typography" isOpen={nameTypoOpen} triggerRef={nameTypoRef}
          onToggle={() => { closeAll(); setNameTypoOpen((o) => !o); }} />
        {nameTypoOpen && (
          <TypographyPopup
            styles={prefixStyles('ts_name')}
            update={prefixUpdate('ts_name')}
            fonts={fonts}
            respKey={null}
            onClose={() => setNameTypoOpen(false)}
            anchorRef={nameTypoRef}
          />
        )}
        <ColorPropRow label="Role Color" value={str('ts_role_color')} onChange={(v) => upS({ ts_role_color: v })} placeholder="#6b7280" />
        <TypographyTriggerRow label="Role Typography" isOpen={roleTypoOpen} triggerRef={roleTypoRef}
          onToggle={() => { closeAll(); setRoleTypoOpen((o) => !o); }} />
        {roleTypoOpen && (
          <TypographyPopup
            styles={prefixStyles('ts_role')}
            update={prefixUpdate('ts_role')}
            fonts={fonts}
            respKey={null}
            onClose={() => setRoleTypoOpen(false)}
            anchorRef={roleTypoRef}
          />
        )}
      </PropGroup>

      <PropGroup label="Stars" defaultOpen={false}>
        <ColorPropRow  label="Filled Color" value={str('ts_star_color')} onChange={(v) => upS({ ts_star_color: v })} placeholder="#f59e0b" />
        <ColorPropRow  label="Empty Color"  value={str('ts_star_empty')} onChange={(v) => upS({ ts_star_empty: v })} placeholder="#d1d5db" />
        <SliderPropRow label="Size"         value={str('ts_star_size')}  onChange={(v) => upS({ ts_star_size: v })}  min={10} max={28} step={1} units={['px']} placeholder="16" />
      </PropGroup>

    </div>
  );
}

const FULL_TEXT_ELEMENTS = new Set(['heading', 'text']);
// Text-bearing schema widgets included so they get the text-oriented generic
// style layout (typography group placement) — same treatment as button/html.
const TEXT_ELEMENTS      = new Set(['heading', 'text', 'button', 'html', 'icon', 'marquee', 'typewriter-text']);

/* =========================================================
   ACCORDION — dedicated Style tab
   ========================================================= */
/* ── Video Style Tab ───────────────────────────────────────────────────── */
function VideoStyleTab({ node, store }: { node: CBNode; store: EditorStore }) {
  const s   = (node.settings ?? {}) as Record<string, unknown>;
  const upS = (patch: Record<string, unknown>) =>
    store.updateNode(node.id, { settings: { ...s, ...patch } });
  const str = (k: string) => (s[k] as string) || '';

  // Width goes to node.styles.width so buildInlineStyles picks it up directly
  const nodeStyles = (node.styles ?? {}) as Record<string, string>;
  const upStyle = (patch: Record<string, string>) =>
    store.updateNode(node.id, { styles: { ...nodeStyles, ...patch } });

  const bdType    = str('vid_bdType') || 'default';
  const hasBorder = bdType !== 'default' && bdType !== 'none';

  return (
    <div className="jalpi-props-section">
      <PropGroup label="Video" defaultOpen>
        <PropRow label="Aspect Ratio" stack>
          <Select
            value={str('vid_aspect') || '16:9'}
            options={[
              { value: '16:9', label: '16:9 — Widescreen' },
              { value: '4:3',  label: '4:3  — Standard'   },
              { value: '1:1',  label: '1:1  — Square'      },
              { value: '9:16', label: '9:16 — Portrait'    },
              { value: '21:9', label: '21:9 — Ultrawide'   },
              { value: '3:4',  label: '3:4  — Portrait'    },
            ]}
            onChange={(v) => upS({ vid_aspect: v })}
          />
        </PropRow>
        <PropRow label="Width" stack>
          <DimensionInput value={nodeStyles.width || ''} onChange={(v) => upStyle({ width: v })} placeholder="100%" units={['%','px','vw']} />
        </PropRow>
      </PropGroup>

      <PropGroup label="Border" defaultOpen={false}>
        <PropRow label="Border Type">
          <Select
            value={bdType}
            options={[
              { value: 'default', label: 'Default' },
              { value: 'none',    label: 'None'    },
              { value: 'solid',   label: 'Solid'   },
              { value: 'dashed',  label: 'Dashed'  },
              { value: 'dotted',  label: 'Dotted'  },
              { value: 'double',  label: 'Double'  },
            ]}
            onChange={(v) => upS({ vid_bdType: v })}
          />
        </PropRow>
        {hasBorder && (
          <>
            <PropRow label="Border Width" responsive>
              <BoxSpacingControl
                values={{ top: str('vid_bdw_top'), right: str('vid_bdw_right'), bottom: str('vid_bdw_bottom'), left: str('vid_bdw_left') }}
                onChange={(side, v) => upS({ [`vid_bdw_${side}`]: v })}
                onChangeAll={(v) => upS({ vid_bdw_top: v, vid_bdw_right: v, vid_bdw_bottom: v, vid_bdw_left: v })}
              />
            </PropRow>
            <ColorPropRow label="Border Color" value={str('vid_bdColor')} onChange={(v) => upS({ vid_bdColor: v })} placeholder="#e5e7eb" />
          </>
        )}
        <PropRow label="Border Radius" responsive>
          <BoxSpacingControl
            values={{ top: str('vid_br_top'), right: str('vid_br_right'), bottom: str('vid_br_bottom'), left: str('vid_br_left') }}
            onChange={(side, v) => upS({ [`vid_br_${side}`]: v })}
            onChangeAll={(v) => upS({ vid_br_top: v, vid_br_right: v, vid_br_bottom: v, vid_br_left: v })}
          />
        </PropRow>
      </PropGroup>
    </div>
  );
}

/* ── Progress Bar Style Tab ────────────────────────────────────────────── */
function ProgressStyleTab({ node, store }: { node: CBNode; store: EditorStore }) {
  const s   = (node.settings ?? {}) as Record<string, unknown>;
  const upS = (patch: Record<string, unknown>) =>
    store.updateNode(node.id, { settings: { ...s, ...patch } });
  const str = (k: string) => (s[k] as string) || '';

  // Popup open state per section
  const [titleTypoOpen,   setTitleTypoOpen]   = useState(false);
  const [titleShadowOpen, setTitleShadowOpen] = useState(false);
  const [pctTypoOpen,     setPctTypoOpen]     = useState(false);
  const [pctShadowOpen,   setPctShadowOpen]   = useState(false);
  const [innerTypoOpen,   setInnerTypoOpen]   = useState(false);
  const [innerShadowOpen, setInnerShadowOpen] = useState(false);
  const titleTypoRef   = useRef<HTMLButtonElement>(null);
  const titleShadowRef = useRef<HTMLButtonElement>(null);
  const pctTypoRef     = useRef<HTMLButtonElement>(null);
  const pctShadowRef   = useRef<HTMLButtonElement>(null);
  const innerTypoRef   = useRef<HTMLButtonElement>(null);
  const innerShadowRef = useRef<HTMLButtonElement>(null);

  const TYPO_KEYS   = ['fontFamily','fontSize','fontWeight','textTransform','fontStyle','textDecoration','lineHeight','letterSpacing','wordSpacing'];
  const SHADOW_KEYS = ['textShadowH','textShadowV','textShadowBlur','textShadowColor'];

  const prefixStyles = (prefix: string, keys: string[]): Record<string, string> =>
    Object.fromEntries(keys.map((k) => [k, str(`${prefix}_${k}`)]));

  const prefixUpdate = (prefix: string) => (updates: Record<string, string>) => {
    const patch: Record<string, unknown> = {};
    for (const [k, v] of Object.entries(updates)) patch[`${prefix}_${k}`] = v;
    upS(patch);
  };

  const closeAll = () => {
    setTitleTypoOpen(false); setTitleShadowOpen(false);
    setPctTypoOpen(false);   setPctShadowOpen(false);
    setInnerTypoOpen(false); setInnerShadowOpen(false);
  };

  const fonts = jalpiEditorConfig.fonts ?? [];

  return (
    <div className="jalpi-props-section">

      {/* ── Title ── */}
      <PropGroup label="Title" defaultOpen>
        <ColorPropRow label="Text Color" value={str('pb_title_color')} onChange={(v) => upS({ pb_title_color: v })} placeholder="#374151" />
        <TypographyTriggerRow label="Typography" isOpen={titleTypoOpen} triggerRef={titleTypoRef}
          onToggle={() => { closeAll(); setTitleTypoOpen((o) => !o); }} />
        {titleTypoOpen && (
          <TypographyPopup styles={prefixStyles('pb_title', TYPO_KEYS)} update={prefixUpdate('pb_title')}
            fonts={fonts} respKey={null} onClose={() => setTitleTypoOpen(false)} anchorRef={titleTypoRef} />
        )}
        <TypographyTriggerRow label="Text Shadow" isOpen={titleShadowOpen} triggerRef={titleShadowRef}
          onToggle={() => { closeAll(); setTitleShadowOpen((o) => !o); }} />
        {titleShadowOpen && ReactDOM.createPortal(
          <TextShadowPopup styles={prefixStyles('pb_title', SHADOW_KEYS)} update={prefixUpdate('pb_title')}
            onClose={() => setTitleShadowOpen(false)} anchorRef={titleShadowRef} />,
          document.body
        )}
      </PropGroup>

      {/* ── Percentage bar ── */}
      <PropGroup label="Percentage" defaultOpen={false}>
        <ColorPropRow label="Background Color" value={str('pb_track_bg')} onChange={(v) => upS({ pb_track_bg: v })} placeholder="#e5e7eb" />
        <SliderPropRow label="Height" responsive value={str('pb_track_height')} onChange={(v) => upS({ pb_track_height: v })} min={4} max={40} step={1} units={['px']} placeholder="8" />
        <SliderPropRow label="Border Radius" responsive value={str('pb_track_radius')} onChange={(v) => upS({ pb_track_radius: v })} min={0} max={40} step={1} units={['px']} placeholder="99" />
        <ColorPropRow label="Percentage Color" value={str('pb_pct_color')} onChange={(v) => upS({ pb_pct_color: v })} placeholder="#6b7280" />
        <TypographyTriggerRow label="Typography" isOpen={pctTypoOpen} triggerRef={pctTypoRef}
          onToggle={() => { closeAll(); setPctTypoOpen((o) => !o); }} />
        {pctTypoOpen && (
          <TypographyPopup styles={prefixStyles('pb_pct', TYPO_KEYS)} update={prefixUpdate('pb_pct')}
            fonts={fonts} respKey={null} onClose={() => setPctTypoOpen(false)} anchorRef={pctTypoRef} />
        )}
        <TypographyTriggerRow label="Text Shadow" isOpen={pctShadowOpen} triggerRef={pctShadowRef}
          onToggle={() => { closeAll(); setPctShadowOpen((o) => !o); }} />
        {pctShadowOpen && ReactDOM.createPortal(
          <TextShadowPopup styles={prefixStyles('pb_pct', SHADOW_KEYS)} update={prefixUpdate('pb_pct')}
            onClose={() => setPctShadowOpen(false)} anchorRef={pctShadowRef} />,
          document.body
        )}
      </PropGroup>

      {/* ── Inner Text ── */}
      <PropGroup label="Inner Text" defaultOpen={false}>
        <ColorPropRow label="Color" value={str('pb_inner_color')} onChange={(v) => upS({ pb_inner_color: v })} placeholder="#9ca3af" />
        <TypographyTriggerRow label="Typography" isOpen={innerTypoOpen} triggerRef={innerTypoRef}
          onToggle={() => { closeAll(); setInnerTypoOpen((o) => !o); }} />
        {innerTypoOpen && (
          <TypographyPopup styles={prefixStyles('pb_inner', TYPO_KEYS)} update={prefixUpdate('pb_inner')}
            fonts={fonts} respKey={null} onClose={() => setInnerTypoOpen(false)} anchorRef={innerTypoRef} />
        )}
        <TypographyTriggerRow label="Text Shadow" isOpen={innerShadowOpen} triggerRef={innerShadowRef}
          onToggle={() => { closeAll(); setInnerShadowOpen((o) => !o); }} />
        {innerShadowOpen && ReactDOM.createPortal(
          <TextShadowPopup styles={prefixStyles('pb_inner', SHADOW_KEYS)} update={prefixUpdate('pb_inner')}
            onClose={() => setInnerShadowOpen(false)} anchorRef={innerShadowRef} />,
          document.body
        )}
      </PropGroup>

    </div>
  );
}

/* ── Counter Style Tab ─────────────────────────────────────────────────── */
function CounterStyleTab({ node, store }: { node: CBNode; store: EditorStore }) {
  type NHState = 'normal' | 'hover';
  const [nh, setNh] = useState<NHState>('normal');

  const s   = (node.settings ?? {}) as Record<string, unknown>;
  const upS = (patch: Record<string, unknown>) =>
    store.updateNode(node.id, { settings: { ...s, ...patch } });
  const str = (k: string) => (s[k] as string) || '';

  const bgKey  = `cnt_bg_${nh}`;
  const bdType = str('cnt_bdType') || 'default';
  const hasBorder = bdType !== 'default' && bdType !== 'none';

  return (
    <div className="jalpi-props-section">

      {/* ── Counter wrapper ── */}
      <PropGroup label="Counter" defaultOpen>
        <div className="jalpi-nh-tabs">
          <button type="button" className={`jalpi-nh-tab${nh === 'normal' ? ' is-active' : ''}`} onClick={() => setNh('normal')}>Normal</button>
          <button type="button" className={`jalpi-nh-tab${nh === 'hover'  ? ' is-active' : ''}`} onClick={() => setNh('hover')}>Hover</button>
        </div>

        <ColorPropRow label="Background" value={str(bgKey)} onChange={(v) => upS({ [bgKey]: v })} placeholder="transparent" />

        <PropRow label="Border Type">
          <Select
            value={bdType}
            options={[
              { value: 'default', label: 'Default' },
              { value: 'none',    label: 'None'    },
              { value: 'solid',   label: 'Solid'   },
              { value: 'dashed',  label: 'Dashed'  },
              { value: 'dotted',  label: 'Dotted'  },
              { value: 'double',  label: 'Double'  },
              { value: 'groove',  label: 'Groove'  },
            ]}
            onChange={(v) => upS({ cnt_bdType: v })}
          />
        </PropRow>

        {hasBorder && (
          <>
            <PropRow label="Border Width" responsive>
              <BoxSpacingControl
                values={{ top: str('cnt_bdw_top'), right: str('cnt_bdw_right'), bottom: str('cnt_bdw_bottom'), left: str('cnt_bdw_left') }}
                onChange={(side, v) => upS({ [`cnt_bdw_${side}`]: v })}
                onChangeAll={(v) => upS({ cnt_bdw_top: v, cnt_bdw_right: v, cnt_bdw_bottom: v, cnt_bdw_left: v })}
              />
            </PropRow>
            <ColorPropRow label="Border Color" value={str('cnt_bdColor')} onChange={(v) => upS({ cnt_bdColor: v })} placeholder="#e5e7eb" />
          </>
        )}

        <PropRow label="Border Radius" responsive>
          <BoxSpacingControl
            values={{ top: str('cnt_br_top'), right: str('cnt_br_right'), bottom: str('cnt_br_bottom'), left: str('cnt_br_left') }}
            onChange={(side, v) => upS({ [`cnt_br_${side}`]: v })}
            onChangeAll={(v) => upS({ cnt_br_top: v, cnt_br_right: v, cnt_br_bottom: v, cnt_br_left: v })}
          />
        </PropRow>

        <PropRow label="Padding" responsive>
          <BoxSpacingControl
            values={{ top: str('cnt_pad_top'), right: str('cnt_pad_right'), bottom: str('cnt_pad_bottom'), left: str('cnt_pad_left') }}
            onChange={(side, v) => upS({ [`cnt_pad_${side}`]: v })}
            onChangeAll={(v) => upS({ cnt_pad_top: v, cnt_pad_right: v, cnt_pad_bottom: v, cnt_pad_left: v })}
          />
        </PropRow>

        <SliderPropRow label="Item Spacing" responsive
          value={str('cnt_gap')}
          onChange={(v) => upS({ cnt_gap: v })}
          min={0} max={120} step={1} units={['px']} placeholder="24" />

        <PropRow label="Title Position" responsive>
          <div className="jalpi-align-btns">
            {([
              { val: 'top',    title: 'Top',
                icon: <svg width="14" height="14" viewBox="0 0 20 20" fill="currentColor"><rect x="2" y="2" width="16" height="3" rx="1"/><rect x="2" y="8" width="16" height="10" rx="1" opacity=".4"/></svg> },
              { val: 'bottom', title: 'Bottom',
                icon: <svg width="14" height="14" viewBox="0 0 20 20" fill="currentColor"><rect x="2" y="2" width="16" height="10" rx="1" opacity=".4"/><rect x="2" y="15" width="16" height="3" rx="1"/></svg> },
              { val: 'left',   title: 'Left',
                icon: <svg width="14" height="14" viewBox="0 0 20 20" fill="currentColor"><rect x="2" y="2" width="5" height="16" rx="1"/><rect x="9" y="2" width="9" height="16" rx="1" opacity=".4"/></svg> },
              { val: 'right',  title: 'Right',
                icon: <svg width="14" height="14" viewBox="0 0 20 20" fill="currentColor"><rect x="2" y="2" width="9" height="16" rx="1" opacity=".4"/><rect x="13" y="2" width="5" height="16" rx="1"/></svg> },
            ] as { val: string; title: string; icon: React.ReactNode }[]).map(opt => (
              <button key={opt.val} type="button"
                className={`jalpi-align-btn${(str('cnt_titlePos') || 'bottom') === opt.val ? ' is-active' : ''}`}
                onClick={() => upS({ cnt_titlePos: opt.val })}
                title={opt.title}
              >{opt.icon}</button>
            ))}
          </div>
        </PropRow>

        <PropRow label="Title Horizontal Alignment" responsive>
          <AlignButtons
            value={str('cnt_titleAlign') || 'center'}
            options={['left', 'center', 'right']}
            onChange={(v) => upS({ cnt_titleAlign: v })}
          />
        </PropRow>

        <SliderPropRow label="Title Gap" responsive
          value={str('cnt_titleGap')}
          onChange={(v) => upS({ cnt_titleGap: v })}
          min={0} max={80} step={1} units={['px']} placeholder="6" />

        <PropRow label="Number Position" responsive>
          <AlignButtons
            value={str('cnt_numPos') || 'center'}
            options={['left', 'center', 'right', 'justify']}
            onChange={(v) => upS({ cnt_numPos: v })}
          />
        </PropRow>
      </PropGroup>

      {/* ── Number ── */}
      <PropGroup label="Number" defaultOpen={false}>
        <ColorPropRow label="Color" value={str('cnt_num_color')} onChange={(v) => upS({ cnt_num_color: v })} placeholder="#6366f1" />
        <SliderPropRow label="Font Size" responsive value={str('cnt_num_fontSize')} onChange={(v) => upS({ cnt_num_fontSize: v })} min={10} max={120} step={1} units={['px', 'em', 'rem']} placeholder="48" />
        <PropRow label="Font Weight" stack>
          <Select
            value={str('cnt_num_fontWeight') || '800'}
            options={[
              { value: '400', label: 'Regular (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)'   },
            ]}
            onChange={(v) => upS({ cnt_num_fontWeight: v })}
          />
        </PropRow>
      </PropGroup>

      {/* ── Title / Label ── */}
      <PropGroup label="Title" defaultOpen={false}>
        <ColorPropRow label="Color" value={str('cnt_title_color')} onChange={(v) => upS({ cnt_title_color: v })} placeholder="#6b7280" />
        <SliderPropRow label="Font Size" responsive value={str('cnt_title_fontSize')} onChange={(v) => upS({ cnt_title_fontSize: v })} min={10} max={48} step={1} units={['px', 'em', 'rem']} placeholder="16" />
        <PropRow label="Font Weight" stack>
          <Select
            value={str('cnt_title_fontWeight') || '500'}
            options={[
              { value: '400', label: 'Regular (400)' },
              { value: '500', label: 'Medium (500)'  },
              { value: '600', label: 'Semi Bold (600)' },
              { value: '700', label: 'Bold (700)'    },
            ]}
            onChange={(v) => upS({ cnt_title_fontWeight: v })}
          />
        </PropRow>
      </PropGroup>

    </div>
  );
}

function AccordionStyleTab({ node, store }: { node: CBNode; store: EditorStore }) {
  type NHAState = 'normal' | 'hover' | 'active';
  const [nha, setNha] = useState<NHAState>('normal');

  const s   = (node.settings ?? {}) as Record<string, unknown>;
  const upS = (patch: Record<string, unknown>) =>
    store.updateNode(node.id, { settings: { ...s, ...patch } });

  const str = (k: string) => (s[k] as string) || '';

  // Background is per-state; everything else is global
  const bgKey = `acc_bg_${nha}`;

  // Global border (no state suffix — same on all tabs)
  const bdType    = str('acc_bdType') || 'default';
  const hasBorder = bdType !== 'default' && bdType !== 'none';

  return (
    <div className="jalpi-props-section">

      {/* ── Accordion wrapper ── */}
      <PropGroup label="Accordion" defaultOpen>
        <SliderPropRow
          label="Space between Items" responsive
          value={str('acc_item_gap')}
          onChange={(v) => upS({ acc_item_gap: v })}
          min={0} max={100} step={1} units={['px']} placeholder="0"
        />
        <SliderPropRow
          label="Distance from content" responsive
          value={str('acc_content_gap')}
          onChange={(v) => upS({ acc_content_gap: v })}
          min={0} max={100} step={1} units={['px']} placeholder="0"
        />

        {/* Normal / Hover / Active — controls Background per state */}
        <div className="jalpi-nh-tabs">
          <button type="button" className={`jalpi-nh-tab${nha === 'normal' ? ' is-active' : ''}`} onClick={() => setNha('normal')}>Normal</button>
          <button type="button" className={`jalpi-nh-tab${nha === 'hover'  ? ' is-active' : ''}`} onClick={() => setNha('hover')}>Hover</button>
          <button type="button" className={`jalpi-nh-tab${nha === 'active' ? ' is-active' : ''}`} onClick={() => setNha('active')}>Active</button>
        </div>

        {/* Background color — per state */}
        <ColorPropRow
          label="Background"
          value={str(bgKey)}
          onChange={(v) => upS({ [bgKey]: v })}
          placeholder="transparent"
        />

        {/* ── Border (global — same across all tabs) ── */}
        <PropRow label="Border Type">
          <Select
            value={bdType}
            options={[
              { value: 'default', label: 'Default' },
              { value: 'none',    label: 'None'    },
              { value: 'solid',   label: 'Solid'   },
              { value: 'dashed',  label: 'Dashed'  },
              { value: 'dotted',  label: 'Dotted'  },
              { value: 'double',  label: 'Double'  },
              { value: 'groove',  label: 'Groove'  },
            ]}
            onChange={(v) => upS({ acc_bdType: v })}
          />
        </PropRow>

        {hasBorder && (
          <>
            <PropRow label="Border Width" responsive>
              <BoxSpacingControl
                values={{
                  top:    str('acc_bdw_top'),
                  right:  str('acc_bdw_right'),
                  bottom: str('acc_bdw_bottom'),
                  left:   str('acc_bdw_left'),
                }}
                onChange={(side, v) => upS({ [`acc_bdw_${side}`]: v })}
                onChangeAll={(v) => upS({ acc_bdw_top: v, acc_bdw_right: v, acc_bdw_bottom: v, acc_bdw_left: v })}
              />
            </PropRow>
            <ColorPropRow
              label="Border Color"
              value={str('acc_bdColor')}
              onChange={(v) => upS({ acc_bdColor: v })}
              placeholder="#e5e7eb"
            />
          </>
        )}

        {/* Border Radius — global */}
        <PropRow label="Border Radius" responsive>
          <BoxSpacingControl
            values={{
              top:    str('acc_br_top'),
              right:  str('acc_br_right'),
              bottom: str('acc_br_bottom'),
              left:   str('acc_br_left'),
            }}
            onChange={(side, v) => upS({ [`acc_br_${side}`]: v })}
            onChangeAll={(v) => upS({ acc_br_top: v, acc_br_right: v, acc_br_bottom: v, acc_br_left: v })}
          />
        </PropRow>

        {/* Padding — global */}
        <PropRow label="Padding" responsive>
          <BoxSpacingControl
            values={{
              top:    str('acc_pad_top'),
              right:  str('acc_pad_right'),
              bottom: str('acc_pad_bottom'),
              left:   str('acc_pad_left'),
            }}
            onChange={(side, v) => upS({ [`acc_pad_${side}`]: v })}
            onChangeAll={(v) => upS({ acc_pad_top: v, acc_pad_right: v, acc_pad_bottom: v, acc_pad_left: v })}
          />
        </PropRow>
      </PropGroup>

      {/* ── Header styles ── */}
      <PropGroup label="Header" defaultOpen={false}>
        <ColorPropRow
          label="Background"
          value={str('acc_hdr_bg')}
          onChange={(v) => upS({ acc_hdr_bg: v })}
          placeholder="#f9fafb"
        />
        <ColorPropRow
          label="Text Color"
          value={str('acc_hdr_color')}
          onChange={(v) => upS({ acc_hdr_color: v })}
          placeholder="#111827"
        />
        <SliderPropRow
          label="Font Size" responsive
          value={str('acc_hdr_fontSize')}
          onChange={(v) => upS({ acc_hdr_fontSize: v })}
          min={10} max={60} step={1} units={['px', 'em', 'rem']} placeholder="16"
        />
        <PropRow label="Padding" responsive>
          <BoxSpacingControl
            values={{
              top:    str('acc_hdr_pad_top'),
              right:  str('acc_hdr_pad_right'),
              bottom: str('acc_hdr_pad_bottom'),
              left:   str('acc_hdr_pad_left'),
            }}
            onChange={(side, v) => upS({ [`acc_hdr_pad_${side}`]: v })}
            onChangeAll={(v) => upS({ acc_hdr_pad_top: v, acc_hdr_pad_right: v, acc_hdr_pad_bottom: v, acc_hdr_pad_left: v })}
          />
        </PropRow>
      </PropGroup>

      {/* ── Content body styles ── */}
      <PropGroup label="Content" defaultOpen={false}>
        <ColorPropRow
          label="Background"
          value={str('acc_body_bg')}
          onChange={(v) => upS({ acc_body_bg: v })}
          placeholder="transparent"
        />
        <ColorPropRow
          label="Text Color"
          value={str('acc_body_color')}
          onChange={(v) => upS({ acc_body_color: v })}
          placeholder="#374151"
        />
        <SliderPropRow
          label="Font Size" responsive
          value={str('acc_body_fontSize')}
          onChange={(v) => upS({ acc_body_fontSize: v })}
          min={10} max={48} step={1} units={['px', 'em', 'rem']} placeholder="14"
        />
        <PropRow label="Padding" responsive>
          <BoxSpacingControl
            values={{
              top:    str('acc_body_pad_top'),
              right:  str('acc_body_pad_right'),
              bottom: str('acc_body_pad_bottom'),
              left:   str('acc_body_pad_left'),
            }}
            onChange={(side, v) => upS({ [`acc_body_pad_${side}`]: v })}
            onChangeAll={(v) => upS({ acc_body_pad_top: v, acc_body_pad_right: v, acc_body_pad_bottom: v, acc_body_pad_left: v })}
          />
        </PropRow>
      </PropGroup>

    </div>
  );
}

/* ── CSS Filters popup (portal) ─────────────────────────────────────────── */
function CssFiltersPopup({
  settings, nh, upS, onClose, anchorRef,
}: {
  settings: Record<string, unknown>;
  nh: 'normal' | 'hover';
  upS: (patch: Record<string, unknown>) => void;
  onClose: () => void;
  anchorRef: React.RefObject<HTMLButtonElement>;
}) {
  const popupRef = useRef<HTMLDivElement>(null);
  const [pos, setPos] = useState<{ top: number; right: number } | null>(null);
  const str = (k: string) => (settings[k] as string) || '';
  const sfx = nh === 'hover' ? 'Hover' : '';

  useEffect(() => {
    if (!anchorRef.current) return;
    const r = anchorRef.current.getBoundingClientRect();
    setPos({ top: Math.max(10, Math.min(r.top - 40, window.innerHeight - 320)), right: window.innerWidth - r.left + 10 });
  }, [anchorRef]);

  useEffect(() => {
    const handler = (e: MouseEvent) => {
      if (popupRef.current && !popupRef.current.contains(e.target as Node) &&
          anchorRef.current && !anchorRef.current.contains(e.target as Node)) onClose();
    };
    document.addEventListener('mousedown', handler);
    return () => document.removeEventListener('mousedown', handler);
  }, [onClose, anchorRef]);

  const reset = () => upS({ [`img_blur${sfx}`]: '', [`img_brightness${sfx}`]: '', [`img_contrast${sfx}`]: '', [`img_saturate${sfx}`]: '', [`img_hue${sfx}`]: '' });

  if (!pos) return null;
  return (
    <div ref={popupRef} className="jalpi-typo-popup" style={{ top: pos.top, right: pos.right }}>
      <div className="jalpi-typo-popup__header">
        <span className="jalpi-typo-popup__title">CSS Filters — {nh === 'hover' ? 'Hover' : 'Normal'}</span>
        <div className="jalpi-typo-popup__header-btns">
          <button type="button" className="jalpi-typo-popup__hbtn" title="Reset" onClick={reset}>
            <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round"><path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/><path d="M3 3v5h5"/></svg>
          </button>
          <button type="button" className="jalpi-typo-popup__hbtn" title="Close" onClick={onClose}>
            <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
          </button>
        </div>
      </div>
      <div className="jalpi-typo-popup__body">
        <ShadowSlider label="Blur"       value={str(`img_blur${sfx}`)}       onChange={(v) => upS({ [`img_blur${sfx}`]: v })}       min={0}   max={20}  />
        <ShadowSlider label="Brightness" value={str(`img_brightness${sfx}`)} onChange={(v) => upS({ [`img_brightness${sfx}`]: v })} min={0}   max={200} />
        <ShadowSlider label="Contrast"   value={str(`img_contrast${sfx}`)}   onChange={(v) => upS({ [`img_contrast${sfx}`]: v })}   min={0}   max={200} />
        <ShadowSlider label="Saturate"   value={str(`img_saturate${sfx}`)}   onChange={(v) => upS({ [`img_saturate${sfx}`]: v })}   min={0}   max={200} />
        <ShadowSlider label="Hue Rotate" value={str(`img_hue${sfx}`)}        onChange={(v) => upS({ [`img_hue${sfx}`]: v })}        min={0}   max={360} />
      </div>
    </div>
  );
}

/* ── Box Shadow popup (portal) ──────────────────────────────────────────── */
function ImgBoxShadowPopup({
  value, onChange, onClose, anchorRef,
}: {
  value: string;
  onChange: (v: string) => void;
  onClose: () => void;
  anchorRef: React.RefObject<HTMLButtonElement>;
}) {
  const popupRef = useRef<HTMLDivElement>(null);
  const [pos, setPos] = useState<{ top: number; right: number } | null>(null);

  // Parse existing value or defaults
  const parts = value.trim().split(/\s+/);
  const isInset = parts[0] === 'inset';
  const nums    = isInset ? parts.slice(1) : parts;
  const [h,   setH]   = useState(parseFloat(nums[0]) || 0);
  const [v,   setV]   = useState(parseFloat(nums[1]) || 0);
  const [blur, setBlur] = useState(parseFloat(nums[2]) || 0);
  const [spread, setSpread] = useState(parseFloat(nums[3]) || 0);
  const [color, setColor]   = useState(nums[4] || 'rgba(0,0,0,0.15)');
  const [inset, setInset]   = useState(isInset);

  const compose = (hv: number, vv: number, bl: number, sp: number, cl: string, ins: boolean) =>
    `${ins ? 'inset ' : ''}${hv}px ${vv}px ${bl}px ${sp}px ${cl}`.trim();

  const update = (hv: number, vv: number, bl: number, sp: number, cl: string, ins: boolean) => {
    onChange(compose(hv, vv, bl, sp, cl, ins));
  };

  useEffect(() => {
    if (!anchorRef.current) return;
    const r = anchorRef.current.getBoundingClientRect();
    setPos({ top: Math.max(10, Math.min(r.top - 40, window.innerHeight - 360)), right: window.innerWidth - r.left + 10 });
  }, [anchorRef]);

  useEffect(() => {
    const handler = (e: MouseEvent) => {
      if (popupRef.current && !popupRef.current.contains(e.target as Node) &&
          anchorRef.current && !anchorRef.current.contains(e.target as Node)) onClose();
    };
    document.addEventListener('mousedown', handler);
    return () => document.removeEventListener('mousedown', handler);
  }, [onClose, anchorRef]);

  const reset = () => { onChange(''); setH(0); setV(0); setBlur(0); setSpread(0); setColor('rgba(0,0,0,0.15)'); setInset(false); };

  if (!pos) return null;
  return (
    <div ref={popupRef} className="jalpi-typo-popup" style={{ top: pos.top, right: pos.right }}>
      <div className="jalpi-typo-popup__header">
        <span className="jalpi-typo-popup__title">Box Shadow</span>
        <div className="jalpi-typo-popup__header-btns">
          <button type="button" className="jalpi-typo-popup__hbtn" title="Reset" onClick={reset}>
            <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round"><path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/><path d="M3 3v5h5"/></svg>
          </button>
          <button type="button" className="jalpi-typo-popup__hbtn" title="Close" onClick={onClose}>
            <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
          </button>
        </div>
      </div>
      <div className="jalpi-typo-popup__body">
        <div className="jalpi-typo-popup__field jalpi-typo-popup__field--inline">
          <label className="jalpi-typo-popup__field-lbl">Color</label>
          <ColorInput value={color} onChange={(c) => { setColor(c); update(h, v, blur, spread, c, inset); }} placeholder="rgba(0,0,0,0.15)" />
        </div>
        <ShadowSlider label="Horizontal" value={h + 'px'} onChange={(val) => { const n = parseFloat(val) || 0; setH(n); update(n, v, blur, spread, color, inset); }} min={-100} max={100} />
        <ShadowSlider label="Vertical"   value={v + 'px'} onChange={(val) => { const n = parseFloat(val) || 0; setV(n); update(h, n, blur, spread, color, inset); }} min={-100} max={100} />
        <ShadowSlider label="Blur"       value={blur + 'px'} onChange={(val) => { const n = parseFloat(val) || 0; setBlur(n); update(h, v, n, spread, color, inset); }} min={0} max={100} />
        <ShadowSlider label="Spread"     value={spread + 'px'} onChange={(val) => { const n = parseFloat(val) || 0; setSpread(n); update(h, v, blur, n, color, inset); }} min={-50} max={50} />
        <div className="jalpi-typo-popup__field jalpi-typo-popup__field--inline">
          <label className="jalpi-typo-popup__field-lbl">Inset</label>
          <Toggle checked={inset} onChange={(v) => { setInset(v); update(h, blur, blur, spread, color, v); }} />
        </div>
      </div>
    </div>
  );
}

/* ── Image Style Tab ───────────────────────────────────────────────────── */
function ImageStyleTab({ node, store }: { node: CBNode; store: EditorStore }) {
  const [nh, setNH] = useState<'normal' | 'hover'>('normal');
  const [filtersOpen,  setFiltersOpen]  = useState(false);
  const [shadowOpen,   setShadowOpen]   = useState(false);
  const [capTypoOpen,  setCapTypoOpen]  = useState(false);
  const [capShadowOpen, setCapShadowOpen] = useState(false);
  const filtersTriggerRef = useRef<HTMLButtonElement>(null);
  const shadowTriggerRef  = useRef<HTMLButtonElement>(null);
  const capTypoRef        = useRef<HTMLButtonElement>(null);
  const capShadowRef      = useRef<HTMLButtonElement>(null);

  const s   = (node.settings ?? {}) as Record<string, unknown>;
  const upS = (patch: Record<string, unknown>) =>
    store.updateNode(node.id, { settings: { ...s, ...patch } });
  const str = (k: string) => (s[k] as string) || '';
  const fonts = jalpiEditorConfig.fonts ?? [];

  const CAP_TYPO_KEYS   = ['fontFamily', 'fontSize', 'fontWeight', 'lineHeight', 'letterSpacing', 'textTransform'];
  const CAP_SHADOW_KEYS = ['textShadowH', 'textShadowV', 'textShadowBlur', 'textShadowColor'];
  const capPrefixStyles = (keys: string[]) =>
    Object.fromEntries(keys.map((k) => [k, str(`img_cap_${k}`)]));
  const capPrefixUpdate = (patch: Record<string, string>) =>
    upS(Object.fromEntries(Object.entries(patch).map(([k, v]) => [`img_cap_${k}`, v])));

  const { state } = store;
  const bp      = state.breakpoint;
  const respKey: 'tabletStyles' | 'mobileStyles' | null = bp === 'tablet' ? 'tabletStyles' : bp === 'mobile' ? 'mobileStyles' : null;
  const respStyles = (respKey ? (node[respKey] ?? {}) : {}) as Record<string, string>;
  const nodeStyles = (node.styles ?? {}) as Record<string, string>;
  const effective  = respKey
    ? { ...nodeStyles, ...Object.fromEntries(Object.entries(respStyles).filter(([, v]) => v)) }
    : nodeStyles;

  const upStyle = (patch: Record<string, string>) => {
    if (respKey) store.updateNode(node.id, { [respKey]: { ...respStyles, ...patch } });
    else store.updateNodeStyles(node.id, patch);
  };

  const bdType    = str('img_bdType') || 'default';
  const hasBorder = bdType !== 'default' && bdType !== 'none';

  return (
    <div className="jalpi-props-section">

      <PropRow label="Alignment" responsive>
        <AlignButtons
          value={effective.textAlign || ''}
          options={['left', 'center', 'right']}
          onChange={(v) => upStyle({ textAlign: v })}
        />
      </PropRow>

      <SliderPropRow label="Width" responsive
        value={effective.width || ''}
        onChange={(v) => upStyle({ width: v })}
        min={0} max={2000} step={1} units={['%', 'px', 'vw']} placeholder="100%" />

      <SliderPropRow label="Max Width" responsive
        value={effective.maxWidth || ''}
        onChange={(v) => upStyle({ maxWidth: v })}
        min={0} max={2000} step={1} units={['%', 'px', 'vw']} placeholder="100%" />

      <SliderPropRow label="Height" responsive
        value={effective.height || ''}
        onChange={(v) => upStyle({ height: v })}
        min={0} max={1000} step={1} units={['px', 'vh']} placeholder="auto" />

      <NormalHoverTabs state={nh} onChange={setNH} />

      <SliderPropRow label="Opacity"
        value={nh === 'normal' ? (str('img_opacity') || '100') : (str('img_opacityHover') || '100')}
        onChange={(v) => upS(nh === 'normal' ? { img_opacity: v } : { img_opacityHover: v })}
        min={0} max={100} step={1} units={[]} placeholder="100" />

      <TypographyTriggerRow
        label="CSS Filters"
        isOpen={filtersOpen}
        triggerRef={filtersTriggerRef}
        onToggle={() => setFiltersOpen((o) => !o)}
      />
      {filtersOpen && ReactDOM.createPortal(
        <CssFiltersPopup
          settings={s}
          nh={nh}
          upS={upS}
          onClose={() => setFiltersOpen(false)}
          anchorRef={filtersTriggerRef}
        />,
        document.body
      )}

      <PropRow label="Border Type">
        <Select
          value={bdType}
          options={[
            { value: 'default', label: 'Default' },
            { value: 'none',    label: 'None'    },
            { value: 'solid',   label: 'Solid'   },
            { value: 'dashed',  label: 'Dashed'  },
            { value: 'dotted',  label: 'Dotted'  },
            { value: 'double',  label: 'Double'  },
          ]}
          onChange={(v) => upS({ img_bdType: v })}
        />
      </PropRow>

      {hasBorder && (
        <>
          <PropRow label="Border Width" responsive>
            <BoxSpacingControl
              values={{ top: str('img_bdw_top'), right: str('img_bdw_right'), bottom: str('img_bdw_bottom'), left: str('img_bdw_left') }}
              onChange={(side, v) => upS({ [`img_bdw_${side}`]: v })}
              onChangeAll={(v) => upS({ img_bdw_top: v, img_bdw_right: v, img_bdw_bottom: v, img_bdw_left: v })}
            />
          </PropRow>
          <ColorPropRow label="Border Color" value={str('img_bdColor')} onChange={(v) => upS({ img_bdColor: v })} placeholder="#e5e7eb" />
        </>
      )}

      <PropRow label="Border Radius" responsive>
        <BoxSpacingControl
          values={{ top: str('img_br_top'), right: str('img_br_right'), bottom: str('img_br_bottom'), left: str('img_br_left') }}
          onChange={(side, v) => upS({ [`img_br_${side}`]: v })}
          onChangeAll={(v) => upS({ img_br_top: v, img_br_right: v, img_br_bottom: v, img_br_left: v })}
        />
      </PropRow>

      <TypographyTriggerRow
        label="Box Shadow"
        isOpen={shadowOpen}
        triggerRef={shadowTriggerRef}
        onToggle={() => setShadowOpen((o) => !o)}
      />
      {shadowOpen && ReactDOM.createPortal(
        <ImgBoxShadowPopup
          value={str('img_boxShadow')}
          onChange={(v) => upS({ img_boxShadow: v })}
          onClose={() => setShadowOpen(false)}
          anchorRef={shadowTriggerRef}
        />,
        document.body
      )}

      <PropGroup label="Caption" defaultOpen={false}>
        <PropRow label="Alignment" responsive>
          <AlignButtons
            value={str('img_cap_textAlign') || ''}
            options={['left', 'center', 'right', 'justify']}
            onChange={(v) => upS({ img_cap_textAlign: v })}
          />
        </PropRow>
        <ColorPropRow label="Text Color"       value={str('img_cap_color')}   onChange={(v) => upS({ img_cap_color: v })}   placeholder="#374151" />
        <ColorPropRow label="Background Color" value={str('img_cap_bgColor')} onChange={(v) => upS({ img_cap_bgColor: v })} placeholder="transparent" />
        <TypographyTriggerRow
          label="Typography"
          isOpen={capTypoOpen}
          triggerRef={capTypoRef}
          onToggle={() => { setCapTypoOpen((o) => !o); setCapShadowOpen(false); }}
        />
        {capTypoOpen && (
          <TypographyPopup
            styles={capPrefixStyles(CAP_TYPO_KEYS)}
            update={capPrefixUpdate}
            fonts={fonts}
            respKey={null}
            onClose={() => setCapTypoOpen(false)}
            anchorRef={capTypoRef}
          />
        )}
        <TypographyTriggerRow
          label="Text Shadow"
          isOpen={capShadowOpen}
          triggerRef={capShadowRef}
          onToggle={() => { setCapShadowOpen((o) => !o); setCapTypoOpen(false); }}
        />
        {capShadowOpen && ReactDOM.createPortal(
          <TextShadowPopup
            styles={capPrefixStyles(CAP_SHADOW_KEYS)}
            update={capPrefixUpdate}
            onClose={() => setCapShadowOpen(false)}
            anchorRef={capShadowRef}
          />,
          document.body
        )}
        <SliderPropRow label="Spacing" responsive
          value={str('img_cap_spacing') || ''}
          onChange={(v) => upS({ img_cap_spacing: v })}
          min={0} max={60} step={1} units={['px']} placeholder="8" />
      </PropGroup>

    </div>
  );
}

/* ── Feature Card Style Tab ─────────────────────────────────────────────── */
function FeatureCardStyleTab({ node, store }: { node: CBNode; store: EditorStore }) {
  const s   = (node.settings ?? {}) as Record<string, unknown>;
  const str = (k: string) => (s[k] as string) || '';
  const upS = (patch: Record<string, unknown>) => store.updateNode(node.id, { settings: { ...s, ...patch } });

  const [titleTypoOpen, setTitleTypoOpen] = useState(false);
  const [descTypoOpen,  setDescTypoOpen]  = useState(false);
  const titleTypoRef = useRef<HTMLButtonElement>(null);
  const descTypoRef  = useRef<HTMLButtonElement>(null);

  const titleTypoStyles: Record<string, string> = {
    fontFamily: str('fc_title_fontFamily'), fontSize: str('fc_title_fontSize'),
    fontWeight: str('fc_title_fontWeight'), fontStyle: str('fc_title_fontStyle'),
    textTransform: str('fc_title_textTransform'), textDecoration: str('fc_title_textDecoration'),
    lineHeight: str('fc_title_lineHeight'), letterSpacing: str('fc_title_letterSpacing'),
  };
  const updateTitleTypo = (patch: Record<string, string>) => {
    const mapped: Record<string, unknown> = {};
    for (const [k, v] of Object.entries(patch)) mapped[`fc_title_${k}`] = v;
    upS(mapped);
  };

  const descTypoStyles: Record<string, string> = {
    fontFamily: str('fc_desc_fontFamily'), fontSize: str('fc_desc_fontSize'),
    fontWeight: str('fc_desc_fontWeight'), fontStyle: str('fc_desc_fontStyle'),
    textTransform: str('fc_desc_textTransform'), textDecoration: str('fc_desc_textDecoration'),
    lineHeight: str('fc_desc_lineHeight'), letterSpacing: str('fc_desc_letterSpacing'),
  };
  const updateDescTypo = (patch: Record<string, string>) => {
    const mapped: Record<string, unknown> = {};
    for (const [k, v] of Object.entries(patch)) mapped[`fc_desc_${k}`] = v;
    upS(mapped);
  };

  const bdType    = str('fc_bdType') || 'none';
  const hasBorder = bdType !== 'none' && bdType !== 'default';

  return (
    <div className="jalpi-props-section">

      {/* ── Box ── */}
      <PropGroup label="Box" defaultOpen>
        <PropRow label="Icon Position" responsive>
          <AlignButtons
            value={str('fc_iconPosition') || 'top'}
            onChange={(v) => upS({ fc_iconPosition: v })}
            options={['left', 'center', 'right', 'justify']}
            icons={[
              <svg key="l" width="14" height="14" viewBox="0 0 20 20" fill="currentColor"><rect x="2" y="2" width="5" height="16" rx="1"/><rect x="9" y="5" width="9" height="10" rx="1" opacity=".4"/></svg>,
              <svg key="c" width="14" height="14" viewBox="0 0 20 20" fill="currentColor"><rect x="7.5" y="2" width="5" height="16" rx="1"/><rect x="2" y="5" width="7" height="10" rx="1" opacity=".4" /><rect x="11" y="5" width="7" height="10" rx="1" opacity=".4"/></svg>,
              <svg key="r" width="14" height="14" viewBox="0 0 20 20" fill="currentColor"><rect x="13" y="2" width="5" height="16" rx="1"/><rect x="2" y="5" width="9" height="10" rx="1" opacity=".4"/></svg>,
              <svg key="t" width="14" height="14" viewBox="0 0 20 20" fill="currentColor"><rect x="2" y="2" width="16" height="5" rx="1"/><rect x="5" y="9" width="10" height="9" rx="1" opacity=".4"/></svg>,
            ]}
          />
        </PropRow>
        <PropRow label="Alignment" responsive>
          <AlignButtons
            value={str('fc_align') || 'center'}
            onChange={(v) => upS({ fc_align: v })}
            options={['left', 'center', 'right', 'justify']}
          />
        </PropRow>
        <SliderPropRow label="Icon Spacing" responsive
          value={str('fc_iconSpacing')} onChange={(v) => upS({ fc_iconSpacing: v })}
          min={0} max={60} step={1} units={['px']} placeholder="15" />
        <SliderPropRow label="Content Spacing" responsive
          value={str('fc_contentSpacing')} onChange={(v) => upS({ fc_contentSpacing: v })}
          min={0} max={60} step={1} units={['px']} placeholder="0" />
      </PropGroup>

      {/* ── Icon ── */}
      <PropGroup label="Icon" defaultOpen={false}>
        <ColorPropRow label="Color"      value={str('fc_icon_color')} onChange={(v) => upS({ fc_icon_color: v })} placeholder="#6366f1" />
        <ColorPropRow label="Background" value={str('fc_icon_bg')}    onChange={(v) => upS({ fc_icon_bg: v })}    placeholder="transparent" />
        <SliderPropRow label="Size" responsive
          value={str('fc_icon_size')} onChange={(v) => upS({ fc_icon_size: v })}
          min={10} max={100} step={1} units={['px']} placeholder="48" />
        <SliderPropRow label="Padding" responsive
          value={str('fc_icon_padding')} onChange={(v) => upS({ fc_icon_padding: v })}
          min={0} max={40} step={1} units={['px']} placeholder="12" />
        <SliderPropRow label="Border Radius" responsive
          value={str('fc_icon_br')} onChange={(v) => upS({ fc_icon_br: v })}
          min={0} max={50} step={1} units={['px', '%']} placeholder="12" />
      </PropGroup>

      {/* ── Title ── */}
      <PropGroup label="Title" defaultOpen={false}>
        <TypographyTriggerRow label="Typography" isOpen={titleTypoOpen} triggerRef={titleTypoRef} onToggle={() => setTitleTypoOpen((o) => !o)} />
        {titleTypoOpen && (
          <TypographyPopup styles={titleTypoStyles} update={updateTitleTypo} fonts={jalpiEditorConfig.fonts ?? []} respKey={null} onClose={() => setTitleTypoOpen(false)} anchorRef={titleTypoRef} />
        )}
        <ColorPropRow label="Color" value={str('fc_title_color')} onChange={(v) => upS({ fc_title_color: v })} placeholder="#111827" />
      </PropGroup>

      {/* ── Description ── */}
      <PropGroup label="Description" defaultOpen={false}>
        <TypographyTriggerRow label="Typography" isOpen={descTypoOpen} triggerRef={descTypoRef} onToggle={() => setDescTypoOpen((o) => !o)} />
        {descTypoOpen && (
          <TypographyPopup styles={descTypoStyles} update={updateDescTypo} fonts={jalpiEditorConfig.fonts ?? []} respKey={null} onClose={() => setDescTypoOpen(false)} anchorRef={descTypoRef} />
        )}
        <ColorPropRow label="Color" value={str('fc_desc_color')} onChange={(v) => upS({ fc_desc_color: v })} placeholder="#6b7280" />
      </PropGroup>

      {/* ── Box Style ── */}
      <PropGroup label="Box Style" defaultOpen={false}>
        <ColorPropRow label="Background" value={str('fc_bg')} onChange={(v) => upS({ fc_bg: v })} placeholder="#ffffff" />
        <PropRow label="Border Type">
          <Select value={bdType}
            options={[
              { value: 'none',   label: 'None'   },
              { value: 'solid',  label: 'Solid'  },
              { value: 'dashed', label: 'Dashed' },
              { value: 'dotted', label: 'Dotted' },
            ]}
            onChange={(v) => upS({ fc_bdType: v })} />
        </PropRow>
        {hasBorder && (
          <>
            <PropRow label="Border Width" responsive>
              <BoxSpacingControl
                values={{ top: str('fc_bdw_top'), right: str('fc_bdw_right'), bottom: str('fc_bdw_bottom'), left: str('fc_bdw_left') }}
                onChange={(side, v) => upS({ [`fc_bdw_${side}`]: v })}
                onChangeAll={(v) => upS({ fc_bdw_top: v, fc_bdw_right: v, fc_bdw_bottom: v, fc_bdw_left: v })}
              />
            </PropRow>
            <ColorPropRow label="Border Color" value={str('fc_bdColor')} onChange={(v) => upS({ fc_bdColor: v })} placeholder="#f3f4f6" />
          </>
        )}
        <PropRow label="Border Radius" responsive>
          <BoxSpacingControl
            values={{ top: str('fc_br_top'), right: str('fc_br_right'), bottom: str('fc_br_bottom'), left: str('fc_br_left') }}
            onChange={(side, v) => upS({ [`fc_br_${side}`]: v })}
            onChangeAll={(v) => upS({ fc_br_top: v, fc_br_right: v, fc_br_bottom: v, fc_br_left: v })}
          />
        </PropRow>
        <PropRow label="Padding" responsive>
          <BoxSpacingControl
            values={{ top: str('fc_pad_top'), right: str('fc_pad_right'), bottom: str('fc_pad_bottom'), left: str('fc_pad_left') }}
            onChange={(side, v) => upS({ [`fc_pad_${side}`]: v })}
            onChangeAll={(v) => upS({ fc_pad_top: v, fc_pad_right: v, fc_pad_bottom: v, fc_pad_left: v })}
          />
        </PropRow>
      </PropGroup>

    </div>
  );
}

function IconListStyleTab({ node, store }: { node: CBNode; store: EditorStore }) {
  const s   = (node.settings ?? {}) as Record<string, unknown>;
  const str = (k: string) => (s[k] as string) || '';
  const upS = (patch: Record<string, unknown>) => store.updateNode(node.id, { settings: { ...s, ...patch } });
  const showDivider = !!s['il_divider'];

  const [typoOpen, setTypoOpen] = useState(false);
  const typoRef = useRef<HTMLButtonElement>(null);

  // Map il_text_* settings to CSS-key object for TypographyPopup
  const typoStyles: Record<string, string> = {
    fontFamily:     str('il_text_fontFamily'),
    fontSize:       str('il_text_fontSize'),
    fontWeight:     str('il_text_fontWeight'),
    fontStyle:      str('il_text_fontStyle'),
    textTransform:  str('il_text_textTransform'),
    textDecoration: str('il_text_textDecoration'),
    lineHeight:     str('il_text_lineHeight'),
    letterSpacing:  str('il_text_letterSpacing'),
    wordSpacing:    str('il_text_wordSpacing'),
  };
  const updateTypo = (patch: Record<string, string>) => {
    const mapped: Record<string, unknown> = {};
    for (const [k, v] of Object.entries(patch)) mapped[`il_text_${k}`] = v;
    upS(mapped);
  };

  return (
    <div className="jalpi-props-section">

      {/* ── List ── */}
      <PropGroup label="List" defaultOpen>
        <SliderPropRow label="Space Between" responsive
          value={str('il_gap')} onChange={(v) => upS({ il_gap: v })}
          min={0} max={60} step={1} units={['px']} placeholder="8" />
        <PropRow label="Divider">
          <input type="checkbox" checked={showDivider} onChange={(e) => upS({ il_divider: e.target.checked })} />
        </PropRow>
        {showDivider && (
          <>
            <ColorPropRow label="Divider Color" value={str('il_div_color')} onChange={(v) => upS({ il_div_color: v })} placeholder="#e5e7eb" />
            <SliderPropRow label="Weight" responsive
              value={str('il_div_weight')} onChange={(v) => upS({ il_div_weight: v })}
              min={1} max={10} step={1} units={['px']} placeholder="1" />
            <PropRow label="Style" stack>
              <Select value={str('il_div_style') || 'solid'}
                options={[{ value: 'solid', label: 'Solid' }, { value: 'dashed', label: 'Dashed' }, { value: 'dotted', label: 'Dotted' }]}
                onChange={(v) => upS({ il_div_style: v })} />
            </PropRow>
          </>
        )}
      </PropGroup>

      {/* ── Icon ── */}
      <PropGroup label="Icon" defaultOpen={false}>
        <ColorPropRow label="Color"      value={str('il_icon_color')} onChange={(v) => upS({ il_icon_color: v })} placeholder="#6366f1" />
        <ColorPropRow label="Background" value={str('il_icon_bg')}    onChange={(v) => upS({ il_icon_bg: v })}    placeholder="transparent" />
        <SliderPropRow label="Size" responsive
          value={str('il_icon_size')} onChange={(v) => upS({ il_icon_size: v })}
          min={8} max={60} step={1} units={['px']} placeholder="14" />
        <SliderPropRow label="Border Radius" responsive
          value={str('il_icon_br')} onChange={(v) => upS({ il_icon_br: v })}
          min={0} max={50} step={1} units={['px', '%']} placeholder="0" />
        <SliderPropRow label="Spacing" responsive
          value={str('il_icon_spacing')} onChange={(v) => upS({ il_icon_spacing: v })}
          min={0} max={40} step={1} units={['px']} placeholder="8" />
      </PropGroup>

      {/* ── Text ── */}
      <PropGroup label="Text" defaultOpen={false}>
        <TypographyTriggerRow
          label="Typography"
          isOpen={typoOpen}
          triggerRef={typoRef}
          onToggle={() => setTypoOpen((o) => !o)}
        />
        {typoOpen && (
          <TypographyPopup
            styles={typoStyles}
            update={updateTypo}
            fonts={jalpiEditorConfig.fonts ?? []}
            respKey={null}
            onClose={() => setTypoOpen(false)}
            anchorRef={typoRef}
          />
        )}
        <ColorPropRow label="Color" value={str('il_text_color')} onChange={(v) => upS({ il_text_color: v })} placeholder="#374151" />
      </PropGroup>

    </div>
  );
}

/* ── CTA Style Tab ────────────────────────────────────────────────────────── */
function CTAStyleTab({ node, store }: { node: CBNode; store: EditorStore }) {
  const s   = (node.settings ?? {}) as Record<string, string>;
  const upS = (patch: Record<string, string>) =>
    store.updateNode(node.id, { settings: { ...s, ...patch } });
  const str = (k: string) => s[k] || '';

  const fonts     = jalpiEditorConfig.fonts ?? [];
  const TYPO_KEYS = ['fontFamily', 'fontSize', 'fontWeight', 'lineHeight', 'letterSpacing', 'textTransform'];
  const prefixStyles = (prefix: string) =>
    Object.fromEntries(TYPO_KEYS.map((k) => [k, str(`${prefix}_${k}`)]));
  const prefixUpdate = (prefix: string) => (patch: Record<string, string>) =>
    upS(Object.fromEntries(Object.entries(patch).map(([k, v]) => [`${prefix}_${k}`, v])));

  const BD_OPTS = [
    { value: 'none',   label: 'None'   },
    { value: 'solid',  label: 'Solid'  },
    { value: 'dashed', label: 'Dashed' },
    { value: 'dotted', label: 'Dotted' },
  ];

  const [wrapNH, setWrapNH] = React.useState<NHState>('normal');
  const [pbNH,   setPbNH]   = React.useState<NHState>('normal');
  const [sbNH,   setSbNH]   = React.useState<NHState>('normal');

  const [titleTypoOpen, setTitleTypoOpen] = React.useState(false);
  const [subTypoOpen,   setSubTypoOpen]   = React.useState(false);
  const [pbTypoOpen,    setPbTypoOpen]    = React.useState(false);
  const [sbTypoOpen,    setSbTypoOpen]    = React.useState(false);
  const [wrapShadowOpen, setWrapShadowOpen] = React.useState(false);
  const [pbShadowOpen,   setPbShadowOpen]   = React.useState(false);
  const [sbShadowOpen,   setSbShadowOpen]   = React.useState(false);
  const titleTypoRef    = React.useRef<HTMLButtonElement>(null);
  const subTypoRef      = React.useRef<HTMLButtonElement>(null);
  const pbTypoRef       = React.useRef<HTMLButtonElement>(null);
  const sbTypoRef       = React.useRef<HTMLButtonElement>(null);
  const wrapShadowRef   = React.useRef<HTMLButtonElement>(null);
  const pbShadowRef     = React.useRef<HTMLButtonElement>(null);
  const sbShadowRef     = React.useRef<HTMLButtonElement>(null);
  const closeAll = () => {
    setTitleTypoOpen(false); setSubTypoOpen(false);
    setPbTypoOpen(false);    setSbTypoOpen(false);
    setWrapShadowOpen(false); setPbShadowOpen(false); setSbShadowOpen(false);
  };

  const bdType   = str('cta_bd_type')      || 'none';
  const pbBdType = str('cta_pbtn_bd_type') || 'none';
  const sbBdType = str('cta_sbtn_bd_type') || 'solid';

  return (
    <div className="jalpi-props-section">

      {/* ── Wrapper ── */}
      <PropGroup label="Wrapper" defaultOpen>
        <NormalHoverTabs state={wrapNH} onChange={setWrapNH} />
        <ColorPropRow
          label="Background"
          value={str(wrapNH === 'normal' ? 'cta_bg' : 'cta_bg_hover')}
          onChange={(v) => upS(wrapNH === 'normal' ? { cta_bg: v } : { cta_bg_hover: v })}
          placeholder={wrapNH === 'normal' ? '#1a1a1a' : ''}
        />
        {bdType !== 'none' && (
          <ColorPropRow
            label="Border Color"
            value={str(wrapNH === 'normal' ? 'cta_bd_color' : 'cta_bd_color_hover')}
            onChange={(v) => upS(wrapNH === 'normal' ? { cta_bd_color: v } : { cta_bd_color_hover: v })}
            placeholder="#e5e7eb"
          />
        )}
        {wrapNH === 'normal' && (
          <>
            <PropRow label="Padding" responsive>
              <BoxSpacingControl
                values={{ top: str('cta_pad_top'), right: str('cta_pad_right'), bottom: str('cta_pad_bottom'), left: str('cta_pad_left') }}
                onChange={(side, v) => upS({ [`cta_pad_${side}`]: v })}
                onChangeAll={(v) => upS({ cta_pad_top: v, cta_pad_right: v, cta_pad_bottom: v, cta_pad_left: v })}
              />
            </PropRow>
            <PropRow label="Border Radius" responsive>
              <BoxSpacingControl
                values={{ top: str('cta_br_top'), right: str('cta_br_right'), bottom: str('cta_br_bottom'), left: str('cta_br_left') }}
                onChange={(side, v) => upS({ [`cta_br_${side}`]: v })}
                onChangeAll={(v) => upS({ cta_br_top: v, cta_br_right: v, cta_br_bottom: v, cta_br_left: v })}
              />
            </PropRow>
            <PropRow label="Border Type">
              <Select value={bdType} options={BD_OPTS} onChange={(v) => upS({ cta_bd_type: v })} />
            </PropRow>
            {bdType !== 'none' && (
              <PropRow label="Border Width" responsive>
                <BoxSpacingControl
                  values={{ top: str('cta_bdw_top'), right: str('cta_bdw_right'), bottom: str('cta_bdw_bottom'), left: str('cta_bdw_left') }}
                  onChange={(side, v) => upS({ [`cta_bdw_${side}`]: v })}
                  onChangeAll={(v) => upS({ cta_bdw_top: v, cta_bdw_right: v, cta_bdw_bottom: v, cta_bdw_left: v })}
                />
              </PropRow>
            )}
            <TypographyTriggerRow label="Box Shadow" isOpen={wrapShadowOpen} triggerRef={wrapShadowRef}
              onToggle={() => { closeAll(); setWrapShadowOpen((o) => !o); }} />
            {wrapShadowOpen && ReactDOM.createPortal(
              <ImgBoxShadowPopup value={str('cta_shadow')} onChange={(v) => upS({ cta_shadow: v })}
                onClose={() => setWrapShadowOpen(false)} anchorRef={wrapShadowRef} />,
              document.body
            )}
          </>
        )}
      </PropGroup>

      {/* ── Alignment ── */}
      <PropGroup label="Alignment" defaultOpen={false}>
        <PropRow label="Text Align">
          <AlignButtons value={str('cta_align') || 'center'} options={['left', 'center', 'right']} onChange={(v) => upS({ cta_align: v })} />
        </PropRow>
        <PropRow label="Buttons">
          <AlignButtons value={str('cta_btn_align') || 'center'} options={['left', 'center', 'right']} onChange={(v) => upS({ cta_btn_align: v })} />
        </PropRow>
      </PropGroup>

      {/* ── Title ── */}
      <PropGroup label="Title" defaultOpen={false}>
        <ColorPropRow label="Text Color" value={str('cta_title_color')} onChange={(v) => upS({ cta_title_color: v })} placeholder="#ffffff" />
        <TypographyTriggerRow label="Typography" isOpen={titleTypoOpen} triggerRef={titleTypoRef}
          onToggle={() => { closeAll(); setTitleTypoOpen((o) => !o); }} />
        {titleTypoOpen && (
          <TypographyPopup
            styles={prefixStyles('cta_title')} update={prefixUpdate('cta_title')}
            fonts={fonts} respKey={null} onClose={() => setTitleTypoOpen(false)} anchorRef={titleTypoRef}
          />
        )}
      </PropGroup>

      {/* ── Subtitle ── */}
      <PropGroup label="Subtitle" defaultOpen={false}>
        <ColorPropRow label="Text Color" value={str('cta_sub_color')} onChange={(v) => upS({ cta_sub_color: v })} placeholder="rgba(255,255,255,0.8)" />
        <TypographyTriggerRow label="Typography" isOpen={subTypoOpen} triggerRef={subTypoRef}
          onToggle={() => { closeAll(); setSubTypoOpen((o) => !o); }} />
        {subTypoOpen && (
          <TypographyPopup
            styles={prefixStyles('cta_sub')} update={prefixUpdate('cta_sub')}
            fonts={fonts} respKey={null} onClose={() => setSubTypoOpen(false)} anchorRef={subTypoRef}
          />
        )}
      </PropGroup>

      {/* ── Primary Button ── */}
      <PropGroup label="Primary Button" defaultOpen={false}>
        <NormalHoverTabs state={pbNH} onChange={setPbNH} />
        <ColorPropRow
          label="Background"
          value={str(pbNH === 'normal' ? 'cta_pbtn_bg' : 'cta_pbtn_bg_hover')}
          onChange={(v) => upS(pbNH === 'normal' ? { cta_pbtn_bg: v } : { cta_pbtn_bg_hover: v })}
          placeholder={pbNH === 'normal' ? '#ffffff' : ''}
        />
        <ColorPropRow
          label="Text Color"
          value={str(pbNH === 'normal' ? 'cta_pbtn_color' : 'cta_pbtn_color_hover')}
          onChange={(v) => upS(pbNH === 'normal' ? { cta_pbtn_color: v } : { cta_pbtn_color_hover: v })}
          placeholder={pbNH === 'normal' ? '#1a1a1a' : ''}
        />
        {pbBdType !== 'none' && (
          <ColorPropRow
            label="Border Color"
            value={str(pbNH === 'normal' ? 'cta_pbtn_bd_color' : 'cta_pbtn_bd_color_hover')}
            onChange={(v) => upS(pbNH === 'normal' ? { cta_pbtn_bd_color: v } : { cta_pbtn_bd_color_hover: v })}
            placeholder=""
          />
        )}
        {pbNH === 'normal' && (
          <>
            <PropRow label="Border Type">
              <Select value={pbBdType} options={BD_OPTS} onChange={(v) => upS({ cta_pbtn_bd_type: v })} />
            </PropRow>
            {pbBdType !== 'none' && (
              <PropRow label="Border Width" responsive>
                <BoxSpacingControl
                  values={{ top: str('cta_pbtn_bdw_top'), right: str('cta_pbtn_bdw_right'), bottom: str('cta_pbtn_bdw_bottom'), left: str('cta_pbtn_bdw_left') }}
                  onChange={(side, v) => upS({ [`cta_pbtn_bdw_${side}`]: v })}
                  onChangeAll={(v) => upS({ cta_pbtn_bdw_top: v, cta_pbtn_bdw_right: v, cta_pbtn_bdw_bottom: v, cta_pbtn_bdw_left: v })}
                />
              </PropRow>
            )}
            <PropRow label="Border Radius" responsive>
              <BoxSpacingControl
                values={{ top: str('cta_pbtn_br_top'), right: str('cta_pbtn_br_right'), bottom: str('cta_pbtn_br_bottom'), left: str('cta_pbtn_br_left') }}
                onChange={(side, v) => upS({ [`cta_pbtn_br_${side}`]: v })}
                onChangeAll={(v) => upS({ cta_pbtn_br_top: v, cta_pbtn_br_right: v, cta_pbtn_br_bottom: v, cta_pbtn_br_left: v })}
              />
            </PropRow>
            <PropRow label="Padding" responsive>
              <BoxSpacingControl
                values={{ top: str('cta_pbtn_pad_top'), right: str('cta_pbtn_pad_right'), bottom: str('cta_pbtn_pad_bottom'), left: str('cta_pbtn_pad_left') }}
                onChange={(side, v) => upS({ [`cta_pbtn_pad_${side}`]: v })}
                onChangeAll={(v) => upS({ cta_pbtn_pad_top: v, cta_pbtn_pad_right: v, cta_pbtn_pad_bottom: v, cta_pbtn_pad_left: v })}
              />
            </PropRow>
            <TypographyTriggerRow label="Box Shadow" isOpen={pbShadowOpen} triggerRef={pbShadowRef}
              onToggle={() => { closeAll(); setPbShadowOpen((o) => !o); }} />
            {pbShadowOpen && ReactDOM.createPortal(
              <ImgBoxShadowPopup value={str('cta_pbtn_shadow')} onChange={(v) => upS({ cta_pbtn_shadow: v })}
                onClose={() => setPbShadowOpen(false)} anchorRef={pbShadowRef} />,
              document.body
            )}
            <TypographyTriggerRow label="Typography" isOpen={pbTypoOpen} triggerRef={pbTypoRef}
              onToggle={() => { closeAll(); setPbTypoOpen((o) => !o); }} />
            {pbTypoOpen && (
              <TypographyPopup
                styles={prefixStyles('cta_pbtn')} update={prefixUpdate('cta_pbtn')}
                fonts={fonts} respKey={null} onClose={() => setPbTypoOpen(false)} anchorRef={pbTypoRef}
              />
            )}
          </>
        )}
      </PropGroup>

      {/* ── Secondary Button ── */}
      <PropGroup label="Secondary Button" defaultOpen={false}>
        <NormalHoverTabs state={sbNH} onChange={setSbNH} />
        <ColorPropRow
          label="Background"
          value={str(sbNH === 'normal' ? 'cta_sbtn_bg' : 'cta_sbtn_bg_hover')}
          onChange={(v) => upS(sbNH === 'normal' ? { cta_sbtn_bg: v } : { cta_sbtn_bg_hover: v })}
          placeholder={sbNH === 'normal' ? 'rgba(255,255,255,0.12)' : ''}
        />
        <ColorPropRow
          label="Text Color"
          value={str(sbNH === 'normal' ? 'cta_sbtn_color' : 'cta_sbtn_color_hover')}
          onChange={(v) => upS(sbNH === 'normal' ? { cta_sbtn_color: v } : { cta_sbtn_color_hover: v })}
          placeholder={sbNH === 'normal' ? '#ffffff' : ''}
        />
        {sbBdType !== 'none' && (
          <ColorPropRow
            label="Border Color"
            value={str(sbNH === 'normal' ? 'cta_sbtn_bd_color' : 'cta_sbtn_bd_color_hover')}
            onChange={(v) => upS(sbNH === 'normal' ? { cta_sbtn_bd_color: v } : { cta_sbtn_bd_color_hover: v })}
            placeholder="rgba(255,255,255,0.3)"
          />
        )}
        {sbNH === 'normal' && (
          <>
            <PropRow label="Border Type">
              <Select value={sbBdType} options={BD_OPTS} onChange={(v) => upS({ cta_sbtn_bd_type: v })} />
            </PropRow>
            {sbBdType !== 'none' && (
              <PropRow label="Border Width" responsive>
                <BoxSpacingControl
                  values={{ top: str('cta_sbtn_bdw_top'), right: str('cta_sbtn_bdw_right'), bottom: str('cta_sbtn_bdw_bottom'), left: str('cta_sbtn_bdw_left') }}
                  onChange={(side, v) => upS({ [`cta_sbtn_bdw_${side}`]: v })}
                  onChangeAll={(v) => upS({ cta_sbtn_bdw_top: v, cta_sbtn_bdw_right: v, cta_sbtn_bdw_bottom: v, cta_sbtn_bdw_left: v })}
                />
              </PropRow>
            )}
            <PropRow label="Border Radius" responsive>
              <BoxSpacingControl
                values={{ top: str('cta_sbtn_br_top'), right: str('cta_sbtn_br_right'), bottom: str('cta_sbtn_br_bottom'), left: str('cta_sbtn_br_left') }}
                onChange={(side, v) => upS({ [`cta_sbtn_br_${side}`]: v })}
                onChangeAll={(v) => upS({ cta_sbtn_br_top: v, cta_sbtn_br_right: v, cta_sbtn_br_bottom: v, cta_sbtn_br_left: v })}
              />
            </PropRow>
            <PropRow label="Padding" responsive>
              <BoxSpacingControl
                values={{ top: str('cta_sbtn_pad_top'), right: str('cta_sbtn_pad_right'), bottom: str('cta_sbtn_pad_bottom'), left: str('cta_sbtn_pad_left') }}
                onChange={(side, v) => upS({ [`cta_sbtn_pad_${side}`]: v })}
                onChangeAll={(v) => upS({ cta_sbtn_pad_top: v, cta_sbtn_pad_right: v, cta_sbtn_pad_bottom: v, cta_sbtn_pad_left: v })}
              />
            </PropRow>
            <TypographyTriggerRow label="Box Shadow" isOpen={sbShadowOpen} triggerRef={sbShadowRef}
              onToggle={() => { closeAll(); setSbShadowOpen((o) => !o); }} />
            {sbShadowOpen && ReactDOM.createPortal(
              <ImgBoxShadowPopup value={str('cta_sbtn_shadow')} onChange={(v) => upS({ cta_sbtn_shadow: v })}
                onClose={() => setSbShadowOpen(false)} anchorRef={sbShadowRef} />,
              document.body
            )}
            <TypographyTriggerRow label="Typography" isOpen={sbTypoOpen} triggerRef={sbTypoRef}
              onToggle={() => { closeAll(); setSbTypoOpen((o) => !o); }} />
            {sbTypoOpen && (
              <TypographyPopup
                styles={prefixStyles('cta_sbtn')} update={prefixUpdate('cta_sbtn')}
                fonts={fonts} respKey={null} onClose={() => setSbTypoOpen(false)} anchorRef={sbTypoRef}
              />
            )}
          </>
        )}
      </PropGroup>

    </div>
  );
}

/* ── Tabs (Advanced) Style tab — Elementor-style tab-button styling ──────────
   Stores namespaced keys in node.settings (e.g. tab_gap, tab_normal_bg,
   tab_hover_borderColor, tab_active_padding_top). The PHP element emits scoped
   CSS for `.jalpi-tabs__btn` (normal), `:hover`, and `.is-active` from these. */
/* =========================================================
   MENU — Style tab.
   Typography, colours, background/border/shadow, spacing and dropdown
   styling. Every value is re-validated in Jalpi_Element_Menu before it
   reaches CSS.
   ========================================================= */

/**
 * Advanced Menu — Style tab.
 *
 * Every key here is read by Jalpi_Element_Adv_Menu::emit_css() and persisted by
 * the sanitizer's 'adv-menu' settings case; the three must stay in step or a
 * control silently does nothing.
 */
function AdvMenuStyleTab({ node, store }: { node: CBNode; store: EditorStore }) {
  const s = (node.settings ?? {}) as Record<string, unknown>;
  const upd = (patch: Record<string, unknown>) => store.updateNodeSettings(node.id, patch);
  const str = (k: string): string => {
    const v = s[k];
    return typeof v === 'string' ? v : typeof v === 'number' ? String(v) : '';
  };

  const [state, setState] = useState<'normal' | 'hover' | 'active'>('normal');
  const colorKey = state === 'normal' ? 'am_color' : state === 'hover' ? 'am_color_hover' : 'am_color_active';
  const bgKey = state === 'normal' ? 'am_bg' : state === 'hover' ? 'am_bg_hover' : 'am_bg_active';

  // Shadows are stored as parts (x/y/blur/spread/color) and reassembled by the
  // element, so no raw CSS string is ever accepted.
  const shadowOf = (key: string) => (s[key] ?? {}) as Record<string, unknown>;
  const setShadow = (key: string, patch: Record<string, unknown>) =>
    upd({ [key]: { ...shadowOf(key), ...patch } });
  const shadowNum = (key: string, part: string): string => {
    const v = shadowOf(key)[part];
    return typeof v === 'number' ? String(v) : typeof v === 'string' ? v : '';
  };

  const ShadowRows = ({ k }: { k: string }) => (
    <>
      <PropRow label="Shadow Color">
        <ColorInput value={String(shadowOf(k).color ?? '')} onChange={(v) => setShadow(k, { color: v })} />
      </PropRow>
      <SliderPropRow label="Offset X" value={shadowNum(k, 'x')} onChange={(v) => setShadow(k, { x: parseInt(v, 10) || 0 })} min={-50} max={50} />
      <SliderPropRow label="Offset Y" value={shadowNum(k, 'y')} onChange={(v) => setShadow(k, { y: parseInt(v, 10) || 0 })} min={-50} max={50} />
      <SliderPropRow label="Blur" value={shadowNum(k, 'blur')} onChange={(v) => setShadow(k, { blur: parseInt(v, 10) || 0 })} min={0} max={100} />
      <SliderPropRow label="Spread" value={shadowNum(k, 'spread')} onChange={(v) => setShadow(k, { spread: parseInt(v, 10) || 0 })} min={-50} max={50} />
    </>
  );

  const BorderRows = ({ p }: { p: string }) => (
    <>
      <PropRow label="Border Style">
        <Select
          value={str(`${p}_style`) || 'none'}
          options={[
            { value: 'none', label: 'None' },
            { value: 'solid', label: 'Solid' },
            { value: 'dashed', label: 'Dashed' },
            { value: 'dotted', label: 'Dotted' },
            { value: 'double', label: 'Double' },
          ]}
          onChange={(v) => upd({ [`${p}_style`]: v })}
        />
      </PropRow>
      {str(`${p}_style`) && str(`${p}_style`) !== 'none' && (
        <>
          <SliderPropRow label="Border Width" value={str(`${p}_width`)} onChange={(v) => upd({ [`${p}_width`]: v })} min={0} max={12} />
          <PropRow label="Border Color">
            <ColorInput value={str(`${p}_color`)} onChange={(v) => upd({ [`${p}_color`]: v })} />
          </PropRow>
        </>
      )}
    </>
  );

  return (
    <div className="jalpi-props-section">
      <PropGroup label="Menu Items" defaultOpen standalone>
        <PropRow label="Font Family">
          <TextInput value={str('am_font_family')} placeholder="inherit" onChange={(v) => upd({ am_font_family: v })} />
        </PropRow>
        <SliderPropRow label="Font Size" value={str('am_font_size')} onChange={(v) => upd({ am_font_size: v })} min={8} max={48} />
        <PropRow label="Font Weight">
          <Select
            value={str('am_font_weight') || ''}
            options={[
              { value: '', label: 'Default' },
              { 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)' },
            ]}
            onChange={(v) => upd({ am_font_weight: v })}
          />
        </PropRow>
        <SliderPropRow label="Line Height" value={str('am_line_height')} onChange={(v) => upd({ am_line_height: v })} min={0} max={4} step={0.1} />
        <SliderPropRow label="Letter Spacing" value={str('am_letter_spacing')} onChange={(v) => upd({ am_letter_spacing: v })} min={-5} max={20} step={0.1} />
        <PropRow label="Transform">
          <Select
            value={str('am_text_transform') || 'none'}
            options={[
              { value: 'none', label: 'None' },
              { value: 'uppercase', label: 'UPPERCASE' },
              { value: 'lowercase', label: 'lowercase' },
              { value: 'capitalize', label: 'Capitalize' },
            ]}
            onChange={(v) => upd({ am_text_transform: v })}
          />
        </PropRow>
        <PropRow label="Padding">
          <TextInput value={str('am_padding')} placeholder="10px 14px" onChange={(v) => upd({ am_padding: v })} />
        </PropRow>
        <PropRow label="Margin">
          <TextInput value={str('am_margin')} placeholder="0" onChange={(v) => upd({ am_margin: v })} />
        </PropRow>
        <SliderPropRow label="Item Gap" value={str('am_gap')} onChange={(v) => upd({ am_gap: v })} min={0} max={80} />
        <PropRow label="Border Radius">
          <TextInput value={str('am_radius')} placeholder="0" onChange={(v) => upd({ am_radius: v })} />
        </PropRow>
        <BorderRows p="am_border" />
      </PropGroup>

      <PropGroup label="Item Colors" defaultOpen standalone>
        <div style={{ display: 'flex', gap: 4, marginBottom: 10 }}>
          {(['normal', 'hover', 'active'] as const).map((st) => (
            <button
              key={st}
              type="button"
              className="jalpi-gs-add-btn"
              style={{
                flex: 1,
                background: state === st ? '#2271b1' : undefined,
                color: state === st ? '#fff' : undefined,
                textTransform: 'capitalize',
              }}
              onClick={() => setState(st)}
            >
              {st}
            </button>
          ))}
        </div>
        <PropRow label="Text Color">
          <ColorInput value={str(colorKey)} onChange={(v) => upd({ [colorKey]: v })} />
        </PropRow>
        <PropRow label="Background">
          <ColorInput value={str(bgKey)} onChange={(v) => upd({ [bgKey]: v })} />
        </PropRow>
        <ShadowRows k="am_shadow" />
      </PropGroup>

      <PropGroup label="Mega Panel" defaultOpen={false} standalone>
        <PropRow label="Background Type">
          <Select
            value={str('am_panel_bg_type') || 'classic'}
            options={[
              { value: 'classic', label: 'Classic' },
              { value: 'gradient', label: 'Gradient' },
            ]}
            onChange={(v) => upd({ am_panel_bg_type: v })}
          />
        </PropRow>
        {str('am_panel_bg_type') === 'gradient' ? (
          <>
            <PropRow label="Gradient Color 1">
              <ColorInput value={str('am_panel_grad_color1')} onChange={(v) => upd({ am_panel_grad_color1: v })} />
            </PropRow>
            <PropRow label="Gradient Color 2">
              <ColorInput value={str('am_panel_grad_color2')} onChange={(v) => upd({ am_panel_grad_color2: v })} />
            </PropRow>
            <SliderPropRow label="Angle" value={str('am_panel_grad_angle')} onChange={(v) => upd({ am_panel_grad_angle: v })} min={0} max={360} />
          </>
        ) : (
          <PropRow label="Background">
            <ColorInput value={str('am_panel_bg')} onChange={(v) => upd({ am_panel_bg: v })} />
          </PropRow>
        )}
        <PropRow label="Overlay">
          <ColorInput value={str('am_panel_overlay')} onChange={(v) => upd({ am_panel_overlay: v })} />
        </PropRow>
        <PropRow label="Padding">
          <TextInput value={str('am_panel_padding')} placeholder="24px" onChange={(v) => upd({ am_panel_padding: v })} />
        </PropRow>
        <PropRow label="Margin">
          <TextInput value={str('am_panel_margin')} placeholder="0" onChange={(v) => upd({ am_panel_margin: v })} />
        </PropRow>
        <PropRow label="Border Radius">
          <TextInput value={str('am_panel_radius')} placeholder="10px" onChange={(v) => upd({ am_panel_radius: v })} />
        </PropRow>
        <BorderRows p="am_panel_border" />
        <ShadowRows k="am_panel_shadow" />
      </PropGroup>

      <PropGroup label="Columns" defaultOpen={false} standalone>
        <SliderPropRow label="Column Gap" value={str('am_col_gap')} onChange={(v) => upd({ am_col_gap: v })} min={0} max={80} />
        <PropRow label="Alignment">
          <Select
            value={str('am_col_align') || 'left'}
            options={[
              { value: 'left', label: 'Left' },
              { value: 'center', label: 'Center' },
              { value: 'right', label: 'Right' },
            ]}
            onChange={(v) => upd({ am_col_align: v })}
          />
        </PropRow>
        <SliderPropRow label="Divider Width" value={str('am_col_divider_width')} onChange={(v) => upd({ am_col_divider_width: v })} min={0} max={10} />
        <PropRow label="Divider Color">
          <ColorInput value={str('am_col_divider_color')} onChange={(v) => upd({ am_col_divider_color: v })} />
        </PropRow>
      </PropGroup>

      <PropGroup label="Panel Headings" defaultOpen={false} standalone>
        <PropRow label="Font Family">
          <TextInput value={str('am_heading_font_family')} placeholder="inherit" onChange={(v) => upd({ am_heading_font_family: v })} />
        </PropRow>
        <SliderPropRow label="Font Size" value={str('am_heading_font_size')} onChange={(v) => upd({ am_heading_font_size: v })} min={8} max={48} />
        <PropRow label="Font Weight">
          <Select
            value={str('am_heading_font_weight') || ''}
            options={[
              { value: '', label: 'Default' },
              { value: '400', label: 'Normal (400)' },
              { value: '500', label: 'Medium (500)' },
              { value: '600', label: 'Semi Bold (600)' },
              { value: '700', label: 'Bold (700)' },
            ]}
            onChange={(v) => upd({ am_heading_font_weight: v })}
          />
        </PropRow>
        <PropRow label="Color">
          <ColorInput value={str('am_heading_color')} onChange={(v) => upd({ am_heading_color: v })} />
        </PropRow>
        <PropRow label="Margin">
          <TextInput value={str('am_heading_margin')} placeholder="0 0 8px" onChange={(v) => upd({ am_heading_margin: v })} />
        </PropRow>
      </PropGroup>

      <PropGroup label="Panel Links" defaultOpen={false} standalone>
        <PropRow label="Font Family">
          <TextInput value={str('am_plink_font_family')} placeholder="inherit" onChange={(v) => upd({ am_plink_font_family: v })} />
        </PropRow>
        <SliderPropRow label="Font Size" value={str('am_plink_font_size')} onChange={(v) => upd({ am_plink_font_size: v })} min={8} max={36} />
        <PropRow label="Font Weight">
          <Select
            value={str('am_plink_font_weight') || ''}
            options={[
              { value: '', label: 'Default' },
              { value: '400', label: 'Normal (400)' },
              { value: '500', label: 'Medium (500)' },
              { value: '600', label: 'Semi Bold (600)' },
              { value: '700', label: 'Bold (700)' },
            ]}
            onChange={(v) => upd({ am_plink_font_weight: v })}
          />
        </PropRow>
        <PropRow label="Color">
          <ColorInput value={str('am_plink_color')} onChange={(v) => upd({ am_plink_color: v })} />
        </PropRow>
        <PropRow label="Hover Color">
          <ColorInput value={str('am_plink_color_hover')} onChange={(v) => upd({ am_plink_color_hover: v })} />
        </PropRow>
      </PropGroup>

      <PropGroup label="Panel Icons" defaultOpen={false} standalone>
        <SliderPropRow label="Size" value={str('am_icon_size')} onChange={(v) => upd({ am_icon_size: v })} min={8} max={64} />
        <PropRow label="Color">
          <ColorInput value={str('am_icon_color')} onChange={(v) => upd({ am_icon_color: v })} />
        </PropRow>
        <PropRow label="Hover Color">
          <ColorInput value={str('am_icon_color_hover')} onChange={(v) => upd({ am_icon_color_hover: v })} />
        </PropRow>
        <SliderPropRow label="Spacing" value={str('am_icon_spacing')} onChange={(v) => upd({ am_icon_spacing: v })} min={0} max={40} />
      </PropGroup>

      <PropGroup label="Panel Images" defaultOpen={false} standalone>
        <PropRow label="Border Radius">
          <TextInput value={str('am_img_radius')} placeholder="8px" onChange={(v) => upd({ am_img_radius: v })} />
        </PropRow>
        <PropRow label="Hover Effect">
          <Select
            value={str('am_img_hover') || 'none'}
            options={[
              { value: 'none', label: 'None' },
              { value: 'zoom', label: 'Zoom' },
              { value: 'lift', label: 'Lift' },
              { value: 'fade', label: 'Fade' },
            ]}
            onChange={(v) => upd({ am_img_hover: v })}
          />
        </PropRow>
        <ShadowRows k="am_img_shadow" />
      </PropGroup>

      <PropGroup label="Panel Buttons" defaultOpen={false} standalone>
        <PropRow label="Font Family">
          <TextInput value={str('am_btn_font_family')} placeholder="inherit" onChange={(v) => upd({ am_btn_font_family: v })} />
        </PropRow>
        <SliderPropRow label="Font Size" value={str('am_btn_font_size')} onChange={(v) => upd({ am_btn_font_size: v })} min={8} max={36} />
        <PropRow label="Font Weight">
          <Select
            value={str('am_btn_font_weight') || ''}
            options={[
              { value: '', label: 'Default' },
              { value: '400', label: 'Normal (400)' },
              { value: '500', label: 'Medium (500)' },
              { value: '600', label: 'Semi Bold (600)' },
              { value: '700', label: 'Bold (700)' },
            ]}
            onChange={(v) => upd({ am_btn_font_weight: v })}
          />
        </PropRow>
        <PropRow label="Text Color">
          <ColorInput value={str('am_btn_color')} onChange={(v) => upd({ am_btn_color: v })} />
        </PropRow>
        <PropRow label="Background">
          <ColorInput value={str('am_btn_bg')} onChange={(v) => upd({ am_btn_bg: v })} />
        </PropRow>
        <PropRow label="Hover Text Color">
          <ColorInput value={str('am_btn_color_hover')} onChange={(v) => upd({ am_btn_color_hover: v })} />
        </PropRow>
        <PropRow label="Hover Background">
          <ColorInput value={str('am_btn_bg_hover')} onChange={(v) => upd({ am_btn_bg_hover: v })} />
        </PropRow>
        <PropRow label="Border Radius">
          <TextInput value={str('am_btn_radius')} placeholder="6px" onChange={(v) => upd({ am_btn_radius: v })} />
        </PropRow>
        <BorderRows p="am_btn_border" />
      </PropGroup>

      <PropGroup label="Dropdown (non-mega)" defaultOpen={false} standalone>
        <PropRow label="Background">
          <ColorInput value={str('am_sub_bg')} onChange={(v) => upd({ am_sub_bg: v })} />
        </PropRow>
        <PropRow label="Text Color">
          <ColorInput value={str('am_sub_color')} onChange={(v) => upd({ am_sub_color: v })} />
        </PropRow>
        <SliderPropRow label="Min Width" value={str('am_sub_width')} onChange={(v) => upd({ am_sub_width: v })} min={80} max={600} />
        <PropRow label="Border Radius">
          <TextInput value={str('am_sub_radius')} placeholder="8px" onChange={(v) => upd({ am_sub_radius: v })} />
        </PropRow>
        <PropRow label="Item Padding">
          <TextInput value={str('am_sub_padding')} placeholder="9px 16px" onChange={(v) => upd({ am_sub_padding: v })} />
        </PropRow>
      </PropGroup>

      <PropGroup label="Hamburger" defaultOpen={false} standalone>
        <SliderPropRow label="Size" value={str('am_burger_size')} onChange={(v) => upd({ am_burger_size: v })} min={12} max={64} />
        <PropRow label="Color">
          <ColorInput value={str('am_burger_color')} onChange={(v) => upd({ am_burger_color: v })} />
        </PropRow>
        <PropRow label="Hover Color">
          <ColorInput value={str('am_burger_color_hover')} onChange={(v) => upd({ am_burger_color_hover: v })} />
        </PropRow>
      </PropGroup>
    </div>
  );
}

function MenuStyleTab({ node, store }: { node: CBNode; store: EditorStore }) {
  const s = (node.settings ?? {}) as Record<string, unknown>;
  const str = (k: string) => (s[k] as string) || '';
  const upd = (patch: Record<string, unknown>) =>
    store.updateNode(node.id, { settings: { ...s, ...patch } });

  const [state, setState] = useState<'normal' | 'hover' | 'active'>('normal');

  // One control per state, swapped by the segmented buttons, so the panel
  // stays short instead of listing three colour rows at once.
  const colorKey = state === 'normal' ? 'menu_color'
    : state === 'hover' ? 'menu_color_hover'
    : 'menu_color_active';
  const bgKey = state === 'normal' ? 'menu_bg'
    : state === 'hover' ? 'menu_bg_hover'
    : 'menu_bg_active';

  const shadow = (s.menu_shadow ?? {}) as Record<string, unknown>;
  const updShadow = (patch: Record<string, unknown>) =>
    upd({ menu_shadow: { ...shadow, ...patch } });

  return (
    <div className="jalpi-props-section">
      <PropGroup label="Typography" defaultOpen standalone>
        <SliderPropRow
          label="Font Size"
          value={str('menu_font_size')}
          onChange={(v) => upd({ menu_font_size: v })}
          min={8}
          max={72}
        />
        <PropRow label="Weight">
          <Select
            value={str('menu_font_weight') || '400'}
            options={[
              { value: '300', label: 'Light (300)' },
              { value: '400', label: 'Normal (400)' },
              { value: '500', label: 'Medium (500)' },
              { value: '600', label: 'Semibold (600)' },
              { value: '700', label: 'Bold (700)' },
            ]}
            onChange={(v) => upd({ menu_font_weight: v })}
          />
        </PropRow>
        <PropRow label="Transform">
          <Select
            value={str('menu_text_transform') || 'none'}
            options={[
              { value: 'none', label: 'None' },
              { value: 'uppercase', label: 'UPPERCASE' },
              { value: 'lowercase', label: 'lowercase' },
              { value: 'capitalize', label: 'Capitalize' },
            ]}
            onChange={(v) => upd({ menu_text_transform: v })}
          />
        </PropRow>
        <SliderPropRow
          label="Letter Spacing"
          value={str('menu_letter_spacing')}
          onChange={(v) => upd({ menu_letter_spacing: v })}
          min={-5}
          max={20}
          step={0.1}
        />
      </PropGroup>

      <PropGroup label="Colors" defaultOpen standalone>
        <div style={{ display: 'flex', gap: 4, marginBottom: 10 }}>
          {(['normal', 'hover', 'active'] as const).map((st) => (
            <button
              key={st}
              type="button"
              className="jalpi-gs-add-btn"
              style={{
                flex: 1,
                background: state === st ? '#2271b1' : undefined,
                color: state === st ? '#fff' : undefined,
                textTransform: 'capitalize',
              }}
              onClick={() => setState(st)}
            >
              {st}
            </button>
          ))}
        </div>

        <PropRow label="Text Color">
          <ColorInput value={str(colorKey)} onChange={(v) => upd({ [colorKey]: v })} />
        </PropRow>
        <PropRow label="Background">
          <ColorInput value={str(bgKey)} onChange={(v) => upd({ [bgKey]: v })} />
        </PropRow>
      </PropGroup>

      <PropGroup label="Border & Shadow" defaultOpen={false} standalone>
        <PropRow label="Border Style">
          <Select
            value={str('menu_border_style') || 'none'}
            options={[
              { value: 'none', label: 'None' },
              { value: 'solid', label: 'Solid' },
              { value: 'dashed', label: 'Dashed' },
              { value: 'dotted', label: 'Dotted' },
              { value: 'double', label: 'Double' },
            ]}
            onChange={(v) => upd({ menu_border_style: v })}
          />
        </PropRow>
        {str('menu_border_style') && str('menu_border_style') !== 'none' && (
          <>
            <SliderPropRow
              label="Border Width"
              value={str('menu_border_width')}
              onChange={(v) => upd({ menu_border_width: v })}
              min={0}
              max={12}
            />
            <PropRow label="Border Color">
              <ColorInput
                value={str('menu_border_color')}
                onChange={(v) => upd({ menu_border_color: v })}
              />
            </PropRow>
          </>
        )}
        <SliderPropRow
          label="Border Radius"
          value={str('menu_radius')}
          onChange={(v) => upd({ menu_radius: v })}
          min={0}
          max={50}
        />

        <div style={{ fontSize: 11, fontWeight: 600, color: '#6b7280', margin: '12px 0 4px' }}>
          BOX SHADOW
        </div>
        <PropRow label="Shadow Color">
          <ColorInput
            value={(shadow.color as string) || ''}
            onChange={(v) => updShadow({ color: v })}
          />
        </PropRow>
        <SliderPropRow
          label="Offset X"
          value={String(shadow.x ?? 0)}
          onChange={(v) => updShadow({ x: parseInt(v, 10) || 0 })}
          min={-50}
          max={50}
          units={['px']}
        />
        <SliderPropRow
          label="Offset Y"
          value={String(shadow.y ?? 0)}
          onChange={(v) => updShadow({ y: parseInt(v, 10) || 0 })}
          min={-50}
          max={50}
          units={['px']}
        />
        <SliderPropRow
          label="Blur"
          value={String(shadow.blur ?? 0)}
          onChange={(v) => updShadow({ blur: parseInt(v, 10) || 0 })}
          min={0}
          max={100}
          units={['px']}
        />
      </PropGroup>

      <PropGroup label="Spacing" defaultOpen={false} standalone>
        <SliderPropRow
          label="Padding"
          value={str('menu_padding')}
          onChange={(v) => upd({ menu_padding: v })}
          min={0}
          max={60}
        />
        <SliderPropRow
          label="Margin"
          value={str('menu_margin')}
          onChange={(v) => upd({ menu_margin: v })}
          min={0}
          max={60}
        />
        <SliderPropRow
          label="Item Gap"
          value={str('menu_gap')}
          onChange={(v) => upd({ menu_gap: v })}
          min={0}
          max={80}
        />
      </PropGroup>

      <PropGroup label="Dropdown" defaultOpen={false} standalone>
        <PropRow label="Background">
          <ColorInput value={str('submenu_bg')} onChange={(v) => upd({ submenu_bg: v })} />
        </PropRow>
        <PropRow label="Text Color">
          <ColorInput value={str('submenu_color')} onChange={(v) => upd({ submenu_color: v })} />
        </PropRow>
        <SliderPropRow
          label="Min Width"
          value={str('submenu_width')}
          onChange={(v) => upd({ submenu_width: v })}
          min={120}
          max={600}
        />
        <SliderPropRow
          label="Corner Radius"
          value={str('submenu_radius')}
          onChange={(v) => upd({ submenu_radius: v })}
          min={0}
          max={40}
        />
        <SliderPropRow
          label="Item Padding"
          value={str('submenu_padding')}
          onChange={(v) => upd({ submenu_padding: v })}
          min={0}
          max={40}
        />
      </PropGroup>
    </div>
  );
}

function TabsAdvStyleTab({ node, store }: { node: CBNode; store: EditorStore }) {
  const s   = (node.settings ?? {}) as Record<string, unknown>;
  const str = (k: string) => (s[k] as string) || '';
  const upS = (patch: Record<string, unknown>) => store.updateNode(node.id, { settings: { ...s, ...patch } });

  type TState = 'normal' | 'hover' | 'active';
  const [state, setState] = useState<TState>('normal');
  const p = (k: string) => `tab_${state}_${k}`; // per-state key

  // 4-side dimension helper bound to a per-state prefix (e.g. padding, radius).
  const dim4 = (prefix: string) => ({
    values: {
      top:    str(p(`${prefix}_top`)),
      right:  str(p(`${prefix}_right`)),
      bottom: str(p(`${prefix}_bottom`)),
      left:   str(p(`${prefix}_left`)),
    },
    onChange: (side: string, val: string) => upS({ [p(`${prefix}_${side}`)]: val }),
    onChangeAll: (val: string) => upS({
      [p(`${prefix}_top`)]: val, [p(`${prefix}_right`)]: val,
      [p(`${prefix}_bottom`)]: val, [p(`${prefix}_left`)]: val,
    }),
  });

  const borderType = str(p('borderType')) || 'none';

  // ── Titles group: typography (state-independent) + per-state color / text
  //    shadow / text stroke on the tab-button title. ──
  const fonts = jalpiEditorConfig.fonts ?? [];
  const TITLE_TYPO_KEYS   = ['fontFamily', 'fontSize', 'fontWeight', 'lineHeight', 'letterSpacing', 'textTransform', 'fontStyle', 'textDecoration'];
  const TITLE_SHADOW_KEYS = ['textShadowH', 'textShadowV', 'textShadowBlur', 'textShadowColor'];
  const pfxStyles = (prefix: string, keys: string[]): Record<string, string> =>
    Object.fromEntries(keys.map((k) => [k, str(`${prefix}_${k}`)]));
  const pfxUpdate = (prefix: string) => (patch: Record<string, string>) =>
    upS(Object.fromEntries(Object.entries(patch).map(([k, v]) => [`${prefix}_${k}`, v])));
  const [titleTypoOpen, setTitleTypoOpen]     = useState(false);
  const [titleShadowOpen, setTitleShadowOpen] = useState(false);
  const titleTypoRef   = useRef<HTMLButtonElement>(null);
  const titleShadowRef = useRef<HTMLButtonElement>(null);
  // per-state title color / text-stroke keys
  const tp = (k: string) => `title_${state}_${k}`;

  return (
    <div className="jalpi-props-section">
      {/* Layout controls (state-independent) */}
      <PropGroup label="Tabs">
        <SliderPropRow label="Gap between tabs" responsive value={str('tab_gap')} onChange={(v) => upS({ tab_gap: v })} min={0} max={60} units={['px']} placeholder="0" />
        <SliderPropRow label="Distance from content" responsive value={str('tab_distance')} onChange={(v) => upS({ tab_distance: v })} min={0} max={80} units={['px']} placeholder="8" />
      </PropGroup>

      {/* Per-state tab-button styling */}
      <PropGroup label="Tab Buttons">
        <div className="jalpi-nh-tabs" style={{ marginBottom: 10 }}>
          {(['normal', 'hover', 'active'] as TState[]).map((st) => (
            <button key={st} type="button" className={`jalpi-nh-tab${state === st ? ' is-active' : ''}`} onClick={() => setState(st)}>
              {st === 'normal' ? 'Normal' : st === 'hover' ? 'Hover' : 'Active'}
            </button>
          ))}
        </div>

        {/* Background + text color */}
        <ColorPropRow label="Background" value={str(p('bg'))} onChange={(v) => upS({ [p('bg')]: v })} placeholder="transparent" />
        <ColorPropRow label="Text Color" value={str(p('color'))} onChange={(v) => upS({ [p('color')]: v })} placeholder="" />

        {/* Border */}
        <PropRow label="Border Type">
          <Select
            value={borderType}
            options={[
              { value: 'none',   label: 'None' },
              { value: 'solid',  label: 'Solid' },
              { value: 'dashed', label: 'Dashed' },
              { value: 'dotted', label: 'Dotted' },
              { value: 'double', label: 'Double' },
            ]}
            onChange={(v) => upS({ [p('borderType')]: v })}
          />
        </PropRow>
        {borderType !== 'none' && (
          <>
            <PropRow label="Border Width" responsive>
              <BoxSpacingControl {...dim4('borderWidth')} />
            </PropRow>
            <ColorPropRow label="Border Color" value={str(p('borderColor'))} onChange={(v) => upS({ [p('borderColor')]: v })} placeholder="#000000" />
          </>
        )}

        {/* Box Shadow */}
        <PropRow label="Box Shadow" stack>
          <TextInput value={str(p('boxShadow'))} onChange={(v) => upS({ [p('boxShadow')]: v })} placeholder="0 4px 16px rgba(0,0,0,0.1)" />
        </PropRow>

        {/* Border Radius (4-side) */}
        <PropRow label="Border Radius" responsive>
          <BoxSpacingControl {...dim4('radius')} />
        </PropRow>

        {/* Padding (4-side) */}
        <PropRow label="Padding" responsive>
          <BoxSpacingControl {...dim4('padding')} />
        </PropRow>
      </PropGroup>

      {/* Titles — typography + per-state color / text shadow / text stroke on
          the tab-button title text. */}
      <PropGroup label="Titles" defaultOpen={false}>
        <TypographyTriggerRow label="Typography" isOpen={titleTypoOpen} triggerRef={titleTypoRef}
          onToggle={() => { setTitleShadowOpen(false); setTitleTypoOpen((o) => !o); }} />
        {titleTypoOpen && (
          <TypographyPopup styles={pfxStyles('title', TITLE_TYPO_KEYS)} update={pfxUpdate('title')}
            fonts={fonts} respKey={null} onClose={() => setTitleTypoOpen(false)} anchorRef={titleTypoRef} />
        )}

        <div className="jalpi-nh-tabs" style={{ margin: '10px 0' }}>
          {(['normal', 'hover', 'active'] as TState[]).map((st) => (
            <button key={st} type="button" className={`jalpi-nh-tab${state === st ? ' is-active' : ''}`} onClick={() => setState(st)}>
              {st === 'normal' ? 'Normal' : st === 'hover' ? 'Hover' : 'Active'}
            </button>
          ))}
        </div>

        <ColorPropRow label="Color" value={str(tp('color'))} onChange={(v) => upS({ [tp('color')]: v })} placeholder="" />

        <TypographyTriggerRow label="Text Shadow" isOpen={titleShadowOpen} triggerRef={titleShadowRef}
          onToggle={() => { setTitleTypoOpen(false); setTitleShadowOpen((o) => !o); }} />
        {titleShadowOpen && ReactDOM.createPortal(
          <TextShadowPopup styles={pfxStyles(`title_${state}`, TITLE_SHADOW_KEYS)} update={pfxUpdate(`title_${state}`)}
            onClose={() => setTitleShadowOpen(false)} anchorRef={titleShadowRef} />,
          document.body
        )}

        {/* Text Stroke — width + color → -webkit-text-stroke (per state). */}
        <PropRow label="Text Stroke Width">
          <DimensionInput value={str(tp('strokeWidth'))} onChange={(v) => upS({ [tp('strokeWidth')]: v })} placeholder="0" units={['px']} />
        </PropRow>
        <ColorPropRow label="Text Stroke Color" value={str(tp('strokeColor'))} onChange={(v) => upS({ [tp('strokeColor')]: v })} placeholder="#000000" />
      </PropGroup>

      {/* Icon — position/size/spacing (state-independent) + per-state color for
          the tab-button icons (set from the Content panel). */}
      <PropGroup label="Icon" defaultOpen={false}>
        <PropRow label="Position" responsive>
          <div className="jalpi-align-btns">
            {([
              ['top',    'Top',    <svg key="t" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M12 20V8m0 0l-4 4m4-4l4 4"/><line x1="4" y1="4" x2="20" y2="4"/></svg>],
              ['right',  'Right',  <svg key="r" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M4 12h12m0 0l-4-4m4 4l-4 4"/><line x1="20" y1="4" x2="20" y2="20"/></svg>],
              ['bottom', 'Bottom', <svg key="b" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M12 4v12m0 0l-4-4m4 4l4-4"/><line x1="4" y1="20" x2="20" y2="20"/></svg>],
              ['left',   'Left',   <svg key="l" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M20 12H8m0 0l4-4m-4 4l4 4"/><line x1="4" y1="4" x2="4" y2="20"/></svg>],
            ] as [string, string, React.ReactNode][]).map(([val, title, ico]) => (
              <button key={val} type="button" title={title}
                className={`jalpi-align-btn${(str('icon_position') || 'left') === val ? ' is-active' : ''}`}
                onClick={() => upS({ icon_position: val })}>
                {ico}
              </button>
            ))}
          </div>
        </PropRow>
        <SliderPropRow label="Size" responsive value={str('icon_size')} onChange={(v) => upS({ icon_size: v })} min={6} max={80} step={1} units={['px']} placeholder="16" />
        <SliderPropRow label="Spacing" responsive value={str('icon_spacing')} onChange={(v) => upS({ icon_spacing: v })} min={0} max={40} step={1} units={['px']} placeholder="6" />

        <div className="jalpi-nh-tabs" style={{ margin: '10px 0' }}>
          {(['normal', 'hover', 'active'] as TState[]).map((st) => (
            <button key={st} type="button" className={`jalpi-nh-tab${state === st ? ' is-active' : ''}`} onClick={() => setState(st)}>
              {st === 'normal' ? 'Normal' : st === 'hover' ? 'Hover' : 'Active'}
            </button>
          ))}
        </div>
        <ColorPropRow label="Color" value={str(`icon_${state}_color`)} onChange={(v) => upS({ [`icon_${state}_color`]: v })} placeholder="" />
      </PropGroup>

      {/* Content — styles the tab content area (.jalpi-tabs__body): background,
          border, radius, padding. State-independent. */}
      <PropGroup label="Content" defaultOpen={false}>
        {/* Background Type: Solid / Gradient */}
        <PropRow label="Background Type">
          <div style={{ display: 'flex', gap: 4 }}>
            <button type="button" title="Solid color"
              className={`jalpi-icon-btn${(str('content_bgType') || 'classic') === 'classic' ? ' is-active' : ''}`}
              onClick={() => upS({ content_bgType: 'classic' })}
              style={{ padding: '4px 10px', fontSize: 11, height: 28, borderRadius: 6 }}>Solid</button>
            <button type="button" title="Gradient"
              className={`jalpi-icon-btn${str('content_bgType') === 'gradient' ? ' is-active' : ''}`}
              onClick={() => upS({ content_bgType: 'gradient' })}
              style={{ padding: '4px 10px', fontSize: 11, height: 28, borderRadius: 6 }}>Gradient</button>
          </div>
        </PropRow>
        {(str('content_bgType') || 'classic') === 'classic' && (
          <ColorPropRow label="Background" value={str('content_bg')} onChange={(v) => upS({ content_bg: v })} placeholder="transparent" />
        )}
        {str('content_bgType') === 'gradient' && (
          <>
            <ColorPropRow label="Color 1" value={str('content_gradColor1')} onChange={(v) => upS({ content_gradColor1: v })} placeholder="#000000" />
            <ColorPropRow label="Color 2" value={str('content_gradColor2')} onChange={(v) => upS({ content_gradColor2: v })} placeholder="#ffffff" />
            <SliderPropRow label="Angle" value={str('content_gradAngle')} onChange={(v) => upS({ content_gradAngle: v })} min={0} max={360} step={1} units={['deg']} placeholder="180" />
          </>
        )}

        {/* Border */}
        <PropRow label="Border Type">
          <Select
            value={str('content_borderType') || 'none'}
            options={[
              { value: 'none',   label: 'Default' },
              { value: 'solid',  label: 'Solid' },
              { value: 'dashed', label: 'Dashed' },
              { value: 'dotted', label: 'Dotted' },
              { value: 'double', label: 'Double' },
            ]}
            onChange={(v) => upS({ content_borderType: v })}
          />
        </PropRow>
        {(str('content_borderType') || 'none') !== 'none' && (
          <>
            <PropRow label="Border Width" responsive>
              <BoxSpacingControl
                values={{ top: str('content_borderWidth_top'), right: str('content_borderWidth_right'), bottom: str('content_borderWidth_bottom'), left: str('content_borderWidth_left') }}
                onChange={(side, val) => upS({ [`content_borderWidth_${side}`]: val })}
                onChangeAll={(val) => upS({ content_borderWidth_top: val, content_borderWidth_right: val, content_borderWidth_bottom: val, content_borderWidth_left: val })}
              />
            </PropRow>
            <ColorPropRow label="Border Color" value={str('content_borderColor')} onChange={(v) => upS({ content_borderColor: v })} placeholder="#000000" />
          </>
        )}

        {/* Border Radius (4-side) */}
        <PropRow label="Border Radius" responsive>
          <BoxSpacingControl
            values={{ top: str('content_radius_top'), right: str('content_radius_right'), bottom: str('content_radius_bottom'), left: str('content_radius_left') }}
            onChange={(side, val) => upS({ [`content_radius_${side}`]: val })}
            onChangeAll={(val) => upS({ content_radius_top: val, content_radius_right: val, content_radius_bottom: val, content_radius_left: val })}
          />
        </PropRow>

        {/* Padding (4-side) */}
        <PropRow label="Padding" responsive>
          <BoxSpacingControl
            values={{ top: str('content_padding_top'), right: str('content_padding_right'), bottom: str('content_padding_bottom'), left: str('content_padding_left') }}
            onChange={(side, val) => upS({ [`content_padding_${side}`]: val })}
            onChangeAll={(val) => upS({ content_padding_top: val, content_padding_right: val, content_padding_bottom: val, content_padding_left: val })}
          />
        </PropRow>
      </PropGroup>
    </div>
  );
}

function StyleTab({ node, store }: { node: CBNode; store: EditorStore }) {
  const [textNH,         setTextNH]         = useState<'normal' | 'hover'>('normal');
  const [typoOpen,       setTypoOpen]       = useState(false);
  const [shadowOpen,     setShadowOpen]     = useState(false);
  const [boxShadowOpen,  setBoxShadowOpen]  = useState(false);
  const typoTriggerRef                      = useRef<HTMLButtonElement>(null);
  const shadowTriggerRef                    = useRef<HTMLButtonElement>(null);
  const boxShadowTriggerRef                 = useRef<HTMLButtonElement>(null);
  if (node.type === 'section' || node.type === 'container' || node.type === 'column') return <SectionStyleTab node={node} store={store} />;
  if (node.type === 'testimonials') return <TestimonialsStyleTab node={node} store={store} />;
  if (node.type === 'video')        return <VideoStyleTab        node={node} store={store} />;
  if (node.type === 'progress')     return <ProgressStyleTab     node={node} store={store} />;
  if (node.type === 'counter')      return <CounterStyleTab      node={node} store={store} />;
  if (node.type === 'accordion')    return <AccordionStyleTab    node={node} store={store} />;
  if (node.type === 'icon-list')    return <IconListStyleTab     node={node} store={store} />;
  if (node.type === 'feature-card') return <FeatureCardStyleTab  node={node} store={store} />;
  if (node.type === 'faq')            return <FaqStyleTab           node={node} store={store} />;
  if (node.type === 'image-carousel') return <ImageCarouselStyleTab node={node} store={store} />;
  if (node.type === 'image')        return <ImageStyleTab        node={node} store={store} />;
  if (node.type === 'cta')          return <CTAStyleTab          node={node} store={store} />;
  if (node.type === 'gallery')      return <GalleryStyleTab      node={node} store={store} />;
  if (node.type === 'products-grid') return <ProductsGridStyleTab node={node} store={store} />;
  if (node.type === 'product-categories') return <ProductCategoriesStyleTab node={node} store={store} />;
  if (node.type === 'single-product')     return <SingleProductStyleTab     node={node} store={store} />;
  if (node.type === 'tabs-adv')     return <TabsAdvStyleTab      node={node} store={store} />;
  if (node.type === 'menu')         return <MenuStyleTab         node={node} store={store} />;
  if (node.type === 'adv-menu')     return <AdvMenuStyleTab      node={node} store={store} />;
  if (node.type === 'breadcrumbs')  return <BreadcrumbsStyleTab  node={node} store={store} />;
  const { state } = store;
  const bp = state.breakpoint;
  const respKey: 'tabletStyles' | 'mobileStyles' | null = bp === 'tablet' ? 'tabletStyles' : bp === 'mobile' ? 'mobileStyles' : null;
  const respStyles: import('../../types').StyleMap = respKey ? (node[respKey] ?? {}) : {};
  // Effective styles: desktop base + non-empty responsive overrides on top
  const styles: import('../../types').StyleMap = respKey
    ? { ...(node.styles ?? {}), ...Object.fromEntries(Object.entries(respStyles).filter(([, v]) => v != null && v !== '')) }
    : (node.styles ?? {});
  const update = (updates: Record<string, string>) => {
    if (respKey) store.updateNode(node.id, { [respKey]: { ...respStyles, ...updates } });
    else store.updateNodeStyles(node.id, updates);
  };
  const colors = jalpiEditorConfig.colors ?? [];
  const fonts  = jalpiEditorConfig.fonts  ?? [];

  const opacityPct  = Math.round(parseFloat(styles.opacity ?? '1') * 100);
  const isFullText  = FULL_TEXT_ELEMENTS.has(node.type);
  const isTextEl    = TEXT_ELEMENTS.has(node.type);

  return (
    <div className="jalpi-props-section">

      {/* ── Widget-specific style controls (schema widgets only) ── */}
      {isSchemaWidget(node.type) && <GenericStyleExtras node={node} store={store} />}

      {/* ── Heading & Text — Elementor-style layout ── */}
      {isFullText && (
        <>
          {/* 1 — Alignment (inline row with responsive indicator) */}
          <PropRow label="Alignment" responsive>
            <AlignButtons
              value={styles.textAlign || ''}
              onChange={(a) => update({ textAlign: a })}
              options={['left', 'center', 'right', 'justify']}
            />
          </PropRow>

          {/* 2 — Typography trigger row + popup */}
          <TypographyTriggerRow
            label="Typography"
            isOpen={typoOpen}
            triggerRef={typoTriggerRef}
            onToggle={() => setTypoOpen((o) => !o)}
          />
          {typoOpen && (
            <TypographyPopup
              styles={styles}
              update={update}
              fonts={fonts}
              respKey={respKey}
              onClose={() => setTypoOpen(false)}
              anchorRef={typoTriggerRef}
            />
          )}

          {/* 3 — Text Shadow trigger row + popup */}
          <TypographyTriggerRow
            label="Text Shadow"
            isOpen={shadowOpen}
            triggerRef={shadowTriggerRef}
            onToggle={() => setShadowOpen((o) => !o)}
          />
          {shadowOpen && ReactDOM.createPortal(
            <TextShadowPopup
              styles={styles}
              update={update}
              onClose={() => setShadowOpen(false)}
              anchorRef={shadowTriggerRef}
            />,
            document.body
          )}

          {/* 4 — Paragraph Spacing */}
          <SliderPropRow
            label="Paragraph Spacing"
            responsive
            value={styles.paragraphSpacing || ''}
            onChange={(v) => update({ paragraphSpacing: v })}
            min={0} max={100} step={1}
            units={['px', 'em', 'rem']}
            placeholder="0"
          />

          {/* 6 — Normal / Hover tabs */}
          <NormalHoverTabs state={textNH} onChange={setTextNH} />

          {/* 7 — Text Color */}
          <ColorPropRow
            label="Text Color"
            value={textNH === 'normal' ? (styles.color || '') : (styles.colorHover || '')}
            onChange={(v) => update(textNH === 'normal' ? { color: v } : { colorHover: v })}
            placeholder="#333333"
            link={{ store, node, styleProp: textNH === 'normal' ? 'color' : 'colorHover' }}
          />

          {/* 8 — Link Color */}
          <ColorPropRow
            label="Link Color"
            value={textNH === 'normal' ? (styles.linkColor || '') : (styles.linkColorHover || '')}
            onChange={(v) => update(textNH === 'normal' ? { linkColor: v } : { linkColorHover: v })}
            placeholder="#1a1a1a"
            link={{ store, node, styleProp: textNH === 'normal' ? 'linkColor' : 'linkColorHover' }}
          />
        </>
      )}

      {/* ── Button — redesigned style panel ── */}
      {node.type === 'button' && (<>

        {/* ── Layout ── */}
        <PropGroup label="Layout">
          <PropRow label="Alignment" responsive>
            <AlignButtons
              value={styles.textAlign || ''}
              onChange={(a) => update({ textAlign: a })}
              options={['left', 'center', 'right']}
            />
          </PropRow>
          <PropRow label="Width" responsive>
            <SliderDimInput
              value={styles.width || ''}
              onChange={(v) => update({ width: v })}
              max={1200} step={10}
              units={['px', '%', 'vw']}
              placeholder="auto"
            />
          </PropRow>
        </PropGroup>

        {/* ── Typography ── */}
        <PropGroup label="Typography">
          <TypographyTriggerRow
            label="Font"
            isOpen={typoOpen}
            triggerRef={typoTriggerRef}
            onToggle={() => setTypoOpen((o) => !o)}
          />
          {typoOpen && (
            <TypographyPopup
              styles={styles}
              update={update}
              fonts={fonts}
              respKey={respKey}
              onClose={() => setTypoOpen(false)}
              anchorRef={typoTriggerRef}
            />
          )}
          <TypographyTriggerRow
            label="Text Shadow"
            isOpen={shadowOpen}
            triggerRef={shadowTriggerRef}
            onToggle={() => setShadowOpen((o) => !o)}
          />
          {shadowOpen && ReactDOM.createPortal(
            <TextShadowPopup
              styles={styles}
              update={update}
              onClose={() => setShadowOpen(false)}
              anchorRef={shadowTriggerRef}
            />,
            document.body
          )}
        </PropGroup>

        {/* ── Colors (Normal / Hover) ── */}
        <PropGroup label="Colors">
          <NormalHoverTabs state={textNH} onChange={setTextNH} />

          {/* Text Color */}
          <ColorPropRow
            label="Text Color"
            value={textNH === 'normal' ? (styles.color || '') : (styles.colorHover || '')}
            onChange={(v) => update(textNH === 'normal' ? { color: v } : { colorHover: v })}
            placeholder="#ffffff"
            link={{ store, node, styleProp: textNH === 'normal' ? 'color' : 'colorHover' }}
          />

          {/* Background Type toggle */}
          <div className="jalpi-prop-row">
            <span className="jalpi-prop-row__label">Background</span>
            <div style={{ display: 'flex', gap: 4 }}>
              <button
                type="button"
                title="Solid color"
                className={`jalpi-icon-btn${(styles.backgroundType || 'classic') === 'classic' ? ' is-active' : ''}`}
                onClick={() => update({ backgroundType: 'classic' })}
                style={{ padding: '4px 10px', fontSize: 11, height: 28, borderRadius: 6 }}
              >Solid</button>
              <button
                type="button"
                title="Gradient"
                className={`jalpi-icon-btn${styles.backgroundType === 'gradient' ? ' is-active' : ''}`}
                onClick={() => update({ backgroundType: 'gradient' })}
                style={{ padding: '4px 10px', fontSize: 11, height: 28, borderRadius: 6 }}
              >Gradient</button>
            </div>
          </div>

          {/* Solid background color */}
          {(styles.backgroundType || 'classic') === 'classic' && (
            <ColorPropRow
              label="BG Color"
              value={textNH === 'normal' ? (styles.backgroundColor || '') : (styles.backgroundColorHover || '')}
              onChange={(v) => update(textNH === 'normal' ? { backgroundColor: v } : { backgroundColorHover: v })}
              placeholder="transparent"
              link={{ store, node, styleProp: textNH === 'normal' ? 'backgroundColor' : 'backgroundColorHover' }}
            />
          )}

          {/* Gradient background */}
          {styles.backgroundType === 'gradient' && (<>
            <ColorPropRow
              label="Color 1"
              value={textNH === 'normal' ? (styles.gradientColor1 || '') : (styles.gradientColor1Hover || '')}
              onChange={(v) => update(textNH === 'normal' ? { gradientColor1: v } : { gradientColor1Hover: v })}
              placeholder="#000000"
              link={{ store, node, styleProp: textNH === 'normal' ? 'gradientColor1' : 'gradientColor1Hover' }}
            />
            <ColorPropRow
              label="Color 2"
              value={textNH === 'normal' ? (styles.gradientColor2 || '') : (styles.gradientColor2Hover || '')}
              onChange={(v) => update(textNH === 'normal' ? { gradientColor2: v } : { gradientColor2Hover: v })}
              placeholder="#ffffff"
              link={{ store, node, styleProp: textNH === 'normal' ? 'gradientColor2' : 'gradientColor2Hover' }}
            />
            {textNH === 'normal' && (
              <PropRow label="Angle">
                <div style={{ display: 'flex', alignItems: 'center', gap: 8, width: '100%' }}>
                  <input
                    type="range" min="0" max="360"
                    value={parseInt(styles.gradientAngle || '90', 10)}
                    onChange={(e) => update({ gradientAngle: e.target.value + 'deg' })}
                    style={{ flex: 1, accentColor: 'var(--jalpi-accent)' }}
                  />
                  <span style={{ fontSize: 11, color: 'var(--jalpi-text-muted)', minWidth: 36, textAlign: 'right' }}>
                    {styles.gradientAngle || '90deg'}
                  </span>
                </div>
              </PropRow>
            )}
          </>)}

          {/* Border Color (hover-aware) */}
          {styles.borderStyle && styles.borderStyle !== 'none' && (
            <ColorPropRow
              label="Border Color"
              value={textNH === 'normal' ? (styles.borderColor || '') : (styles.borderColorHover || '')}
              onChange={(v) => update(textNH === 'normal' ? { borderColor: v } : { borderColorHover: v })}
              placeholder="#000000"
              link={{ store, node, styleProp: textNH === 'normal' ? 'borderColor' : 'borderColorHover' }}
            />
          )}
        </PropGroup>

        {/* ── Border ── */}
        <PropGroup label="Border">
          <PropRow label="Style">
            <Select
              value={styles.borderStyle || 'default'}
              options={[
                { label: 'Default', value: 'default' },
                { label: 'None',    value: 'none'    },
                { label: 'Solid',   value: 'solid'   },
                { label: 'Dashed',  value: 'dashed'  },
                { label: 'Dotted',  value: 'dotted'  },
                { label: 'Double',  value: 'double'  },
                { label: 'Groove',  value: 'groove'  },
              ]}
              onChange={(v) => update({ borderStyle: v === 'default' ? '' : v })}
            />
          </PropRow>

          {styles.borderStyle && styles.borderStyle !== 'none' && (
            <PropRow label="Width" responsive>
              <BoxSpacingControl
                values={{
                  top:    styles.borderTopWidth    || '',
                  right:  styles.borderRightWidth  || '',
                  bottom: styles.borderBottomWidth || '',
                  left:   styles.borderLeftWidth   || '',
                }}
                onChange={(side, val) => update({ [`border${side.charAt(0).toUpperCase() + side.slice(1)}Width`]: val })}
              />
            </PropRow>
          )}

          <PropRow label="Radius" responsive>
            <BoxSpacingControl
              values={{
                top:    styles.borderTopLeftRadius     || '',
                right:  styles.borderTopRightRadius    || '',
                bottom: styles.borderBottomRightRadius || '',
                left:   styles.borderBottomLeftRadius  || '',
              }}
              onChange={(side, val) => {
                const map: Record<string, string> = { top: 'borderTopLeftRadius', right: 'borderTopRightRadius', bottom: 'borderBottomRightRadius', left: 'borderBottomLeftRadius' };
                update({ [map[side]]: val });
              }}
            />
          </PropRow>
        </PropGroup>

        {/* ── Box Shadow ── */}
        <PropGroup label="Shadow">
          <TypographyTriggerRow
            label="Box Shadow"
            isOpen={boxShadowOpen}
            triggerRef={boxShadowTriggerRef}
            onToggle={() => setBoxShadowOpen((o) => !o)}
          />
          {boxShadowOpen && ReactDOM.createPortal(
            <TextShadowPopup
              styles={styles}
              update={update}
              onClose={() => setBoxShadowOpen(false)}
              anchorRef={boxShadowTriggerRef}
            />,
            document.body
          )}
        </PropGroup>

        {/* ── Spacing ── */}
        <PropGroup label="Spacing">
          <PropRow label="Padding" responsive>
            <BoxSpacingControl
              values={{
                top:    styles.paddingTop    || '',
                right:  styles.paddingRight  || '',
                bottom: styles.paddingBottom || '',
                left:   styles.paddingLeft   || '',
              }}
              onChange={(side, val) => update({ [`padding${side.charAt(0).toUpperCase() + side.slice(1)}`]: val })}
            />
          </PropRow>
        </PropGroup>

      </>)}

      {/* ── Divider style section ── */}
      {node.type === 'divider' && (
        <PropGroup label="Divider">
          <ColorPropRow
            label="Color"
            value={styles.borderTopColor || ''}
            onChange={(v) => update({ borderTopColor: v })}
            placeholder="#e5e7eb"
          />
          <SliderPropRow
            label="Size"
            value={styles.borderTopWidth || ''}
            onChange={(v) => update({ borderTopWidth: v })}
            min={1}
            max={20}
            step={1}
            units={['px']}
            placeholder="1"
          />
          <SliderPropRow
            label="Gap"
            responsive
            value={styles.marginTop || ''}
            onChange={(v) => update({ marginTop: v, marginBottom: v })}
            min={0}
            max={100}
            step={1}
            units={['px']}
            placeholder="20"
          />
        </PropGroup>
      )}

      {/* ── Icon Style (Elementor-style) ── */}
      {node.type === 'icon' && (
        <IconStyleSection styles={styles} update={update} />
      )}

      {/* ── Basic Typography for html (not button, not icon) ── */}
      {!isFullText && isTextEl && node.type !== 'button' && node.type !== 'icon' && (
        <>
          <div className="jalpi-typo-trigger">
            <span className="jalpi-typo-trigger__label">
              <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" style={{ marginRight: 6, flexShrink: 0, color: 'var(--jalpi-accent)' }}>
                <polyline points="4 7 4 4 20 4 20 7"/><line x1="9" y1="20" x2="15" y2="20"/><line x1="12" y1="4" x2="12" y2="20"/>
              </svg>
              Typography
            </span>
            <div className="jalpi-typo-trigger__preview">
              {styles.fontFamily && <span>{styles.fontFamily.split(',')[0]}</span>}
              {styles.fontSize   && <span>{styles.fontSize}</span>}
            </div>
            <button
              ref={typoTriggerRef}
              type="button"
              className={`jalpi-typo-trigger__btn${typoOpen ? ' is-active' : ''}`}
              title="Edit typography"
              onClick={() => setTypoOpen((o) => !o)}
            >
              <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
                <path d="M12 20h9"/><path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z"/>
              </svg>
            </button>
          </div>
          {typoOpen && (
            <TypographyPopup
              styles={styles}
              update={update}
              fonts={fonts}
              respKey={respKey}
              onClose={() => setTypoOpen(false)}
              anchorRef={typoTriggerRef}
            />
          )}
          <PropRow label="Color">
            <ColorInput value={styles.color || ''} onChange={(v) => update({ color: v })} placeholder="#000000" />
          </PropRow>
          <PropRow label="Align">
            <AlignButtons
              value={styles.textAlign || ''}
              onChange={(a) => update({ textAlign: a })}
              options={['left','center','right','justify']}
            />
          </PropRow>
        </>
      )}

      {/* ── Background — hidden for heading/text/button/icon/divider/spacer.
           Also hidden for social-share: its Style tab uses only the bespoke
           Icon Hover + Icon groups (see GenericStyleExtras schema fields). ── */}
      {!isFullText && node.type !== 'button' && node.type !== 'icon' && node.type !== 'divider' && node.type !== 'spacer' && node.type !== 'social-share' && (
        <PropGroup label="Background" defaultOpen={!isTextEl} icon={
          <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
            <rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="8.5" cy="8.5" r="1.5" fill="currentColor" stroke="none"/><polyline points="21 15 16 10 5 21"/>
          </svg>
        }>
          <PropRow label="Color">
            <ColorInput value={styles.backgroundColor || ''} onChange={(v) => update({ backgroundColor: v })} placeholder="transparent" />
          </PropRow>
          {colors.length > 0 && (
            <div className="jalpi-color-swatches" style={{ marginTop: 8 }}>
              {colors.map((c) => (
                <button
                  key={c.slug}
                  className="jalpi-color-swatch"
                  style={{ backgroundColor: c.color }}
                  title={c.name}
                  onClick={() => update({ backgroundColor: c.color })}
                />
              ))}
            </div>
          )}
        </PropGroup>
      )}

      {/* ── Size — hidden for heading/text/button/icon/divider/spacer/social-share ── */}
      {!isFullText && node.type !== 'button' && node.type !== 'icon' && node.type !== 'divider' && node.type !== 'spacer' && node.type !== 'social-share' && (
        <PropGroup label="Size" defaultOpen={!isTextEl} icon={
          <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
            <path d="M15 3h6v6M9 21H3v-6M21 3l-7 7M3 21l7-7"/>
          </svg>
        }>
          <PropRow label="Width">
            <DimensionInput value={styles.width || ''} onChange={(v) => update({ width: v })} placeholder="auto" units={['px', '%', 'vw']} />
          </PropRow>
          <PropRow label="Height">
            <DimensionInput value={styles.height || ''} onChange={(v) => update({ height: v })} placeholder="auto" units={['px', '%', 'vh']} />
          </PropRow>
          <PropRow label="Min Height">
            <DimensionInput value={styles.minHeight || ''} onChange={(v) => update({ minHeight: v })} placeholder="auto" units={['px', '%', 'vh']} />
          </PropRow>
        </PropGroup>
      )}

      {/* ── Border & Radius — hidden for heading/text/button/icon/divider/spacer/social-share ── */}
      {!isFullText && node.type !== 'button' && node.type !== 'icon' && node.type !== 'divider' && node.type !== 'spacer' && node.type !== 'social-share' && (
        <PropGroup label="Border" defaultOpen={false} icon={
          <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
            <rect x="3" y="3" width="18" height="18" rx="4"/>
          </svg>
        }>
          <PropRow label="Radius">
            <div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
              <div style={{ flex: 1, minWidth: 0 }}>
                <DimensionInput value={styles.borderRadius || ''} onChange={(v) => update({ borderRadius: v })} placeholder="0" units={['px', '%']} />
              </div>
              <TokenLinkControl store={store} node={node} styleProp="borderRadius" category="borderRadius" />
            </div>
          </PropRow>
          <PropRow label="Border">
            <TextInput value={styles.border || ''} onChange={(v) => update({ border: v })} placeholder="1px solid #ccc" />
          </PropRow>
        </PropGroup>
      )}

      {/* ── Opacity — applies to every element type that reaches this shared tab
           (except social-share, whose Style tab is bespoke Icon Hover + Icon). ── */}
      {node.type !== 'social-share' && (
        <PropGroup label="Effects" defaultOpen={false} icon={
          <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
            <circle cx="12" cy="12" r="10"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/>
          </svg>
        }>
          <SliderPropRow
            label="Opacity"
            value={styles.opacity || ''}
            onChange={(v) => update({ opacity: v })}
            min={0} max={1} step={0.05} units={['']}
            placeholder="1"
            link={{ store, node, styleProp: 'opacity' }}
          />
        </PropGroup>
      )}

    </div>
  );
}

/* =========================================================
   ANIMATION TAB — Interaction Engine
   Extends the (previously FAQ-only) Animation panel into a generic engine
   available to every node type. "Animation" = quick one-click entrance
   presets; "Interaction" = the full rule builder (Trigger / Action /
   Settings). Reuses only existing primitives (PropGroup/PropRow/Select/
   TextInput/Toggle) — no new low-level controls.
   ========================================================= */

const INTERACTION_TRIGGER_OPTIONS: Array<{ value: InteractionTrigger; label: string }> = [
  { value: 'pageLoad',       label: 'On Page Load' },
  { value: 'hover',          label: 'On Hover' },
  { value: 'parentHover',    label: 'On Parent Hover' },
  { value: 'mouseEnter',     label: 'On Mouse Enter' },
  { value: 'mouseLeave',     label: 'On Mouse Leave' },
  { value: 'click',          label: 'On Click' },
  { value: 'doubleClick',    label: 'On Double Click' },
  { value: 'scrollIntoView', label: 'On Scroll Into View' },
  { value: 'scrollProgress', label: 'On Scroll Progress' },
  { value: 'mouseMove',      label: 'On Mouse Move' },
  { value: 'focus',          label: 'On Focus' },
  { value: 'blur',           label: 'On Blur' },
];

const INTERACTION_ACTION_OPTIONS: Array<{ value: InteractionAction; label: string }> = [
  { value: 'fade',            label: 'Fade' },
  { value: 'slide',           label: 'Slide' },
  { value: 'scale',           label: 'Scale' },
  { value: 'rotate',          label: 'Rotate' },
  { value: 'translate',       label: 'Translate' },
  { value: 'opacity',         label: 'Opacity' },
  { value: 'blur',            label: 'Blur' },
  { value: 'skew',            label: 'Skew' },
  { value: 'flip',            label: 'Flip' },
  { value: 'bounce',          label: 'Bounce' },
  { value: 'zoom',            label: 'Zoom' },
  { value: 'customTransform', label: 'Custom Transform' },
  { value: 'jackInTheBox',    label: 'Jack In The Box' },
  { value: 'elastic',         label: 'Elastic' },
  { value: 'reveal',          label: 'Reveal' },
  { value: 'wipe',            label: 'Wipe' },
];

const INTERACTION_CLIP_SHAPE_OPTIONS = [
  { value: 'edge',    label: 'Edge (directional)' },
  { value: 'circle',  label: 'Circle' },
  { value: 'diamond', label: 'Diamond' },
  { value: 'square',  label: 'Square' },
];

const INTERACTION_EASING_OPTIONS = [
  { value: 'ease',         label: 'Ease' },
  { value: 'ease-in',      label: 'Ease In' },
  { value: 'ease-out',     label: 'Ease Out' },
  { value: 'ease-in-out',  label: 'Ease In-Out' },
  { value: 'linear',       label: 'Linear' },
  { value: 'cubic-bezier(0.68,-0.55,0.265,1.55)', label: 'Bounce (overshoot)' },
];

const INTERACTION_DIRECTION_OPTIONS = [
  { value: '',                 label: '—' },
  { value: 'up',                label: 'Up' },
  { value: 'down',              label: 'Down' },
  { value: 'left',              label: 'Left' },
  { value: 'right',             label: 'Right' },
  { value: 'normal',            label: 'Normal' },
  { value: 'reverse',           label: 'Reverse' },
  { value: 'alternate',         label: 'Alternate' },
  { value: 'alternate-reverse', label: 'Alternate Reverse' },
];

function AnimationTab({ node, store }: { node: CBNode; store: EditorStore }) {
  const rules = node.interactions ?? [];
  const [pickerOpen, setPickerOpen] = useState(false);

  const applyPreset = (preset: AnimationPreset) => {
    store.addInteraction(node.id, 'scrollIntoView', preset.action, preset.settings);
    setPickerOpen(false);
  };

  const moveRule = (index: number, dir: -1 | 1) => {
    const ids = rules.map((r) => r.id);
    const target = index + dir;
    if (target < 0 || target >= ids.length) return;
    [ids[index], ids[target]] = [ids[target], ids[index]];
    store.reorderInteractions(node.id, ids);
  };

  return (
    <div className="jalpi-props-section">
      <PropGroup label="Animation" defaultOpen={true} standalone>
        <p className="jalpi-ia-hint">
          Pick a named entrance animation from the library, or build a fully custom rule below under Interaction.
        </p>
        <button type="button" className="jalpi-gs-add-btn" onClick={() => setPickerOpen(true)}>
          <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round">
            <path d="M12 5v14M5 12h14"/>
          </svg>
          Browse Animation Library
        </button>
        {pickerOpen && (
          <AnimationPresetPicker onClose={() => setPickerOpen(false)} onSelect={applyPreset} />
        )}
      </PropGroup>

      <PropGroup label="Interaction" defaultOpen={true} standalone>
        {rules.length === 0 && (
          <p className="jalpi-ia-hint">No interaction rules yet. Add one to trigger an animation on hover, click, scroll, and more.</p>
        )}
        {rules.map((rule, index) => (
          <InteractionRuleEditor
            key={rule.id}
            node={node}
            rule={rule}
            store={store}
            isFirst={index === 0}
            isLast={index === rules.length - 1}
            onMoveUp={() => moveRule(index, -1)}
            onMoveDown={() => moveRule(index, 1)}
          />
        ))}
        <button type="button" className="jalpi-gs-add-btn" onClick={() => store.addInteraction(node.id)}>
          <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round">
            <path d="M12 5v14M5 12h14"/>
          </svg>
          Add Interaction
        </button>
      </PropGroup>

      <SplitTextGroup node={node} store={store} />
    </div>
  );
}

/* =========================================================
   SPLIT TEXT — per character / word / line / paragraph reveal.

   Lives in the Animation tab so every text-bearing widget (Heading, Text
   Editor, Button, CTA, FAQ, Pricing, Testimonials, …) gets it from one
   place — the PHP side emits the attribute from get_css_attrs(), which
   every element already calls, so no per-widget wiring is needed.

   Everything is gated behind st_enabled, which is absent on all existing
   content: off means no attribute, no CSS and no script enqueue, so saved
   pages render exactly as they did before this group existed.
   ========================================================= */

const ST_ANIMATION_OPTIONS = [
  { value: 'fadeUp',         label: 'Fade Up' },
  { value: 'fadeDown',       label: 'Fade Down' },
  { value: 'fadeLeft',       label: 'Fade Left' },
  { value: 'fadeRight',      label: 'Fade Right' },
  { value: 'zoomIn',         label: 'Zoom In' },
  { value: 'zoomOut',        label: 'Zoom Out' },
  { value: 'rotate',         label: 'Rotate' },
  { value: 'blurReveal',     label: 'Blur Reveal' },
  { value: 'scale',          label: 'Scale' },
  { value: 'slide',          label: 'Slide' },
  { value: 'flip',           label: 'Flip' },
  { value: 'elastic',        label: 'Elastic' },
  { value: 'bounce',         label: 'Bounce' },
  { value: 'typewriter',     label: 'Typewriter' },
  { value: 'wave',           label: 'Wave' },
  { value: 'maskReveal',     label: 'Mask Reveal' },
  { value: 'clipReveal',     label: 'Clip Reveal' },
  { value: 'glowReveal',     label: 'Glow Reveal' },
  { value: 'letterSpacing',  label: 'Letter Spacing Reveal' },
  { value: 'gradientReveal', label: 'Gradient Reveal' },
];

const ST_SPLIT_OPTIONS = [
  { value: 'chars',      label: 'Characters' },
  { value: 'words',      label: 'Words' },
  { value: 'lines',      label: 'Lines' },
  { value: 'paragraphs', label: 'Paragraphs' },
];

const ST_TRIGGER_OPTIONS = [
  { value: 'scroll', label: 'When Visible (on scroll)' },
  { value: 'load',   label: 'On Page Load' },
];

const ST_EASE_OPTIONS = [
  { value: 'ease',        label: 'Ease' },
  { value: 'ease-in',     label: 'Ease In' },
  { value: 'ease-out',    label: 'Ease Out' },
  { value: 'ease-in-out', label: 'Ease In-Out' },
  { value: 'linear',      label: 'Linear' },
  { value: 'cubic-bezier(0.68,-0.55,0.265,1.55)', label: 'Bounce (overshoot)' },
];

function SplitTextGroup({ node, store }: { node: CBNode; store: EditorStore }) {
  const s = (node.settings ?? {}) as Record<string, unknown>;
  const enabled = !!s.st_enabled;

  const upd = (patch: Record<string, unknown>) =>
    store.updateNode(node.id, { settings: { ...s, ...patch } });

  const num = (k: string, dflt: number) => {
    const v = s[k];
    return v === undefined || v === null || v === '' ? dflt : Number(v);
  };
  const str = (k: string, dflt = '') => (s[k] as string) ?? dflt;

  return (
    <PropGroup label="Split Text" defaultOpen={false} standalone>
      <PropRow label="Enable">
        <Toggle
          checked={enabled}
          onChange={(v) => {
            // First enable seeds sensible defaults so the effect is visible
            // immediately; disabling only flips the flag, leaving the tuned
            // values intact for when it is switched back on.
            if (v && !s.st_animation) {
              upd({
                st_enabled: true,
                st_animation: 'fadeUp',
                st_split: 'chars',
                st_trigger: 'scroll',
                st_duration: 800,
                st_stagger: 40,
                st_delay: 0,
              });
            } else {
              upd({ st_enabled: v });
            }
          }}
        />
      </PropRow>

      {!enabled && (
        <p className="jalpi-ia-hint">
          Split the text into characters, words, lines or paragraphs and reveal them in
          sequence. Off by default — existing content is unaffected.
        </p>
      )}

      {enabled && (
        <>
          <PropRow label="Split By">
            <Select
              value={str('st_split', 'chars')}
              options={ST_SPLIT_OPTIONS}
              onChange={(v) => upd({ st_split: v })}
            />
          </PropRow>

          <PropRow label="Animation">
            <Select
              value={str('st_animation', 'fadeUp')}
              options={ST_ANIMATION_OPTIONS}
              onChange={(v) => upd({ st_animation: v })}
            />
          </PropRow>

          <PropRow label="Trigger">
            <Select
              value={str('st_trigger', 'scroll')}
              options={ST_TRIGGER_OPTIONS}
              onChange={(v) => upd({ st_trigger: v })}
            />
          </PropRow>

          <SliderPropRow
            label="Duration"
            value={String(num('st_duration', 800))}
            onChange={(v) => upd({ st_duration: parseInt(v, 10) || 0 })}
            min={0}
            max={5000}
            step={50}
            units={['ms']}
          />

          <SliderPropRow
            label="Delay"
            value={String(num('st_delay', 0))}
            onChange={(v) => upd({ st_delay: parseInt(v, 10) || 0 })}
            min={0}
            max={5000}
            step={50}
            units={['ms']}
          />

          <SliderPropRow
            label="Stagger"
            value={String(num('st_stagger', 40))}
            onChange={(v) => upd({ st_stagger: parseInt(v, 10) || 0 })}
            min={0}
            max={500}
            step={5}
            units={['ms']}
          />

          <PropRow label="Easing">
            <Select
              value={str('st_ease', 'ease')}
              options={ST_EASE_OPTIONS}
              onChange={(v) => upd({ st_ease: v })}
            />
          </PropRow>

          <SliderPropRow
            label="Repeat"
            value={String(num('st_repeat', 1))}
            onChange={(v) => upd({ st_repeat: parseInt(v, 10) || 1 })}
            min={1}
            max={20}
            step={1}
            units={['x']}
          />

          {str('st_trigger', 'scroll') === 'scroll' && (
            <>
              <SliderPropRow
                label="Trigger Point"
                value={String(num('st_viewport_pct', 20))}
                onChange={(v) => upd({ st_viewport_pct: parseInt(v, 10) || 0 })}
                min={0}
                max={100}
                step={5}
                units={['%']}
              />

              <PropRow label="Replay On Scroll">
                <Toggle
                  checked={!!s.st_replay}
                  onChange={(v) => upd({ st_replay: v })}
                />
              </PropRow>
            </>
          )}

          <PropRow label="Reverse Order">
            <Toggle
              checked={!!s.st_reverse}
              onChange={(v) => upd({ st_reverse: v })}
            />
          </PropRow>

          <p className="jalpi-ia-hint">
            The animation plays on the published page. Original text stays in the markup
            for search engines, and readers who prefer reduced motion see it unanimated.
          </p>
        </>
      )}
    </PropGroup>
  );
}

function InteractionRuleEditor({ node, rule, store, isFirst, isLast, onMoveUp, onMoveDown }: {
  node: CBNode; rule: InteractionRule; store: EditorStore;
  isFirst: boolean; isLast: boolean; onMoveUp: () => void; onMoveDown: () => void;
}) {
  const [open, setOpen] = useState(false);
  const s = rule.settings || {};
  const upd = (updates: Partial<InteractionRule>) => store.updateInteraction(node.id, rule.id, updates);
  const updSettings = (patch: Partial<InteractionSettings>) => store.updateInteraction(node.id, rule.id, { settings: patch } as Partial<InteractionRule>);

  const isScroll     = rule.trigger === 'scrollIntoView' || rule.trigger === 'scrollProgress';
  const isHover       = rule.trigger === 'hover' || rule.trigger === 'parentHover';
  const isMouseMove   = rule.trigger === 'mouseMove';
  const isDisabled    = rule.enabled === false;
  const resp = rule.responsive ?? {};

  const triggerLabel = INTERACTION_TRIGGER_OPTIONS.find((t) => t.value === rule.trigger)?.label ?? rule.trigger;
  const actionLabel  = INTERACTION_ACTION_OPTIONS.find((a) => a.value === rule.action)?.label ?? rule.action;

  return (
    <div className={`jalpi-ia-rule${open ? ' is-open' : ''}${isDisabled ? ' is-disabled' : ''}`}>
      <div className="jalpi-ia-rule__header" onClick={() => setOpen((o) => !o)}>
        <span className="jalpi-ia-rule__title">{triggerLabel} → {actionLabel}</span>
        <span className="jalpi-ia-rule__order" onClick={(e) => e.stopPropagation()}>
          <button type="button" disabled={isFirst} onClick={onMoveUp} aria-label="Move rule up">
            <svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="3" strokeLinecap="round"><path d="M6 15l6-6 6 6"/></svg>
          </button>
          <button type="button" disabled={isLast} onClick={onMoveDown} aria-label="Move rule down">
            <svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="3" strokeLinecap="round"><path d="M6 9l6 6 6-6"/></svg>
          </button>
        </span>
        <span className="jalpi-ia-rule__toggle" onClick={(e) => e.stopPropagation()} title={isDisabled ? 'Disabled' : 'Enabled'}>
          <Toggle checked={!isDisabled} onChange={(v) => upd({ enabled: v })} />
        </span>
        <button
          type="button"
          className="jalpi-ia-rule__del"
          title="Delete rule"
          aria-label="Delete rule"
          onClick={(e) => { e.stopPropagation(); store.removeInteraction(node.id, rule.id); }}
        >
          <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round">
            <path d="M3 6h18M8 6V4h8v2M19 6l-1 14H6L5 6"/>
          </svg>
        </button>
        <svg className="jalpi-ia-rule__chevron" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
          <path d="M6 9l6 6 6-6"/>
        </svg>
      </div>

      {open && (
        <div className="jalpi-ia-rule__body">
          <PropRow label="Trigger">
            <Select value={rule.trigger} options={INTERACTION_TRIGGER_OPTIONS} onChange={(v) => upd({ trigger: v as InteractionTrigger })} />
          </PropRow>
          <PropRow label="Action">
            <Select value={rule.action} options={INTERACTION_ACTION_OPTIONS} onChange={(v) => upd({ action: v as InteractionAction })} />
          </PropRow>

          <PropGroup label="Settings" defaultOpen={true} standalone>
            <PropRow label="Duration">
              <TextInput value={s.duration ?? ''} onChange={(v) => updSettings({ duration: v })} placeholder="0.6s" />
            </PropRow>
            <PropRow label="Delay">
              <TextInput value={s.delay ?? ''} onChange={(v) => updSettings({ delay: v })} placeholder="0s" />
            </PropRow>
            <PropRow label="Easing">
              <Select value={s.easing ?? 'ease'} options={INTERACTION_EASING_OPTIONS} onChange={(v) => updSettings({ easing: v })} />
            </PropRow>
            <PropRow label="Repeat">
              <TextInput value={s.repeat ?? ''} onChange={(v) => updSettings({ repeat: v })} placeholder="1" />
            </PropRow>
            <PropRow label="Infinite Loop">
              <Toggle checked={!!s.infinite} onChange={(v) => updSettings({ infinite: v })} />
            </PropRow>
            <PropRow label="Direction">
              <Select value={s.direction ?? ''} options={INTERACTION_DIRECTION_OPTIONS} onChange={(v) => updSettings({ direction: v })} />
            </PropRow>
            <PropRow label="Distance">
              <TextInput value={s.distance ?? ''} onChange={(v) => updSettings({ distance: v })} placeholder="30px" />
            </PropRow>
            <PropRow label="Rotation °">
              <TextInput value={s.rotationDeg ?? ''} onChange={(v) => updSettings({ rotationDeg: v })} placeholder="15" />
            </PropRow>
            <PropRow label="Scale Value">
              <TextInput value={s.scaleValue ?? ''} onChange={(v) => updSettings({ scaleValue: v })} placeholder="0.8" />
            </PropRow>
            <PropRow label="Blur Amount">
              <TextInput value={s.blurAmount ?? ''} onChange={(v) => updSettings({ blurAmount: v })} placeholder="8px" />
            </PropRow>
            <PropRow label="Opacity Value">
              <TextInput value={s.opacityValue ?? ''} onChange={(v) => updSettings({ opacityValue: v })} placeholder="1" />
            </PropRow>
            <PropRow label="Transform Origin">
              <TextInput value={s.transformOrigin ?? ''} onChange={(v) => updSettings({ transformOrigin: v })} placeholder="center" />
            </PropRow>
            {rule.action === 'customTransform' && (
              <>
                <PropRow label="Custom Transform">
                  <TextInput value={s.customTransform ?? ''} onChange={(v) => updSettings({ customTransform: v })} placeholder="rotate(10deg) scale(1.1)" />
                </PropRow>
                <PropRow label="Custom Filter">
                  <TextInput value={s.customFilter ?? ''} onChange={(v) => updSettings({ customFilter: v })} placeholder="blur(10px)" />
                </PropRow>
              </>
            )}
            {(rule.action === 'reveal' || rule.action === 'wipe') && (
              <PropRow label="Clip Shape">
                <Select
                  value={s.clipShape ?? 'edge'}
                  options={INTERACTION_CLIP_SHAPE_OPTIONS}
                  onChange={(v) => updSettings({ clipShape: v as InteractionSettings['clipShape'] })}
                />
              </PropRow>
            )}
          </PropGroup>

          {isScroll && (
            <PropGroup label="Scroll" defaultOpen={false} standalone>
              <PropRow label="Start Offset">
                <TextInput value={s.startOffset ?? ''} onChange={(v) => updSettings({ startOffset: v })} placeholder="0px" />
              </PropRow>
              <PropRow label="End Offset">
                <TextInput value={s.endOffset ?? ''} onChange={(v) => updSettings({ endOffset: v })} placeholder="0px" />
              </PropRow>
              <PropRow label="Viewport %">
                <TextInput value={s.viewportPct ?? ''} onChange={(v) => updSettings({ viewportPct: v })} placeholder="10" />
              </PropRow>
              <PropRow label="Replay on Re-enter">
                <Toggle checked={!!s.replay} onChange={(v) => updSettings({ replay: v })} />
              </PropRow>
              <PropRow label="Scroll Direction">
                <Select
                  value={s.scrollDirection ?? 'vertical'}
                  options={[{ value: 'vertical', label: 'Vertical' }, { value: 'horizontal', label: 'Horizontal' }, { value: 'both', label: 'Both' }]}
                  onChange={(v) => updSettings({ scrollDirection: v as InteractionSettings['scrollDirection'] })}
                />
              </PropRow>
            </PropGroup>
          )}

          {isHover && (
            <PropGroup label="Hover" defaultOpen={false} standalone>
              <PropRow label="Hide After Playing">
                <Toggle checked={!!s.hideAfterPlay} onChange={(v) => updSettings({ hideAfterPlay: v })} />
              </PropRow>
              <p className="jalpi-ia-hint">
                {s.hideAfterPlay
                  ? 'Hidden at rest. Each hover plays the animation once, then it disappears again until the next hover.'
                  : 'Plays on hover-in, reverses back on hover-out.'}
              </p>
              <PropRow label="Hover In">
                <Select value={s.hoverInAction ?? rule.action} options={INTERACTION_ACTION_OPTIONS} onChange={(v) => updSettings({ hoverInAction: v as InteractionAction })} />
              </PropRow>
              {!s.hideAfterPlay && (
                <PropRow label="Hover Out">
                  <Select value={s.hoverOutAction ?? rule.action} options={INTERACTION_ACTION_OPTIONS} onChange={(v) => updSettings({ hoverOutAction: v as InteractionAction })} />
                </PropRow>
              )}
              <PropRow label="Transition Duration">
                <TextInput value={s.transitionDuration ?? ''} onChange={(v) => updSettings({ transitionDuration: v })} placeholder="0.3s" />
              </PropRow>
            </PropGroup>
          )}

          {isMouseMove && (
            <PropGroup label="Mouse Move" defaultOpen={false} standalone>
              <PropRow label="Horizontal">
                <TextInput value={s.moveX ?? ''} onChange={(v) => updSettings({ moveX: v })} placeholder="20" />
              </PropRow>
              <PropRow label="Vertical">
                <TextInput value={s.moveY ?? ''} onChange={(v) => updSettings({ moveY: v })} placeholder="20" />
              </PropRow>
              <PropRow label="Strength">
                <TextInput value={s.strength ?? ''} onChange={(v) => updSettings({ strength: v })} placeholder="50" />
              </PropRow>
              <PropRow label="Reverse Direction">
                <Toggle checked={!!s.reverse} onChange={(v) => updSettings({ reverse: v })} />
              </PropRow>
              <PropRow label="Smoothness">
                <TextInput value={s.smoothness ?? ''} onChange={(v) => updSettings({ smoothness: v })} placeholder="15" />
              </PropRow>
            </PropGroup>
          )}

          <PropGroup label="Responsive" defaultOpen={false} standalone>
            <p className="jalpi-ia-hint">Turn this animation off on specific breakpoints. Enabled everywhere by default.</p>
            <PropRow label="Desktop">
              <Toggle checked={resp.desktop !== false} onChange={(v) => upd({ responsive: { ...resp, desktop: v } })} />
            </PropRow>
            <PropRow label="Tablet">
              <Toggle checked={resp.tablet !== false} onChange={(v) => upd({ responsive: { ...resp, tablet: v } })} />
            </PropRow>
            <PropRow label="Mobile">
              <Toggle checked={resp.mobile !== false} onChange={(v) => upd({ responsive: { ...resp, mobile: v } })} />
            </PropRow>
          </PropGroup>

          <button
            type="button"
            className="jalpi-ia-rule__restart"
            onClick={() => restartRuleOnCanvas(node.id, rule)}
          >
            <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
              <path d="M3 12a9 9 0 1 1 3 6.7M3 12v6M3 12h6" />
            </svg>
            Restart Preview
          </button>
        </div>
      )}
    </div>
  );
}

/** Re-plays a rule's *actual configured* animation on its live canvas element
 * via the Web Animations API (buildWaapiPreviewKeyframes — the same builder
 * the preset picker's hover swatches use), so the preview reflects real
 * action+settings rather than a generic placeholder. Editor-only; never
 * touches CanvasNode.tsx's render path or the public frontend bundle. */
function restartRuleOnCanvas(nodeId: string, rule: InteractionRule): void {
  // The canvas identifies nodes via data-node-id (see CanvasNode.tsx's own
  // selection/resize lookups, e.g. `[data-node-id="${node.id}"]`) — distinct
  // from the .jalpi-{id} class the published frontend renders.
  const el = document.querySelector(`[data-node-id="${nodeId}"]`) as HTMLElement | null;
  if (!el || typeof el.animate !== 'function') return;
  el.getAnimations().forEach((a) => { if (a.id === `jalpi-preview-${rule.id}`) a.cancel(); });
  const s = rule.settings || {};
  const easing = s.easing && (['linear', 'ease', 'ease-in', 'ease-out', 'ease-in-out'].includes(s.easing) || s.easing.startsWith('cubic-bezier')) ? s.easing : 'ease-out';
  const anim = el.animate(buildWaapiPreviewKeyframes(rule.action, s), {
    duration: (parseFloat(s.duration || '0.6') || 0.6) * 1000,
    delay: (parseFloat(s.delay || '0') || 0) * 1000,
    easing,
    fill: 'both',
  });
  anim.id = `jalpi-preview-${rule.id}`;
  anim.onfinish = () => anim.cancel();
}

/* =========================================================
   ADVANCED TAB  (Spacing always on desktop + Responsive overrides)
   ========================================================= */

const MarginIcon = () => (
  <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
    <rect x="6" y="6" width="12" height="12" rx="1"/><line x1="3" y1="6" x2="3" y2="18"/><line x1="21" y1="6" x2="21" y2="18"/><line x1="6" y1="3" x2="18" y2="3"/><line x1="6" y1="21" x2="18" y2="21"/>
  </svg>
);

const PaddingIcon = () => (
  <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
    <rect x="3" y="3" width="18" height="18" rx="2" strokeDasharray="3 2"/><rect x="8" y="8" width="8" height="8" rx="1"/>
  </svg>
);

const ResponsiveIcon = () => (
  <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
    <rect x="2" y="3" width="20" height="14" rx="2"/><path d="M8 21h8M12 17v4"/>
  </svg>
);

function AdvancedTab({ node, store }: { node: CBNode; store: EditorStore }) {
  // Hooks must come before any early returns (React rules of hooks)
  const [bgNH,     setBgNH]     = useState<NHState>('normal');
  const [ovNH,     setOvNH]     = useState<NHState>('normal');
  const [borderNH, setBorderNH] = useState<NHState>('normal');

  if (node.type === 'section' || node.type === 'container' || node.type === 'column') return <SectionSettingsTab node={node} store={store} />;
  const { state } = store;
  const bp = state.breakpoint;

  const isRichEl   = node.type === 'heading' || node.type === 'text';
  const isButton   = node.type === 'button';
  const settings   = (node.settings ?? {}) as Record<string, unknown>;
  const upSettings = (key: string, val: unknown) =>
    store.updateNode(node.id, { settings: { ...settings, [key]: val } });

  const bgKey  = bgNH     === 'normal' ? 'bgNormal'      : 'bgHover';
  const ovKey  = ovNH     === 'normal' ? 'overlayNormal' : 'overlayHover';
  const bdKey  = borderNH === 'normal' ? 'borderNormal'  : 'borderHover';
  const bgData = (settings[bgKey] as Record<string, unknown>) ?? {};
  const ovData = (settings[ovKey] as Record<string, unknown>) ?? {};
  const bdData = (settings[bdKey] as Record<string, unknown>) ?? {};
  const upBd   = (k: string, v: unknown) =>
    upSettings(bdKey, { ...bdData, [k]: v });

  const desktopStyles = node.styles ?? {};
  const updateDesktop = (updates: Record<string, string>) =>
    store.updateNodeStyles(node.id, updates);

  const respKey     = bp === 'tablet' ? 'tabletStyles' : 'mobileStyles';
  const respStyles  = (node[respKey as keyof CBNode] as StyleMap) ?? {};
  const updateResp  = (updates: Record<string, string>) =>
    store.updateNode(node.id, { [respKey]: { ...respStyles, ...updates } });

  // ── Inherited-style indicator (Class-Based Styling System) ──────────────
  // Attached classes resolved in cascade order (store.state.classes is kept
  // sorted by the class manager's own order — see editorStore.ts setClasses).
  const attachedClassKeys = (settings.cssClasses as string[] | undefined) ?? [];
  const attachedClasses   = attachedClassKeys
    .map((k) => store.state.classes.find((c) => c.key === k))
    .filter((c): c is JalpiClass => !!c);

  // Which attached class (if any) supplies a value for one of `keys`, given
  // the node itself has no local override for any of them — local always wins.
  const inheritedFrom = (keys: string[]): string | null => {
    const hasLocal = keys.some((k) => !!(desktopStyles as Record<string, string>)[k]);
    if (hasLocal) return null;
    for (let i = attachedClasses.length - 1; i >= 0; i--) {
      const c = attachedClasses[i];
      if (keys.some((k) => !!(c.styles as Record<string, string> | undefined)?.[k])) return c.name;
    }
    return null;
  };

  return (
    <div className="jalpi-props-section">

      {/* ── Spacing — always shown for desktop editing ── */}
      {bp === 'desktop' && (
        <>
          <PropGroup
            label="Margin"
            icon={<MarginIcon />}
            badge={<InheritedBadge source={inheritedFrom(['marginTop', 'marginRight', 'marginBottom', 'marginLeft'])} />}
          >
            <BoxSpacingControl
              values={{
                top:    desktopStyles.marginTop    || '',
                right:  desktopStyles.marginRight  || '',
                bottom: desktopStyles.marginBottom || '',
                left:   desktopStyles.marginLeft   || '',
              }}
              onChange={(side, val) =>
                updateDesktop({ [`margin${side.charAt(0).toUpperCase() + side.slice(1)}`]: val })
              }
            />
          </PropGroup>

          <PropGroup
            label="Padding"
            icon={<PaddingIcon />}
            badge={<InheritedBadge source={inheritedFrom(isButton
              ? ['outerPaddingTop', 'outerPaddingRight', 'outerPaddingBottom', 'outerPaddingLeft']
              : ['paddingTop', 'paddingRight', 'paddingBottom', 'paddingLeft'])} />}
          >
            <BoxSpacingControl
              values={isButton ? {
                top:    desktopStyles.outerPaddingTop    || '',
                right:  desktopStyles.outerPaddingRight  || '',
                bottom: desktopStyles.outerPaddingBottom || '',
                left:   desktopStyles.outerPaddingLeft   || '',
              } : {
                top:    desktopStyles.paddingTop    || '',
                right:  desktopStyles.paddingRight  || '',
                bottom: desktopStyles.paddingBottom || '',
                left:   desktopStyles.paddingLeft   || '',
              }}
              onChange={(side, val) => {
                const cap = side.charAt(0).toUpperCase() + side.slice(1);
                updateDesktop({ [isButton ? `outerPadding${cap}` : `padding${cap}`]: val });
              }}
            />
          </PropGroup>
        </>
      )}

      {/* ── Width / Background / Overlay / Border — heading & text only ── */}
      {isRichEl && bp === 'desktop' && (
        <>
          {/* Width */}
          <PropGroup label="Width" icon={
            <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
              <line x1="21" y1="12" x2="3" y2="12"/><polyline points="8 7 3 12 8 17"/><polyline points="16 7 21 12 16 17"/>
            </svg>
          }>
            <PropRow label="Width" stack>
              <DimensionInput
                value={(settings.elWidth as string) || ''}
                onChange={(v) => upSettings('elWidth', v)}
                placeholder="100%"
                units={['px', '%', 'rem', 'em', 'vw']}
              />
            </PropRow>
          </PropGroup>

          {/* Background */}
          <PropGroup label="Background" icon={
            <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
              <rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="8.5" cy="8.5" r="1.5" fill="currentColor" stroke="none"/><polyline points="21 15 16 10 5 21"/>
            </svg>
          }>
            <NormalHoverTabs state={bgNH} onChange={setBgNH} />
            <SectionBgEditor data={bgData} onChange={(d) => upSettings(bgKey, d)} />
          </PropGroup>

          {/* Background Overlay */}
          <PropGroup label="Background Overlay" defaultOpen={false} icon={
            <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
              <rect x="3" y="3" width="18" height="18" rx="2" opacity=".4"/><rect x="6" y="6" width="12" height="12" rx="1"/>
            </svg>
          }>
            <NormalHoverTabs state={ovNH} onChange={setOvNH} />
            <SectionBgEditor data={ovData} onChange={(d) => upSettings(ovKey, d)} showOpacity />
          </PropGroup>

          {/* Border */}
          <PropGroup label="Border" defaultOpen={false} icon={
            <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
              <rect x="3" y="3" width="18" height="18" rx="4"/>
            </svg>
          }>
            <NormalHoverTabs state={borderNH} onChange={setBorderNH} />
            <PropRow label="Border Type" stack>
              <Select
                value={(bdData.borderType as string) || 'none'}
                options={[
                  { value: 'none',   label: 'None' },
                  { value: 'solid',  label: 'Solid' },
                  { value: 'dashed', label: 'Dashed' },
                  { value: 'dotted', label: 'Dotted' },
                  { value: 'double', label: 'Double' },
                ]}
                onChange={(v) => upBd('borderType', v)}
              />
            </PropRow>
            {(bdData.borderType as string) && (bdData.borderType as string) !== 'none' && (
              <>
                <PropRow label="Border Width">
                  <BoxSpacingControl
                    values={{
                      top:    (bdData.borderTopWidth    as string) || '',
                      right:  (bdData.borderRightWidth  as string) || '',
                      bottom: (bdData.borderBottomWidth as string) || '',
                      left:   (bdData.borderLeftWidth   as string) || '',
                    }}
                    onChange={(side, val) => {
                      const cap = side.charAt(0).toUpperCase() + side.slice(1);
                      upBd(`border${cap}Width`, val);
                    }}
                    onChangeAll={(val) =>
                      upSettings(bdKey, { ...bdData, borderTopWidth: val, borderRightWidth: val, borderBottomWidth: val, borderLeftWidth: val })
                    }
                  />
                </PropRow>
                <PropRow label="Border Color">
                  <ColorInput
                    value={(bdData.borderColor as string) || '#000000'}
                    onChange={(v) => upBd('borderColor', v)}
                  />
                </PropRow>
              </>
            )}
            <PropRow label="Border Radius">
              <BoxSpacingControl
                values={{
                  top:    (bdData.borderTopLeftRadius     as string) || '',
                  right:  (bdData.borderTopRightRadius    as string) || '',
                  bottom: (bdData.borderBottomRightRadius as string) || '',
                  left:   (bdData.borderBottomLeftRadius  as string) || '',
                }}
                onChange={(side, val) => {
                  const cornerMap: Record<string, string> = {
                    top:    'borderTopLeftRadius',
                    right:  'borderTopRightRadius',
                    bottom: 'borderBottomRightRadius',
                    left:   'borderBottomLeftRadius',
                  };
                  upBd(cornerMap[side], val);
                }}
                onChangeAll={(val) =>
                  upSettings(bdKey, { ...bdData, borderTopLeftRadius: val, borderTopRightRadius: val, borderBottomRightRadius: val, borderBottomLeftRadius: val })
                }
              />
            </PropRow>
            <PropRow label="Box Shadow" stack>
              <TextInput
                value={(bdData.boxShadow as string) || ''}
                onChange={(v) => upBd('boxShadow', v)}
                placeholder="0 4px 16px rgba(0,0,0,0.1)"
              />
            </PropRow>
          </PropGroup>
        </>
      )}

      {/* ── Visibility ── */}
      <VisibilityDeviceRow node={node} store={store} />

      {/* ── Position & Z-index (desktop only) ── */}
      {bp === 'desktop' && (
        <PropGroup label="Position" defaultOpen={false} icon={
          <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
            <circle cx="12" cy="12" r="3"/><line x1="12" y1="2" x2="12" y2="5"/><line x1="12" y1="19" x2="12" y2="22"/>
            <line x1="2" y1="12" x2="5" y2="12"/><line x1="19" y1="12" x2="22" y2="12"/>
          </svg>
        }>
          <PropRow label="Mode" stack>
            <div className="jalpi-toggle-group">
              {(['static', 'relative', 'absolute', 'sticky'] as const).map((p) => (
                <button key={p}
                  className={`jalpi-toggle-group__btn${(desktopStyles.position || 'static') === p ? ' is-active' : ''}`}
                  onClick={() => updateDesktop({ position: p === 'static' ? '' : p })}>
                  {p.charAt(0).toUpperCase() + p.slice(1)}
                </button>
              ))}
            </div>
          </PropRow>
          {desktopStyles.position && desktopStyles.position !== 'static' && (
            <>
              <PropRow label="Top">
                <DimensionInput value={desktopStyles.top || ''} onChange={(v) => updateDesktop({ top: v })} placeholder="auto" units={['px', '%', 'vh', 'rem']} />
              </PropRow>
              <PropRow label="Right">
                <DimensionInput value={desktopStyles.right || ''} onChange={(v) => updateDesktop({ right: v })} placeholder="auto" units={['px', '%', 'vw', 'rem']} />
              </PropRow>
              <PropRow label="Bottom">
                <DimensionInput value={desktopStyles.bottom || ''} onChange={(v) => updateDesktop({ bottom: v })} placeholder="auto" units={['px', '%', 'vh', 'rem']} />
              </PropRow>
              <PropRow label="Left">
                <DimensionInput value={desktopStyles.left || ''} onChange={(v) => updateDesktop({ left: v })} placeholder="auto" units={['px', '%', 'vw', 'rem']} />
              </PropRow>
            </>
          )}
          <PropRow label="Z-Index">
            <TextInput value={desktopStyles.zIndex || ''} onChange={(v) => updateDesktop({ zIndex: v })} placeholder="auto" />
          </PropRow>
        </PropGroup>
      )}

      {/* Hint (desktop only) */}
      {bp === 'desktop' && (
        <div className="jalpi-advanced-hint">
          <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
            <rect x="2" y="3" width="20" height="14" rx="2"/><path d="M8 21h8M12 17v4"/>
          </svg>
          Switch to Tablet or Mobile above to add responsive overrides.
        </div>
      )}

      {/* ── Responsive overrides (tablet / mobile) ── */}
      {bp !== 'desktop' && (
        <>
          <div className="jalpi-props-notice">
            Overrides for <strong>{bp}</strong> only — blank = inherit desktop.
          </div>

          <PropGroup label="Margin" icon={<MarginIcon />}>
            <BoxSpacingControl
              values={{
                top:    respStyles.marginTop    || '',
                right:  respStyles.marginRight  || '',
                bottom: respStyles.marginBottom || '',
                left:   respStyles.marginLeft   || '',
              }}
              onChange={(side, val) =>
                updateResp({ [`margin${side.charAt(0).toUpperCase() + side.slice(1)}`]: val })
              }
            />
          </PropGroup>

          <PropGroup label="Padding" icon={<PaddingIcon />}>
            <BoxSpacingControl
              values={isButton ? {
                top:    respStyles.outerPaddingTop    || '',
                right:  respStyles.outerPaddingRight  || '',
                bottom: respStyles.outerPaddingBottom || '',
                left:   respStyles.outerPaddingLeft   || '',
              } : {
                top:    respStyles.paddingTop    || '',
                right:  respStyles.paddingRight  || '',
                bottom: respStyles.paddingBottom || '',
                left:   respStyles.paddingLeft   || '',
              }}
              onChange={(side, val) => {
                const cap = side.charAt(0).toUpperCase() + side.slice(1);
                updateResp({ [isButton ? `outerPadding${cap}` : `padding${cap}`]: val });
              }}
            />
          </PropGroup>

          <PropGroup label="Responsive Size" icon={<ResponsiveIcon />} defaultOpen={false}>
            <PropRow label="Width">
              <DimensionInput value={respStyles.width || ''} onChange={(v) => updateResp({ width: v })} placeholder="inherit" units={['px', '%', 'vw']} />
            </PropRow>
            <PropRow label="Height">
              <DimensionInput value={respStyles.height || ''} onChange={(v) => updateResp({ height: v })} placeholder="inherit" units={['px', '%', 'vh']} />
            </PropRow>
          </PropGroup>

          <PropGroup label="Responsive Typography" icon={
            <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
              <polyline points="4 7 4 4 20 4 20 7"/><line x1="9" y1="20" x2="15" y2="20"/><line x1="12" y1="4" x2="12" y2="20"/>
            </svg>
          } defaultOpen={false}>
            <PropRow label="Font Size">
              <DimensionInput value={respStyles.fontSize || ''} onChange={(v) => updateResp({ fontSize: v })} placeholder="inherit" units={['px', 'em', 'rem']} />
            </PropRow>
            <PropRow label="Font Weight" stack>
              <Select
                value={respStyles.fontWeight || ''}
                options={[
                  { value: '',    label: '— Inherit —' },
                  { value: '300', label: 'Light 300' },
                  { value: '400', label: 'Regular 400' },
                  { value: '500', label: 'Medium 500' },
                  { value: '600', label: 'SemiBold 600' },
                  { value: '700', label: 'Bold 700' },
                  { value: '800', label: 'ExtraBold 800' },
                ]}
                onChange={(v) => updateResp({ fontWeight: v })}
              />
            </PropRow>
            <PropRow label="Line Height">
              <DimensionInput value={respStyles.lineHeight || ''} onChange={(v) => updateResp({ lineHeight: v })} placeholder="inherit" units={['px', 'em']} />
            </PropRow>
            <PropRow label="Letter Spacing">
              <DimensionInput value={respStyles.letterSpacing || ''} onChange={(v) => updateResp({ letterSpacing: v })} placeholder="inherit" units={['px', 'em']} />
            </PropRow>
            <PropRow label="Align">
              <AlignButtons
                value={respStyles.textAlign || ''}
                onChange={(a) => updateResp({ textAlign: a })}
                options={['left','center','right']}
              />
            </PropRow>
            <PropRow label="Color">
              <ColorInput
                value={respStyles.color || ''}
                onChange={(v) => updateResp({ color: v })}
                placeholder="inherit"
              />
            </PropRow>
          </PropGroup>

          <PropGroup label="Order & Display" defaultOpen={false} icon={
            <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
              <line x1="3" y1="6" x2="21" y2="6"/><line x1="3" y1="12" x2="21" y2="12"/><line x1="3" y1="18" x2="21" y2="18"/>
            </svg>
          }>
            <PropRow label="Order">
              <TextInput
                value={respStyles.order || ''}
                onChange={(v) => updateResp({ order: v })}
                placeholder="0 (auto)"
              />
            </PropRow>
            <PropRow label="Display">
              <Select
                value={respStyles.display || ''}
                options={[
                  { value: '',      label: '— Inherit —' },
                  { value: 'block', label: 'Block' },
                  { value: 'flex',  label: 'Flex' },
                  { value: 'none',  label: 'None (hide)' },
                ]}
                onChange={(v) => updateResp({ display: v })}
              />
            </PropRow>
          </PropGroup>
        </>
      )}

      <CssIdentityGroup node={node} store={store} />
      <CustomCssGroup   node={node} store={store} />

    </div>
  );
}

/* ──────────────────────────────────────────────────────────
   Shared: CSS ID / CSS Class (used in every element's Advanced tab)
   ────────────────────────────────────────────────────────── */
function CssIdentityGroup({ node, store }: { node: CBNode; store: EditorStore }) {
  const settings   = (node.settings ?? {}) as Record<string, unknown>;
  const upSettings = (key: string, val: unknown) =>
    store.updateNode(node.id, { settings: { ...settings, [key]: val } });

  return (
    <PropGroup label="CSS" defaultOpen={false} icon={
      <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
        <polyline points="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 18"/>
      </svg>
    }>
      <PropRow label="Classes" stack>
        <ClassPicker node={node} store={store} />
      </PropRow>
      <PropRow label="CSS ID">
        <TextInput
          value={(settings.cssId as string) || ''}
          onChange={(v) => upSettings('cssId', v)}
          placeholder="my-element-id"
        />
      </PropRow>
      <PropRow label="Custom CSS Class" stack>
        <TextInput
          value={(settings.cssClass as string) || ''}
          onChange={(v) => upSettings('cssClass', v)}
          placeholder="my-class another-class"
        />
      </PropRow>
    </PropGroup>
  );
}

/* ──────────────────────────────────────────────────────────
   Class-Based Styling System — chip picker (attach/detach/search/create)
   ────────────────────────────────────────────────────────── */
function ClassPicker({ node, store }: { node: CBNode; store: EditorStore }) {
  const api = useApi();
  const settings = (node.settings ?? {}) as { cssClasses?: string[] };
  const attachedKeys = settings.cssClasses ?? [];
  const [query, setQuery] = useState('');
  const [open, setOpen] = useState(false);
  const [creating, setCreating] = useState(false);

  const attached = attachedKeys
    .map((key) => store.state.classes.find((c) => c.key === key))
    .filter((c): c is JalpiClass => !!c);

  const suggestions = store.state.classes
    .filter((c) => !attachedKeys.includes(c.key))
    .filter((c) => query === '' || c.name.toLowerCase().includes(query.toLowerCase()))
    .slice(0, 8);

  const exactMatch = suggestions.find((c) => c.name.toLowerCase() === query.trim().toLowerCase())
    ?? store.state.classes.find((c) => c.name.toLowerCase() === query.trim().toLowerCase());

  const attach = (key: string) => {
    store.addClassToNode(node.id, key);
    setQuery('');
    setOpen(false);
  };

  const detach = (key: string) => store.removeClassFromNode(node.id, key);

  const createAndAttach = async () => {
    const name = query.trim();
    if (!name || creating) return;
    setCreating(true);
    try {
      const created = await api.createClass(name);
      store.upsertClass(created);
      attach(created.key);
    } catch {
      // Name collisions etc. are auto-suffixed server-side — a failure here
      // is a network/permission issue, not worth a blocking error for a
      // small inline picker.
    } finally {
      setCreating(false);
    }
  };

  return (
    <div className="jalpi-class-picker">
      <div className="jalpi-class-picker__chips">
        {attached.map((c) => (
          <span
            key={c.key}
            className={`jalpi-class-picker__chip${store.state.editingClassKey === c.key ? ' is-editing' : ''}`}
          >
            <button
              type="button"
              className="jalpi-class-picker__chip-name"
              onClick={() => store.setEditingClassKey(c.key)}
              title="Edit this class's styles — updates every element it's attached to"
            >
              .{c.name}
            </button>
            <button type="button" className="jalpi-class-picker__chip-remove" onClick={() => detach(c.key)} title="Detach">×</button>
          </span>
        ))}
        <input
          type="text"
          className="jalpi-class-picker__input"
          placeholder={attached.length ? 'Add class…' : 'Search or create a class…'}
          value={query}
          onFocus={() => setOpen(true)}
          onChange={(e) => { setQuery(e.target.value); setOpen(true); }}
          onKeyDown={(e) => {
            if (e.key !== 'Enter') return;
            e.preventDefault();
            if (exactMatch) attach(exactMatch.key);
            else if (query.trim()) createAndAttach();
          }}
          onBlur={() => setTimeout(() => setOpen(false), 150)}
        />
      </div>
      {open && (
        <div className="jalpi-class-picker__dropdown">
          {suggestions.map((c) => (
            <button type="button" key={c.key} className="jalpi-class-picker__option" onMouseDown={() => attach(c.key)}>
              .{c.name}
            </button>
          ))}
          {query.trim() && !exactMatch && (
            <button type="button" className="jalpi-class-picker__option jalpi-class-picker__option--create" onMouseDown={createAndAttach} disabled={creating}>
              + Create ".{query.trim()}"
            </button>
          )}
          {!query.trim() && suggestions.length === 0 && (
            <div className="jalpi-class-picker__empty">No other classes yet — type a name to create one.</div>
          )}
        </div>
      )}
      {store.state.editingClassKey && attachedKeys.includes(store.state.editingClassKey) && (
        <ClassStyleEditorModal
          store={store}
          classKey={store.state.editingClassKey}
          onClose={() => store.setEditingClassKey(null)}
        />
      )}
    </div>
  );
}

/* ──────────────────────────────────────────────────────────
   Real Custom CSS field — the `selector` token is replaced server-side
   with this node's own scoped selector (Jalpi_Renderer::maybe_add_custom_css).
   ────────────────────────────────────────────────────────── */
function CustomCssGroup({ node, store }: { node: CBNode; store: EditorStore }) {
  const settings   = (node.settings ?? {}) as Record<string, unknown>;
  const upSettings = (key: string, val: unknown) =>
    store.updateNode(node.id, { settings: { ...settings, [key]: val } });

  return (
    <PropGroup label="Custom CSS" defaultOpen={false} icon={
      <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
        <polyline points="4 17 10 11 4 5"/><line x1="12" y1="19" x2="20" y2="19"/>
      </svg>
    }>
      <PropRow label="CSS" stack>
        <textarea
          className="jalpi-custom-css-textarea"
          rows={6}
          spellCheck={false}
          value={(settings.customCss as string) || ''}
          onChange={(e) => upSettings('customCss', e.target.value)}
          placeholder={'selector {\n  /* your CSS */\n}\nselector:hover {\n  color: blue;\n}'}
        />
      </PropRow>
      <p className="jalpi-custom-css-hint">
        Use <code>selector</code> in place of this element — it's replaced with its own scoped class at render time.
      </p>
    </PropGroup>
  );
}

/* ──────────────────────────────────────────────────────────
   Class-Based Styling System — edit a class's own styles. Reuses the same
   shared controls as the per-element Style/Advanced tabs (TypographyPopup,
   SectionBgEditor, BoxSpacingControl, …) so a class is edited with the exact
   same building blocks that already compile to the same CSS server-side
   (Jalpi_Renderer::compile_class_css reuses styles_to_css/bg_data_to_css/
   border_data_to_css — the same helpers a node's own styles go through).
   Changes are pushed into the shared store immediately (so every attached
   element updates live in the canvas) and persisted to the server debounced,
   mirroring the page autosave pattern.
   ────────────────────────────────────────────────────────── */
function ClassStyleEditorModal({
  store,
  classKey,
  onClose,
}: {
  store:    EditorStore;
  classKey: string;
  onClose:  () => void;
}) {
  const api = useApi();
  const cls = store.state.classes.find((c) => c.key === classKey);
  const [nh, setNh] = useState<NHState>('normal');
  const [typoOpen, setTypoOpen] = useState(false);
  const typoRef = useRef<HTMLButtonElement>(null);
  const saveTimer = useRef<ReturnType<typeof setTimeout> | null>(null);

  useEffect(() => () => { if (saveTimer.current) clearTimeout(saveTimer.current); }, []);

  if (!cls) return null;

  const styles: Record<string, string> = (cls.styles as Record<string, string>) ?? {};
  const settings: NonNullable<JalpiClass['settings']> = cls.settings ?? {};

  const persist = (next: JalpiClass) => {
    store.upsertClass(next);
    if (saveTimer.current) clearTimeout(saveTimer.current);
    saveTimer.current = setTimeout(() => {
      api.updateClass(classKey, {
        styles: next.styles,
        tabletStyles: next.tabletStyles,
        mobileStyles: next.mobileStyles,
        settings: next.settings,
      }).catch(() => {});
    }, 500);
  };

  const updStyles = (updates: Record<string, string>) => persist({ ...cls, styles: { ...styles, ...updates } });
  const updSettings = (key: string, val: unknown) => persist({ ...cls, settings: { ...settings, [key]: val } });

  const bgKey = nh === 'normal' ? 'bgNormal' : 'bgHover';
  const bdKey = nh === 'normal' ? 'borderNormal' : 'borderHover';
  const bgData = ((nh === 'normal' ? settings.bgNormal : settings.bgHover) ?? {}) as unknown as Record<string, unknown>;
  const bdData = ((nh === 'normal' ? settings.borderNormal : settings.borderHover) ?? {}) as unknown as Record<string, unknown>;

  let usageCount = 0;
  const walkUsage = (nodes: CBNode[]) => {
    for (const n of nodes) {
      const attached = (n.settings as { cssClasses?: string[] } | undefined)?.cssClasses ?? [];
      if (attached.includes(classKey)) usageCount++;
      if (n.children?.length) walkUsage(n.children);
    }
  };
  walkUsage(store.state.pageData.sections);

  return ReactDOM.createPortal(
    <div className="jalpi-modal-overlay" onClick={onClose}>
      <div className="jalpi-modal jalpi-class-style-editor" onClick={(e) => e.stopPropagation()}>
        <div className="jalpi-class-style-editor__header">
          <h3>.{cls.name}</h3>
          <button type="button" className="jalpi-modal__close" onClick={onClose} aria-label="Close">
            <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round"><path d="M18 6L6 18M6 6l12 12" /></svg>
          </button>
        </div>
        <p className="jalpi-class-style-editor__usage">
          Used on {usageCount} element{usageCount === 1 ? '' : 's'} on this page — changes apply everywhere this class is attached.
        </p>

        <div className="jalpi-class-style-editor__body">
          <PropGroup label="Typography" icon={
            <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M4 12h16M4 6h16M4 18h8" /></svg>
          }>
            <TypographyTriggerRow label="Font" isOpen={typoOpen} triggerRef={typoRef} onToggle={() => setTypoOpen((o) => !o)} />
            {typoOpen && (
              <TypographyPopup
                styles={styles as Record<string, string>}
                update={updStyles}
                fonts={jalpiEditorConfig.fonts ?? []}
                respKey={null}
                onClose={() => setTypoOpen(false)}
                anchorRef={typoRef}
              />
            )}
            <PropRow label="Color">
              <ColorInput
                value={(nh === 'normal' ? styles.color : styles.colorHover) || ''}
                onChange={(v) => updStyles(nh === 'normal' ? { color: v } : { colorHover: v })}
              />
            </PropRow>
            <NormalHoverTabs state={nh} onChange={setNh} />
          </PropGroup>

          <PropGroup label="Background" defaultOpen={false} icon={
            <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><rect x="3" y="3" width="18" height="18" rx="2" /><circle cx="8.5" cy="8.5" r="1.5" fill="currentColor" stroke="none" /><polyline points="21 15 16 10 5 21" /></svg>
          }>
            <NormalHoverTabs state={nh} onChange={setNh} />
            <SectionBgEditor data={bgData} onChange={(d) => updSettings(bgKey, d)} />
          </PropGroup>

          <PropGroup label="Border" defaultOpen={false} icon={
            <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><rect x="3" y="3" width="18" height="18" rx="4" /></svg>
          }>
            <NormalHoverTabs state={nh} onChange={setNh} />
            <PropRow label="Type" stack>
              <Select
                value={(bdData.borderType as string) || 'none'}
                options={[
                  { value: 'none', label: 'None' }, { value: 'solid', label: 'Solid' },
                  { value: 'dashed', label: 'Dashed' }, { value: 'dotted', label: 'Dotted' }, { value: 'double', label: 'Double' },
                ]}
                onChange={(v) => updSettings(bdKey, { ...bdData, borderType: v })}
              />
            </PropRow>
            {(bdData.borderType as string) && (bdData.borderType as string) !== 'none' && (
              <>
                <PropRow label="Width">
                  <BoxSpacingControl
                    values={{
                      top: (bdData.borderTopWidth as string) || '', right: (bdData.borderRightWidth as string) || '',
                      bottom: (bdData.borderBottomWidth as string) || '', left: (bdData.borderLeftWidth as string) || '',
                    }}
                    onChange={(side, val) => updSettings(bdKey, { ...bdData, [`border${side.charAt(0).toUpperCase() + side.slice(1)}Width`]: val })}
                  />
                </PropRow>
                <PropRow label="Color">
                  <ColorInput value={(bdData.borderColor as string) || '#000000'} onChange={(v) => updSettings(bdKey, { ...bdData, borderColor: v })} />
                </PropRow>
              </>
            )}
            <PropRow label="Radius">
              <DimensionInput value={(bdData.borderRadius as string) || ''} onChange={(v) => updSettings(bdKey, { ...bdData, borderRadius: v })} />
            </PropRow>
          </PropGroup>

          <PropGroup label="Spacing" defaultOpen={false} icon={
            <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><rect x="3" y="3" width="18" height="18" rx="2" /></svg>
          }>
            <PropRow label="Margin">
              <BoxSpacingControl
                values={{ top: styles.marginTop || '', right: styles.marginRight || '', bottom: styles.marginBottom || '', left: styles.marginLeft || '' }}
                onChange={(side, val) => updStyles({ [`margin${side.charAt(0).toUpperCase() + side.slice(1)}`]: val })}
              />
            </PropRow>
            <PropRow label="Padding">
              <BoxSpacingControl
                values={{ top: styles.paddingTop || '', right: styles.paddingRight || '', bottom: styles.paddingBottom || '', left: styles.paddingLeft || '' }}
                onChange={(side, val) => updStyles({ [`padding${side.charAt(0).toUpperCase() + side.slice(1)}`]: val })}
              />
            </PropRow>
          </PropGroup>
        </div>
      </div>
    </div>,
    document.body
  );
}


/* =========================================================
   Shared UI components
   ========================================================= */

/* Collapsible section group */
/* =========================================================
   Accordion context — one scope per tab, reset on node change.
   PropGroups inside an AccordionScope auto-collapse siblings.
   ========================================================= */
interface AccordionCtx {
  openLabel:    string | null;
  setOpenLabel: React.Dispatch<React.SetStateAction<string | null>>;
}
const AccordionContext = React.createContext<AccordionCtx | null>(null);

function AccordionScope({
  children,
  defaultGroup = null,
}: {
  children:      React.ReactNode;
  defaultGroup?: string | null;
}) {
  const [openLabel, setOpenLabel] = useState<string | null>(defaultGroup);
  return (
    <AccordionContext.Provider value={{ openLabel, setOpenLabel }}>
      {children}
    </AccordionContext.Provider>
  );
}

function PropGroup({
  label,
  icon,
  children,
  defaultOpen = true,
  badge,
  standalone = false,
}: {
  label:        string;
  icon?:        React.ReactElement;
  children:     React.ReactNode;
  defaultOpen?: boolean;
  /** Optional inline indicator (e.g. "inherited from .card") next to the title. */
  badge?:       React.ReactNode;
  /** Opt out of the enclosing AccordionScope and use independent local
   * open/close state instead. Needed anywhere the same label can appear more
   * than once in one scope (e.g. one "Settings" group per interaction rule)
   * — accordion mode keys purely off the label string, so repeated labels
   * would otherwise all open/close together. */
  standalone?:  boolean;
}) {
  const accordion  = React.useContext(AccordionContext);
  const [localOpen, setLocalOpen] = useState(defaultOpen);

  // Accordion mode: controlled by context; else use local state
  const open   = !standalone && accordion ? accordion.openLabel === label : localOpen;
  const toggle = () => {
    if (!standalone && accordion) {
      accordion.setOpenLabel(prev => prev === label ? null : label);
    } else {
      setLocalOpen(o => !o);
    }
  };

  return (
    <div className={`jalpi-prop-group${open ? ' is-open' : ''}`}>
      <button type="button" className="jalpi-prop-group__header" onClick={toggle}>
        <div className="jalpi-prop-group__header-left">
          {icon && <span className="jalpi-prop-group__icon">{icon}</span>}
          <span className="jalpi-prop-group__title">{label}</span>
          {badge}
        </div>
        <svg
          className={`jalpi-prop-group__chevron${open ? ' is-open' : ''}`}
          width="12" height="12" viewBox="0 0 24 24" fill="none"
          stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round"
        >
          <path d="m6 9 6 6 6-6"/>
        </svg>
      </button>
      {open && <div className="jalpi-prop-group__body">{children}</div>}
    </div>
  );
}

/* Small "inherited from .class-name" pill shown on a PropGroup title when
 * none of its properties are set locally but an attached class supplies one. */
function InheritedBadge({ source }: { source: string | null }) {
  if (!source) return null;
  return (
    <span className="jalpi-inherited-badge" title={`Inherited from .${source} — set a value here to override it locally`}>
      from .{source}
    </span>
  );
}

/* Label + control row */
function PropRow({ label, children, stack, responsive }: { label: string; children: React.ReactNode; stack?: boolean; responsive?: boolean }) {
  return (
    <div className={`jalpi-prop-row${stack ? ' jalpi-prop-row--stack' : ''}`}>
      <label className="jalpi-prop-row__label" title={label}>
        {label}
        {responsive && (
          <svg className="jalpi-prop-row__resp-icon" width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-label="Responsive">
            <rect x="2" y="3" width="20" height="14" rx="2"/><path d="M8 21h8M12 17v4"/>
          </svg>
        )}
      </label>
      <div className="jalpi-prop-row__control">{children}</div>
    </div>
  );
}

/* ── Icon Style Section (Elementor-style) ──────────────────────────────── */
function IconStyleSection({
  styles,
  update,
}: {
  styles: import('../../types').StyleMap;
  update: (u: Record<string, string>) => void;
}) {
  const [nh, setNh] = useState<NHState>('normal');

  // Extract rotate degrees from transform, e.g. "rotate(45deg)" → "45"
  const rotateDeg = (() => {
    const t = styles.iconRotate || '';
    return t.replace(/[^0-9.-]/g, '') || '';
  })();

  const setRotate = (v: string) => {
    const n = parseFloat(v);
    update({ iconRotate: isNaN(n) ? '' : `${n}deg` });
  };

  return (
    <>
      {/* Alignment */}
      <PropRow label="Alignment" responsive>
        <AlignButtons
          value={styles.textAlign || ''}
          onChange={(a) => update({ textAlign: a })}
          options={['left', 'center', 'right']}
        />
      </PropRow>

      {/* Normal / Hover tabs */}
      <NormalHoverTabs state={nh} onChange={setNh} />

      {/* Primary Color */}
      <ColorPropRow
        label="Primary Color"
        value={nh === 'normal' ? (styles.color || '') : (styles.iconColorHover || '')}
        onChange={(v) => update(nh === 'normal' ? { color: v } : { iconColorHover: v })}
        placeholder="currentColor"
      />

      {/* Secondary Color */}
      <ColorPropRow
        label="Secondary Color"
        value={nh === 'normal' ? (styles.iconBoxColor || '') : (styles.iconBoxColorHover || '')}
        onChange={(v) => update(nh === 'normal' ? { iconBoxColor: v } : { iconBoxColorHover: v })}
        placeholder="#f0f0f0"
      />

      {/* Size */}
      <SliderPropRow
        label="Size"
        responsive
        value={styles.iconSize || ''}
        onChange={(v) => update({ iconSize: v })}
        min={8} max={300} step={1}
        units={['px', 'em']}
        placeholder="48"
      />

      {/* Padding */}
      <SliderPropRow
        label="Padding"
        value={styles.iconPadding || ''}
        onChange={(v) => update({ iconPadding: v })}
        min={0} max={120} step={1}
        units={['px']}
        placeholder="0"
      />

      {/* Rotate */}
      <SliderPropRow
        label="Rotate"
        responsive
        value={rotateDeg ? `${rotateDeg}deg` : ''}
        onChange={setRotate}
        min={0} max={360} step={1}
        units={['deg']}
        placeholder="0"
      />
    </>
  );
}

/* Elementor-style 2-row slider row: label + unit on top, slider + number on bottom */
function SliderPropRow({
  label,
  value,
  onChange,
  min = 0,
  max = 200,
  step = 1,
  units = ['px', 'em', 'rem'],
  placeholder = '0',
  responsive = false,
  link,
}: {
  label:        string;
  value:        string;
  onChange:     (v: string) => void;
  min?:         number;
  max?:         number;
  step?:        number;
  units?:       string[];
  placeholder?: string;
  responsive?:  boolean;
  /** Optional — same Design System token linking pattern as ColorPropRow's `link` prop. */
  link?: { store: EditorStore; node: CBNode; styleProp: string };
}) {
  const linkCategory = link ? tokenCategoryFor(link.styleProp) : undefined;
  const extractUnit = (v: string) => {
    for (const u of ['rem', 'em', 'px', '%', 'vw', 'vh']) {
      if (v.endsWith(u) && units.includes(u)) return u;
    }
    return units[0];
  };

  const isEmpty    = !value;
  const activeUnit = isEmpty ? units[0] : extractUnit(value);
  const numVal     = isEmpty ? 0 : parseFloat(value) || 0;
  const rangeMax   = (activeUnit === '%' || activeUnit === 'vh' || activeUnit === 'vw') ? 100 : max;

  const setNum  = (n: number | string) => onChange(n === '' ? '' : `${n}${activeUnit}`);
  const setUnit = (u: string) => onChange(numVal ? `${numVal}${u}` : `0${u}`);

  return (
    <div className="jalpi-slprop">
      <div className="jalpi-slprop__header">
        <span className="jalpi-slprop__label">
          {label}
          {responsive && (
            <svg className="jalpi-prop-row__resp-icon" width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-label="Responsive">
              <rect x="2" y="3" width="20" height="14" rx="2"/><path d="M8 21h8M12 17v4"/>
            </svg>
          )}
        </span>
        {link && linkCategory && (
          <TokenLinkControl store={link.store} node={link.node} styleProp={link.styleProp} category={linkCategory} />
        )}
        <select
          className="jalpi-slprop__unit"
          value={activeUnit}
          onChange={(e) => setUnit(e.target.value)}
        >
          {units.map((u) => <option key={u} value={u}>{u}</option>)}
        </select>
      </div>
      <div className="jalpi-slprop__body">
        <input
          type="range"
          className="jalpi-slprop__track"
          min={min} max={rangeMax} step={step} value={numVal}
          style={{ '--pct': `${Math.min(100, (numVal / rangeMax) * 100)}%` } as React.CSSProperties}
          onChange={(e) => setNum(e.target.value)}
        />
        <input
          type="number"
          className="jalpi-slprop__num"
          value={isEmpty ? '' : numVal}
          placeholder={placeholder}
          min={min}
          onChange={(e) => {
            const n = e.target.value;
            if (n === '') { onChange(''); return; }
            onChange(parseFloat(n) + activeUnit);
          }}
        />
      </div>
    </div>
  );
}

/* Dimension input with px / % / other unit toggle */
function DimensionInput({
  value,
  onChange,
  placeholder = 'auto',
  units = ['px', '%'],
}: {
  value:        string;
  onChange:     (v: string) => void;
  placeholder?: string;
  units?:       string[];
}) {
  // The "pending" unit is what will be appended when the user starts typing from
  // an empty field. It stays in local state so cycling units while empty works.
  const [pendingUnit, setPendingUnit] = useState<string>(units[0]);

  // Extract the unit currently embedded in the value string.
  const extractUnit = (v: string): string | null => {
    for (const u of ['dvh', 'vw', 'vh', 'rem', 'em', 'px', '%']) {
      if (v.endsWith(u) && units.includes(u)) return u;
    }
    return null;
  };

  const isEmpty = !value;
  const isAuto  = value === 'auto';

  // Active unit: prefer what's in the value, fall back to pending.
  const activeUnit = (!isEmpty && !isAuto && extractUnit(value)) ? extractUnit(value)! : pendingUnit;

  const numPart = (!isEmpty && !isAuto) ? value.replace(/[^0-9.-]/g, '') : '';

  const cycleUnit = () => {
    const idx  = units.indexOf(activeUnit);
    const next = units[(idx + 1) % units.length];
    setPendingUnit(next);
    // If a number exists, emit the updated value immediately.
    if (numPart) onChange(numPart + next);
    // If empty, just update the pending unit (badge changes, ready for typing).
  };

  return (
    <div className="jalpi-dim-input">
      <input
        type="text"
        className="jalpi-dim-input__num"
        value={isAuto ? '' : numPart}
        placeholder={isAuto ? 'auto' : placeholder}
        onChange={(e) => {
          const v = e.target.value.trim();
          if (v === '')     { onChange('');     return; }
          if (v === 'auto') { onChange('auto'); return; }
          // Accept raw CSS (e.g. typed "50%") or append active unit.
          onChange(v.match(/[a-z%]$/i) ? v : v + activeUnit);
        }}
      />
      <button
        type="button"
        className={`jalpi-dim-input__unit${isEmpty ? ' is-unset' : ''}`}
        onClick={cycleUnit}
        title="Click to cycle units"
      >
        {activeUnit}
      </button>
    </div>
  );
}

/* Reusable "link to global Design System token" control — dropdown of
 * matching-category tokens + unlink, modeled on App.tsx's PresetLinker.
 * A literal value written via the control's own onChange (e.g. picking a
 * color) always takes visual precedence over the link, both in the canvas
 * preview (CanvasNode.tsx) and on the live frontend (Jalpi_Renderer::
 * resolve_token_refs) — the link stays stored but has no effect until the
 * literal is cleared, exactly like the existing typography/button preset
 * linking behavior in App.tsx's PresetLinker.
 */
function TokenLinkControl({
  store,
  node,
  styleProp,
  category,
}: {
  store: EditorStore;
  node: CBNode;
  styleProp: string;
  category: TokenCategory;
}) {
  const [open, setOpen] = useState(false);
  const tokens = store.state.designSystem[category] as DesignToken[];
  const ref = node.globalStyleRef?.tokens?.[styleProp];
  const linked = ref ? tokens.find((t) => t.id === ref.id) : null;

  return (
    <div className="jalpi-token-link">
      <button
        type="button"
        className={`jalpi-el-control-row__globe${linked ? ' is-linked' : ''}`}
        title={linked ? `Linked: ${linked.name}` : 'Link to global token'}
        onClick={(e) => { e.stopPropagation(); setOpen((o) => !o); }}
      >
        <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round">
          <circle cx="12" cy="12" r="10"/>
          <line x1="2" y1="12" x2="22" y2="12"/>
          <path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/>
        </svg>
      </button>
      {open && (
        <select
          autoFocus
          className="jalpi-token-link__select"
          value={ref?.id ?? ''}
          onBlur={() => setOpen(false)}
          onChange={(e) => {
            if (e.target.value) store.linkNodeToken(node.id, styleProp, category, e.target.value);
            else store.unlinkNodeToken(node.id, styleProp);
            setOpen(false);
          }}
        >
          <option value="">— None —</option>
          {tokens.map((t) => <option key={t.id} value={t.id}>{t.name} ({t.value})</option>)}
        </select>
      )}
    </div>
  );
}

/* Elementor-style color property row: label | globe | swatch */
function ColorPropRow({
  label,
  value,
  onChange,
  placeholder = '#000000',
  link,
}: {
  label:        string;
  value:        string;
  onChange:     (v: string) => void;
  placeholder?: string;
  /** Optional — when provided, the globe button becomes a live Design System
   *  color-token linker instead of the inert placeholder. Omit to keep the
   *  existing (harmless) inert globe. */
  link?: { store: EditorStore; node: CBNode; styleProp: string };
}) {
  const swatchColor = value && value.startsWith('#') ? value : placeholder;
  const linkCategory = link ? tokenCategoryFor(link.styleProp) : undefined;
  return (
    <div className="jalpi-color-prop-row">
      <span className="jalpi-color-prop-row__label">{label}</span>
      <div className="jalpi-color-prop-row__right">
        {link && linkCategory ? (
          <TokenLinkControl store={link.store} node={link.node} styleProp={link.styleProp} category={linkCategory} />
        ) : (
          <button type="button" className="jalpi-el-control-row__globe" title="Global color" tabIndex={-1}>
            <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round">
              <circle cx="12" cy="12" r="10"/>
              <line x1="2" y1="12" x2="22" y2="12"/>
              <path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/>
            </svg>
          </button>
        )}
        <label className="jalpi-color-prop-row__swatch-wrap" title={value || placeholder} style={{ cursor: 'pointer', position: 'relative', display: 'inline-flex' }}>
          <span className="jalpi-color-prop-row__swatch" style={{ background: swatchColor }} />
          <input
            type="color"
            value={swatchColor}
            onChange={(e) => onChange(e.target.value)}
            style={{ position: 'absolute', inset: 0, opacity: 0, width: '100%', height: '100%', cursor: 'pointer', border: 'none', padding: 0 }}
          />
        </label>
      </div>
    </div>
  );
}

/* Color swatch + hex text field */
function ColorInput({
  value,
  onChange,
  placeholder = '#000000',
}: {
  value:        string;
  onChange:     (v: string) => void;
  placeholder?: string;
}) {
  return (
    <div className="jalpi-color-input">
      <input
        type="color"
        className="jalpi-color-input__swatch"
        value={value && value.startsWith('#') ? value : '#000000'}
        onChange={(e) => onChange(e.target.value)}
        title="Pick a color"
      />
      <input
        type="text"
        className="jalpi-color-input__text"
        value={value}
        onChange={(e) => onChange(e.target.value)}
        placeholder={placeholder}
      />
    </div>
  );
}

/* Visual spacing control — clean 2×2 grid + link-all toggle */
function BoxSpacingControl({
  values,
  onChange,
  onChangeAll,
}: {
  values:        { top: string; right: string; bottom: string; left: string };
  onChange:      (side: string, val: string) => void;
  onChangeAll?:  (val: string) => void;
}) {
  // Default linked when all non-empty sides share the same value (or all are empty).
  const vals = [values.top, values.right, values.bottom, values.left];
  const nonEmpty = vals.filter((v) => v !== '');
  const allEqual = nonEmpty.length === 0 || nonEmpty.every((v) => v === nonEmpty[0]);
  const [linked, setLinked] = useState(allEqual);
  // When linked, show the shared representative value in every field.
  const repValue = linked ? (nonEmpty[0] ?? '') : null;

  const handle = (side: string, val: string) => {
    if (linked) {
      if (onChangeAll) {
        onChangeAll(val);
      } else {
        (['top', 'right', 'bottom', 'left'] as const).forEach((s) => onChange(s, val));
      }
    } else {
      onChange(side, val);
    }
  };

  const toggleLinked = () => {
    if (!linked) {
      // Switching to linked — sync all sides to the first non-empty value in one call
      const rep = vals.find((v) => v !== '') || '';
      if (rep) {
        if (onChangeAll) {
          onChangeAll(rep);
        } else {
          (['top', 'right', 'bottom', 'left'] as const).forEach((s) => onChange(s, rep));
        }
      }
    }
    setLinked((v) => !v);
  };

  const handleKeyDown = (side: string) => (e: React.KeyboardEvent<HTMLInputElement>) => {
    if (e.key !== 'ArrowUp' && e.key !== 'ArrowDown') return;
    e.preventDefault();
    const step       = e.shiftKey ? 10 : 1;
    const dir        = e.key === 'ArrowUp' ? 1 : -1;
    const currentVal = e.currentTarget.value;
    const match      = currentVal.match(/^(-?[\d.]+)(.*)$/);
    if (match) {
      const num    = parseFloat(match[1]);
      const unit   = match[2] || 'px';
      const newNum = Math.max(0, num + dir * step);
      handle(side, `${newNum}${unit}`);
    } else {
      handle(side, `${Math.max(0, dir * step)}px`);
    }
  };

  const fields: { key: keyof typeof values; label: string }[] = [
    { key: 'top',    label: 'Top' },
    { key: 'right',  label: 'Right' },
    { key: 'bottom', label: 'Bottom' },
    { key: 'left',   label: 'Left' },
  ];

  return (
    <div className="jalpi-spacing-grid">
      <div className="jalpi-spacing-grid__row">
        <div className="jalpi-spacing-grid__fields">
          {fields.map(({ key, label }) => (
            <div key={key} className="jalpi-spacing-field">
              <input
                type="text"
                className="jalpi-spacing-field__input"
                value={repValue !== null ? repValue : values[key]}
                onChange={(e) => handle(key, e.target.value)}
                onKeyDown={handleKeyDown(key)}
                placeholder="0"
              />
              <span className="jalpi-spacing-field__label">{label}</span>
            </div>
          ))}
        </div>
        <button
          type="button"
          className={`jalpi-spacing-grid__link${linked ? ' is-linked' : ''}`}
          onClick={toggleLinked}
          title={linked ? 'Unlink sides' : 'Link all sides equal'}
        >
          <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round">
            {linked ? (
              <>
                <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/>
                <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/>
              </>
            ) : (
              <>
                <path d="M18.84 12.25l1.72-1.71a5.004 5.004 0 0 0-.12-7.07 5.006 5.006 0 0 0-6.95 0l-1.72 1.71"/>
                <path d="M5.17 11.75l-1.71 1.71a5.004 5.004 0 0 0 .12 7.07 5.006 5.006 0 0 0 6.95 0l1.72-1.71"/>
                <line x1="8" y1="2" x2="8" y2="5"/>
                <line x1="2" y1="8" x2="5" y2="8"/>
                <line x1="16" y1="19" x2="16" y2="22"/>
                <line x1="19" y1="16" x2="22" y2="16"/>
              </>
            )}
          </svg>
        </button>
      </div>
    </div>
  );
}

/* Text input */
function TextInput({
  value,
  onChange,
  placeholder,
  inputRef,
}: {
  value:        string;
  onChange:     (v: string) => void;
  placeholder?: string;
  inputRef?:    React.RefObject<HTMLInputElement>;
}) {
  return (
    <input
      ref={inputRef}
      type="text"
      className="jalpi-input"
      value={value}
      onChange={(e) => onChange(e.target.value)}
      placeholder={placeholder}
    />
  );
}

/* Select */
function Select({
  value,
  options,
  onChange,
}: {
  value:   string;
  options: Array<{ value: string; label: string }>;
  onChange: (v: string) => void;
}) {
  return (
    <select className="jalpi-select" value={value} onChange={(e) => onChange(e.target.value)}>
      {options.map((opt) => (
        <option key={opt.value} value={opt.value}>{opt.label}</option>
      ))}
    </select>
  );
}

/* ── Google Font loader ────────────────────────────────────────────── */

const _loadedGFonts = new Set<string>();

/** Inject a Google Fonts <link> tag so the font renders in the editor. */
function loadGoogleFont(family: string) {
  if (!family || _loadedGFonts.has(family)) return;
  _loadedGFonts.add(family);
  const link = document.createElement('link');
  link.rel = 'stylesheet';
  link.href = `https://fonts.googleapis.com/css2?family=${encodeURIComponent(family)}:wght@400;500;600;700&display=swap`;
  document.head.appendChild(link);
}

/* Category labels for the grouped dropdown */
const FONT_CATEGORY_LABELS: Record<string, string> = {
  'sans-serif':  'Sans Serif',
  'serif':       'Serif',
  'display':     'Display',
  'handwriting': 'Handwriting',
  'monospace':   'Monospace',
};

/* Searchable font family picker with category groups and Google Font preview */
function FontFamilySearch({ value, onChange }: { value: string; onChange: (v: string) => void }) {
  const fonts = (jalpiEditorConfig as any).fonts ?? [];
  const [query, setQuery]   = useState('');
  const [open,  setOpen]    = useState(false);
  const wrapRef             = useRef<HTMLDivElement>(null);
  const inputRef            = useRef<HTMLInputElement>(null);

  type FontEntry = { value: string; label: string; category?: string; source?: string };
  const allOptions: FontEntry[] = [{ value: '', label: '— Default —' }, ...fonts];
  const q = query.toLowerCase();
  const filtered = allOptions.filter((f) => f.label.toLowerCase().includes(q));

  const displayLabel = value
    ? (fonts.find((f: FontEntry) => f.value === value)?.label ?? value)
    : '— Default —';

  // Load the selected font on mount so the trigger renders in the right typeface.
  useEffect(() => {
    if (value) {
      const entry = fonts.find((f: FontEntry) => f.value === value);
      if (entry?.source === 'google') loadGoogleFont(entry.label);
    }
  }, [value]);

  useEffect(() => {
    const handler = (e: MouseEvent) => {
      if (wrapRef.current && !wrapRef.current.contains(e.target as Node)) {
        setOpen(false);
        setQuery('');
      }
    };
    document.addEventListener('mousedown', handler);
    return () => document.removeEventListener('mousedown', handler);
  }, []);

  const openDropdown = () => {
    setOpen(true);
    requestAnimationFrame(() => inputRef.current?.focus());
  };

  const handleSelect = (f: FontEntry) => {
    if (f.source === 'google') loadGoogleFont(f.label);
    onChange(f.value);
    setOpen(false);
    setQuery('');
  };

  // Group filtered fonts by category for the dropdown.
  const renderGroupedList = () => {
    if (filtered.length === 0) {
      return <div className="jalpi-font-search__empty">No fonts found</div>;
    }

    // When searching, show a flat filtered list (no category headers).
    if (q) {
      return filtered.map((f) => (
        <button
          key={f.value}
          type="button"
          className={`jalpi-font-search__option${value === f.value ? ' is-active' : ''}`}
          style={{ fontFamily: f.value || undefined }}
          onMouseDown={(e) => { e.preventDefault(); handleSelect(f); }}
          onMouseEnter={() => { if (f.source === 'google') loadGoogleFont(f.label); }}
        >
          {f.label}
        </button>
      ));
    }

    // No search query — group by category.
    const special  = filtered.filter((f) => !f.category || f.category === 'system');
    const grouped: Record<string, FontEntry[]> = {};
    for (const f of filtered) {
      const cat = f.category;
      if (!cat || cat === 'system') continue;
      if (!grouped[cat]) grouped[cat] = [];
      grouped[cat].push(f);
    }

    const items: React.ReactNode[] = [];

    // Special entries first (Default, System Default).
    for (const f of special) {
      items.push(
        <button
          key={f.value}
          type="button"
          className={`jalpi-font-search__option${value === f.value ? ' is-active' : ''}`}
          style={{ fontFamily: f.value || undefined }}
          onMouseDown={(e) => { e.preventDefault(); handleSelect(f); }}
        >
          {f.label}
        </button>
      );
    }

    // Category groups.
    const categoryOrder = ['sans-serif', 'serif', 'display', 'handwriting', 'monospace'];
    for (const cat of categoryOrder) {
      const catFonts = grouped[cat];
      if (!catFonts?.length) continue;
      items.push(
        <div key={`cat-${cat}`} className="jalpi-font-search__category">
          {FONT_CATEGORY_LABELS[cat] ?? cat}
        </div>
      );
      for (const f of catFonts) {
        items.push(
          <button
            key={f.value}
            type="button"
            className={`jalpi-font-search__option${value === f.value ? ' is-active' : ''}`}
            style={{ fontFamily: f.value || undefined }}
            onMouseDown={(e) => { e.preventDefault(); handleSelect(f); }}
            onMouseEnter={() => { if (f.source === 'google') loadGoogleFont(f.label); }}
          >
            {f.label}
          </button>
        );
      }
    }
    return items;
  };

  return (
    <div className="jalpi-font-search" ref={wrapRef}>
      <button type="button" className="jalpi-font-search__trigger" onClick={openDropdown}>
        <span style={{ fontFamily: value || undefined }}>{displayLabel}</span>
        <svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
          <path d="m6 9 6 6 6-6"/>
        </svg>
      </button>
      {open && (
        <div className="jalpi-font-search__dropdown">
          <div className="jalpi-font-search__search-wrap">
            <svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
              <circle cx="11" cy="11" r="8"/><path d="m21 21-4.35-4.35"/>
            </svg>
            <input
              ref={inputRef}
              className="jalpi-font-search__input"
              placeholder="Search fonts…"
              value={query}
              onChange={(e) => setQuery(e.target.value)}
            />
          </div>
          <div className="jalpi-font-search__list">
            {renderGroupedList()}
          </div>
        </div>
      )}
    </div>
  );
}

/* Shared Elementor-style control trigger row (label | globe | pencil) */
function TypographyTriggerRow({
  label,
  isOpen,
  triggerRef,
  onToggle,
}: {
  label:      string;
  isOpen:     boolean;
  triggerRef: React.RefObject<HTMLButtonElement>;
  onToggle:   () => void;
}) {
  return (
    <div className="jalpi-el-control-row">
      <span className="jalpi-el-control-row__label">{label}</span>
      <div className="jalpi-el-control-row__actions">
        {/* Globe — placeholder for future global-style linkage */}
        <button type="button" className="jalpi-el-control-row__globe" title="Global typography" tabIndex={-1}>
          <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round">
            <circle cx="12" cy="12" r="10"/>
            <line x1="2" y1="12" x2="22" y2="12"/>
            <path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/>
          </svg>
        </button>
        {/* Pencil — opens the popup */}
        <button
          ref={triggerRef}
          type="button"
          className={`jalpi-el-control-row__edit${isOpen ? ' is-active' : ''}`}
          title={`Edit ${label}`}
          onClick={onToggle}
        >
          <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
            <path d="M12 20h9"/><path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z"/>
          </svg>
        </button>
      </div>
    </div>
  );
}

/* Typography popup card (renders via portal, positioned to the left of the panel) */
function TypographyPopup({
  styles,
  update,
  fonts,
  respKey,
  onClose,
  anchorRef,
}: {
  styles:    Record<string, string>;
  update:    (u: Record<string, string>) => void;
  fonts:     Array<{ label: string; value: string }>;
  respKey:   'tabletStyles' | 'mobileStyles' | null;
  onClose:   () => void;
  anchorRef: React.RefObject<HTMLButtonElement>;
}) {
  const popupRef = useRef<HTMLDivElement>(null);
  const [pos, setPos] = useState<{ top: number; right: number } | null>(null);

  useEffect(() => {
    if (!anchorRef.current) return;
    const r = anchorRef.current.getBoundingClientRect();
    const POPUP_H = 540;
    const top   = Math.max(10, Math.min(r.top - 40, window.innerHeight - POPUP_H - 10));
    const right = window.innerWidth - r.left + 10;
    setPos({ top, right });
  }, [anchorRef]);

  useEffect(() => {
    const handler = (e: MouseEvent) => {
      if (
        popupRef.current   && !popupRef.current.contains(e.target as Node) &&
        anchorRef.current  && !anchorRef.current.contains(e.target as Node)
      ) onClose();
    };
    document.addEventListener('mousedown', handler);
    return () => document.removeEventListener('mousedown', handler);
  }, [onClose, anchorRef]);

  const reset = () => update({
    fontFamily: '', fontSize: '', fontWeight: '', textTransform: '',
    fontStyle: '', textDecoration: '', lineHeight: '', letterSpacing: '', wordSpacing: '',
  });

  const respIcon = respKey ? (
    <svg className="jalpi-typo-popup__resp-icon" width="10" height="10" viewBox="0 0 24 24"
      fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
      <rect x="2" y="3" width="20" height="14" rx="2"/><path d="M8 21h8M12 17v4"/>
    </svg>
  ) : null;

  if (!pos) return null;

  return ReactDOM.createPortal(
    <div ref={popupRef} className="jalpi-typo-popup" style={{ top: pos.top, right: pos.right }}>

      {/* ── Header ── */}
      <div className="jalpi-typo-popup__header">
        <span className="jalpi-typo-popup__title">Typography</span>
        <div className="jalpi-typo-popup__header-btns">
          <button type="button" className="jalpi-typo-popup__hbtn" title="Reset all" onClick={reset}>
            <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round">
              <path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/>
              <path d="M3 3v5h5"/>
            </svg>
          </button>
          <button type="button" className="jalpi-typo-popup__hbtn" title="Close" onClick={onClose}>
            <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
              <line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/>
            </svg>
          </button>
        </div>
      </div>

      {/* ── Body ── */}
      <div className="jalpi-typo-popup__body">

        {/* Family — inline: label left, picker right */}
        <div className="jalpi-typo-popup__field jalpi-typo-popup__field--inline">
          <label className="jalpi-typo-popup__field-lbl">Family</label>
          <FontFamilySearch value={styles.fontFamily || ''} onChange={(v) => update({ fontFamily: v })} />
        </div>

        {/* Size — 2-row slider */}
        <PopupSliderField
          label="Size" respIcon={respIcon}
          value={styles.fontSize || ''} onChange={(v) => update({ fontSize: v })}
          max={200} step={1} units={['px', 'em', 'rem', 'vw']} placeholder="16"
        />

        {/* Weight — inline: label left, select right */}
        <div className="jalpi-typo-popup__field jalpi-typo-popup__field--inline">
          <label className="jalpi-typo-popup__field-lbl">Weight</label>
          <Select
            value={styles.fontWeight || ''}
            options={[
              { value: '',    label: 'Default'       },
              { value: '100', label: 'Thin 100'      },
              { value: '300', label: 'Light 300'     },
              { value: '400', label: 'Regular 400'   },
              { value: '500', label: 'Medium 500'    },
              { value: '600', label: 'SemiBold 600'  },
              { value: '700', label: 'Bold 700'      },
              { value: '800', label: 'ExtraBold 800' },
              { value: '900', label: 'Black 900'     },
            ]}
            onChange={(v) => update({ fontWeight: v })}
          />
        </div>

        {/* Transform — inline */}
        <div className="jalpi-typo-popup__field jalpi-typo-popup__field--inline">
          <label className="jalpi-typo-popup__field-lbl">Transform</label>
          <Select
            value={styles.textTransform || ''}
            options={[
              { value: '',           label: 'Default'   },
              { value: 'uppercase',  label: 'Uppercase' },
              { value: 'lowercase',  label: 'Lowercase' },
              { value: 'capitalize', label: 'Capitalize'},
            ]}
            onChange={(v) => update({ textTransform: v })}
          />
        </div>

        {/* Style — inline */}
        <div className="jalpi-typo-popup__field jalpi-typo-popup__field--inline">
          <label className="jalpi-typo-popup__field-lbl">Style</label>
          <Select
            value={styles.fontStyle || ''}
            options={[
              { value: '',        label: 'Default' },
              { value: 'italic',  label: 'Italic'  },
              { value: 'oblique', label: 'Oblique' },
            ]}
            onChange={(v) => update({ fontStyle: v })}
          />
        </div>

        {/* Decoration — inline */}
        <div className="jalpi-typo-popup__field jalpi-typo-popup__field--inline">
          <label className="jalpi-typo-popup__field-lbl">Decoration</label>
          <Select
            value={styles.textDecoration || ''}
            options={[
              { value: '',             label: 'Default'     },
              { value: 'none',         label: 'None'        },
              { value: 'underline',    label: 'Underline'   },
              { value: 'line-through', label: 'Line Through'},
              { value: 'overline',     label: 'Overline'    },
            ]}
            onChange={(v) => update({ textDecoration: v })}
          />
        </div>

        {/* Line Height — 2-row slider */}
        <PopupSliderField
          label="Line Height" respIcon={respIcon}
          value={styles.lineHeight || ''} onChange={(v) => update({ lineHeight: v })}
          max={10} step={0.1} units={['em', 'px']} placeholder="1.5"
        />

        {/* Letter Spacing — 2-row slider */}
        <PopupSliderField
          label="Letter Spacing" respIcon={respIcon}
          value={styles.letterSpacing || ''} onChange={(v) => update({ letterSpacing: v })}
          min={-5} max={20} step={0.5} units={['px', 'em']} placeholder="0"
        />

        {/* Word Spacing — 2-row slider */}
        <PopupSliderField
          label="Word Spacing" respIcon={respIcon}
          value={styles.wordSpacing || ''} onChange={(v) => update({ wordSpacing: v })}
          min={-5} max={50} step={1} units={['px', 'em']} placeholder="0"
        />

      </div>
    </div>,
    document.body
  );
}

/* Popup slider with unit on header row (label+unit / slider+number) */
function PopupSliderField({
  label, value, onChange, min = 0, max = 200, step = 1,
  units = ['px', 'em'], placeholder = '0', respIcon = null,
}: {
  label:        string;
  value:        string;
  onChange:     (v: string) => void;
  min?:         number;
  max?:         number;
  step?:        number;
  units?:       string[];
  placeholder?: string;
  respIcon?:    React.ReactNode;
}) {
  const extractUnit = (v: string) => {
    for (const u of ['rem', 'em', 'px', '%', 'vw', 'vh']) {
      if (v.endsWith(u) && units.includes(u)) return u;
    }
    return units[0];
  };
  const isEmpty    = !value;
  const activeUnit = isEmpty ? units[0] : extractUnit(value);
  const numVal     = isEmpty ? 0 : parseFloat(value) || 0;
  const rangeMax   = (activeUnit === '%' || activeUnit === 'vh' || activeUnit === 'vw') ? 100 : max;
  const pct        = Math.min(100, Math.max(0, ((numVal - min) / (rangeMax - min)) * 100));

  return (
    <div className="jalpi-typo-popup__field">
      <div className="jalpi-popup-field-hdr">
        <label className="jalpi-typo-popup__field-lbl">{label}{respIcon && <>{respIcon}</>}</label>
        <select
          className="jalpi-popup-field-unit"
          value={activeUnit}
          onChange={(e) => {
            const u = e.target.value;
            onChange(numVal ? `${numVal}${u}` : `0${u}`);
          }}
        >
          {units.map((u) => <option key={u} value={u}>{u}</option>)}
        </select>
      </div>
      <div className="jalpi-popup-slider">
        <input
          type="range"
          className="jalpi-popup-slider__track"
          min={min} max={rangeMax} step={step} value={numVal}
          style={{ '--pct': `${pct}%` } as React.CSSProperties}
          onChange={(e) => onChange(`${e.target.value}${activeUnit}`)}
        />
        <input
          type="number"
          className="jalpi-popup-slider__num"
          value={isEmpty ? '' : numVal}
          placeholder={placeholder}
          min={min}
          onChange={(e) => {
            const n = e.target.value;
            if (n === '') { onChange(''); return; }
            onChange(parseFloat(n) + activeUnit);
          }}
        />
      </div>
    </div>
  );
}

/* Reusable slider row for shadow popup (label on top, slider+number on bottom) */
function ShadowSlider({
  label, value, onChange, min = -50, max = 50,
}: {
  label: string; value: string; onChange: (v: string) => void; min?: number; max?: number;
}) {
  const num = parseFloat(value) || 0;
  const pct = Math.min(100, ((num - min) / (max - min)) * 100);
  return (
    <div className="jalpi-typo-popup__field">
      <label className="jalpi-typo-popup__field-lbl">{label}</label>
      <div className="jalpi-popup-slider">
        <input
          type="range"
          className="jalpi-popup-slider__track"
          min={min} max={max} step={1} value={num}
          style={{ '--pct': `${pct}%` } as React.CSSProperties}
          onChange={(e) => onChange(e.target.value !== '' ? `${e.target.value}px` : '')}
        />
        <input
          type="number"
          className="jalpi-popup-slider__num"
          value={value ? num : ''}
          placeholder="0"
          min={min} max={max}
          onChange={(e) => {
            const n = e.target.value;
            onChange(n !== '' ? `${parseFloat(n)}px` : '');
          }}
        />
      </div>
    </div>
  );
}

/* Text Shadow popup (portal, same positioning logic as TypographyPopup) */
function TextShadowPopup({
  styles,
  update,
  onClose,
  anchorRef,
}: {
  styles:    Record<string, string>;
  update:    (u: Record<string, string>) => void;
  onClose:   () => void;
  anchorRef: React.RefObject<HTMLButtonElement>;
}) {
  const popupRef = useRef<HTMLDivElement>(null);
  const [pos, setPos] = useState<{ top: number; right: number } | null>(null);

  useEffect(() => {
    if (!anchorRef.current) return;
    const r = anchorRef.current.getBoundingClientRect();
    const top   = Math.max(10, Math.min(r.top - 40, window.innerHeight - 300));
    const right = window.innerWidth - r.left + 10;
    setPos({ top, right });
  }, [anchorRef]);

  useEffect(() => {
    const handler = (e: MouseEvent) => {
      if (
        popupRef.current  && !popupRef.current.contains(e.target as Node) &&
        anchorRef.current && !anchorRef.current.contains(e.target as Node)
      ) onClose();
    };
    document.addEventListener('mousedown', handler);
    return () => document.removeEventListener('mousedown', handler);
  }, [onClose, anchorRef]);

  const reset = () => update({ textShadowH: '', textShadowV: '', textShadowBlur: '', textShadowColor: '' });

  if (!pos) return null;

  return (
    <div ref={popupRef} className="jalpi-typo-popup" style={{ top: pos.top, right: pos.right }}>
      <div className="jalpi-typo-popup__header">
        <span className="jalpi-typo-popup__title">Text Shadow</span>
        <div className="jalpi-typo-popup__header-btns">
          <button type="button" className="jalpi-typo-popup__hbtn" title="Reset" onClick={reset}>
            <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round">
              <path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/><path d="M3 3v5h5"/>
            </svg>
          </button>
          <button type="button" className="jalpi-typo-popup__hbtn" title="Close" onClick={onClose}>
            <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
              <line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/>
            </svg>
          </button>
        </div>
      </div>
      <div className="jalpi-typo-popup__body">
        {/* Color — inline: label left, swatch right */}
        <div className="jalpi-typo-popup__field jalpi-typo-popup__field--inline">
          <label className="jalpi-typo-popup__field-lbl">Color</label>
          <ColorInput value={styles.textShadowColor || ''} onChange={(c) => update({ textShadowColor: c })} placeholder="#000000" />
        </div>
        {/* Blur */}
        <ShadowSlider label="Blur"       value={styles.textShadowBlur || ''} onChange={(v) => update({ textShadowBlur: v })} min={0}   max={50} />
        {/* Horizontal */}
        <ShadowSlider label="Horizontal" value={styles.textShadowH    || ''} onChange={(v) => update({ textShadowH:    v })} min={-50} max={50} />
        {/* Vertical */}
        <ShadowSlider label="Vertical"   value={styles.textShadowV    || ''} onChange={(v) => update({ textShadowV:    v })} min={-50} max={50} />
      </div>
    </div>
  );
}

/* Toggle switch */
function Toggle({ checked, onChange }: { checked: boolean; onChange: (v: boolean) => void }) {
  return (
    <button
      type="button"
      className={`jalpi-toggle${checked ? ' is-on' : ''}`}
      onClick={() => onChange(!checked)}
    >
      <span className="jalpi-toggle__knob" />
    </button>
  );
}

/* Alignment buttons */
function AlignButtons({
  value,
  onChange,
  options,
}: {
  value:   string;
  onChange: (v: string) => void;
  options:  string[];
}) {
  const ICONS_MAP: Record<string, React.ReactNode> = {
    left: (
      <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
        <line x1="3" y1="6"  x2="21" y2="6"/><line x1="3" y1="12" x2="15" y2="12"/><line x1="3" y1="18" x2="18" y2="18"/>
      </svg>
    ),
    center: (
      <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
        <line x1="3" y1="6"  x2="21" y2="6"/><line x1="6" y1="12" x2="18" y2="12"/><line x1="4" y1="18" x2="20" y2="18"/>
      </svg>
    ),
    right: (
      <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
        <line x1="3" y1="6"  x2="21" y2="6"/><line x1="9" y1="12" x2="21" y2="12"/><line x1="6" y1="18" x2="21" y2="18"/>
      </svg>
    ),
    justify: (
      <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
        <line x1="3" y1="6"  x2="21" y2="6"/><line x1="3" y1="12" x2="21" y2="12"/><line x1="3" y1="18" x2="21" y2="18"/>
      </svg>
    ),
  };

  return (
    <div className="jalpi-align-btns">
      {options.map((a) => (
        <button
          key={a}
          type="button"
          className={`jalpi-align-btn${value === a ? ' is-active' : ''}`}
          onClick={() => onChange(a)}
          title={a.charAt(0).toUpperCase() + a.slice(1)}
        >
          {ICONS_MAP[a] ?? a[0].toUpperCase()}
        </button>
      ))}
    </div>
  );
}
