/* ============================================================
   Miss XV VIP — Order Flow (order-wizard.jsx)
   À la carte en USD · 5 pasos · preview + video demos en vivo
   Solo capa visual/UX. Sin backend real.
   ============================================================ */
const { useState, useMemo, useEffect, useRef } = React;

/* ---------- helpers de cliente (sin backend) ---------- */
// Copia texto al portapapeles con fallback a un input temporal + execCommand.
async function copyText(text) {
  try {
    if (navigator.clipboard && navigator.clipboard.writeText) {
      await navigator.clipboard.writeText(text);
      return true;
    }
  } catch (e) { /* cae al fallback */ }
  try {
    const ta = document.createElement('textarea');
    ta.value = text;
    ta.setAttribute('readonly', '');
    ta.style.position = 'fixed';
    ta.style.top = '-9999px';
    ta.style.opacity = '0';
    document.body.appendChild(ta);
    ta.focus(); ta.select();
    const ok = document.execCommand('copy');
    document.body.removeChild(ta);
    return ok;
  } catch (e) { return false; }
}
// Lee un File como data URL (para guardarlo en el borrador, sin servidor).
function fileToDataURL(file, cb) {
  const r = new FileReader();
  r.onload = (e) => cb(e.target.result);
  r.readAsDataURL(file);
}
// Botón "Copiar" con confirmación visual breve.
function CopyChip({ text, label = 'Copiar' }) {
  const [done, setDone] = useState(false);
  const onCopy = async (e) => {
    e.preventDefault();
    e.stopPropagation();
    const ok = await copyText(String(text));
    if (ok) { setDone(true); setTimeout(() => setDone(false), 1500); }
  };
  return (
    <button type="button" className={`ow-copy ${done ? 'is-done' : ''}`} onClick={onCopy}>
      {done ? '¡Copiado!' : label}
    </button>
  );
}

/* ---------- icons ---------- */
const P = {
  check: 'M20 6L9 17l-5-5', chevright: 'M9 18l6-6-6-6', chevleft: 'M15 18l-6-6 6-6',
  x: 'M18 6L6 18M6 6l12 12', plus: 'M12 5v14M5 12h14',
  eye: 'M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8zM12 15a3 3 0 100-6 3 3 0 000 6z',
  play: 'M6 4l14 8-14 8V4z', pause: 'M6 4h4v16H6zM14 4h4v16h-4z',
  sparkle: 'M12 2l1.8 5.5L19.5 9l-5.7 1.5L12 16l-1.8-5.5L4.5 9l5.7-1.5z',
  star: 'M12 2l3.09 6.26L22 9.27l-5 4.87L18.18 22 12 18.27 5.82 22 7 14.14l-5-4.87 6.91-1.01z',
  qr: 'M3 3h7v7H3zM14 3h7v7h-7zM3 14h7v7H3zM14 14h2v2h-2zM18 14h3v3h-3zM14 18h2v3h-2zM18 19h3v2h-3z',
  image: 'M19 3H5a2 2 0 00-2 2v14a2 2 0 002 2h14a2 2 0 002-2V5a2 2 0 00-2-2zM8.5 10a1.5 1.5 0 100-3 1.5 1.5 0 000 3zM21 15l-5-5L5 21',
  film: 'M3 3h18v18H3zM7 3v18M17 3v18M3 8h4M3 16h4M17 8h4M17 16h4M3 12h18',
  users: 'M16 21v-2a4 4 0 00-4-4H5a4 4 0 00-4 4v2M8.5 11a4 4 0 100-8 4 4 0 000 8zM23 21v-2a4 4 0 00-3-3.87M16 3.13a4 4 0 010 7.75',
  user: 'M20 21v-2a4 4 0 00-4-4H8a4 4 0 00-4 4v2M12 11a4 4 0 100-8 4 4 0 000 8z',
  calendar: 'M19 4H5a2 2 0 00-2 2v14a2 2 0 002 2h14a2 2 0 002-2V6a2 2 0 00-2-2zM16 2v4M8 2v4M3 10h18',
  clock: 'M12 22a10 10 0 100-20 10 10 0 000 20zM12 6v6l4 2',
  mappin: 'M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0118 0zM12 13a3 3 0 100-6 3 3 0 000 6z',
  palette: 'M12 22a10 10 0 110-20 10 10 0 0110 8 4 4 0 01-4 4h-2a2 2 0 00-1 4 2 2 0 01-3 2zM6.5 12a1.5 1.5 0 100-3 1.5 1.5 0 000 3zM10.5 8a1.5 1.5 0 100-3 1.5 1.5 0 000 3zM15.5 8a1.5 1.5 0 100-3 1.5 1.5 0 000 3z',
  heart: 'M20.84 4.61a5.5 5.5 0 00-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 00-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 000-7.78z',
  edit: 'M11 4H4a2 2 0 00-2 2v14a2 2 0 002 2h14a2 2 0 002-2v-7M18.5 2.5a2.121 2.121 0 113 3L12 15l-4 1 1-4 9.5-9.5z',
  seat: 'M3 18v-6a3 3 0 013-3h12a3 3 0 013 3v6M3 18h18M5 18v2M19 18v2M6 9V7a3 3 0 013-3h6a3 3 0 013 3v2',
  lock: 'M19 11H5a2 2 0 00-2 2v7a2 2 0 002 2h14a2 2 0 002-2v-7a2 2 0 00-2-2zM7 11V7a5 5 0 0110 0v4',
  shield: 'M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z',
  gift: 'M20 12v10H4V12M2 7h20v5H2zM12 22V7M12 7H7.5a2.5 2.5 0 010-5C11 2 12 7 12 7zM12 7h4.5a2.5 2.5 0 000-5C13 2 12 7 12 7z',
  zap: 'M13 2L3 14h9l-1 8 10-12h-9l1-8z',
  crown: 'M3 18l2-11 5 6 2-8 2 8 5-6 2 11z',
  message: 'M21 15a2 2 0 01-2 2H7l-4 4V5a2 2 0 012-2h14a2 2 0 012 2z',
  globe: 'M12 22a10 10 0 100-20 10 10 0 000 20zM2 12h20M12 2a15 15 0 010 20 15 15 0 010-20z',
  shirt: 'M16 3l4 3-3 3-1-1v11H8V8L7 9 4 6l4-3 2 2h4z',
  ticket: 'M3 8a2 2 0 012-2h14a2 2 0 012 2v2a2 2 0 000 4v2a2 2 0 01-2 2H5a2 2 0 01-2-2v-2a2 2 0 000-4zM12 6v2M12 11v2M12 16v2',
  music: 'M9 18V5l12-2v13M9 18a3 3 0 11-6 0 3 3 0 016 0zM21 16a3 3 0 11-6 0 3 3 0 016 0z',
  mail: 'M4 4h16a2 2 0 012 2v12a2 2 0 01-2 2H4a2 2 0 01-2-2V6a2 2 0 012-2zM22 6l-10 7L2 6',
  info: 'M12 22a10 10 0 100-20 10 10 0 000 20zM12 8h.01M11 12h1v4h1'
};
function Ic({ n, s = 16, style, cls }) {
  return (
    <svg width={s} height={s} viewBox="0 0 24 24" fill="none" stroke="currentColor"
    strokeWidth="1.7" strokeLinecap="round" strokeLinejoin="round" style={style} className={cls}>
      <path d={P[n] || P.info} />
    </svg>);

}
function GoogleG({ s = 18 }) {
  return (
    <svg viewBox="0 0 48 48" width={s} height={s} aria-hidden="true">
      <path fill="#FFC107" d="M43.6 20.5H42V20H24v8h11.3c-1.6 4.7-6.1 8-11.3 8-6.6 0-12-5.4-12-12s5.4-12 12-12c3.1 0 5.9 1.2 8 3.1l5.7-5.7C34 6.5 29.3 4.5 24 4.5 13.2 4.5 4.5 13.2 4.5 24S13.2 43.5 24 43.5 43.5 34.8 43.5 24c0-1.2-.1-2.3-.4-3.5z" />
      <path fill="#FF3D00" d="M6.3 14.7l6.6 4.8C14.7 16 19 13 24 13c3.1 0 5.9 1.2 8 3.1l5.7-5.7C34 6.5 29.3 4.5 24 4.5 16.3 4.5 9.7 8.9 6.3 14.7z" />
      <path fill="#4CAF50" d="M24 43.5c5.2 0 9.9-2 13.4-5.2l-6.2-5.2c-2 1.5-4.6 2.4-7.2 2.4-5.2 0-9.6-3.3-11.3-8l-6.5 5C9.6 39 16.2 43.5 24 43.5z" />
      <path fill="#1976D2" d="M43.6 20.5H42V20H24v8h11.3c-.8 2.3-2.3 4.3-4.1 5.6l6.2 5.2C39.7 36.9 43.5 31.2 43.5 24c0-1.2-.1-2.3-.4-3.5z" />
    </svg>);

}

/* ---------- catálogo real (USD) ---------- */
const SERVICES = [
{ id: 'invitacion', base: true, name: 'Invitación Digital VIP Original', short: 'Tu invitación base, personalizada', price: 100, icon: 'sparkle', tint: '#f5c945', badge: 'Invitación', demoUrl: 'https://www.missxvvip.com/inv/xv-maria-fernanda-valdes', desc: 'La invitación digital animada y personalizada a tu medida, siempre incluida en tu orden.' },
{ id: 'signature', name: 'Signature', short: 'Invitación diseñada a tu medida', price: 200, icon: 'palette', tint: '#d4a3ff', badge: 'Signature', desc: 'Diseñamos tu invitación a tu medida, con tus colores, temática y estilo.' },
{ id: 'savedate', name: 'Anuncia tu Fecha', short: 'Save the Date', price: 150, icon: 'calendar', tint: '#b347ff', badge: 'Save the Date', desc: 'Video animado para anunciar tu fecha y crear expectativa entre tus invitados.' },
{ id: 'historia', name: 'Cuenta tu Historia', short: 'Video Cronológico', price: 300, popular: true, icon: 'film', tint: '#fb923c', badge: 'Tu Historia', desc: 'Hasta 80 fotos, editadas individualmente · Video cronológico que narra tu historia con fotos y momentos clave.' },
{ id: 'qrvip', name: 'Vive tu Fiesta', short: 'QR álbum digital', price: 50, icon: 'qr', tint: '#22d34a', badge: 'QR-VIP', desc: 'Álbum colaborativo y photo wall en vivo: tus invitados suben fotos con un QR.' },
{ id: 'seating', name: 'Organizador de Mesas', short: 'Mapa de mesas digital', price: 50, icon: 'seat', tint: '#3b8bd6', badge: 'Mesas', desc: 'Organizador de mesas visual para acomodar tus mesas e invitados sin estrés.' },
{ id: 'bannerfisico', name: 'Roll-Up Banner', short: 'Diseño + impresión + envío + stand', price: 200, icon: 'image', tint: '#60a5fa', badge: 'Banner', desc: 'Roll-Up Banner con diseño, impresión, envío y stand.' },
{ id: 'slideshow', name: 'VIP Slideshow', short: 'Hasta 25 fotos', price: 100, was: 300, icon: 'image', tint: '#ec4899', badge: 'Slideshow', desc: 'Video slideshow con música y transiciones elegantes, máximo 25 fotos.' },
{ id: 'digitaldesign', name: 'Paquete de Diseño Digital', short: 'Postcard QR + número de mesa + arte para mesas', price: 50, icon: 'edit', tint: '#14b8a6', badge: 'Diseño', desc: 'Tres artes digitales coordinadas para QR y mesas: postcard/flyer QR, número de mesa y arte complementario para mesas.' }];

const PROMO_INVITACIONES_VIDEO = '/orden/lite/assets/videos/promo-invitaciones-digitales-miss-xv-vip-voiceoff.mp4';

/* Bilingüe viene incluido en la invitación. */
const bilingualFee = () => 0;

const THEME_PALETTES = {
  'princesa-disney': { mood: 'Princesa · cuento', bg: ['#2b1835', '#100817'], text: '#fff4fb', accent: '#f3b5d7', dots: ['#6f3a83', '#f3b5d7', '#fff4fb'] },
  natural: { mood: 'Natural · encantado', bg: ['#16341f', '#0a1d11'], text: '#fff8e6', accent: '#e8c76b', dots: ['#1f5a33', '#e8c76b', '#fff8e6'] },
  elegante: { mood: 'Elegante · gala', bg: ['#14161f', '#08080d'], text: '#fff8e6', accent: '#f5c945', dots: ['#2a2a3a', '#f5c945', '#fff8e6'] },
  celestial: { mood: 'Celestial · brillo', bg: ['#151c3f', '#080b1e'], text: '#f4f6ff', accent: '#9db7ff', dots: ['#263b85', '#9db7ff', '#f4f6ff'] },
  fantasia: { mood: 'Fantasía · mágico', bg: ['#241640', '#120a26'], text: '#f3ecff', accent: '#d4a3ff', dots: ['#5a2e9e', '#d4a3ff', '#f3ecff'] },
  cultural: { mood: 'Cultural · color', bg: ['#321b16', '#120908'], text: '#fff2e8', accent: '#f2a65a', dots: ['#8a3f2a', '#f2a65a', '#fff2e8'] },
  boda: { mood: 'Boda · elegante', bg: ['#2d2a24', '#11100d'], text: '#fffaf0', accent: '#e6c98b', dots: ['#635d50', '#e6c98b', '#fffaf0'] },
  deportes: { mood: 'Deporte · energía', bg: ['#142a24', '#071310'], text: '#effff9', accent: '#58d68d', dots: ['#1f6f55', '#58d68d', '#effff9'] },
  especial: { mood: 'Especial · personalizado', bg: ['#202033', '#0c0c18'], text: '#f4f1ff', accent: '#f0c46b', dots: ['#4b4b70', '#f0c46b', '#f4f1ff'] },
  custom: { mood: 'Personalizada · a medida', bg: ['#221c2b', '#0c0910'], text: '#fff8e6', accent: '#eaa121', dots: ['#4b395c', '#eaa121', '#fff8e6'] }
};
const THEME_PREVIEWS = {
  'aladdin': '/orden/lite/assets/theme-hero-previews/aladdin.webp',
  'alice-in-wonderland': '/orden/lite/assets/theme-hero-previews/alice-in-wonderland.webp',
  'bella-y-la-bestia': '/orden/lite/assets/theme-hero-previews/bella-y-la-bestia.webp',
  'bosque-encantado': '/orden/lite/assets/theme-hero-previews/bosque-encantado.webp',
  'brave-merida': '/orden/lite/assets/theme-hero-previews/brave-merida.webp',
  'cherry-blossom': '/orden/lite/assets/theme-hero-previews/cherry-blossom.webp',
  'cinderela': '/orden/lite/assets/theme-hero-previews/cinderela.webp',
  'colibri-magico': '/orden/lite/assets/theme-hero-previews/colibri-magico.webp',
  'era-victoriana': '/orden/lite/assets/theme-hero-previews/era-victoriana.webp',
  'frozen-elsa-anna': '/orden/lite/assets/theme-hero-previews/frozen-elsa-anna.webp',
  'jardin-encantado': '/orden/lite/assets/theme-hero-previews/jardin-encantado.webp',
  'luna-y-estrellas': '/orden/lite/assets/theme-hero-previews/luna-y-estrellas.webp',
  'masquerade-ball': '/orden/lite/assets/theme-hero-previews/masquerade-ball.webp',
  'mermaid-ariel': '/orden/lite/assets/theme-hero-previews/mermaid-ariel.webp',
  'moana-ocean': '/orden/lite/assets/theme-hero-previews/moana-ocean.webp',
  'once-upon-a-time': '/orden/lite/assets/theme-hero-previews/once-upon-a-time.webp',
  'pocahontas-native': '/orden/lite/assets/theme-hero-previews/pocahontas-native.webp',
  'princesa-y-el-sapo': '/orden/lite/assets/theme-hero-previews/princesa-y-el-sapo.webp',
  'princesas-disney-nubes': '/orden/lite/assets/theme-hero-previews/princesas-disney-nubes.webp',
  'rapunzel': '/orden/lite/assets/theme-hero-previews/rapunzel.webp',
  'rosa-encantada': '/orden/lite/assets/theme-hero-previews/rosa-encantada.webp',
  'sleeping-beauty-aurora': '/orden/lite/assets/theme-hero-previews/sleeping-beauty-aurora.webp',
  'snow-white': '/orden/lite/assets/theme-hero-previews/snow-white.webp',
  'starry-night': '/orden/lite/assets/theme-hero-previews/starry-night.webp',
  'talavera': '/orden/lite/assets/theme-hero-previews/talavera.webp',
  'under-the-sea': '/orden/lite/assets/theme-hero-previews/under-the-sea.webp',
  'wedding-boda': '/orden/lite/assets/theme-hero-previews/wedding-boda.webp',
  'winter-wonderland': '/orden/lite/assets/theme-hero-previews/winter-wonderland.webp'
};
const theme = (id, name, category, featured = false) => ({ id, name, category, featured, preview: THEME_PREVIEWS[id] || '', ...THEME_PALETTES[category] || THEME_PALETTES.custom });
// FASE 2 ORDER-VIP: este array YA NO es la fuente en runtime -- StepTheme ahora
// consume /orden/api.php?action=themes_public (mxv_order_themes, gate K3/K5 real).
// Se conserva como fixture de referencia/pruebas hasta demostrar CATALOG READ = PASS +
// BACKFILL = PASS + CLIENT SELECTOR = PASS + LEGACY BRIEFS = PASS (Spec FASE 1B / orden
// FASE 2 seccion 20). No usar para pintar el selector del cliente.
const LEGACY_THEMES_FIXTURE = [
theme('aladdin', 'Aladdin / Jasmin', 'princesa-disney'),
theme('alice-in-wonderland', 'Alice in Wonderland', 'fantasia'),
theme('snow-white', 'Blancanieves / Snow White', 'princesa-disney'),
theme('wedding-boda', 'Boda Elegante', 'boda', true),
theme('bosque-encantado', 'Bosque Encantado', 'natural', true),
theme('brave-merida', 'Brave / Merida', 'princesa-disney'),
theme('cinderela', 'Cenicienta / Cinderella', 'princesa-disney', true),
theme('cherry-blossom', 'Cherry Blossom', 'natural'),
theme('colibri-magico', 'Colibri Mágico', 'natural'),
theme('era-victoriana', 'Era Victoriana', 'elegante'),
theme('frozen-elsa-anna', 'Frozen (Elsa & Anna)', 'princesa-disney', true),
theme('jardin-encantado', 'Jardín Encantado', 'natural', true),
theme('bella-y-la-bestia', 'La Bella y la Bestia', 'princesa-disney', true),
theme('princesa-y-el-sapo', 'La Princesa y el Sapo / Tiana', 'princesa-disney'),
theme('mermaid-ariel', 'La Sirenita / Ariel', 'princesa-disney'),
theme('luna-y-estrellas', 'Luna y Estrellas', 'celestial', true),
theme('masquerade-ball', 'Masquerade Ball', 'elegante', true),
theme('moana-ocean', 'Moana / Ocean', 'princesa-disney'),
theme('once-upon-a-time', 'Once Upon A Time', 'fantasia'),
theme('pocahontas-native', 'Pocahontas / Native', 'princesa-disney'),
theme('princesas-disney-nubes', 'Princesas Disney (nubes)', 'princesa-disney'),
theme('rapunzel', 'Rapunzel / Enredados', 'princesa-disney', true),
theme('rosa-encantada', 'Rosa Encantada', 'natural', true),
theme('sleeping-beauty-aurora', 'Sleeping Beauty / Aurora', 'princesa-disney'),
theme('starry-night', 'Starry Night', 'celestial'),
theme('talavera', 'Talavera', 'cultural'),
theme('under-the-sea', 'Under the Sea', 'natural'),
theme('winter-wonderland', 'Winter Wonderland', 'natural')
];

// FASE 2 ORDER-VIP: cache compartido del catalogo REAL de temáticas (mxv_order_themes
// via action=themes_public, gate K3/K5 servidor). Un solo fetch para todo el wizard --
// StepTheme, Preview, StepSummary y themeOpsEdits() leen de aqui, no de
// LEGACY_THEMES_FIXTURE. FAIL-CLOSED (orden FASE 2 seccion 19): si falla, status='error'
// y data=[] -- nunca se reintroducen los 28 temas legacy como fallback silencioso.
const themeCatalogCache = { status: 'idle', data: [], listeners: new Set() };
function order_theme_adapt(t) {
  return {
    id: t.slug, name: t.name, category: t.category, featured: !!t.featured,
    preview: t.thumbnail_url || '', demoUrl: t.demo_url || '',
    ...(THEME_PALETTES[t.category] || THEME_PALETTES.custom),
  };
}
function fetchThemeCatalog() {
  if (themeCatalogCache.status === 'loading' || themeCatalogCache.status === 'ready') return;
  themeCatalogCache.status = 'loading';
  fetch('/orden/api.php?action=themes_public', { credentials: 'same-origin' })
    .then((r) => { if (!r.ok) throw new Error('http_' + r.status); return r.json(); })
    .then((d) => {
      if (!d || d.success !== true || !Array.isArray(d.themes)) throw new Error('shape');
      themeCatalogCache.status = 'ready';
      themeCatalogCache.data = d.themes.map(order_theme_adapt);
      themeCatalogCache.listeners.forEach((fn) => fn());
    })
    .catch(() => {
      themeCatalogCache.status = 'error';
      themeCatalogCache.data = [];
      themeCatalogCache.listeners.forEach((fn) => fn());
    });
}
function useThemeCatalog() {
  const [, forceRender] = useState(0);
  useEffect(() => {
    const listener = () => forceRender((n) => n + 1);
    themeCatalogCache.listeners.add(listener);
    fetchThemeCatalog();
    return () => themeCatalogCache.listeners.delete(listener);
  }, []);
  return themeCatalogCache;
}

const STEPS = ['Tus servicios', 'La festejada', 'Secciones', 'Temática', 'Resumen', 'Pago'];
/* secciones de la invitación VIP real (VIPINV_SECTIONS del portal INV) — core = siempre activa; req = servicio del que dependen */
const SECTIONS = [
{ id: 'video_intro', name: 'Video de intro / apertura', desc: 'Pantalla animada de bienvenida', icon: 'play' },
{ id: 'hero', name: 'Portada', desc: 'Nombre y fecha principal', icon: 'crown' },
{ id: 'countdown', name: 'Cuenta regresiva', desc: 'Días, horas y minutos al gran día', icon: 'clock' },
{ id: 'parents', name: 'Padres / Familia', desc: 'Mamá, papá y familia principal', icon: 'heart' },
{ id: 'godparents', name: 'Padrinos', desc: 'Padrinos de honor o bendición', icon: 'users' },
{ id: 'ceremony', name: 'Ceremonia', desc: 'Misa o acto religioso', icon: 'sparkle' },
{ id: 'reception', name: 'Recepción', desc: 'Dirección, hora y mapa del salón', icon: 'mappin' },
{ id: 'food_menu', name: 'Comida / Menú', desc: 'Menú, buffet o cocktail para invitados', icon: 'gift' },
{ id: 'logistics', name: 'Logística / Hoteles', desc: 'Hotel, parking, shuttle y after-party', icon: 'mappin' },
{ id: 'itinerary', name: 'Itinerario', desc: 'Programa del evento', icon: 'calendar' },
{ id: 'dress_code', name: 'Vestimenta', desc: 'Código de vestimenta', icon: 'shirt' },
{ id: 'registry', name: 'Mesa de Regalos', desc: 'Links a tu registro o datos', icon: 'gift' },
{ id: 'corte', name: 'Corte de Honor', desc: 'Chambelanes y damas de honor', icon: 'users' },
{ id: 'gallery', name: 'Galería', desc: 'Hasta 6 fotos en tu invitación', icon: 'image' },
{ id: 'music', name: 'Música de fondo', desc: 'Activa una pista al abrir', icon: 'music' },
{ id: 'qrvip', name: 'Álbum de Fotos QR', desc: 'Muro de fotos colaborativo de invitados', icon: 'qr' },
{ id: 'rsvp', name: 'RSVP', desc: 'Confirmación de asistencia', icon: 'mail' },
{ id: 'guestbook', name: 'Libro de Visitas', desc: 'Mensajes y firmas de invitados', icon: 'message' },
{ id: 'thanks', name: 'Agradecimiento', desc: 'Mensaje de cierre', icon: 'sparkle' },
{ id: 'after_party', name: 'After Party', desc: 'Lugar, mapa y hora de inicio', icon: 'mappin' }];

const REQ_LABEL = { slideshow: 'VIP Slideshow', qrvip: 'QR-VIP', savedate: 'Save the Date' };
/* las secciones son contenido libre — el cliente las enciende/apaga, no dependen de servicios */
function secAvailable() {return true;}
/* paquetes de inicio rápido — precios fijos de negocio (ids no-base) */
const PKGS = [
{ id: 'esencial', name: 'Original', icon: 'sparkle', price: 100, tagline: 'Diseño original de nuestra colección, listo para personalizar.', items: [] },
{ id: 'signature', name: 'Signature', icon: 'palette', price: 200, tagline: 'Invitación a tu medida: tus colores, tu temática y tu estilo.', items: ['signature'], noSavings: true },
{ id: 'lujo', name: 'Premium', icon: 'crown', popular: true, price: 500, tagline: 'Invitación + QR álbum + Organizador de Mesas + Save the Date + video cronológico + artes QR/mesas.', items: ['qrvip', 'seating', 'savedate', 'historia', 'digitaldesign'], comboSlug: 'lujo-vip' }];

function pkgActive(services, pkg) {
  if (services.size !== pkg.items.length) return false;
  return pkg.items.every((id) => services.has(id));
}
function findPkg(services) {return PKGS.find((p) => pkgActive(services, p)) || null;}
function pkgListValue(pkg) {
  if (pkg.noSavings) return pkg.price;
  return SERVICES.filter((s) => s.base || pkg.items.includes(s.id)).reduce((a, s) => a + s.price, 0);
}
/* CARRILES SEPARADOS: paquete = precio fijo sin promos | à la carte = lista + promos por monto */
function orderState(services) {
  const pkg = findPkg(services);
  if (pkg) return { mode: 'pkg', pkg, total: pkg.price, gross: pkg.price, discount: 0, freeIds: new Set(), freeQS: false, freeSlide: false };
  return { mode: 'alc', pkg: null, ...calcTotals(services) };
}
function effectiveServicesForTheme(data) {
  const effective = new Set(data.services);
  if (!chosenTheme(data)) {
    effective.add('signature');
  }
  return effective;
}
function effectivePackageId(data, pkg) {
  return !chosenTheme(data) ? 'signature' : pkg ? pkg.id : 'esencial';
}
const USD = (n) => '$' + n.toLocaleString('en-US');
const USD2 = (n) => '$' + n.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
/* ── COMISIÓN TARJETA (Stripe) ──────────────────────────────────
   Ajusta estos dos valores a la tasa real de tu cuenta Stripe.
   El recargo SOLO aplica a pagos con tarjeta; el cliente lo absorbe. */
const CARD_FEE_PCT = 0.029; // 2.9 %
const CARD_FEE_FIXED = 0.30; // $0.30 USD por transacción
/* Stripe activo: cuando es false, no se muestra el sello "Recomendado" en tarjeta */
const STRIPE_ACTIVE = false;
/* grossing-up: lo que cobramos para recibir `total` neto tras la comisión */
function cardCharge(total) {
  return Math.round((total + CARD_FEE_FIXED) / (1 - CARD_FEE_PCT) * 100) / 100;
}
const MESES = ['enero', 'febrero', 'marzo', 'abril', 'mayo', 'junio', 'julio', 'agosto', 'septiembre', 'octubre', 'noviembre', 'diciembre'];
const PAY = { zelle: { handle: '(818) 674-3792', titular: 'Manuel Acevedo' }, venmo: { handle: '@missxvvip' } };
/* número de WhatsApp del negocio (formato wa.me, sin + ni espacios). Edítalo aquí. */
const WHATSAPP_NUMBER = '18183106910';
function waLink(msg) {return `https://wa.me/${WHATSAPP_NUMBER}?text=${encodeURIComponent(msg)}`;}

function fmtDate(d) {
  if (!d) return null;
  const [y, m, day] = d.split('-').map(Number);
  if (!y || !m || !day) return null;
  return `${String(day).padStart(2, '0')} de ${MESES[m - 1]} de ${y}`;
}
const capMes = (m) => m.charAt(0).toUpperCase() + m.slice(1);
/* etiqueta de fecha flexible: exacta · mes/año · por confirmar */
function eventDateLabel(data) {
  const mode = data.dateMode || 'exact';
  if (mode === 'tbd') return 'Fecha por confirmar';
  if (mode === 'monthyear') {
    const dateMY = data.eventMYYear && data.eventMYMonth ? `${data.eventMYYear}-${data.eventMYMonth}` : data.dateMY;
    if (!dateMY) return null;
    const [y, m] = dateMY.split('-').map(Number);
    if (!y || !m) return null;
    return `${capMes(MESES[m - 1])} ${y}`;
  }
  const exactDate = data.eventYear && data.eventMonth && data.eventDay ? `${data.eventYear}-${data.eventMonth}-${data.eventDay}` : data.date;
  return fmtDate(exactDate);
}
/* ¿la fecha quedó pendiente de afinar? (tbd o mes/año o vacía) */
function dateIsPending(data) {
  const mode = data.dateMode || 'exact';
  if (mode === 'tbd') return true;
  if (mode === 'monthyear') return !(data.eventMYYear && data.eventMYMonth || data.dateMY);
  return !(data.eventYear && data.eventMonth && data.eventDay || data.date);
}
function godparentsText(data) {
  const rows = Array.isArray(data.godparentRows) ? data.godparentRows : [];
  const text = rows.filter((r) => r.names).map((r) => `${r.role === 'Otro' && r.customRole ? r.customRole : r.role || 'Otro'}: ${r.names}`).join('\n');
  return text || data.padrinos || '';
}
function courtText(data) {
  const rows = Array.isArray(data.courtRows) ? data.courtRows : [];
  const text = rows.filter((r) => r.name).map((r) => `${r.role || 'Otro'}: ${r.name}`).join('\n');
  return text || data.corteNames || '';
}
/* folio único de orden para reconciliar pagos (Zelle/Venmo) */
function genFolio() {return 'MXV-' + (1000 + Math.floor(Math.random() * 9000));}
function countdown(d, t) {
  if (!d) return null;
  const diff = new Date(`${d}T${t || '20:00'}:00`) - new Date();
  if (isNaN(diff) || diff <= 0) return null;
  return { days: Math.floor(diff / 86400000), hrs: Math.floor(diff % 86400000 / 3600000), min: Math.floor(diff % 3600000 / 60000) };
}
/* promo-aware totals */
function calcTotals(services) {
  const usesSignature = services.has('signature');
  const chosen = SERVICES.filter((s) => (s.base && !usesSignature) || services.has(s.id));
  const gross = chosen.reduce((a, s) => a + s.price, 0);
  const freeQS = gross >= 400,freeSlide = gross >= 700;
  const freeIds = new Set();
  let discount = 0;
  chosen.forEach((s) => {
    if (freeQS && (s.id === 'qrvip' || s.id === 'seating')) {discount += s.price;freeIds.add(s.id);}
    if (freeSlide && s.id === 'slideshow') {discount += s.price;freeIds.add(s.id);}
  });
  return { gross, discount, total: gross - discount, freeIds, freeQS, freeSlide };
}

/* ============================================================
   PANEL DERECHO — invitación en vivo
   ============================================================ */
function Preview({ data, totals, step }) {
  const catalog = useThemeCatalog();
  // FASE 2 ORDER-VIP: SIN fallback silencioso a otro tema (era el bug de "se ve
  // Aladdin" -- orden FASE 2 seccion 19 / auditoria previa seccion L11). Si el slug
  // guardado no esta en el catalogo (brief legacy con free-text, o tema retirado), usa
  // una paleta neutra y el nombre crudo guardado -- nunca sustituye por otro tema real.
  const th = catalog.data.find((t) => t.id === data.theme)
    || { id: data.theme || '', name: data.theme || '', ...THEME_PALETTES.custom };
  const exactDate = data.eventYear && data.eventMonth && data.eventDay ? `${data.eventYear}-${data.eventMonth}-${data.eventDay}` : data.date;
  const cd = (data.dateMode || 'exact') === 'exact' ? countdown(exactDate, data.time) : null;
  const dateStr = eventDateLabel(data);
  const badges = SERVICES.filter((s) => !s.base && data.services.has(s.id)).map((s) => s.badge);
  const sec = data.sections;
  const godparents = godparentsText(data);
  const court = courtText(data);
  const pills = ['Eligiendo tus servicios', 'Capturando los datos', 'Eligiendo secciones', 'Aplicando la temática', 'Revisando tu orden', 'Lista para publicar'];
  const nameStyle = data.style === 'moderno' ?
  { fontFamily: "'Poppins', sans-serif", fontStyle: 'normal', fontWeight: 800, letterSpacing: '-0.02em', textTransform: 'uppercase', fontSize: 33 } : {};
  return (
    <div className="ow-screen" style={{ background: `radial-gradient(120% 80% at 50% 0%, ${th.accent}22, transparent 55%), linear-gradient(180deg, ${th.bg[0]}, ${th.bg[1]})` }}>
      <div className="ow-inv" style={{ color: th.text }}>
        {sec.hero && <div className="ow-inv-eyebrow" style={{ color: th.accent }}>Mis XV Años</div>}
        {sec.hero && <div className="ow-inv-name" style={{ color: th.text, ...nameStyle }}>{data.honoree || 'Tu Festejada'}</div>}
        {sec.hero && <div className="ow-inv-rule" style={{ background: th.accent }} />}
        {data.welcome && <div className="ow-inv-welcome" style={{ color: th.text }}>{data.welcome}</div>}
        {sec.hero && <div className="ow-inv-date" style={{ color: th.accent }}>{dateStr || 'Fecha por definir'}</div>}
        {sec.countdown && cd &&
        <div className="ow-inv-count">
            {[['días', cd.days], ['hrs', cd.hrs], ['min', cd.min]].map(([l, v]) =>
          <div className="ow-inv-count-u" key={l}>
                <div className="ow-inv-count-n" style={{ color: th.accent }}>{String(v).padStart(2, '0')}</div>
                <div className="ow-inv-count-l">{l}</div>
              </div>
          )}
          </div>
        }
        {sec.reception && (data.venue || data.city) &&
        <div style={{ marginTop: sec.countdown && cd ? 14 : 22 }}>
            {data.venue && <div className="ow-inv-venue" style={{ color: th.accent }}>{data.venue}</div>}
            {data.city && <div className="ow-inv-city">{data.city}</div>}
            {data.recepTime && <div className="ow-inv-city" style={{ marginTop: 2 }}>Recepción · {data.recepTime}</div>}
          </div>
        }
        {sec.parents && (data.mom || data.dad) &&
        <div className="ow-inv-parents">
            <div style={{ fontSize: 9.5, letterSpacing: '0.2em', textTransform: 'uppercase', opacity: 0.7, marginBottom: 4 }}>Con la bendición de</div>
            {data.mom && <div className="nm">{data.mom}</div>}
            {data.mom && data.dad && <div style={{ opacity: 0.5, fontSize: 10 }}>&</div>}
            {data.dad && <div className="nm">{data.dad}</div>}
          </div>
        }
        {sec.godparents && godparents &&
        <div className="ow-inv-parents" style={{ marginTop: 18 }}>
            <div style={{ fontSize: 9.5, letterSpacing: '0.2em', textTransform: 'uppercase', opacity: 0.7, marginBottom: 4 }}>Padrinos</div>
            <div style={{ whiteSpace: 'pre-line', fontSize: 10.5, opacity: 0.82 }}>{godparents}</div>
          </div>
        }
        {sec.corte && court &&
        <div className="ow-inv-parents" style={{ marginTop: 18 }}>
            <div style={{ fontSize: 9.5, letterSpacing: '0.2em', textTransform: 'uppercase', opacity: 0.7, marginBottom: 4 }}>Corte de Honor</div>
            <div style={{ whiteSpace: 'pre-line', fontSize: 11, opacity: 0.82 }}>{court}</div>
          </div>
        }
        {sec.dress_code && data.dressCode && <div className="ow-inv-dress" style={{ color: th.accent, borderColor: th.accent }}>{data.dressCode}</div>}
        {badges.length > 0 &&
        <div className="ow-inv-badges" style={{ color: th.accent }}>
            {badges.map((b) => <span className="ow-inv-badge" key={b}>{b}</span>)}
          </div>
        }
        {sec.thanks && <div className="ow-inv-foot" style={{ color: th.text }}>Miss XV VIP</div>}
      </div>
      <div className="ow-inv-pill"><span className="ow-live-dot" />{pills[step]}</div>
    </div>);

}

/* ============================================================
   PANEL DERECHO — video demos (paso 1)
   ============================================================ */
function VideoShowcase({ data, set, toggleSvc, focus, setFocus, totals, activePkg }) {
  const [playing, setPlaying] = useState(false);
  const svc = SERVICES.find((s) => s.id === focus) || SERVICES[0];
  const promoVideo = true;
  const isFree = totals.freeIds.has(svc.id);
  const on = svc.base || data.services.has(svc.id);
  const inPkg = !!activePkg && on && !svc.base;
  const toggle = () => {if (!svc.base) toggleSvc(svc.id);};
  React.useEffect(() => {setPlaying(false);}, [focus]);
  return (
    <div className="ow-video-wrap">
      <div className="ow-player">
        {promoVideo ?
        <video className="ow-player-video" src={PROMO_INVITACIONES_VIDEO} controls preload="metadata" playsInline /> :
        <div className="ow-player-poster" style={{ background: `radial-gradient(120% 100% at 50% 0%, ${svc.tint}55, transparent 60%), linear-gradient(160deg, ${svc.tint}26, #070a1e 82%)` }} />}
        <div className="ow-player-badge">{promoVideo ? `Comercial · ${svc.badge}` : svc.demoUrl ? 'En vivo ↑' : `Demo · ${svc.badge}`}</div>
        <div className="ow-player-price">{inPkg ? <span style={{ color: '#5bf07c' }}>Incluido</span> : isFree ? <span style={{ color: '#5bf07c' }}>GRATIS</span> : <>{svc.was && <span className="was">{USD(svc.was)}</span>}{USD(svc.price)}</>}</div>
        {!promoVideo &&
        <div className="ow-player-controls">
          <Ic n={playing ? 'pause' : 'play'} s={13} style={{ color: '#fff' }} />
          <div className="barwrap"><i className={playing ? 'play' : ''} style={playing ? {} : { width: '0%' }} /></div>
          <span className="ow-player-time">{svc.demoUrl ? 'Invitación real · en vivo' : playing ? '0:04 / 0:18' : '0:18'}</span>
        </div>}
      </div>

      <div>
        <div className="ow-vcap-name">{svc.name}</div>
        <div className="ow-vcap-desc">{svc.desc}</div>
        <div className="ow-vcap-row">
          <div className="ow-vcap-price">{inPkg ? <span className="free">Incluido en paquete</span> : isFree ? <span className="free">GRATIS · promo</span> : USD(svc.price)}</div>
          {svc.demoUrl &&
          <a className="ow-vcap-add on" href={svc.demoUrl} target="_blank" rel="noopener noreferrer" style={{ textDecoration: 'none' }}><Ic n="play" s={15} />Ver invitación real</a>}
          <button className={`ow-vcap-add ${svc.base ? 'base' : on ? 'on' : 'off'}`} onClick={toggle}>
            {svc.base ? <><Ic n="check" s={15} />Incluida</> : on ? <><Ic n="check" s={15} />Agregado</> : <><Ic n="plus" s={15} />Agregar</>}
          </button>
        </div>
      </div>

      <div className="ow-thumbs">
        {SERVICES.map((s) => {
          const added = s.base || data.services.has(s.id);
          return (
            <button key={s.id} className={`ow-thumb ${focus === s.id ? 'active' : ''}`} onClick={() => setFocus(s.id)}
            style={{ background: `linear-gradient(150deg, ${s.tint}40, #0a0e2a)` }}>
              <Ic n={s.icon} s={16} />
              {added && <span className="chk"><Ic n="check" s={9} /></span>}
            </button>);

        })}
      </div>
    </div>);

}

/* ============================================================
   helpers de form
   ============================================================ */
function Field({ label, opt, hint, children, full }) {
  return (
    <div className={`ow-field ${full ? 'col-2' : ''}`}>
      <label className="ow-label">{label}{opt && <span className="opt">· opcional</span>}</label>
      {children}
      {hint && <span className="ow-field-hint">{hint}</span>}
    </div>);

}
function AccountGate({ data, set, onAuthenticated }) {
  // RMR-685: cuenta REAL contra el backend (account_register / account_login).
  // Antes los botones eran placebo (set auth:true sin crear cuenta) -> con el gate del Paso 1
  // eso dejaria pasar gente SIN cuenta y sus datos no irian a BD. Aqui se crea cuenta de verdad.
  const emailOk = /.+@.+\..+/.test(data.cEmail);
  const phoneOk = (data.cPhone || '').replace(/\D/g, '').length >= 8;
  const passOk = data.cPass.length >= 4;
  const [busy, setBusy] = React.useState(false);
  const [err, setErr] = React.useState('');
  const [mode, setMode] = React.useState('register'); // 'register' | 'login'

  async function syncCurrentAccount() {
    try {
      const res = await fetch('/orden/api.php?action=account_me', {
        credentials: 'same-origin',
        cache: 'no-store',
      });
      let j = {}; try { j = await res.json(); } catch (e) {}
      const u = j.user || j;
      if (res.ok && u && (u.id || u.email)) {
        set({ auth: true, authMethod: 'email', cEmail: u.email || (data.cEmail || '').trim(), cPhone: u.phone || data.cPhone || '', cName: data.cName || u.name || '' });
        setErr('');
        return true;
      }
    } catch (e) {}
    return false;
  }

  async function owAuth(action) {
    if (busy) return; // lock anti doble-tap (RMR-669)
    setErr(''); setBusy(true);
    const controller = new AbortController();
    const timeoutId = setTimeout(() => controller.abort(), 12000);
    try {
      const res = await fetch('/orden/api.php?action=' + action, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        credentials: 'same-origin',
        signal: controller.signal,
        body: JSON.stringify({ email: (data.cEmail || '').trim(), password: data.cPass, phone: (data.cPhone || '').trim(), csrf_token: window.OW_CSRF }),
      });
      let j = {}; try { j = await res.json(); } catch (e) {}
      if (res.ok && (j.success || j.user_id)) {
        // cuenta creada/sesion iniciada de verdad -> auth real. auth_register/login ya auto-loguean
        // y ligan cualquier draft guest a vipinv_brief.user_id en el servidor.
        set({ auth: true, authMethod: 'email', cEmail: j.email || (data.cEmail || '').trim(), cPhone: j.phone || data.cPhone || '', cName: data.cName || j.name || '' });
        if (typeof onAuthenticated === 'function') onAuthenticated();
        setErr('');
      } else if (res.status === 409) {
        // correo ya registrado -> cambiar a login, SIN dejar el boton muerto
        setMode('login');
        setErr('Este correo ya tiene cuenta. Escribe tu contraseña e inicia sesión.');
      } else if (res.status === 429) {
        setErr('Demasiados intentos. Espera un momento e inténtalo de nuevo.');
      } else {
        setErr(j.error || 'No se pudo completar. Revisa tus datos e inténtalo de nuevo.');
      }
    } catch (e) {
      const recovered = await syncCurrentAccount();
      if (!recovered) setErr('Problema de conexión. Revisa tu internet e inténtalo de nuevo.');
    } finally {
      clearTimeout(timeoutId);
      setBusy(false); // SIEMPRE re-habilita -> nunca dead-end
    }
  }

  if (data.auth) {
    const initial = (data.cName || data.cEmail || 'U').trim().charAt(0).toUpperCase();
    return (
      <div className="ow-acct">
        <div className="ow-acct-done">
          <div className="ow-acct-avatar">{initial}</div>
          <div className="ow-acct-done-body">
            <div className="ow-acct-done-name">{data.cName || 'Tu cuenta Miss XV VIP'}</div>
            <div className="ow-acct-done-mail">{data.cEmail}</div>
          </div>
          <span className="ow-acct-saved"><Ic n="check" s={12} />Tu progreso se guarda en tu cuenta</span>
          <button className="ow-acct-change" onClick={() => set({ auth: false, authMethod: null })}>Cambiar</button>
        </div>
      </div>);

  }
  return (
    <div className="ow-acct">
      <div className="ow-acct-top">
        <div className="ow-acct-ico"><Ic n="lock" s={20} /></div>
        <div>
          <div className="ow-acct-title">Crea tu cuenta para comenzar</div>
          <div className="ow-acct-desc">Tu invitación y todo tu progreso se guardan en tu cuenta — <strong>continúa desde cualquier celular o computadora</strong>, y accede a tu RSVP, invitados y álbum QR.</div>
        </div>
      </div>
      <div className="ow-acct-form">
        <div className="ow-field col-2">
          <label className="ow-label">Correo</label>
          <input className="ow-input" type="email" autoComplete="email" placeholder="tu@correo.com" value={data.cEmail} onChange={(e) => set({ cEmail: e.target.value })} />
        </div>
        {mode === 'register' ? (
          <div className="ow-field col-2">
            <label className="ow-label">WhatsApp</label>
            <input className="ow-input" type="tel" autoComplete="tel" placeholder="+1 305 123 4567" value={data.cPhone} onChange={(e) => set({ cPhone: e.target.value })} />
          </div>
        ) : null}
        <div className="ow-field col-2">
          <label className="ow-label">Contraseña <span className="opt">· mín. 4 caracteres</span></label>
          <input className="ow-input" type="password" autoComplete={mode === 'login' ? 'current-password' : 'new-password'} placeholder={mode === 'login' ? 'Tu contraseña' : 'Crea tu contraseña'} value={data.cPass} onChange={(e) => set({ cPass: e.target.value })} />
        </div>
        {err ? <div className="ow-acct-err" style={{ gridColumn: '1 / -1', color: '#c0392b', fontSize: '.86rem', margin: '2px 0 6px', lineHeight: 1.4 }}>{err}</div> : null}
        <button className="ow-acct-submit" disabled={busy || !(emailOk && passOk && (mode === 'login' || phoneOk))} onClick={() => owAuth(mode === 'login' ? 'account_login' : 'account_register')}>
          <Ic n="check" s={15} />{busy ? 'Un momento…' : (mode === 'login' ? 'Iniciar sesión y continuar' : 'Crear cuenta y comenzar')}
        </button>
      </div>
      <div className="ow-acct-mini">
        {mode === 'login'
          ? <>¿No tienes cuenta? <span className="lk" onClick={() => { setMode('register'); setErr(''); }}>Crear cuenta</span></>
          : <>¿Ya tienes cuenta? <span className="lk" onClick={() => { setMode('login'); setErr(''); }}>Inicia sesión</span></>}
      </div>
    </div>);

}

/* ============================================================
   PASO 1 — cuenta + servicios à la carte + promos
   ============================================================ */
function StepServices({ data, set, totals, toggleSvc, focus, setFocus, onAuthenticated }) {
  const toggle = (s) => {if (!s.base) toggleSvc(s.id);};
  const toGo400 = Math.max(0, 400 - totals.gross);
  const toGo700 = Math.max(0, 700 - totals.gross);
  const activePkg = totals.pkg;
  return (
    <div className="ow-step">
      <h1 className="ow-step-title">Bienvenida, <em>Miss XV VIP</em> <span className="ow-spark">✨</span></h1>
      <p className="ow-step-sub">Aquí empieza tu historia. Elige y personaliza tu invitación paso a paso, a tu manera, con una vista previa en vivo mientras decides. Al confirmar tu orden, nuestro equipo produce la versión final —fondos, estilos y animación de intro— y te la entrega en 1 a 3 días. Nosotros nos encargamos del resto. ♡</p>

      <AccountGate data={data} set={set} onAuthenticated={onAuthenticated} />

      <p className="ow-section-label"><Ic n="zap" s={14} style={{ color: 'var(--gold)' }} />Empieza con un paquete <span className="hint">— o personaliza abajo</span></p>
      <div className="ow-pkgrow">
        {PKGS.map((p) => {
          const active = activePkg && activePkg.id === p.id;
          const t = p.price;
          const list = pkgListValue(p);
          const save = list - t;
          return (
            <button key={p.id} className={`ow-pkgcard ${active ? 'sel' : ''} ${p.popular ? 'pop' : ''}`} onClick={() => set({ services: new Set(p.items) })}>
              {p.popular && <span className="ow-pkgcard-pop">Más popular</span>}
              {active && <span className="ow-pkgcard-chk"><Ic n="check" s={13} /></span>}
              <div className="ow-pkgcard-ico"><Ic n={p.icon} s={20} /></div>
              <div className="ow-pkgcard-name">{p.name}</div>
              <div className="ow-pkgcard-tag">{p.tagline}</div>
              <div className="ow-pkgcard-price">{USD(t)}<small>USD</small>{save > 0 && <span className="ow-pkgcard-was">{USD(list)}</span>}</div>
              {save > 0 ? <div className="ow-pkgcard-save"><Ic n="zap" s={11} />Ahorras {USD(save)}</div> : <div className="ow-pkgcard-save flat">Precio base</div>}
            </button>);

        })}
      </div>

      {activePkg ?
      <div className="ow-promos">
          <div className="ow-promo unlocked">
            <div className="ow-promo-ico"><Ic n="check" s={17} /></div>
            <div className="ow-promo-body">
              <div className="ow-promo-txt"><strong>Paquete {activePkg.name} seleccionado</strong> · {activePkg.noSavings ? 'invitación personalizada' : `${activePkg.items.length + 1} ${activePkg.items.length + 1 === 1 ? 'servicio' : 'servicios'}`} por <strong>{USD(activePkg.price)} USD</strong>. Agrega o quita abajo para personalizarlo.</div>
            </div>
          </div>
        </div> :

      <div className="ow-promos">
        <div className={`ow-promo ${totals.freeQS ? 'unlocked' : 'locked'}`}>
          <div className="ow-promo-ico"><Ic n={totals.freeQS ? 'check' : 'gift'} s={17} /></div>
          <div className="ow-promo-body">
            <div className="ow-promo-txt">{totals.freeQS ?
              <><strong>¡Desbloqueado!</strong> QR álbum y Organizador de Mesas <strong>GRATIS</strong> en tu orden.</> :
              <>Agrega <strong>{USD(toGo400)}</strong> más y llévate <strong>QR álbum + Organizador de Mesas gratis</strong> ($400+).</>}</div>
            {!totals.freeQS && <div className="ow-promo-bar"><i style={{ width: `${Math.min(100, totals.gross / 400 * 100)}%` }} /></div>}
          </div>
        </div>
        <div className={`ow-promo ${totals.freeSlide ? 'unlocked' : 'locked'}`}>
          <div className="ow-promo-ico"><Ic n={totals.freeSlide ? 'check' : 'zap'} s={17} /></div>
          <div className="ow-promo-body">
            <div className="ow-promo-txt">{totals.freeSlide ?
              <><strong>¡Desbloqueado!</strong> VIP Slideshow <strong>GRATIS</strong> en tu orden.</> :
              <>Llega a <strong>$700</strong> y el <strong>VIP Slideshow</strong> es gratis — te faltan <strong>{USD(toGo700)}</strong>.</>}</div>
            {!totals.freeSlide && <div className="ow-promo-bar"><i style={{ width: `${Math.min(100, totals.gross / 700 * 100)}%` }} /></div>}
          </div>
        </div>
      </div>
      }

      <p className="ow-section-label"><Ic n="sparkle" s={14} style={{ color: 'var(--gold)' }} />O personaliza servicio por servicio <span className="hint">— toca para ver el demo, + para agregar</span></p>
      <div className="ow-svclist">
        {SERVICES.map((s) => {
          const signatureOn = data.services.has('signature');
          const on = s.base ? !signatureOn : data.services.has(s.id);
          const free = totals.freeIds.has(s.id);
          if (s.id === 'signature' && activePkg && activePkg.id !== 'signature') return null;
          return (
            <div key={s.id} className={`ow-svc ${on ? 'on' : ''} ${s.base ? 'base' : ''} ${focus === s.id ? 'focus' : ''}`}
            onMouseEnter={() => setFocus(s.id)} onClick={() => setFocus(s.id)}>
              <div className="ow-svc-ico"><Ic n={s.icon} s={19} /></div>
              <div className="ow-svc-body">
                <div className="ow-svc-name">{s.name}{s.popular && <span className="ow-svc-pop">Más popular</span>}<span className="ow-svc-play"><Ic n="play" s={9} />Demo</span></div>
                <div className="ow-svc-desc">{s.short} · {s.base ? 'siempre incluida' : s.desc}</div>
              </div>
              <div className="ow-svc-right">
                <div className="ow-svc-price">
                  {activePkg && on ? <span className="free">Incluido</span> : <>
                    {s.was && !free && <span className="was">{USD(s.was)}</span>}
                    {free ? <span className="free">GRATIS</span> : USD(s.price)}
                  </>}
                </div>
                <button className="ow-svc-add" onClick={(e) => {e.stopPropagation();toggle(s);}} aria-label="Agregar">
                  <Ic n={on ? 'check' : 'plus'} s={15} />
                </button>
              </div>
            </div>);

        })}
      </div>
    </div>);

}

function StepHonoree({ data, set }) {
  const mode = data.dateMode || 'exact';
  const months = [
  ['01', 'Enero'], ['02', 'Febrero'], ['03', 'Marzo'], ['04', 'Abril'], ['05', 'Mayo'], ['06', 'Junio'],
  ['07', 'Julio'], ['08', 'Agosto'], ['09', 'Septiembre'], ['10', 'Octubre'], ['11', 'Noviembre'], ['12', 'Diciembre']];
  const days = Array.from({ length: 31 }, (_, i) => String(i + 1).padStart(2, '0'));
  const hours = Array.from({ length: 36 }, (_, i) => {
    const hour = Math.floor(i / 2) + 6;
    const minute = i % 2 === 0 ? '00' : '30';
    const value = `${String(hour).padStart(2, '0')}:${minute}`;
    const labelHour = hour === 12 ? 12 : hour > 12 ? hour - 12 : hour;
    return [value, `${labelHour}:${minute} ${hour >= 12 ? 'PM' : 'AM'}`];
  });
  return (
    <div className="ow-step">
      <h1 className="ow-step-title">Datos del <em>evento</em></h1>
      <p className="ow-step-sub">Cuéntanos lo esencial para comenzar a crear tu invitación.</p>
      <div className="ow-grid ow-grid-2">
        <Field label="Nombre de la festejada" full><input className="ow-input display" placeholder="Ej. María Fernanda" value={data.honoree} onChange={(e) => set({ honoree: e.target.value })} /></Field>
        <Field label="Fecha del evento" full hint="¿Aún no confirmas el día? Elige solo mes y año, o déjala por confirmar — no es obligatoria.">
          <div className="ow-datemode">
            {[['exact', 'Fecha exacta'], ['monthyear', 'Mes y año'], ['tbd', 'Por confirmar']].map(([m, lbl]) =>
            <button key={m} type="button" className={`ow-dm ${mode === m ? 'on' : ''}`} onClick={() => set({ dateMode: m })}>{lbl}</button>
            )}
          </div>
          {mode === 'exact' &&
          <div className="ow-date-row">
              <select className="ow-input" value={data.eventMonth || ''} onChange={(e) => set({ eventMonth: e.target.value })} aria-label="Mes del evento">
                <option value="">Mes</option>
                {months.map(([value, label]) => <option key={value} value={value}>{label}</option>)}
              </select>
              <select className="ow-input" value={data.eventDay || ''} onChange={(e) => set({ eventDay: e.target.value })} aria-label="Día del evento">
                <option value="">Día</option>
                {days.map((day) => <option key={day} value={day}>{day}</option>)}
              </select>
              <input className="ow-input" inputMode="numeric" maxLength="4" placeholder="Año" value={data.eventYear || ''} onChange={(e) => set({ eventYear: e.target.value.replace(/\D/g, '').slice(0, 4) })} aria-label="Año del evento" />
              <select className="ow-input" value={data.time || ''} onChange={(e) => set({ time: e.target.value })} aria-label="Hora del evento">
                <option value="">Hora</option>
                {hours.map(([value, label]) => <option key={value} value={value}>{label}</option>)}
              </select>
              <select className="ow-input" value={data.endTime || ''} onChange={(e) => set({ endTime: e.target.value })} aria-label="Hora de finalización">
                <option value="">Fin</option>
                {hours.map(([value, label]) => <option key={value} value={value}>{label}</option>)}
              </select>
            </div>
          }
          {mode === 'monthyear' &&
          <div className="ow-date-row ow-date-row-month">
              <select className="ow-input" value={data.eventMYMonth || ''} onChange={(e) => set({ eventMYMonth: e.target.value })} aria-label="Mes del evento">
                <option value="">Mes</option>
                {months.map(([value, label]) => <option key={value} value={value}>{label}</option>)}
              </select>
              <input className="ow-input" inputMode="numeric" maxLength="4" placeholder="Año" value={data.eventMYYear || ''} onChange={(e) => set({ eventMYYear: e.target.value.replace(/\D/g, '').slice(0, 4) })} aria-label="Año del evento" />
          </div>
          }
          {mode === 'tbd' &&
          <div className="ow-date-tbd"><Ic n="clock" s={15} />Tu invitación dirá <strong>“Fecha por confirmar”</strong>. Podrás fijar el día después desde tu panel.</div>
          }
        </Field>
        <Field label="Lugar / Salón" opt><input className="ow-input" placeholder="Ej. The Grand Ballroom" value={data.venue} onChange={(e) => set({ venue: e.target.value })} /></Field>
        <Field label="Ciudad" opt><input className="ow-input" placeholder="Ej. Houston, TX" value={data.city} onChange={(e) => set({ city: e.target.value })} /></Field>
      </div>
      <div className="ow-divider" />
      <p className="ow-subhead"><Ic n="heart" s={15} />Familia principal <span style={{ color: 'var(--text-4)', fontWeight: 500 }}>· opcional</span></p>
      <div className="ow-grid ow-grid-2">
        <Field label="Mamá" opt><input className="ow-input" placeholder="Nombre de mamá" value={data.mom} onChange={(e) => set({ mom: e.target.value })} /></Field>
        <Field label="Papá" opt><input className="ow-input" placeholder="Nombre de papá" value={data.dad} onChange={(e) => set({ dad: e.target.value })} /></Field>
        <Field label="Abuelitos" opt><textarea className="ow-input" rows="2" placeholder="Nombres de abuelitos o familiares especiales" value={data.grandparents || ''} onChange={(e) => set({ grandparents: e.target.value })} /></Field>
      </div>
    </div>);

}

/* secciones cuyo contenido se arma después en el panel (no se captura en la compra) */
const SEC_PANEL_NOTE = {
  gallery: 'Indica cómo enviarás tus fotos y cualquier link de referencia disponible.',
  music: 'Escribe la canción o pega un link de Spotify / YouTube si ya lo tienes.',
  video_intro: 'La animación de apertura es preparada por nuestro equipo según el estilo de tu evento.'
};

function SecInlineFields({ id, data, set }) {
  const hours = Array.from({ length: 36 }, (_, i) => {
    const hour = Math.floor(i / 2) + 6;
    const minute = i % 2 === 0 ? '00' : '30';
    const value = `${String(hour).padStart(2, '0')}:${minute}`;
    const labelHour = hour === 12 ? 12 : hour > 12 ? hour - 12 : hour;
    return [value, `${labelHour}:${minute} ${hour >= 12 ? 'PM' : 'AM'}`];
  });
  const updateList = (key, index, patch) => {
    const rows = Array.isArray(data[key]) ? data[key] : [];
    set({ [key]: rows.map((row, i) => i === index ? { ...row, ...patch } : row) });
  };
  const addRow = (key, row) => set({ [key]: [...(Array.isArray(data[key]) ? data[key] : []), row] });
  const removeRow = (key, index, fallbackRow) => {
    const rows = (Array.isArray(data[key]) ? data[key] : []).filter((_, i) => i !== index);
    set({ [key]: rows.length ? rows : [fallbackRow] });
  };
  if (id === 'reception') return (
    <div className="ow-grid ow-grid-2">
      <Field label="Dirección del salón" full><input className="ow-input" placeholder="Ej. 2700 SW Freeway, Houston, TX 77098" value={data.recepAddr} onChange={(e) => set({ recepAddr: e.target.value })} /></Field>
      <Field label="Hora de recepción"><select className="ow-input" value={data.recepTime || ''} onChange={(e) => set({ recepTime: e.target.value })}>
        <option value="">Hora</option>
        {hours.map(([value, label]) => <option key={value} value={value}>{label}</option>)}
      </select></Field>
      <Field label="Enlace de Google Maps" opt><input className="ow-input" placeholder="https://maps.app.goo.gl/…" value={data.recepMaps} onChange={(e) => set({ recepMaps: e.target.value })} /></Field>
    </div>);

  if (id === 'ceremony') return (
    <div className="ow-grid ow-grid-2">
      <Field label="Lugar de ceremonia / iglesia" opt><input className="ow-input" placeholder="Ej. St. Mary's Church" value={data.ceremonyVenue || ''} onChange={(e) => set({ ceremonyVenue: e.target.value })} /></Field>
      <Field label="Hora de ceremonia" opt><select className="ow-input" value={data.ceremonyTime || ''} onChange={(e) => set({ ceremonyTime: e.target.value })}>
        <option value="">Hora</option>
        {hours.map(([value, label]) => <option key={value} value={value}>{label}</option>)}
      </select></Field>
      <Field label="Dirección de ceremonia" opt full><input className="ow-input" placeholder="Dirección completa" value={data.ceremonyAddress || ''} onChange={(e) => set({ ceremonyAddress: e.target.value })} /></Field>
      <Field label="Enlace de Google Maps" opt full><input className="ow-input" placeholder="https://maps.google.com/..." value={data.ceremonyMaps || ''} onChange={(e) => set({ ceremonyMaps: e.target.value })} /></Field>
    </div>);

  if (id === 'parents') return (
    <div className="ow-grid">
      <div className="ow-sec-note"><Ic n="info" s={14} />Mamá y papá se toman de Datos del evento. Aquí agrega solo familia adicional o notas para producción.</div>
      <Field label="Notas de familia" opt full><textarea className="ow-input" placeholder="Ej. Abuelos, familia especial o una nota breve para producción" value={data.familyNotes || ''} onChange={(e) => set({ familyNotes: e.target.value })} /></Field>
    </div>);

  if (id === 'godparents') {
    const roles = ['Honor', 'Vals', 'Brindis', 'Pastel', 'Recuerdo', 'Ramo', 'Velación', 'Cojines', 'Libro y Rosario', 'Anillos', 'Otro'];
    const rows = Array.isArray(data.godparentRows) && data.godparentRows.length ? data.godparentRows : [{ role: 'Honor', customRole: '', names: '' }];
    return (
      <div className="ow-grid" style={{ gap: 10 }}>
        {rows.map((row, index) =>
        <div className="ow-grid ow-grid-2" key={index} style={{ alignItems: 'end' }}>
          <Field label="Rol"><select className="ow-input" value={row.role || 'Honor'} onChange={(e) => updateList('godparentRows', index, { role: e.target.value })}>
            {roles.map((role) => <option key={role}>{role}</option>)}
          </select></Field>
          <Field label="Nombre(s)"><input className="ow-input" placeholder="Persona, pareja o grupo" value={row.names || ''} onChange={(e) => updateList('godparentRows', index, { names: e.target.value })} /></Field>
          {row.role === 'Otro' && <Field label="Rol personalizado" opt full><input className="ow-input" placeholder="Ej. Biblia, zapatillas, corona" value={row.customRole || ''} onChange={(e) => updateList('godparentRows', index, { customRole: e.target.value })} /></Field>}
          <button type="button" className="ow-link-btn" onClick={() => removeRow('godparentRows', index, { role: 'Honor', customRole: '', names: '' })}>Quitar</button>
        </div>)}
        <button type="button" className="ow-btn ghost" onClick={() => addRow('godparentRows', { role: 'Honor', customRole: '', names: '' })}>Agregar padrino</button>
      </div>);
  }

  if (id === 'food_menu') return (
    <div className="ow-grid ow-grid-2">
      <Field label="Tipo de servicio" full><select className="ow-input" value={data.foodServiceType || ''} onChange={(e) => set({ foodServiceType: e.target.value })}>
        <option value="">Selecciona</option>
        <option>Menú servido</option>
        <option>Buffet</option>
        <option>Cocktail / bocadillos</option>
        <option>Estaciones de comida</option>
        <option>Otro</option>
      </select></Field>
      <Field label="Menú u opciones principales" full><textarea className="ow-input" placeholder="Ej. Entrada, plato fuerte, postre o estaciones del buffet" value={data.foodMenuText || ''} onChange={(e) => set({ foodMenuText: e.target.value })} /></Field>
      <Field label="Notas dietéticas o acomodos" opt full><textarea className="ow-input" placeholder="Ej. opción vegetariana, alergias, menú infantil" value={data.foodAccommodationNotes || ''} onChange={(e) => set({ foodAccommodationNotes: e.target.value })} /></Field>
      <Field label="Hora de cena / servicio" opt><select className="ow-input" value={data.foodServingTime || ''} onChange={(e) => set({ foodServingTime: e.target.value })}>
        <option value="">Hora</option>
        {hours.map(([value, label]) => <option key={value} value={value}>{label}</option>)}
      </select></Field>
      <Field label="Hora de cocktail / aperitivos" opt><select className="ow-input" value={data.cocktailTime || ''} onChange={(e) => set({ cocktailTime: e.target.value })}>
        <option value="">Hora</option>
        {hours.map(([value, label]) => <option key={value} value={value}>{label}</option>)}
      </select></Field>
    </div>);

  if (id === 'logistics') return (
    <div className="ow-grid ow-grid-2">
      <Field label="Hotel recomendado" opt><input className="ow-input" placeholder="Ej. Hilton Garden Inn Burbank" value={data.hotelName || ''} onChange={(e) => set({ hotelName: e.target.value })} /></Field>
      <Field label="Dirección del hotel" opt><input className="ow-input" placeholder="Dirección completa" value={data.hotelAddress || ''} onChange={(e) => set({ hotelAddress: e.target.value })} /></Field>
      <Field label="Mapa del hotel" opt><input className="ow-input" placeholder="https://maps.google.com/..." value={data.hotelMaps || ''} onChange={(e) => set({ hotelMaps: e.target.value })} /></Field>
      <Field label="Código / tarifa de grupo" opt><input className="ow-input" placeholder="Ej. XVVIP2027" value={data.hotelRateCode || ''} onChange={(e) => set({ hotelRateCode: e.target.value })} /></Field>
      <Field label="Parking" opt full><textarea className="ow-input" placeholder="Ej. Valet disponible, estacionamiento gratis, entrada por calle lateral" value={data.parkingNotes || ''} onChange={(e) => set({ parkingNotes: e.target.value })} /></Field>
      <Field label="Shuttle / transporte" opt full><textarea className="ow-input" placeholder="Ej. Shuttle del hotel al salón a las 6:30 PM" value={data.shuttleInfo || ''} onChange={(e) => set({ shuttleInfo: e.target.value })} /></Field>
    </div>);

  if (id === 'itinerary') {
    const rows = Array.isArray(data.itineraryRows) && data.itineraryRows.length ? data.itineraryRows : [{ time: '', activity: '' }];
    return (
      <div className="ow-grid" style={{ gap: 10 }}>
        {rows.map((row, index) =>
        <div className="ow-grid ow-grid-2" key={index} style={{ alignItems: 'end' }}>
          <Field label="Hora" opt><select className="ow-input" value={row.time || ''} onChange={(e) => updateList('itineraryRows', index, { time: e.target.value })}>
            <option value="">Hora</option>
            {hours.map(([value, label]) => <option key={value} value={value}>{label}</option>)}
          </select></Field>
          <Field label="Actividad" opt><input className="ow-input" placeholder="Ej. Entrada, vals, cena, brindis" value={row.activity || ''} onChange={(e) => updateList('itineraryRows', index, { activity: e.target.value })} /></Field>
          <button type="button" className="ow-link-btn" onClick={() => removeRow('itineraryRows', index, { time: '', activity: '' })}>Quitar</button>
        </div>)}
        <button type="button" className="ow-btn ghost" onClick={() => addRow('itineraryRows', { time: '', activity: '' })}>Agregar actividad</button>
      </div>);
  }

  if (id === 'registry') return (
    <Field label="Datos o links de tu registro" full><textarea className="ow-input" placeholder="Ej. Liverpool #1234567 · Amazon: https://…&#10;Transferencia: CLABE 0123…" value={data.registry} onChange={(e) => set({ registry: e.target.value })} /></Field>);

  if (id === 'corte') return (
    <div className="ow-grid" style={{ gap: 10 }}>
      {(Array.isArray(data.courtRows) && data.courtRows.length ? data.courtRows : [{ role: 'Dama', name: '' }]).map((row, index) =>
      <div className="ow-grid ow-grid-2" key={index} style={{ alignItems: 'end' }}>
        <Field label="Rol"><select className="ow-input" value={row.role || 'Dama'} onChange={(e) => updateList('courtRows', index, { role: e.target.value })}>
          <option>Dama</option>
          <option>Chambelán</option>
          <option>Dama de Honor</option>
          <option>Chambelán de Honor</option>
          <option>Otro</option>
        </select></Field>
        <Field label="Nombre"><input className="ow-input" placeholder="Nombre de la persona" value={row.name || ''} onChange={(e) => updateList('courtRows', index, { name: e.target.value })} /></Field>
        <button type="button" className="ow-link-btn" onClick={() => removeRow('courtRows', index, { role: 'Dama', name: '' })}>Quitar</button>
      </div>)}
      <button type="button" className="ow-btn ghost" onClick={() => addRow('courtRows', { role: 'Dama', name: '' })}>Agregar persona</button>
    </div>);

  if (id === 'dress_code') return (
    <Field label="Código de vestimenta" full><input className="ow-input" placeholder="Ej. Etiqueta formal · Vestido largo" value={data.dressCode} onChange={(e) => set({ dressCode: e.target.value })} /></Field>);

  if (id === 'rsvp') return (
    <Field label="Fecha límite de confirmación" opt full><input className="ow-input" type="date" value={data.rsvpDeadline} onChange={(e) => set({ rsvpDeadline: e.target.value })} /></Field>);

  if (id === 'gallery') return (
    <div className="ow-grid ow-grid-2">
      <Field label="Cantidad de fotos" opt><select className="ow-input" value={data.galleryPhotoCount || ''} onChange={(e) => set({ galleryPhotoCount: e.target.value })}>
        <option value="">Selecciona</option>
        {[1, 2, 3, 4, 5, 6].map((n) => <option key={n} value={n}>{n}</option>)}
      </select></Field>
      <Field label="Cómo enviarás las fotos" opt><select className="ow-input" value={data.galleryMethod || ''} onChange={(e) => set({ galleryMethod: e.target.value })}>
        <option value="">Selecciona</option>
        <option>WhatsApp</option>
        <option>Email</option>
        <option>Link Google Drive</option>
      </select></Field>
      <Field label="Link Drive de fotos" opt full><input className="ow-input" placeholder="https://drive.google.com/..." value={data.galleryDriveLink || ''} onChange={(e) => set({ galleryDriveLink: e.target.value })} /></Field>
    </div>);

  if (id === 'music') return (
    <div className="ow-grid ow-grid-2">
      <Field label="Canción y artista" opt><input className="ow-input" placeholder="Ej. Tiempo de Vals - Chayanne" value={data.musicText || ''} onChange={(e) => set({ musicText: e.target.value })} /></Field>
      <Field label="Link de Spotify / YouTube" opt><input className="ow-input" placeholder="https://open.spotify.com/..." value={data.musicUrl || ''} onChange={(e) => set({ musicUrl: e.target.value })} /></Field>
    </div>);

  if (id === 'qrvip') return (
    <div className="ow-grid ow-grid-2">
      <Field label="Nombre del álbum QR" opt><input className="ow-input" placeholder="Ej. XV de Isabella" value={data.qrAlbumName || ''} onChange={(e) => set({ qrAlbumName: e.target.value })} /></Field>
      <Field label="Estilo visual del QR" opt><input className="ow-input" placeholder="Ej. dorado, elegante, floral" value={data.qrAlbumStyle || ''} onChange={(e) => set({ qrAlbumStyle: e.target.value })} /></Field>
      <Field label="Notas para los letreros QR" opt full><textarea className="ow-input" placeholder="Ej. texto corto, color, frase para mesa de bienvenida" value={data.qrAlbumNotes || ''} onChange={(e) => set({ qrAlbumNotes: e.target.value })} /></Field>
    </div>);

  if (id === 'guestbook') return (
    <Field label="Mensaje para el libro de visitas" opt full><textarea className="ow-input" placeholder="Ej. Déjanos un mensaje bonito para recordar esta noche" value={data.guestbookPrompt || ''} onChange={(e) => set({ guestbookPrompt: e.target.value })} /></Field>);

  if (id === 'thanks') return (
    <Field label="Mensaje de agradecimiento" opt full><textarea className="ow-input" placeholder="Ej. Gracias por acompañarme en este día tan especial" value={data.thanksMessage || ''} onChange={(e) => set({ thanksMessage: e.target.value })} /></Field>);

  if (id === 'hero') return (
    <Field label="Frase de bienvenida" opt full><input className="ow-input" placeholder="Ej. Acompáñame a celebrar mis XV años" value={data.welcome} onChange={(e) => set({ welcome: e.target.value })} /></Field>);

  if (id === 'after_party') return (
    <div className="ow-grid ow-grid-2">
      <Field label="Incluir After Party" full><select className="ow-input" value={data.includeAfterParty || ''} onChange={(e) => set({ includeAfterParty: e.target.value })}>
        <option value="">Selecciona</option>
        <option value="si">Sí</option>
        <option value="no">No</option>
      </select></Field>
      <Field label="Lugar / nombre del After Party" opt><input className="ow-input" placeholder="Ej. Casa de los García" value={data.afterPartyVenue || ''} onChange={(e) => set({ afterPartyVenue: e.target.value })} /></Field>
      <Field label="Dirección del After Party" opt full><input className="ow-input" placeholder="Dirección completa" value={data.afterPartyAddress || ''} onChange={(e) => set({ afterPartyAddress: e.target.value })} /></Field>
      <Field label="Mapa del After Party" opt full><input className="ow-input" placeholder="https://maps.google.com/..." value={data.afterPartyMaps || ''} onChange={(e) => set({ afterPartyMaps: e.target.value })} /></Field>
      <Field label="Hora de inicio del After Party" opt><select className="ow-input" value={data.afterPartyTime || ''} onChange={(e) => set({ afterPartyTime: e.target.value })}>
        <option value="">Hora</option>
        {hours.map(([value, label]) => <option key={value} value={value}>{label}</option>)}
      </select></Field>
    </div>);

  if (SEC_PANEL_NOTE[id]) return (
    <div className="ow-sec-note"><Ic n="info" s={14} />{SEC_PANEL_NOTE[id]}</div>);

  return null;
}
const SEC_HAS_FIELDS = (id) => ['ceremony', 'reception', 'parents', 'godparents', 'food_menu', 'logistics', 'itinerary', 'registry', 'corte', 'dress_code', 'rsvp', 'gallery', 'music', 'qrvip', 'guestbook', 'thanks', 'hero', 'after_party'].includes(id) || !!SEC_PANEL_NOTE[id];

function StepSections({ data, set, toggleSec }) {
  const lang = data.lang;
  const langOpts = [['es', 'Español'], ['en', 'Inglés'], ['bi', 'Bilingüe ES/EN']];
  return (
    <div className="ow-step">
      <h1 className="ow-step-title">Secciones de <em>tu invitación</em></h1>
      <p className="ow-step-sub">Elige qué deseas incluir y completa aquí los detalles principales de tu invitación.</p>

      <div className="ow-seclist">
        {SECTIONS.map((sec) => {
          const on = data.sections[sec.id];
          const expand = on && SEC_HAS_FIELDS(sec.id);
          return (
            <div key={sec.id} className={`ow-sec-wrap ${on ? 'on' : ''}`}>
              <button className={`ow-sec ${on ? 'on' : ''}`} onClick={() => toggleSec(sec.id)}>
                <div className="ow-sec-ico"><Ic n={sec.icon} s={17} /></div>
                <div className="ow-sec-body">
                  <div className="ow-sec-name">{sec.name}</div>
                  <div className="ow-sec-desc">{sec.desc}</div>
                </div>
                <span className="ow-tgl" />
              </button>
              {expand &&
              <div className="ow-sec-panel">
                  <SecInlineFields id={sec.id} data={data} set={set} />
                </div>
              }
            </div>);

        })}
      </div>

      <div className="ow-divider" />
      <Field label="Idioma de la invitación">
        <div className="ow-lang">
          {langOpts.map(([v, l]) =>
          <button key={v} className={`ow-lang-opt ${lang === v ? 'on' : ''}`} onClick={() => set({ lang: v })}><Ic n="globe" s={13} />{l}</button>
          )}
        </div>
      </Field>
    </div>);

}

function StepTheme({ data, set }) {
  const isOperator = typeof window !== 'undefined' && window.OW_IS_OPERATOR === true;
  const hasOperatorTheme = !!(data.customThemeName || data.customThemeDescription || data.themeCoverUrl || data.themePreviewUrl || (Array.isArray(data.themeColors) && data.themeColors.some(Boolean)));
  const [showOperatorTheme, setShowOperatorTheme] = useState(false);
  const [editExistingTheme, setEditExistingTheme] = useState('');
  const [previewTheme, setPreviewTheme] = useState('');

  // FASE 2 ORDER-VIP: catalogo real via cache compartido (una sola llamada de red para
  // todo el wizard). catalogState: 'idle'/'loading' = cargando, 'error' = fallo
  // (fail-closed, sin fallback legacy silencioso), 'ready' = catalogo cargado.
  const catalog = useThemeCatalog();
  const loadCatalog = () => { themeCatalogCache.status = 'idle'; fetchThemeCatalog(); };

  const THEMES = catalog.status === 'ready' ? catalog.data : [];

  const editingTheme = THEMES.find((t) => t.id === editExistingTheme);
  const previewingTheme = THEMES.find((t) => t.id === previewTheme);
  const usesSignature = !chosenTheme(data);
  const previewUrlFor = (theme) => data[`themeEdit_${theme.id}_themePreviewUrl`] || theme.demoUrl || '';
  const openPreview = (theme) => {
    const url = previewUrlFor(theme);
    if (url) window.open(url, '_blank', 'noopener,noreferrer');
    else setPreviewTheme(theme.id);
  };
  return (
    <div className="ow-step">
      <h1 className="ow-step-title">Elige el estilo de <em>tu invitación</em></h1>
      <p className="ow-step-sub">Explora nuestros diseños originales y selecciona la temática que mejor combine con tu evento. Si no ves tu estilo ideal, continúa con Signature y nuestro equipo crea una invitación desde cero para ti.</p>
      <div className={`ow-theme-package ${usesSignature ? 'signature' : 'original'}`}>
        <Ic n={usesSignature ? 'palette' : 'sparkle'} s={15} />
        <div>
          <strong>{usesSignature ? 'Continúa con tu Signature personalizada' : 'Este diseño queda en Original'}</strong>
          <span>{usesSignature ? 'Nuestro equipo diseña una temática a tu medida · $200 USD' : 'Si esta temática no es exactamente lo que buscas, podemos crear una desde cero para ti con Signature · $200 USD.'}</span>
        </div>
        {!usesSignature &&
        <button type="button" className="ow-theme-signature-btn" onClick={() => set({ theme: '', themeSelected: false })}>
            Quiero una temática personalizada
          </button>
        }
      </div>
      {(catalog.status === 'idle' || catalog.status === 'loading') &&
      <p className="ow-theme-loading">Cargando temáticas…</p>
      }
      {catalog.status === 'error' &&
      <div className="ow-theme-error">
          <p>No pudimos cargar el catálogo de temáticas.</p>
          <button type="button" onClick={loadCatalog}>Reintentar</button>
        </div>
      }
      {catalog.status === 'ready' &&
      <div className="ow-themes">
        {THEMES.map((t) =>
        <div key={t.id} className={`ow-theme ${data.theme === t.id ? 'sel' : ''}`} role="button" tabIndex="0" onClick={() => set({ theme: t.id, themeSelected: true })} onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); set({ theme: t.id, themeSelected: true }); } }}>
            <div className="ow-theme-check"><Ic n="check" s={14} /></div>
            {isOperator && data.theme === t.id &&
            <button type="button" className="ow-theme-edit-chip" onClick={(e) => { e.preventDefault(); e.stopPropagation(); setEditExistingTheme(t.id); setShowOperatorTheme(false); }}>
                <Ic n="edit" s={12} />Personalizar para esta clienta
              </button>
            }
            <div className={`ow-theme-swatch ${t.preview ? 'has-img' : ''}`} style={{ background: `radial-gradient(120% 90% at 50% 0%, ${t.accent}33, transparent 55%), linear-gradient(180deg, ${t.bg[0]}, ${t.bg[1]})` }}>
              {t.preview ? <img src={t.preview} alt="" loading="lazy" /> : <span style={{ color: t.accent }}>{(data.honoree || 'Aa').slice(0, 2)}</span>}
            </div>
            <div className="ow-theme-info">
              <div><div className="ow-theme-name">{t.name}</div><div className="ow-theme-mood">{t.mood}</div></div>
              <div className="ow-theme-dots">{t.dots.map((c, i) => <span className="ow-theme-dot" key={i} style={{ background: c }} />)}</div>
            </div>
            <button type="button" className="ow-theme-preview-chip" onClick={(e) => { e.preventDefault(); e.stopPropagation(); openPreview(t); }}>
              <Ic n="eye" s={12} />Vista previa
            </button>
          </div>
        )}
      </div>
      }
      {previewingTheme &&
      <div className="ow-theme-preview-modal" role="dialog" aria-modal="true">
          <button type="button" className="ow-theme-preview-close" onClick={() => setPreviewTheme('')}><Ic n="x" s={16} /></button>
          <div className={`ow-theme-preview-hero ${previewingTheme.preview ? 'has-img' : ''}`} style={{ background: `radial-gradient(120% 90% at 50% 0%, ${previewingTheme.accent}33, transparent 55%), linear-gradient(180deg, ${previewingTheme.bg[0]}, ${previewingTheme.bg[1]})` }}>
            {previewingTheme.preview ? <img src={previewingTheme.preview} alt="" /> : <span style={{ color: previewingTheme.accent }}>{previewingTheme.name.slice(0, 2)}</span>}
          </div>
          <div className="ow-theme-preview-body">
            <h2>{previewingTheme.name}</h2>
            <p>{previewingTheme.mood}</p>
            <div className="ow-theme-preview-dots">{previewingTheme.dots.map((c, i) => <span key={i} style={{ background: c }} />)}</div>
          </div>
        </div>
      }
      {isOperator &&
      <div className="ow-ops-theme-shell" data-operator-only="theme-config">
          {/* FASE 2 ORDER-VIP: separacion explicita de conceptos (orden seccion 10-12).
              Esto YA NO pretende crear/editar el catalogo -- solo personaliza el brief
              de ESTA clienta (section_2.theme_edits). Para el catalogo global real:
              administrar en /orden/formularios/temas/. */}
          <a className="ow-ops-catalog-link" href="/orden/formularios/temas/" target="_blank" rel="noopener noreferrer">
            <Ic n="palette" s={14} />Administrar catálogo de temáticas →
          </a>
          <button type="button" className="ow-ops-create-btn" onClick={() => setShowOperatorTheme((v) => !v)}>
            <Ic n={showOperatorTheme ? 'chevleft' : 'plus'} s={15} />
            {showOperatorTheme ? 'Ocultar personalización' : hasOperatorTheme ? 'Editar personalización de esta clienta' : 'Personalizar para esta clienta'}
          </button>
          {showOperatorTheme && <OperatorThemePanel data={data} set={set} />}
          {editingTheme && <OperatorThemePanel data={data} set={set} mode="existing" theme={editingTheme} onClose={() => setEditExistingTheme('')} />}
        </div>
      }
    </div>);

}

function OperatorThemePanel({ data, set, mode = 'new', theme = null, onClose = null }) {
  const prefix = mode === 'existing' && theme ? `themeEdit_${theme.id}` : '';
  const getValue = (field, fallback = '') => {
    if (mode === 'existing' && prefix) return data[`${prefix}_${field}`] || fallback;
    return data[field] || fallback;
  };
  const setValue = (field, value) => {
    if (mode === 'existing' && prefix) set({ [`${prefix}_${field}`]: value });
    else set({ [field]: value });
  };
  const updateColor = (index, value) => {
    const colorKey = mode === 'existing' && prefix ? `${prefix}_themeColors` : 'themeColors';
    const colors = Array.isArray(data[colorKey]) ? data[colorKey].slice(0, 3) : [];
    while (colors.length < 3) colors.push('');
    colors[index] = value;
    set({ [colorKey]: colors });
  };
  const sourceColors = mode === 'existing' && prefix ? data[`${prefix}_themeColors`] : data.themeColors;
  const fallbackColors = theme ? theme.dots || [] : ['', '', ''];
  const colors = Array.isArray(sourceColors) ? sourceColors.slice(0, 3) : fallbackColors.slice(0, 3);
  while (colors.length < 3) colors.push('');
  return (
    <div className="ow-ops-theme">
      {mode === 'existing' && theme &&
      <div className="ow-ops-edit-head">
          <span>Editando temática: {theme.name}</span>
          <button type="button" onClick={onClose}><Ic n="x" s={13} />Cerrar</button>
        </div>
      }
      <div className="ow-grid">
        <Field label={mode === 'existing' ? 'Título' : 'Nombre de temática nueva'} opt><input className="ow-input" placeholder={theme ? theme.name : 'Ej. Jardín dorado personalizado'} value={getValue('customThemeName', theme ? theme.name : '')} onChange={(e) => setValue('customThemeName', e.target.value)} /></Field>
        <Field label="Imagen de portada" opt><input className="ow-input" placeholder="https://..." value={getValue('themeCoverUrl', theme ? theme.preview : '')} onChange={(e) => setValue('themeCoverUrl', e.target.value)} /></Field>
        <Field label="Link de vista previa" opt><input className="ow-input" placeholder="https://..." value={getValue('themePreviewUrl', theme ? theme.demoUrl || '' : '')} onChange={(e) => setValue('themePreviewUrl', e.target.value)} /></Field>
        <Field label="Descripción" opt><textarea className="ow-input" rows="3" placeholder="Ej. Flores doradas, luz cálida y estilo elegante para una quinceañera de noche." value={getValue('customThemeDescription', theme ? theme.mood : '')} onChange={(e) => setValue('customThemeDescription', e.target.value)} /></Field>
      </div>
      <div className="ow-ops-colors">
        {colors.map((color, index) =>
          <Field label={`Color ${index + 1}`} opt key={index}>
            <div className="ow-ops-color-row">
              <input className="ow-color-input" type="color" value={color || '#eaa121'} onChange={(e) => updateColor(index, e.target.value)} />
              <input className="ow-input" placeholder="#EAA121" value={color || ''} onChange={(e) => updateColor(index, e.target.value)} />
            </div>
          </Field>
        )}
      </div>
      <p>Este formulario define la tarjeta de la temática y el enlace que abre su vista previa.</p>
    </div>);
}

function StepSummary({ data, totals, goTo, activePkg, orderTotal }) {
  const catalog = useThemeCatalog();
  const th = catalog.data.find((t) => t.id === data.theme);
  const lines = SERVICES.filter((s) => s.base || data.services.has(s.id));
  const recap = [
  { lbl: 'Festejada', ico: 'user', val: data.honoree },
  { lbl: 'Fecha', ico: 'calendar', val: eventDateLabel(data), pending: dateIsPending(data) },
  { lbl: 'Lugar', ico: 'mappin', val: data.venue && data.city ? `${data.venue} · ${data.city}` : data.venue || data.city },
  { lbl: 'Temática', ico: 'palette', val: (th && th.name) || data.theme || '' }];

  const langName = { es: 'Español', en: 'Inglés', bi: 'Bilingüe ES/EN' }[data.lang];
  const activeSecs = SECTIONS.filter((s) => secAvailable(s, data.services) && data.sections[s.id]);
  const pendingData = {
    reception: !(data.recepAddr || data.venue),
    registry: !data.registry,
    corte: !(data.corteNames || (Array.isArray(data.courtRows) && data.courtRows.some((r) => r.name))),
    itinerary: !(Array.isArray(data.itineraryRows) && data.itineraryRows.some((r) => r.time || r.activity)),
    dress_code: !data.dressCode,
    parents: !(data.mom || data.dad || data.grandparents || data.familyNotes),
    godparents: !(data.padrinos || (Array.isArray(data.godparentRows) && data.godparentRows.some((r) => r.names))),
    food_menu: !(data.foodServiceType && data.foodMenuText),
    logistics: !(data.hotelName || data.hotelAddress || data.hotelMaps || data.hotelRateCode || data.parkingNotes || data.shuttleInfo),
    after_party: !(data.includeAfterParty || data.afterPartyVenue || data.afterPartyAddress || data.afterPartyMaps || data.afterPartyTime),
    // Estas secciones no tienen campos obligatorios en este wizard; las completa produccion/panel.
    gallery: false, music: false, video_intro: false
  };
  const secPending = (id) => !!pendingData[id];
  const pendingCount = activeSecs.filter((s) => secPending(s.id)).length;
  return (
    <div className="ow-step">
      <h1 className="ow-step-title">Resumen de <em>tu orden</em></h1>
      <p className="ow-step-sub">Revisa la información antes de pagar. Puedes volver a cualquier paso para hacer ajustes.</p>
      <div className="ow-event-recap">
        {recap.map((r) =>
        <div className={`ow-recap-card ${r.pending && r.val ? 'pending' : ''}`} key={r.lbl}>
            <div className="ow-recap-lbl"><Ic n={r.pending && r.val ? 'clock' : r.ico} s={12} />{r.lbl}</div>
            <div className={`ow-recap-val ${r.val ? '' : 'empty'}`}>{r.val || 'Por definir'}</div>
            {r.pending && r.val && <div className="ow-recap-note">Pendiente de afinar — lo actualizas en tu panel</div>}
          </div>
        )}
      </div>
      <div className="ow-recap-secs">
        <div className="ow-recap-lbl"><Ic n="sparkle" s={12} />Secciones de la invitación · <span style={{ textTransform: 'none', letterSpacing: 0, color: 'var(--text-2)' }}>Idioma: {langName}</span></div>
        <div className="ow-recap-chips">
          {activeSecs.map((s) => {
            const pend = secPending(s.id);
            return <span className={`ow-recap-chip ${pend ? 'pending' : ''}`} key={s.id}><Ic n={pend ? 'clock' : s.icon} s={11} />{s.name}</span>;
          })}
        </div>
        {pendingCount > 0 &&
        <div className="ow-recap-pending"><Ic n="info" s={13} />{pendingCount} {pendingCount === 1 ? 'sección encendida' : 'secciones encendidas'} con datos pendientes — los podrás completar o actualizar después en tu panel. No bloquea tu compra.</div>
        }
      </div>
      <div className="ow-summary">
        {activePkg &&
        <div className="ow-line">
            <div className="ow-line-ico gold"><Ic n={activePkg.icon} s={17} /></div>
            <div className="ow-line-body">
              <div className="ow-line-name">Paquete {activePkg.name}</div>
              <div className="ow-line-meta">{lines.length} {lines.length === 1 ? 'servicio incluido' : 'servicios incluidos'}</div>
            </div>
            <div className="ow-line-price">{USD(activePkg.price)}</div>
          </div>
        }
        {lines.map((s) => {
          const free = totals.freeIds.has(s.id);
          if (activePkg) {
            return (
              <div className="ow-line" key={s.id}>
                <div className={`ow-line-ico ${s.base ? 'gold' : ''}`}><Ic n={s.icon} s={17} /></div>
                <div className="ow-line-body">
                  <div className="ow-line-name">{s.name}</div>
                  <div className="ow-line-meta">{s.short}</div>
                </div>
                <div className="ow-line-price free">Incluido</div>
              </div>);

          }
          return (
            <div className="ow-line" key={s.id}>
              <div className={`ow-line-ico ${s.base ? 'gold' : ''}`}><Ic n={s.icon} s={17} /></div>
              <div className="ow-line-body">
                <div className="ow-line-name">{s.name}</div>
                <div className="ow-line-meta">{s.base ? 'Invitación base · incluida' : free ? 'Servicio · gratis por promoción' : s.short}</div>
              </div>
              <div className={`ow-line-price ${free ? 'free' : ''}`}>{free ? 'GRATIS' : USD(s.price)}</div>
            </div>);

        })}
        <div className="ow-totals">
          {!activePkg && <div className="ow-total-row"><span>Subtotal</span><span className="num">{USD(totals.gross)}</span></div>}
          {!activePkg && totals.discount > 0 && <div className="ow-total-row" style={{ color: 'var(--success-2)' }}><span>Descuento promociones</span><span className="num">− {USD(totals.discount)}</span></div>}
          {data.lang === 'bi' && <div className="ow-total-row"><span>Bilingüe EN/ES</span><span className="num">Incluido</span></div>}
          <div className="ow-total-row grand"><span>Total</span><span className="num">{USD(orderTotal)} <span style={{ fontSize: 12, color: 'var(--text-3)', fontWeight: 500 }}>USD</span></span></div>
          {activePkg && pkgListValue(activePkg) - activePkg.price > 0 &&
          <div className="ow-total-row" style={{ color: 'var(--success-2)', fontWeight: 700, marginTop: 8 }}><span>Ahorras vs. precio de lista</span><span className="num">{USD(pkgListValue(activePkg) - activePkg.price)}</span></div>
          }
        </div>
      </div>
      <button className="ow-edit-link" onClick={() => goTo(0)}><Ic n="edit" s={13} />Editar servicios</button>
    </div>);

}

function StepPay({ data, set, totals, orderTotal }) {
  const detail = data.payMethod === 'zelle' || data.payMethod === 'venmo' ? PAY[data.payMethod] : null;
  const isCard = data.payMethod === 'card';
  const charge = cardCharge(orderTotal);
  const fee = Math.round((charge - orderTotal) * 100) / 100;
  const depositDue = orderTotal > 300 ? 50 : orderTotal;
  const balanceDue = Math.max(0, orderTotal - depositDue);
  // Comprobante como data URL en el borrador (sin backend), con preview.
  const onReceipt = (e) => {
    const f = e.target.files && e.target.files[0];
    if (!f) return;
    fileToDataURL(f, (url) => set({ receiptName: f.name, receiptData: url }));
    e.target.value = '';
  };
  return (
    <div className="ow-step">
      <h1 className="ow-step-title">Confirma tu <em>orden</em></h1>
      <p className="ow-step-sub">Elige tu método de pago para completar la orden. Nuestro equipo preparará tu invitación en 1 a 3 días hábiles.</p>

      <div className="ow-pay-note">
        <Ic n="check" s={18} />
        <div className="ow-pay-note-txt"><strong>Sesión iniciada{data.cEmail ? <> como {data.cEmail}</> : ''}.</strong> Tu progreso está guardado — vuelve a tu panel de RSVP, invitados y álbum QR cuando quieras.</div>
      </div>

      <p className="ow-subhead"><Ic n="shield" s={15} />Elige tu método de pago</p>
      <div className="ow-pay-methods col">
        <button className={`ow-pm featured ${isCard ? 'on' : ''}`} onClick={() => set({ payMethod: 'card' })}>
          <div className="ow-pm-logo card"><Ic n="card" s={18} /></div>
          <div className="ow-pm-main"><div className="ow-pm-name">Tarjeta de crédito o débito{STRIPE_ACTIVE && <span className="ow-pm-reco">Recomendado</span>}</div><div className="ow-pm-sub ok">Pago en línea seguro · confirmación inmediata</div></div>
          <span className="ow-pm-check"><Ic n="check" s={13} /></span>
        </button>
        <div className="ow-pm-pair">
          <button className={`ow-pm ${data.payMethod === 'zelle' ? 'on' : ''}`} onClick={() => set({ payMethod: 'zelle' })}>
            <div className="ow-pm-logo zelle">Z</div>
            <div className="ow-pm-main"><div className="ow-pm-name">Zelle{!STRIPE_ACTIVE && <span className="ow-pm-reco">Recomendado</span>}</div><div className="ow-pm-sub ok">Sin comisión ni impuestos · confirmación después de verificar tu pago</div></div>
          </button>
          <button className={`ow-pm ${data.payMethod === 'venmo' ? 'on' : ''}`} onClick={() => set({ payMethod: 'venmo' })}>
            <div className="ow-pm-logo venmo">V</div>
            <div className="ow-pm-main"><div className="ow-pm-name">Venmo</div><div className="ow-pm-sub">Sin comisión · confirmación después de verificar tu pago</div></div>
          </button>
        </div>
      </div>

      {isCard &&
      <>
          <div className="ow-pay-detail">
            <div className="ow-pay-detail-row"><span className="ow-pay-detail-k">Subtotal</span><span className="ow-pay-detail-v">{USD2(orderTotal)}</span></div>
            <div className="ow-pay-detail-row"><span className="ow-pay-detail-k">Comisión de procesamiento</span><span className="ow-pay-detail-v">{USD2(fee)}</span></div>
            <div className="ow-pay-detail-row"><span className="ow-pay-detail-k">Total con tarjeta</span><span className="ow-pay-detail-v big">{USD2(charge)} USD</span></div>
          </div>
          <div className="ow-pay-feenote"><Ic n="info" s={13} />Las transferencias (Zelle/Venmo) no tienen comisión y mantienen el total en {USD2(orderTotal)}.</div>
          <div className="ow-stripe-note"><Ic n="lock" s={14} />Te llevamos al checkout seguro de <strong>Stripe</strong> para completar tu pago. No guardamos los datos de tu tarjeta.</div>
          <a className="ow-wa-btn secondary" href={waLink(`¡Hola! Ya pagué mi Orden #${data.orderId} con tarjeta. Quedo al pendiente de los siguientes pasos.`)} target="_blank" rel="noopener noreferrer">
            <Ic n="message" s={16} />Escríbenos por WhatsApp
          </a>
        </>
      }

      {detail &&
      <>
          <div className="ow-pay-detail">
            <div className="ow-pay-detail-row">
              <span className="ow-pay-detail-k">Envía a ({data.payMethod === 'zelle' ? 'Zelle' : 'Venmo'})</span>
              <span className="ow-pay-detail-v">{detail.handle}<CopyChip text={detail.handle} /></span>
            </div>
            {data.payMethod === 'zelle' && detail.titular &&
            <div className="ow-pay-detail-row">
              <span className="ow-pay-detail-k">Titular Zelle</span>
              <span className="ow-pay-detail-v">{detail.titular}</span>
            </div>}
            <div className="ow-pay-detail-row">
              <span className="ow-pay-detail-k">{balanceDue > 0 ? 'Anticipo a enviar' : 'Monto exacto'}</span>
              <span className="ow-pay-detail-v big">{USD(depositDue)} USD</span>
            </div>
            {balanceDue > 0 &&
            <div className="ow-pay-detail-row">
              <span className="ow-pay-detail-k">Saldo pendiente</span>
              <span className="ow-pay-detail-v">{USD(balanceDue)} USD</span>
            </div>
            }
            <div className="ow-pay-detail-row">
              <span className="ow-pay-detail-k">Concepto / referencia</span>
              <span className="ow-pay-detail-v">Orden #{data.orderId}<CopyChip text={`Orden #${data.orderId}`} /></span>
            </div>
          </div>

          <div className="ow-grid ow-grid-2">
            <Field label="Nombre del titular" full><input className="ow-input" placeholder="¿A nombre de quién va la orden?" value={data.cName} onChange={(e) => set({ cName: e.target.value })} /></Field>
            <Field label="WhatsApp" full hint="Te escribiremos por WhatsApp para confirmar tu pago. Incluye el código de país (ej. +52, +1)."><input className="ow-input" type="tel" placeholder="+1 305 123 4567" value={data.cPhone} onChange={(e) => set({ cPhone: e.target.value })} /></Field>
          </div>

          <div className="ow-separator-lbl"><span>Sube tu comprobante</span></div>
          {data.receiptData ? (
          <div className="ow-receipt done">
            {data.receiptData.startsWith('data:image')
              ? <img src={data.receiptData} alt="Comprobante" className="ow-receipt-thumb" />
              : <div className="ow-receipt-ico done"><Ic n="check" s={18} /></div>}
            <div className="ow-receipt-body">
              <div className="ow-receipt-t">Comprobante adjunto</div>
              <div className="ow-receipt-d">{data.receiptName}</div>
            </div>
            <div className="ow-receipt-actions">
              <label className="ow-receipt-act">
                <input type="file" accept="image/*,application/pdf" style={{ display: 'none' }} onChange={onReceipt} />
                Reemplazar
              </label>
              <button type="button" className="ow-receipt-act danger" onClick={() => set({ receiptName: '', receiptData: '' })}>Quitar</button>
            </div>
          </div>
          ) : (
          <label className="ow-receipt">
            <input type="file" accept="image/*,application/pdf" style={{ display: 'none' }} onChange={onReceipt} />
            <div className="ow-receipt-ico"><Ic n="image" s={18} /></div>
            <div className="ow-receipt-body"><div className="ow-receipt-t">Adjunta tu comprobante de Zelle o Venmo</div><div className="ow-receipt-d">Opcional · ayuda a verificar tu pago más rápido</div></div>
          </label>
          )}

          <a className="ow-wa-btn" href={waLink(balanceDue > 0 ? `¡Hola! Acabo de enviar el anticipo de mi Orden #${data.orderId} por ${USD(depositDue)} USD vía ${data.payMethod === 'venmo' ? 'Venmo' : 'Zelle'}. El total de la orden es ${USD(orderTotal)} USD y queda un saldo pendiente de ${USD(balanceDue)} USD. Aquí les envío mi comprobante.` : `¡Hola! Acabo de enviar el pago completo de mi Orden #${data.orderId} por ${USD(orderTotal)} USD vía ${data.payMethod === 'venmo' ? 'Venmo' : 'Zelle'}. Aquí les envío mi comprobante.`)} target="_blank" rel="noopener noreferrer">
            <Ic n="message" s={17} />Enviar comprobante por WhatsApp
          </a>

          <div className="ow-wa-note">
            <Ic n="message" s={18} />
            <div className="ow-wa-note-txt"><strong>Verificaremos tu pago y te confirmaremos por WhatsApp.</strong> Después, nuestro equipo comenzará a preparar tu invitación.</div>
          </div>
        </>
      }
    </div>);

}

function Paid({ data, totals, orderTotal }) {
  const isCard = data.payMethod === 'card';
  const paidAmount = isCard ? cardCharge(orderTotal) : orderTotal;
  const depositDue = orderTotal > 300 ? 50 : orderTotal;
  const balanceDue = Math.max(0, orderTotal - depositDue);
  const payDetail = (data.payMethod === 'zelle' || data.payMethod === 'venmo') ? PAY[data.payMethod] : null;
  const payLabel = data.payMethod === 'venmo' ? 'Venmo' : 'Zelle';
  const missing = [
  { ok: !dateIsPending(data), label: 'Fecha exacta del evento' },
  { ok: !!(data.venue && data.recepAddr), label: 'Lugar y dirección de recepción' },
  { ok: !!(data.mom || data.dad || data.grandparents || data.padrinos), label: 'Nombres de papás / abuelitos / padrinos' },
  { ok: !data.sections.food_menu || !!(data.foodServiceType && data.foodMenuText), label: 'Comida / menú' },
  { ok: !data.sections.dress_code || !!data.dressCode, label: 'Código de vestimenta' },
  { ok: !data.sections.corte || !!(data.corteNames || (Array.isArray(data.courtRows) && data.courtRows.some((r) => r.name))), label: 'Corte de honor' }];

  const pend = missing.filter((m) => !m.ok);
  return (
    <div className="ow-step" style={{ textAlign: 'center', maxWidth: 470, margin: '20px auto 0' }}>
      <div className="ow-acct-avatar" style={{ margin: '0 auto 18px', width: 64, height: 64, fontSize: 28 }}><Ic n={isCard ? 'check' : 'clock'} s={30} /></div>
      <h1 className="ow-step-title" style={{ fontSize: 30 }}>{isCard ? <>¡Pago <em>confirmado</em>!</> : <>¡Orden <em>recibida</em>!</>}</h1>
      <div className="ow-folio-chip">Orden #{data.orderId}</div>
      <p className="ow-step-sub" style={{ margin: '10px auto 18px' }}>
        {isCard ?
        <>Gracias{data.cName ? `, ${data.cName.split(' ')[0]}` : ''}. Tu pago de <strong style={{ color: 'var(--gold-2)' }}>{USD2(paidAmount)} USD</strong> con tarjeta quedó confirmado. Nuestro equipo comenzará a preparar tu invitación y te la enviamos por WhatsApp{data.cPhone ? <> al {data.cPhone}</> : ''} en <strong style={{ color: 'var(--text)' }}>1 a 3 días hábiles</strong>.</> :
        <>Gracias{data.cName ? `, ${data.cName.split(' ')[0]}` : ''}. Registramos tu orden con <strong style={{ color: 'var(--text)' }}>{payLabel}</strong> como método de pago. Sigue los pasos de abajo para que tu orden avance.</>}
      </p>
      {!isCard && payDetail &&
      <div className="ow-pay-detail" style={{ textAlign: 'left', marginBottom: 16 }}>
        <div className="ow-pay-detail-row" style={{ alignItems: 'flex-start' }}>
          <span className="ow-pay-detail-k">{balanceDue > 0 ? 'Anticipo a enviar' : 'Monto a enviar'}</span>
          <span className="ow-pay-detail-v"><strong style={{ color: 'var(--gold-2)', fontSize: 17 }}>{USD2(depositDue)} USD</strong></span>
        </div>
        {balanceDue > 0 &&
        <div className="ow-pay-detail-row" style={{ alignItems: 'flex-start' }}>
          <span className="ow-pay-detail-k">Saldo pendiente</span>
          <span className="ow-pay-detail-v"><strong style={{ color: 'var(--text)', fontSize: 15 }}>{USD2(balanceDue)} USD</strong></span>
        </div>
        }
        <div className="ow-pay-detail-row">
          <span className="ow-pay-detail-k">Envía a ({payLabel})</span>
          <span className="ow-pay-detail-v">{payDetail.handle}<CopyChip text={payDetail.handle} /></span>
        </div>
        {data.payMethod === 'zelle' && payDetail.titular &&
        <div className="ow-pay-detail-row">
          <span className="ow-pay-detail-k">Titular Zelle</span>
          <span className="ow-pay-detail-v">{payDetail.titular}</span>
        </div>}
        <div className="ow-wa-note" style={{ textAlign: 'left', marginTop: 12 }}>
          <Ic n="info" s={18} />
          <div className="ow-wa-note-txt"><strong>¿Ya enviaste tu comprobante?</strong> Si todavía no, envía <strong style={{ color: 'var(--gold-2)' }}>{USD2(depositDue)} USD</strong> por {payLabel}{balanceDue > 0 ? <> como anticipo. El saldo pendiente será <strong style={{ color: 'var(--text)' }}>{USD2(balanceDue)} USD</strong>.</> : <>.</>} Tu orden avanza cuando verifiquemos el pago.</div>
        </div>
      </div>}

      <div className="ow-paid-cta" style={{ cursor: 'default' }}>
        <span className="ow-paid-cta-main"><Ic n="check" s={18} />Tu formulario quedó guardado en esta orden</span>
        <span className="ow-paid-cta-sub">{pend.length > 0 ? `Revisa los datos pendientes aquí antes de producción` : 'Tenemos la información principal para iniciar producción'} <Ic n="check" s={16} /></span>
      </div>

      {!isCard &&
      <div className="ow-checklist" style={{ textAlign: 'left', marginBottom: 16 }}>
        <div className="ow-checklist-head"><Ic n="info" s={15} /><span>Próximos pasos</span></div>
        <ol className="ow-next-steps" style={{ margin: 0, padding: '4px 0 0', listStyle: 'none', counterReset: 'ows' }}>
          {[`Envía tu pago por ${payLabel} si todavía no lo hiciste`,
            'Nuestro equipo revisa la información guardada en esta orden',
            'Verificamos tu pago y te confirmamos por WhatsApp',
            'Preparamos y entregamos tu invitación en 1 a 3 días'].map((t, i) =>
          <li key={i} style={{ display: 'flex', alignItems: 'flex-start', gap: 10, padding: '6px 0' }}>
            <span style={{ flex: '0 0 auto', width: 22, height: 22, borderRadius: '50%', background: 'var(--gold-soft)', color: 'var(--gold-2)', border: '1px solid var(--gold-line)', display: 'grid', placeItems: 'center', fontSize: 12, fontWeight: 700 }}>{i + 1}</span>
            <span style={{ fontSize: 13.5, lineHeight: 1.4, color: 'var(--text-2)', paddingTop: 2 }}>{t}</span>
          </li>)}
        </ol>
      </div>}
      {isCard &&
      <div className="ow-wa-note" style={{ textAlign: 'left' }}>
        <Ic n="message" s={18} />
        <div className="ow-wa-note-txt">Recibirás el acceso a tu panel {data.cEmail ? <>en <strong>{data.cEmail}</strong></> : ''} para ver el avance, tus RSVP y tu álbum QR.</div>
      </div>}
      {pend.length > 0 &&
      <div className="ow-checklist">
          <div className="ow-checklist-head"><Ic n="info" s={15} /><span>{isCard ? <>Datos que faltan para que quede perfecta</> : <>Datos pendientes de tu invitación</>}</span></div>
          <div className="ow-checklist-items">
            {missing.map((m) =>
          <div className={`ow-checklist-item ${m.ok ? 'ok' : ''}`} key={m.label}>
                <span className="ow-checklist-mark"><Ic n={m.ok ? 'check' : 'plus'} s={12} /></span>{m.label}
              </div>
          )}
          </div>
        </div>
      }
    </div>);

}

/* ============================================================
   APP
   ============================================================ */
const DRAFT_KEY = 'vipinv_wizard_draft';
// TOS-ORD-013: id de sesion por pestana (sessionStorage). El draft se restaura SOLO si su sid
// coincide con el de esta sesion -> una visita/persona distinta en el mismo navegador no
// autocompleta el borrador de otra. Refresh de la misma pestana si resume (sessionStorage persiste).
function wizSid() {
  try {
    let s = sessionStorage.getItem('vipinv_wizard_sid');
    if (!s) { s = Date.now().toString(36) + Math.random().toString(36).slice(2); sessionStorage.setItem('vipinv_wizard_sid', s); }
    return s;
  } catch (e) { return ''; }
}
const DEFAULT_SECTIONS = SECTIONS.reduce((acc, sec) => ({ ...acc, [sec.id]: false }), {});
const DEFAULT_DATA = {
  services: new Set(),
  honoree: '', date: '', eventMonth: '', eventDay: '', eventYear: '', time: '20:00', dateMode: 'exact', dateMY: '', eventMYMonth: '', eventMYYear: '', venue: '', city: '', mom: '', dad: '', grandparents: '', padrinos: '',
  theme: '', style: '', themeSelected: false, styleSelected: false, customThemeName: '', customThemeDescription: '', themeCoverUrl: '', themePreviewUrl: '', themeColors: ['', '', ''], orderId: genFolio(),
  sections: DEFAULT_SECTIONS,
  endTime: '', recepAddr: '', recepTime: '', recepMaps: '', ceremonyVenue: '', ceremonyTime: '', ceremonyAddress: '', ceremonyMaps: '', foodServiceType: '', foodMenuText: '', foodAccommodationNotes: '', foodServingTime: '', cocktailTime: '', familyNotes: '', hotelName: '', hotelAddress: '', hotelMaps: '', hotelRateCode: '', parkingNotes: '', shuttleInfo: '', includeAfterParty: '', afterPartyVenue: '', afterPartyAddress: '', afterPartyMaps: '', afterPartyTime: '', itineraryRows: [{ time: '', activity: '' }], registry: '', godparentRows: [{ role: 'Honor', customRole: '', names: '' }], courtRows: [{ role: 'Dama', name: '' }], corteNames: '', dressCode: '', welcome: '', rsvpDeadline: '', galleryPhotoCount: '', galleryMethod: '', galleryDriveLink: '', musicText: '', musicUrl: '', qrAlbumName: '', qrAlbumStyle: '', qrAlbumNotes: '', guestbookPrompt: '', thanksMessage: '', lang: 'es',
  cName: '', cEmail: '', cPass: '', cPhone: '', receiptName: '', receiptData: '',
  auth: false, authMethod: null, payMethod: null
};
function loadDraft() {
  try {
    const raw = localStorage.getItem(DRAFT_KEY);
    if (!raw) return null;
    const p = JSON.parse(raw);
    if (!p || !p.data) return null;
    if (p.sid && p.sid !== wizSid()) return null; // TOS-ORD-013: borrador de otra sesion/persona -> no autocompletar
    const merged = { ...DEFAULT_DATA, ...p.data, sections: { ...DEFAULT_DATA.sections, ...(p.data.sections || {}) } };
    merged.services = new Set(Array.isArray(p.data.services) ? p.data.services : []);
    return { step: typeof p.step === 'number' ? p.step : 0, data: merged };
  } catch (e) {return null;}
}
function normalizeWizardData(raw) {
  const source = raw && typeof raw === 'object' ? raw : {};
  const merged = { ...DEFAULT_DATA, ...source, sections: { ...DEFAULT_DATA.sections, ...(source.sections || {}) } };
  merged.services = new Set(Array.isArray(source.services) ? source.services : []);
  if (!source.themeSelected && source.theme === 'esmeralda' && (!source.style || source.style === 'clasico')) merged.theme = '';
  if (!source.styleSelected && source.style === 'clasico') merged.style = '';
  merged.cPass = '';
  merged.receiptData = '';
  return merged;
}
function wizardSnapshot(data) {
  const copy = { ...data, services: Array.from(data.services), cPass: '', receiptData: '' };
  return copy;
}
function chosenTheme(data) {
  return data.themeSelected && data.theme ? data.theme : '';
}
function chosenStyle(data) {
  return data.styleSelected && data.style ? data.style : '';
}
function themeOpsEdits(data) {
  // Funcion imperativa (llamada desde saveStep, no un componente) -- no puede usar el
  // hook useThemeCatalog(). Lee el cache compartido directamente; si aun no cargo
  // (guardado disparado muy temprano), itera vacio -- no pierde nada, theme_edits
  // solo importa para temas YA cargados/visibles cuando el operador los edito.
  const out = {};
  themeCatalogCache.data.forEach((t) => {
    const prefix = `themeEdit_${t.id}`;
    const title = data[`${prefix}_customThemeName`] || '';
    const description = data[`${prefix}_customThemeDescription`] || '';
    const cover = data[`${prefix}_themeCoverUrl`] || '';
    const previewUrl = data[`${prefix}_themePreviewUrl`] || '';
    const colors = Array.isArray(data[`${prefix}_themeColors`]) ? data[`${prefix}_themeColors`].filter(Boolean) : [];
    if (title || description || cover || previewUrl || colors.length) {
      out[t.id] = { title, description, cover_url: cover, preview_url: previewUrl, colors };
    }
  });
  return out;
}
function draftFromBrief(brief) {
  const data = brief && brief.data || {};
  if (data._wizard && data._wizard.data) {
    return {
      step: typeof data._wizard.step === 'number' ? data._wizard.step : Math.max(0, Math.min((brief.current_section || 1) - 1, STEPS.length - 1)),
      data: normalizeWizardData(data._wizard.data)
    };
  }
  const s1 = data.section_1 || {};
  const s2 = data.section_2 || {};
  const themeEdits = s2.theme_edits && typeof s2.theme_edits === 'object' ? s2.theme_edits : {};
  const restoredThemeEdits = {};
  Object.keys(themeEdits).forEach((id) => {
    const edit = themeEdits[id] || {};
    const prefix = `themeEdit_${id}`;
    restoredThemeEdits[`${prefix}_customThemeName`] = edit.title || '';
    restoredThemeEdits[`${prefix}_customThemeDescription`] = edit.description || '';
    restoredThemeEdits[`${prefix}_themeCoverUrl`] = edit.cover_url || '';
    restoredThemeEdits[`${prefix}_themePreviewUrl`] = edit.preview_url || '';
    restoredThemeEdits[`${prefix}_themeColors`] = Array.isArray(edit.colors) ? edit.colors : [];
  });
  const s3 = data.section_3 || {};
  const s4 = data.section_4 || {};
  const fallback = normalizeWizardData({
    services: Array.isArray(s4.services) ? s4.services : [],
    honoree: s1.honoree_full_name || '',
    dateMode: s1.date_mode || 'exact',
    date: s1.event_date || '',
    time: s1.event_time || '20:00',
    endTime: s1.event_end_time || '',
    venue: s1.venue || '',
    city: s1.city || '',
    mom: s1.mom || '',
    dad: s1.dad || '',
    grandparents: s1.grandparents || '',
    padrinos: s1.padrinos || '',
    theme: s2.theme || '',
    style: s2.style || '',
    customThemeName: s2.custom_theme_name || '',
    customThemeDescription: s2.theme_description || '',
    themeCoverUrl: s2.cover_url || '',
    themePreviewUrl: s2.preview_url || '',
    themeColors: Array.isArray(s2.theme_colors) ? s2.theme_colors : DEFAULT_DATA.themeColors,
    ...restoredThemeEdits,
    themeSelected: !!s2.theme,
    styleSelected: !!s2.style,
    sections: s3.sections || DEFAULT_DATA.sections,
    lang: s4.lang || data.section_15 && data.section_15.language || DEFAULT_DATA.lang
  });
  if (s1.event_date && /^\d{4}-\d{2}-\d{2}$/.test(s1.event_date)) {
    [fallback.eventYear, fallback.eventMonth, fallback.eventDay] = s1.event_date.split('-');
  }
  if (s1.event_month_year && /^\d{4}-\d{2}$/.test(s1.event_month_year)) {
    fallback.dateMode = 'monthyear';
    [fallback.eventMYYear, fallback.eventMYMonth] = s1.event_month_year.split('-');
  }
  return { step: Math.max(0, Math.min((brief.current_section || 1) - 1, STEPS.length - 1)), data: fallback };
}

function App() {
  const draft = useMemo(() => loadDraft(), []);
  const [step, setStep] = useState(draft ? draft.step : 0);
  const [paid, setPaid] = useState(false);
  const [submitting, setSubmitting] = useState(false);
  const [submitError, setSubmitError] = useState('');
  const submittingRef = useRef(false);
  const [showPrev, setShowPrev] = useState(false);
  const [focus, setFocus] = useState('historia');
  const [saved, setSaved] = useState(false);
  const [savedExit, setSavedExit] = useState(false); // RMR-685: confirmacion "Guardar y continuar despues"
  const [data, setData] = useState(draft ? draft.data : DEFAULT_DATA);
  const set = (patch) => setData((d) => ({ ...d, ...patch }));
  const toggleSec = (id) => setData((d) => ({ ...d, sections: { ...d.sections, [id]: !d.sections[id] } }));
  const toggleSvc = (id) => setData((d) => {const s = new Set(d.services);s.has(id) ? s.delete(id) : s.add(id);return { ...d, services: s };});
  const restoreServerBrief = React.useCallback((brief) => {
    if (!brief || !brief.data) return false;
    const restored = draftFromBrief(brief);
    if (brief.token) window.OW_GUEST_TOKEN = brief.token;
    setStep(restored.step);
    setData((current) => ({ ...restored.data, auth: true, authMethod: 'email', cEmail: current.cEmail || restored.data.cEmail || '', cName: current.cName || restored.data.cName || '' }));
    try {
      localStorage.setItem(DRAFT_KEY, JSON.stringify({ step: restored.step, sid: wizSid(), data: wizardSnapshot(restored.data) }));
    } catch (e) {}
    return true;
  }, []);
  const loadAccountDraft = React.useCallback(async () => {
    try {
      const res = await fetch('/orden/api.php?action=account_active_brief', { credentials: 'same-origin', cache: 'no-store' });
      let j = {}; try { j = await res.json(); } catch (e) {}
      if (res.ok && j.success && j.brief) return restoreServerBrief(j.brief);
    } catch (e) {}
    return false;
  }, [restoreServerBrief]);

  React.useEffect(() => {
    let alive = true;
    (async () => {
      try {
        const res = await fetch('/orden/api.php?action=account_me', { credentials: 'same-origin', cache: 'no-store' });
        let j = {}; try { j = await res.json(); } catch (e) {}
        if (!alive || !res.ok || !j.logged_in || !j.user) return;
        setData((d) => ({ ...d, auth: true, authMethod: 'email', cEmail: d.cEmail || j.user.email || '', cName: d.cName || j.user.name || '' }));
        await loadAccountDraft();
      } catch (e) {}
    })();
    return () => { alive = false; };
  }, [loadAccountDraft]);

  // auto-guardado en localStorage (solo este navegador)
  const mounted = useRef(false);
  useEffect(() => {
    try {
      localStorage.setItem(DRAFT_KEY, JSON.stringify({ step, sid: wizSid(), data: { ...data, services: [...data.services] } }));
    } catch (e) {}
    if (mounted.current) {
      setSaved(true);
      const t = setTimeout(() => setSaved(false), 1900);
      return () => clearTimeout(t);
    }
    mounted.current = true;
  }, [data, step]);

  const effectiveServices = useMemo(() => effectiveServicesForTheme(data), [data.services, data.theme, data.themeSelected]);
  const order = useMemo(() => orderState(effectiveServices), [effectiveServices]);
  const totals = order;
  const activePkg = order.pkg;
  const packageId = effectivePackageId(data, activePkg);
  const orderTotal = order.total + bilingualFee(data.lang);

  const last = STEPS.length - 1;
  const phoneOk = data.cPhone.replace(/\D/g, '').length >= 8;
  const payChargeNow = data.payMethod === 'card' ? cardCharge(orderTotal) : orderTotal;
  const canNext =
  step === 0 ? !!data.auth /* RMR-685 (JP, decision de producto) SUPERSEDE RMR-475/WIZARD-017: cuenta OBLIGATORIA en Paso 1. JP la pidio explicito: cliente serio crea su cuenta de entrada -> sus datos viven en BD ligados a su cuenta DESDE el inicio (cross-device, cero perdida, sin mala experiencia). El AccountGate se renderiza EN este mismo paso (StepServices) y crea cuenta REAL via account_register/login, reintentable ante fallo (409/429/red) -> NUNCA queda dead-end. NO quitar este gate sin orden explicita de JP (no es el bug RMR-635; es la decision opuesta y deliberada). */ :
  step === 1 ? !!data.honoree.trim() :
  step === last ? data.payMethod === 'card' ? true : !!(data.payMethod && data.cName.trim() && phoneOk) :
  true;
  const owBriefSave = (section, payload, recoverStep = step) => {
    // RMR-685: autosave SERVER-SIDE por paso. La cuenta se crea en Paso 1 -> el usuario queda logueado,
    // asi brief_save (rama guest por token) sincroniza a vipinv_brief.user_id en BD (cross-device).
    // Fire-and-forget + try/catch: NUNCA bloquea el avance (anti dead-end). localStorage queda de red de seguridad.
    try {
      const tok = window.OW_GUEST_TOKEN;
      if (!tok || !window.OW_CSRF) return; // token aun no sembrado por el server -> solo localStorage por ahora
      fetch('/orden/api.php?action=brief_save', {
        method: 'POST', headers: { 'Content-Type': 'application/json' }, credentials: 'same-origin',
        body: JSON.stringify({ token: tok, section, data: payload, csrf_token: window.OW_CSRF, package_type: packageId, total_sections: STEPS.length, wizard: { step: recoverStep, total: STEPS.length, data: wizardSnapshot(data) } }),
      }).catch(() => {});
    } catch (e) {}
  };
  const saveStep = (s, recoverStep = s) => {
    // mapeo paso->seccion del brief que el backend reconoce (sec1=festejada, sec2=tema, sec3=secciones, sec4=servicios)
    if (s === 0) owBriefSave(4, { services: Array.from(data.services), package_id: packageId, lang: data.lang }, recoverStep);
    else if (s === 1) {
      const exactDate = data.dateMode === 'exact' && data.eventYear && data.eventMonth && data.eventDay ? `${data.eventYear}-${data.eventMonth}-${data.eventDay}` : '';
      const monthYear = data.dateMode === 'monthyear' && data.eventMYYear && data.eventMYMonth ? `${data.eventMYYear}-${data.eventMYMonth}` : '';
      owBriefSave(1, { honoree_full_name: data.honoree, date_mode: data.dateMode, event_date: exactDate, event_month_year: monthYear, event_time: data.time, event_end_time: data.endTime, venue: data.venue, city: data.city, mom: data.mom, dad: data.dad, grandparents: data.grandparents, padrinos: data.padrinos }, recoverStep);
    }
    else if (s === 2) {
      const fieldValue = (label, fallback = '') => {
        const fields = Array.from(document.querySelectorAll('.ow-field'));
        const found = fields.find((f) => (f.innerText || '').toLowerCase().includes(label.toLowerCase()));
        const el = found && found.querySelector('input, textarea, select');
        return el ? el.value : fallback;
      };
      owBriefSave(3, {
        sections: data.sections,
        mom: data.mom,
        dad: data.dad,
        grandparents: data.grandparents,
        padrinos: (Array.isArray(data.godparentRows) ? data.godparentRows : []).filter((r) => r.names).map((r) => `${r.role === 'Otro' && r.customRole ? r.customRole : r.role || 'Otro'}: ${r.names}`).join('\n') || data.padrinos,
        mother_name: data.mom,
        father_name: data.dad,
        family_other_notes: fieldValue('Notas de familia', data.familyNotes),
        godparents: (Array.isArray(data.godparentRows) ? data.godparentRows : []).filter((r) => r.names).map((r) => ({ role: r.role || 'Otro', custom_role: r.role === 'Otro' ? r.customRole || '' : '', names: r.names })),
        recep_addr: fieldValue('Dirección del salón', data.recepAddr),
        recep_time: fieldValue('Hora de recepción', data.recepTime),
        recep_maps: fieldValue('Enlace de Google Maps', data.recepMaps),
        ceremony_venue: fieldValue('Lugar de ceremonia / iglesia', data.ceremonyVenue),
        ceremony_time: fieldValue('Hora de ceremonia', data.ceremonyTime),
        ceremony_address: fieldValue('Dirección de ceremonia', data.ceremonyAddress),
        ceremony_maps: fieldValue('Mapa de ceremonia', data.ceremonyMaps),
        food_service_type: fieldValue('Tipo de servicio', data.foodServiceType),
        food_menu_text: fieldValue('Menú u opciones principales', data.foodMenuText),
        food_accommodation_notes: fieldValue('Notas dietéticas o acomodos', data.foodAccommodationNotes),
        food_serving_time: fieldValue('Hora de cena / servicio', data.foodServingTime),
        cocktail_time: fieldValue('Hora de cocktail / aperitivos', data.cocktailTime),
        hotel_name: fieldValue('Hotel recomendado', data.hotelName),
        hotel_address: fieldValue('Dirección del hotel', data.hotelAddress),
        hotel_maps: fieldValue('Mapa del hotel', data.hotelMaps),
        hotel_rate_code: fieldValue('Código / tarifa de grupo', data.hotelRateCode),
        parking_notes: fieldValue('Parking', data.parkingNotes),
        shuttle_info: fieldValue('Shuttle / transporte', data.shuttleInfo),
        include_after_party: fieldValue('Incluir After Party', data.includeAfterParty),
        after_party_venue: fieldValue('Lugar / nombre del After Party', data.afterPartyVenue),
        after_party_address: fieldValue('Dirección del After Party', data.afterPartyAddress),
        after_party_maps: fieldValue('Mapa del After Party', data.afterPartyMaps),
        after_party_time: fieldValue('Hora de inicio del After Party', data.afterPartyTime),
        itinerary_items: (Array.isArray(data.itineraryRows) ? data.itineraryRows : []).filter((r) => r.time || r.activity),
        itinerary_text: (Array.isArray(data.itineraryRows) ? data.itineraryRows : []).filter((r) => r.time || r.activity).map((r) => `${r.time || 'Sin hora'} - ${r.activity || ''}`.trim()).join('\n'),
        registry: fieldValue('Datos o links de tu registro', data.registry),
        court_of_honor: (Array.isArray(data.courtRows) ? data.courtRows : []).filter((r) => r.name).map((r) => ({ role: r.role || 'Otro', name: r.name })),
        corte_names: (Array.isArray(data.courtRows) ? data.courtRows : []).filter((r) => r.name).map((r) => `${r.role || 'Otro'}: ${r.name}`).join('\n') || data.corteNames,
        dress_code: fieldValue('Código de vestimenta', data.dressCode),
        rsvp_deadline: fieldValue('Fecha límite de confirmación', data.rsvpDeadline),
        gallery_photo_count: fieldValue('Cantidad de fotos', data.galleryPhotoCount),
        gallery_method: fieldValue('Cómo enviarás las fotos', data.galleryMethod),
        gallery_drive_link: fieldValue('Link Drive de fotos', data.galleryDriveLink),
        music_text: fieldValue('Canción y artista', data.musicText),
        music_url: fieldValue('Link de Spotify / YouTube', data.musicUrl),
        qr_album_name: fieldValue('Nombre del álbum QR', data.qrAlbumName),
        qr_album_style: fieldValue('Estilo visual del QR', data.qrAlbumStyle),
        qr_album_notes: fieldValue('Notas para los letreros QR', data.qrAlbumNotes),
        guestbook_prompt: fieldValue('Mensaje para el libro de visitas', data.guestbookPrompt),
        thanks_message: fieldValue('Mensaje de agradecimiento', data.thanksMessage),
        welcome: fieldValue('Frase de bienvenida', data.welcome),
        lang: data.lang
      }, recoverStep);
    }
    else if (s === 3) owBriefSave(2, { event_type: 'quinceanera', theme: chosenTheme(data), style: chosenStyle(data), custom_theme_name: data.customThemeName || '', theme_description: data.customThemeDescription || '', cover_url: data.themeCoverUrl || '', preview_url: data.themePreviewUrl || '', theme_colors: (Array.isArray(data.themeColors) ? data.themeColors : []).filter(Boolean), theme_edits: themeOpsEdits(data) }, recoverStep);
    else if (s === last) owBriefSave(10, { rsvp_whatsapp: data.cPhone }, recoverStep);
  };
  const saveAndExit = () => {
    // RMR-685: "Guardar y continuar despues" -> guarda el paso actual en la cuenta (BD) + confirma cross-device.
    saveStep(step);
    setSavedExit(true);
    setTimeout(() => setSavedExit(false), 6000);
  };
  const buildOrderBriefData = () => {
    const exactDate = data.dateMode === 'exact' && data.eventYear && data.eventMonth && data.eventDay ? `${data.eventYear}-${data.eventMonth}-${data.eventDay}` : data.date || '';
    const monthYear = data.dateMode === 'monthyear' && data.eventMYYear && data.eventMYMonth ? `${data.eventMYYear}-${data.eventMYMonth}` : '';
    const godparents = (Array.isArray(data.godparentRows) ? data.godparentRows : []).filter((r) => r.names).map((r) => ({
      role: r.role || 'Otro',
      custom_role: r.role === 'Otro' ? r.customRole || '' : '',
      names: r.names
    }));
    const court = (Array.isArray(data.courtRows) ? data.courtRows : []).filter((r) => r.name).map((r) => ({
      role: r.role || 'Otro',
      name: r.name
    }));
    const itinerary = (Array.isArray(data.itineraryRows) ? data.itineraryRows : []).filter((r) => r.time || r.activity);
    return {
      section_1: {
        honoree_full_name: data.honoree,
        date_mode: data.dateMode,
        event_date: exactDate,
        event_month_year: monthYear,
        event_time: data.time,
        event_end_time: data.endTime,
        venue: data.venue,
        city: data.city,
        mom: data.mom,
        dad: data.dad,
        grandparents: data.grandparents,
        padrinos: data.padrinos
      },
      section_2: { event_type: 'quinceanera', theme: chosenTheme(data), style: chosenStyle(data), custom_theme_name: data.customThemeName || '', theme_description: data.customThemeDescription || '', cover_url: data.themeCoverUrl || '', preview_url: data.themePreviewUrl || '', theme_colors: (Array.isArray(data.themeColors) ? data.themeColors : []).filter(Boolean), theme_edits: themeOpsEdits(data) },
      _contact: { email: data.cEmail, phone: data.cPhone, name: data.cName },
      section_3: {
        sections: data.sections,
        dress_code: data.dressCode,
        itinerary_items: itinerary,
        itinerary_text: itinerary.map((r) => `${r.time || 'Sin hora'} - ${r.activity || ''}`.trim()).join('\n')
      },
      section_4: {
        include_ceremony: !!data.sections.ceremony,
        ceremony_venue: data.ceremonyVenue,
        ceremony_time: data.ceremonyTime,
        ceremony_address: data.ceremonyAddress,
        ceremony_maps: data.ceremonyMaps
      },
      section_5: {
        venue_name: data.venue,
        venue_address: data.recepAddr,
        venue_maps: data.recepMaps,
        venue_time_start: data.recepTime
      },
      section_6: {
        mother_name: data.mom,
        father_name: data.dad,
        grandparents: data.grandparents,
        family_other_notes: data.familyNotes,
        court_of_honor: court,
        corte_names: court.map((r) => `${r.role || 'Otro'}: ${r.name || ''}`.trim()).join('\n') || data.corteNames
      },
      section_7: {
        godparents,
        padrinos: godparents.map((r) => `${r.role === 'Otro' && r.custom_role ? r.custom_role : r.role || 'Otro'}: ${r.names}`).join('\n') || data.padrinos
      },
      section_10: { include_rsvp: !!data.sections.rsvp, rsvp_deadline: data.rsvpDeadline, rsvp_whatsapp: data.cPhone },
      section_8: {
        include_gallery: !!data.sections.gallery,
        gallery_photo_count: data.galleryPhotoCount,
        gallery_method: data.galleryMethod,
        gallery_drive_link: data.galleryDriveLink
      },
      section_9: {
        include_music: !!data.sections.music,
        music_text: data.musicText,
        music_url: data.musicUrl
      },
      section_11: {
        include_itinerary: !!data.sections.itinerary,
        itinerary_items: itinerary,
        itinerary_text: itinerary.map((r) => `${r.time || 'Sin hora'} - ${r.activity || ''}`.trim()).join('\n'),
        include_registry: !!data.sections.registry,
        registry: data.registry
      },
      section_12: {
        include_qrvip: !!data.sections.qrvip,
        qr_album_name: data.qrAlbumName,
        qr_album_style: data.qrAlbumStyle,
        qr_album_notes: data.qrAlbumNotes
      },
      section_13: {
        welcome: data.welcome,
        guestbook_prompt: data.guestbookPrompt,
        thanks_message: data.thanksMessage
      },
      section_15: { language: data.lang, bilingual: data.lang === 'bi' },
      section_17: {
        include_food_menu: !!data.sections.food_menu,
        food_service_type: data.foodServiceType,
        food_menu_text: data.foodMenuText,
        food_accommodation_notes: data.foodAccommodationNotes,
        food_serving_time: data.foodServingTime,
        cocktail_time: data.cocktailTime
      },
      section_16: {
        include_logistics: !!data.sections.logistics,
        hotel_name: data.hotelName,
        hotel_address: data.hotelAddress,
        hotel_maps: data.hotelMaps,
        hotel_rate_code: data.hotelRateCode,
        parking_notes: data.parkingNotes,
        shuttle_info: data.shuttleInfo,
        include_after_party: data.includeAfterParty,
        after_party_venue: data.afterPartyVenue,
        after_party_address: data.afterPartyAddress,
        after_party_maps: data.afterPartyMaps,
        after_party_time: data.afterPartyTime
      },
      logistics: {
        hotel_name: data.hotelName,
        hotel_address: data.hotelAddress,
        hotel_maps: data.hotelMaps,
        hotel_rate_code: data.hotelRateCode,
        parking_notes: data.parkingNotes,
        shuttle_info: data.shuttleInfo,
        include_after_party: data.includeAfterParty,
        after_party_venue: data.afterPartyVenue,
        after_party_address: data.afterPartyAddress,
        after_party_maps: data.afterPartyMaps,
        after_party_time: data.afterPartyTime
      }
    };
  };
  const next = () => {
    if (step < last) {
      saveStep(step, step + 1); // autosave server-side del paso recuperable siguiente
      setStep(step + 1); return;
    }
    if (submitting || submittingRef.current) return; // guard anti doble-cobro (ref sync + state)
    // Paso Pago: si el backend expuso el hook, delegamos la creación de orden +
    // cobro real (Stripe / Zelle / Venmo). El shim llama onDone() cuando toca.
    if (typeof window.__onOrderSubmit === 'function') {
      submittingRef.current = true;
      setSubmitError('');
      setSubmitting(true);
      // red de seguridad: si el backend no resuelve ni redirige, re-habilita el botón
      const safety = setTimeout(() => { submittingRef.current = false; setSubmitting(false); setSubmitError('Esto está tardando más de lo normal. Revisa tu conexión e inténtalo de nuevo.'); }, 20000);
      try {
        window.__onOrderSubmit({
          services: Array.from(data.services),
          honoree: data.honoree,
          date: data.dateMode === 'exact' ? data.eventYear && data.eventMonth && data.eventDay ? `${data.eventYear}-${data.eventMonth}-${data.eventDay}` : data.date : '',
          venue: data.venue,
          city: data.city,
          theme: chosenTheme(data),
          lang: data.lang,
          payMethod: data.payMethod,
          cName: data.cName,
          cEmail: data.cEmail,
          cPhone: data.cPhone,
          orderId: data.orderId,
          packageId,
          total: orderTotal,
          briefData: buildOrderBriefData(),
          onDone: () => { clearTimeout(safety); submittingRef.current = false; try { localStorage.removeItem(DRAFT_KEY); } catch (e) {} setSubmitting(false); setPaid(true); },
        });
      } catch (e) {
        clearTimeout(safety);
        submittingRef.current = false;
        setSubmitting(false);
        setSubmitError('No pudimos procesar tu orden. Intenta de nuevo.');
      }
      return;
    }
    setPaid(true); // fallback demo (sin backend)
  };
  const goTo = (i) => {setStep(i);setPaid(false);};

  const mode = 'video';
  const showDemos = step === 0 && !paid;

  return (
    <div className={`ow ${showDemos ? '' : 'ow-single'} ${showPrev ? 'show-prev' : ''}`}>
      {showDemos && <button className="ow-prev-toggle" onClick={() => setShowPrev(true)}><Ic n="play" s={15} />Ver demos</button>}

      <div className="ow-left">
        <div className="ow-head">
          <div className="ow-brand">
            <img className="ow-brand-logo" src="assets/missxv-logo.png" alt="Miss XV VIP" />
            <div className="ow-brand-name">Miss<em>XV</em>VIP<span className="sub">Orden de invitación</span></div>
            <div className={`ow-secure-pill ${data.auth ? 'saved' : ''}`}>
              {data.auth ? <><Ic n="check" s={13} />Progreso guardado</> : <><Ic n="lock" s={13} />Compra segura</>}
            </div>
          </div>
          <div className="ow-progress">
            <div className="ow-progress-bars">
              {STEPS.map((_, i) => <div key={i} className={`ow-progress-seg ${paid || i < step ? 'done' : i === step ? 'active' : ''}`} />)}
            </div>
            <div className="ow-progress-meta">
              <span className="ow-step-kicker">{paid ? 'Completado' : STEPS[step]}</span>
              <span className="ow-step-count">{paid ? `${STEPS.length} de ${STEPS.length}` : `Paso ${step + 1} de ${STEPS.length}`}</span>
            </div>
          </div>
        </div>

        <div className="ow-body">
          {paid ? <Paid data={data} totals={totals} orderTotal={orderTotal} /> :
          step === 0 ? <StepServices data={data} set={set} totals={totals} toggleSvc={toggleSvc} focus={focus} setFocus={setFocus} onAuthenticated={loadAccountDraft} /> :
          step === 1 ? <StepHonoree data={data} set={set} /> :
          step === 2 ? <StepSections data={data} set={set} toggleSec={toggleSec} /> :
          step === 3 ? <StepTheme data={data} set={set} /> :
          step === 4 ? <StepSummary data={data} totals={totals} goTo={goTo} activePkg={activePkg} orderTotal={orderTotal} /> :
          <StepPay data={data} set={set} totals={totals} orderTotal={orderTotal} />}
        </div>

        <div className={`ow-autosave ${saved ? 'show' : ''}`}><Ic n="check" s={12} />Borrador guardado automáticamente</div>

        {!paid && data.auth && step > 0 && step < last &&
        <div style={{ textAlign: 'center', padding: '2px 0 8px' }}>
          {savedExit
            ? <span style={{ color: 'var(--gold, #EAA121)', fontSize: 12.5, fontWeight: 600 }}><Ic n="check" s={12} /> Guardado en tu cuenta</span>
            : <button type="button" onClick={saveAndExit} style={{ background: 'none', border: 'none', color: 'inherit', opacity: .85, textDecoration: 'underline', cursor: 'pointer', fontSize: 12.5, fontWeight: 600 }}>Guardar mi avance</button>}
        </div>}

        {!paid &&
        <>
        {submitError && <div className="ow-pay-err" role="alert" style={{ color: '#fb7185', fontSize: 12.5, fontWeight: 600, textAlign: 'center', padding: '2px 0 8px' }}>{submitError}</div>}
        <div className="ow-foot">
            {step !== last &&
            <div className="ow-foot-total">
              <span className="lbl">Total de la orden</span>
              <span className="val">{USD(orderTotal)}<small>USD</small></span>
            </div>}
            {step > 0 && <button className="ow-btn-ghost" onClick={() => setStep(step - 1)}><Ic n="chevleft" s={16} />Atrás</button>}
            <button className={`ow-btn ${step === last ? 'ow-btn-pay' : 'ow-btn-cta'}`} disabled={!canNext || submitting} onClick={next} style={{ flex: step === last ? '1 1 auto' : '0 0 auto', minWidth: 0 }}>
              {submitting ? <>Procesando…</> :
            step === last ?
            data.payMethod === 'card' ?
            <><Ic n="card" s={16} />Pagar con tarjeta {USD2(payChargeNow)}</> :
            <><Ic n="check" s={16} />Enviar mi orden</> :
            <>Guardar y continuar<Ic n="chevright" s={16} /></>}
            </button>
          </div>
        </>
        }
      </div>

      {showDemos &&
      <div className="ow-right">
        <button className="ow-prev-close" onClick={() => setShowPrev(false)}><Ic n="x" s={18} /></button>
        <div className="ow-right-head">
          <div className="ow-live-chip"><span className="ow-live-dot" />Mira cada servicio en acción</div>
        </div>
        <VideoShowcase data={data} set={set} toggleSvc={toggleSvc} focus={focus} setFocus={setFocus} totals={totals} activePkg={activePkg} />
        <div className="ow-right-foot">Conoce nuestros servicios premium y mira cada demo antes de agregarla a tu orden.</div>
      </div>}
    </div>);

}

ReactDOM.createRoot(document.getElementById('root')).render(<App />);



