// ─── NexWall Corp AI — App Screens & Flows ───

// ══════════════════════════════════════════
// HELPER: country typo suggestion (Levenshtein-based)
// ══════════════════════════════════════════
const KNOWN_COUNTRIES = [
  'Chile', 'Argentina', 'México', 'Colombia', 'Perú', 'España', 'Brasil',
  'Uruguay', 'Paraguay', 'Bolivia', 'Venezuela', 'Ecuador', 'Panamá',
  'Costa Rica', 'Guatemala', 'Honduras', 'Nicaragua', 'El Salvador',
  'República Dominicana', 'Puerto Rico', 'Cuba', 'Estados Unidos',
  'Canadá', 'Reino Unido', 'Francia', 'Portugal', 'Italia', 'Alemania',
];
function levenshtein(a, b) {
  const m = a.length, n = b.length;
  if (!m) return n; if (!n) return m;
  const dp = Array.from({ length: m + 1 }, (_, i) => [i, ...Array(n).fill(0)]);
  for (let j = 0; j <= n; j++) dp[0][j] = j;
  for (let i = 1; i <= m; i++) for (let j = 1; j <= n; j++) {
    const cost = a[i - 1] === b[j - 1] ? 0 : 1;
    dp[i][j] = Math.min(dp[i - 1][j] + 1, dp[i][j - 1] + 1, dp[i - 1][j - 1] + cost);
  }
  return dp[m][n];
}
// Normalize a country/city string to lowercase, strip accents, trim.
function normalizeCountryName(s) {
  return String(s || '').trim().toLowerCase().normalize('NFD').replace(/[̀-ͯ]/g, '');
}
// Returns true if the input matches a known country exactly (case/accent-insensitive).
function isCountryExactMatch(input) {
  const n = normalizeCountryName(input);
  if (!n) return false;
  return KNOWN_COUNTRIES.some(c => normalizeCountryName(c) === n);
}
// Core fuzzy matcher — catches typos (swap/insert/delete), truncations (prefix), and
// over-typing (extra letters at the end). Tolerant of common mistakes like:
//   "argentine"    → Argentina (1 edit)
//   "argentiaana"  → Argentina (2 inserts)
//   "argen"        → Argentina (prefix match)
//   "argentinne"   → Argentina (1 insert + 1 edit)
//   "arg"          → Argentina (prefix match, but below min-chars → no match)
function suggestCountryCorrection(input) {
  if (!input) return null;
  const clean = String(input).trim();
  if (clean.length < 3) return null;
  const n = normalizeCountryName(clean);
  // Early exit: already a valid country.
  if (KNOWN_COUNTRIES.some(c => normalizeCountryName(c) === n)) return null;

  let best = null, bestScore = Infinity;
  for (const c of KNOWN_COUNTRIES) {
    const cNorm = normalizeCountryName(c);
    // Score = Levenshtein distance, but we also boost (reduce) when the typed input is a
    // meaningful prefix of the country name — typical when the user started typing and we
    // want to help them finish. Prefix of >= 3 chars counts as almost-match.
    const lev = levenshtein(n, cNorm);
    let score = lev;
    // Prefix bonus: if n is a prefix of cNorm OR cNorm starts with n substring of length >= 3,
    // consider it a near-match even if lev is larger (e.g. "argen" → "argentina" lev=4, prefix=yes).
    if (n.length >= 3 && cNorm.startsWith(n)) score = Math.min(score, 1);
    // Substring bonus: "argentne" contains "argent" overlap with "argentina" → use partial score.
    // Simple heuristic: longest common prefix length.
    let lcp = 0;
    while (lcp < n.length && lcp < cNorm.length && n[lcp] === cNorm[lcp]) lcp++;
    if (lcp >= 4) score = Math.min(score, lev - Math.floor(lcp / 2));

    // Looser threshold for longer names. Max acceptable: 4 edits (covers "argentiaana" etc.).
    const threshold = Math.max(3, Math.ceil(cNorm.length * 0.45));
    if (score <= threshold && score < bestScore) { best = c; bestScore = score; }
  }
  return best;
}

// ══════════════════════════════════════════
// SCREEN: LANDING PAGE
// ══════════════════════════════════════════
const LandingPage = ({ onGoLogin, lang='es' }) => {
  const t = (I18N && I18N[lang]) ? I18N[lang].landingApp : I18N.es.landingApp;
  const features = t.features;

  return (
    <div style={{ minHeight: '100vh', background: NX.bg, color: NX.text, fontFamily: "'DM Sans', sans-serif", overflowY: 'auto' }}>

      {/* NAV */}
      <nav style={{ position: 'sticky', top: 0, zIndex: 50, backdropFilter: 'blur(20px)', background: 'rgba(8,8,8,0.8)', borderBottom: `1px solid ${NX.border}`, padding: '0 32px', height: '60px', display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
        <div style={{ display: 'flex', alignItems: 'center', gap: '10px' }}>
          <RobotIcon size={28}/>
          <span style={{ fontSize: '16px', fontWeight: 600, letterSpacing: '-0.02em' }}>NexWall Corp AI</span>
        </div>
        <div style={{ display: 'flex', gap: '10px' }}>
          <NxButton variant="ghost" onClick={() => onGoLogin('login')} style={{ fontSize: '13px', padding: '8px 18px' }}>{t.signIn}</NxButton>
          <NxButton variant="accent" onClick={() => onGoLogin('register')} style={{ fontSize: '13px', padding: '8px 18px' }}>{t.startFree}</NxButton>
        </div>
      </nav>

      {/* HERO */}
      <div style={{ maxWidth: '860px', margin: '0 auto', padding: '60px 32px 80px', textAlign: 'center' }}>
        {/* Animated nebula icon */}
        <div style={{ position: 'relative', width: '320px', height: '320px', margin: '0 auto 32px', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
          <div style={{ position: 'absolute', width: '300px', height: '300px', borderRadius: '50%', background: 'radial-gradient(circle, rgba(139,92,246,0.28) 0%, rgba(139,92,246,0.14) 50%, transparent 70%)', animation: 'nebulaGlow 2.4s ease-in-out infinite' }}/>
          <div style={{ position: 'absolute', width: '360px', height: '360px', borderRadius: '50%', background: 'radial-gradient(circle, rgba(139,92,246,0.22) 0%, rgba(139,92,246,0.08) 60%, transparent 80%)', animation: 'nebulaGlow2 3.1s ease-in-out infinite' }}/>
          <svg viewBox="0 0 320 320" fill="none" style={{ position: 'absolute', width: '320px', height: '320px' }}>
            <circle cx="160" cy="160" r="148" stroke="#8b5cf6" strokeWidth="0.8" strokeDasharray="4 10" opacity="0.35" style={{ animation: 'spin 12s linear infinite', transformOrigin: '160px 160px' }}/>
            <circle cx="160" cy="160" r="124" stroke="#8b5cf6" strokeWidth="0.6" strokeDasharray="2 14" opacity="0.25" style={{ animation: 'spinR 8s linear infinite', transformOrigin: '160px 160px' }}/>
            <circle cx="160" cy="160" r="98" stroke="#8b5cf6" strokeWidth="0.5" strokeDasharray="6 6" opacity="0.15" style={{ animation: 'spin 18s linear infinite', transformOrigin: '160px 160px' }}/>
          </svg>
          <div style={{ position: 'absolute', width: '10px', height: '10px', borderRadius: '50%', background: '#8b5cf6', boxShadow: '0 0 10px 4px #8b5cf6bb', animation: 'energyOrbit 3s linear infinite', top: '50%', left: '50%', marginTop: '-5px', marginLeft: '-5px' }}/>
          <div style={{ position: 'absolute', width: '8px', height: '8px', borderRadius: '50%', background: '#8b5cf6', boxShadow: '0 0 8px 4px #8b5cf6aa', animation: 'energyOrbit2 4.5s linear infinite', top: '50%', left: '50%', marginTop: '-4px', marginLeft: '-4px' }}/>
          <div style={{ position: 'absolute', width: '6px', height: '6px', borderRadius: '50%', background: '#a78bfa', boxShadow: '0 0 6px 3px #a78bfa88', animation: 'energyOrbit3 6s linear infinite', top: '50%', left: '50%', marginTop: '-3px', marginLeft: '-3px' }}/>
          <img src="/icono500x500.png" alt="NexWall Corp AI" style={{ width: '180px', height: '180px', objectFit: 'contain', position: 'relative', zIndex: 2, animation: 'iconPulse 2s ease-in-out infinite' }}/>
        </div>
        <div style={{ display: 'inline-flex', alignItems: 'center', gap: '8px', padding: '6px 14px', background: 'rgba(139,92,246,0.1)', border: '1px solid rgba(139,92,246,0.2)', borderRadius: '100px', fontSize: '12px', color: NX.accent, marginBottom: '28px' }}>
          <span style={{ width: '6px', height: '6px', background: NX.accent, borderRadius: '50%', display: 'inline-block' }}/>
          {t.badge}
        </div>
        <h1 style={{ fontSize: 'clamp(36px, 6vw, 64px)', fontWeight: 600, lineHeight: 1.1, letterSpacing: '-0.03em', marginBottom: '24px' }}>
          {t.h1}<br/>
          <span style={{ background: 'linear-gradient(135deg, #8b5cf6, #a78bfa)', WebkitBackgroundClip: 'text', WebkitTextFillColor: 'transparent' }}>{t.h1g}</span>
        </h1>
        <p style={{ fontSize: '18px', color: NX.muted, lineHeight: 1.6, maxWidth: '560px', margin: '0 auto 40px' }}>
          {t.subtitle}
        </p>
        <div style={{ display: 'flex', gap: '12px', justifyContent: 'center', flexWrap: 'wrap' }}>
          <NxButton variant="accent" onClick={() => onGoLogin('register')} style={{ fontSize: '15px', padding: '14px 32px' }}>
            {t.ctaRegister}
          </NxButton>
          <button onClick={() => { window.location.href = `${API}/auth/google`; }} style={{
            display: 'flex', alignItems: 'center', gap: '10px', padding: '14px 24px',
            background: 'transparent', border: `1px solid ${NX.border2}`, borderRadius: '10px',
            color: NX.text, fontSize: '15px', fontFamily: "'DM Sans', sans-serif", cursor: 'pointer',
          }}>
            <svg width="18" height="18" viewBox="0 0 18 18"><path d="M17.64 9.2c0-.637-.057-1.251-.164-1.84H9v3.481h4.844c-.209 1.125-.843 2.078-1.796 2.716v2.258h2.908c1.702-1.567 2.684-3.874 2.684-6.615z" fill="#4285F4"/><path d="M9 18c2.43 0 4.467-.806 5.956-2.185l-2.908-2.258c-.806.54-1.837.86-3.048.86-2.344 0-4.328-1.584-5.036-3.711H.957v2.332C2.438 15.983 5.482 18 9 18z" fill="#34A853"/><path d="M3.964 10.706A5.41 5.41 0 0 1 3.682 9c0-.593.102-1.17.282-1.706V4.962H.957A8.996 8.996 0 0 0 0 9c0 1.452.348 2.827.957 4.038l3.007-2.332z" fill="#FBBC05"/><path d="M9 3.58c1.321 0 2.508.454 3.44 1.345l2.582-2.58C13.463.891 11.426 0 9 0 5.482 0 2.438 2.017.957 4.962L3.964 6.294C4.672 4.169 6.656 3.58 9 3.58z" fill="#EA4335"/></svg>
            {t.ctaGoogle}
          </button>
        </div>
        <p style={{ marginTop: '16px', fontSize: '12px', color: NX.muted }}>{t.noCreditCard}</p>
      </div>

      {/* FEATURES */}
      <div style={{ maxWidth: '860px', margin: '0 auto', padding: '0 32px 100px' }}>
        <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))', gap: '16px' }}>
          {features.map(f => (
            <div key={f.title} style={{ background: NX.bg3, border: `1px solid ${NX.border}`, borderRadius: '16px', padding: '24px' }}>
              <div style={{ fontSize: '28px', marginBottom: '12px' }}>{f.icon}</div>
              <div style={{ fontSize: '14px', fontWeight: 600, marginBottom: '8px' }}>{f.title}</div>
              <div style={{ fontSize: '13px', color: NX.muted, lineHeight: 1.6 }}>{f.desc}</div>
            </div>
          ))}
        </div>
      </div>

      {/* FOOTER CTA */}
      <div style={{ borderTop: `1px solid ${NX.border}`, padding: '60px 32px', textAlign: 'center' }}>
        <h2 style={{ fontSize: '28px', fontWeight: 600, marginBottom: '12px', letterSpacing: '-0.02em' }}>{t.footer.title}</h2>
        <p style={{ color: NX.muted, marginBottom: '28px', fontSize: '15px' }}>{t.footer.subtitle}</p>
        <NxButton variant="accent" onClick={() => onGoLogin('register')} style={{ fontSize: '15px', padding: '14px 32px' }}>{t.footer.btn}</NxButton>
      </div>

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

// ══════════════════════════════════════════
// SCREEN: LOGIN / REGISTRO
// ══════════════════════════════════════════
const LoginScreen = ({ onLogin, initialMode = 'login', onBack, lang='es' }) => {
  const t = (I18N && I18N[lang]) ? I18N[lang].login : I18N.es.login;
  // Detect ?reset_token=xxx in the URL on mount — if present, jump directly to the reset form.
  const urlResetToken = typeof window !== 'undefined'
    ? new URLSearchParams(window.location.search).get('reset_token')
    : null;
  const [mode, setMode] = React.useState(urlResetToken ? 'reset' : initialMode); // 'login' | 'register' | 'forgot' | 'reset'
  const [name, setName] = React.useState('');
  const [email, setEmail] = React.useState('');
  const [password, setPassword] = React.useState('');
  const [password2, setPassword2] = React.useState('');
  const [loading, setLoading] = React.useState(false);
  const [error, setError] = React.useState(null);
  const [info, setInfo] = React.useState(null); // success banner text
  const [tokenChecking, setTokenChecking] = React.useState(!!urlResetToken);

  // Validate the reset token against the backend before rendering the form — catches expired or
  // already-used links so the user doesn't have to type a password first to find out.
  React.useEffect(() => {
    if (!urlResetToken) return;
    setTokenChecking(true);
    fetch(`${API}/auth/reset-password/${urlResetToken}`)
      .then(r => r.json())
      .then(data => {
        if (!data.valid) {
          setError(data.error || (lang === 'en' ? 'Invalid or expired link' : 'Enlace inválido o caducado'));
        }
      })
      .catch(() => setError(lang === 'en' ? 'Could not validate link' : 'No se pudo validar el enlace'))
      .finally(() => setTokenChecking(false));
  }, [urlResetToken]);

  const submit = async (e) => {
    e.preventDefault();
    setLoading(true);
    setError(null);
    setInfo(null);
    try {
      if (mode === 'forgot') {
        const res = await fetch(`${API}/auth/forgot-password`, {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ email }),
        });
        const data = await res.json().catch(() => ({}));
        if (!res.ok) throw new Error(data.error || 'Error');
        setInfo(lang === 'en'
          ? 'If an account exists with that email, we just sent you a reset link. Check your inbox and spam.'
          : 'Si existe una cuenta con ese email, te enviamos un enlace para restablecer. Revisa tu inbox y spam.');
      } else if (mode === 'reset') {
        if (password !== password2) { setError(lang === 'en' ? 'Passwords do not match' : 'Las contraseñas no coinciden'); setLoading(false); return; }
        if (String(password).length < 6) { setError(lang === 'en' ? 'Minimum 6 characters' : 'Mínimo 6 caracteres'); setLoading(false); return; }
        const res = await fetch(`${API}/auth/reset-password`, {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ token: urlResetToken, password }),
        });
        const data = await res.json().catch(() => ({}));
        if (!res.ok) throw new Error(data.error || 'Error');
        setInfo(lang === 'en'
          ? '✓ Password updated. You can now log in with your new password.'
          : '✓ Contraseña actualizada. Ya puedes iniciar sesión con tu nueva contraseña.');
        // Clean the URL and switch to login after 2s
        window.history.replaceState({}, '', window.location.pathname);
        setTimeout(() => { setMode('login'); setPassword(''); setPassword2(''); setInfo(null); }, 2500);
      } else {
        const endpoint = mode === 'login' ? '/auth/login' : '/auth/register';
        const body = mode === 'login' ? { email, password } : { email, password, name };
        const res = await fetch(`${API}${endpoint}`, {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify(body),
        });
        const data = await res.json();
        if (!res.ok) throw new Error(data.error || 'Error desconocido');
        onLogin(data.token, data.user);
      }
    } catch (err) {
      setError(err.message);
    } finally {
      setLoading(false);
    }
  };

  const inputStyle = {
    width: '100%', padding: '12px 14px', background: NX.bg3,
    border: `1px solid ${NX.border}`, borderRadius: '10px',
    color: NX.text, fontSize: '14px', fontFamily: "'DM Sans', sans-serif",
    outline: 'none', boxSizing: 'border-box',
  };
  const labelStyle = { fontSize: '12px', color: NX.muted, marginBottom: '6px', display: 'block' };

  return (
    <div style={{
      height: '100vh', display: 'flex', alignItems: 'center', justifyContent: 'center',
      background: NX.bg, position: 'relative', overflow: 'hidden',
    }}>
      {/* Background glow */}
      <div style={{ position: 'absolute', width: '600px', height: '600px', borderRadius: '50%', background: 'radial-gradient(circle, rgba(139,92,246,0.06) 0%, transparent 70%)', top: '50%', left: '50%', transform: 'translate(-50%, -50%)', pointerEvents: 'none' }}/>

      <div style={{ width: '100%', maxWidth: '380px', padding: '0 20px', animation: 'modalIn 0.25s ease' }}>
        {onBack && <button onClick={onBack} style={{ background: 'none', border: 'none', color: NX.muted, fontSize: '13px', cursor: 'pointer', marginBottom: '16px', padding: 0 }}>{t.back}</button>}
        {/* Logo */}
        <div style={{ textAlign: 'center', marginBottom: '36px' }}>
          <a href="/" style={{ display: 'inline-flex', alignItems: 'center', gap: '10px', marginBottom: '8px', textDecoration: 'none' }}>
            <img src="/favicon.png" alt="NexWall" style={{ width: 32, height: 32, borderRadius: 8, objectFit: 'cover' }}/>
            <span style={{ fontSize: '18px', fontWeight: 500, color: NX.text, letterSpacing: '-0.02em' }}>NexWall Corp AI</span>
          </a>
          <p style={{ color: NX.muted, fontSize: '13px', margin: 0 }}>
            {mode === 'login' ? t.tagline
              : mode === 'register' ? t.taglineRegister
              : mode === 'forgot' ? (lang === 'en' ? 'Enter your email to receive a reset link' : 'Ingresa tu email para recibir un enlace de restablecimiento')
              : (lang === 'en' ? 'Enter your new password' : 'Elige tu nueva contraseña')}
          </p>
        </div>

        {/* Card */}
        <div style={{ background: NX.bg2, border: `1px solid ${NX.border}`, borderRadius: '16px', padding: '28px' }}>
          {error && (
            <div style={{ padding: '10px 14px', background: 'rgba(248,113,113,0.08)', border: '1px solid rgba(248,113,113,0.2)', borderRadius: '8px', fontSize: '13px', color: NX.danger, marginBottom: '16px' }}>
              {error}
            </div>
          )}
          {info && (
            <div style={{ padding: '10px 14px', background: 'rgba(52,211,153,0.08)', border: '1px solid rgba(52,211,153,0.25)', borderRadius: '8px', fontSize: '13px', color: NX.success, marginBottom: '16px' }}>
              {info}
            </div>
          )}
          {/* Google OAuth — only on login/register modes */}
          {(mode === 'login' || mode === 'register') && <>
            <button type="button" onClick={() => { window.location.href = `${API}/auth/google`; }} style={{
              width: '100%', display: 'flex', alignItems: 'center', justifyContent: 'center', gap: '10px',
              padding: '11px 14px', background: 'transparent', border: `1px solid ${NX.border2}`,
              borderRadius: '10px', color: NX.text, fontSize: '14px', fontFamily: "'DM Sans', sans-serif",
              cursor: 'pointer', marginBottom: '16px', transition: 'background 0.15s',
            }} onMouseEnter={e => e.currentTarget.style.background = NX.bg3}
               onMouseLeave={e => e.currentTarget.style.background = 'transparent'}>
              <svg width="18" height="18" viewBox="0 0 18 18"><path d="M17.64 9.2c0-.637-.057-1.251-.164-1.84H9v3.481h4.844c-.209 1.125-.843 2.078-1.796 2.716v2.258h2.908c1.702-1.567 2.684-3.874 2.684-6.615z" fill="#4285F4"/><path d="M9 18c2.43 0 4.467-.806 5.956-2.185l-2.908-2.258c-.806.54-1.837.86-3.048.86-2.344 0-4.328-1.584-5.036-3.711H.957v2.332C2.438 15.983 5.482 18 9 18z" fill="#34A853"/><path d="M3.964 10.706A5.41 5.41 0 0 1 3.682 9c0-.593.102-1.17.282-1.706V4.962H.957A8.996 8.996 0 0 0 0 9c0 1.452.348 2.827.957 4.038l3.007-2.332z" fill="#FBBC05"/><path d="M9 3.58c1.321 0 2.508.454 3.44 1.345l2.582-2.58C13.463.891 11.426 0 9 0 5.482 0 2.438 2.017.957 4.962L3.964 6.294C4.672 4.169 6.656 3.58 9 3.58z" fill="#EA4335"/></svg>
              {t.continueGoogle}
            </button>
            <div style={{ display: 'flex', alignItems: 'center', gap: '10px', marginBottom: '16px' }}>
              <div style={{ flex: 1, height: '1px', background: NX.border }}/>
              <span style={{ fontSize: '12px', color: NX.muted }}>o</span>
              <div style={{ flex: 1, height: '1px', background: NX.border }}/>
            </div>
          </>}

          <form onSubmit={submit}>
            {mode === 'register' && (
              <div style={{ marginBottom: '14px' }}>
                <label style={labelStyle}>{t.labelName}</label>
                <input style={inputStyle} type="text" placeholder={t.placeholderName} value={name} onChange={e => setName(e.target.value)} required/>
              </div>
            )}
            {(mode === 'login' || mode === 'register' || mode === 'forgot') && (
              <div style={{ marginBottom: '14px' }}>
                <label style={labelStyle}>{t.labelEmail}</label>
                <input style={inputStyle} type="email" placeholder="tu@email.com" value={email} onChange={e => setEmail(e.target.value)} required/>
              </div>
            )}
            {(mode === 'login' || mode === 'register') && (
              <div style={{ marginBottom: '20px' }}>
                <label style={labelStyle}>{t.labelPassword}</label>
                <input style={inputStyle} type="password" placeholder="••••••••" value={password} onChange={e => setPassword(e.target.value)} required minLength={6}/>
              </div>
            )}
            {mode === 'reset' && (
              <>
                {tokenChecking && (
                  <div style={{ padding: '10px 14px', background: NX.bg3, border: `1px solid ${NX.border}`, borderRadius: '8px', fontSize: '12px', color: NX.muted, marginBottom: '16px', display: 'flex', alignItems: 'center', gap: '8px' }}>
                    <span style={{ display: 'inline-block', width: '10px', height: '10px', border: '1.5px solid currentColor', borderTopColor: 'transparent', borderRadius: '50%', animation: 'spin 0.7s linear infinite' }}/>
                    {lang === 'en' ? 'Validating link…' : 'Validando enlace…'}
                  </div>
                )}
                {!tokenChecking && !error && (
                  <>
                    <div style={{ marginBottom: '14px' }}>
                      <label style={labelStyle}>{lang === 'en' ? 'New password' : 'Nueva contraseña'}</label>
                      <input style={inputStyle} type="password" placeholder={lang === 'en' ? 'At least 6 characters' : 'Mínimo 6 caracteres'} value={password} onChange={e => setPassword(e.target.value)} required minLength={6} autoFocus/>
                    </div>
                    <div style={{ marginBottom: '20px' }}>
                      <label style={labelStyle}>{lang === 'en' ? 'Confirm password' : 'Confirma la contraseña'}</label>
                      <input style={inputStyle} type="password" placeholder={lang === 'en' ? 'Type it again' : 'Escríbela de nuevo'} value={password2} onChange={e => setPassword2(e.target.value)} required minLength={6}/>
                    </div>
                  </>
                )}
              </>
            )}
            {/* Hide submit when the reset token is invalid — only leave the "request new link" option */}
            {!(mode === 'reset' && (tokenChecking || error)) && (
              <NxButton variant="accent" full type="submit" disabled={loading}>
                {loading ? t.loading
                  : mode === 'login' ? t.submit
                  : mode === 'register' ? t.submitRegister
                  : mode === 'forgot' ? (lang === 'en' ? 'Send reset link' : 'Enviar enlace')
                  : (lang === 'en' ? 'Update password' : 'Actualizar contraseña')}
              </NxButton>
            )}
            {mode === 'reset' && error && !tokenChecking && (
              <NxButton variant="ghost" full type="button" onClick={() => { window.history.replaceState({}, '', window.location.pathname); setMode('forgot'); setError(null); setInfo(null); }}>
                {lang === 'en' ? 'Request a new link' : 'Solicitar nuevo enlace'}
              </NxButton>
            )}
            {mode === 'login' && (
              <div style={{ textAlign: 'center', marginTop: '12px' }}>
                <span onClick={() => { setMode('forgot'); setError(null); setInfo(null); }}
                  style={{ color: NX.muted, cursor: 'pointer', fontSize: '12px' }}>
                  {lang === 'en' ? '¿Forgot your password?' : '¿Olvidaste tu contraseña?'}
                </span>
              </div>
            )}
          </form>
        </div>

        {/* Toggle */}
        <p style={{ textAlign: 'center', marginTop: '16px', fontSize: '13px', color: NX.muted }}>
          {mode === 'login' && <>{t.noAccount}{' '}
            <span onClick={() => { setMode('register'); setError(null); setInfo(null); }} style={{ color: NX.accent, cursor: 'pointer' }}>{t.register}</span>
          </>}
          {mode === 'register' && <>{t.hasAccount}{' '}
            <span onClick={() => { setMode('login'); setError(null); setInfo(null); }} style={{ color: NX.accent, cursor: 'pointer' }}>{t.signIn}</span>
          </>}
          {(mode === 'forgot' || mode === 'reset') && (
            <span onClick={() => { setMode('login'); setError(null); setInfo(null); }} style={{ color: NX.accent, cursor: 'pointer' }}>
              {lang === 'en' ? '← Back to login' : '← Volver a iniciar sesión'}
            </span>
          )}
        </p>
      </div>
    </div>
  );
};

// ══════════════════════════════════════════
// SCREEN: PRIMEROS PASOS (Onboarding Hub)
// ══════════════════════════════════════════
const STEP_ICONS_LG = [
  // Connect channels
  <svg width="48" height="48" viewBox="0 0 48 48" fill="none">
    <circle cx="24" cy="24" r="23" stroke="url(#g0)" strokeWidth="1.2" opacity="0.3"/>
    <path d="M24 8C14.06 8 6 16.06 6 26s8.06 18 18 18 18-8.06 18-18" stroke="#8b5cf6" strokeWidth="2" strokeLinecap="round"/>
    <path d="M34 8l8 8-8 8" stroke="#8b5cf6" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
    <circle cx="24" cy="26" r="4" fill="#8b5cf6" opacity="0.2"/>
    <defs><linearGradient id="g0" x1="0" y1="0" x2="48" y2="48"><stop stopColor="#8b5cf6"/><stop offset="1" stopColor="#8b5cf6"/></linearGradient></defs>
  </svg>,
  // Assets
  <svg width="48" height="48" viewBox="0 0 48 48" fill="none">
    <circle cx="24" cy="24" r="23" stroke="url(#g1)" strokeWidth="1.2" opacity="0.3"/>
    <rect x="8" y="8" width="13" height="13" rx="3" stroke="#8b5cf6" strokeWidth="1.8"/>
    <rect x="27" y="8" width="13" height="13" rx="3" stroke="#8b5cf6" strokeWidth="1.8"/>
    <rect x="8" y="27" width="13" height="13" rx="3" stroke="#8b5cf6" strokeWidth="1.8"/>
    <rect x="27" y="27" width="13" height="13" rx="3" stroke="#8b5cf6" strokeWidth="1.8"/>
    <defs><linearGradient id="g1" x1="0" y1="0" x2="48" y2="48"><stop stopColor="#8b5cf6"/><stop offset="1" stopColor="#8b5cf6"/></linearGradient></defs>
  </svg>,
  // Brand
  <svg width="48" height="48" viewBox="0 0 48 48" fill="none">
    <circle cx="24" cy="24" r="23" stroke="url(#g2)" strokeWidth="1.2" opacity="0.3"/>
    <path d="M24 7l4.5 10H39l-8.5 6.5 3.3 10L24 27l-9.8 6.5 3.3-10L9 13h10.5L24 7z" stroke="#8b5cf6" strokeWidth="1.8" strokeLinejoin="round"/>
    <defs><linearGradient id="g2" x1="0" y1="0" x2="48" y2="48"><stop stopColor="#8b5cf6"/><stop offset="1" stopColor="#8b5cf6"/></linearGradient></defs>
  </svg>,
  // Strategy
  <svg width="48" height="48" viewBox="0 0 48 48" fill="none">
    <circle cx="24" cy="24" r="23" stroke="url(#g3)" strokeWidth="1.2" opacity="0.3"/>
    <path d="M8 38l9-18 7 12 6-9 9 15" stroke="#8b5cf6" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
    <circle cx="38" cy="12" r="4" fill="#8b5cf6" opacity="0.6"/>
    <path d="M35 9l3 3 5-5" stroke="white" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/>
    <defs><linearGradient id="g3" x1="0" y1="0" x2="48" y2="48"><stop stopColor="#8b5cf6"/><stop offset="1" stopColor="#8b5cf6"/></linearGradient></defs>
  </svg>,
];

const STEP_TOOLTIPS_ES = ['Canales de anuncios', 'Activos del negocio', 'Identidad de marca', 'Primera estrategia'];
const STEP_TOOLTIPS_EN = ['Ad channels', 'Business assets', 'Brand identity', 'First strategy'];

// Tiny shared hook: viewport is below the responsive breakpoint (≤820 = phones + small tablets).
// Used to flip between desktop two-col home and the stacked-mobile home where the chat card
// sits on top with the mascot inlined inside it.
const useIsMobile = (breakpoint = 820) => {
  const [m, setM] = React.useState(() => typeof window !== 'undefined' && window.innerWidth <= breakpoint);
  React.useEffect(() => {
    const onResize = () => setM(window.innerWidth <= breakpoint);
    window.addEventListener('resize', onResize);
    return () => window.removeEventListener('resize', onResize);
  }, [breakpoint]);
  return m;
};

const PrimerosPasos = ({ stepsCompleted, setActiveFlow, onStartStrategy, lang='es', campaigns = [], setView, onAutoComplete, onAssistantComplete, onOpenAdvanced, defaultImageModel='gemini-3.1-flash-image-preview' }) => {
  const isMobile = useIsMobile(820);
  const t = (I18N && I18N[lang]) ? I18N[lang].onboarding : I18N.es.onboarding;
  const tooltips = lang === 'en' ? STEP_TOOLTIPS_EN : STEP_TOOLTIPS_ES;
  const [tooltip, setTooltip] = React.useState(-1);
  const [hoveredCard, setHoveredCard] = React.useState(-1);
  // Fallback al asistente — el modo advanced quedó deprecated como entry point.
  const startStrategy = onStartStrategy || (() => setActiveFlow('assistant'));

  // Verify assets (step 1) against the backend — stepsCompleted comes from localStorage and can be stale.
  // Re-fetches on mount, when stepsCompleted changes, and when a 'nw-assets-refresh' window event fires
  // (dispatched by the parent after AssetsFlow completes).
  const [metaAssets, setMetaAssets] = React.useState(null);
  const [assetsChecked, setAssetsChecked] = React.useState(false);
  const refreshAssets = React.useCallback(() => {
    const bizId = localStorage.getItem('nw_active_biz_id') || '1';
    authFetch(`/api/meta/assets?biz_id=${encodeURIComponent(bizId)}`)
      .then(r => r.ok ? r.json() : null)
      .then(data => { setMetaAssets(data || null); setAssetsChecked(true); })
      .catch(() => setAssetsChecked(true));
  }, []);
  React.useEffect(() => {
    refreshAssets();
    const handler = () => refreshAssets();
    window.addEventListener('nw-assets-refresh', handler);
    window.addEventListener('focus', handler);
    return () => {
      window.removeEventListener('nw-assets-refresh', handler);
      window.removeEventListener('focus', handler);
    };
  }, [refreshAssets]);
  // Re-fetch when stepsCompleted changes (after any step is marked done)
  React.useEffect(() => { refreshAssets(); }, [stepsCompleted.length, refreshAssets]);
  const assetsComplete = !!(metaAssets?.ad_account_id && metaAssets?.page_id);
  const step1Broken = assetsChecked && stepsCompleted.includes(1) && !assetsComplete;

  // Same pattern for step 2 (brand DNA) — verify against the backend so a user who has step 2 marked
  // done in localStorage but no DNA row in DB (e.g. DB was reset, bad network during save, wiped
  // account, data loss) sees a broken state instead of a false "all done" island.
  const [brandDna, setBrandDna] = React.useState(null);
  const [brandChecked, setBrandChecked] = React.useState(false);
  const refreshBrand = React.useCallback(() => {
    const bizId = localStorage.getItem('nw_active_biz_id') || '1';
    authFetch(`/api/brand-dna/${encodeURIComponent(bizId)}`)
      .then(r => r.ok ? r.json() : null)
      .then(data => { setBrandDna(data && typeof data === 'object' ? data : null); setBrandChecked(true); })
      .catch(() => setBrandChecked(true));
  }, []);
  React.useEffect(() => {
    refreshBrand();
    const handler = () => refreshBrand();
    window.addEventListener('nw-brand-refresh', handler);
    window.addEventListener('focus', handler);
    return () => {
      window.removeEventListener('nw-brand-refresh', handler);
      window.removeEventListener('focus', handler);
    };
  }, [refreshBrand]);
  React.useEffect(() => { refreshBrand(); }, [stepsCompleted.length, refreshBrand]);
  // A valid brand DNA has at least one meaningful field. An empty object counts as "not configured".
  const brandComplete = !!(brandDna && typeof brandDna === 'object' && Object.keys(brandDna).some(k => {
    const v = brandDna[k];
    return (typeof v === 'string' && v.trim()) || (Array.isArray(v) && v.length > 0);
  }));
  const step2Broken = brandChecked && stepsCompleted.includes(2) && !brandComplete;

  // Backend-driven reconciliation: if the server already has brand DNA / connected Meta assets
  // for this business, mark the corresponding onboarding step as complete — even if local
  // storage says it is pending (happens when the user signs in on a new device). Never REMOVE
  // a step from stepsCompleted here; removal is handled by the "broken" banners above.
  React.useEffect(() => {
    if (!onAutoComplete) return;
    // Step 0 (Meta connected) + Step 1 (ad account + page chosen). Both require metaAssets.
    if (assetsChecked && metaAssets && !stepsCompleted.includes(0)) onAutoComplete(0);
    if (assetsChecked && assetsComplete && !stepsCompleted.includes(1)) onAutoComplete(1);
  }, [assetsChecked, assetsComplete, metaAssets, stepsCompleted, onAutoComplete]);

  React.useEffect(() => {
    if (!onAutoComplete) return;
    if (brandChecked && brandComplete && !stepsCompleted.includes(2)) onAutoComplete(2);
  }, [brandChecked, brandComplete, stepsCompleted, onAutoComplete]);

  const steps = [
    { id: 0, flow: 'connect',  time: t.steps[0].time, title: t.steps[0].title, desc: t.steps[0].desc },
    { id: 1, flow: 'assets',   time: t.steps[1].time, title: t.steps[1].title, desc: t.steps[1].desc },
    { id: 2, flow: 'brand',    time: t.steps[2].time, title: t.steps[2].title, desc: t.steps[2].desc },
    { id: 3, flow: 'strategy', time: t.steps[3].time, title: t.steps[3].title, desc: t.steps[3].desc },
  ];

  // Step 3 (first strategy) is "effectively done" if the user has ANY campaign in the DB — even
  // if stepsCompleted never got [3] persisted (happens if the user closed the tab before clicking
  // "Ver mis estrategias" on the success screen, or navigated away via the nav bar).
  const hasLaunchedCampaign = Array.isArray(campaigns) && campaigns.some(c => c && c.status !== 'draft');
  const step3Effective = stepsCompleted.includes(3) || hasLaunchedCampaign;

  // A step is "effectively done" only if the user marked it done AND the backend confirms it.
  // We subtract broken steps from the completion count so the welcome island tells the truth.
  const baseDone = stepsCompleted.filter(i => !(i === 1 && step1Broken) && !(i === 2 && step2Broken));
  const effectiveDone = step3Effective && !baseDone.includes(3) ? [...baseDone, 3] : baseDone;
  const completedCount = effectiveDone.length;
  const showIsland = completedCount > 0 || stepsCompleted.length > 0;
  const setupDone = effectiveDone.includes(0) && effectiveDone.includes(1) && effectiveDone.includes(2);
  const allDone = completedCount === 4;
  const totalCampaigns = Array.isArray(campaigns) ? campaigns.length : 0;
  const activeCampaigns = Array.isArray(campaigns) ? campaigns.filter(c => c && c.status !== 'draft').length : 0;

  // Pending steps to show as big cards (exclude done steps). A step whose backend state is broken
  // (step 1 assets missing, step 2 brand DNA missing) is treated as pending so the user can fix it.
  const pendingSteps = steps.filter((_, i) => !effectiveDone.includes(i));
  const cols = pendingSteps.length === 1 ? 1 : pendingSteps.length === 2 ? 2 : pendingSteps.length === 3 ? 3 : 4;

  return (
    <div style={{ height: '100%', overflowY: 'auto', overflowX: 'hidden', padding: '40px 24px', display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
    {/* Cap a 900px en estado de onboarding (steps + banners). Cuando allDone, relajamos
        a 1440px para que el AssistantFlow embedded (panel + mascota) pueda quedar
        ópticamente centrado en pantalla con un spacer izquierdo que mirroreee la mascota. */}
    <div style={{ width: '100%', maxWidth: allDone ? '1440px' : '900px', display: 'flex', flexDirection: 'column', gap: '32px' }}>

      {/* Header — only show when no island yet */}
      {!showIsland && (
        <div>
          <h1 style={{ fontSize: '28px', fontWeight: 300, letterSpacing: '-0.03em', color: NX.text, margin: '0 0 8px' }}>{t.welcome}</h1>
          <p style={{ color: NX.muted, fontSize: '14px', margin: 0, lineHeight: 1.6 }}>{t.subtitle}</p>
        </div>
      )}

      {/* Step 1 assets broken banner — shown when localStorage says step 1 done but backend says otherwise */}
      {step1Broken && (
        <div style={{ padding: '14px 18px', background: 'rgba(248,113,113,0.1)', border: `1.5px solid ${NX.danger}`, borderRadius: '12px', display: 'flex', alignItems: 'center', gap: '14px', flexWrap: 'wrap' }}>
          <svg width="28" height="28" viewBox="0 0 24 24" fill="none" style={{ flexShrink: 0 }}>
            <circle cx="12" cy="12" r="10" stroke={NX.danger} strokeWidth="1.8"/>
            <path d="M12 7v6M12 16.5v.01" stroke={NX.danger} strokeWidth="2" strokeLinecap="round"/>
          </svg>
          <div style={{ flex: 1, minWidth: '240px' }}>
            <div style={{ fontSize: '14px', fontWeight: 600, color: NX.danger, marginBottom: '2px' }}>
              {lang === 'en' ? 'Your Meta assets are not connected' : 'Tus activos de Meta no están conectados'}
            </div>
            <div style={{ fontSize: '12px', color: NX.muted, lineHeight: 1.5 }}>
              {lang === 'en'
                ? 'You marked "Configure assets" as done, but we cannot find a connected ad account or page. Redo this step, or reset all connections from Integrations if you want to start fresh.'
                : 'Marcaste "Configurar activos" como hecho, pero no encontramos una cuenta publicitaria o página conectadas. Vuelve a hacer este paso, o ve a Integraciones para resetear todas las conexiones y empezar de cero.'}
            </div>
          </div>
          <div style={{ display: 'flex', gap: '8px', flexShrink: 0 }}>
            <NxButton variant="danger" onClick={() => setActiveFlow('assets')}>
              {lang === 'en' ? 'Fix now' : 'Arreglar ahora'}
            </NxButton>
          </div>
        </div>
      )}

      {/* Step 2 brand DNA broken banner — marked done locally but DB has no DNA row for this biz */}
      {step2Broken && (
        <div style={{ padding: '14px 18px', background: 'rgba(248,113,113,0.1)', border: `1.5px solid ${NX.danger}`, borderRadius: '12px', display: 'flex', alignItems: 'center', gap: '14px', flexWrap: 'wrap' }}>
          <svg width="28" height="28" viewBox="0 0 24 24" fill="none" style={{ flexShrink: 0 }}>
            <circle cx="12" cy="12" r="10" stroke={NX.danger} strokeWidth="1.8"/>
            <path d="M12 7v6M12 16.5v.01" stroke={NX.danger} strokeWidth="2" strokeLinecap="round"/>
          </svg>
          <div style={{ flex: 1, minWidth: '240px' }}>
            <div style={{ fontSize: '14px', fontWeight: 600, color: NX.danger, marginBottom: '2px' }}>
              {lang === 'en' ? 'Your brand identity is empty' : 'Tu identidad de marca está vacía'}
            </div>
            <div style={{ fontSize: '12px', color: NX.muted, lineHeight: 1.5 }}>
              {lang === 'en'
                ? 'You marked "Brand DNA" as done, but we cannot find any saved brand identity for this business. Re-run the brand step so AI generations use your real tone and audience again.'
                : 'Marcaste "Marca" como hecho, pero no encontramos una identidad de marca guardada para este negocio. Vuelve a hacer el paso para que la IA use tu tono y audiencia reales de nuevo.'}
            </div>
          </div>
          <div style={{ display: 'flex', gap: '8px', flexShrink: 0 }}>
            <NxButton variant="danger" onClick={() => setActiveFlow('brand')}>
              {lang === 'en' ? 'Reconfigure' : 'Reconfigurar'}
            </NxButton>
          </div>
        </div>
      )}

      {/* Progress Island — when allDone WITH no broken backend state, the island disappears
          entirely (the user has nothing to fix; the island just adds visual noise). It only
          surfaces in the corner if a step is broken (red) so the user can click to re-do it. */}
      {showIsland && allDone && (step1Broken || step2Broken) && (
        <div style={{ position:'fixed', top:'66px', left:'20px', zIndex:42, display:'inline-flex', alignItems:'center', gap:'4px', background:'rgba(10,10,14,0.85)', backdropFilter:'blur(20px)', border:'1px solid rgba(255,255,255,0.08)', borderRadius:'100px', padding:'5px 8px', boxShadow:'0 4px 18px rgba(0,0,0,0.4)' }}>
          {steps.map((step, i) => {
            const broken = (i === 1 && step1Broken) || (i === 2 && step2Broken);
            const done = stepsCompleted.includes(i) && !broken;
            return (
              <div key={i} style={{ position:'relative', display:'flex', alignItems:'center', cursor: broken ? 'pointer' : 'default' }}
                onMouseEnter={() => setTooltip(i)}
                onMouseLeave={() => setTooltip(-1)}
                onClick={() => { if (broken) setActiveFlow(i === 1 ? 'assets' : 'brand'); }}>
                <div style={{
                  width:'22px', height:'22px', borderRadius:'50%', display:'flex', alignItems:'center', justifyContent:'center',
                  background: broken ? 'rgba(248,113,113,0.18)' : done ? 'rgba(52,211,153,0.18)' : NX.bg3,
                  border:`1.5px solid ${broken ? NX.danger : done ? NX.success : NX.border}`,
                  fontSize:'10px', fontFamily:"'DM Mono',monospace", fontWeight:700, color: broken ? NX.danger : done ? NX.success : NX.muted,
                }}>
                  {done
                    ? <svg width="11" height="11" viewBox="0 0 14 14" fill="none"><path d="M2.5 7l3 3L11.5 4" stroke={NX.success} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/></svg>
                    : (i + 1)}
                </div>
                {tooltip === i && (
                  <div style={{ position:'absolute', top:'calc(100% + 8px)', left:'50%', transform:'translateX(-50%)', background:'rgba(10,10,14,0.95)', border:`1px solid ${NX.border}`, borderRadius:'8px', padding:'5px 10px', whiteSpace:'nowrap', fontSize:'11px', color:NX.text, pointerEvents:'none', zIndex:100, boxShadow:'0 4px 16px rgba(0,0,0,0.5)' }}>
                    {tooltips[i]}{done && <span style={{ color: NX.success, marginLeft:'6px' }}>✓</span>}
                  </div>
                )}
              </div>
            );
          })}
        </div>
      )}
      {showIsland && !allDone && (
        <div style={{ display: 'flex', flexDirection: 'column', gap: '16px', alignItems: 'stretch' }}>
          <div>
            <h1 style={{ fontSize: '24px', fontWeight: 300, letterSpacing: '-0.03em', color: NX.text, margin: '0 0 4px' }}>{t.welcome}</h1>
            <p style={{ color: NX.muted, fontSize: '13px', margin: 0 }}>{completedCount}{t.completed} — {pendingSteps.length > 0 ? `${pendingSteps.length} paso${pendingSteps.length>1?'s':''} restante${pendingSteps.length>1?'s':''}` : t.allDone}</p>
          </div>
          {/* Island pill (full inline version for in-progress users) */}
          <div style={{ display: 'inline-flex', alignItems: 'center', gap: '6px', background: 'rgba(10,10,14,0.85)', backdropFilter: 'blur(20px)', border: `1px solid rgba(255,255,255,0.08)`, borderRadius: '100px', padding: '10px 20px', boxShadow: '0 4px 24px rgba(0,0,0,0.4)', alignSelf: 'flex-start', position: 'relative' }}>
            {steps.map((step, i) => {
              const broken = (i === 1 && step1Broken) || (i === 2 && step2Broken);
              const done = stepsCompleted.includes(i) && !broken;
              return (
                <div key={i} style={{ position: 'relative', display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '4px', cursor: broken ? 'pointer' : undefined }}
                  onMouseEnter={() => setTooltip(i)}
                  onMouseLeave={() => setTooltip(-1)}
                  onClick={() => { if (broken) setActiveFlow(i === 1 ? 'assets' : 'brand'); }}>
                  {/* Number label above circle */}
                  <span style={{ fontFamily: "'DM Mono',monospace", fontSize: '9px', fontWeight: 700, color: broken ? NX.danger : done ? NX.accent : NX.muted, letterSpacing: '0.05em' }}>{i + 1}</span>
                  <div style={{
                    width: '32px', height: '32px', borderRadius: '50%', display: 'flex', alignItems: 'center', justifyContent: 'center', cursor: 'pointer',
                    background: broken ? 'rgba(248,113,113,0.15)' : done ? 'rgba(52,211,153,0.18)' : NX.bg3,
                    border: `1.5px solid ${broken ? NX.danger : done ? NX.success : NX.border}`,
                    transition: 'all 0.3s',
                    boxShadow: done ? '0 0 12px rgba(52,211,153,0.35)' : 'none',
                  }}
                    onClick={() => i === 3 ? startStrategy() : setActiveFlow(step.flow)}>
                    {done
                      ? <svg width="14" height="14" viewBox="0 0 14 14" fill="none"><path d="M2.5 7l3 3L11.5 4" stroke={NX.success} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/></svg>
                      : <span style={{ fontSize: '10px', fontFamily: "'DM Mono',monospace", color: NX.muted, fontWeight: 600 }}>—</span>}
                  </div>
                  {/* Tooltip */}
                  {tooltip === i && (
                    <div style={{ position: 'absolute', bottom: 'calc(100% + 12px)', left: '50%', transform: 'translateX(-50%)', background: 'rgba(10,10,14,0.95)', backdropFilter: 'blur(12px)', border: `1px solid ${NX.border}`, borderRadius: '8px', padding: '5px 10px', whiteSpace: 'nowrap', fontSize: '11px', color: NX.text, fontWeight: 500, pointerEvents: 'none', zIndex: 100, boxShadow: '0 4px 16px rgba(0,0,0,0.5)' }}>
                      {tooltips[i]}
                      {done && <span style={{ color: NX.success, marginLeft: '6px' }}>✓</span>}
                    </div>
                  )}
                  {/* Connector line */}
                  {i < 3 && <div style={{ width: '20px', height: '1.5px', background: done && stepsCompleted.includes(i+1) ? 'linear-gradient(90deg, #8b5cf6, #a78bfa)' : NX.border, position: 'absolute', top: '50%', left: '100%', marginTop: '4px' }}/>}
                </div>
              );
            })}
          </div>
        </div>
      )}

      {/* Big step cards — only pending steps */}
      {pendingSteps.length > 0 && (
        <div className="nx-steps-grid" style={{ display: 'grid', gridTemplateColumns: `repeat(${Math.min(cols, 3)}, 1fr)`, gap: '20px', flex: 1 }}>
          {pendingSteps.map((step) => {
            const i = step.id;
            const locked = i > 0 && !stepsCompleted.includes(i - 1);
            const isStrategy = i === 3;
            const hovered = hoveredCard === i;

            return (
              <div key={i} className="nx-step-card"
                onMouseEnter={() => !locked && setHoveredCard(i)}
                onMouseLeave={() => setHoveredCard(-1)}
                style={{
                  position: 'relative', borderRadius: '20px', overflow: 'hidden',
                  background: hovered && !locked ? 'rgba(22,22,30,0.95)' : NX.bg3,
                  border: `1.5px solid ${hovered && !locked ? (isStrategy ? NX.accent2 : NX.accent) : NX.border}`,
                  transition: 'all 0.25s', cursor: locked ? 'default' : 'pointer',
                  opacity: locked ? 0.4 : 1,
                  boxShadow: hovered && !locked ? `0 0 40px rgba(139,92,246,0.15), 0 8px 32px rgba(0,0,0,0.4)` : '0 2px 12px rgba(0,0,0,0.2)',
                  display: 'flex', flexDirection: 'column',
                  minHeight: setupDone && isStrategy ? '340px' : '280px',
                  gridColumn: setupDone && isStrategy ? '1 / -1' : 'auto',
                }}
                onClick={() => !locked && (i === 3 ? startStrategy() : setActiveFlow(step.flow))}>

                {/* Gradient glow bg on hover */}
                {hovered && !locked && (
                  <div style={{ position: 'absolute', inset: 0, background: `radial-gradient(ellipse at 30% 20%, ${isStrategy ? 'rgba(139,92,246,0.08)' : 'rgba(139,92,246,0.08)'} 0%, transparent 70%)`, pointerEvents: 'none' }}/>
                )}

                <div style={{ padding: '32px', display: 'flex', flexDirection: 'column', flex: 1, gap: '20px' }}>
                  {/* Top row: PASO number + time */}
                  <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start' }}>
                    <span style={{ fontFamily: "'DM Mono',monospace", fontSize: '10px', fontWeight: 700, letterSpacing: '0.15em', color: isStrategy ? NX.accent2 : NX.accent, textTransform: 'uppercase' }}>
                      PASO 0{i + 1}
                    </span>
                    <span style={{ fontFamily: "'DM Mono',monospace", fontSize: '10px', color: NX.muted, background: NX.bg4, padding: '3px 10px', borderRadius: '100px', border: `1px solid ${NX.border}` }}>⏱ {step.time}</span>
                  </div>

                  {/* Icon */}
                  <div style={{ display: 'flex', justifyContent: setupDone && isStrategy ? 'flex-start' : 'flex-start' }}>
                    <div style={{ opacity: locked ? 0.4 : 1, transition: 'transform 0.25s', transform: hovered ? 'scale(1.08)' : 'scale(1)' }}>
                      {STEP_ICONS_LG[i]}
                    </div>
                  </div>

                  {/* Title */}
                  <div style={{ flex: 1 }}>
                    <h3 style={{ fontSize: setupDone && isStrategy ? '28px' : '22px', fontWeight: 400, letterSpacing: '-0.02em', color: NX.text, margin: '0 0 10px', lineHeight: 1.2 }}>
                      {step.title}
                    </h3>
                    <p style={{ fontSize: '14px', color: NX.muted, margin: 0, lineHeight: 1.6, maxWidth: setupDone && isStrategy ? '500px' : '100%' }}>
                      {step.desc}
                    </p>
                  </div>

                  {/* CTA */}
                  <div>
                    <NxButton
                      variant={isStrategy && !locked ? 'accent' : locked ? 'ghost' : 'primary'}
                      full={!(setupDone && isStrategy)}
                      disabled={locked}
                      onClick={e => { e.stopPropagation(); if (!locked) { i === 3 ? startStrategy() : setActiveFlow(step.flow); } }}>
                      {locked ? t.locked : t.connect}
                    </NxButton>
                  </div>
                </div>
              </div>
            );
          })}
        </div>
      )}

      {/* All done state — Apple-minimal home. Layout differs on mobile vs desktop:
          - Desktop: greeting + Modo avanzado on top in a 2-col header, mascot + chat hero, then campaigns.
          - Mobile: chat card first (mascot inlined inside), Modo avanzado below it, then campaigns.
            The greeting still sits on top but compact. */}
      {allDone && (
        <div style={{ width:'100%', maxWidth:'1440px', alignSelf:'center', display:'flex', flexDirection:'column', gap:'18px' }}>
          {/* Greeting — solo en mobile. En desktop el saludo se muestra arriba de la
              mascota (dentro del AssistantFlow embedded, columna derecha) para que toda
              la composición quede alineada y prolija en una sola fila. */}
          {isMobile && (
            <div style={{ padding: '0', minWidth: 0 }}>
              <h2 style={{ fontSize: '17px', fontWeight:400, letterSpacing:'-0.02em', color:NX.text, margin:'0 0 4px', lineHeight:1.2 }}>
                {lang === 'en' ? 'Hi, welcome to ' : 'Hola, bienvenido a '}
                <span style={{ background:'linear-gradient(135deg, #8b5cf6, #a78bfa)', WebkitBackgroundClip:'text', WebkitTextFillColor:'transparent', backgroundClip:'text' }}>NexWall Corp AI</span>
                <span style={{ marginLeft:'6px' }}>👋</span>
              </h2>
              <p style={{ color: NX.muted, fontSize: '11px', margin:0, lineHeight:1.5 }}>
                {lang === 'en' ? 'Your marketing copilot powered by AI' : 'Tu copiloto de marketing con IA'}
              </p>
            </div>
          )}

          {/* Modo avanzado desactivado — flujo unificado vía asistente. */}

          {/* Assistant hero — on mobile we tell the assistant to inline the mascot inside
              the chat card instead of rendering it as a separate column. */}
          <AssistantFlow
            embedded
            mobileLayout={isMobile}
            lang={lang}
            defaultImageModel={defaultImageModel}
            onAdvancedMode={null}
            onComplete={onAssistantComplete}
            onClose={() => {}}
          />


          {/* (Active campaigns panel removed — already accessible via the left island
              "Mis estrategias" icon. No need to duplicate on the home dashboard.) */}
        </div>
      )}
    </div>
    </div>
  );
};

// ══════════════════════════════════════════
// QUALITY PICKER MODAL
// ══════════════════════════════════════════
const QualityPicker = ({ onSelect, onClose }) => {
  const [hovered, setHovered] = React.useState(null);
  const opts = [
    { key: 'premium',      model: 'gemini-3.1-flash-image-preview',     label: 'Premium',           badge: 'NexWall AI · Premium',   desc: 'Alta calidad fotorealista con tipografía nítida. Ideal para la mayoría de campañas.', icon: '\u26a1', color: NX.accent },
    { key: 'ultra',        model: 'gpt-image-2',                    label: 'Premium Ultra MAX', badge: 'NexWall AI · Ultra MAX', desc: 'Máxima calidad y detalle. Para campañas de alto impacto.',  icon: '\u2605', color: NX.accent2 },
  ];
  return (
    <div style={{ position: 'fixed', top: 0, left: 0, right: 0, bottom: 0, background: 'rgba(0,0,0,0.72)', backdropFilter: 'blur(8px)', display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 300 }}
      onClick={onClose}>
      <div style={{ background: NX.bg2, border: `1px solid ${NX.border}`, borderRadius: '20px', padding: '32px', maxWidth: '520px', width: '90%', animation: 'modalIn 0.2s ease' }}
        onClick={e => e.stopPropagation()}>
        <div style={{ textAlign: 'center', marginBottom: '28px' }}>
          {/* Animated icon */}
          <div style={{ position: 'relative', width: '100px', height: '100px', margin: '0 auto 16px', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
            {/* glow rings */}
            <svg viewBox="0 0 100 100" fill="none" style={{ position: 'absolute', width: '100px', height: '100px', animation: 'ringSpinSlow 14s linear infinite' }}>
              <circle cx="50" cy="50" r="46" stroke="url(#qpg1)" strokeWidth="1" strokeDasharray="5 9" style={{ animation: 'glowRing 2.5s ease-in-out infinite' }}/>
              <defs><linearGradient id="qpg1" x1="0" y1="0" x2="100" y2="100" gradientUnits="userSpaceOnUse"><stop stopColor="#8b5cf6"/><stop offset="1" stopColor="#8b5cf6"/></linearGradient></defs>
            </svg>
            <svg viewBox="0 0 100 100" fill="none" style={{ position: 'absolute', width: '80px', height: '80px', animation: 'ringSpinSlowR 9s linear infinite' }}>
              <circle cx="50" cy="50" r="46" stroke="url(#qpg2)" strokeWidth="0.8" strokeDasharray="2 12" style={{ animation: 'glowRing 3.2s 0.5s ease-in-out infinite' }}/>
              <defs><linearGradient id="qpg2" x1="0" y1="0" x2="100" y2="100" gradientUnits="userSpaceOnUse"><stop stopColor="#8b5cf6"/><stop offset="1" stopColor="#8b5cf6"/></linearGradient></defs>
            </svg>
            {/* nebula blob */}
            <div style={{ position: 'absolute', width: '90px', height: '90px', borderRadius: '50%', background: 'radial-gradient(circle, #8b5cf633 0%, #8b5cf622 50%, transparent 70%)', animation: 'nebulaGlow 2.8s ease-in-out infinite' }}/>
            {/* the NexWall mascot — eyes track the mouse */}
            <div style={{ position:'relative', zIndex:2 }}><Mascot size={84}/></div>
          </div>
          <h2 style={{ fontSize: '20px', fontWeight: 400, color: NX.text, margin: '0 0 6px', letterSpacing: '-0.02em' }}>Calidad de Imágenes</h2>
          <p style={{ color: NX.muted, fontSize: '13px', margin: 0 }}>Elige el nivel de IA para generar tus creativos</p>
        </div>
        <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '14px' }}>
          {opts.map(opt => (
            <div key={opt.key}
              onMouseEnter={() => setHovered(opt.key)}
              onMouseLeave={() => setHovered(null)}
              onClick={() => onSelect(opt.model)}
              style={{ background: hovered === opt.key ? NX.bg4 : NX.bg3, border: `1.5px solid ${hovered === opt.key ? opt.color : NX.border}`, borderRadius: '14px', padding: '20px', cursor: 'pointer', transition: 'all 0.2s', boxShadow: hovered === opt.key ? `0 0 24px ${opt.color}44` : 'none' }}>
              <div style={{ fontSize: '24px', marginBottom: '10px' }}>{opt.icon}</div>
              <div style={{ fontFamily: "'DM Mono',monospace", fontSize: '9px', fontWeight: 700, color: opt.color, letterSpacing: '0.1em', textTransform: 'uppercase', marginBottom: '4px' }}>{opt.badge}</div>
              <div style={{ fontSize: '15px', fontWeight: 500, color: NX.text, marginBottom: '8px' }}>{opt.label}</div>
              <div style={{ fontSize: '12px', color: NX.muted, lineHeight: 1.5 }}>{opt.desc}</div>
            </div>
          ))}
        </div>
        <p style={{ textAlign: 'center', fontSize: '11px', color: NX.muted, marginTop: '16px', marginBottom: 0 }}>Puedes cambiar la calidad en cada nueva campaña</p>
      </div>
    </div>
  );
};

// ══════════════════════════════════════════
// STRATEGY MODE PICKER — Asistente IA (default) vs Modo avanzado
// ══════════════════════════════════════════
// Replaces QualityPicker as the entry point when the user clicks "+ Nueva estrategia".
// Default path: AI Assistant (conversational, AI auto-decides). Escape hatch: "Modo avanzado"
// link → opens the existing QualityPicker → StrategyFlow path that power users already know.
const StrategyModePicker = ({ onPickAssistant, onPickAdvanced, onClose, lang='es' }) => {
  return (
    <div style={{ position:'fixed', inset:0, background:'rgba(0,0,0,0.78)', backdropFilter:'blur(10px)', zIndex:300, display:'flex', alignItems:'center', justifyContent:'center', padding:'24px' }}
      onClick={onClose}>
      <div onClick={e => e.stopPropagation()}
        style={{ background: NX.bg2, border:`1px solid ${NX.border2}`, borderRadius:'24px', padding:'40px 32px', width:'100%', maxWidth:'520px', textAlign:'center', animation:'modalIn 0.22s ease', boxShadow:'0 30px 80px rgba(10,6,16,0.8)' }}>
        {/* Living nebula avatar */}
        <div style={{ position:'relative', width:'130px', height:'130px', margin:'0 auto 22px' }}>
          <div style={{ position:'absolute', inset:'-20px', borderRadius:'50%', background:'radial-gradient(circle at 30% 40%, rgba(139,92,246,0.32), transparent 60%), radial-gradient(circle at 70% 60%, rgba(167,139,250,0.28), transparent 60%)', filter:'blur(16px)', animation:'nbShift 8s ease-in-out infinite' }}/>
          <svg viewBox="0 0 130 130" fill="none" style={{ position:'absolute', inset:0, width:'100%', height:'100%', animation:'ringSpinSlow 18s linear infinite' }}>
            <circle cx="65" cy="65" r="56" stroke="rgba(139,92,246,0.4)" strokeWidth="0.8" strokeDasharray="2 8"/>
          </svg>
          <svg viewBox="0 0 130 130" fill="none" style={{ position:'absolute', inset:0, width:'100%', height:'100%', animation:'ringSpinSlowR 12s linear infinite' }}>
            <circle cx="65" cy="65" r="42" stroke="rgba(167,139,250,0.32)" strokeWidth="0.6" strokeDasharray="1 6"/>
          </svg>
          <div style={{ position:'absolute', inset:0, display:'flex', alignItems:'center', justifyContent:'center' }}>
            <Mascot size={100}/>
          </div>
        </div>

        <h2 style={{ fontSize:'24px', fontWeight:300, color:NX.text, margin:'0 0 8px', letterSpacing:'-0.02em' }}>
          {lang === 'en' ? 'How do you want to launch?' : '¿Cómo quieres lanzar?'}
        </h2>
        <p style={{ color:NX.muted, fontSize:'13px', margin:'0 0 28px', lineHeight:1.55 }}>
          {lang === 'en'
            ? 'Let the AI guide you step by step, or jump into the full advanced editor.'
            : 'Deja que la IA te guíe paso a paso, o entra al editor avanzado.'}
        </p>

        <button onClick={onPickAssistant}
          style={{ width:'100%', padding:'18px 20px', background:NX.accentGrad, border:'none', borderRadius:'16px', cursor:'pointer', display:'flex', alignItems:'center', gap:'14px', textAlign:'left', boxShadow:'0 12px 36px rgba(139,92,246,0.32)', fontFamily:"'DM Sans',sans-serif", transition:'transform 0.18s' }}
          onMouseEnter={e => e.currentTarget.style.transform='translateY(-2px)'}
          onMouseLeave={e => e.currentTarget.style.transform='translateY(0)'}>
          <div style={{ width:'42px', height:'42px', borderRadius:'12px', background:'rgba(255,255,255,0.15)', display:'flex', alignItems:'center', justifyContent:'center', flexShrink:0 }}>
            <svg width="22" height="22" viewBox="0 0 24 24" fill="none"><path d="M12 2v3M12 19v3M4.93 4.93l2.12 2.12M16.95 16.95l2.12 2.12M2 12h3M19 12h3M4.93 19.07l2.12-2.12M16.95 7.05l2.12-2.12" stroke="#fff" strokeWidth="1.6" strokeLinecap="round"/><circle cx="12" cy="12" r="4" fill="#fff"/></svg>
          </div>
          <div style={{ flex:1 }}>
            <div style={{ fontSize:'15px', fontWeight:600, color:'#fff', marginBottom:'2px' }}>
              {lang === 'en' ? 'AI Assistant' : 'Asistente IA'}
              <span style={{ marginLeft:'8px', fontSize:'9px', fontWeight:700, padding:'2px 7px', borderRadius:'5px', background:'rgba(255,255,255,0.25)', letterSpacing:'0.06em' }}>NUEVO</span>
            </div>
            <div style={{ fontSize:'12px', color:'rgba(255,255,255,0.85)', lineHeight:1.4 }}>
              {lang === 'en' ? 'Chat with the AI — it picks the objective, country, format and more for you.' : 'Conversa con la IA — elige objetivo, país, formato y más por ti.'}
            </div>
          </div>
          <svg width="18" height="18" viewBox="0 0 24 24" fill="none" style={{ flexShrink:0 }}><path d="M9 6l6 6-6 6" stroke="#fff" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/></svg>
        </button>

        <button onClick={onPickAdvanced}
          style={{ width:'100%', marginTop:'12px', padding:'12px 16px', background:'transparent', border:`1px solid ${NX.border}`, borderRadius:'12px', cursor:'pointer', color:NX.muted, fontSize:'12px', fontFamily:"'DM Sans',sans-serif", display:'flex', alignItems:'center', justifyContent:'center', gap:'6px', transition:'all 0.15s' }}
          onMouseEnter={e => { e.currentTarget.style.color = NX.text; e.currentTarget.style.borderColor = NX.border2; }}
          onMouseLeave={e => { e.currentTarget.style.color = NX.muted; e.currentTarget.style.borderColor = NX.border; }}>
          <svg width="13" height="13" viewBox="0 0 16 16" fill="none"><path d="M3 8h10M3 4h10M3 12h6" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round"/></svg>
          {lang === 'en' ? 'Advanced mode (full editor)' : 'Modo avanzado (editor completo)'}
        </button>
      </div>
    </div>
  );
};

// ══════════════════════════════════════════
// ASSISTANT FLOW — conversational AI campaign builder
// ══════════════════════════════════════════
// A chat-style guided flow that collects the minimum info from the user, auto-decides
// objective / aspect ratio / language, and hands the prefilled answers off to StrategyFlow
// which runs the existing image+copy generation, review, and Meta publish pipeline.
//
// Why hand off to StrategyFlow instead of duplicating its review UI?
//   The image-grid review, copy editor, Facebook mockup, draft saving, Meta publish (with
//   SSE progress), pause/resume, etc. are ~3000 lines battle-tested code. Duplicating that
//   for the assistant would double maintenance and create UI drift. So the assistant is a
//   thin pre-collection layer + summary card; StrategyFlow takes over from image generation.

// Naive timezone → country mapping for "default to user's location" UX. We don't ask for
// geolocation permission (annoying); timezone is enough for ~95% of LATAM users.
const detectCountryFromTimezone = () => {
  try {
    const tz = (Intl.DateTimeFormat().resolvedOptions().timeZone || '').toString();
    const map = {
      'America/Santiago':'Chile','Pacific/Easter':'Chile',
      'America/Argentina/Buenos_Aires':'Argentina','America/Buenos_Aires':'Argentina','America/Argentina/Cordoba':'Argentina','America/Argentina/Mendoza':'Argentina',
      'America/Mexico_City':'México','America/Cancun':'México','America/Tijuana':'México','America/Monterrey':'México','America/Hermosillo':'México',
      'America/Lima':'Perú',
      'America/Bogota':'Colombia',
      'America/Caracas':'Venezuela',
      'America/La_Paz':'Bolivia',
      'America/Asuncion':'Paraguay',
      'America/Montevideo':'Uruguay',
      'America/Guayaquil':'Ecuador',
      'America/Panama':'Panamá',
      'America/Costa_Rica':'Costa Rica',
      'America/Guatemala':'Guatemala',
      'America/El_Salvador':'El Salvador',
      'America/Tegucigalpa':'Honduras',
      'America/Managua':'Nicaragua',
      'America/Santo_Domingo':'República Dominicana',
      'America/Havana':'Cuba',
      'America/Puerto_Rico':'Puerto Rico',
      'America/Sao_Paulo':'Brasil','America/Bahia':'Brasil','America/Recife':'Brasil','America/Fortaleza':'Brasil',
      'Europe/Madrid':'España','Atlantic/Canary':'España',
      'Europe/Lisbon':'Portugal',
      'Europe/London':'Reino Unido',
      'Europe/Paris':'Francia',
      'Europe/Berlin':'Alemania',
      'Europe/Rome':'Italia',
      'America/New_York':'Estados Unidos','America/Chicago':'Estados Unidos','America/Denver':'Estados Unidos','America/Los_Angeles':'Estados Unidos','America/Phoenix':'Estados Unidos','America/Anchorage':'Estados Unidos','America/Miami':'Estados Unidos',
      'America/Toronto':'Canadá','America/Vancouver':'Canadá',
    };
    return map[tz] || null;
  } catch { return null; }
};

// Heuristic objective inference. Backend is the source of truth, but a quick local guess
// gives instant feedback in the summary screen and avoids a wasted Claude call.
const inferObjectiveFromDesc = (desc, hasUrl) => {
  const s = (desc || '').toLowerCase();
  if (/\b(lead|cotiza|cotización|reserva|registro|registrar|formulario|inscríb|consulta|asesor|appointment|agend)\b/.test(s)) return 'leads';
  if (/\b(visit|tráfico|trafico|conoce|descubre|información|info|aprende|landing|website|web)\b/.test(s) && hasUrl) return 'trafico';
  if (/\b(mensaje|whatsapp|chatea|chatea|hablanos|escribe|conversa|dm)\b/.test(s)) return 'mensajes';
  if (/\b(compra|comprar|venta|vender|pedido|carrito|tienda|ecommerce|envío|envio|stock|talla|precio|\$|usd|clp|mxn|ars)\b/.test(s)) return 'ventas';
  if (/\b(marca|conocimiento|brand|reconocimiento|awareness|recordación|recordacion)\b/.test(s)) return 'reconocimiento';
  return 'ventas';
};

// Currency-aware budget chips. Meta does NOT allow changing the ad account currency, so the
// assistant must always show numbers in whatever currency the user's account is configured
// in. Showing $3 USD as a "starter" chip to a CLP account would be invalid (below minimum)
// and confusing. Each currency has its own minimum daily spend per Meta rules + a sensible
// chip ladder around it.
const CURRENCY_MIN_DAILY = {
  USD: 5, EUR: 5, GBP: 5, CAD: 5, AUD: 5,
  CLP: 2000, MXN: 100, ARS: 2000, COP: 20000, PEN: 20, BRL: 25, UYU: 200, BOB: 35,
  PYG: 30000, CRC: 3000, GTQ: 40, HNL: 120, NIO: 180, DOP: 300, VES: 200, CUP: 1200,
  JPY: 500, KRW: 5000, VND: 100000,
};
const BUDGET_CHIPS = {
  USD: [5, 10, 20, 50, 100], EUR: [5, 10, 20, 50, 100], GBP: [5, 10, 20, 50, 100],
  CAD: [5, 10, 20, 50, 100], AUD: [5, 10, 20, 50, 100],
  CLP: [2000, 5000, 10000, 20000, 50000],
  MXN: [100, 200, 500, 1000, 2000],
  ARS: [2000, 5000, 10000, 20000, 50000],
  COP: [20000, 50000, 100000, 200000, 500000],
  PEN: [20, 50, 100, 200, 500],
  BRL: [25, 50, 100, 200, 500],
  UYU: [200, 500, 1000, 2000, 5000],
  BOB: [35, 70, 150, 300, 700],
  PYG: [30000, 70000, 150000, 300000, 700000],
  CRC: [3000, 7000, 15000, 30000, 70000],
  GTQ: [40, 100, 200, 500, 1000],
  HNL: [120, 300, 600, 1200, 3000],
  NIO: [180, 400, 800, 1600, 4000],
  DOP: [300, 700, 1500, 3000, 7000],
  VES: [200, 500, 1000, 2000, 5000],
  CUP: [1200, 2500, 5000, 10000, 25000],
  JPY: [500, 1000, 2500, 5000, 10000],
  KRW: [5000, 10000, 25000, 50000, 100000],
  VND: [100000, 250000, 500000, 1000000, 2500000],
};
const formatBudget = (amount, currency) => {
  // Locale-aware grouping. CLP/JPY/etc don't use decimals; pass minimumFractionDigits=0.
  try { return new Intl.NumberFormat(undefined, { style:'currency', currency, maximumFractionDigits: 0 }).format(amount); }
  catch { return `${currency} ${amount}`; }
};

const AssistantFlow = ({ onClose, onComplete, lang='es', defaultImageModel='gemini-3.1-flash-image-preview', embedded=false, onAdvancedMode=null, mobileLayout=false, draftCampaign=null }) => {
  // Conversation state machine. Each step renders ONE question + ONE input widget. The user's
  // answer advances to the next step. A summary screen at the end auto-decides the objective
  // and lets the user kick off generation (which transitions to handoff = render StrategyFlow).
  const [step, setStep] = React.useState(0);
  const [productDesc, setProductDesc] = React.useState('');
  const [productFile, setProductFile] = React.useState(null);
  const [productPreviewUrl, setProductPreviewUrl] = React.useState(null);
  // Imagen de referencia opcional secundaria (máx 2 total). Se pasa al StrategyFlow
  // vía assistantPrefill — el StrategyFlow la hidrata como extraProductFiles[0].
  const [extraProductFile, setExtraProductFile] = React.useState(null);
  const [extraProductPreviewUrl, setExtraProductPreviewUrl] = React.useState(null);
  const [productUrl, setProductUrl] = React.useState('');
  // Destination of the click. Three modes:
  //   'web'         → URL → landing page (default)
  //   'messaging'   → WhatsApp / IG DM / Messenger (click-to-chat)
  //   'existing_ig' → boost an existing Instagram post (no new creative generated)
  const [destMode, setDestMode] = React.useState('web');
  const [destChannels, setDestChannels] = React.useState({ whatsapp: false, instagram: false, messenger: false });
  // IG post picker state (only used when destMode === 'existing_ig'). The grid + selection
  // mirrors the StrategyFlow advanced editor — same /api/meta/ig-posts endpoint.
  const [igPosts, setIgPosts] = React.useState([]);
  const [igPostsLoading, setIgPostsLoading] = React.useState(false);
  const [igPostsError, setIgPostsError] = React.useState(null);
  const [selectedIgPost, setSelectedIgPost] = React.useState(null);
  const fetchIgPosts = React.useCallback(async () => {
    if (igPostsLoading || igPosts.length > 0) return;
    setIgPostsLoading(true); setIgPostsError(null);
    try {
      const bizId = localStorage.getItem('nw_active_biz_id') || '1';
      const res = await authFetch(`/api/meta/ig-posts?biz_id=${encodeURIComponent(bizId)}&limit=18`);
      if (!res.ok) {
        const err = await res.json().catch(() => ({}));
        // Permission #10 for IG media means the instagram_basic scope is not declared in
        // the Meta App Dashboard yet. The end-user can't fix this — only the NexWall admin can.
        const isPermissionError = err.code === 10 || /\(#10\)/.test(err.error || '');
        if (isPermissionError) {
          throw new Error(err.error || t(
            'La función de promocionar publicaciones de Instagram aún no está habilitada. Estamos esperando la aprobación del permiso instagram_basic en Meta.',
            'The "boost an Instagram post" feature is not yet enabled. We are waiting for instagram_basic to be approved on Meta.'
          ));
        }
        throw new Error(err.error || 'No se pudieron cargar las publicaciones');
      }
      const data = await res.json();
      setIgPosts(data.posts || []);
    } catch (e) {
      setIgPostsError(e.message || 'Error al cargar publicaciones');
    }
    setIgPostsLoading(false);
  }, [igPostsLoading, igPosts.length]);
  const [country, setCountry] = React.useState(() => detectCountryFromTimezone() || '');
  const [dailyBudget, setDailyBudget] = React.useState('');
  const [imageModel, setImageModel] = React.useState(defaultImageModel);
  const [imageCount, setImageCount] = React.useState(5);
  // Saldo de créditos del usuario — se usa en el step de calidad de imagen para mostrar
  // cuántas imágenes puede sacar con cada tier (Premium = 25 cr, Ultra MAX = 100 cr).
  // Reutiliza cache compartido (window.getCreditsBalance) para evitar duplicar la llamada
  // con CreditsPill y TopBar — el dashboard pasa de 3 calls paralelas a 1 sola.
  const [creditsState, setCreditsState] = React.useState(null);
  React.useEffect(() => {
    let cancelled = false;
    (async () => {
      if (!window.getCreditsBalance) return;
      const j = await window.getCreditsBalance(typeof API !== 'undefined' ? API : '');
      if (!cancelled && j) setCreditsState(j);
    })();
    return () => { cancelled = true; };
  }, []);
  // Generation mode: 'ai' (AI generates with Gemini/gpt-image-2) | 'uploaded' (user already
  // has their own creatives — saves credits/tokens since no AI call is made for images).
  const [genMode, setGenMode] = React.useState('ai');
  const [ownImages, setOwnImages] = React.useState([]); // [{ file, previewUrl }]
  const [handoff, setHandoff] = React.useState(false);

  // ─── Voice note input (step 1 textarea) ─────────────────────────────────────
  // El user puede dictar la descripción del producto con el micrófono o subir un audio
  // ya grabado (mp3/m4a/webm). Whisper de OpenAI transcribe → llena el textarea.
  // Costo: ~$0.006/min — no se cobran créditos al usuario, es un input helper.
  const [isRecording, setIsRecording] = React.useState(false);
  const [recordingSecs, setRecordingSecs] = React.useState(0);
  const [isTranscribing, setIsTranscribing] = React.useState(false);
  const [audioError, setAudioError] = React.useState(null);
  const mediaRecorderRef = React.useRef(null);
  const audioChunksRef = React.useRef([]);
  const recordingTimerRef = React.useRef(null);

  const sendAudioForTranscription = React.useCallback(async (blob, filename) => {
    setIsTranscribing(true); setAudioError(null);
    try {
      const fd = new FormData();
      fd.append('audio', blob, filename || 'note.webm');
      fd.append('language', language || 'es');
      const token = localStorage.getItem('nw_token');
      const res = await fetch(`${API}/api/transcribe-audio`, {
        method: 'POST',
        body: fd,
        headers: { Authorization: `Bearer ${token}` },
      });
      const data = await res.json();
      if (!res.ok || !data.text) throw new Error(data.error || 'Transcripción falló');
      // Append (con espacio si ya hay texto) para que el user pueda dictar varias notas.
      setProductDesc(prev => prev ? `${prev.trim()} ${data.text}` : data.text);
    } catch (e) {
      setAudioError(e.message || 'Error al transcribir');
    }
    setIsTranscribing(false);
  }, [language]);

  const startRecording = React.useCallback(async () => {
    setAudioError(null);
    if (!navigator.mediaDevices?.getUserMedia) {
      setAudioError(t('Tu navegador no soporta grabación de audio', 'Your browser does not support audio recording'));
      return;
    }
    try {
      const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
      const mr = new MediaRecorder(stream, { mimeType: MediaRecorder.isTypeSupported('audio/webm') ? 'audio/webm' : '' });
      audioChunksRef.current = [];
      mr.ondataavailable = (ev) => { if (ev.data && ev.data.size > 0) audioChunksRef.current.push(ev.data); };
      mr.onstop = () => {
        const blob = new Blob(audioChunksRef.current, { type: mr.mimeType || 'audio/webm' });
        // Detener todas las pistas para que el navegador no muestre el indicador de grabación.
        stream.getTracks().forEach(t => t.stop());
        if (blob.size > 1024) sendAudioForTranscription(blob, `note.webm`);
        else setAudioError(t('La grabación fue muy corta', 'Recording was too short'));
      };
      mr.start();
      mediaRecorderRef.current = mr;
      setIsRecording(true);
      setRecordingSecs(0);
      recordingTimerRef.current = setInterval(() => setRecordingSecs(s => s + 1), 1000);
    } catch (e) {
      setAudioError(t('No se pudo acceder al micrófono', 'Could not access the microphone') + (e?.message ? `: ${e.message}` : ''));
    }
  }, [sendAudioForTranscription]);

  const stopRecording = React.useCallback(() => {
    const mr = mediaRecorderRef.current;
    if (mr && mr.state === 'recording') mr.stop();
    if (recordingTimerRef.current) { clearInterval(recordingTimerRef.current); recordingTimerRef.current = null; }
    setIsRecording(false);
  }, []);

  React.useEffect(() => () => {
    // Cleanup al desmontar — evita que un timer o stream queden vivos.
    if (recordingTimerRef.current) clearInterval(recordingTimerRef.current);
    const mr = mediaRecorderRef.current;
    if (mr && mr.state === 'recording') mr.stop();
  }, []);


  // Session resume detection — escanea localStorage por creativos ya generados (imágenes
  // o copies) del bizId activo para que cuando el usuario presiona Atrás aquí pueda elegir:
  //   1) Continuar con la configuración actual (re-hidrata el StrategyFlow desde el cache)
  //   2) Comenzar nueva publicación (limpia el cache y empieza limpio)
  const [cachedSession, setCachedSession] = React.useState(null);
  const scanCachedSession = React.useCallback(() => {
    try {
      const bizId = localStorage.getItem('nw_active_biz_id') || '1';
      const imgPrefix = `nw_images_cache_v1_${bizId}_`;
      const imgKeys = Object.keys(localStorage).filter(k => k.startsWith(imgPrefix));
      let best = null;
      for (const key of imgKeys) {
        try {
          const data = JSON.parse(localStorage.getItem(key) || 'null');
          if (!data || !Array.isArray(data.generatedImages)) continue;
          const ai = data.generatedImages.filter(i => !i.isOriginal && i.previewUrl);
          if (ai.length === 0) continue;
          if (!best || (data.ts || 0) > (best.ts || 0)) best = { key, ...data, imgCount: ai.length };
        } catch {}
      }
      if (!best) { setCachedSession(null); return; }
      // Recover copy count from the matching nw_copies_cache key (same hash tail).
      const tail = best.key.replace(imgPrefix, '');
      const copiesKey = `nw_copies_cache_v3_${bizId}_${tail}`;
      let copyCount = 0;
      try {
        const copyData = JSON.parse(localStorage.getItem(copiesKey) || 'null');
        if (copyData && Array.isArray(copyData.copies)) copyCount = copyData.copies.length;
      } catch {}
      setCachedSession({ ...best, copyCount });
    } catch { setCachedSession(null); }
  }, []);
  React.useEffect(() => { scanCachedSession(); }, [scanCachedSession]);
  // Re-escanear cuando StrategyFlow notifica que el cache fue limpiado (publish con éxito o
  // borrado manual desde otra pantalla) — así el banner "Tienes creativos sin publicar"
  // desaparece sin necesidad de recargar la página.
  React.useEffect(() => {
    const handler = () => scanCachedSession();
    window.addEventListener('nx-creatives-cache-cleared', handler);
    return () => window.removeEventListener('nx-creatives-cache-cleared', handler);
  }, [scanCachedSession]);

  const resumeSession = () => {
    if (!cachedSession) return;
    if (cachedSession.productDesc) setProductDesc(cachedSession.productDesc);
    setHandoff(true);
  };
  const startFreshSession = () => {
    if (!window.confirm(t('¿Borrar la configuración actual y empezar una nueva publicación?', 'Discard current setup and start a new publication?'))) return;
    try {
      const bizId = localStorage.getItem('nw_active_biz_id') || '1';
      const imgPrefix = `nw_images_cache_v1_${bizId}_`;
      const copyPrefix = `nw_copies_cache_v3_${bizId}_`;
      Object.keys(localStorage).filter(k => k.startsWith(imgPrefix) || k.startsWith(copyPrefix)).forEach(k => localStorage.removeItem(k));
    } catch {}
    setProductDesc('');
    setProductFile(null);
    setProductPreviewUrl(null);
    setProductUrl('');
    setCountry(detectCountryFromTimezone() || '');
    setDailyBudget('');
    setImageCount(5);
    setGenMode('ai');
    setOwnImages([]);
    setHandoff(false);
    setCachedSession(null);
  };
  const fileRef = React.useRef(null);
  const ownImagesInputRef = React.useRef(null);

  // URL → description auto-fill. Reuses /api/scrape-product (same endpoint the advanced
  // editor uses). Only enabled when productUrl looks like a real URL.
  const [scraping, setScraping] = React.useState(false);
  const [scrapeError, setScrapeError] = React.useState(null);
  const isValidUrl = (() => {
    const u = productUrl.trim();
    if (!u) return false;
    try { const p = new URL(u.startsWith('http') ? u : `https://${u}`); return !!p.hostname && p.hostname.includes('.'); }
    catch { return false; }
  })();
  const fillDescFromUrl = async () => {
    if (!isValidUrl || scraping) return;
    setScraping(true); setScrapeError(null);
    try {
      const url = productUrl.trim();
      const finalUrl = url.startsWith('http') ? url : `https://${url}`;
      const res = await authFetch('/api/scrape-product', {
        method: 'POST',
        body: JSON.stringify({ url: finalUrl, language }),
      });
      const data = await res.json();
      if (res.ok && data.description) {
        setProductDesc(data.description);
      } else {
        setScrapeError(data.error || (lang === 'en' ? 'Could not read that URL' : 'No se pudo leer esa URL'));
      }
    } catch {
      setScrapeError(lang === 'en' ? 'Connection error — check the URL' : 'Error de conexión — verifica la URL');
    } finally {
      setScraping(false);
    }
  };

  // Currency comes from the user's Meta ad account — Meta does NOT allow changing it, so
  // the budget chips MUST be in that currency from the start. Try localStorage first
  // (instant paint), then refresh from the backend on mount.
  const [adCurrency, setAdCurrency] = React.useState(() => {
    try {
      const bizId = localStorage.getItem('nw_active_biz_id') || '1';
      const cached = JSON.parse(localStorage.getItem(`nw_assets_${bizId}`) || 'null');
      return cached?.ad_account_currency || 'USD';
    } catch { return 'USD'; }
  });
  // The WhatsApp number connected to the user's FB Page (legacy `page.whatsapp_number`).
  // Surfaced under the WhatsApp destination chip so the user sees which number their ad
  // will route to BEFORE picking it. Falls back to localStorage cache for instant paint.
  const [pageWhatsAppNumber, setPageWhatsAppNumber] = React.useState(() => {
    try {
      const bizId = localStorage.getItem('nw_active_biz_id') || '1';
      const cached = JSON.parse(localStorage.getItem(`nw_assets_${bizId}`) || 'null');
      return cached?.page_whatsapp_number || null;
    } catch { return null; }
  });
  React.useEffect(() => {
    const bizId = localStorage.getItem('nw_active_biz_id') || '1';
    authFetch(`/api/meta/assets?biz_id=${encodeURIComponent(bizId)}`)
      .then(r => r.ok ? r.json() : null)
      .then(data => {
        if (data?.ad_account_currency) {
          setAdCurrency(data.ad_account_currency);
          try { localStorage.setItem(`nw_assets_${bizId}`, JSON.stringify(data)); } catch {}
        }
        if (data?.page_whatsapp_number) setPageWhatsAppNumber(data.page_whatsapp_number);
      })
      .catch(() => {});
  }, []);
  const minDailyForCurrency = CURRENCY_MIN_DAILY[adCurrency] || 5;
  const budgetChips = BUDGET_CHIPS[adCurrency] || BUDGET_CHIPS.USD;

  // Idioma del CONTENIDO de la campaña (textos + imágenes generadas) — INDEPENDIENTE del
  // idioma de la UI del dashboard (`nw_lang`). El usuario puede tener la app en español
  // pero crear campañas en inglés sin que cambie su interfaz.
  // Resolución:
  //   1) `nw_content_lang` (preferencia guardada por el usuario al elegir antes).
  //   2) `nw_lang` (idioma actual de la UI — default razonable la primera vez).
  //   3) navigator.language (autodetect del browser).
  const [language, setLanguage] = React.useState(() => {
    try {
      const stored = localStorage.getItem('nw_content_lang');
      if (stored === 'es' || stored === 'en') return stored;
    } catch {}
    try {
      const uiLang = localStorage.getItem('nw_lang');
      if (uiLang === 'es' || uiLang === 'en') return uiLang;
    } catch {}
    try {
      const navLang = (navigator.language || navigator.userLanguage || '').toLowerCase();
      if (navLang.startsWith('en')) return 'en';
      if (navLang.startsWith('es')) return 'es';
      if (['pt','it','gl','ca'].some(p => navLang.startsWith(p))) return 'es';
    } catch {}
    return lang === 'en' ? 'en' : 'es';
  });
  // Persistir SOLO en `nw_content_lang` — nunca tocar `nw_lang` (ese es el idioma de la UI).
  React.useEffect(() => {
    try { localStorage.setItem('nw_content_lang', language); } catch {}
  }, [language]);
  // `t()` traduce los TEXTOS DE LA UI del asistente (pasos, botones, labels).
  // Usa `lang` (idioma global de la app, viene del navegador / TopBar) — NO `language`
  // (que es el idioma del CONTENIDO de la campaña, controlado por el chip ES/EN).
  // Cambiar el chip solo afecta los textos+imágenes generados; la interfaz sigue en el
  // idioma con el que el usuario abrió el dashboard.
  const t = (es, en) => (lang === 'en' ? en : es);

  // Heurística de detección de idioma del CONTENIDO escrito por el usuario.
  // Devuelve 'es' o 'en' según predominen marcadores típicos. Conservadora: si no detecta
  // marcadores claros, retorna null (no traducir, dejar pasar tal cual).
  const detectLang = (text) => {
    if (!text || text.trim().length < 8) return null;
    const t = ` ${text.toLowerCase()} `;
    // Caracteres exclusivos del español → señal fuerte (ñ, á, é, í, ó, ú, ¿, ¡)
    if (/[ñáéíóú¿¡]/i.test(text)) return 'es';
    const esWords = [' que ', ' el ', ' la ', ' los ', ' las ', ' de ', ' del ', ' con ', ' para ', ' por ', ' en ', ' un ', ' una ', ' es ', ' son ', ' está ', ' lo ', ' le ', ' se ', ' su ', ' sus ', ' y ', ' o ', ' pero ', ' más ', ' también ', ' tu ', ' tus '];
    const enWords = [' the ', ' a ', ' an ', ' and ', ' or ', ' but ', ' with ', ' for ', ' to ', ' of ', ' in ', ' on ', ' is ', ' are ', ' was ', ' were ', ' this ', ' that ', ' you ', ' your ', ' our ', ' we '];
    const count = (arr) => arr.reduce((acc, w) => acc + (t.split(w).length - 1), 0);
    const esCount = count(esWords);
    const enCount = count(enWords);
    if (esCount === 0 && enCount === 0) return null;
    return esCount >= enCount ? 'es' : 'en';
  };
  const [translatingDesc, setTranslatingDesc] = React.useState(false);

  // Objective inference per destination:
  //   existing_ig → 'interaccion' (the only Meta objective that allows boosting an existing post)
  //   messaging   → 'mensajes' (forces OUTCOME_ENGAGEMENT + destination_type per channel)
  //   web         → infer from description / URL presence
  const inferredObjective = destMode === 'existing_ig'
    ? 'interaccion'
    : destMode === 'messaging' && Object.values(destChannels).some(Boolean)
      ? 'mensajes'
      : inferObjectiveFromDesc(productDesc, !!productUrl);
  const objectiveLabel = ({
    'ventas': t('Ventas', 'Sales'),
    'leads': t('Leads', 'Leads'),
    'trafico': t('Tráfico web', 'Website traffic'),
    'mensajes': t('Mensajes', 'Messages'),
    'reconocimiento': t('Reconocimiento', 'Brand awareness'),
  })[inferredObjective] || 'Ventas';

  // Accepts a File (from input change OR drag-drop). Filters non-image files silently.
  // Si ya hay primary, el siguiente upload va al slot 2 (extra). Si ya hay 2 imágenes,
  // el nuevo upload reemplaza la extra (el user puede borrar para volver atrás).
  const acceptReferenceImage = (f) => {
    if (!f || !/^image\//.test(f.type)) return;
    const r = new FileReader();
    if (!productFile) {
      setProductFile(f);
      r.onload = ev => setProductPreviewUrl(ev.target.result);
    } else {
      setExtraProductFile(f);
      r.onload = ev => setExtraProductPreviewUrl(ev.target.result);
    }
    r.readAsDataURL(f);
  };
  const onUpload = (e) => acceptReferenceImage(e.target.files?.[0]);

  // Multi-file picker for "I have my own creatives" path. Each file gets a local preview URL
  // (uploaded to Cloudinary later by StrategyFlow's submitUserUploadedImages).
  const acceptOwnImages = (filesLike) => {
    const files = Array.from(filesLike || []).filter(f => /^image\//.test(f.type));
    if (files.length === 0) return;
    const next = files.map(file => ({ file, previewUrl: URL.createObjectURL(file) }));
    setOwnImages(prev => [...prev, ...next]);
  };
  const onOwnUpload = (e) => acceptOwnImages(e.target.files);

  // Drag/drop visual state — used by both upload zones to highlight while a file is hovering.
  const [dragOverZone, setDragOverZone] = React.useState(null); // 'reference' | 'own' | null
  const dragHandlers = (zone, accept) => ({
    onDragOver: (e) => { e.preventDefault(); e.stopPropagation(); setDragOverZone(zone); },
    onDragEnter: (e) => { e.preventDefault(); e.stopPropagation(); setDragOverZone(zone); },
    onDragLeave: (e) => { e.preventDefault(); e.stopPropagation(); if (e.currentTarget.contains(e.relatedTarget)) return; setDragOverZone(null); },
    onDrop: (e) => { e.preventDefault(); e.stopPropagation(); setDragOverZone(null); accept(e.dataTransfer?.files); },
  });
  const removeOwnImage = (idx) => {
    setOwnImages(prev => {
      const removed = prev[idx];
      if (removed?.previewUrl?.startsWith('blob:')) URL.revokeObjectURL(removed.previewUrl);
      return prev.filter((_, i) => i !== idx);
    });
  };

  // After hand-off, render StrategyFlow with the prefilled data so the existing review/launch
  // UI takes over (image grid → copies → Facebook mockup → publish). When the user said they
  // already have creatives we pass them as `userImages` so StrategyFlow skips AI generation
  // and just uploads them to Cloudinary — zero token/credit cost on images.
  // In embedded mode, closing StrategyFlow comes back to the assistant config (not parent close).
  // `onRestart` lets StrategyFlow's "Borrar todo" reset the assistant to its starting screen
  // (step 0) when entered via the assistant flow — wipes EVERY field. Used only by the
  // destructive "discard all" action.
  const restartAssistant = React.useCallback(() => {
    setStep(0);
    setProductDesc('');
    setProductFile(null);
    setProductPreviewUrl(null);
    setProductUrl('');
    setCountry(detectCountryFromTimezone() || '');
    setDailyBudget('');
    setImageModel(defaultImageModel);
    setImageCount(5);
    setGenMode('ai');
    setOwnImages([]);
    setHandoff(false);
  }, [defaultImageModel]);
  // `onBackToAssistant` is the non-destructive variant: returns to the conversational flow
  // KEEPING all the data the user already typed (description, country, budget, IG post, etc.)
  // so they can tweak one field without having to re-enter everything. Lands on step 0 so
  // they see the description first, but every other state stays intact.
  const goBackToAssistant = React.useCallback(() => {
    setHandoff(false);
    setStep(0);
  }, []);

  // Re-launch hydration: when AssistantFlow receives a draftCampaign (clicked "Reutilizar"
  // on Mis Estrategias), skip the question-by-question flow and go DIRECTLY into the handoff
  // (StrategyFlow). StrategyFlow has a complete draft hydration (line ~3294) that restores
  // images, copies, strategy, and lands on step 5 (preview/launch) — much more complete
  // than what we could reproduce by re-asking questions in the assistant.
  const [draftHydrated, setDraftHydrated] = React.useState(false);
  React.useEffect(() => {
    if (!draftCampaign || draftHydrated) return;
    setDraftHydrated(true);
    setHandoff(true);
  }, [draftCampaign, draftHydrated]);

  if (handoff) {
    // Re-launch path: pass draftCampaign so StrategyFlow restores everything (images, copies,
    // strategy, etc.) and lands on step 5. Normal forward path: pass assistantPrefill so
    // StrategyFlow runs image generation from the assistant's collected answers.
    return (
      <StrategyFlow
        lang={lang}
        imageModel={imageModel}
        onClose={() => { if (embedded) setHandoff(false); else onClose && onClose(); }}
        onComplete={onComplete}
        draftCampaign={draftCampaign || null}
        assistantPrefill={draftCampaign ? null : {
          objective: inferredObjective,
          productDesc, productUrl,
          country, language,
          imageCount: genMode === 'uploaded' ? ownImages.length : imageCount,
          aspectRatio: '4:5',
          dailyBudget,
          productFile, productPreviewUrl,
          extraProductFile, extraProductPreviewUrl,
          destMode,
          destChannels,
          // Boost-an-existing-IG-post mode: hand over the selected post so the StrategyFlow
          // step 0 picker shows up pre-filled (no extra click). The advanced editor's
          // promotionMode='existing_ig' branch will then drive the publish payload.
          promotionMode: destMode === 'existing_ig' ? 'existing_ig' : 'new',
          selectedIgPost: destMode === 'existing_ig' ? selectedIgPost : null,
          userImages: genMode === 'uploaded' ? ownImages : null,
          onRestart: restartAssistant,
          onBackToAssistant: goBackToAssistant,
        }}
      />
    );
  }

  // ── Step config ── each step has: question, input renderer, and a "can advance" check.
  // Steps (gen mode picked early so we know whether to ask for reference photo):
  //   0 description + optional website URL
  //   1 generation mode (AI vs own images)
  //   2 country
  //   3 budget
  //   4 product photo (optional, AI ONLY — auto-skipped when genMode='uploaded')
  //   5 quality (AI only) OR own images upload (uploaded only)
  //   6 image count + summary (AI only) OR summary alone (uploaded only)
  const totalSteps = 7;

  const StepIndicator = (
    <div style={{ display:'flex', justifyContent:'center', gap:'5px', marginBottom:'18px' }}>
      {Array.from({ length: totalSteps }).map((_, i) => (
        <div key={i} style={{ width: i === step ? '22px' : '6px', height:'6px', borderRadius:'3px', background: i <= step ? NX.accent : 'rgba(139,92,246,0.18)', transition:'all 0.3s' }}/>
      ))}
    </div>
  );

  // Apple-style minimal: just a soft radial glow behind the mascot for depth — no orbits,
  // no dashed rings, no roaming particles. The mascot itself is the focus.
  const NebulaAvatar = (
    <div style={{ position:'relative', width:'260px', height:'260px', flexShrink:0, display:'flex', alignItems:'center', justifyContent:'center' }}>
      <div style={{ position:'absolute', inset:'-30px', borderRadius:'50%', background:'radial-gradient(circle at 50% 50%, rgba(139,92,246,0.42) 0%, rgba(91,142,240,0.18) 35%, rgba(91,142,240,0) 65%)', filter:'blur(24px)' }}/>
      <div style={{ position:'relative', zIndex:2 }}><Mascot size={240}/></div>
    </div>
  );

  // On mobile (mobileLayout), the mascot sits inline next to the question heading + sub
  // (saves vertical space, gives more room for the textarea). On desktop the heading is
  // centered as before; the mascot lives in its own column to the left.
  const Question = ({ q, sub }) => (
    <div style={{ marginBottom:'18px', display: mobileLayout ? 'flex' : 'block', alignItems:'center', gap:'14px', textAlign: mobileLayout ? 'left' : 'center' }}>
      {mobileLayout && (
        <div style={{ position:'relative', width:'82px', height:'82px', flexShrink:0 }}>
          <div style={{ position:'absolute', inset:'-10px', borderRadius:'50%', background:'radial-gradient(circle at 50% 50%, rgba(139,92,246,0.45) 0%, rgba(91,142,240,0.18) 40%, rgba(91,142,240,0) 70%)', filter:'blur(12px)' }}/>
          <div style={{ position:'relative', zIndex:2 }}><Mascot size={82}/></div>
        </div>
      )}
      <div style={{ flex:1, minWidth:0 }}>
        <h2 style={{ fontSize: mobileLayout ? '17px' : '24px', fontWeight: mobileLayout ? 500 : 300, color:NX.text, margin:'0 0 6px', letterSpacing:'-0.02em', lineHeight:1.25 }}>{q}</h2>
        {sub && <p style={{ color:NX.muted, fontSize:'12px', margin:0, lineHeight:1.5 }}>{sub}</p>}
      </div>
    </div>
  );

  // Render the active question + answer widget
  let body = null;
  let canAdvance = false;
  let primaryLabel = t('Continuar', 'Continue');
  // Smart advance that skips steps that don't apply per the current path:
  //   • Boosting an existing IG post → skip gen mode (1), foto referencia (4) y quality/upload (5)
  //     because the post ya tiene su imagen y copy.
  //   • genMode='uploaded' → skip foto referencia (4) — no aplica cuando el usuario sube
  //     sus propias creatividades.
  const SKIP_BOOST_STEPS = new Set([1, 4, 5]);
  const shouldSkipStep = (s) => {
    if (destMode === 'existing_ig' && SKIP_BOOST_STEPS.has(s)) return true;
    if (genMode === 'uploaded' && s === 4) return true;
    return false;
  };
  const advanceStep = () => {
    setStep(s => {
      let next = s + 1;
      while (next < totalSteps && shouldSkipStep(next)) next += 1;
      return next;
    });
  };
  const retreatStep = () => {
    setStep(s => {
      let prev = s - 1;
      while (prev > 0 && shouldSkipStep(prev)) prev -= 1;
      return Math.max(0, prev);
    });
  };
  let onPrimary = advanceStep;
  let secondaryLabel = null;
  let onSecondary = null;

  if (step === 0) {
    const hasMessagingPick = Object.values(destChannels).some(Boolean);
    // Per-mode validation:
    //   web         → URL is optional (we accept campaigns without a website)
    //   messaging   → at least 1 channel must be picked
    //   existing_ig → an IG post must be selected
    const destOk = destMode === 'web'         ? true
                 : destMode === 'messaging'   ? hasMessagingPick
                 : destMode === 'existing_ig' ? !!selectedIgPost
                 : false;
    // For boost-post mode the user does not need to write a description (we use the post's
    // own caption downstream), so we relax the description requirement.
    const descOk = destMode === 'existing_ig' ? true : productDesc.trim().length >= 6;
    canAdvance = descOk && destOk && !translatingDesc;

    // Si la descripción está en otro idioma que el seleccionado, traducirla antes de avanzar.
    // Solo aplica para modo web/messaging (boost no usa descripción del usuario).
    onPrimary = async () => {
      if (translatingDesc) return;
      const desc = productDesc.trim();
      if (destMode !== 'existing_ig' && desc.length >= 8) {
        const detected = detectLang(desc);
        if (detected && detected !== language) {
          setTranslatingDesc(true);
          try {
            const res = await authFetch('/api/translate-text', {
              method: 'POST',
              body: JSON.stringify({ text: desc, target_lang: language }),
            });
            if (res.ok) {
              const data = await res.json();
              if (data?.text) setProductDesc(data.text);
            }
          } catch {}
          setTranslatingDesc(false);
        }
      }
      advanceStep();
    };
    if (translatingDesc) {
      primaryLabel = t('Traduciendo…', 'Translating…');
    }
    body = (<>
      {/* Resume banner — aparece cuando hay creativos cacheados de una sesión previa.
          El usuario elige: continuar con esa configuración, o empezar limpio. */}
      {cachedSession && cachedSession.productDesc && !productDesc && (
        <div style={{
          marginBottom: '14px',
          padding: '14px 16px',
          background: 'rgba(139,92,246,0.08)',
          border: `1px solid ${NX.border2}`,
          borderRadius: '12px',
          display: 'flex', flexDirection: 'column', gap: '10px',
        }}>
          <div style={{ display: 'flex', alignItems: 'flex-start', gap: '10px' }}>
            <svg width="18" height="18" viewBox="0 0 20 20" fill="none" style={{ flexShrink: 0, marginTop: '2px' }}>
              <circle cx="10" cy="10" r="8" stroke={NX.accent2} strokeWidth="1.5"/>
              <path d="M10 6v4l3 2" stroke={NX.accent2} strokeWidth="1.5" strokeLinecap="round"/>
            </svg>
            <div style={{ flex: 1, minWidth: 0 }}>
              <div style={{ fontSize: '13px', fontWeight: 600, color: NX.text, marginBottom: '4px' }}>
                {t('Tienes creativos sin publicar', 'You have unpublished creatives')}
              </div>
              <div style={{ fontSize: '12px', color: NX.muted, lineHeight: 1.45 }}>
                {t(
                  `${cachedSession.imgCount} imágen${cachedSession.imgCount === 1 ? '' : 'es'}${cachedSession.copyCount ? ` y ${cachedSession.copyCount} text${cachedSession.copyCount === 1 ? 'o' : 'os'}` : ''} guardados para "${(cachedSession.productDesc || '').slice(0, 70)}${(cachedSession.productDesc || '').length > 70 ? '…' : ''}".`,
                  `${cachedSession.imgCount} image${cachedSession.imgCount === 1 ? '' : 's'}${cachedSession.copyCount ? ` and ${cachedSession.copyCount} text${cachedSession.copyCount === 1 ? '' : 's'}` : ''} saved for "${(cachedSession.productDesc || '').slice(0, 70)}${(cachedSession.productDesc || '').length > 70 ? '…' : ''}".`
                )}
              </div>
            </div>
          </div>
          <div style={{ display: 'flex', gap: '8px', flexWrap: 'wrap' }}>
            <button onClick={resumeSession}
              style={{
                flex: 1, minWidth: '180px', padding: '9px 14px', borderRadius: '10px',
                background: NX.accentGrad, border: 'none', color: '#fff',
                fontSize: '13px', fontWeight: 600, cursor: 'pointer',
                fontFamily: "'DM Sans',sans-serif",
                boxShadow: '0 4px 14px rgba(139,92,246,0.35)',
              }}>
              {t('Continuar con la configuración actual →', 'Resume current setup →')}
            </button>
            <button onClick={startFreshSession}
              style={{
                flex: 1, minWidth: '180px', padding: '9px 14px', borderRadius: '10px',
                background: 'transparent', border: `1px solid ${NX.danger}`, color: NX.danger,
                fontSize: '13px', fontWeight: 500, cursor: 'pointer',
                fontFamily: "'DM Sans',sans-serif",
              }}>
              {t('Comenzar nueva publicación', 'Start a new publication')}
            </button>
          </div>
        </div>
      )}

      <Question
        q={t('¿Qué quieres promocionar?', 'What do you want to promote?')}
        sub={t('Descríbeme tu producto, servicio u oferta. Mientras más detalle, mejores creativos.', 'Describe your product, service or offer. The more detail, the better the creatives.')}/>
      {/* Wrapper relativo: el toggle de idioma vive en el top-right del textarea y el botón
          del micrófono en el bottom-center. Composición minimalista — toda la interacción
          orbita la misma caja. El padding top/bottom del textarea reserva el espacio para
          que el texto del usuario nunca quede oculto detrás de los controles. */}
      <div style={{ position:'relative' }}>
        <textarea autoFocus value={productDesc} onChange={e => setProductDesc(e.target.value)}
          placeholder={t('Ej: Zapatillas running unisex con suela de goma reciclada, ideales para corredores urbanos…', 'E.g. Unisex running shoes with recycled-rubber soles, ideal for urban runners…')}
          style={{ width:'100%', minHeight:'150px', padding:'40px 16px 50px', borderRadius:'14px', background:'rgba(26,17,40,0.6)', border:`1.5px solid ${isRecording ? NX.danger : NX.border2}`, color:NX.text, fontSize:'14px', fontFamily:"'DM Sans',sans-serif", lineHeight:1.55, outline:'none', resize:'vertical', transition:'border-color 0.18s' }}/>
        {/* Toggle de idioma del CONTENIDO — define el idioma de hooks, headlines y descripciones
            que generará Opus. Persiste en nw_content_lang (separado del idioma de la UI). */}
        <div style={{ position:'absolute', top:'8px', right:'8px', display:'flex', gap:'2px', padding:'3px', background:'rgba(8,8,10,0.62)', border:`1px solid ${NX.border}`, borderRadius:'100px', backdropFilter:'blur(6px)', WebkitBackdropFilter:'blur(6px)' }}>
          {[
            { id:'es', flag:'🇪🇸' },
            { id:'en', flag:'🇺🇸' },
          ].map(l => {
            const isActive = language === l.id;
            return (
              <button key={l.id} type="button"
                onClick={(e) => {
                  e.preventDefault();
                  e.stopPropagation();
                  setLanguage(l.id);
                  try { localStorage.setItem('nw_content_lang', l.id); } catch {}
                }}
                title={l.id === 'es' ? t('Generar en español','Generate in Spanish') : t('Generar en inglés','Generate in English')}
                style={{ padding:'3px 9px', borderRadius:'100px', background: isActive ? NX.accent : 'transparent', color: isActive ? '#fff' : NX.muted, border:'none', cursor:'pointer', fontSize:'10px', fontWeight: isActive ? 700 : 600, fontFamily:"'DM Mono',monospace", letterSpacing:'0.05em', display:'inline-flex', alignItems:'center', gap:'4px', transition:'all 0.15s', boxShadow: isActive ? '0 2px 6px rgba(139,92,246,0.35)' : 'none' }}>
                <span style={{ fontSize:'11px' }}>{l.flag}</span>{l.id.toUpperCase()}
              </button>
            );
          })}
        </div>
        <button type="button"
          onClick={isRecording ? stopRecording : startRecording}
          disabled={isTranscribing}
          title={isRecording ? t('Detener grabación','Stop recording') : (isTranscribing ? t('Transcribiendo…','Transcribing…') : t('Dictar nota de voz','Dictate voice note'))}
          aria-label={t('Dictar nota de voz','Dictate voice note')}
          style={{
            position:'absolute', bottom:'10px', left:'50%', transform:'translateX(-50%)',
            width:'34px', height:'34px', borderRadius:'50%',
            background: isRecording ? 'rgba(248,113,113,0.16)' : 'rgba(139,92,246,0.10)',
            border:`1px solid ${isRecording ? NX.danger : 'rgba(139,92,246,0.45)'}`,
            color: isRecording ? NX.danger : NX.accent2,
            cursor: isTranscribing ? 'wait' : 'pointer',
            display:'inline-flex', alignItems:'center', justifyContent:'center',
            transition:'all 0.18s', padding:0,
            boxShadow: isRecording ? '0 0 0 4px rgba(248,113,113,0.12)' : 'none',
          }}>
          {isTranscribing ? (
            <span style={{ width:'14px', height:'14px', border:`1.6px solid ${NX.accent2}`, borderTopColor:'transparent', borderRadius:'50%', animation:'spin 0.7s linear infinite' }}/>
          ) : isRecording ? (
            <span style={{ width:'10px', height:'10px', borderRadius:'2px', background: NX.danger, animation:'pulse 1.2s ease-in-out infinite' }}/>
          ) : (
            <svg width="14" height="14" viewBox="0 0 16 16" fill="none">
              <rect x="6" y="2" width="4" height="8" rx="2" stroke="currentColor" strokeWidth="1.4"/>
              <path d="M3.5 7.5a4.5 4.5 0 009 0M8 12v2M5.5 14h5" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round"/>
            </svg>
          )}
        </button>
      </div>
      {/* Estado: timer mientras graba + error si falla. Bajo el textarea para no romper
          la simetría del icono centrado. */}
      {(isRecording || audioError) && (
        <div style={{ marginTop:'6px', fontSize:'11px', textAlign:'center', color: audioError ? NX.danger : NX.muted, fontFamily:"'DM Sans',sans-serif" }}>
          {audioError ? audioError : `${t('Grabando','Recording')} ${String(Math.floor(recordingSecs/60)).padStart(2,'0')}:${String(recordingSecs%60).padStart(2,'0')} · ${t('toca el micro para detener','tap mic to stop')}`}
        </div>
      )}

      {/* Click destination — pick ONE: a website URL OR one/more messaging channels.
          Meta needs a destination so the user can never advance without choosing. */}
      <div style={{ marginTop:'14px' }}>
        <label style={{ display:'flex', alignItems:'center', gap:'6px', fontSize:'11px', color: NX.muted, fontFamily:"'DM Mono',monospace", letterSpacing:'0.08em', fontWeight:600, marginBottom:'8px', textTransform:'uppercase' }}>
          <svg width="12" height="12" viewBox="0 0 16 16" fill="none"><path d="M8 1a7 7 0 100 14A7 7 0 008 1zM1 8h14M8 1a11 11 0 010 14M8 1a11 11 0 000 14" stroke="currentColor" strokeWidth="1.2" strokeLinecap="round"/></svg>
          {t('¿Adónde mando el tráfico?', 'Where should the traffic go?')}
        </label>
        {/* Mode toggle: web | messaging | existing_ig. On mobile we drop to a 2-col grid
            that wraps so the third tab doesn't squeeze into a tiny strip. */}
        {/* Hidden for now: { id:'existing_ig', label:'Post de Instagram', icon:'📸' } —
            depende de instagram_basic en App Dashboard que aún no está aprobado. Cuando
            esté disponible, vuelve a sumarlo al array y al gridTemplateColumns. */}
        <div style={{ display:'grid', gridTemplateColumns:'1fr 1fr', gap:'8px', marginBottom:'10px' }}>
          {[
            { id:'web', label: t('Página web','Website'), icon:'🌐' },
            { id:'messaging', label: t('Mensajería','Messaging'), icon:'💬' },
          ].map(m => (
            <button key={m.id} type="button"
              onClick={() => setDestMode(m.id)}
              style={{
                padding:'10px 12px', borderRadius:'10px', cursor:'pointer',
                background: destMode === m.id ? 'rgba(139,92,246,0.14)' : 'rgba(26,17,40,0.5)',
                border:`1px solid ${destMode === m.id ? NX.accent2 : NX.border}`,
                color: destMode === m.id ? NX.accent2 : NX.muted,
                fontSize:'13px', fontWeight: destMode === m.id ? 600 : 500,
                fontFamily:"'DM Sans',sans-serif", textAlign:'center',
                display:'inline-flex', alignItems:'center', justifyContent:'center', gap:'6px',
                transition:'all 0.15s',
              }}>
              <span style={{ fontSize:'15px' }}>{m.icon}</span>{m.label}
            </button>
          ))}
        </div>

        {destMode === 'web' && (<>
          <div style={{ display:'flex', flexDirection: mobileLayout ? 'column' : 'row', gap:'8px', alignItems:'stretch' }}>
            <input type="url" value={productUrl} onChange={e => { setProductUrl(e.target.value); setScrapeError(null); }}
              placeholder="https://..."
              style={{ flex:1, minWidth:0, width: mobileLayout ? '100%' : 'auto', padding:'12px 14px', borderRadius:'12px', background:'rgba(26,17,40,0.5)', border:`1px solid ${NX.border}`, color:NX.text, fontSize:'13px', fontFamily:"'DM Sans',sans-serif", outline:'none' }}/>
            <button type="button" onClick={fillDescFromUrl} disabled={!isValidUrl || scraping}
              title={!isValidUrl ? t('Pega primero una URL válida','Paste a valid URL first') : t('Leer la web y rellenar la descripción','Read the page and fill the description')}
              style={{
                padding:'12px 14px', borderRadius:'12px', border:'none', whiteSpace:'nowrap',
                background: (!isValidUrl || scraping) ? 'rgba(139,92,246,0.18)' : 'linear-gradient(135deg, #7c3aed 0%, #8b5cf6 50%, #a78bfa 100%)',
                color:'#fff', fontSize:'13px', fontWeight:600, fontFamily:"'DM Sans',sans-serif",
                cursor: (!isValidUrl || scraping) ? 'not-allowed' : 'pointer',
                opacity: (!isValidUrl || scraping) ? 0.55 : 1,
                boxShadow: (!isValidUrl || scraping) ? 'none' : '0 6px 18px rgba(139,92,246,0.35), inset 0 1px 0 rgba(255,255,255,0.18)',
                display:'inline-flex', alignItems:'center', justifyContent: 'center', gap:'6px', transition:'all 0.18s',
                width: mobileLayout ? '100%' : 'auto',
              }}>
              {scraping ? (
                <>
                  <span style={{ display:'inline-block', width:'10px', height:'10px', border:'1.5px solid rgba(255,255,255,0.45)', borderTopColor:'#fff', borderRadius:'50%', animation:'spin 0.7s linear infinite' }}/>
                  {t('Leyendo…','Reading…')}
                </>
              ) : (
                <>
                  {/* 4-point sparkle (estilo Gemini/AI) — más reconocible como "AI" que la
                      estrella clásica de 5 puntas. Combinado con un pequeño accent debajo. */}
                  <svg width="13" height="13" viewBox="0 0 16 16" fill="none">
                    <path d="M8 1.5L9.2 6.5L14 7.8L9.2 9.1L8 14.1L6.8 9.1L2 7.8L6.8 6.5L8 1.5Z" fill="#fff"/>
                    <path d="M13 1.2L13.45 2.55L14.8 3L13.45 3.45L13 4.8L12.55 3.45L11.2 3L12.55 2.55L13 1.2Z" fill="#fff" opacity="0.85"/>
                  </svg>
                  {t('Generar contexto','Generate context')}
                </>
              )}
            </button>
          </div>
          {scrapeError ? (
            <div style={{ marginTop:'6px', fontSize:'11px', color: NX.danger, lineHeight:1.45 }}>{scrapeError}</div>
          ) : (
            <div style={{ marginTop:'6px', fontSize: mobileLayout ? '10px' : '11px', color: NX.muted, lineHeight:1.45 }}>
              {t('La usaremos como destino del clic. ¿No tienes web? Cambia a Mensajería arriba.', "We'll use it as the click destination. No website? Switch to Messaging above.")}
            </div>
          )}
        </>)}

        {destMode === 'messaging' && (<>
          <div style={{ display:'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: mobileLayout ? '6px' : '10px' }}>
            {[
              { id:'whatsapp', label:'WhatsApp', color:'#25D366', bg:'rgba(37,211,102,0.10)',
                icon:(<svg width="32" height="32" viewBox="0 0 32 32" fill="#25D366"><path d="M16 0C7.163 0 0 7.163 0 16c0 2.824.737 5.474 2.03 7.77L0 32l8.43-2.212A15.93 15.93 0 0016 32c8.837 0 16-7.163 16-16S24.837 0 16 0zm0 29.2c-2.515 0-4.852-.686-6.867-1.877l-.493-.293-4.988 1.31 1.332-4.867-.321-.505A13.145 13.145 0 012.8 16C2.8 8.72 8.72 2.8 16 2.8S29.2 8.72 29.2 16 23.28 29.2 16 29.2zm7.575-9.864c-.415-.207-2.454-1.21-2.834-1.349-.38-.138-.657-.207-.933.208-.276.414-1.07 1.348-1.312 1.625-.242.277-.484.312-.898.104-.415-.208-1.75-.645-3.333-2.057-1.232-1.098-2.064-2.454-2.306-2.868-.242-.415-.026-.64.182-.847.186-.186.415-.484.622-.726.207-.242.276-.415.415-.692.138-.277.069-.519-.034-.727-.104-.207-.934-2.25-1.28-3.08-.336-.81-.678-.7-.933-.712l-.795-.014c-.276 0-.726.104-1.106.519-.38.415-1.45 1.417-1.45 3.46 0 2.042 1.485 4.015 1.692 4.292.207.277 2.924 4.463 7.088 6.26 2.453 1.058 3.41 1.147 4.637.965.746-.111 2.454-1.003 2.8-1.972.346-.969.346-1.8.242-1.973-.104-.173-.38-.276-.795-.484z"/></svg>) },
              { id:'instagram', label: t('Instagram DM','Instagram DM'), color:'#E1306C', bg:'rgba(225,48,108,0.10)',
                icon:(<svg width="32" height="32" viewBox="0 0 24 24"><defs><linearGradient id={`igdmg-asst-${destMode}`} x1="0" y1="0" x2="24" y2="24" gradientUnits="userSpaceOnUse"><stop offset="0%" stopColor="#F58529"/><stop offset="50%" stopColor="#DD2A7B"/><stop offset="100%" stopColor="#8134AF"/></linearGradient></defs><path fill={`url(#igdmg-asst-${destMode})`} d="M12 2.163c3.204 0 3.584.012 4.85.07 3.252.148 4.771 1.691 4.919 4.919.058 1.265.069 1.645.069 4.849 0 3.205-.012 3.584-.069 4.849-.149 3.225-1.664 4.771-4.919 4.919-1.266.058-1.644.07-4.85.07-3.204 0-3.584-.012-4.849-.07-3.26-.149-4.771-1.699-4.919-4.92-.058-1.265-.07-1.644-.07-4.849 0-3.204.013-3.583.07-4.849.149-3.227 1.664-4.771 4.919-4.919 1.266-.057 1.645-.069 4.849-.069zm0-2.163c-3.259 0-3.667.014-4.947.072-4.358.2-6.78 2.618-6.98 6.98-.059 1.281-.073 1.689-.073 4.948 0 3.259.014 3.668.072 4.948.2 4.358 2.618 6.78 6.98 6.98 1.281.058 1.689.072 4.948.072 3.259 0 3.668-.014 4.948-.072 4.354-.2 6.782-2.618 6.979-6.98.059-1.28.073-1.689.073-4.948 0-3.259-.014-3.667-.072-4.947-.196-4.354-2.617-6.78-6.979-6.98-1.281-.059-1.69-.073-4.949-.073zm0 5.838c-3.403 0-6.162 2.759-6.162 6.162s2.759 6.163 6.162 6.163 6.162-2.759 6.162-6.163c0-3.403-2.759-6.162-6.162-6.162zm0 10.162c-2.209 0-4-1.79-4-4 0-2.209 1.791-4 4-4s4 1.791 4 4c0 2.21-1.791 4-4 4zm6.406-11.845c-.796 0-1.441.645-1.441 1.44s.645 1.44 1.441 1.44c.795 0 1.439-.645 1.439-1.44s-.644-1.44-1.439-1.44z"/></svg>) },
              { id:'messenger', label:'Messenger', color:'#0084FF', bg:'rgba(0,132,255,0.10)',
                icon:(<svg width="32" height="32" viewBox="0 0 32 32"><defs><linearGradient id={`msgrg-asst-${destMode}`} x1="0" y1="32" x2="32" y2="0" gradientUnits="userSpaceOnUse"><stop offset="0%" stopColor="#0084FF"/><stop offset="100%" stopColor="#44BEC7"/></linearGradient></defs><path fill={`url(#msgrg-asst-${destMode})`} d="M16 0C7.163 0 0 6.613 0 14.778c0 4.72 2.385 8.915 6.091 11.633V32l5.565-3.057c1.48.408 3.04.626 4.656.626 8.837 0 16-6.613 16-14.778C32 6.613 24.837 0 16 0zm1.585 19.903l-4.09-4.36-7.98 4.36L14.326 10.5l4.185 4.36 7.885-4.36-8.811 9.403z"/></svg>) },
            ].map(ch => {
              const active = destChannels[ch.id];
              return (
                <button key={ch.id} type="button"
                  onClick={() => setDestChannels({ whatsapp: false, instagram: false, messenger: false, [ch.id]: !active })}
                  style={{
                    padding: mobileLayout ? '12px 6px 10px' : '14px 10px 12px',
                    background: active ? ch.bg : 'rgba(26,17,40,0.5)',
                    border:`1.5px solid ${active ? ch.color : NX.border}`,
                    borderRadius:'12px', cursor:'pointer',
                    display:'flex', flexDirection:'column', alignItems:'center', gap:'8px',
                    fontFamily:"'DM Sans',sans-serif",
                    boxShadow: active ? `0 0 18px ${ch.color}33` : 'none',
                    transition:'all 0.15s', position:'relative',
                  }}>
                  {active && (
                    <div style={{ position:'absolute', top:'6px', right:'6px', width:'16px', height:'16px', borderRadius:'50%', background: ch.color, display:'flex', alignItems:'center', justifyContent:'center' }}>
                      <svg width="9" height="9" viewBox="0 0 14 14" fill="none"><path d="M3 7l3 3 5-7" stroke="#fff" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round"/></svg>
                    </div>
                  )}
                  <div style={{ width: mobileLayout ? '32px' : '36px', height: mobileLayout ? '32px' : '36px', display:'flex', alignItems:'center', justifyContent:'center', opacity: active ? 1 : 0.85 }}>
                    {ch.icon}
                  </div>
                  <div style={{ fontSize: mobileLayout ? '11px' : '12px', fontWeight:600, color: active ? ch.color : NX.text, letterSpacing:'-0.01em', textAlign:'center', lineHeight:1.2 }}>
                    {ch.label}
                  </div>
                  {ch.id === 'whatsapp' && pageWhatsAppNumber && (
                    <div style={{ fontSize: mobileLayout ? '9px' : '10px', color: active ? ch.color : NX.muted, fontFamily:"'DM Mono',monospace", letterSpacing:'0.04em', textAlign:'center', marginTop:'-4px' }}>
                      {pageWhatsAppNumber}
                    </div>
                  )}
                </button>
              );
            })}
          </div>
          <div style={{ marginTop:'8px', fontSize:'11px', color: hasMessagingPick ? NX.muted : NX.warning, lineHeight:1.45 }}>
            {hasMessagingPick
              ? t('Listo: Meta enviará a quien haga clic a un chat por el canal elegido.', 'Done: Meta will send clickers to a chat in the selected channel.')
              : t('Elige UN canal de destino. La Marketing API de Meta solo permite uno por anuncio.', 'Pick ONE destination channel. Meta\'s Marketing API only allows one per ad.')}
          </div>
          {/* WhatsApp-only preflight: Meta's API rejects WhatsApp destinations unless the
              page's WhatsApp number is registered as a WABA inside Business Manager.
              Surface this BEFORE publish so the user knows what's needed. When picked
              alongside Instagram/Messenger, our backend routes to the other channel
              automatically (matching what Ads Manager does in practice). */}
          {/* WhatsApp preflight: Meta valida estricto que el número de la página sea WhatsApp
              Business (no personal). Como ahora solo se puede elegir UN canal, este warning
              aparece cuando el canal elegido es WhatsApp. */}
          {destChannels.whatsapp && (
            <div style={{ marginTop:'8px', padding:'10px 12px', background:'rgba(251,191,36,0.08)', border:`1px solid ${NX.warning}33`, borderRadius:'10px', fontSize:'11px', color: NX.warning, lineHeight:1.5 }}>
              {t(
                '⚠ Meta exige que tu número de WhatsApp esté registrado como WhatsApp Business en la página. Si te tira error al publicar, elegí Instagram o Messenger.',
                '⚠ Meta requires your WhatsApp number to be registered as WhatsApp Business on the page. If publishing fails, switch to Instagram or Messenger.'
              )}
            </div>
          )}
        </>)}

        {destMode === 'existing_ig' && (<>
          {igPostsError && (
            <div style={{ fontSize:'11px', color: NX.danger, padding:'8px 10px', background:'rgba(248,113,113,0.08)', border:'1px solid rgba(248,113,113,0.25)', borderRadius:'8px', marginBottom:'10px', display:'flex', alignItems:'center', justifyContent:'space-between', gap:'8px' }}>
              <span style={{ flex:1 }}>{igPostsError}</span>
              <button type="button" onClick={() => { setIgPostsError(null); setIgPosts([]); fetchIgPosts(); }}
                style={{ padding:'3px 10px', background:'transparent', border:`1px solid ${NX.danger}`, borderRadius:'6px', color:NX.danger, fontSize:'10px', cursor:'pointer' }}>
                {t('Reintentar','Retry')}
              </button>
            </div>
          )}
          {igPostsLoading && (
            <div style={{ display:'flex', alignItems:'center', justifyContent:'center', padding:'24px', gap:'10px', color: NX.muted, fontSize:'12px' }}>
              <span style={{ display:'inline-block', width:'14px', height:'14px', border:'2px solid currentColor', borderTopColor:'transparent', borderRadius:'50%', animation:'spin 0.7s linear infinite' }}/>
              {t('Cargando tus publicaciones…','Loading your IG posts…')}
            </div>
          )}
          {!igPostsLoading && igPosts.length > 0 && (
            <>
              <div style={{ fontSize:'10px', color: NX.muted, fontFamily:"'DM Mono',monospace", letterSpacing:'0.1em', marginBottom:'8px', textTransform:'uppercase' }}>
                {t(`${igPosts.length} publicaciones — elige una`, `${igPosts.length} posts — pick one`)}
              </div>
              <div style={{ display:'grid', gridTemplateColumns: mobileLayout ? 'repeat(auto-fill, minmax(80px, 1fr))' : 'repeat(auto-fill, minmax(92px, 1fr))', gap:'8px', maxHeight: mobileLayout ? '260px' : '320px', overflowY:'auto', padding:'4px', borderRadius:'10px' }}>
                {igPosts.map(post => {
                  const active = selectedIgPost?.id === post.id;
                  return (
                    <div key={post.id} onClick={() => setSelectedIgPost(post)}
                      title={post.caption ? post.caption.slice(0, 120) : ''}
                      style={{ position:'relative', aspectRatio:'1 / 1', borderRadius:'10px', overflow:'hidden', cursor:'pointer', border:`2px solid ${active ? NX.accent2 : 'transparent'}`, boxShadow: active ? '0 0 20px rgba(139,92,246,0.35)' : 'none', transition:'all 0.15s' }}>
                      <img src={post.image} alt="" style={{ width:'100%', height:'100%', objectFit:'cover', background: NX.bg4 }}/>
                      {post.type === 'VIDEO' && (
                        <div style={{ position:'absolute', top:'6px', right:'6px', width:'18px', height:'18px', borderRadius:'50%', background:'rgba(0,0,0,0.6)', display:'flex', alignItems:'center', justifyContent:'center' }}>
                          <svg width="9" height="9" viewBox="0 0 12 12" fill="none"><path d="M3 2l7 4-7 4V2z" fill="#fff"/></svg>
                        </div>
                      )}
                      {post.type === 'CAROUSEL_ALBUM' && (
                        <div style={{ position:'absolute', top:'6px', right:'6px', width:'18px', height:'18px', borderRadius:'4px', background:'rgba(0,0,0,0.6)', display:'flex', alignItems:'center', justifyContent:'center' }}>
                          <svg width="11" height="11" viewBox="0 0 14 14" fill="none"><rect x="3" y="3" width="8" height="8" rx="1" stroke="#fff" strokeWidth="1.3"/><rect x="5" y="1" width="8" height="8" rx="1" stroke="#fff" strokeWidth="1.3"/></svg>
                        </div>
                      )}
                      {active && (
                        <div style={{ position:'absolute', bottom:'4px', left:'4px', width:'22px', height:'22px', borderRadius:'50%', background: NX.accent2, display:'flex', alignItems:'center', justifyContent:'center' }}>
                          <svg width="12" height="12" viewBox="0 0 14 14" fill="none"><path d="M3 7l3 3 5-7" stroke="#fff" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round"/></svg>
                        </div>
                      )}
                      <div style={{ position:'absolute', bottom:0, left:0, right:0, padding:'4px 6px', background:'linear-gradient(to top, rgba(0,0,0,0.75), transparent)', color:'#fff', fontSize:'9px', fontWeight:600, display:'flex', gap:'8px' }}>
                        <span>♥ {post.likes}</span>
                        <span>💬 {post.comments}</span>
                      </div>
                    </div>
                  );
                })}
              </div>
              {selectedIgPost && (
                <div style={{ marginTop:'10px', fontSize:'11px', color: NX.success, display:'flex', alignItems:'center', gap:'6px' }}>
                  <svg width="12" height="12" viewBox="0 0 12 12" fill="none"><path d="M2 6l3 3 5-7" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round"/></svg>
                  {t('Publicación seleccionada','Post selected')}
                  {' · '}
                  {selectedIgPost.type === 'VIDEO' ? t('Video','Video') : selectedIgPost.type === 'CAROUSEL_ALBUM' ? t('Carrusel','Carousel') : t('Imagen','Image')}
                </div>
              )}
            </>
          )}
          {!igPostsLoading && !igPostsError && igPosts.length === 0 && (
            <div style={{ fontSize:'11px', color: NX.warning, padding:'12px 10px', background:'rgba(251,191,36,0.06)', border:'1px solid rgba(251,191,36,0.22)', borderRadius:'8px', lineHeight:1.5 }}>
              {t('No encontramos publicaciones recientes en tu Instagram. Asegúrate de tener una cuenta de Instagram Business vinculada en "Configurar activos".', 'We did not find recent Instagram posts. Make sure your Instagram Business account is linked in "Configurar activos".')}
            </div>
          )}
          <div style={{ marginTop:'8px', fontSize:'11px', color: NX.muted, lineHeight:1.45 }}>
            {t('Promocionaremos tu publicación tal cual — sin generar imagen ni texto nuevo.', "We'll boost your post as-is — no new image or text generated.")}
          </div>
        </>)}
      </div>
    </>);
  } else if (step === 4) {
    // Foto referencia — solo aplica si IA va a generar (sirve como ground-truth para que la
    // IA respete el producto). Si el usuario eligió subir sus propias imágenes, este paso se
    // salta automáticamente por el SKIP_UPLOADED_STEPS de abajo.
    canAdvance = true; // optional
    primaryLabel = productFile ? t('Continuar', 'Continue') : t('Saltar (sin imagen)', 'Skip (no image)');
    body = (<>
      <Question
        q={t('¿Tienes una foto de referencia?', 'Got a reference photo?')}
        sub={t('Es opcional. Si la subes, la IA respetará el producto exacto en cada creativo.', 'Optional. If you upload one, the AI will keep the exact product in every creative.')}/>
      <input ref={fileRef} type="file" accept="image/jpeg,image/png,image/webp" onChange={onUpload} style={{ display:'none' }}/>
      {productPreviewUrl ? (
        <div style={{ display:'flex', flexDirection:'column', gap:'8px' }}>
          {/* Slot 1 — referencia primaria */}
          <div style={{ display:'flex', alignItems:'center', gap:'14px', padding:'12px 14px', background:'rgba(139,92,246,0.08)', border:`1px solid ${NX.accent}`, borderRadius:'14px' }}>
            <img src={productPreviewUrl} alt="" style={{ width:'56px', height:'56px', objectFit:'cover', borderRadius:'10px', flexShrink:0 }}/>
            <div style={{ flex:1, minWidth:0 }}>
              <div style={{ fontSize:'13px', color:NX.text, fontWeight:500, whiteSpace:'nowrap', overflow:'hidden', textOverflow:'ellipsis' }}>{productFile?.name}</div>
              <div style={{ fontSize:'11px', color:NX.muted }}>{productFile?.size ? `${(productFile.size/1024).toFixed(0)} KB · ` : ''}{t('Referencia principal', 'Primary reference')}</div>
            </div>
            <button type="button" onClick={() => { setProductFile(null); setProductPreviewUrl(null); setExtraProductFile(null); setExtraProductPreviewUrl(null); }}
              style={{ width:'32px', height:'32px', background:'transparent', border:'none', borderRadius:'8px', color:NX.muted, cursor:'pointer', display:'flex', alignItems:'center', justifyContent:'center' }}>
              <svg width="14" height="14" viewBox="0 0 16 16" fill="none"><path d="M4 4l8 8M12 4l-8 8" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round"/></svg>
            </button>
          </div>
          {/* Slot 2 — referencia secundaria opcional (filled o "+ 2ª imagen") */}
          {extraProductPreviewUrl ? (
            <div style={{ display:'flex', alignItems:'center', gap:'14px', padding:'12px 14px', background:'rgba(139,92,246,0.04)', border:`1px solid ${NX.border2}`, borderRadius:'14px' }}>
              <img src={extraProductPreviewUrl} alt="" style={{ width:'56px', height:'56px', objectFit:'cover', borderRadius:'10px', flexShrink:0 }}/>
              <div style={{ flex:1, minWidth:0 }}>
                <div style={{ fontSize:'13px', color:NX.text, fontWeight:500, whiteSpace:'nowrap', overflow:'hidden', textOverflow:'ellipsis' }}>{extraProductFile?.name}</div>
                <div style={{ fontSize:'11px', color:NX.muted }}>{extraProductFile?.size ? `${(extraProductFile.size/1024).toFixed(0)} KB · ` : ''}{t('2ª imagen (ángulo adicional)', '2nd image (extra angle)')}</div>
              </div>
              <button type="button" onClick={() => { setExtraProductFile(null); setExtraProductPreviewUrl(null); }}
                style={{ width:'32px', height:'32px', background:'transparent', border:'none', borderRadius:'8px', color:NX.muted, cursor:'pointer', display:'flex', alignItems:'center', justifyContent:'center' }}>
                <svg width="14" height="14" viewBox="0 0 16 16" fill="none"><path d="M4 4l8 8M12 4l-8 8" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round"/></svg>
              </button>
            </div>
          ) : (
            <button type="button" onClick={() => fileRef.current?.click()}
              {...dragHandlers('reference', acceptReferenceImage)}
              style={{ display:'flex', alignItems:'center', justifyContent:'center', gap:'8px', padding:'10px 14px',
                background: dragOverZone === 'reference' ? 'rgba(139,92,246,0.16)' : 'transparent',
                border:`1.5px dashed ${dragOverZone === 'reference' ? NX.accent : NX.border}`,
                borderRadius:'12px', color:NX.muted, cursor:'pointer', fontSize:'12px', fontFamily:"'DM Sans',sans-serif", transition:'all 0.15s' }}>
              <svg width="14" height="14" viewBox="0 0 24 24" fill="none"><path d="M12 5v14M5 12h14" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round"/></svg>
              {t('Agregar 2ª imagen (opcional, máx 2)', '+ 2nd image (optional, max 2)')}
            </button>
          )}
        </div>
      ) : (
        // Drag-and-drop enabled: the same button is also a drop target. Drag-over highlights
        // the border and tints the background so the user gets feedback before releasing.
        <button type="button" onClick={() => fileRef.current?.click()}
          {...dragHandlers('reference', acceptReferenceImage)}
          style={{ width:'100%', padding:'22px 16px',
            background: dragOverZone === 'reference' ? 'linear-gradient(180deg, rgba(139,92,246,0.20), rgba(139,92,246,0.10))' : 'linear-gradient(180deg, rgba(139,92,246,0.04), rgba(139,92,246,0.02))',
            border:`1.5px dashed ${dragOverZone === 'reference' ? NX.accent : NX.border2}`,
            borderRadius:'14px', cursor:'pointer', color:NX.text, display:'flex', alignItems:'center', justifyContent:'center', gap:'12px', fontFamily:"'DM Sans',sans-serif",
            transition:'all 0.18s' }}>
          <div style={{ width:'40px', height:'40px', borderRadius:'10px', background:'linear-gradient(135deg, rgba(139,92,246,0.18), rgba(139,92,246,0.12))', display:'flex', alignItems:'center', justifyContent:'center' }}>
            <svg width="18" height="18" viewBox="0 0 24 24" fill="none"><path d="M12 16V4m0 0l-4 4m4-4l4 4M4 16v2a2 2 0 002 2h12a2 2 0 002-2v-2" stroke={NX.accent} strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"/></svg>
          </div>
          <div style={{ textAlign:'left' }}>
            <div style={{ fontSize:'13px', fontWeight:500 }}>{dragOverZone === 'reference' ? t('Suelta para subir', 'Drop to upload') : t('Arrastra o haz click para subir', 'Drag or click to upload')}</div>
            <div style={{ fontSize:'11px', color:NX.muted, marginTop:'2px' }}>{t('JPG, PNG, WEBP — opcional', 'JPG, PNG, WEBP — optional')}</div>
          </div>
        </button>
      )}
    </>);
  } else if (step === 2) {
    // Country auto-correct: if the user wrote "Chilo" we offer (and apply on Continue) "Chile".
    // Block advance if neither exact match nor correctable suggestion (we still allow exact
    // free-text match — KNOWN_COUNTRIES isn't exhaustive but suggests the closest LATAM/EU
    // member).
    const countryClean = country.trim();
    const countryExact = countryClean ? isCountryExactMatch(countryClean) : false;
    const countrySuggestion = countryClean && !countryExact ? suggestCountryCorrection(countryClean) : null;
    // City-only inputs: the user typed a city without specifying country (no comma) AND the
    // input doesn't match any known country (exact or fuzzy). Without a country hint, Meta
    // /search?type=adgeolocation can return the wrong city when the name exists in multiple
    // countries (e.g. "La Serena" exists in Chile and elsewhere). We surface a country selector
    // so the user explicitly disambiguates → backend resolves to a SINGLE city, not the whole
    // country, and the campaign launches only in that city.
    const hasCommaHint = countryClean.includes(',');
    const looksLikeCity = countryClean.length >= 3 && !countryExact && !countrySuggestion && !hasCommaHint;
    canAdvance = countryClean.length >= 2 && (countryExact || !!countrySuggestion || hasCommaHint);
    if (countrySuggestion) {
      onPrimary = () => { setCountry(countrySuggestion); advanceStep(); };
      primaryLabel = t(`Continuar con "${countrySuggestion}"`, `Continue with "${countrySuggestion}"`);
    }
    body = (<>
      <Question
        q={t('¿En qué país lanzar?', 'Which country to launch in?')}
        sub={t('Detecté tu zona horaria — ajusta si es necesario.', 'I detected your timezone — adjust if needed.')}/>
      <input type="text" autoFocus value={country} onChange={e => setCountry(e.target.value)}
        placeholder={t('Ej: Chile, México, La Serena…', 'E.g. Chile, Mexico, La Serena…')}
        style={{ width:'100%', padding:'14px 16px', borderRadius:'14px', background:'rgba(26,17,40,0.6)', border:`1.5px solid ${countrySuggestion ? NX.warning : looksLikeCity ? NX.accent : NX.border2}`, color:NX.text, fontSize:'14px', fontFamily:"'DM Sans',sans-serif", outline:'none' }}/>
      {countrySuggestion && (
        <div style={{ marginTop:'10px', padding:'10px 14px', borderRadius:'12px', background:'rgba(251,191,36,0.08)', border:'1px solid rgba(251,191,36,0.25)', display:'flex', alignItems:'center', justifyContent:'space-between', gap:'10px', flexWrap:'wrap' }}>
          <div style={{ fontSize:'12px', color: NX.text, lineHeight:1.45 }}>
            {t('¿Quisiste decir', 'Did you mean')} <strong style={{ color: NX.warning }}>{countrySuggestion}</strong>?
          </div>
          <button type="button" onClick={() => setCountry(countrySuggestion)}
            style={{ padding:'6px 12px', background:'rgba(251,191,36,0.18)', border:'1px solid rgba(251,191,36,0.35)', borderRadius:'10px', color: NX.warning, fontSize:'11px', fontWeight:600, cursor:'pointer', fontFamily:"'DM Sans',sans-serif" }}>
            {t('Sí, corregir', 'Yes, fix it')}
          </button>
        </div>
      )}
      {looksLikeCity && (
        <div style={{ marginTop:'10px', padding:'12px 14px', borderRadius:'12px', background:'rgba(139,92,246,0.08)', border:'1px solid rgba(139,92,246,0.28)', display:'flex', flexDirection:'column', gap:'8px' }}>
          <div style={{ fontSize:'12px', color: NX.text, lineHeight:1.45 }}>
            {t('Parece que escribiste una ciudad. ¿En qué país está', 'Looks like you typed a city. Which country is')} <strong style={{ color: NX.accent }}>{countryClean}</strong>?
          </div>
          <select value="" onChange={e => { if (e.target.value) setCountry(`${countryClean}, ${e.target.value}`); }}
            style={{ padding:'10px 12px', borderRadius:'10px', background:'rgba(26,17,40,0.7)', border:`1px solid ${NX.border2}`, color:NX.text, fontSize:'13px', fontFamily:"'DM Sans',sans-serif", outline:'none', cursor:'pointer' }}>
            <option value="">{t('Elegí el país…', 'Pick the country…')}</option>
            {KNOWN_COUNTRIES.map(c => <option key={c} value={c}>{c}</option>)}
          </select>
          <div style={{ fontSize:'10px', color: NX.muted, lineHeight:1.4, fontFamily:"'DM Mono',monospace" }}>
            {t('🎯 La campaña va a salir SOLO en esa ciudad, excluyendo el resto del país.', '🎯 The campaign will run ONLY in that city, excluding the rest of the country.')}
          </div>
        </div>
      )}
    </>);
  } else if (step === 3) {
    // Min spend per currency comes from Meta. Block advance if below — saves a publish-time
    // error from Meta's API (which is cryptic and frustrating).
    // Multi-channel messaging: Meta exige un adset por canal (cada uno con su mínimo). Si
    // el user marcó 2+ canales, el budget total debe ser ≥ minDaily × cantidad de canales.
    const budgetNum = Number(dailyBudget);
    const channelsPicked = destMode === 'messaging'
      ? Object.entries(destChannels).filter(([, v]) => v).map(([k]) => k)
      : [];
    const channelsCount = Math.max(1, channelsPicked.length);
    const minRequired = minDailyForCurrency * channelsCount;
    const belowMin = budgetNum > 0 && budgetNum < minRequired;
    canAdvance = budgetNum >= minRequired;
    const subText = channelsCount >= 2
      ? t(
          `Elegiste ${channelsCount} canales — Meta crea un conjunto por canal y cada uno necesita al menos ${formatBudget(minDailyForCurrency, adCurrency)}/día. Mínimo total: ${formatBudget(minRequired, adCurrency)}/día.`,
          `You picked ${channelsCount} channels — Meta creates one ad set per channel and each needs at least ${formatBudget(minDailyForCurrency, adCurrency)}/day. Total minimum: ${formatBudget(minRequired, adCurrency)}/day.`,
        )
      : t(
          `Tu cuenta de Meta está en ${adCurrency} — Meta no permite cambiar la moneda. Mínimo: ${formatBudget(minDailyForCurrency, adCurrency)}/día.`,
          `Your Meta account is in ${adCurrency} — Meta does not allow changing currency. Minimum: ${formatBudget(minDailyForCurrency, adCurrency)}/day.`,
        );
    body = (<>
      <Question
        q={t('¿Cuánto puedes invertir al día?', 'Daily budget?')}
        sub={subText}/>
      <div style={{ display:'flex', flexWrap:'wrap', gap:'8px', marginBottom:'12px' }}>
        {budgetChips.map(n => {
          const scaled = n * channelsCount;
          const active = String(scaled) === dailyBudget;
          return (
            <button key={n} type="button" onClick={() => setDailyBudget(String(scaled))}
              style={{ padding:'10px 16px', borderRadius:'12px', background: active ? NX.accentGradCool : 'rgba(26,17,40,0.55)', border:`1.5px solid ${active ? NX.accent : NX.border}`, color: active ? '#fff' : NX.text, fontSize:'13px', fontWeight:600, cursor:'pointer', fontFamily:"'DM Sans',sans-serif", transition:'all 0.15s', whiteSpace:'nowrap' }}>
              {formatBudget(scaled, adCurrency)}
            </button>
          );
        })}
      </div>
      <div style={{ position:'relative' }}>
        <span style={{ position:'absolute', left:'14px', top:'50%', transform:'translateY(-50%)', fontSize:'12px', color: NX.muted, fontFamily:"'DM Mono',monospace", fontWeight:600, pointerEvents:'none' }}>{adCurrency}</span>
        <input type="number" min={minRequired} value={dailyBudget} onChange={e => setDailyBudget(e.target.value)}
          placeholder={t(`Mínimo ${minRequired}`, `Minimum ${minRequired}`)}
          style={{ width:'100%', padding:'14px 16px 14px 60px', borderRadius:'14px', background:'rgba(26,17,40,0.6)', border:`1.5px solid ${belowMin ? NX.danger : NX.border2}`, color:NX.text, fontSize:'14px', fontFamily:"'DM Sans',sans-serif", outline:'none' }}/>
      </div>
      {belowMin && (
        <div style={{ marginTop:'8px', fontSize:'11px', color: NX.danger, fontFamily:"'DM Sans',sans-serif" }}>
          {channelsCount >= 2
            ? t(
                `Subí el presupuesto a al menos ${formatBudget(minRequired, adCurrency)}/día. Con ${channelsCount} canales son ${channelsCount} conjuntos de anuncios y cada uno necesita ${formatBudget(minDailyForCurrency, adCurrency)} mínimo.`,
                `Increase the budget to at least ${formatBudget(minRequired, adCurrency)}/day. With ${channelsCount} channels you'll have ${channelsCount} ad sets and each needs ${formatBudget(minDailyForCurrency, adCurrency)} minimum.`,
              )
            : t(
                `Meta requiere mínimo ${formatBudget(minDailyForCurrency, adCurrency)}/día para ${adCurrency}.`,
                `Meta requires at least ${formatBudget(minDailyForCurrency, adCurrency)}/day for ${adCurrency}.`,
              )}
        </div>
      )}
    </>);
  } else if (step === 1) {
    // ── Generation mode: AI generates OR user already has creatives ──
    // Movido a step 1 (antes era step 4) para que el usuario decida ANTES si vamos por
    // path IA o uploaded — así podemos saltar la foto de referencia (step 4) si no aplica.
    canAdvance = true;
    body = (<>
      <Question
        q={t('¿Generar imágenes o ya las tienes?', 'Generate images or upload your own?')}
        sub={t('Si ya tienes tus creativos, no gastamos créditos en generar — los subes y vamos directo a los textos.', "If you already have creatives, we save credits — just upload them and we go straight to the texts.")}/>
      <div style={{ display:'grid', gridTemplateColumns:'1fr 1fr', gap:'12px' }}>
        {[
          { id:'ai',       label: t('Que la IA las genere', 'Let the AI generate them'), sub: t('Premium o Premium Ultra MAX','Premium or Premium Ultra MAX'), color: NX.accent,
            iconSvg: <svg width="20" height="20" viewBox="0 0 24 24" fill="none"><path d="M12 2l2.09 4.26L18.5 7l-3.5 3.41L15.91 15 12 12.77 8.09 15 9 10.41 5.5 7l4.41-.74L12 2z" fill="currentColor"/></svg> },
          { id:'uploaded', label: t('Yo subo mis imágenes', 'I upload my own'),  sub: t('Sin costo en creditos','No credit cost'), color: NX.success,
            iconSvg: <svg width="20" height="20" viewBox="0 0 24 24" fill="none"><path d="M3 7a2 2 0 012-2h4l2 2h8a2 2 0 012 2v9a2 2 0 01-2 2H5a2 2 0 01-2-2V7z" stroke="currentColor" strokeWidth="1.6"/><path d="M12 11v5M10 13l2-2 2 2" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round"/></svg> },
        ].map(opt => {
          const active = genMode === opt.id;
          return (
            <button key={opt.id} type="button" onClick={() => { setGenMode(opt.id); advanceStep(); }}
              style={{ padding:'18px 14px', borderRadius:'14px', background: active ? 'rgba(36,24,56,0.85)' : 'rgba(26,17,40,0.5)', border:`1.5px solid ${active ? opt.color : NX.border}`, color:NX.text, cursor:'pointer', fontFamily:"'DM Sans',sans-serif", textAlign:'left', boxShadow: active ? `0 6px 22px ${opt.color}33` : 'none', transition:'all 0.18s' }}>
              <div style={{ display:'flex', alignItems:'center', gap:'8px', marginBottom:'6px', color: opt.color }}>{opt.iconSvg}</div>
              <div style={{ fontSize:'14px', fontWeight:600, color: NX.text, marginBottom:'3px', lineHeight:1.3 }}>{opt.label}</div>
              <div style={{ fontSize:'11px', color: NX.muted, lineHeight:1.4 }}>{opt.sub}</div>
            </button>
          );
        })}
      </div>
    </>);
  } else if (step === 5 && genMode === 'ai') {
    // ── Quality picker (only when AI generates) ──
    canAdvance = true;
    body = (<>
      <Question
        q={t('¿Calidad de imagen?', 'Image quality?')}
        sub={t('Premium = rápido y excelente. Premium Ultra MAX = máxima calidad para alto impacto.', 'Premium = fast and excellent. Premium Ultra MAX = max quality for high-impact campaigns.')}/>
      <div style={{ display:'grid', gridTemplateColumns:'1fr 1fr', gap:'12px' }}>
        {[
          {
            id:'gemini-3.1-flash-image-preview',
            label:t('Premium','Premium'),
            badge:t('Premium','Premium'),
            // Cyan/azul brillante — distintivo "rápido y eficiente"
            color: '#22d3ee',
            gradient: 'linear-gradient(135deg, #06b6d4 0%, #22d3ee 100%)',
            tag: 'linear-gradient(135deg, #0891b2 0%, #22d3ee 100%)',
            recommended: false,
            // Lightning bolt — speed
            icon: <svg width="22" height="22" viewBox="0 0 24 24" fill="none"><path d="M13 2L3 14h7l-1 8 10-12h-7l1-8z" fill="currentColor"/></svg>,
          },
          {
            id:'gpt-image-2',
            label:t('Premium Ultra MAX','Premium Ultra MAX'),
            badge:t('Ultra MAX','Ultra MAX'),
            // Violeta/rosa — distintivo "alta calidad luxury"
            color: '#f472b6',
            gradient: 'linear-gradient(135deg, #a855f7 0%, #f472b6 100%)',
            tag: 'linear-gradient(135deg, #7c3aed 0%, #f472b6 100%)',
            recommended: true,
            // Diamond / sparkle — premium quality
            icon: <svg width="22" height="22" viewBox="0 0 24 24" fill="none"><path d="M12 2l2.39 5.5L20 8.5l-4.5 4 1.5 6.5L12 16l-5 3 1.5-6.5L4 8.5l5.61-1L12 2z" fill="currentColor"/></svg>,
          },
        ].map(opt => {
          const active = imageModel === opt.id;
          // Plan business NO accede a Ultra MAX. Solo business_max / reviewer / admin.
          const isUltra = opt.id === 'gpt-image-2';
          const planAllowsUltra = !creditsState
            || creditsState.is_admin
            || creditsState.plan === 'business_max'
            || creditsState.plan === 'reviewer';
          const locked = isUltra && !planAllowsUltra;
          return (
            <button key={opt.id} type="button"
              onClick={() => {
                if (locked) {
                  alert(t('Premium Ultra MAX está disponible solo en el plan Business MAX. Cambia de plan en /precios.','Premium Ultra MAX is only available on the Business MAX plan. Upgrade in /pricing.'));
                  return;
                }
                setImageModel(opt.id); advanceStep();
              }}
              style={{ position:'relative', padding:'18px 14px', borderRadius:'14px', background: active ? `linear-gradient(135deg, ${opt.color}1a 0%, rgba(26,17,40,0.85) 100%)` : 'rgba(26,17,40,0.5)', border:`1.5px solid ${active ? opt.color : opt.recommended ? `${opt.color}66` : NX.border}`, color: locked ? NX.muted : NX.text, cursor: locked ? 'not-allowed' : 'pointer', fontFamily:"'DM Sans',sans-serif", textAlign:'left', boxShadow: active ? `0 6px 22px ${opt.color}40` : opt.recommended ? `0 4px 18px ${opt.color}22` : 'none', transition:'all 0.18s', opacity: locked ? 0.55 : 1 }}>
              {locked && (
                <span style={{ position:'absolute', top:'8px', right:'8px', display:'inline-flex', alignItems:'center', gap:'4px', padding:'2px 7px', borderRadius:'100px', background:'rgba(245,158,11,0.18)', border:'1px solid rgba(245,158,11,0.4)', color:'#f59e0b', fontSize:'9px', fontWeight:700, fontFamily:"'DM Mono',monospace", letterSpacing:'0.05em', textTransform:'uppercase' }}>
                  <svg width="9" height="9" viewBox="0 0 16 16" fill="none"><path d="M4 7V5a4 4 0 018 0v2M3 7h10v7H3V7z" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round" strokeLinejoin="round"/></svg>
                  {t('Business MAX','Business MAX')}
                </span>
              )}
              {opt.recommended && (
                <span style={{ position:'absolute', top:'-9px', right:'10px', fontSize:'9px', fontWeight:800, letterSpacing:'0.1em', padding:'3px 9px', borderRadius:'100px', background: opt.tag, color:'#fff', textTransform:'uppercase', boxShadow:`0 4px 12px ${opt.color}55`, whiteSpace:'nowrap' }}>
                  ★ {t('Recomendada','Recommended')}
                </span>
              )}
              <div style={{ display:'flex', alignItems:'center', gap:'8px', marginBottom:'8px' }}>
                <span style={{ background: opt.gradient, WebkitBackgroundClip:'text', WebkitTextFillColor:'transparent', backgroundClip:'text', display:'inline-flex' }}>{opt.icon}</span>
                <span style={{ fontSize:'9px', fontWeight:700, letterSpacing:'0.1em', textTransform:'uppercase', fontFamily:"'DM Mono',monospace", color: opt.color }}>{opt.badge}</span>
              </div>
              <div style={{ fontSize:'15px', fontWeight:600, background: active ? opt.gradient : 'transparent', WebkitBackgroundClip: active ? 'text' : 'border-box', WebkitTextFillColor: active ? 'transparent' : NX.text, backgroundClip: active ? 'text' : 'border-box', color: active ? 'transparent' : NX.text }}>{opt.label}</div>
              {/* Indicador "puedes sacar X imágenes con tu saldo".
                  Admin y reviewer siempre muestran ∞ — sus llamadas no consumen créditos. */}
              {(() => {
                if (!creditsState) return null;
                const unlimited = !!creditsState.is_admin || creditsState.plan === 'reviewer';
                const balance = Number(creditsState.balance || 0);
                const isUltra = opt.id === 'gpt-image-2';
                const cost = isUltra ? (window.CREDIT_COSTS?.image_ultra_max || 100) : (window.CREDIT_COSTS?.image_premium || 25);
                const possible = unlimited ? '∞' : Math.floor(balance / cost);
                const lowSaldo = !unlimited && Number(possible) < 5;
                return (
                  <div style={{ marginTop:'10px', paddingTop:'10px', borderTop:`1px solid ${NX.border}`, display:'flex', flexDirection:'column', gap:'4px' }}>
                    <div style={{ display:'flex', alignItems:'baseline', gap:'5px', flexWrap:'wrap' }}>
                      <span style={{ fontSize:'18px', fontWeight:800, fontFamily:"'DM Mono',monospace", color: lowSaldo ? NX.warning : opt.color, letterSpacing:'-0.02em', lineHeight:1 }}>
                        ≈ {possible}
                      </span>
                      <span style={{ fontSize:'12px', color: NX.text, fontWeight:500 }}>
                        {t('imágenes con tu saldo','images with your balance')}
                      </span>
                    </div>
                    <div style={{ fontSize:'11px', color: NX.muted, fontFamily:"'DM Mono',monospace" }}>
                      {cost} {t('créditos por imagen','credits per image')}
                    </div>
                  </div>
                );
              })()}
            </button>
          );
        })}
      </div>
      {/* Línea de saldo total — admin y reviewer ven ∞, resto ve número real */}
      {creditsState && (creditsState.is_admin || creditsState.plan === 'reviewer') && (
        <div style={{ marginTop:'14px', textAlign:'center', fontSize:'13px', color:NX.success, fontFamily:"'DM Mono',monospace", fontWeight:600 }}>
          {t('Saldo ilimitado','Unlimited balance')} ∞
        </div>
      )}
      {creditsState && !creditsState.is_admin && creditsState.plan && creditsState.plan !== 'reviewer' && (
        <div style={{ marginTop:'14px', textAlign:'center', fontSize:'13px', color:NX.text, fontFamily:"'DM Sans',sans-serif" }}>
          {t('Saldo disponible','Available balance')}: <span style={{ color:NX.accent2, fontWeight:700, fontFamily:"'DM Mono',monospace" }}>{Number(creditsState.balance || 0).toLocaleString()}</span> {t('créditos','credits')}
        </div>
      )}
    </>);
  } else if (step === 5 && genMode === 'uploaded') {
    // ── Own images uploader. Two visual states:
    //   • Empty (ownImages.length === 0) → a single full-width drop zone, ~140px tall, big
    //     dashed border + cloud icon + "Arrastra y suelta" hint. Click also opens the picker.
    //   • With images (length > 0) → compact grid of 5 cols (1 row up to 5 imgs, 2 rows for
    //     6-10) with a small trailing "Agregar más" tile.
    // Drag-and-drop is wired on BOTH states via dragHandlers('own', ...).
    canAdvance = ownImages.length > 0;
    const cols = 5;
    const showAddTile = ownImages.length > 0 && ownImages.length < 10;
    const isEmpty = ownImages.length === 0;
    body = (<>
      <Question
        q={t('Sube tus creativos', 'Upload your creatives')}
        sub={t('Las usaremos tal cual en la campaña, sin retoques. Hasta 10 imágenes.', 'We will use them as-is, no retouching. Up to 10 images.')}/>
      <input ref={ownImagesInputRef} type="file" accept="image/jpeg,image/png,image/webp" multiple onChange={onOwnUpload} style={{ display:'none' }}/>

      {isEmpty ? (
        // EMPTY STATE — big dropzone that fills the card width and is obviously a drop target.
        <button type="button"
          {...dragHandlers('own', acceptOwnImages)}
          onClick={() => ownImagesInputRef.current?.click()}
          style={{
            width:'100%', minHeight:'150px',
            display:'flex', flexDirection:'column', alignItems:'center', justifyContent:'center', gap:'10px',
            padding:'24px',
            background: dragOverZone === 'own'
              ? 'linear-gradient(180deg, rgba(139,92,246,0.18), rgba(139,92,246,0.08))'
              : 'linear-gradient(180deg, rgba(139,92,246,0.05), rgba(139,92,246,0.015))',
            border:`1.5px dashed ${dragOverZone === 'own' ? NX.accent : NX.border2}`,
            borderRadius:'14px', cursor:'pointer',
            color: dragOverZone === 'own' ? NX.accent2 : NX.accent,
            transition:'all 0.18s',
            fontFamily:"'DM Sans',sans-serif",
          }}>
          <svg width="40" height="40" viewBox="0 0 24 24" fill="none">
            <path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4M17 8l-5-5-5 5M12 3v12" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round"/>
          </svg>
          <div style={{ fontSize:'14px', fontWeight:600, color: NX.text }}>
            {dragOverZone === 'own'
              ? t('Suelta para subir','Drop to upload')
              : t('Arrastra y suelta tus imágenes aquí','Drag and drop your images here')}
          </div>
          <div style={{ fontSize:'12px', color: NX.muted }}>
            {t('o haz click para seleccionar (JPG, PNG, WebP)','or click to browse (JPG, PNG, WebP)')}
          </div>
        </button>
      ) : (
        // WITH IMAGES — compact 5-col grid with thumbs + trailing add tile.
        <div
          {...dragHandlers('own', acceptOwnImages)}
          style={{
            display:'grid',
            gridTemplateColumns: `repeat(${cols}, 1fr)`,
            gap:'8px',
            padding: dragOverZone === 'own' ? '8px' : '4px',
            background: dragOverZone === 'own' ? 'rgba(139,92,246,0.10)' : 'transparent',
            border: `1.5px dashed ${dragOverZone === 'own' ? NX.accent : 'transparent'}`,
            borderRadius:'12px', transition:'all 0.18s',
          }}>
          {ownImages.map((img, i) => (
            <div key={i} style={{ position:'relative', aspectRatio:'1/1', borderRadius:'10px', overflow:'hidden', border:`1.5px solid ${NX.border2}`, background: NX.bg4 }}>
              <img src={img.previewUrl} alt={`Creativo ${i+1}`} style={{ width:'100%', height:'100%', objectFit:'cover' }}/>
              <button type="button" onClick={() => removeOwnImage(i)}
                style={{ position:'absolute', top:'4px', right:'4px', width:'20px', height:'20px', borderRadius:'50%', background:'rgba(0,0,0,0.65)', border:'none', color:'#fff', cursor:'pointer', display:'flex', alignItems:'center', justifyContent:'center' }}>
                <svg width="9" height="9" viewBox="0 0 16 16" fill="none"><path d="M4 4l8 8M12 4l-8 8" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round"/></svg>
              </button>
              <div style={{ position:'absolute', bottom:'4px', left:'6px', fontSize:'9px', color:'#fff', fontFamily:"'DM Mono',monospace", fontWeight:700, textShadow:'0 1px 2px rgba(0,0,0,0.6)' }}>#{i+1}</div>
            </div>
          ))}
          {showAddTile && (
            <button type="button" onClick={() => ownImagesInputRef.current?.click()}
              title={t('Arrastra archivos aquí o haz click para agregar', 'Drag files here or click to add')}
              style={{
                aspectRatio:'1/1', borderRadius:'10px',
                background: dragOverZone === 'own'
                  ? 'linear-gradient(180deg, rgba(139,92,246,0.22), rgba(139,92,246,0.12))'
                  : 'linear-gradient(180deg, rgba(139,92,246,0.06), rgba(139,92,246,0.02))',
                border:`1.5px dashed ${dragOverZone === 'own' ? NX.accent : NX.border2}`,
                cursor:'pointer', color: NX.accent,
                display:'flex', flexDirection:'column', alignItems:'center', justifyContent:'center', gap:'4px',
                fontFamily:"'DM Sans',sans-serif", fontSize:'10px', fontWeight:600,
                transition:'all 0.18s', textAlign:'center', padding:'8px',
              }}>
              <svg width="20" height="20" viewBox="0 0 24 24" fill="none"><path d="M12 5v14M5 12h14" stroke="currentColor" strokeWidth="2" strokeLinecap="round"/></svg>
              <span>{t('Agregar','Add')}</span>
            </button>
          )}
        </div>
      )}

      <div style={{ marginTop:'10px', display:'flex', alignItems:'center', justifyContent:'space-between', fontSize:'11px', color:NX.muted, fontFamily:"'DM Mono',monospace" }}>
        <span>{ownImages.length}/10 {ownImages.length === 1 ? t('imagen','image') : t('imágenes','images')}</span>
        {!isEmpty && <span>{t('Arrastra o haz click','Drag or click')}</span>}
      </div>
    </>);
  } else if (step === 6) {
    // ── Final step: count (AI only) + summary ──
    canAdvance = true;
    primaryLabel = genMode === 'uploaded'
      ? t('Continuar →', 'Continue →')
      : t('Generar creativos →', 'Generate creatives →');
    onPrimary = () => setHandoff(true);
    body = (<>
      {genMode === 'ai' ? (<>
        <Question
          q={t('¿Cuántos creativos generar?', 'How many creatives?')}
          sub={t('Más variedad = más datos para que Meta encuentre el mejor ángulo.', 'More variety = more data for Meta to find the winning angle.')}/>
        <div style={{ display:'grid', gridTemplateColumns:'repeat(4, 1fr)', gap:'10px', marginBottom:'18px' }}>
          {[
            { n: 1,  label: t('Test','Test') },
            { n: 3,  label: t('Mínimo','Minimum') },
            { n: 5,  label: t('Recomendado','Recommended') },
            { n: 10, label: t('Máximo','Maximum') },
          ].map(opt => {
            const active = imageCount === opt.n;
            const isRecommended = opt.n === 5;
            // Costo total = N imágenes × costo por tier. No incluye estrategia/copies — esos
            // se muestran agregados en el step final junto al CTA "Lanzar campaña".
            // Admin y reviewer muestran ∞ — sus llamadas no consumen créditos.
            const perImage = (window.creditCostForImage ? window.creditCostForImage(imageModel) : 25);
            const totalCr = opt.n * perImage;
            const unlimited = !!(creditsState && (creditsState.is_admin || creditsState.plan === 'reviewer'));
            const insufficient = !unlimited && creditsState && Number(creditsState.balance || 0) < totalCr;
            return (
              <button key={opt.n} type="button" onClick={() => setImageCount(opt.n)}
                style={{ position:'relative', padding:'16px 8px', borderRadius:'12px', background: active ? 'rgba(36,24,56,0.85)' : 'rgba(26,17,40,0.5)', border:`1.5px solid ${active ? NX.accent : isRecommended ? `${NX.accent}55` : NX.border}`, color:NX.text, cursor:'pointer', fontFamily:"'DM Sans',sans-serif", textAlign:'center', boxShadow: active ? '0 6px 22px rgba(139,92,246,0.25)' : isRecommended ? `0 4px 14px ${NX.accent}22` : 'none', transition:'all 0.18s' }}>
                {isRecommended && !active && (
                  <span style={{ position:'absolute', top:'-7px', right:'6px', fontSize:'7px', fontWeight:800, letterSpacing:'0.08em', padding:'2px 6px', borderRadius:'100px', background: NX.accent, color:'#fff', textTransform:'uppercase' }}>★</span>
                )}
                <div style={{ fontSize:'26px', fontWeight:700, color: active ? NX.accent : NX.text, lineHeight:1, marginBottom:'4px', letterSpacing:'-0.04em' }}>{opt.n}</div>
                <div style={{ fontSize:'9px', color: NX.muted, fontFamily:"'DM Mono',monospace", textTransform:'uppercase', letterSpacing:'0.08em' }}>{opt.label}</div>
                <div style={{ marginTop:'8px', paddingTop:'8px', borderTop:`1px solid ${NX.border}`, fontSize:'15px', fontWeight:800, fontFamily:"'DM Mono',monospace", color: unlimited ? NX.success : insufficient ? NX.danger : (active ? NX.accent2 : NX.text), letterSpacing:'-0.01em' }}>
                  {unlimited ? '∞' : totalCr}
                  <span style={{ fontSize:'10px', fontWeight:600, color: NX.muted, marginLeft:'3px' }}>cr</span>
                </div>
              </button>
            );
          })}
        </div>
      </>) : (
        <Question
          q={t('Listo para revisar y publicar', 'Ready to review and publish')}
          sub={t('Vamos a usar tus imágenes tal cual y generar los textos en torno a ellas.', "We'll use your images as-is and generate the texts around them.")}/>
      )}
      {/* Auto-decisions summary — what the AI is taking off the user's plate */}
      {(() => {
        // Build a human-readable description of where the click traffic goes. This needs to
        // tell the user EXACTLY what they picked in step 0 — web URL / messaging channel(s) /
        // existing IG post — so they can spot a wrong selection before publishing.
        let trafficLabel;
        let trafficValue;
        if (destMode === 'existing_ig' && selectedIgPost) {
          trafficLabel = t('Promocionar post','Boost post');
          const postType = selectedIgPost.type === 'VIDEO' ? t('Video','Video') : selectedIgPost.type === 'CAROUSEL_ALBUM' ? t('Carrusel','Carousel') : t('Imagen','Image');
          const captionSnippet = (selectedIgPost.caption || '').split('\n')[0].slice(0, 40);
          trafficValue = captionSnippet ? `IG · ${postType} · "${captionSnippet}${selectedIgPost.caption?.length > 40 ? '…' : ''}"` : `IG · ${postType}`;
        } else if (destMode === 'messaging') {
          const picked = Object.entries(destChannels).filter(([, v]) => v).map(([k]) => k);
          const labelMap = { whatsapp: 'WhatsApp', instagram: t('Instagram DM','Instagram DM'), messenger: 'Messenger' };
          trafficLabel = t('Tráfico a','Traffic to');
          trafficValue = picked.length ? picked.map(k => labelMap[k] || k).join(' + ') : t('Mensajería','Messaging');
        } else {
          trafficLabel = t('Tráfico a','Traffic to');
          if (productUrl.trim()) {
            try {
              const u = new URL(productUrl.trim().startsWith('http') ? productUrl.trim() : `https://${productUrl.trim()}`);
              trafficValue = u.hostname.replace(/^www\./, '');
            } catch { trafficValue = productUrl.trim().slice(0, 40); }
          } else {
            trafficValue = t('Página web (sin URL)','Website (no URL)');
          }
        }
        return (
          <div style={{ padding:'14px 16px', background:'rgba(139,92,246,0.07)', border:`1px solid ${NX.border2}`, borderRadius:'12px', display:'flex', flexDirection:'column', gap:'8px' }}>
            <div style={{ fontSize:'10px', color: NX.accent, fontWeight:700, letterSpacing:'0.12em', fontFamily:"'DM Mono',monospace" }}>
              {t('LO QUE DECIDÍ POR TI', 'WHAT I DECIDED FOR YOU')}
            </div>
            <div style={{ display:'grid', gridTemplateColumns:'1fr 1fr', gap:'8px 16px', fontSize:'12px' }}>
              <div><span style={{ color: NX.muted }}>{t('Objetivo','Objective')}: </span><span style={{ color: NX.text, fontWeight:600 }}>{objectiveLabel}</span></div>
              <div><span style={{ color: NX.muted }}>{trafficLabel}: </span><span style={{ color: NX.text, fontWeight:600 }}>{trafficValue}</span></div>
              <div><span style={{ color: NX.muted }}>{t('País','Country')}: </span><span style={{ color: NX.text, fontWeight:600 }}>{country || '—'}</span></div>
              <div><span style={{ color: NX.muted }}>{t('Idioma','Language')}: </span><span style={{ color: NX.text, fontWeight:600 }}>{language === 'en' ? 'English' : 'Español'}</span></div>
              <div><span style={{ color: NX.muted }}>{t('Presupuesto','Budget')}: </span><span style={{ color: NX.text, fontWeight:600 }}>{dailyBudget ? `${formatBudget(Number(dailyBudget), adCurrency)}/${t('día','day')}` : '—'}</span></div>
              <div><span style={{ color: NX.muted }}>{t('Formato','Format')}: </span><span style={{ color: NX.text, fontWeight:600 }}>{destMode === 'existing_ig' ? t('Post original','Original post') : '4:5 (Feed)'}</span></div>
              {destMode !== 'existing_ig' && (
                <div style={{ gridColumn:'1 / -1' }}><span style={{ color: NX.muted }}>{t('Imágenes','Images')}: </span><span style={{ color: NX.text, fontWeight:600 }}>
                  {genMode === 'uploaded' ? `${ownImages.length} ${t('tuyas','yours')}` : `${imageCount} (${imageModel === 'gpt-image-2' ? 'Premium Ultra MAX' : 'Premium'})`}
                </span></div>
              )}
            </div>
          </div>
        );
      })()}
    </>);
  }

  // Header buttons. Modo avanzado removido — el flujo unificado es solo el asistente.
  // Mantengo el close X cuando se renderiza en modal (no embedded).
  const HeaderButtons = (
    <div style={{ position:'absolute', top:'14px', right:'14px', display:'flex', gap:'8px', zIndex:2 }}>
      {!embedded && (
        <button onClick={onClose}
          style={{ width:'32px', height:'32px', borderRadius:'50%', background:'rgba(26,17,40,0.6)', border:`1px solid ${NX.border}`, color:NX.muted, cursor:'pointer', display:'flex', alignItems:'center', justifyContent:'center' }}>
          <svg width="14" height="14" viewBox="0 0 16 16" fill="none"><path d="M4 4l8 8M12 4l-8 8" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round"/></svg>
        </button>
      )}
    </div>
  );

  // Embedded mode: Apple-style minimal — mascot floats free with a soft glow on the canvas
  // background. ONLY the chat panel on the right has a frame. The "Modo avanzado" button
  // floats in its top-right corner. No wrapping card, no border between mascot and chat.
  // mobileLayout=true: skip the standalone mascot column and inline a small mascot inside
  // the card, next to the question. Saves vertical space on phones.
  if (embedded) {
    return (
      <div style={{ width:'100%', maxWidth:'1440px', margin:'0 auto', padding: mobileLayout ? '0' : '8px 8px 24px' }}>
        <div className="nx-two-col" style={{ display:'flex', alignItems: mobileLayout ? 'stretch' : 'center', gap: mobileLayout ? 0 : '24px', minHeight: mobileLayout ? 'auto' : '520px' }}>
          {/* LEFT spacer (desktop only): mirror de la columna de la mascota para que el
              panel quede ópticamente centrado en la pantalla. flex shrink permite que el
              spacer ceda espacio si el viewport es estrecho. */}
          {!mobileLayout && <div style={{ flex:'0 1 340px', minWidth:0 }} aria-hidden="true"/>}
          {/* CENTER: chat panel — el "panel de trabajo" en el centro óptico. */}
          <div style={{
            flex:1, position:'relative', minWidth:0,
            background:'rgba(26,17,40,0.62)',
            border:`1px solid ${NX.border2}`,
            borderRadius:'22px',
            padding: mobileLayout
              ? (onAdvancedMode ? '50px 16px 18px' : '16px 16px 18px')
              : (onAdvancedMode ? '56px 24px 22px' : '20px 22px 22px'),
            boxShadow:'0 20px 60px rgba(10,6,16,0.45), inset 0 1px 0 rgba(255,255,255,0.04)',
            backdropFilter:'blur(16px)',
            WebkitBackdropFilter:'blur(16px)',
            display:'flex', flexDirection:'column',
          }}>
            {HeaderButtons}
            {StepIndicator}
            <div style={{ flex:1 }}>{body}</div>
            <div style={{ display:'flex', gap:'8px', marginTop: mobileLayout ? '18px' : '24px', alignItems:'stretch' }}>
              {step > 0 && (
                <button onClick={retreatStep}
                  style={{ flex:'0 0 auto', padding: mobileLayout ? '13px 16px' : '12px 18px', background:'transparent', border:`1px solid ${NX.border}`, borderRadius:'12px', color:NX.muted, fontSize:'13px', cursor:'pointer', fontFamily:"'DM Sans',sans-serif" }}>
                  ← {t('Atrás','Back')}
                </button>
              )}
              <div style={{ flex:1 }}/>
              <button onClick={canAdvance ? onPrimary : undefined} disabled={!canAdvance}
                style={{ flex: mobileLayout ? '1 1 auto' : '0 0 auto', padding:'13px 24px', background: canAdvance ? NX.accentGrad : 'rgba(139,92,246,0.18)', border:'none', borderRadius:'12px', color:'#fff', fontSize:'14px', fontWeight:600, cursor: canAdvance ? 'pointer' : 'not-allowed', fontFamily:"'DM Sans',sans-serif", boxShadow: canAdvance ? '0 8px 28px rgba(139,92,246,0.4)' : 'none', opacity: canAdvance ? 1 : 0.55, transition:'all 0.18s' }}>
                {primaryLabel}
              </button>
            </div>
          </div>
          {/* RIGHT (desktop only): saludo arriba + mascota debajo, todo en una columna
              centrada. El saludo queda alineado verticalmente con la altura del panel. */}
          {!mobileLayout && (
            <div style={{ flex:'0 0 340px', display:'flex', flexDirection:'column', alignItems:'center', justifyContent:'center', padding:'20px 0', gap:'18px' }}>
              <div style={{ textAlign:'center', maxWidth:'320px' }}>
                <h2 style={{ fontSize:'20px', fontWeight:400, letterSpacing:'-0.02em', color:NX.text, margin:'0 0 4px', lineHeight:1.2 }}>
                  {lang === 'en' ? 'Hi, welcome to ' : 'Hola, bienvenido a '}
                  <span style={{ background:'linear-gradient(135deg, #8b5cf6, #a78bfa)', WebkitBackgroundClip:'text', WebkitTextFillColor:'transparent', backgroundClip:'text' }}>NexWall Corp AI</span>
                  <span style={{ marginLeft:'6px' }}>👋</span>
                </h2>
                <p style={{ color: NX.muted, fontSize:'12px', margin:0, lineHeight:1.5 }}>
                  {lang === 'en' ? 'Your marketing copilot powered by AI' : 'Tu copiloto de marketing con IA'}
                </p>
              </div>
              {NebulaAvatar}
            </div>
          )}
        </div>
      </div>
    );
  }

  return (
    <div style={{ position:'fixed', inset:0, background:'rgba(0,0,0,0.85)', backdropFilter:'blur(14px)', zIndex:300, display:'flex', alignItems:'center', justifyContent:'center', padding:'24px' }}>
      <div style={{ background: NX.bg2, border:`1px solid ${NX.border2}`, borderRadius:'24px', width:'100%', maxWidth:'860px', maxHeight:'94vh', overflow:'auto', animation:'modalIn 0.22s ease', boxShadow:'0 30px 80px rgba(10,6,16,0.85), 0 0 60px rgba(139,92,246,0.18)', position:'relative' }}>
        {HeaderButtons}

        <div className="nx-two-col" style={{ display:'flex', alignItems:'center', gap:0, minHeight:'520px' }}>
          {/* LEFT: free-floating mascot with soft glow — no border between halves */}
          <div style={{ flex:'0 0 320px', display:'flex', alignItems:'center', justifyContent:'center', padding:'24px 8px' }}>
            {NebulaAvatar}
          </div>

          {/* RIGHT: question + answer */}
          <div style={{ flex:1, display:'flex', flexDirection:'column', padding:'40px 36px 28px', minWidth:0 }}>
            {StepIndicator}
            <div style={{ flex:1 }}>{body}</div>

            {/* Footer actions */}
            <div style={{ display:'flex', gap:'10px', marginTop:'24px', alignItems:'center' }}>
              {step > 0 && (
                <button onClick={retreatStep}
                  style={{ padding:'12px 18px', background:'transparent', border:`1px solid ${NX.border}`, borderRadius:'12px', color:NX.muted, fontSize:'13px', cursor:'pointer', fontFamily:"'DM Sans',sans-serif" }}>
                  ← {t('Atrás','Back')}
                </button>
              )}
              <div style={{ flex:1 }}/>
              {secondaryLabel && (
                <button onClick={onSecondary}
                  style={{ padding:'12px 18px', background:'transparent', border:`1px solid ${NX.border}`, borderRadius:'12px', color:NX.muted, fontSize:'13px', cursor:'pointer', fontFamily:"'DM Sans',sans-serif" }}>
                  {secondaryLabel}
                </button>
              )}
              <button onClick={canAdvance ? onPrimary : undefined} disabled={!canAdvance}
                style={{ padding:'13px 24px', background: canAdvance ? NX.accentGrad : 'rgba(139,92,246,0.18)', border:'none', borderRadius:'12px', color:'#fff', fontSize:'14px', fontWeight:600, cursor: canAdvance ? 'pointer' : 'not-allowed', fontFamily:"'DM Sans',sans-serif", boxShadow: canAdvance ? '0 8px 28px rgba(139,92,246,0.4)' : 'none', opacity: canAdvance ? 1 : 0.55, transition:'all 0.18s' }}>
                {primaryLabel}
              </button>
            </div>
          </div>
        </div>
      </div>
    </div>
  );
};

// ══════════════════════════════════════════
// ANIMATION: META OAUTH SUCCESS
// ══════════════════════════════════════════
const MetaSuccessScreen = ({ onComplete, tc }) => {
  const [phase, setPhase] = React.useState('logo'); // logo → dissolve → check → done

  React.useEffect(() => {
    const t1 = setTimeout(() => setPhase('dissolve'), 1200);
    const t2 = setTimeout(() => setPhase('check'),    1900);
    const t3 = setTimeout(() => setPhase('done'),     3000);
    const t4 = setTimeout(() => onComplete(),         3600);
    return () => [t1,t2,t3,t4].forEach(clearTimeout);
  }, []);

  const style = `
    @keyframes logoIn   { from { opacity:0; transform:scale(0.6) } to { opacity:1; transform:scale(1) } }
    @keyframes pulse    { 0%,100% { box-shadow:0 0 0 0 rgba(139,92,246,0.4) } 50% { box-shadow:0 0 0 20px rgba(139,92,246,0) } }
    @keyframes dissolve { from { opacity:1; transform:scale(1) filter:blur(0) } to { opacity:0; transform:scale(1.4); filter:blur(8px) } }
    @keyframes checkIn  { from { opacity:0; transform:scale(0.4) } to { opacity:1; transform:scale(1) } }
    @keyframes checkDraw{ from { stroke-dashoffset:60 } to { stroke-dashoffset:0 } }
    @keyframes ringPop  { from { transform:scale(0.5); opacity:0 } to { transform:scale(1); opacity:1 } }
    @keyframes textUp   { from { opacity:0; transform:translateY(12px) } to { opacity:1; transform:translateY(0) } }
    @keyframes spark    { 0% { transform:scale(0) rotate(0deg); opacity:1 }
                          60% { transform:scale(1) rotate(180deg); opacity:1 }
                          100% { transform:scale(0.8) rotate(360deg); opacity:0 } }
  `;

  const particles = [0,1,2,3,4,5,6,7].map(i => {
    const angle = (i / 8) * 360;
    const dist = 54 + Math.random() * 16;
    return { angle, dist, delay: 0.05 * i, size: 3 + Math.random() * 4 };
  });

  return (
    <div style={{
      position: 'fixed', inset: 0, zIndex: 1000,
      background: 'rgba(8,8,8,0.96)',
      display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center',
      backdropFilter: 'blur(12px)',
    }}>
      <style>{style}</style>

      <div style={{ position: 'relative', width: '120px', height: '120px', marginBottom: '32px' }}>

        {/* Logo phase */}
        {(phase === 'logo' || phase === 'dissolve') && (
          <div style={{
            position: 'absolute', inset: 0, borderRadius: '50%',
            background: 'linear-gradient(135deg, rgba(139,92,246,0.12), rgba(139,92,246,0.12))',
            border: '1.5px solid rgba(139,92,246,0.3)',
            display: 'flex', alignItems: 'center', justifyContent: 'center',
            animation: phase === 'logo'
              ? 'logoIn 0.5s cubic-bezier(0.34,1.56,0.64,1) forwards, pulse 1.4s ease-in-out 0.5s infinite'
              : 'dissolve 0.6s ease-in forwards',
          }}>
            <RobotIcon size={52}/>
          </div>
        )}

        {/* Check phase */}
        {(phase === 'check' || phase === 'done') && (<>
          {/* Spark particles */}
          {particles.map((p, i) => (
            <div key={i} style={{
              position: 'absolute',
              top: '50%', left: '50%',
              width: `${p.size}px`, height: `${p.size}px`,
              borderRadius: '50%',
              background: i % 2 === 0 ? NX.accent : NX.accent2,
              transform: `rotate(${p.angle}deg) translateX(${p.dist}px)`,
              marginTop: `-${p.size/2}px`, marginLeft: `-${p.size/2}px`,
              animation: `spark 0.7s ease-out ${p.delay}s both`,
            }}/>
          ))}

          {/* Green ring */}
          <div style={{
            position: 'absolute', inset: 0, borderRadius: '50%',
            background: 'rgba(52,211,153,0.1)',
            border: `2px solid ${NX.success}`,
            animation: 'ringPop 0.4s cubic-bezier(0.34,1.56,0.64,1) forwards',
            boxShadow: `0 0 32px rgba(52,211,153,0.25)`,
          }}/>

          {/* Checkmark */}
          <div style={{
            position: 'absolute', inset: 0,
            display: 'flex', alignItems: 'center', justifyContent: 'center',
            animation: 'checkIn 0.35s cubic-bezier(0.34,1.56,0.64,1) 0.1s both',
          }}>
            <svg width="52" height="52" viewBox="0 0 52 52" fill="none">
              <path d="M10 26l11 11 21-22"
                stroke={NX.success} strokeWidth="4"
                strokeLinecap="round" strokeLinejoin="round"
                strokeDasharray="60" strokeDashoffset="60"
                style={{ animation: 'checkDraw 0.5s ease-out 0.2s forwards' }}/>
            </svg>
          </div>
        </>)}
      </div>

      {/* Text */}
      <div style={{ textAlign: 'center', animation: phase !== 'logo' ? 'textUp 0.5s ease forwards' : 'none', opacity: phase === 'logo' ? 0 : 1 }}>
        {phase === 'dissolve' && (
          <p style={{ fontSize: '14px', color: NX.muted, letterSpacing: '0.05em' }}>Verificando conexión...</p>
        )}
        {(phase === 'check' || phase === 'done') && (<>
          <h2 style={{ fontSize: '22px', fontWeight: 500, color: NX.text, margin: '0 0 8px', letterSpacing: '-0.02em' }}>
            {tc.successTitle}
          </h2>
          <p style={{ fontSize: '13px', color: NX.muted, maxWidth: '280px' }}>{tc.successMsg}</p>
          {phase === 'done' && (
            <div style={{ marginTop: '20px', display: 'flex', alignItems: 'center', gap: '6px', justifyContent: 'center', fontSize: '12px', color: NX.muted }}>
              <div style={{ width: '6px', height: '6px', borderRadius: '50%', background: NX.success, animation: 'pulse 1s ease infinite' }}/>
              Continuando al paso 2...
            </div>
          )}
        </>)}
      </div>
    </div>
  );
};

// ══════════════════════════════════════════
// FLOW: CONECTAR CANALES (Step 1)
// ══════════════════════════════════════════
const ConnectFlow = ({ onComplete, onClose, oauthResult, lang='es' }) => {
  const tc = (I18N && I18N[lang]) ? I18N[lang].connect : I18N.es.connect;
  // oauthResult = 'success' | 'error' | null (passed from App after OAuth redirect)
  const [selected, setSelected] = React.useState(null);

  const platforms = [
    { id: 'meta', name: 'Meta Ads', desc: 'Facebook e Instagram', color: '#1877F2',
      icon: <svg width="24" height="24" viewBox="0 0 24 24" fill="#1877F2"><path d="M24 12.073c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.99 4.388 10.954 10.125 11.854v-8.385H7.078v-3.47h3.047V9.43c0-3.007 1.792-4.669 4.533-4.669 1.312 0 2.686.235 2.686.235v2.953H15.83c-1.491 0-1.956.925-1.956 1.874v2.25h3.328l-.532 3.47h-2.796v8.385C19.612 23.027 24 18.062 24 12.073z"/></svg> },
    { id: 'google', name: 'Google Ads', desc: 'Search, Display y YouTube', color: '#4285F4', soon: true,
      icon: <svg width="24" height="24" viewBox="0 0 24 24"><path d="M12.48 10.92v3.28h7.84c-.24 1.84-.853 3.187-1.787 4.133-1.147 1.147-2.933 2.4-6.053 2.4-4.827 0-8.6-3.893-8.6-8.72s3.773-8.72 8.6-8.72c2.6 0 4.507 1.027 5.907 2.347l2.307-2.307C18.747 1.44 16.133 0 12.48 0 5.867 0 .307 5.387.307 12s5.56 12 12.173 12c3.573 0 6.267-1.173 8.373-3.36 2.16-2.16 2.84-5.213 2.84-7.667 0-.76-.053-1.467-.173-2.053H12.48z" fill="#4285F4"/></svg> },
    { id: 'tiktok', name: 'TikTok Ads', desc: 'Audiencias jóvenes y virales', color: '#fff', soon: true,
      icon: <svg width="24" height="24" viewBox="0 0 24 24" fill="white"><path d="M19.59 6.69a4.83 4.83 0 01-3.77-4.25V2h-3.45v13.67a2.89 2.89 0 01-2.88 2.5 2.89 2.89 0 01-2.89-2.89 2.89 2.89 0 012.89-2.89c.28 0 .54.04.79.1V9.01a6.33 6.33 0 00-.79-.05 6.34 6.34 0 00-6.34 6.34 6.34 6.34 0 006.34 6.34 6.34 6.34 0 006.33-6.34V8.69a8.24 8.24 0 004.82 1.55V6.79a4.85 4.85 0 01-1.05-.1z"/></svg> },
  ];

  const connectMeta = () => {
    const token = localStorage.getItem('nw_token');
    window.location.href = `${API}/auth/meta?token=${token}`;
  };

  // ── Returned from OAuth with success — branded animation ──
  if (oauthResult === 'success') {
    return <MetaSuccessScreen onComplete={onComplete} tc={tc} />;
  }

  // ── Returned from OAuth with error ──
  if (oauthResult === 'error') {
    return (
      <NxModal onClose={onClose}>
        <div style={{ padding: '28px', textAlign: 'center' }}>
          <div style={{ width: '64px', height: '64px', borderRadius: '50%', background: 'rgba(248,113,113,0.1)', border: '1px solid rgba(248,113,113,0.3)', display: 'flex', alignItems: 'center', justifyContent: 'center', margin: '0 auto 20px' }}>
            <svg width="28" height="28" viewBox="0 0 28 28" fill="none"><path d="M8 8l12 12M20 8L8 20" stroke={NX.danger} strokeWidth="2.5" strokeLinecap="round"/></svg>
          </div>
          <h3 style={{ fontSize: '18px', fontWeight: 500, color: NX.text, marginBottom: '8px' }}>{tc.errorTitle}</h3>
          <p style={{ color: NX.muted, fontSize: '13px', marginBottom: '24px' }}>{tc.errorMsg}</p>
          <div style={{ display: 'flex', gap: '10px' }}>
            <NxButton variant="ghost" onClick={onClose} full>{tc.cancel}</NxButton>
            <NxButton variant="accent" onClick={connectMeta} full>{tc.retry}</NxButton>
          </div>
        </div>
      </NxModal>
    );
  }

  // ── Default: choose platform ──
  return (
    <NxModal onClose={onClose}>
      <div style={{ padding: '28px' }}>
        <div style={{ marginBottom: '24px' }}>
          <h2 style={{ fontSize: '18px', fontWeight: 500, color: NX.text, margin: '0 0 6px' }}>{tc.title}</h2>
          <p style={{ color: NX.muted, fontSize: '13px', margin: 0 }}>{tc.subtitle}</p>
        </div>
        <div style={{ display: 'flex', flexDirection: 'column', gap: '10px', marginBottom: '24px' }}>
          {platforms.map(p => (
            <div key={p.id} className="nx-platform-row" onClick={() => !p.soon && setSelected(p.id)}
              style={{
                display: 'flex', alignItems: 'center', gap: '14px', padding: '14px 16px',
                background: selected === p.id ? NX.bg4 : NX.bg3,
                border: `1px solid ${selected === p.id ? NX.border2 : NX.border}`,
                borderRadius: '12px', cursor: p.soon ? 'default' : 'pointer',
                opacity: p.soon ? 0.5 : 1, transition: 'all 0.15s',
              }}>
              <div className="nx-platform-icon" style={{ width: '40px', height: '40px', borderRadius: '10px', background: NX.bg, display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0 }}>{p.icon}</div>
              <div style={{ flex: 1 }}>
                <div style={{ fontSize: '14px', fontWeight: 500, color: NX.text }}>{p.name} {p.soon && <span style={{ fontSize: '11px', color: NX.muted, fontWeight: 400 }}>· {tc.soon}</span>}</div>
                <div style={{ fontSize: '12px', color: NX.muted }}>{p.desc}</div>
              </div>
              <div style={{ width: '18px', height: '18px', borderRadius: '50%', border: `2px solid ${selected === p.id ? NX.accent : NX.border}`, background: selected === p.id ? NX.accent : 'transparent', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
                {selected === p.id && <div style={{ width: '6px', height: '6px', borderRadius: '50%', background: '#fff' }}/>}
              </div>
            </div>
          ))}
        </div>
        <div style={{ display: 'flex', gap: '10px' }}>
          <NxButton variant="ghost" onClick={onClose} full>{tc.cancel}</NxButton>
          <NxButton variant="accent" onClick={() => selected === 'meta' && connectMeta()} disabled={!selected} full>
            {tc.connectBtn} {selected ? platforms.find(p=>p.id===selected)?.name : ''}
          </NxButton>
        </div>
      </div>
    </NxModal>
  );
};

// ══════════════════════════════════════════
// FLOW: CONFIGURAR ACTIVOS (Step 2)
// ══════════════════════════════════════════
const AssetsFlow = ({ onComplete, onClose, lang='es' }) => {
  const ta = (I18N && I18N[lang]) ? I18N[lang].assets : I18N.es.assets;
  const [step, setStep] = React.useState(0);
  const [loading, setLoading] = React.useState(false);
  const [apiError, setApiError] = React.useState(null);

  // Selected values
  const [selectedBM, setSelectedBM] = React.useState(null);
  const [selectedAccount, setSelectedAccount] = React.useState(null);
  const [selectedPage, setSelectedPage] = React.useState(null);
  const [selectedPixel, setSelectedPixel] = React.useState(null);

  // Data from API
  const [businesses, setBusinesses] = React.useState([]);
  const [adAccounts, setAdAccounts] = React.useState([]);
  const [pages, setPages] = React.useState([]);
  const [igAccount, setIgAccount] = React.useState(null);
  const [waNumbers, setWaNumbers] = React.useState([]);
  const [pageWaNumber, setPageWaNumber] = React.useState(null);
  // Flag manual: el user confirmó que su Page tiene WABA linked aunque NexWall no pudo
  // detectar el número (requiere whatsapp_business_management / Tech Provider). Se persiste
  // en meta_assets.whatsapp_via_waba y `runMetaPublish` igual usa destination_type=WHATSAPP.
  const [whatsappViaWaba, setWhatsappViaWaba] = React.useState(false);
  const [pixels, setPixels] = React.useState([]);
  const [siteUrl, setSiteUrl] = React.useState('');
  const [pixelStatus, setPixelStatus] = React.useState(null); // null | 'found' | 'not_found'
  const [creatingPixel, setCreatingPixel] = React.useState(false);
  const [pixelError, setPixelError] = React.useState(null);
  const [newPixelName, setNewPixelName] = React.useState('');

  const stepLabels = ta.stepLabels;

  const apiFetch = async (path) => {
    const res = await authFetch(path);
    if (!res.ok) {
      const err = await res.json().catch(() => ({}));
      throw new Error(err.error || `Error ${res.status}`);
    }
    return res.json();
  };

  // Load Business Managers on mount
  React.useEffect(() => {
    setLoading(true);
    setApiError(null);
    apiFetch('/api/meta/businesses')
      .then(data => setBusinesses(data))
      .catch(e => setApiError(e.message))
      .finally(() => setLoading(false));
  }, []);

  const selectBM = async (bm) => {
    setSelectedBM(bm);
    setLoading(true);
    setApiError(null);
    try {
      const accounts = await apiFetch(`/api/meta/businesses/${bm.id}/adaccounts`);
      setAdAccounts(accounts);
      setStep(1);
    } catch (e) { setApiError(e.message); }
    finally { setLoading(false); }
  };

  const selectAccount = async (account) => {
    setSelectedAccount(account);
    setLoading(true);
    setApiError(null);
    try {
      const pgs = await apiFetch('/api/meta/pages');
      setPages(pgs);
      setStep(2);
    } catch (e) { setApiError(e.message); }
    finally { setLoading(false); }
  };

  const selectPage = async (page) => {
    setSelectedPage(page);
    setLoading(true);
    setApiError(null);
    try {
      // Forward the Page access token so the backend can also probe the legacy
      // `connected_instagram_account` / `page_backed_instagram_accounts` fields,
      // which require a Page token (User token returns Graph #190).
      const qs = page.access_token ? `?page_access_token=${encodeURIComponent(page.access_token)}` : '';
      const ig = await apiFetch(`/api/meta/pages/${page.id}/instagram${qs}`);
      setIgAccount(ig);
      setStep(3);
    } catch (e) { setApiError(e.message); }
    finally { setLoading(false); }
  };

  const goToWhatsApp = async () => {
    setLoading(true);
    try {
      // 1) Try the WABA endpoint (requires whatsapp_business_management scope — usually empty
      //    until NexWall is approved as Tech Provider).
      const wa = await apiFetch(`/api/meta/businesses/${selectedBM.id}/whatsapp`).catch(() => []);
      setWaNumbers(wa || []);
      // 2) Fallback que SIEMPRE funciona con nuestros scopes: leer `page.whatsapp_number`
      //    (legacy). Si la Page tiene una WABA moderna sin número legacy, no la podemos
      //    detectar con los scopes actuales (requiere whatsapp_business_management que
      //    necesita Tech Provider Status) y caemos al fallback "¿Quieres conectar WhatsApp?".
      if (selectedPage?.id) {
        const r = await authFetch(`/api/meta/page-whatsapp?page_id=${encodeURIComponent(selectedPage.id)}`);
        if (r.ok) {
          const data = await r.json();
          if (data.whatsapp_number) setPageWaNumber(data.whatsapp_number);
        }
      }
    } catch (e) { /* WA is optional, ignore errors */ }
    finally { setLoading(false); setStep(4); }
  };

  const createPixel = async () => {
    if (!selectedAccount?.id) {
      setPixelError(lang === 'en' ? 'No ad account selected.' : 'No se seleccionó cuenta publicitaria.');
      return;
    }
    setCreatingPixel(true);
    setPixelError(null);
    try {
      const name = (newPixelName || selectedBM?.name || 'NexWall').trim() + ' Pixel';
      const r = await authFetch(`/api/meta/adaccounts/${encodeURIComponent(selectedAccount.id)}/pixels`, {
        method: 'POST',
        body: JSON.stringify({ name }),
      });
      const data = await r.json();
      if (!r.ok) throw new Error(data.error || 'Error creando pixel');
      // Auto-select and persist — user was just told "we need a pixel"; we want zero friction here.
      setPixels([data]);
      setSelectedPixel(data);
      await persistAndComplete(data);
    } catch (e) {
      setPixelError(e.message || 'Error creando pixel');
    }
    setCreatingPixel(false);
  };

  const loadPixels = async () => {
    setLoading(true);
    try {
      // Pixels asignados a la cuenta publicitaria (lo que el usuario ve en Events Manager)
      let px = [];
      if (selectedAccount?.id) {
        try { px = await apiFetch(`/api/meta/adaccounts/${encodeURIComponent(selectedAccount.id)}/pixels`); } catch {}
      }
      // Fallback: pixels del Business Manager
      if (!px || px.length === 0) {
        try { px = await apiFetch(`/api/meta/businesses/${selectedBM.id}/pixels`); } catch {}
      }
      setPixels(px || []);
    } catch (e) { /* optional */ }
    finally { setLoading(false); setStep(5); }
  };

  const persistAndComplete = async (pixelOverride) => {
    const pixel = pixelOverride !== undefined ? pixelOverride : selectedPixel;
    const bizId = localStorage.getItem('nw_active_biz_id') || '1';
    const payload = {
      biz_id: bizId,
      bm_id: selectedBM?.id || null,
      ad_account_id: selectedAccount?.id || null,
      ad_account_currency: selectedAccount?.currency || null,
      ad_account_name: selectedAccount?.name || null,
      page_id: selectedPage?.id || null,
      page_name: selectedPage?.name || null,
      page_access_token: selectedPage?.access_token || null,
      ig_id: igAccount?.id || null,
      pixel_id: pixel?.id || null,
      page_whatsapp_number: pageWaNumber || null,
      whatsapp_via_waba: whatsappViaWaba || false,
    };
    try {
      const res = await authFetch('/api/meta/assets', { method: 'POST', body: JSON.stringify(payload) });
      if (!res.ok) {
        const d = await res.json().catch(() => ({}));
        setApiError(d.error || `Error al guardar activos (HTTP ${res.status})`);
        return;
      }
      // Backend OK — ahora sí cacheamos en localStorage y marcamos el paso como hecho.
      localStorage.setItem(`nw_assets_${bizId}`, JSON.stringify({
        ad_account_id: payload.ad_account_id,
        ad_account_currency: payload.ad_account_currency,
        ad_account_name: payload.ad_account_name,
        page_id: payload.page_id,
        page_name: payload.page_name,
        ig_id: payload.ig_id,
        pixel_id: payload.pixel_id,
        page_whatsapp_number: payload.page_whatsapp_number,
      }));
    } catch (e) {
      setApiError(e.message || 'Error al guardar activos. Verifica tu conexión.');
      return;
    }
    onComplete();
  };

  const OptionList = ({ items, labelFn, sublabelFn, onSelect, loading }) => (
    <div style={{ display: 'flex', flexDirection: 'column', gap: '8px', marginBottom: '20px' }}>
      {loading
        ? [1,2,3].map(i => <div key={i} style={{ height: '52px', background: NX.bg3, borderRadius: '10px', border: `1px solid ${NX.border}`, opacity: 0.5 }}/>)
        : items.length === 0
          ? <div style={{ padding: '20px', textAlign: 'center', color: NX.muted, fontSize: '13px' }}>{ta.noItems}</div>
          : items.map((item, i) => (
            <div key={i} onClick={() => onSelect(item)}
              style={{ padding: '14px 16px', background: NX.bg3, border: `1px solid ${NX.border}`, borderRadius: '10px', cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'space-between', transition: 'all 0.15s' }}>
              <div>
                <div style={{ fontSize: '14px', color: NX.text, fontWeight: 500 }}>{labelFn(item)}</div>
                {sublabelFn && <div style={{ fontSize: '12px', color: NX.muted, marginTop: '2px' }}>{sublabelFn(item)}</div>}
              </div>
              <svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M6 4l4 4-4 4" stroke={NX.muted} strokeWidth="1.3" strokeLinecap="round"/></svg>
            </div>
          ))
      }
    </div>
  );

  return (
    <NxModal onClose={onClose} noBackdropClose>
      <div style={{ padding: '28px' }}>
        <NxProgress steps={stepLabels} current={step} completed={Array.from({length: step}, (_,i) => i)}/>

        {apiError && (
          <div style={{ margin: '0 0 16px', padding: '12px', background: 'rgba(248,113,113,0.08)', border: '1px solid rgba(248,113,113,0.2)', borderRadius: '10px', fontSize: '13px', color: NX.danger }}>
            {apiError}
          </div>
        )}

        {/* Step 0: Business Manager */}
        {step === 0 && <>
          <h2 style={{ fontSize: '17px', fontWeight: 500, color: NX.text, margin: '0 0 6px' }}>{ta.bm.title}</h2>
          <p style={{ color: NX.muted, fontSize: '13px', margin: '0 0 20px' }}>{ta.bm.subtitle}</p>
          <OptionList
            items={businesses}
            loading={loading}
            labelFn={b => b.name}
            sublabelFn={b => `ID: ${b.id}`}
            onSelect={selectBM}
          />
          <NxButton variant="ghost" onClick={onClose} full>{ta.cancel}</NxButton>
        </>}

        {/* Step 1: Ad Account */}
        {step === 1 && <>
          <h2 style={{ fontSize: '17px', fontWeight: 500, color: NX.text, margin: '0 0 6px' }}>{ta.account.title}</h2>
          <p style={{ color: NX.muted, fontSize: '13px', margin: '0 0 20px' }}>{ta.account.subtitle}</p>
          <OptionList
            items={adAccounts}
            loading={loading}
            labelFn={a => a.name}
            sublabelFn={a => `${a.currency} · ${a.account_status === 1 ? ta.activeAccount : ta.inactiveAccount}`}
            onSelect={selectAccount}
          />
          <NxButton variant="ghost" onClick={() => setStep(0)} full>{ta.back}</NxButton>
        </>}

        {/* Step 2: Facebook Page */}
        {step === 2 && <>
          <h2 style={{ fontSize: '17px', fontWeight: 500, color: NX.text, margin: '0 0 6px' }}>{ta.page.title}</h2>
          <p style={{ color: NX.muted, fontSize: '13px', margin: '0 0 20px' }}>{ta.page.subtitle}</p>
          <OptionList
            items={pages}
            loading={loading}
            labelFn={p => p.name}
            sublabelFn={p => p.category || ''}
            onSelect={selectPage}
          />
          <NxButton variant="ghost" onClick={() => setStep(1)} full>{ta.back}</NxButton>
        </>}

        {/* Step 3: Instagram — explicit Continue button so the user doesn't accidentally
            skip by clicking the IG card (previous version had onClick=goToWhatsApp on the
            whole card, easy to misclick). The card is now informational; advance via button. */}
        {step === 3 && <>
          <h2 style={{ fontSize: '17px', fontWeight: 500, color: NX.text, margin: '0 0 6px' }}>{ta.instagram.title}</h2>
          <p style={{ color: NX.muted, fontSize: '13px', margin: '0 0 20px' }}>{ta.instagram.subtitle}</p>
          {loading
            ? <div style={{ height: '52px', background: NX.bg3, borderRadius: '10px', border: `1px solid ${NX.border}`, marginBottom: '20px' }}/>
            : igAccount
              ? <div style={{ padding: '14px 16px', background: NX.bg3, border: `1px solid ${NX.border2}`, borderRadius: '10px', display: 'flex', alignItems: 'center', gap: '12px', marginBottom: '20px' }}>
                  {igAccount.profile_picture_url && <img src={igAccount.profile_picture_url} alt="" style={{ width: '36px', height: '36px', borderRadius: '50%' }}/>}
                  <div style={{ flex: 1, minWidth: 0 }}>
                    <div style={{ fontSize: '14px', color: NX.text, fontWeight: 500 }}>@{igAccount.username}</div>
                    <div style={{ fontSize: '12px', color: NX.muted }}>{igAccount.followers_count?.toLocaleString()} {lang === 'en' ? 'followers' : 'seguidores'}</div>
                  </div>
                  <NxBadge color="success">{lang === 'en' ? 'Connected' : 'Conectado'}</NxBadge>
                </div>
              : <div style={{ padding: '16px', background: NX.bg3, border: `1px solid ${NX.border}`, borderRadius: '10px', marginBottom: '20px', fontSize: '13px', color: NX.muted, display: 'flex', flexDirection: 'column', gap: '12px' }}>
                  <div>{ta.noIg}</div>
                  {selectedPage?.id && (
                    <a
                      href={`https://www.facebook.com/${selectedPage.id}/settings/?tab=linked_accounts`}
                      target="_blank"
                      rel="noopener noreferrer"
                      style={{ display: 'inline-flex', alignItems: 'center', justifyContent: 'center', gap: '8px', fontSize: '12px', color: NX.text, background: 'rgba(139,92,246,0.12)', border: '1px solid rgba(139,92,246,0.35)', borderRadius: '8px', padding: '9px 12px', textDecoration: 'none', fontWeight: 600, fontFamily: "'DM Sans',sans-serif" }}
                    >
                      <svg width="13" height="13" viewBox="0 0 24 24" fill="none"><path d="M12 2.16c3.2 0 3.58.01 4.85.07 1.17.05 1.8.25 2.23.41.56.22.96.48 1.38.9.42.42.68.82.9 1.38.16.42.36 1.06.41 2.23.06 1.27.07 1.65.07 4.85s-.01 3.58-.07 4.85c-.05 1.17-.25 1.8-.41 2.23-.22.56-.48.96-.9 1.38-.42.42-.82.68-1.38.9-.42.16-1.06.36-2.23.41-1.27.06-1.65.07-4.85.07s-3.58-.01-4.85-.07c-1.17-.05-1.8-.25-2.23-.41-.56-.22-.96-.48-1.38-.9-.42-.42-.68-.82-.9-1.38-.16-.42-.36-1.06-.41-2.23C2.17 15.58 2.16 15.2 2.16 12s.01-3.58.07-4.85c.05-1.17.25-1.8.41-2.23.22-.56.48-.96.9-1.38.42-.42.82-.68 1.38-.9.42-.16 1.06-.36 2.23-.41C8.42 2.17 8.8 2.16 12 2.16zM12 0C8.74 0 8.33.01 7.05.07 5.78.13 4.9.33 4.14.63c-.79.31-1.46.72-2.13 1.38C1.35 2.68.94 3.35.63 4.14.33 4.9.13 5.78.07 7.05.01 8.33 0 8.74 0 12s.01 3.67.07 4.95c.06 1.27.26 2.15.56 2.91.31.79.72 1.46 1.38 2.13.67.66 1.34 1.07 2.13 1.38.76.3 1.64.5 2.91.56C8.33 23.99 8.74 24 12 24s3.67-.01 4.95-.07c1.27-.06 2.15-.26 2.91-.56.79-.31 1.46-.72 2.13-1.38.66-.67 1.07-1.34 1.38-2.13.3-.76.5-1.64.56-2.91.06-1.28.07-1.69.07-4.95s-.01-3.67-.07-4.95c-.06-1.27-.26-2.15-.56-2.91-.31-.79-.72-1.46-1.38-2.13C21.32 1.35 20.65.94 19.86.63c-.76-.3-1.64-.5-2.91-.56C15.67.01 15.26 0 12 0zm0 5.84c-3.4 0-6.16 2.76-6.16 6.16s2.76 6.16 6.16 6.16 6.16-2.76 6.16-6.16-2.76-6.16-6.16-6.16zm0 10.16c-2.21 0-4-1.79-4-4s1.79-4 4-4 4 1.79 4 4-1.79 4-4 4zm6.41-11.85a1.44 1.44 0 100 2.88 1.44 1.44 0 000-2.88z" fill="currentColor"/></svg>
                      {lang === 'en' ? 'Link Instagram on Meta' : 'Vincular Instagram en Meta'}
                      <svg width="10" height="10" viewBox="0 0 16 16" fill="none"><path d="M6 3h7v7M13 3L4 12" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/></svg>
                    </a>
                  )}
                </div>
          }
          <div style={{ display: 'flex', gap: '10px' }}>
            <NxButton variant="ghost" onClick={() => setStep(2)} full>{ta.back}</NxButton>
            {!loading && (
              <NxButton variant="accent" onClick={goToWhatsApp} full>
                {igAccount
                  ? (lang === 'en' ? `Continue with @${igAccount.username}` : `Continuar con @${igAccount.username}`)
                  : ta.continueNoIg}
              </NxButton>
            )}
          </div>
        </>}

        {/* Step 4: WhatsApp */}
        {step === 4 && <>
          <h2 style={{ fontSize: '17px', fontWeight: 500, color: NX.text, margin: '0 0 6px' }}>{ta.wa.title}</h2>
          <p style={{ color: NX.muted, fontSize: '13px', margin: '0 0 20px' }}>{ta.wa.subtitle}</p>
          {waNumbers.length > 0 ? (
            <OptionList items={waNumbers} loading={loading} labelFn={w => w.name} sublabelFn={w => w.phone_numbers?.[0]?.display_phone_number} onSelect={loadPixels}/>
          ) : pageWaNumber ? (
            // Detected the WhatsApp number already linked on the user's Facebook Page.
            // Surface it as a confirmation card — clicking continues straight to the next step.
            <div style={{ display: 'flex', flexDirection: 'column', gap: '10px', marginBottom: '20px' }}>
              <div style={{ padding: '14px 16px', background: 'rgba(37,211,102,0.08)', border: '1px solid rgba(37,211,102,0.35)', borderRadius: '12px', display: 'flex', alignItems: 'center', gap: '12px' }}>
                <div style={{ width: '40px', height: '40px', borderRadius: '50%', background: '#25D366', display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0 }}>
                  <svg width="22" height="22" viewBox="0 0 24 24" fill="#fff"><path d="M17.472 14.382c-.297-.149-1.758-.867-2.03-.967-.273-.099-.471-.148-.67.15-.197.297-.767.966-.94 1.164-.173.199-.347.223-.644.075-.297-.15-1.255-.463-2.39-1.475-.883-.788-1.48-1.761-1.653-2.059-.173-.297-.018-.458.13-.606.134-.133.298-.347.446-.52.149-.174.198-.298.298-.497.099-.198.05-.371-.025-.52-.075-.149-.669-1.612-.916-2.207-.242-.579-.487-.5-.669-.51-.173-.008-.371-.01-.57-.01-.198 0-.52.074-.792.372-.272.297-1.04 1.016-1.04 2.479 0 1.462 1.065 2.875 1.213 3.074.149.198 2.096 3.2 5.077 4.487.709.306 1.262.489 1.694.625.712.227 1.36.195 1.871.118.571-.085 1.758-.719 2.006-1.413.248-.694.248-1.289.173-1.413-.074-.124-.272-.198-.57-.347m-5.421 7.403h-.004a9.87 9.87 0 01-5.031-1.378l-.361-.214-3.741.982.998-3.648-.235-.374a9.86 9.86 0 01-1.51-5.26c.001-5.45 4.436-9.884 9.888-9.884 2.64 0 5.122 1.03 6.988 2.898a9.825 9.825 0 012.893 6.994c-.003 5.45-4.437 9.884-9.885 9.884"/></svg>
                </div>
                <div style={{ flex: 1, minWidth: 0 }}>
                  <div style={{ fontSize: '11px', color: NX.muted, fontFamily: "'DM Mono',monospace", letterSpacing: '0.06em', marginBottom: '2px' }}>
                    {lang === 'en' ? 'WHATSAPP DETECTED ON YOUR PAGE' : 'WHATSAPP DETECTADO EN TU PÁGINA'}
                  </div>
                  <div style={{ fontSize: '16px', fontWeight: 600, color: NX.text }}>{pageWaNumber}</div>
                </div>
              </div>
              <NxCard onClick={loadPixels} style={{ cursor: 'pointer' }}>
                <div style={{ fontWeight: 500, color: NX.text, marginBottom: '4px' }}>
                  {lang === 'en' ? 'Use this number for ads' : 'Usar este número para los anuncios'}
                </div>
                <div style={{ fontSize: '13px', color: NX.muted }}>
                  {lang === 'en' ? 'Customers who click the WhatsApp button will start a chat with you here.' : 'Los clientes que toquen el botón de WhatsApp iniciarán un chat con vos en este número.'}
                </div>
              </NxCard>
              <NxCard onClick={loadPixels} style={{ cursor: 'pointer' }}>
                <div style={{ fontWeight: 500, color: NX.text, marginBottom: '4px' }}>{ta.notNow}</div>
                <div style={{ fontSize: '13px', color: NX.muted }}>{ta.notNowDesc}</div>
              </NxCard>
            </div>
          ) : (
            <div style={{ display: 'flex', flexDirection: 'column', gap: '10px', marginBottom: '20px' }}>
              {/* Card NUEVA: el user confirma manualmente que su Page tiene WABA moderna linked.
                  NexWall no puede leer el número (requiere whatsapp_business_management / Tech
                  Provider) pero al lanzar la ad con destination_type=WHATSAPP, Meta resuelve
                  el WABA conectado a la Page del lado servidor. */}
              <NxCard onClick={() => { setWhatsappViaWaba(true); loadPixels(); }} style={{ cursor: 'pointer', borderColor: 'rgba(37,211,102,0.35)', background: 'rgba(37,211,102,0.06)' }}>
                <div style={{ display: 'flex', alignItems: 'center', gap: '10px', marginBottom: '4px' }}>
                  <div style={{ width: '24px', height: '24px', borderRadius: '50%', background: '#25D366', display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0 }}>
                    <svg width="14" height="14" viewBox="0 0 24 24" fill="#fff"><path d="M17.472 14.382c-.297-.149-1.758-.867-2.03-.967-.273-.099-.471-.148-.67.15-.197.297-.767.966-.94 1.164-.173.199-.347.223-.644.075-.297-.15-1.255-.463-2.39-1.475-.883-.788-1.48-1.761-1.653-2.059-.173-.297-.018-.458.13-.606.134-.133.298-.347.446-.52.149-.174.198-.298.298-.497.099-.198.05-.371-.025-.52-.075-.149-.669-1.612-.916-2.207-.242-.579-.487-.5-.669-.51-.173-.008-.371-.01-.57-.01-.198 0-.52.074-.792.372-.272.297-1.04 1.016-1.04 2.479 0 1.462 1.065 2.875 1.213 3.074.149.198 2.096 3.2 5.077 4.487.709.306 1.262.489 1.694.625.712.227 1.36.195 1.871.118.571-.085 1.758-.719 2.006-1.413.248-.694.248-1.289.173-1.413-.074-.124-.272-.198-.57-.347m-5.421 7.403h-.004a9.87 9.87 0 01-5.031-1.378l-.361-.214-3.741.982.998-3.648-.235-.374a9.86 9.86 0 01-1.51-5.26c.001-5.45 4.436-9.884 9.888-9.884 2.64 0 5.122 1.03 6.988 2.898a9.825 9.825 0 012.893 6.994c-.003 5.45-4.437 9.884-9.885 9.884"/></svg>
                  </div>
                  <div style={{ fontWeight: 500, color: NX.text }}>
                    {lang === 'en' ? 'My Page already has WhatsApp Business linked' : 'Mi Página ya tiene WhatsApp Business vinculado'}
                  </div>
                </div>
                <div style={{ fontSize: '13px', color: NX.muted, lineHeight: 1.45 }}>
                  {lang === 'en'
                    ? 'Verify in Meta Business Suite. Meta will route messages to your linked WhatsApp number automatically when ads launch.'
                    : 'Verificalo en Meta Business Suite. Meta enrutará los mensajes al WhatsApp vinculado a tu Página automáticamente cuando lances anuncios.'}
                </div>
              </NxCard>
              <NxCard onClick={loadPixels} style={{ cursor: 'pointer' }}>
                <div style={{ fontWeight: 500, color: NX.text, marginBottom: '4px' }}>{ta.connectNow}</div>
                <div style={{ fontSize: '13px', color: NX.muted }}>{ta.connectNowDesc}</div>
              </NxCard>
              <NxCard onClick={loadPixels} style={{ cursor: 'pointer' }}>
                <div style={{ fontWeight: 500, color: NX.text, marginBottom: '4px' }}>{ta.notNow}</div>
                <div style={{ fontSize: '13px', color: NX.muted }}>{ta.notNowDesc}</div>
              </NxCard>
            </div>
          )}
        </>}

        {/* Step 5: Pixel */}
        {step === 5 && <>
          <h2 style={{ fontSize: '17px', fontWeight: 500, color: NX.text, margin: '0 0 6px' }}>{ta.pixel.title}</h2>
          <p style={{ color: NX.muted, fontSize: '13px', margin: '0 0 20px' }}>{ta.pixel.subtitle}</p>
          {pixels.length > 0
            ? <>
                <p style={{ fontSize: '13px', color: NX.muted, margin: '0 0 12px' }}>{ta.availablePixels}</p>
                <OptionList items={pixels} loading={loading} labelFn={px => px.name} sublabelFn={px => `ID: ${px.id}`} onSelect={(px) => { setSelectedPixel(px); persistAndComplete(px); }}/>
                <div style={{ padding: '12px 14px', background: NX.bg3, border: `1px dashed ${NX.border}`, borderRadius: '10px', marginTop: '10px', display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: '10px', flexWrap: 'wrap' }}>
                  <div style={{ fontSize: '12px', color: NX.muted, flex: 1, minWidth: '180px' }}>
                    {lang === 'en' ? 'Need another one? Create a fresh pixel for this business.' : '¿Necesitas otro? Crea un pixel nuevo para este negocio.'}
                  </div>
                  <button onClick={createPixel} disabled={creatingPixel}
                    style={{ padding: '8px 14px', background: 'transparent', border: `1px solid ${NX.accent}`, borderRadius: '8px', color: NX.accent, fontSize: '12px', fontWeight: 600, cursor: creatingPixel ? 'wait' : 'pointer', fontFamily: "'DM Sans',sans-serif", display: 'flex', alignItems: 'center', gap: '6px' }}>
                    {creatingPixel && <span style={{ display: 'inline-block', width: '10px', height: '10px', border: '1.5px solid currentColor', borderTopColor: 'transparent', borderRadius: '50%', animation: 'spin 0.7s linear infinite' }}/>}
                    <svg width="12" height="12" viewBox="0 0 16 16" fill="none"><path d="M8 3v10M3 8h10" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round"/></svg>
                    {creatingPixel ? (lang === 'en' ? 'Creating…' : 'Creando…') : (lang === 'en' ? 'Create new pixel' : 'Crear nuevo pixel')}
                  </button>
                </div>
                {pixelError && <div style={{ fontSize: '11px', color: NX.danger, marginTop: '8px' }}>{pixelError}</div>}
              </>
            : <>
                {/* No pixel detected — RECOMMENDED but not required.
                    Pixel is only useful for website traffic campaigns. Businesses that sell via
                    WhatsApp / Instagram DM / Messenger don't need one. */}
                <div style={{ padding: '14px 16px', background: 'rgba(139,92,246,0.06)', border: '1px solid rgba(139,92,246,0.25)', borderRadius: '12px', marginBottom: '14px' }}>
                  <div style={{ display: 'flex', alignItems: 'center', gap: '10px', marginBottom: '6px' }}>
                    <svg width="18" height="18" viewBox="0 0 24 24" fill="none"><circle cx="12" cy="12" r="10" stroke={NX.accent} strokeWidth="1.8"/><path d="M12 16v-4M12 8v.01" stroke={NX.accent} strokeWidth="2" strokeLinecap="round"/></svg>
                    <div style={{ fontSize: '13px', fontWeight: 600, color: NX.text }}>
                      {lang === 'en' ? 'Pixel recommended for website traffic' : 'Pixel recomendado si envías tráfico a una web'}
                    </div>
                  </div>
                  <div style={{ fontSize: '12px', color: NX.muted, lineHeight: 1.55 }}>
                    {lang === 'en'
                      ? <>If you plan to send traffic to a website, create a pixel — it tracks conversions and lets Meta optimize the campaigns.<br/>If you only sell through <b>WhatsApp, Instagram DM or Messenger</b>, you can skip this step.</>
                      : <>Si vas a enviar tráfico a una página web, crea un pixel — rastrea las conversiones y deja que Meta optimice las campañas.<br/>Si solo vendes por <b>WhatsApp, Instagram DM o Messenger</b>, puedes saltarte este paso.</>}
                  </div>
                </div>

                <div style={{ fontSize: '12px', color: NX.muted, marginBottom: '6px', fontWeight: 500 }}>
                  {lang === 'en' ? 'Pixel name (optional)' : 'Nombre del pixel (opcional)'}
                </div>
                <NxInput
                  placeholder={(selectedBM?.name || 'NexWall') + ' Pixel'}
                  value={newPixelName}
                  onChange={e => setNewPixelName(e.target.value)}
                  full
                />

                {pixelError && (
                  <div style={{ margin: '10px 0 0', padding: '10px 12px', background: 'rgba(248,113,113,0.08)', border: '1px solid rgba(248,113,113,0.25)', borderRadius: '8px', fontSize: '12px', color: NX.danger }}>
                    {pixelError}
                  </div>
                )}

                <div style={{ display: 'flex', gap: '10px', marginTop: '14px', flexWrap: 'wrap' }}>
                  <NxButton variant="ghost" onClick={() => setStep(4)} disabled={creatingPixel} full>{ta.back}</NxButton>
                  <NxButton variant="ghost" onClick={() => persistAndComplete(null)} disabled={creatingPixel} full>
                    {lang === 'en' ? 'Skip (no pixel)' : 'Saltar (sin pixel)'}
                  </NxButton>
                  <NxButton variant="accent" onClick={createPixel} disabled={creatingPixel} full>
                    {creatingPixel
                      ? (lang === 'en' ? 'Creating pixel…' : 'Creando pixel…')
                      : (lang === 'en' ? 'Create pixel' : 'Crear pixel')}
                  </NxButton>
                </div>
              </>
          }
        </>}
      </div>
    </NxModal>
  );
};

// ══════════════════════════════════════════
// FLOW: IDENTIDAD DE TU NEGOCIO (Step 3)
// ══════════════════════════════════════════
const BrandFlow = ({ onComplete, onClose, lang='es' }) => {
  const tb = (I18N && I18N[lang]) ? I18N[lang].brand : I18N.es.brand;
  const [step, setStep] = React.useState(0); // 0=choose, 1=input, 2=processing, 3=result
  const [method, setMethod] = React.useState(null);
  const [recording, setRecording] = React.useState(false);
  const [recTime, setRecTime] = React.useState(0);
  const [text, setText] = React.useState('');
  const [audioTranscript, setAudioTranscript] = React.useState('');
  const [liveTranscript, setLiveTranscript] = React.useState('');
  const [dna, setDna] = React.useState(null);
  const [analyzeError, setAnalyzeError] = React.useState(null);
  const [websiteUrl, setWebsiteUrl] = React.useState('');
  // Upload de documento de identidad de marca (PDF / TXT / MD). El backend acepta
  // multipart con field "file" — PDFs se pasan a Opus como `document` content
  // nativo (Opus lee el PDF tal cual, incluyendo layout, colores y tipografías).
  const [brandFile, setBrandFile] = React.useState(null);
  const [brandFileDragActive, setBrandFileDragActive] = React.useState(false);
  const brandFileInputRef = React.useRef(null);
  const recognitionRef = React.useRef(null);
  const timerRef = React.useRef(null);

  React.useEffect(() => {
    if (recording) {
      timerRef.current = setInterval(() => setRecTime(s => s + 1), 1000);
    } else {
      clearInterval(timerRef.current);
    }
    return () => clearInterval(timerRef.current);
  }, [recording]);

  const fmtTime = s => `${String(Math.floor(s/60)).padStart(2,'0')}:${String(s%60).padStart(2,'0')}`;

  const startSpeech = () => {
    const SR = window.SpeechRecognition || window.webkitSpeechRecognition;
    if (!SR) {
      setAnalyzeError(lang === 'es'
        ? 'Tu navegador no soporta reconocimiento de voz. Usa Chrome.'
        : 'Your browser does not support voice recognition. Use Chrome.');
      return;
    }
    setAudioTranscript('');
    setLiveTranscript('');
    setRecTime(0);
    const rec = new SR();
    rec.lang = lang === 'es' ? 'es-ES' : 'en-US';
    rec.continuous = true;
    rec.interimResults = true;
    let finalText = '';
    rec.onresult = (event) => {
      let interim = '';
      for (let i = event.resultIndex; i < event.results.length; i++) {
        if (event.results[i].isFinal) finalText += event.results[i][0].transcript + ' ';
        else interim = event.results[i][0].transcript;
      }
      setAudioTranscript(finalText);
      setLiveTranscript(interim);
    };
    rec.onerror = (e) => { if (e.error !== 'aborted') console.error('Speech:', e.error); };
    rec.onend = () => setLiveTranscript('');
    rec.start();
    recognitionRef.current = rec;
    setRecording(true);
    setAnalyzeError(null);
  };

  const stopSpeech = () => {
    if (recognitionRef.current) { recognitionRef.current.stop(); recognitionRef.current = null; }
    setRecording(false);
  };

  const analyzeText = async (inputText, inputUrl) => {
    setAnalyzeError(null);
    try {
      const res = await fetch(`${API}/api/brand-dna/analyze`, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          Authorization: `Bearer ${localStorage.getItem('nw_token')}`,
        },
        body: JSON.stringify(inputUrl ? { url: inputUrl } : { text: inputText }),
      });
      const data = await res.json();
      if (!res.ok) throw new Error(data.error || 'Error al analizar');
      setDna(data);
      setStep(3);
    } catch (err) {
      setAnalyzeError(err.message);
      setStep(1);
    }
  };

  const handleSendToAnalyze = () => {
    const inputText = method === 'record'
      ? (audioTranscript + liveTranscript).trim()
      : text.trim();
    if (inputText.length < 20) return;
    stopSpeech();
    setStep(2);
    analyzeText(inputText, null);
  };

  const handleAnalyzeUrl = () => {
    const trimmed = websiteUrl.trim();
    if (!trimmed) return;
    const withProtocol = /^https?:\/\//i.test(trimmed) ? trimmed : `https://${trimmed}`;
    setStep(2);
    analyzeText(null, withProtocol);
  };

  // Sube el archivo de identidad de marca (PDF/TXT/MD) como multipart al backend.
  // Opus lo recibe directo y extrae la DNA. No requiere parseo previo en cliente.
  const handleAnalyzeFile = async () => {
    if (!brandFile) return;
    setAnalyzeError(null);
    setStep(2);
    try {
      const fd = new FormData();
      fd.append('file', brandFile);
      const res = await fetch(`${API}/api/brand-dna/analyze`, {
        method: 'POST',
        headers: { Authorization: `Bearer ${localStorage.getItem('nw_token')}` },
        body: fd,
      });
      const data = await res.json();
      if (!res.ok) throw new Error(data.error || 'Error al analizar el documento');
      setDna(data);
      setStep(3);
    } catch (err) {
      setAnalyzeError(err.message);
      setStep(1);
    }
  };

  const onBrandFilePicked = (file) => {
    if (!file) return;
    const sizeMB = file.size / (1024 * 1024);
    if (sizeMB > 15) {
      setAnalyzeError(lang === 'es' ? 'El archivo supera los 15 MB.' : 'File exceeds 15 MB.');
      return;
    }
    const okExt = /\.(pdf|txt|md|markdown)$/i.test(file.name);
    const okMime = ['application/pdf', 'text/plain', 'text/markdown'].includes(file.type);
    if (!okExt && !okMime) {
      setAnalyzeError(lang === 'es' ? 'Solo PDF, TXT o MD. Para DOC/DOCX, exportá a PDF primero.' : 'PDF, TXT or MD only. For DOC/DOCX, export to PDF first.');
      return;
    }
    setAnalyzeError(null);
    setBrandFile(file);
  };

  const handleComplete = async () => {
    const bizId = localStorage.getItem('nw_active_biz_id') || '1';
    try {
      await fetch(`${API}/api/brand-dna/${bizId}`, {
        method: 'PUT',
        headers: {
          'Content-Type': 'application/json',
          Authorization: `Bearer ${localStorage.getItem('nw_token')}`,
        },
        body: JSON.stringify({ dna }),
      });
    } catch (e) { console.warn('Could not save brand DNA:', e); }
    onComplete(dna);
  };

  return (
    <NxModal onClose={onClose} noBackdropClose>
      <div style={{ padding: '28px' }}>
        {step === 0 && <>
          <h2 style={{ fontSize: '17px', fontWeight: 500, color: NX.text, margin: '0 0 6px' }}>{tb.title}</h2>
          <p style={{ color: NX.muted, fontSize: '13px', margin: '0 0 20px' }}>{tb.subtitle}</p>
          <div style={{ display: 'grid', gridTemplateColumns: 'repeat(2,1fr)', gap: '10px', marginBottom: '20px' }}>
            {[
              { id:'record', label: tb.methods[0].label, icon: <svg width="22" height="22" viewBox="0 0 22 22" fill="none"><rect x="8" y="2" width="6" height="12" rx="3" stroke={NX.accent} strokeWidth="1.4"/><path d="M4 11a7 7 0 0014 0" stroke={NX.accent} strokeWidth="1.4" strokeLinecap="round"/><path d="M11 18v2M8 20h6" stroke={NX.accent} strokeWidth="1.4" strokeLinecap="round"/></svg>, desc: tb.methods[0].desc },
              { id:'text',   label: tb.methods[1].label, icon: <svg width="22" height="22" viewBox="0 0 22 22" fill="none"><path d="M4 6h14M4 10h10M4 14h8" stroke={NX.accent} strokeWidth="1.4" strokeLinecap="round"/></svg>, desc: tb.methods[1].desc },
              { id:'url',    label: tb.methods[2].label, icon: <svg width="22" height="22" viewBox="0 0 22 22" fill="none"><circle cx="11" cy="11" r="8" stroke={NX.accent} strokeWidth="1.4"/><path d="M11 3c0 0-3 3-3 8s3 8 3 8M11 3c0 0 3 3 3 8s-3 8-3 8M3 11h16" stroke={NX.accent} strokeWidth="1.4" strokeLinecap="round"/></svg>, desc: tb.methods[2].desc, highlight: true },
              { id:'upload', label: tb.methods[3].label, icon: <svg width="22" height="22" viewBox="0 0 22 22" fill="none"><path d="M11 14V4M7 8l4-4 4 4" stroke={NX.muted} strokeWidth="1.4" strokeLinecap="round" strokeLinejoin="round"/><path d="M3 17h16M3 17v1a2 2 0 002 2h12a2 2 0 002-2v-1" stroke={NX.muted} strokeWidth="1.4" strokeLinecap="round"/></svg>, desc: tb.methods[3].desc },
            ].map(m => (
              <div key={m.id} onClick={() => { setMethod(m.id); setStep(1); setAnalyzeError(null); }}
                style={{ padding: '16px 12px', background: NX.bg3, border: `1px solid ${m.highlight ? 'rgba(139,92,246,0.35)' : NX.border}`, borderRadius: '12px', cursor: 'pointer', textAlign: 'center', transition: 'all 0.15s', position: 'relative' }}>
                {m.highlight && <span style={{ position: 'absolute', top: '-9px', left: '50%', transform: 'translateX(-50%)', background: NX.accent, borderRadius: '100px', padding: '1px 9px', fontSize: '9px', fontWeight: 700, color: '#fff', whiteSpace: 'nowrap' }}>RECOMENDADO</span>}
                <div style={{ marginBottom: '8px', display: 'flex', justifyContent: 'center' }}>{m.icon}</div>
                <div style={{ fontSize: '13px', fontWeight: 500, color: m.highlight ? NX.accent : NX.text, marginBottom: '3px' }}>{m.label}</div>
                <div style={{ fontSize: '11px', color: NX.muted }}>{m.desc}</div>
              </div>
            ))}
          </div>
          <NxButton variant="ghost" onClick={onClose} full>{tb.cancel}</NxButton>
        </>}

        {step === 1 && method === 'record' && <>
          <h2 style={{ fontSize: '17px', fontWeight: 500, color: NX.text, margin: '0 0 6px' }}>{tb.record.title}</h2>
          <p style={{ color: NX.muted, fontSize: '13px', margin: '0 0 16px' }}>{tb.record.subtitle}</p>

          {/* Guiding questions */}
          <div style={{ background: NX.bg3, borderRadius: '10px', padding: '12px 16px', marginBottom: '16px' }}>
            {tb.questions.map((q, i) => (
              <div key={i} style={{ fontSize: '12px', color: NX.muted, padding: '4px 0', display: 'flex', gap: '8px' }}>
                <span style={{ color: NX.accent, fontFamily: "'DM Mono',monospace", flexShrink: 0 }}>{i+1}.</span>{q}
              </div>
            ))}
          </div>

          {/* Recording controls */}
          <div style={{ textAlign: 'center', margin: '12px 0 16px' }}>
            <div style={{ fontSize: '28px', fontWeight: 300, fontFamily: "'DM Mono',monospace", color: NX.text, marginBottom: '12px' }}>
              {fmtTime(recTime)}
              {recording && <span style={{ display: 'inline-block', width: '8px', height: '8px', borderRadius: '50%', background: NX.danger, marginLeft: '10px', animation: 'pulse 1s infinite' }}/>}
            </div>
            <button onClick={recording ? stopSpeech : startSpeech}
              style={{ width: '60px', height: '60px', borderRadius: '50%', border: 'none', cursor: 'pointer', background: recording ? NX.danger : NX.accent, display: 'flex', alignItems: 'center', justifyContent: 'center', margin: '0 auto', transition: 'all 0.2s' }}>
              {recording
                ? <div style={{ width: '14px', height: '14px', background: '#fff', borderRadius: '2px' }}/>
                : <svg width="22" height="22" viewBox="0 0 24 24" fill="none"><rect x="9" y="4" width="6" height="12" rx="3" stroke="white" strokeWidth="2"/><path d="M5 11a7 7 0 0014 0" stroke="white" strokeWidth="2" strokeLinecap="round"/></svg>}
            </button>
            <p style={{ fontSize: '11px', color: NX.muted, marginTop: '8px' }}>{recording ? tb.record.tapPause : tb.record.tapStart}</p>
          </div>

          {/* Live transcript preview */}
          {(audioTranscript || liveTranscript) && (
            <div style={{ background: NX.bg3, border: `1px solid ${NX.border}`, borderRadius: '10px', padding: '12px', marginBottom: '12px', maxHeight: '100px', overflowY: 'auto' }}>
              <span style={{ fontSize: '12px', color: NX.text }}>{audioTranscript}</span>
              <span style={{ fontSize: '12px', color: NX.muted }}>{liveTranscript}</span>
            </div>
          )}

          {analyzeError && (
            <div style={{ background: 'rgba(248,113,113,0.1)', border: `1px solid rgba(248,113,113,0.3)`, borderRadius: '8px', padding: '10px 12px', marginBottom: '12px', fontSize: '12px', color: NX.danger }}>
              {analyzeError}
            </div>
          )}

          <div style={{ display: 'flex', gap: '10px' }}>
            <NxButton variant="ghost" onClick={() => { stopSpeech(); setStep(0); }} full>{tb.back}</NxButton>
            <NxButton variant="accent" onClick={handleSendToAnalyze}
              disabled={!audioTranscript || audioTranscript.trim().length < 20} full>
              {tb.record.sendBtn}
            </NxButton>
          </div>
        </>}

        {step === 1 && method === 'text' && <>
          <h2 style={{ fontSize: '17px', fontWeight: 500, color: NX.text, margin: '0 0 6px' }}>{tb.text.title}</h2>
          <p style={{ color: NX.muted, fontSize: '13px', margin: '0 0 20px' }}>{tb.text.subtitle}</p>
          <div style={{ background: NX.bg3, borderRadius: '10px', padding: '12px', marginBottom: '16px' }}>
            {tb.questions.map((q,i) => (
              <div key={i} style={{ fontSize: '12px', color: NX.accent, marginBottom: '4px', fontFamily: "'DM Mono',monospace" }}>• {q}</div>
            ))}
          </div>
          <textarea value={text} onChange={e => setText(e.target.value)}
            placeholder={tb.text.placeholder}
            style={{ width: '100%', minHeight: '140px', background: NX.bg3, border: `1px solid ${NX.border}`, borderRadius: '10px', color: NX.text, fontSize: '13px', padding: '12px', fontFamily: "'DM Sans',sans-serif", resize: 'vertical', outline: 'none', boxSizing: 'border-box', lineHeight: 1.6 }}/>
          {analyzeError && (
            <div style={{ background: 'rgba(248,113,113,0.1)', border: `1px solid rgba(248,113,113,0.3)`, borderRadius: '8px', padding: '10px 12px', marginBottom: '12px', fontSize: '12px', color: NX.danger }}>
              {analyzeError}
            </div>
          )}
          <div style={{ display: 'flex', gap: '10px', marginTop: '16px' }}>
            <NxButton variant="ghost" onClick={() => setStep(0)} full>{tb.back}</NxButton>
            <NxButton variant="accent" onClick={handleSendToAnalyze} disabled={text.trim().length < 20} full>{tb.text.btn}</NxButton>
          </div>
        </>}

        {step === 1 && method === 'url' && <>
          <h2 style={{ fontSize: '17px', fontWeight: 500, color: NX.text, margin: '0 0 6px' }}>{tb.url.title}</h2>
          <p style={{ color: NX.muted, fontSize: '13px', margin: '0 0 20px' }}>{tb.url.subtitle}</p>

          {/* URL examples */}
          <div style={{ background: NX.bg3, borderRadius: '10px', padding: '12px 16px', marginBottom: '16px' }}>
            {['Tu tienda online', 'Página de producto en Mercado Libre', 'Landing page de tu servicio', 'Perfil de Instagram / Bio link'].map((ex, i) => (
              <div key={i} style={{ fontSize: '12px', color: NX.muted, padding: '3px 0', display: 'flex', gap: '8px' }}>
                <span style={{ color: NX.accent }}>•</span>{ex}
              </div>
            ))}
          </div>

          {/* URL input */}
          <div style={{ position: 'relative', marginBottom: '12px' }}>
            <div style={{ position: 'absolute', left: '12px', top: '50%', transform: 'translateY(-50%)' }}>
              <svg width="16" height="16" viewBox="0 0 22 22" fill="none"><circle cx="11" cy="11" r="8" stroke={NX.muted} strokeWidth="1.4"/><path d="M11 3c0 0-3 3-3 8s3 8 3 8M11 3c0 0 3 3 3 8s-3 8-3 8M3 11h16" stroke={NX.muted} strokeWidth="1.4" strokeLinecap="round"/></svg>
            </div>
            <input
              type="url"
              value={websiteUrl}
              onChange={e => { setWebsiteUrl(e.target.value); setAnalyzeError(null); }}
              onKeyDown={e => e.key === 'Enter' && websiteUrl.trim().length > 4 && handleAnalyzeUrl()}
              placeholder={tb.url.placeholder}
              style={{ width: '100%', background: NX.bg3, border: `1px solid ${websiteUrl.trim() ? NX.accent : NX.border}`, borderRadius: '10px', color: NX.text, fontSize: '14px', padding: '12px 12px 12px 36px', outline: 'none', boxSizing: 'border-box', fontFamily: "'DM Sans',sans-serif", transition: 'border-color 0.15s' }}
            />
          </div>

          {analyzeError && (
            <div style={{ background: 'rgba(248,113,113,0.1)', border: `1px solid rgba(248,113,113,0.3)`, borderRadius: '8px', padding: '10px 12px', marginBottom: '12px', fontSize: '12px', color: NX.danger }}>
              {analyzeError}
            </div>
          )}

          <div style={{ display: 'flex', gap: '10px' }}>
            <NxButton variant="ghost" onClick={() => { setStep(0); setAnalyzeError(null); }} full>{tb.back}</NxButton>
            <NxButton variant="accent" onClick={handleAnalyzeUrl} disabled={websiteUrl.trim().length < 5} full>{tb.url.btn}</NxButton>
          </div>
        </>}

        {step === 1 && method === 'upload' && <>
          <h2 style={{ fontSize: '17px', fontWeight: 500, color: NX.text, margin: '0 0 6px' }}>{tb.upload.title}</h2>
          <p style={{ color: NX.muted, fontSize: '13px', margin: '0 0 20px' }}>{lang === 'es' ? 'Sube tu manual de marca, brandbook o guía de identidad. NexWall AI extraerá voz, audiencia, valores y diferenciador.' : 'Upload your brand book, manual, or identity guide. NexWall AI will extract voice, audience, values, and positioning.'}</p>
          <input ref={brandFileInputRef} type="file" accept=".pdf,.txt,.md,application/pdf,text/plain,text/markdown" onChange={e => onBrandFilePicked(e.target.files?.[0])} style={{ display: 'none' }}/>
          {brandFile ? (
            // Filled state — archivo cargado listo para analizar
            <div style={{ display: 'flex', alignItems: 'center', gap: '14px', padding: '14px 16px', background: 'linear-gradient(180deg, rgba(139,92,246,0.06), rgba(139,92,246,0.04))', border: `1px solid ${NX.accent}`, borderRadius: '12px', marginBottom: '16px', boxShadow: '0 0 0 4px rgba(139,92,246,0.06)' }}>
              <div style={{ width: '40px', height: '40px', borderRadius: '10px', background: 'rgba(139,92,246,0.20)', display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0 }}>
                <svg width="20" height="20" viewBox="0 0 24 24" fill="none"><path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z" stroke={NX.accent} strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round"/><path d="M14 2v6h6M9 13h6M9 17h4" stroke={NX.accent} strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round"/></svg>
              </div>
              <div style={{ flex: 1, minWidth: 0 }}>
                <div style={{ fontSize: '13px', color: NX.text, fontWeight: 500, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{brandFile.name}</div>
                <div style={{ fontSize: '11px', color: NX.muted, marginTop: '2px' }}>{(brandFile.size / 1024).toFixed(0)} KB · {brandFile.type || (lang === 'es' ? 'tipo desconocido' : 'unknown type')}</div>
              </div>
              <button type="button" onClick={() => setBrandFile(null)} title={lang === 'es' ? 'Quitar' : 'Remove'}
                style={{ width: '30px', height: '30px', background: 'transparent', border: 'none', borderRadius: '8px', color: NX.muted, cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center' }}
                onMouseEnter={e => { e.currentTarget.style.background = 'rgba(248,113,113,0.1)'; e.currentTarget.style.color = NX.danger; }}
                onMouseLeave={e => { e.currentTarget.style.background = 'transparent'; e.currentTarget.style.color = NX.muted; }}>
                <svg width="14" height="14" viewBox="0 0 24 24" fill="none"><path d="M3 6h18M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2M19 6l-1 14a2 2 0 01-2 2H8a2 2 0 01-2-2L5 6" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round"/></svg>
              </button>
            </div>
          ) : (
            // Empty state — drop zone funcional
            <div onClick={() => brandFileInputRef.current?.click()}
              onDragEnter={e => { e.preventDefault(); e.stopPropagation(); setBrandFileDragActive(true); }}
              onDragOver={e => { e.preventDefault(); e.stopPropagation(); setBrandFileDragActive(true); }}
              onDragLeave={e => { e.preventDefault(); e.stopPropagation(); setBrandFileDragActive(false); }}
              onDrop={e => {
                e.preventDefault(); e.stopPropagation(); setBrandFileDragActive(false);
                const f = e.dataTransfer?.files?.[0];
                if (f) onBrandFilePicked(f);
              }}
              style={{
                border: `1.5px dashed ${brandFileDragActive ? NX.accent : NX.border2}`,
                background: brandFileDragActive ? 'linear-gradient(180deg, rgba(139,92,246,0.10), rgba(139,92,246,0.06))' : 'transparent',
                borderRadius: '14px', padding: '32px 16px', marginBottom: '16px',
                cursor: 'pointer', transition: 'all 0.2s', textAlign: 'center',
                boxShadow: brandFileDragActive ? '0 0 0 6px rgba(139,92,246,0.08)' : 'none',
              }}>
              <div style={{ width: '52px', height: '52px', borderRadius: '14px', background: brandFileDragActive ? 'linear-gradient(135deg, #8b5cf6, #a78bfa)' : 'linear-gradient(135deg, rgba(139,92,246,0.15), rgba(139,92,246,0.12))', display: 'flex', alignItems: 'center', justifyContent: 'center', margin: '0 auto 12px', transition: 'all 0.2s' }}>
                <svg width="24" height="24" viewBox="0 0 24 24" fill="none">
                  <path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z" stroke={brandFileDragActive ? '#fff' : NX.accent} strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round"/>
                  <path d="M14 2v6h6" stroke={brandFileDragActive ? '#fff' : NX.accent} strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round"/>
                  <path d="M12 18v-6M9 15l3-3 3 3" stroke={brandFileDragActive ? '#fff' : NX.accent} strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round"/>
                </svg>
              </div>
              <div style={{ fontSize: '13px', color: NX.text, fontWeight: 500, marginBottom: '4px' }}>
                {brandFileDragActive
                  ? (lang === 'es' ? 'Suelta el documento aquí' : 'Drop the document here')
                  : (lang === 'es' ? 'Arrastra tu manual de marca o haz click' : 'Drag your brand book or click to upload')}
              </div>
              <div style={{ fontSize: '11px', color: NX.muted, lineHeight: 1.5 }}>
                {lang === 'es' ? 'PDF, TXT o MD · hasta 15 MB · Opus lee el PDF directo' : 'PDF, TXT or MD · up to 15 MB · Opus reads PDFs directly'}
              </div>
            </div>
          )}
          {analyzeError && (
            <div style={{ background: 'rgba(248,113,113,0.08)', border: `1px solid rgba(248,113,113,0.30)`, color: NX.danger, fontSize: '12px', padding: '10px 12px', borderRadius: '8px', marginBottom: '12px' }}>{analyzeError}</div>
          )}
          <div style={{ display: 'flex', gap: '10px' }}>
            <NxButton variant="ghost" onClick={() => { setBrandFile(null); setAnalyzeError(null); setStep(0); }} full>{tb.back}</NxButton>
            <NxButton variant="accent" onClick={handleAnalyzeFile} disabled={!brandFile} full>
              {lang === 'es' ? 'Analizar documento' : 'Analyze document'}
            </NxButton>
          </div>
        </>}

        {step === 2 && (
          <div style={{ padding: '20px 0' }}>
            <AiProcessing label={tb.processing} sublabel={tb.processingSubLabel}/>
          </div>
        )}

        {step === 3 && dna && <>
          <div style={{ textAlign: 'center', marginBottom: '20px' }}>
            <div style={{ width: '52px', height: '52px', borderRadius: '50%', background: 'rgba(139,92,246,0.1)', border: '1px solid rgba(139,92,246,0.3)', display: 'flex', alignItems: 'center', justifyContent: 'center', margin: '0 auto 14px' }}>
              <NxOrbIcon size={26}/>
            </div>
            <h2 style={{ fontSize: '19px', fontWeight: 500, color: NX.text, margin: '0 0 5px' }}>{tb.resultTitle}</h2>
            <p style={{ color: NX.muted, fontSize: '13px', margin: 0 }}>{tb.resultSubtitle}</p>
          </div>
          <NxCard padding="18px" style={{ marginBottom: '16px' }}>
            {[
              { k: 'voz',          v: dna.voz },
              { k: 'personalidad', v: dna.personalidad },
              { k: 'audiencia',    v: dna.audiencia },
              { k: 'industria',    v: dna.industria },
              { k: 'propuesta',    v: dna.propuesta },
              { k: 'productos',    v: dna.productos },
              { k: 'diferenciador',v: dna.diferenciador },
              { k: 'tono',         v: dna.tono },
            ].filter(row => row.v).map(({ k, v }) => (
              <div key={k} style={{ display: 'flex', gap: '12px', padding: '8px 0', borderBottom: `1px solid ${NX.border}` }}>
                <span style={{ fontFamily: "'DM Mono',monospace", fontSize: '10px', color: NX.accent, width: '90px', flexShrink: 0, paddingTop: '3px', textTransform: 'capitalize' }}>{k}</span>
                <span style={{ fontSize: '13px', color: NX.text, lineHeight: 1.5 }}>
                  {Array.isArray(v)
                    ? v.map((t, i) => <NxBadge key={i} color="accent" style={{ marginRight: '4px', marginBottom: '2px' }}>{t}</NxBadge>)
                    : v}
                </span>
              </div>
            ))}
          </NxCard>
          <NxButton variant="accent" onClick={handleComplete} full>{tb.doneBtn}</NxButton>
        </>}
      </div>
    </NxModal>
  );
};

// Per-tile progress indicator for image generation. Each card runs its own ease-out timer
// (asymptote at 95 % around `etaSeconds`) so the user sees smooth motion instead of a binary
// spinner. When the image actually finishes (`done`), we snap to 100 % and let the parent
// fade in the result. Sub-component because the parent renders cards inside `.map()` and
// hooks can't live there. Default eta=90s tuned for Ultra MAX (gpt-image-2). Premium (Gemini)
// passes eta=30 so the curve doesn't stall at 95 % for too long while waiting for the snap.
const ImageGenProgress = ({ done, bg, angle, etaSeconds = 90 }) => {
  const [progress, setProgress] = React.useState(0);
  React.useEffect(() => {
    if (done) { setProgress(100); return; }
    const start = Date.now();
    const tau = (etaSeconds * 1000) / 3; // at t=eta, ≈ 1 - e^-3 ≈ 95 %
    const id = setInterval(() => {
      const elapsed = Date.now() - start;
      const pct = Math.min(95, 95 * (1 - Math.exp(-elapsed / tau)));
      setProgress(pct);
    }, 80);
    return () => clearInterval(id);
  }, [done, etaSeconds]);
  const radius = 26;
  const circumference = 2 * Math.PI * radius;
  const dashOffset = circumference * (1 - progress / 100);
  return (
    <div style={{ position: 'absolute', inset: 0, background: bg, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', gap: '10px', backdropFilter: 'blur(2px)' }}>
      <div style={{ position: 'relative', width: '72px', height: '72px' }}>
        <div style={{ position: 'absolute', inset: '4px', borderRadius: '50%', background: 'radial-gradient(circle, #8b5cf644 0%, #8b5cf622 50%, transparent 70%)', animation: 'nebulaGlow 2.4s ease-in-out infinite' }}/>
        <svg width="72" height="72" viewBox="0 0 72 72" style={{ position: 'absolute', inset: 0, transform: 'rotate(-90deg)' }}>
          <circle cx="36" cy="36" r={radius} stroke="rgba(139,92,246,0.18)" strokeWidth="3" fill="none"/>
          <circle cx="36" cy="36" r={radius} stroke="#a78bfa" strokeWidth="3" fill="none" strokeLinecap="round"
            strokeDasharray={circumference} strokeDashoffset={dashOffset}
            style={{ transition: 'stroke-dashoffset 0.25s linear', filter: 'drop-shadow(0 0 6px rgba(167,139,250,0.5))' }}/>
        </svg>
        <div style={{ position: 'absolute', inset: 0, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
          <span style={{ fontSize: '15px', fontWeight: 700, color: '#fff', fontFamily: "'DM Sans',sans-serif", textShadow: '0 1px 4px rgba(0,0,0,0.4)' }}>{Math.round(progress)}%</span>
        </div>
      </div>
      {angle && (
        <span style={{ fontSize: '8px', color: 'rgba(167,139,250,0.85)', fontFamily: "'DM Mono',monospace", fontWeight: 700, letterSpacing: '0.1em' }}>
          {angle.toUpperCase()}
        </span>
      )}
    </div>
  );
};

// ══════════════════════════════════════════
// FLOW: CREAR ESTRATEGIA (Step 4)
// ══════════════════════════════════════════
const StrategyFlow = ({ onComplete, onClose, lang='es', imageModel='gemini-3.1-flash-image-preview', draftCampaign=null, assistantPrefill=null }) => {
  const ts = (I18N && I18N[lang]) ? I18N[lang].strategy : I18N.es.strategy;
  // Initial step is chosen by the entry path so the user never sees a flash of the wrong
  // screen while the hydration effect runs:
  //   - draftCampaign → step 5 (preview/launch)
  //   - assistantPrefill → step 2 (loading) until uploadAndGenerate kicks setStep(3)
  //   - normal → step 0 (objective picker)
  const [step, setStep] = React.useState(() => {
    if (draftCampaign) return 5;
    // Boosting an existing IG post: skip the image-generation pipeline (steps 2-4) entirely
    // and land on the preview/launch screen — there's no creative to generate or review.
    if (assistantPrefill?.promotionMode === 'existing_ig') return 5;
    if (assistantPrefill) return 2;
    return 0;
  });
  const [objective, setObjective] = React.useState('ventas');
  const [imageCount, setImageCount] = React.useState(5);
  // Ultra HD (gpt-image-2) defaults to 1:1 because gpt-image-2 only renders 1:1, 2:3 or 3:2
  // natively. Premium (Gemini) defaults to 4:5 — the most engaging portrait for Meta Feed.
  // Both engines now natively support the full Meta-friendly ratio set: gpt-image-2 accepts any
  // multiples-of-16 W×H, and Gemini handles every preset. So we share one ratio picker, no
  // engine-specific snap-back. Default to 4:5 — the highest-engagement portrait for Meta Feed.
  const [aspectRatio, setAspectRatio] = React.useState('4:5');
  const [selectedImgs, setSelectedImgs] = React.useState([0]);
  const [regeneratingSlots, setRegeneratingSlots] = React.useState(new Set());
  const [approvedCopies, setApprovedCopies] = React.useState(new Set([0]));
  // Per-variant approval Set with composite keys "imgIdx:varIdx".
  const [approvedVariants, setApprovedVariants] = React.useState(new Set());
  const [activeVariantIdx, setActiveVariantIdx] = React.useState({});
  // Pagination: which image (0..N-1) is currently being reviewed in step 4.
  // Step 4 shows ONE image at a time with its 5 variants. User approves/edits/regens
  // and clicks "Aprobar todas y siguiente imagen →" to advance.
  const [currentImgPage, setCurrentImgPage] = React.useState(0);
  // Per-variant editing: which (imgIdx, varIdx) is currently being edited inline.
  const [editingVariant, setEditingVariant] = React.useState(null); // { i, v } | null
  // Per-variant regeneration loading state.
  const [regeneratingVariant, setRegeneratingVariant] = React.useState(new Set()); // Set of "i:v"
  const [launching, setLaunching] = React.useState(false);
  // flashField: when the user presses a CTA that needs a field filled, instead of disabling
  // the button we flash the missing field red + shake + scroll to it. Auto-clears after ~1.4s.
  const [flashField, setFlashField] = React.useState(null);
  const flashRef = React.useRef(null);
  const triggerFieldFlash = React.useCallback((fieldKey) => {
    setFlashField(fieldKey);
    clearTimeout(flashRef.current);
    flashRef.current = setTimeout(() => setFlashField(null), 1400);
    // Scroll the element into view if present
    setTimeout(() => {
      const el = document.querySelector(`[data-flash-target="${fieldKey}"]`);
      if (el && el.scrollIntoView) el.scrollIntoView({ behavior: 'smooth', block: 'center' });
      if (el && el.focus) try { el.focus(); } catch {}
    }, 50);
  }, []);
  const [productFile, setProductFile] = React.useState(null);        // primary reference (= productFiles[0])
  const [productPreviewUrl, setProductPreviewUrl] = React.useState(null);
  const [extraProductFiles, setExtraProductFiles] = React.useState([]);   // additional angles
  const [extraProductPreviews, setExtraProductPreviews] = React.useState([]);
  const [productCloudinaryUrl, setProductCloudinaryUrl] = React.useState(null);
  const [productDesc, setProductDesc] = React.useState('');
  const [country, setCountry] = React.useState('');
  // Idioma del CONTENIDO de la campaña — INDEPENDIENTE del idioma de la UI (`nw_lang`).
  // El usuario puede tener el dashboard en español pero crear campañas en inglés sin que
  // cambie la interfaz. Resolución idéntica a AssistantFlow.
  const [language, setLanguage] = React.useState(() => {
    try {
      const stored = localStorage.getItem('nw_content_lang');
      if (stored === 'es' || stored === 'en') return stored;
    } catch {}
    try {
      const uiLang = localStorage.getItem('nw_lang');
      if (uiLang === 'es' || uiLang === 'en') return uiLang;
    } catch {}
    try {
      const navLang = (navigator.language || navigator.userLanguage || '').toLowerCase();
      if (navLang.startsWith('en')) return 'en';
      if (navLang.startsWith('es')) return 'es';
      if (['pt','it','gl','ca'].some(p => navLang.startsWith(p))) return 'es';
    } catch {}
    return lang === 'en' ? 'en' : 'es';
  });
  // Persistir SOLO en `nw_content_lang` — nunca tocar `nw_lang` (idioma de la UI).
  React.useEffect(() => {
    try { localStorage.setItem('nw_content_lang', language); } catch {}
  }, [language]);
  const [uploadError, setUploadError] = React.useState(null);
  const [imageGenError, setImageGenError] = React.useState(null);
  const [productUrl, setProductUrl] = React.useState('');
  const [urlLoading, setUrlLoading] = React.useState(false);
  const [dragActive, setDragActive] = React.useState(false);
  const [urlError, setUrlError] = React.useState(null);
  const [dailyBudget, setDailyBudget] = React.useState('');
  const [metaAssets, setMetaAssets] = React.useState(() => {
    try {
      const bizId = localStorage.getItem('nw_active_biz_id') || '1';
      return JSON.parse(localStorage.getItem(`nw_assets_${bizId}`) || 'null');
    } catch { return null; }
  });
  // ── Destination (where the click goes) ──
  // 'web' → visita la URL del producto · 'messaging' → abre un canal (WhatsApp/Instagram/Messenger)
  const [destMode, setDestMode] = React.useState('web');
  // Instagram profile URL/handle — used as destination for the TRAFFIC objective when the user
  // wants to drive visits to their IG profile instead of a website.
  const [igProfile, setIgProfile] = React.useState('');
  // Promotion mode for ENGAGEMENT / AWARENESS objectives:
  //   'new'          → generate new creatives as usual
  //   'existing_ig'  → boost an existing Instagram post (user picks from their feed)
  const [promotionMode, setPromotionMode] = React.useState('new');
  const [igPosts, setIgPosts] = React.useState([]);
  const [igPostsLoading, setIgPostsLoading] = React.useState(false);
  const [igPostsError, setIgPostsError] = React.useState(null);
  const [selectedIgPost, setSelectedIgPost] = React.useState(null); // { id, image, caption, ... }
  // Fetch IG posts once when the user switches to "existing IG post" mode for an eligible objective.
  const fetchIgPosts = React.useCallback(async () => {
    if (igPostsLoading || igPosts.length > 0) return;
    setIgPostsLoading(true); setIgPostsError(null);
    try {
      const bizId = localStorage.getItem('nw_active_biz_id') || '1';
      const res = await authFetch(`/api/meta/ig-posts?biz_id=${encodeURIComponent(bizId)}&limit=18`);
      if (!res.ok) {
        const err = await res.json().catch(() => ({}));
        const isPermissionError = err.code === 10 || /\(#10\)/.test(err.error || '');
        if (isPermissionError) {
          throw new Error(err.error || (lang === 'en'
            ? 'The "boost an Instagram post" feature is not yet enabled. We are waiting for instagram_basic to be approved on Meta.'
            : 'La función de promocionar publicaciones de Instagram aún no está habilitada. Estamos esperando la aprobación del permiso instagram_basic en Meta.'));
        }
        throw new Error(err.error || 'No se pudieron cargar las publicaciones');
      }
      const data = await res.json();
      setIgPosts(data.posts || []);
    } catch (e) {
      setIgPostsError(e.message || 'Error al cargar publicaciones');
    }
    setIgPostsLoading(false);
  }, [igPostsLoading, igPosts.length, lang]);
  const [destChannels, setDestChannels] = React.useState({ whatsapp: false, instagram: false, messenger: false });

  // ── Meta CTA button (standardized — NOT free text) ──
  // Meta only accepts these values; we map to mapCtaToMeta() on publish.
  const META_CTA_OPTIONS = [
    { id: 'LEARN_MORE',   labelEs: 'Más información',    labelEn: 'Learn More' },
    { id: 'SHOP_NOW',     labelEs: 'Comprar ahora',      labelEn: 'Shop Now' },
    { id: 'SIGN_UP',      labelEs: 'Registrarse',        labelEn: 'Sign Up' },
    { id: 'SUBSCRIBE',    labelEs: 'Suscribirse',        labelEn: 'Subscribe' },
    { id: 'BOOK_TRAVEL',  labelEs: 'Reservar',           labelEn: 'Book Now' },
    { id: 'CONTACT_US',   labelEs: 'Contáctanos',        labelEn: 'Contact Us' },
    { id: 'DOWNLOAD',     labelEs: 'Descargar',          labelEn: 'Download' },
    { id: 'GET_OFFER',    labelEs: 'Obtener oferta',     labelEn: 'Get Offer' },
    { id: 'GET_QUOTE',    labelEs: 'Solicitar cotización', labelEn: 'Get Quote' },
    { id: 'APPLY_NOW',    labelEs: 'Solicitar',          labelEn: 'Apply Now' },
    { id: 'ORDER_NOW',    labelEs: 'Pedir ahora',        labelEn: 'Order Now' },
    { id: 'SEND_MESSAGE', labelEs: 'Enviar mensaje',     labelEn: 'Send Message' },
    { id: 'WHATSAPP_MESSAGE', labelEs: 'Enviar WhatsApp', labelEn: 'Send WhatsApp' },
  ];
  const ctaLabel = (id) => {
    const o = META_CTA_OPTIONS.find(x => x.id === id);
    return o ? (language === 'en' ? o.labelEn : o.labelEs) : '—';
  };
  const [ctaButton, setCtaButton] = React.useState('LEARN_MORE');
  const [ctaPickerOpen, setCtaPickerOpen] = React.useState(false);
  const [previewPlatform, setPreviewPlatform] = React.useState('facebook'); // 'facebook' | 'instagram'
  const [previewPopupOpen, setPreviewPopupOpen] = React.useState(false); // mobile-friendly popup for ad previews

  // Campaign strategy (generated by paid-ads skill + Opus)
  const [strategy, setStrategy] = React.useState(null);
  const [strategyLoading, setStrategyLoading] = React.useState(false);
  const [strategyOpen, setStrategyOpen] = React.useState(false);

  const generateStrategy = React.useCallback(async (opts = {}) => {
    const force = opts && opts.force === true;
    if (strategyLoading) return strategy;
    // When `force=true` we bypass the cache and always hit the API again — used by the
    // "Generar nueva estrategia" button in the modal header. Default behavior keeps the
    // existing memoization so the modal opens instantly on subsequent views.
    if (!force && strategy && !strategy.error) return strategy;
    setStrategyLoading(true);
    let finalStrategy = null;
    try {
      const bizId = localStorage.getItem('nw_active_biz_id') || '1';
      const dnaRes = await authFetch(`/api/brand-dna/${bizId}`).then(r => r.ok ? r.json() : null).catch(() => null);
      const res = await authFetch('/api/strategy/generate', {
        method: 'POST',
        body: JSON.stringify({
          objective, productDesc, country, language,
          dailyBudget: Number(dailyBudget) || null, currency,
          destMode, destChannels: Object.entries(destChannels).filter(([, v]) => v).map(([k]) => k),
          brandDNA: dnaRes, pageName: metaAssets?.page_name,
          numCreatives: selectedImgs.length || 5,
        }),
      });
      if (!res.ok) throw new Error((await res.json()).error || 'Error');
      const data = await res.json();
      setStrategy(data);
      finalStrategy = data;
    } catch (e) {
      const err = { error: e.message || 'Error al generar estrategia' };
      setStrategy(err);
      finalStrategy = err;
    }
    setStrategyLoading(false);
    return finalStrategy;
  }, [strategy, strategyLoading, objective, productDesc, country, language, dailyBudget, currency, destMode, destChannels, metaAssets?.page_name, selectedImgs.length]);

  // Strategy is generated on-demand: when the user clicks "Lanzar campaña" (inside publishCampaign)
  // or when the user clicks "Ver detalles de estrategia". We deliberately do NOT auto-generate
  // on step 5 — that wastes Anthropic tokens every time the user revisits the preview or saves a draft.
  // Smart default based on objective + destination
  React.useEffect(() => {
    if (destMode === 'messaging' && destChannels.whatsapp) setCtaButton('WHATSAPP_MESSAGE');
    else if (destMode === 'messaging') setCtaButton('SEND_MESSAGE');
    else if (objective === 'ventas' || objective === 'conversiones') setCtaButton('SHOP_NOW');
    else if (objective === 'leads') setCtaButton('SIGN_UP');
    else setCtaButton('LEARN_MORE');
  }, [objective, destMode, destChannels.whatsapp]);

  // ── Image source ── 'ai' → IA genera · 'uploaded' → el usuario sube sus propias imágenes
  const [imageSource, setImageSource] = React.useState('ai');
  const [userUploadedImages, setUserUploadedImages] = React.useState([]); // [{file, previewUrl, cloudinaryUrl}]
  const userImageInputRef = React.useRef(null);

  // Confirmation state for discarding in-flight AI images when user wants to start over
  const [confirmDiscardImgs, setConfirmDiscardImgs] = React.useState(false);

  // Step 4 inline editing — which card is being edited (null = none)
  const [editingCopyIdx, setEditingCopyIdx] = React.useState(null);
  // Edits from the FB-style preview card (single-text view). The publish payload reads from
  // BOTH the top-level `c.texto` AND from `c.pool.primaryTexts[0]` (the backend prefers the
  // pool when it has any items). If we updated only one side, the other would carry a stale
  // value and Meta would launch the un-edited copy. So we mirror every edit into the pool's
  // first variant — same for titulo/descripcion — to keep both representations in sync.
  const updateCopyField = (idx, field, value) => {
    if (field === 'texto') {
      setGeneratedCopies(prev => prev.map((c, i) => {
        if (i !== idx) return c;
        const pool = c?.pool || { primaryTexts: [], titulos: [], descripciones: [] };
        const primaryTexts = [...(pool.primaryTexts || [])];
        const cur = primaryTexts[0];
        if (typeof cur === 'string' || cur == null) primaryTexts[0] = value;
        else primaryTexts[0] = { ...cur, texto: value };
        return { ...c, texto: value, pool: { ...pool, primaryTexts } };
      }));
    } else if (field === 'titulo') {
      setCampaignTitulos(prev => { const next = [...prev]; next[idx] = value; return next; });
      setGeneratedCopies(prev => prev.map((c, i) => {
        if (i !== idx) return c;
        const pool = c?.pool || { primaryTexts: [], titulos: [], descripciones: [] };
        const titulos = [...(pool.titulos || [])];
        titulos[0] = value;
        return { ...c, titulo: value, pool: { ...pool, titulos } };
      }));
    } else if (field === 'descripcion') {
      setCampaignDescs(prev => { const next = [...prev]; next[idx] = value; return next; });
      setGeneratedCopies(prev => prev.map((c, i) => {
        if (i !== idx) return c;
        const pool = c?.pool || { primaryTexts: [], titulos: [], descripciones: [] };
        const descripciones = [...(pool.descripciones || [])];
        descripciones[0] = value;
        return { ...c, descripcion: value, pool: { ...pool, descripciones } };
      }));
    }
  };
  const currency = metaAssets?.ad_account_currency || 'USD';
  const CURRENCY_MIN_DAILY = { USD: 5, EUR: 5, GBP: 5, CLP: 2000, MXN: 100, ARS: 2000, COP: 20000, PEN: 20, BRL: 25, UYU: 200, BOB: 35, PYG: 30000, CRC: 3000, GTQ: 40, HNL: 120, NIO: 180, DOP: 300, VES: 200, CUP: 1200, JPY: 500, KRW: 5000, VND: 100000, CAD: 5, AUD: 5 };
  const minDailyForCurrency = CURRENCY_MIN_DAILY[currency] || 5;
  // No preselected publish mode in EITHER flow (assistant or advanced) — the user must
  // explicitly choose "Pausa" or "Ahora" before launching, so the decision to start
  // spending money on Meta is never accidental. Drafts hydrate to whatever they had saved.
  const [launchMode, setLaunchMode] = React.useState(null);
  const [confirmLaunchOpen, setConfirmLaunchOpen] = React.useState(false);
  const [launchModeError, setLaunchModeError] = React.useState(false);
  // Bump on every "launch without picking" attempt so the CSS animation restarts each time
  // (changing the React key remounts the pills, restarting the keyframe).
  const [launchFlashKey, setLaunchFlashKey] = React.useState(0);
  const [publishError, setPublishError] = React.useState(null);
  const [metaPublishInfo, setMetaPublishInfo] = React.useState(null);
  // Live progress modal state — fed by the SSE /publish-meta-stream endpoint.
  const [progressSteps, setProgressSteps] = React.useState([]); // [{ msg, ts }]
  const [progressCurrent, setProgressCurrent] = React.useState(null); // latest msg
  const [progressOpen, setProgressOpen] = React.useState(false);

  // Refresh assets from backend on mount (in case localStorage is stale)
  React.useEffect(() => {
    const bizId = localStorage.getItem('nw_active_biz_id') || '1';
    authFetch(`/api/meta/assets?biz_id=${encodeURIComponent(bizId)}`)
      .then(r => r.ok ? r.json() : null)
      .then(data => {
        if (data) {
          setMetaAssets(data);
          localStorage.setItem(`nw_assets_${bizId}`, JSON.stringify(data));
        }
      })
      .catch(() => {});
  }, []);

  // Hydrate from a saved draft when the user clicks "Lanzar" on a draft card in Mis Estrategias.
  // Pre-fills every field and jumps to step 5 (preview) for final review + publish.
  const [hydrated, setHydrated] = React.useState(false);
  React.useEffect(() => {
    if (!draftCampaign || hydrated) return;
    const c = draftCampaign;
    setObjective(c.objective || null);
    setProductDesc(c.product_desc || '');
    setProductUrl(c.product_url || '');
    setDailyBudget(c.daily_budget ? String(c.daily_budget) : '');
    // Drafts intentionally land with NO preselected publish mode — drafts were never
    // actually launched on Meta, so when the user clicks "Reutilizar" and arrives here
    // they must explicitly choose Pausa or Ahora again before lanzar. Same UX as a
    // brand-new campaign — no silent default that risks an accidental spend start.
    setLaunchMode(null);
    setDestMode(c.destination_mode || 'web');
    const ch = { whatsapp: false, instagram: false, messenger: false };
    (c.destination_channels || []).forEach(k => { if (ch[k] !== undefined) ch[k] = true; });
    setDestChannels(ch);
    setImageSource(c.image_source || 'ai');
    setCtaButton(c.cta_button || 'LEARN_MORE');
    if (c.country) setCountry(c.country);
    if (c.language) setLanguage(c.language);
    if (c.aspect_ratio) setAspectRatio(c.aspect_ratio);
    setProductCloudinaryUrl(c.product_image_url || null);
    if (c.product_image_url) setProductPreviewUrl(c.product_image_url);
    // Restaurar imagen de referencia 2 si la campaña la tenía
    setProductCloudinaryUrl2(c.product_image_url_2 || null);
    if (c.product_image_url_2) {
      setExtraProductPreviews([c.product_image_url_2]);
    }

    // Restore the creative images so step 3 and step 5 show the saved creatives
    const urls = Array.isArray(c.creative_image_urls) ? c.creative_image_urls : [];
    const prompts = Array.isArray(c.creative_prompts) ? c.creative_prompts : [];
    const slots = urls.map((url, i) => ({
      id: i, genIndex: i, isOriginal: false, previewUrl: url, cloudinaryUrl: url,
      bg: MOCK_GRADIENTS[i % MOCK_GRADIENTS.length],
      label: `Creativo ${i + 1}`, loading: false, angle: `Angle ${i + 1}`,
      prompt: prompts[i] || null,
    }));
    setGeneratedImages(slots);
    setSelectedImgs(slots.map((_, i) => i));

    // Restore the single selected copy we saved (we don't keep the 5 alternatives,
    // only the one the user approved for publish)
    setGeneratedCopies([{ texto: c.copy_desc || '', titulo: c.copy_title || '', angulo: 'Selected', emojis: false }]);
    setApprovedCopies(new Set([0]));
    setCampaignTitulos(c.copy_title ? [c.copy_title] : []);
    setCampaignDescs(c.copy_cta ? [c.copy_cta] : []);

    // Restore the saved strategy if the draft already has one — otherwise the "Ver detalles"
    // click and the publish step would both regenerate (wasting Anthropic tokens).
    if (c.strategy && typeof c.strategy === 'object' && !Array.isArray(c.strategy) && !c.strategy.error) {
      setStrategy(c.strategy);
    }

    // Restore the saved per-creative copies if present (paired with the images by index)
    if (Array.isArray(c.creative_copies) && c.creative_copies.length) {
      const texts = c.creative_copies.map(cc => ({ texto: cc.primary_text || '', titulo: cc.headline || '', angulo: 'Saved', emojis: false }));
      setGeneratedCopies(texts);
      setApprovedCopies(new Set(texts.map((_, i) => i)));
      setCampaignTitulos(c.creative_copies.map(cc => cc.headline || ''));
      setCampaignDescs(c.creative_copies.map(cc => cc.description || ''));
    }

    setHydrated(true);
    setStep(5); // jump straight to preview
  }, [draftCampaign, hydrated]);

  // ── ASSISTANT PREFILL ── when AssistantFlow handed off prefilled data, hydrate state and
  // immediately fire image generation (skipping the manual objective/upload steps).
  // Uses a ref to call the latest `uploadAndGenerate` after React commits the state updates.
  // If `userImages` is set, the user already has their own creatives — skip AI generation
  // entirely and just upload them, saving tokens/credits.
  const [assistantHydrated, setAssistantHydrated] = React.useState(false);
  const uploadAndGenerateRef = React.useRef(null);
  const submitUserUploadedImagesRef = React.useRef(null);
  React.useEffect(() => {
    if (!assistantPrefill || assistantHydrated) return;
    const p = assistantPrefill;
    if (p.objective) setObjective(p.objective);
    if (p.productDesc) setProductDesc(p.productDesc);
    if (p.productUrl) setProductUrl(p.productUrl);
    if (p.country) setCountry(p.country);
    if (p.language) setLanguage(p.language);
    if (typeof p.imageCount === 'number') setImageCount(p.imageCount);
    if (p.aspectRatio) setAspectRatio(p.aspectRatio);
    if (p.dailyBudget) setDailyBudget(String(p.dailyBudget));
    if (p.destMode) setDestMode(p.destMode);
    if (p.destChannels && typeof p.destChannels === 'object') {
      setDestChannels({
        whatsapp: !!p.destChannels.whatsapp,
        instagram: !!p.destChannels.instagram,
        messenger: !!p.destChannels.messenger,
      });
    }
    // Boost-an-existing-IG-post hand-off from the assistant: hydrate StrategyFlow's own
    // promotionMode + selectedIgPost state so the picker UI lands pre-selected.
    if (p.promotionMode) setPromotionMode(p.promotionMode);
    if (p.selectedIgPost) setSelectedIgPost(p.selectedIgPost);
    if (p.productFile) {
      setProductFile(p.productFile);
      if (p.productPreviewUrl) setProductPreviewUrl(p.productPreviewUrl);
    }
    // 2ª imagen de referencia opcional desde el AssistantFlow → hidrata al sistema
    // extraProductFiles/extraProductPreviews que StrategyFlow ya tiene wired al backend.
    if (p.extraProductFile) {
      setExtraProductFiles([p.extraProductFile]);
      if (p.extraProductPreviewUrl) setExtraProductPreviews([p.extraProductPreviewUrl]);
    }
    const useUserImages = Array.isArray(p.userImages) && p.userImages.length > 0;
    if (useUserImages) {
      setImageSource('uploaded');
      setUserUploadedImages(p.userImages.map(u => ({ file: u.file, previewUrl: u.previewUrl, cloudinaryUrl: u.cloudinaryUrl || null })));
    } else {
      setImageSource('ai');
    }
    setAssistantHydrated(true);
    // Si hay creativos en el cache de localStorage para este producto (usuario ya generó y
    // está volviendo desde Atrás), saltamos la regeneración — sería tirar a la basura los
    // créditos ya cobrados. El cache se limpia solo desde "Borrar todo".
    let hasCachedImages = false;
    try {
      const bizId = localStorage.getItem('nw_active_biz_id') || '1';
      const descKey = (p.productDesc || '').trim().toLowerCase().slice(0, 80);
      if (descKey) {
        const cacheKey = `nw_images_cache_v1_${bizId}_${btoa(unescape(encodeURIComponent(descKey))).slice(0, 24)}`;
        const raw = localStorage.getItem(cacheKey);
        if (raw) {
          const data = JSON.parse(raw);
          hasCachedImages = data && Array.isArray(data.generatedImages) && data.generatedImages.length > 0;
        }
      }
    } catch {}
    // Wait one tick so React commits the state updates, then trigger the right pipeline.
    setTimeout(() => {
      try {
        if (useUserImages) {
          submitUserUploadedImagesRef.current && submitUserUploadedImagesRef.current();
        } else if (hasCachedImages) {
          // El effect de hidratación ya restauró generatedImages — solo navegamos al grid.
          setStep(3);
        } else {
          uploadAndGenerateRef.current && uploadAndGenerateRef.current();
        }
      } catch (e) { console.warn('[assistant] generation kick failed', e); }
    }, 220);
  }, [assistantPrefill, assistantHydrated]);
  const [usedImageModel, setUsedImageModel] = React.useState(null);
  // Copies-cache key. Keyed by biz_id + first 80 chars of trimmed/lowercased productDesc so:
  //   • If user changes product description, cache naturally invalidates.
  //   • If user goes BACK from StrategyFlow to AssistantFlow config and forward again,
  //     StrategyFlow re-mounts but copies survive (no re-call to /api/generate-copies).
  // This prevents wasting Anthropic tokens when the user navigates back.
  const copiesCacheKey = React.useMemo(() => {
    try {
      const bizId = localStorage.getItem('nw_active_biz_id') || '1';
      const descKey = (productDesc || '').trim().toLowerCase().slice(0, 80);
      if (!descKey) return null;
      // Version suffix v3: bumped 2026-04-27 cuando endurecimos las reglas de TÍTULOS = CTAs
      // cortos (no preguntas, max 30 chars, 5 tipos: comando/oferta/escasez/proof/teaser).
      // Invalida todo el cache previo automáticamente.
      return `nw_copies_cache_v3_${bizId}_${btoa(unescape(encodeURIComponent(descKey))).slice(0, 24)}`;
    } catch { return null; }
  }, [productDesc]);
  const [generatedCopies, setGeneratedCopies] = React.useState(() => {
    // Hydrate from cache on first mount so re-mounts (back-then-forward) don't lose state.
    try {
      const bizId = localStorage.getItem('nw_active_biz_id') || '1';
      const guesses = Object.keys(localStorage).filter(k => k.startsWith(`nw_copies_cache_${bizId}_`));
      // We can't compute the key here because productDesc isn't initialized yet — defer to effect.
      return [];
    } catch { return []; }
  });
  const [campaignTitulos, setCampaignTitulos] = React.useState([]);
  const [campaignDescs, setCampaignDescs] = React.useState([]);
  // Hydrate copies from cache when productDesc is available (and thus copiesCacheKey is set).
  // 24h freshness — older cache is considered stale and ignored.
  React.useEffect(() => {
    if (!copiesCacheKey) return;
    if (generatedCopies && generatedCopies.length > 0) return; // already populated, skip
    try {
      const cached = JSON.parse(localStorage.getItem(copiesCacheKey) || 'null');
      if (cached?.copies?.length && Date.now() - (cached.ts || 0) < 24 * 3600 * 1000) {
        setGeneratedCopies(cached.copies);
        if (Array.isArray(cached.titulos)) setCampaignTitulos(cached.titulos);
        if (Array.isArray(cached.descs)) setCampaignDescs(cached.descs);
      }
    } catch {}
  }, [copiesCacheKey]);
  // Persist copies on every change (debounced via React's microtask batching).
  React.useEffect(() => {
    if (!copiesCacheKey || !generatedCopies?.length) return;
    try {
      localStorage.setItem(copiesCacheKey, JSON.stringify({
        copies: generatedCopies,
        titulos: campaignTitulos,
        descs: campaignDescs,
        ts: Date.now(),
      }));
    } catch {}
  }, [copiesCacheKey, generatedCopies, campaignTitulos, campaignDescs]);
  // Cache key compartido con copies para que los creativos completos (imgs + texts) sobrevivan
  // remounts de StrategyFlow (ej. usuario presiona Atrás al asistente y vuelve a entrar).
  // Solo "Borrar todo" limpia este cache.
  const imagesCacheKey = React.useMemo(() => {
    try {
      const bizId = localStorage.getItem('nw_active_biz_id') || '1';
      const descKey = (productDesc || '').trim().toLowerCase().slice(0, 80);
      if (!descKey) return null;
      return `nw_images_cache_v1_${bizId}_${btoa(unescape(encodeURIComponent(descKey))).slice(0, 24)}`;
    } catch { return null; }
  }, [productDesc]);
  const [generatedImages, setGeneratedImages] = React.useState([]);
  // Hydrate imágenes desde localStorage en cuanto tengamos cacheKey (= productDesc disponible).
  React.useEffect(() => {
    if (!imagesCacheKey) return;
    try {
      const raw = localStorage.getItem(imagesCacheKey);
      if (!raw) return;
      const data = JSON.parse(raw);
      if (data && Array.isArray(data.generatedImages) && data.generatedImages.length > 0) {
        // Solo hidratamos si no hay imágenes ya cargadas (evita pisar generación en curso).
        setGeneratedImages(prev => prev.length === 0 ? data.generatedImages : prev);
        if (Array.isArray(data.selectedImgs)) setSelectedImgs(prev => prev.length <= 1 ? data.selectedImgs : prev);
        if (data.usedImageModel) setUsedImageModel(prev => prev || data.usedImageModel);
      }
    } catch {}
  }, [imagesCacheKey]);
  // Persist imágenes al localStorage cuando cambian (solo si están todas listas — sin loaders).
  React.useEffect(() => {
    if (!imagesCacheKey) return;
    try {
      const aiImgs = generatedImages.filter(i => !i.isOriginal);
      const allReady = aiImgs.length > 0 && aiImgs.every(i => i.previewUrl && !i.loading);
      if (!allReady) return;
      localStorage.setItem(imagesCacheKey, JSON.stringify({
        generatedImages,
        selectedImgs,
        usedImageModel,
        productDesc,    // guardado para que el resume del asistente recupere el desc original
        ts: Date.now(),
      }));
    } catch {}
  }, [imagesCacheKey, generatedImages, selectedImgs, usedImageModel]);
  const [generatingCopies, setGeneratingCopies] = React.useState(false);
  // Indices currently being regenerated one-at-a-time. Used to overlay a shimmer loader
  // ONLY on the affected card while /api/generate-copies is in flight.
  const [regeneratingCopyIdx, setRegeneratingCopyIdx] = React.useState(new Set());
  const [zoomedImage, setZoomedImage] = React.useState(null); // lightbox
  const fileInputRef = React.useRef(null);
  const productBase64Ref = React.useRef(null); // shared between generation and regen
  const productMimeRef = React.useRef('image/jpeg');
  // Imagen de referencia secundaria opcional — refs paralelos a los primarios para
  // que regen reuse y pasada a /api/generate-* endpoints como productImageBase64_2.
  const productBase64Ref2 = React.useRef(null);
  const productMimeRef2 = React.useRef('image/jpeg');
  const [productCloudinaryUrl2, setProductCloudinaryUrl2] = React.useState(null);
  // Tracks the description that came from an URL import so that if the user toggles the language
  // (ES ↔ EN) we can re-translate it automatically. Manual typing resets this to null.
  const lastImportedRef = React.useRef(null);

  // When the user flips the language toggle AFTER importing a description, re-translate it so the
  // subsequent copy/strategy generation uses a description in the selected language.
  const prevLangRef = React.useRef(language);
  React.useEffect(() => {
    const prev = prevLangRef.current;
    prevLangRef.current = language;
    if (prev === language) return;
    const current = productDesc.trim();
    if (!current || current.length < 10) return;
    // Only translate if the text currently in the box matches what we imported (user hasn't edited).
    if (lastImportedRef.current && lastImportedRef.current === current) {
      authFetch('/api/translate-text', {
        method: 'POST',
        body: JSON.stringify({ text: current, target_lang: language }),
      })
        .then(r => r.ok ? r.json() : null)
        .then(data => { if (data?.text) { setProductDesc(data.text); lastImportedRef.current = data.text; } })
        .catch(() => {});
    }
  }, [language]);

  const fetchFromUrl = async () => {
    const url = productUrl.trim();
    if (!url) return;
    setUrlLoading(true);
    setUrlError(null);
    try {
      const res = await authFetch('/api/scrape-product', {
        method: 'POST',
        body: JSON.stringify({ url, language }),
      });
      const data = await res.json();
      if (res.ok && data.description) {
        setProductDesc(data.description);
        lastImportedRef.current = data.description; // mark as "imported" so language toggle re-translates
      } else {
        setUrlError(data.error || 'No se pudo obtener información de esa URL');
      }
    } catch {
      setUrlError('Error al conectar. Verifica la URL e intenta de nuevo.');
    } finally {
      setUrlLoading(false);
    }
  };

  // Máximo 2 imágenes de referencia: 1 primaria + 1 extra. Tanto Opus como Gemini y
  // gpt-image-2 reciben ambas para tener más contexto visual del producto.
  const MAX_EXTRA_IMAGES = 1;
  const handleFileSelect = (e) => {
    const files = Array.from(e.target.files || []);
    if (files.length === 0) return;
    const hasPrimary = !!productFile;
    const [first, ...rest] = files;
    if (!hasPrimary) {
      setProductFile(first);
      setProductPreviewUrl(URL.createObjectURL(first));
      setExtraProductFiles(prev => [...prev, ...rest].slice(0, MAX_EXTRA_IMAGES));
      setExtraProductPreviews(prev => [...prev, ...rest.map(f => URL.createObjectURL(f))].slice(0, MAX_EXTRA_IMAGES));
    } else {
      setExtraProductFiles(prev => [...prev, ...files].slice(0, MAX_EXTRA_IMAGES));
      setExtraProductPreviews(prev => [...prev, ...files.map(f => URL.createObjectURL(f))].slice(0, MAX_EXTRA_IMAGES));
    }
    setUploadError(null);
    e.target.value = '';
  };

  const handleFileDrop = (e) => {
    e.preventDefault();
    e.stopPropagation();
    setDragActive(false);
    const files = Array.from(e.dataTransfer?.files || []).filter(f => /^image\/(jpeg|png|webp)$/.test(f.type));
    if (files.length === 0) return;
    const hasPrimary = !!productFile;
    const [first, ...rest] = files;
    if (!hasPrimary) {
      setProductFile(first);
      setProductPreviewUrl(URL.createObjectURL(first));
      setExtraProductFiles(prev => [...prev, ...rest].slice(0, MAX_EXTRA_IMAGES));
      setExtraProductPreviews(prev => [...prev, ...rest.map(f => URL.createObjectURL(f))].slice(0, MAX_EXTRA_IMAGES));
    } else {
      setExtraProductFiles(prev => [...prev, ...files].slice(0, MAX_EXTRA_IMAGES));
      setExtraProductPreviews(prev => [...prev, ...files.map(f => URL.createObjectURL(f))].slice(0, MAX_EXTRA_IMAGES));
    }
    setUploadError(null);
  };

  const removeExtraImage = (idx) => {
    setExtraProductFiles(prev => prev.filter((_, i) => i !== idx));
    setExtraProductPreviews(prev => prev.filter((_, i) => i !== idx));
    // Si removí el único extra (el slot 0), también limpio el cache base64 + cloudinary
    setExtraProductFiles(prev => {
      if (prev.length === 0) {
        productBase64Ref2.current = null;
        setProductCloudinaryUrl2(null);
      }
      return prev;
    });
  };

  const promotePrimary = (idx) => {
    // Swap an extra with the primary (user wants that angle to be the main reference for Gemini)
    setExtraProductFiles(prev => {
      const file = prev[idx]; if (!file) return prev;
      const newExtras = [...prev]; newExtras[idx] = productFile;
      setProductFile(file);
      return newExtras;
    });
    setExtraProductPreviews(prev => {
      const pv = prev[idx]; if (!pv) return prev;
      const newPvs = [...prev]; newPvs[idx] = productPreviewUrl;
      setProductPreviewUrl(pv);
      return newPvs;
    });
  };

  const MOCK_GRADIENTS = [
    'linear-gradient(135deg,#1e1e2e,#2d1b69)',
    'linear-gradient(135deg,#0f2027,#203a43)',
    'linear-gradient(135deg,#1a1a2e,#16213e)',
    'linear-gradient(135deg,#0d1117,#1c2128)',
    'linear-gradient(135deg,#12002f,#1a0050)',
    'linear-gradient(135deg,#002020,#003333)',
    'linear-gradient(135deg,#1a0a00,#3d1a00)',
    'linear-gradient(135deg,#001a33,#002b55)',
    'linear-gradient(135deg,#1a001a,#330033)',
    'linear-gradient(135deg,#0a1628,#0d2137)',
  ];

  const MOCK_COPIES_FALLBACK = [
    { titulo: 'Impulsa tu negocio hoy', texto: 'Consigue más clientes con campañas de publicidad que funcionan. Sin complicaciones.', cta: 'Comenzar ahora', angulo: 'Resultado directo' },
    { titulo: '¿Cansado de no vender?', texto: 'Llega a tus clientes ideales con anuncios creados por IA en minutos.', cta: 'Prueba gratis', angulo: 'Dolor' },
    { titulo: 'Más ventas. Menos esfuerzo.', texto: 'La publicidad inteligente que trabaja por ti 24/7. Meta, Google y TikTok en un solo lugar.', cta: 'Ver cómo funciona', angulo: 'Beneficio-primero' },
  ];

  const COMBO_ANGLES = ['Pain Point', 'Testimonials', 'Benefits', 'Urgency', 'Lifestyle', 'Authority', 'Transformation', 'Identity', 'Comparison', 'Outcome'];

  // When the user provides their own images — skip AI generation, just upload each to Cloudinary
  // and feed them into the generatedImages slots. Then jump to copy-selection (step 4 after copy is generated).
  const submitUserUploadedImages = async () => {
    if (userUploadedImages.length === 0) return;
    if (!productDesc.trim()) { setUploadError('Describe tu producto o servicio antes de continuar.'); return; }
    setUploadError(null);
    setImageGenError(null);
    setStep(2);

    try {
      const uploaded = await Promise.all(userUploadedImages.map(async (item, i) => {
        if (item.cloudinaryUrl) return item;
        const formData = new FormData();
        formData.append('image', item.file);
        const res = await fetch(`${API}/api/upload/product-image`, {
          method: 'POST',
          headers: { Authorization: `Bearer ${localStorage.getItem('nw_token')}` },
          body: formData,
        });
        const data = await res.json();
        if (!res.ok) throw new Error(data.error || 'Upload failed');
        return { ...item, cloudinaryUrl: data.url };
      }));
      setUserUploadedImages(uploaded);

      // Use the first uploaded as product reference (for copy generation context)
      setProductCloudinaryUrl(uploaded[0].cloudinaryUrl);

      // Compress first image to base64 so copy-generation Claude call has visual context
      try {
        const b64 = await new Promise((resolve, reject) => {
          const reader = new FileReader();
          reader.onerror = reject;
          reader.onload = () => {
            const img = new Image();
            img.onerror = reject;
            img.onload = () => {
              const MAX = 1024;
              let w = img.width, h = img.height;
              if (w > MAX || h > MAX) {
                if (w > h) { h = Math.round(h * MAX / w); w = MAX; }
                else { w = Math.round(w * MAX / h); h = MAX; }
              }
              const canvas = document.createElement('canvas');
              canvas.width = w; canvas.height = h;
              canvas.getContext('2d').drawImage(img, 0, 0, w, h);
              resolve(canvas.toDataURL('image/jpeg', 0.85).split(',')[1]);
            };
            img.src = reader.result;
          };
          reader.readAsDataURL(uploaded[0].file);
        });
        productBase64Ref.current = b64;
        productMimeRef.current = 'image/jpeg';
      } catch {}

      // Build generatedImages with user uploads (no AI prompts — these are user's own)
      const slots = uploaded.map((img, i) => ({
        id: i, genIndex: i, isOriginal: false, previewUrl: img.cloudinaryUrl, cloudinaryUrl: img.cloudinaryUrl,
        bg: MOCK_GRADIENTS[i % MOCK_GRADIENTS.length], label: `Tu imagen ${i + 1}`, loading: false,
        angle: 'User Uploaded', prompt: null, userUploaded: true,
      }));
      setGeneratedImages(slots);
      setSelectedImgs(slots.map((_, i) => i));
      setProductPreviewUrl(uploaded[0].previewUrl);
      setStep(3);
    } catch (err) {
      setUploadError(err.message || 'Error al subir imágenes');
      setStep(1);
    }
  };

  const uploadAndGenerate = async () => {
    if (!productDesc.trim()) {
      setUploadError('Describe tu producto o servicio antes de continuar.');
      return;
    }
    setUploadError(null);
    setImageGenError(null);
    setStep(2);

    // Helper: comprime una File a Blob max 1920px, JPEG 85%. Cloudinary free tier
    // corta uploads >10MB (limit del API, no de multer) — la mayoría de fotos de
    // teléfono modernas pesan 8-15MB. Reducimos a una resolución apta para Meta Ads
    // (Meta downscalea a 1080×1350 portrait de todos modos) antes de subir.
    const compressImageForUpload = (file, maxEdge = 1920, quality = 0.85) => new Promise((resolve, reject) => {
      const reader = new FileReader();
      reader.onerror = reject;
      reader.onload = () => {
        const img = new Image();
        img.onerror = reject;
        img.onload = () => {
          let w = img.width, h = img.height;
          if (w > maxEdge || h > maxEdge) {
            if (w > h) { h = Math.round(h * maxEdge / w); w = maxEdge; }
            else { w = Math.round(w * maxEdge / h); h = maxEdge; }
          }
          const canvas = document.createElement('canvas');
          canvas.width = w; canvas.height = h;
          canvas.getContext('2d').drawImage(img, 0, 0, w, h);
          canvas.toBlob(blob => {
            if (!blob) return reject(new Error('canvas toBlob returned null'));
            // Mantener el nombre original con sufijo para tracking en logs
            const baseName = (file.name || 'image').replace(/\.[^.]+$/, '');
            const compressedFile = new File([blob], `${baseName}_compressed.jpg`, { type: 'image/jpeg' });
            resolve(compressedFile);
          }, 'image/jpeg', quality);
        };
        img.src = reader.result;
      };
      reader.readAsDataURL(file);
    });

    // Upload image to Cloudinary
    let imageUrl = null;
    if (productFile) {
      try {
        // Comprimir SIEMPRE para evitar el 10MB cap de Cloudinary free tier.
        // Costo CPU mínimo (~200ms para una foto típica) vs evitar un upload fallido.
        let toUpload = productFile;
        try {
          if (productFile.size > 1024 * 1024) { // solo comprimir >1MB; archivos chicos van directo
            toUpload = await compressImageForUpload(productFile, 1920, 0.85);
            console.log(`[upload] compressed ${productFile.name}: ${(productFile.size / 1024).toFixed(0)}KB → ${(toUpload.size / 1024).toFixed(0)}KB`);
          }
        } catch (e) { console.warn('Compression failed, uploading original:', e.message); }

        const formData = new FormData();
        formData.append('image', toUpload);
        const res = await fetch(`${API}/api/upload/product-image`, {
          method: 'POST',
          headers: { Authorization: `Bearer ${localStorage.getItem('nw_token')}` },
          body: formData,
        });
        // Si el server devuelve HTML (413 default, proxy error, etc.) en lugar de JSON,
        // res.json() throws "Unexpected token '<'". Hacemos parse defensivo.
        let data = {};
        try { data = await res.json(); } catch { data = {}; }
        if (!res.ok) {
          if (res.status === 413 || data.code === 'FILE_TOO_LARGE') {
            throw new Error(data.error || (lang === 'en' ? 'Image is too large (max 20 MB).' : 'La imagen pesa demasiado (máx 20 MB).'));
          }
          throw new Error(data.error || `Error al subir imagen (HTTP ${res.status})`);
        }
        imageUrl = data.url;
        setProductCloudinaryUrl(imageUrl);
      } catch (err) {
        setUploadError(err.message);
        setStep(1);
        return;
      }
    }

    // Compress product image to base64 (max 1024px) — stored in ref for regen reuse
    if (productFile) {
      try {
        const b64 = await new Promise((resolve, reject) => {
          const reader = new FileReader();
          reader.onerror = reject;
          reader.onload = () => {
            const img = new Image();
            img.onerror = reject;
            img.onload = () => {
              const MAX = 1024;
              let w = img.width, h = img.height;
              if (w > MAX || h > MAX) {
                if (w > h) { h = Math.round(h * MAX / w); w = MAX; }
                else { w = Math.round(w * MAX / h); h = MAX; }
              }
              const canvas = document.createElement('canvas');
              canvas.width = w; canvas.height = h;
              canvas.getContext('2d').drawImage(img, 0, 0, w, h);
              resolve(canvas.toDataURL('image/jpeg', 0.85).split(',')[1]);
            };
            img.src = reader.result;
          };
          reader.readAsDataURL(productFile);
        });
        productBase64Ref.current = b64;
        productMimeRef.current = 'image/jpeg';
      } catch (e) { console.warn('Image resize failed:', e.message); }
    }

    // Imagen de referencia 2 (opcional, max 1 extra). Subimos a Cloudinary + comprimimos
    // a base64 paralelo a la primaria. Si falla, seguimos sin bloquear la generación —
    // simplemente Opus/Gemini/gpt-image-2 reciben solo 1 referencia.
    const extraFile = extraProductFiles[0] || null;
    if (extraFile) {
      try {
        let toUpload2 = extraFile;
        try {
          if (extraFile.size > 1024 * 1024) {
            toUpload2 = await compressImageForUpload(extraFile, 1920, 0.85);
            console.log(`[upload] compressed extra ${extraFile.name}: ${(extraFile.size / 1024).toFixed(0)}KB → ${(toUpload2.size / 1024).toFixed(0)}KB`);
          }
        } catch (e) { console.warn('Extra compression failed, uploading original:', e.message); }
        const formData2 = new FormData();
        formData2.append('image', toUpload2);
        const res2 = await fetch(`${API}/api/upload/product-image`, {
          method: 'POST',
          headers: { Authorization: `Bearer ${localStorage.getItem('nw_token')}` },
          body: formData2,
        });
        let data2 = {};
        try { data2 = await res2.json(); } catch { data2 = {}; }
        if (res2.ok && data2.url) setProductCloudinaryUrl2(data2.url);
        else console.warn('Extra image upload non-OK:', res2.status, data2.error || '');
      } catch (e) { console.warn('Extra image upload failed (non-fatal):', e.message); }
      try {
        const b64_2 = await new Promise((resolve, reject) => {
          const reader = new FileReader();
          reader.onerror = reject;
          reader.onload = () => {
            const img = new Image();
            img.onerror = reject;
            img.onload = () => {
              const MAX = 1024;
              let w = img.width, h = img.height;
              if (w > MAX || h > MAX) {
                if (w > h) { h = Math.round(h * MAX / w); w = MAX; }
                else { w = Math.round(w * MAX / h); h = MAX; }
              }
              const canvas = document.createElement('canvas');
              canvas.width = w; canvas.height = h;
              canvas.getContext('2d').drawImage(img, 0, 0, w, h);
              resolve(canvas.toDataURL('image/jpeg', 0.85).split(',')[1]);
            };
            img.src = reader.result;
          };
          reader.readAsDataURL(extraFile);
        });
        productBase64Ref2.current = b64_2;
        productMimeRef2.current = 'image/jpeg';
      } catch (e) { console.warn('Extra image resize failed:', e.message); }
    }

    // La estrategia NO se genera acá — se difiere hasta el momento del lanzamiento
    // (publishCampaign llama generateStrategy() justo antes de publicar). Razones:
    //  1) La estrategia se beneficia de ver el estado FINAL: N imágenes definitivas,
    //     budget definitivo, copies confirmados. Si generábamos temprano, la estrategia
    //     podía quedar desactualizada cuando el user cambia N o budget.
    //  2) El user ve imágenes antes (la parte "creativa") sin esperar 40-60s extra de Opus.
    //  3) Si el user abandona el flow antes de lanzar, no quemamos créditos en estrategia.
    // Trade-off: las imágenes pierden el contexto de "personas" — los ángulos (Identity etc.)
    // se basan solo en productDesc + brand DNA + país. Es una pérdida menor de targeting
    // visual vs el beneficio de la estrategia más fresca y la UX más rápida.

    // Get prompts from Claude vision (Opus sees the product image + description)
    let prompts = [];
    try {
      const promptRes = await authFetch('/api/generate-prompts', {
        method: 'POST',
        body: JSON.stringify({
          count: imageCount,
          productDesc: productDesc.trim(),
          objective, aspectRatio, country, language,
          productImageBase64: productBase64Ref.current,
          productImageMime: productMimeRef.current,
          productImageBase64_2: productBase64Ref2.current,
          productImageMime_2: productMimeRef2.current,
          imageModel,
          productUrl: productUrl || null,
        }),
      });
      if (promptRes.status === 402) {
        // Plan required — frontend usually catches this earlier via hasAiAccess, but keep a safety net.
        const d = await promptRes.json().catch(() => ({}));
        alert(d.error || 'Necesitas una suscripción activa.');
        onClose && onClose();
        return;
      }
      if (promptRes.ok) {
        const d = await promptRes.json();
        prompts = Array.isArray(d.prompts) ? d.prompts : [];
      }
    } catch (e) { console.warn('Prompt gen failed:', e.message); }
    while (prompts.length < imageCount) {
      const angle = COMBO_ANGLES[prompts.length % COMBO_ANGLES.length];
      prompts.push(`Professional ${angle} Facebook ad for ${productDesc.trim()}. Photorealistic. ${aspectRatio} format.`);
    }

    // Build skeleton slots (loading: true) and jump to step 3 immediately
    const skeletons = [];
    if (productPreviewUrl) {
      skeletons.push({ id: 0, genIndex: -1, isOriginal: true, previewUrl: productPreviewUrl, cloudinaryUrl: imageUrl, bg: MOCK_GRADIENTS[0], label: 'Tu imagen', loading: false });
    }
    for (let i = 0; i < imageCount; i++) {
      const idx = skeletons.length;
      skeletons.push({ id: idx, genIndex: i, isOriginal: false, previewUrl: null, cloudinaryUrl: null, bg: MOCK_GRADIENTS[idx % MOCK_GRADIENTS.length], label: `IA ${i + 1}`, loading: true, angle: COMBO_ANGLES[i % COMBO_ANGLES.length], prompt: prompts[i] || null });
    }
    setGeneratedImages(skeletons);
    setSelectedImgs([]);
    setUsedImageModel(null);
    setStep(3); // Show grid immediately with loading spinners

    // Fire all image requests in parallel — each slot fills as its own request completes
    // (avoids rate limiting from sequential calls where image 5 starts 3+ min after image 1)
    await new Promise(r => setTimeout(r, 1500)); // brief warm-up after Claude call
    await Promise.allSettled(
      Array.from({ length: imageCount }, async (_, i) => {
        const slotArrayIdx = productPreviewUrl ? i + 1 : i;
        // Stagger start times slightly so Railway doesn't get hit all at once
        await new Promise(r => setTimeout(r, i * 400));
        try {
          const res = await authFetch('/api/generate-single-image', {
            method: 'POST',
            body: JSON.stringify({
              productImageBase64: productBase64Ref.current,
              productImageMime: productMimeRef.current,
              productImageBase64_2: productBase64Ref2.current,
              productImageMime_2: productMimeRef2.current,
              productImageUrl_2: productCloudinaryUrl2,
              productDesc: productDesc.trim(),
              objective, angleIndex: i,
              aspectRatio, country, language,
              imageModel,
              customPrompt: prompts[i] || null,
              productUrl: productUrl || null,
            }),
          });
          if (res.ok) {
            const imgData = await res.json();
            if (imgData.modelUsed) setUsedImageModel(imgData.modelUsed);
            setGeneratedImages(prev => prev.map(slot =>
              slot.genIndex === i
                ? { ...slot, previewUrl: imgData.previewUrl, cloudinaryUrl: imgData.cloudinaryUrl, angle: imgData.angle || slot.angle, loading: false, prompt: imgData.prompt || slot.prompt || null }
                : slot
            ));
            if (imgData.previewUrl) {
              setSelectedImgs(prev => prev.includes(slotArrayIdx) ? prev : [...prev, slotArrayIdx]);
            }
          } else {
            setGeneratedImages(prev => prev.map(slot => slot.genIndex === i ? { ...slot, loading: false } : slot));
            const errText = await res.text().catch(() => '');
            setImageGenError(`Imagen ${i + 1}: ${errText.slice(0, 120) || 'Error al generar'}`);
          }
        } catch (e) {
          setGeneratedImages(prev => prev.map(slot => slot.genIndex === i ? { ...slot, loading: false } : slot));
          setImageGenError(`Imagen ${i + 1}: ${e.message}`);
        }
      })
    );
  };

  // Keep refs to the LATEST generation closures so the assistant-prefill effect can call
  // them after state has been committed (a first-render closure would see empty state).
  React.useEffect(() => {
    uploadAndGenerateRef.current = uploadAndGenerate;
    submitUserUploadedImagesRef.current = submitUserUploadedImages;
  });

  // Called when user clicks "Continuar con copies" after approving images.
  // If copies already exist (user came back from step 4), skip the Claude call and just advance.
  // Pass `{ force: true }` to regenerate explicitly (wired to a "Regenerar copies" button in step 4).
  const generateCopiesAndContinue = async (opts = {}) => {
    const force = opts && opts.force === true;
    // Reuse cache ONLY if the cache size matches the current number of selected images
    // AND every entry has its own pool (5 variants). If pools are missing, regenerate.
    const need = selectedImgs.length || 5;
    const cacheValid = generatedCopies && generatedCopies.length === need
      && generatedCopies.every(c => Array.isArray(c?.pool?.primaryTexts) && c.pool.primaryTexts.length >= 1);
    if (!force && cacheValid) {
      setStep(4);
      return;
    }
    setGeneratingCopies(true);
    try {
      const bizId = localStorage.getItem('nw_active_biz_id') || '1';
      const dnaRes = await authFetch(`/api/brand-dna/${bizId}`);
      const brandDNA = dnaRes.ok ? await dnaRes.json() : null;

      // La estrategia (con personas) se difiere al publish — los copies acá NO la usan.
      // Sin contexto de personas, los copies se basan en: productDesc, brandDNA, country,
      // imageContext (qué ángulo es esa imagen específica). Cuando el user lance la campaña,
      // la estrategia se generará con el estado final y Meta arma los adsets con esas personas.

      // PER-IMAGE POOLS: one /api/generate-copies call per image (parallel via Promise.all),
      // each returning 5 primary_texts + 5 titles + 5 descriptions tailored to that image.
      // Each ad ends up with its OWN dedicated pool of variants, not a shared global pool.
      const need = selectedImgs.length || 5;
      // Standard Access aprobado 2026-05-04 → desbloqueado MTO (Multi Text Optimization).
      // Pool 5×5×5 por imagen: el backend manda 5 primary_texts + 5 titles + 5 descriptions
      // y runMetaPublish los embebe en asset_feed_spec con
      // creative_features_spec.text_optimizations.OPT_IN. Meta rota la combinación óptima.
      const VARIANTS_PER_IMAGE = 5;
      const VARIANT_FALLBACK = MOCK_COPIES_FALLBACK[0];
      const padArr = (arr, len, make) => {
        const out = Array.isArray(arr) ? arr.slice(0, len) : [];
        while (out.length < len) out.push(make(out.length));
        return out;
      };

      // Build image-context strings — Opus uses these to tailor the 5 variants to each visual.
      const imageContexts = selectedImgs.map((idx, k) => {
        const img = generatedImages[idx];
        return img?.angle
          ? `${img.angle}${img.label ? ` — ${img.label}` : ''}`
          : (img?.label || `Imagen ${k + 1}`);
      });

      // 1 call por imagen en paralelo, cada una pidiendo VARIANTS_PER_IMAGE textos.
      // Si Anthropic está saturado (HTTP 503 + code='ANTHROPIC_OVERLOADED'), threamos
      // un error especial para que el catch externo muestre un mensaje accionable al
      // user en vez de llenar las 5 cards con el mismo texto fallback ("Consigue más
      // clientes con campañas..."). El user merece saber que es un problema temporal
      // de Anthropic, no un bug nuestro ni un AI que "no pudo escribir variantes".
      const poolResults = await Promise.all(imageContexts.map(async (ctx, i) => {
        try {
          const res = await authFetch('/api/generate-copies', {
            method: 'POST',
            body: JSON.stringify({
              objective, productDesc: productDesc.trim(), brandDNA, country, language,
              numCopies: VARIANTS_PER_IMAGE, imageIndex: i, imageContext: ctx,
              // personas omitido: la estrategia se difiere al publish (ver publishCampaign).
            }),
          });
          if (!res.ok) {
            const errData = await res.json().catch(() => ({}));
            // Si es overload de Anthropic, propagamos el error con código específico para que el
            // catch principal lo maneje (sin silencioso fallback). Para 402 (sin créditos) también
            // tiramos error claro. Para otros 5xx, mantenemos null y se rellena con fallback parcial.
            if (errData.code === 'ANTHROPIC_OVERLOADED') {
              const e = new Error('ANTHROPIC_OVERLOADED');
              e.code = 'ANTHROPIC_OVERLOADED';
              e.retryAfter = errData.retry_after_seconds || 45;
              throw e;
            }
            if (errData.code === 'INSUFFICIENT_CREDITS') {
              const e = new Error('INSUFFICIENT_CREDITS');
              e.code = 'INSUFFICIENT_CREDITS';
              throw e;
            }
            return null;
          }
          const data = await res.json();
          return {
            primaryTexts: Array.isArray(data?.primaryTexts) ? data.primaryTexts : [],
            titulos:       Array.isArray(data?.titulos)       ? data.titulos       : [],
            descripciones: Array.isArray(data?.descripciones) ? data.descripciones : [],
          };
        } catch (e) {
          // Re-tiramos los errores específicos para que el catch externo los reconozca.
          if (e && (e.code === 'ANTHROPIC_OVERLOADED' || e.code === 'INSUFFICIENT_CREDITS')) throw e;
          return null;
        }
      }));

      // Each card in step 4 shows variant 1 of its pool; the full pool travels with the card
      // for editing AND for backend asset_feed_spec when publishing.
      const newCopies = poolResults.map((data, i) => {
        const ptArr = padArr(data?.primaryTexts, VARIANTS_PER_IMAGE, () => ({ texto: VARIANT_FALLBACK.texto, titulo: '', angulo: `Variante`, emojis: false }));
        const tlArr = padArr(data?.titulos, VARIANTS_PER_IMAGE, () => VARIANT_FALLBACK.titulo || '');
        const dsArr = padArr(data?.descripciones, VARIANTS_PER_IMAGE, () => '');
        const first = ptArr[0] || { texto: '', titulo: '', angulo: `Variante 1`, emojis: false };
        return {
          texto: first.texto || first.text || '',
          titulo: first.titulo || tlArr[0] || '',
          angulo: first.angulo || `Imagen ${i + 1}`,
          emojis: first.emojis || false,
          pool: {
            primaryTexts: ptArr,
            titulos:       tlArr,
            descripciones: dsArr,
          },
        };
      });
      setGeneratedCopies(newCopies);
      setCampaignTitulos(newCopies.map(c => c.pool.titulos[0] || ''));
      setCampaignDescs(newCopies.map(c => c.pool.descripciones[0] || ''));
      setApprovedCopies(new Set(Array.from({ length: need }, (_, i) => i)));
      // Mark every variant of every image as approved by default. User can reject individual
      // variants; the publish step only sends approved ones to Meta's asset_feed_spec pool.
      const allVars = new Set();
      for (let i = 0; i < need; i++) {
        for (let v = 0; v < VARIANTS_PER_IMAGE; v++) {
          allVars.add(`${i}:${v}`);
        }
      }
      setApprovedVariants(allVars);
      setActiveVariantIdx({});
      setCurrentImgPage(0);
      setEditingVariant(null);
    } catch (e) {
      console.warn('Copy generation failed:', e.message);
      // ANTHROPIC_OVERLOADED → Anthropic está saturado temporalmente. NO procedemos a
      // step 4 con fallback (que muestra las 5 cards idénticas y confunde al user).
      // Mostramos un alert claro y dejamos al user en step 3 para que pueda reintentar.
      if (e?.code === 'ANTHROPIC_OVERLOADED') {
        setGeneratingCopies(false);
        alert(`⚠️ Anthropic (el motor de texto) está temporalmente saturado.\n\nReintentá en ~${e.retryAfter || 45} segundos haciendo click en "Continuar con copies" otra vez. Tus imágenes generadas se mantienen, no hace falta volver a empezar.`);
        return;
      }
      if (e?.code === 'INSUFFICIENT_CREDITS') {
        setGeneratingCopies(false);
        alert('No tenés créditos suficientes para generar los copies. Actualizá tu plan o esperá al próximo ciclo.');
        return;
      }
      // Para cualquier otro error inesperado, mostramos fallback PERO con un alert para
      // que el user sepa que algo salió mal (en vez de mostrar silenciosamente texto repetido).
      alert('No se pudieron generar los textos. Te mostramos un placeholder — podés editar manualmente o reintentar.');
      setGeneratedCopies(MOCK_COPIES_FALLBACK);
      setApprovedCopies(new Set([0]));
    }
    setGeneratingCopies(false);
    setStep(4);
  };

  // Regenerate ONE variant (imgIdx, varIdx). Reuses /api/generate-copies with numCopies=1
  // and the same imageContext that was used for the original pool. Replaces only the
  // primaryTexts[v]/titulos[v]/descripciones[v] of pool[i] — keeps the other 4 variants intact.
  const regenerateVariant = async (i, v) => {
    const key = `${i}:${v}`;
    if (regeneratingVariant.has(key)) return;
    setRegeneratingVariant(prev => { const n = new Set(prev); n.add(key); return n; });
    try {
      const bizId = localStorage.getItem('nw_active_biz_id') || '1';
      const dnaRes = await authFetch(`/api/brand-dna/${bizId}`);
      const brandDNA = dnaRes.ok ? await dnaRes.json() : null;
      const imgSlot = generatedImages[selectedImgs[i]];
      const imageContext = imgSlot?.angle
        ? `${imgSlot.angle}${imgSlot.label ? ` — ${imgSlot.label}` : ''}`
        : (imgSlot?.label || `Imagen ${i + 1}`);
      // Pasar como previousCopies los textos de OTRAS imágenes (excluida la actual)
      // para que la regeneración no reintroduzca duplicados.
      const previousCopies = generatedCopies
        .map((c, idx) => idx === i ? null : (c?.pool?.primaryTexts?.[0] ? {
          primary_text: c.pool.primaryTexts[0]?.texto || c.pool.primaryTexts[0]?.text || '',
          headline: c.pool.titulos?.[0] || '',
          description: c.pool.descripciones?.[0] || '',
        } : null))
        .filter(Boolean);
      const res = await authFetch('/api/generate-copies', {
        method: 'POST',
        body: JSON.stringify({
          objective, productDesc: productDesc.trim(), brandDNA, country, language,
          numCopies: 1, imageIndex: i, imageContext, previousCopies,
          personas: (strategy && Array.isArray(strategy.adsets))
            ? strategy.adsets.map(a => a.persona).filter(Boolean)
            : undefined,
        }),
      });
      const data = await res.json();
      const newPt = data?.primaryTexts?.[0];
      const newTl = data?.titulos?.[0] || '';
      const newDc = data?.descripciones?.[0] || '';
      setGeneratedCopies(prev => prev.map((c, idx) => {
        if (idx !== i) return c;
        const pool = c?.pool || { primaryTexts: [], titulos: [], descripciones: [] };
        const newPrimaryTexts = [...(pool.primaryTexts || [])];
        const newTitulos = [...(pool.titulos || [])];
        const newDescs = [...(pool.descripciones || [])];
        if (newPt) newPrimaryTexts[v] = newPt;
        if (newTl) newTitulos[v] = newTl;
        if (newDc) newDescs[v] = newDc;
        return {
          ...c,
          // If we regenerated variant 0, also update the preview-level fields for legacy code paths
          ...(v === 0 && newPt ? { texto: typeof newPt === 'string' ? newPt : (newPt.texto || newPt.text || ''), titulo: newTl } : {}),
          pool: { primaryTexts: newPrimaryTexts, titulos: newTitulos, descripciones: newDescs },
        };
      }));
    } catch (e) {
      console.warn('Variant regeneration failed:', e.message);
    } finally {
      setRegeneratingVariant(prev => { const n = new Set(prev); n.delete(key); return n; });
    }
  };

  // Inline edit of a single variant field (texto / titulo / descripcion).
  // When v=0 we also mirror the change into the top-level fields (c.texto, campaignTitulos[i],
  // campaignDescs[i]) because the FB-style preview card and the publish payload's
  // single-text legacy fields read from there. Without this mirror, editing variant 0 alone
  // leaves the top-level stale and the preview/publish would show the old text.
  const updateVariantField = (i, v, field, value) => {
    setGeneratedCopies(prev => prev.map((c, idx) => {
      if (idx !== i) return c;
      const pool = c?.pool || { primaryTexts: [], titulos: [], descripciones: [] };
      const next = {
        primaryTexts: [...(pool.primaryTexts || [])],
        titulos:       [...(pool.titulos       || [])],
        descripciones: [...(pool.descripciones || [])],
      };
      if (field === 'texto') {
        const cur = next.primaryTexts[v];
        if (typeof cur === 'string') next.primaryTexts[v] = value;
        else next.primaryTexts[v] = { ...(cur || {}), texto: value };
      } else if (field === 'titulo') {
        next.titulos[v] = value;
      } else if (field === 'descripcion') {
        next.descripciones[v] = value;
      }
      const updated = { ...c, pool: next };
      if (v === 0) {
        if (field === 'texto')        updated.texto = value;
        else if (field === 'titulo')      updated.titulo = value;
        else if (field === 'descripcion') updated.descripcion = value;
      }
      return updated;
    }));
    if (v === 0) {
      if (field === 'titulo')      setCampaignTitulos(prev => { const next = [...prev]; next[i] = value; return next; });
      else if (field === 'descripcion') setCampaignDescs(prev => { const next = [...prev]; next[i] = value; return next; });
    }
  };

  // Approve all variants for image i (used by "Aprobar todas y siguiente").
  const approveAllForImage = (i) => {
    setApprovedVariants(prev => {
      const next = new Set(prev);
      for (let v = 0; v < 5; v++) next.add(`${i}:${v}`);
      return next;
    });
  };

  // "Ver preview del anuncio": approves EVERY variant of EVERY image and advances to step 5.
  const goToPreviewApprovingAll = () => {
    const all = new Set();
    for (let i = 0; i < generatedCopies.length; i++) {
      for (let v = 0; v < 5; v++) all.add(`${i}:${v}`);
    }
    setApprovedVariants(all);
    setApprovedCopies(new Set(Array.from({ length: generatedCopies.length }, (_, i) => i)));
    setStep(5);
  };

  // Single-slot regeneration: triggered when the user hits "Rechazar" on a copy card. Calls
  // /api/generate-copies with numCopies=1, replaces ONLY that index in the state arrays, and
  // auto-approves the new variant. The original angle is preserved so the slot still matches
  // its image (e.g. the "Social Proof" image keeps a Social Proof copy).
  const regenerateSingleCopy = async (i) => {
    if (regeneratingCopyIdx.has(i)) return;
    setRegeneratingCopyIdx(prev => { const n = new Set(prev); n.add(i); return n; });
    try {
      const bizId = localStorage.getItem('nw_active_biz_id') || '1';
      const dnaRes = await authFetch(`/api/brand-dna/${bizId}`);
      const brandDNA = dnaRes.ok ? await dnaRes.json() : null;
      // Pasar como previousCopies los textos de OTRAS imágenes (excluida la actual) para evitar
      // que la regeneración produzca un duplicado.
      const previousCopies = generatedCopies
        .map((c, idx) => idx === i ? null : (c?.texto ? {
          primary_text: c.texto || '',
          headline: campaignTitulos[idx] || c.titulo || '',
          description: campaignDescs[idx] || '',
        } : null))
        .filter(Boolean);
      const res = await authFetch('/api/generate-copies', {
        method: 'POST',
        body: JSON.stringify({
          objective, productDesc: productDesc.trim(), brandDNA, country, language, numCopies: 1, previousCopies,
          personas: (strategy && Array.isArray(strategy.adsets))
            ? strategy.adsets.map(a => a.persona).filter(Boolean)
            : undefined,
        }),
      });
      const data = await res.json();
      const newPrimary = (data?.primaryTexts && data.primaryTexts[0]) || null;
      const newTitulo  = (data?.titulos && data.titulos[0]) || '';
      const newDesc    = (data?.descripciones && data.descripciones[0]) || '';
      if (newPrimary) {
        setGeneratedCopies(prev => prev.map((c, idx) => idx !== i ? c : ({
          ...newPrimary,
          // Preserve the original angle slot so the regenerated copy still pairs with the
          // image that was rendered for that angle (Social Proof / Pain Point / etc.).
          angulo: c?.angulo || newPrimary.angulo || `Variante ${i + 1}`,
        })));
        setCampaignTitulos(prev => prev.map((t, idx) => idx === i ? newTitulo : t));
        setCampaignDescs(prev => prev.map((d, idx) => idx === i ? newDesc : d));
        // Auto-approve the new variant — the user already said "no" to the previous one and
        // asked for a replacement, so default the result to approved.
        setApprovedCopies(prev => { const n = new Set(prev); n.add(i); return n; });
      }
    } catch (e) {
      console.warn('[regen-single] copy regen failed:', e.message);
    }
    setRegeneratingCopyIdx(prev => { const n = new Set(prev); n.delete(i); return n; });
  };

  const [finalCampaign, setFinalCampaign] = React.useState(null);

  // Collect everything the backend needs to persist the campaign
  const buildCampaignBody = (extraStatus, strategyOverride) => {
    const strategyToUse = strategyOverride !== undefined ? strategyOverride : strategy;
    const primaryText = generatedCopies[[...approvedCopies][0] ?? 0] || MOCK_COPIES_FALLBACK[0];
    // Boost-post mode: title from the post caption (first sentence), since there are no
    // generated titles. Fall back to "Boost: <type>" if the post has no caption.
    const isBoostingPost = promotionMode === 'existing_ig' && selectedIgPost;
    const boostTitle = isBoostingPost
      ? (selectedIgPost.caption
          ? selectedIgPost.caption.split('\n')[0].slice(0, 60).trim()
          : `Boost: ${selectedIgPost.type === 'VIDEO' ? 'Video' : selectedIgPost.type === 'CAROUSEL_ALBUM' ? 'Carrusel' : 'Imagen'}`)
      : null;
    const titulo = boostTitle || campaignTitulos[0] || primaryText.titulo || primaryText.title || 'Campaña';
    const firstAiImg = generatedImages[selectedImgs[0] ?? 0];
    const imageUrl = firstAiImg?.cloudinaryUrl || firstAiImg?.previewUrl || productCloudinaryUrl || null;
    const bizId = localStorage.getItem('nw_active_biz_id') || '1';
    const approvedImgs = selectedImgs
      .map(i => generatedImages[i])
      .filter(img => img && !img.isOriginal && (img.cloudinaryUrl || img.previewUrl));
    // Pair each selected image with its matching copy (primary text + headline + description).
    // If fewer copies than images, cycle through the available copies.
    const creativeUrls = approvedImgs.map(img => img.cloudinaryUrl || img.previewUrl);
    const creative_copies = creativeUrls.map((url, i) => {
      const pt = generatedCopies[i] || generatedCopies[i % Math.max(generatedCopies.length, 1)] || primaryText;
      const pool = pt && pt.pool;
      // Filter out empty strings — pool slots that weren't filled by the backend (or were
      // edited to blank) shouldn't go to Meta as empty asset variants. Keep only non-empty.
      const allPrimary = Array.isArray(pool?.primaryTexts)
        ? pool.primaryTexts.map(p => typeof p === 'string' ? p : (p?.texto || p?.text || '')).filter(s => s && s.trim())
        : [];
      const allTitulos = Array.isArray(pool?.titulos)
        ? pool.titulos.filter(s => s && String(s).trim())
        : [];
      const allDescs = Array.isArray(pool?.descripciones)
        ? pool.descripciones.filter(s => s && String(s).trim())
        : [];
      // Fallback: si el pool quedó vacío por algún motivo, al menos mandamos el texto único
      // del top-level para que Meta tenga 1. Pero priorizamos pool si tiene items.
      const fallbackText = (pt && (pt.texto || pt.text || pt.desc)) || '';
      const fallbackTitle = campaignTitulos[i] || campaignTitulos[i % Math.max(campaignTitulos.length, 1)] || titulo;
      const fallbackDesc = campaignDescs[i] || campaignDescs[i % Math.max(campaignDescs.length, 1)] || '';
      const finalPrimary = allPrimary.length ? allPrimary : (fallbackText ? [fallbackText] : []);
      const finalTitulos = allTitulos.length ? allTitulos : (fallbackTitle ? [fallbackTitle] : []);
      const finalDescs = allDescs.length ? allDescs : (fallbackDesc ? [fallbackDesc] : []);
      console.log(`[publish/payload] image #${i}: pool sizes ${finalPrimary.length}b/${finalTitulos.length}t/${finalDescs.length}d`);
      return {
        image_url: url,
        // Single-text "preview" values (for legacy callers and the UI cards)
        primary_text: fallbackText,
        headline: fallbackTitle,
        description: fallbackDesc,
        // PER-AD POOL — backend uses these to build asset_feed_spec per-ad so each image
        // rotates its OWN dedicated pool of variants. Forzamos siempre al menos 1 item para
        // que el backend no caiga al modo legacy-single y use asset_feed_spec.
        primaryTexts:  finalPrimary,
        titulos:       finalTitulos,
        descripciones: finalDescs,
      };
    });
    return {
      title: titulo,
      objective,
      copy_title: titulo,
      copy_desc: primaryText.texto || primaryText.desc,
      copy_cta: campaignDescs[0] || primaryText.cta,
      product_image_url: productCloudinaryUrl,
      product_image_url_2: productCloudinaryUrl2,
      selected_image_url: imageUrl,
      biz_id: bizId,
      // For the TRAFFIC → Instagram profile path, we send the IG URL as product_url so Meta
      // uses it as the ad's landing link. Normalize handles like "@brand" to a full URL.
      product_url: (destMode === 'instagram' && igProfile.trim())
        ? (igProfile.trim().startsWith('http')
            ? igProfile.trim()
            : `https://instagram.com/${igProfile.trim().replace(/^@+/, '')}`)
        : (productUrl || null),
      daily_budget: Number(dailyBudget) || null,
      currency,
      launch_mode: launchMode,
      creative_image_urls: creativeUrls,
      creative_copies,
      creative_prompts: approvedImgs.map(img => img.prompt || ''),
      selected_image_prompt: firstAiImg?.prompt || null,
      product_desc: productDesc || null,
      destination_mode: destMode,
      destination_channels: Object.entries(destChannels).filter(([, v]) => v).map(([k]) => k),
      // Mark which UI flow created/saved this campaign so re-launch from "Mis estrategias"
      // brings the user back to the SAME flow. assistantPrefill is set when StrategyFlow was
      // entered via the AssistantFlow handoff; otherwise this is the advanced path.
      created_via: assistantPrefill ? 'assistant' : 'advanced',
      image_source: imageSource,
      cta_button: ctaButton,
      country: country || null,
      language: language || null,
      aspect_ratio: aspectRatio || null,
      strategy: strategyToUse && !strategyToUse.error ? strategyToUse : null,
      image_model: usedImageModel || imageModel || null,
      // Boost-an-existing-IG-post mode: send the picked post id + permalink so the publish
      // worker creates an adcreative referencing the existing post via object_story_id
      // instead of generating a new image + copy.
      ig_post_id: promotionMode === 'existing_ig' && selectedIgPost ? selectedIgPost.id : null,
      ig_post_permalink: promotionMode === 'existing_ig' && selectedIgPost ? (selectedIgPost.permalink || null) : null,
      status: extraStatus || 'active',
    };
  };

  // Save to DB as draft (status='draft') — NO Meta publish. User can finish later from Mis Estrategias.
  const saveDraft = async () => {
    setPublishError(null);
    setMetaPublishInfo(null);
    setLaunching(true);
    try {
      const res = await authFetch('/api/campaigns', {
        method: 'POST',
        body: JSON.stringify(buildCampaignBody('draft')),
      });
      if (!res.ok) throw new Error((await res.json()).error || 'Error al guardar');
      const campaign = await res.json();
      setFinalCampaign({ ...campaign, savedAsDraft: true });
      setLaunching(false);
      setStep(6);
    } catch (e) {
      setPublishError(e.message || 'Error al guardar la campaña');
      setLaunching(false);
    }
  };

  const publishCampaign = async () => {
    setPublishError(null);
    setMetaPublishInfo(null);
    setLaunching(true);
    const bizId = localStorage.getItem('nw_active_biz_id') || '1';

    // 0) Generate the strategy NOW if we don't have a valid one yet (or the one we have is a prior
    // error). This keeps Anthropic tokens from being burned on draft saves and step-5 revisits.
    // If the user already opened "Ver detalles de estrategia" and it succeeded, we reuse it.
    let strategyForBody = strategy && !strategy.error ? strategy : null;
    if (!strategyForBody) {
      setProgressSteps([]);
      setProgressCurrent(lang === 'en' ? 'Building your Meta Ads strategy…' : 'Creando la estrategia de Meta Ads…');
      setProgressOpen(true);
      const s = await generateStrategy();
      if (s && !s.error) strategyForBody = s;
      else {
        setPublishError((s && s.error) || (lang === 'en' ? 'Could not generate strategy' : 'No se pudo generar la estrategia'));
        setProgressOpen(false);
        setLaunching(false);
        return;
      }
    }

    // 1) Save campaign to DB (including the fresh strategy)
    let campaign;
    try {
      const res = await authFetch('/api/campaigns', {
        method: 'POST',
        body: JSON.stringify(buildCampaignBody('active', strategyForBody)),
      });
      if (!res.ok) throw new Error((await res.json()).error || 'Error al guardar');
      campaign = await res.json();
    } catch (e) {
      setPublishError(e.message || 'Error al guardar la campaña');
      setProgressOpen(false);
      setLaunching(false);
      return;
    }

    // If we came from a DRAFT, remove the original draft row so the user doesn't see
    // duplicates in Mis Estrategias (images are reused — they stay in Cloudinary).
    // If we came from an ACTIVE campaign (Reutilizar), preserve the source — the user
    // expects the original ad in Meta to keep running alongside the new reused one.
    if (draftCampaign?.id && draftCampaign.status === 'draft') {
      try {
        // Temporarily null the creative_image_urls of the draft so cleanup doesn't delete Cloudinary assets still in use
        await authFetch(`/api/campaigns/${draftCampaign.id}/unlink-creatives`, { method: 'POST' }).catch(() => {});
        await authFetch(`/api/campaigns/${draftCampaign.id}`, { method: 'DELETE' });
      } catch {}
    }

    // 2) Publish to Meta Ads via SSE so we can show live progress in the modal.
    setProgressSteps([]);
    setProgressCurrent(lang === 'en' ? 'Starting…' : 'Empezando…');
    setProgressOpen(true);

    const token = localStorage.getItem('nw_token') || '';
    // is_relaunch=true sólo cuando reutilizamos una campaña ya publicada (no draft).
    // Backend cobra 5 créditos en ese caso; primer publish original sigue gratis.
    const isRelaunch = !!(draftCampaign?.id && draftCampaign.status !== 'draft');
    const params = new URLSearchParams({ token, biz_id: bizId, country: country || '', is_relaunch: isRelaunch ? 'true' : 'false' });
    const url = `${API}/api/campaigns/${campaign.id}/publish-meta-stream?${params.toString()}`;

    await new Promise((resolve) => {
      const es = new EventSource(url);
      let finished = false;
      const finish = (err, data) => {
        if (finished) return; finished = true;
        es.close();
        if (err) {
          setPublishError(err);
          setFinalCampaign(campaign);
          setProgressOpen(false);
          setLaunching(false);
        } else {
          setMetaPublishInfo(data);
          setFinalCampaign({ ...campaign, ...data });
          setProgressOpen(false);
          setLaunching(false);
          setStep(6);
        }
        resolve();
      };
      es.onmessage = (ev) => {
        try {
          const d = JSON.parse(ev.data);
          if (d.step === 'error') {
            // SSE no atraviesa nxFetch — propaga el 402 manualmente al modal global.
            if (d.code === 'INSUFFICIENT_CREDITS' || d.code === 'PLAN_REQUIRED') {
              window.dispatchEvent(new CustomEvent('nx-credits-blocked', { detail: d }));
            }
            finish(d.error || 'Error al publicar en Meta'); return;
          }
          if (d.step === 'complete' || (d.step === 'done' && d.ok)) {
            window.dispatchEvent(new CustomEvent('nx-credits-changed'));
            // Campaña publicada con éxito → limpiar el cache de creativos para que el
            // banner "Tienes creativos sin publicar" no aparezca en el home.
            try {
              if (imagesCacheKey) localStorage.removeItem(imagesCacheKey);
              if (copiesCacheKey) localStorage.removeItem(copiesCacheKey);
              window.dispatchEvent(new CustomEvent('nx-creatives-cache-cleared'));
            } catch {}
            finish(null, d); return;
          }
          if (d.msg) {
            setProgressCurrent(d.msg);
            setProgressSteps(prev => [...prev, { msg: d.msg, ts: Date.now() }]);
          }
        } catch {}
      };
      es.onerror = () => { finish(lang === 'en' ? 'Connection lost while publishing' : 'Se perdió la conexión al publicar'); };
    });
  };

  const saveCampaign = () => {
    onComplete({ campaign: finalCampaign });
  };

  const objectives = ts.objectives;

  const toggleImg = (i) => {
    setSelectedImgs(prev => prev.includes(i) ? prev.filter(x => x !== i) : [...prev, i]);
  };

  const rejectAndRegenerate = async (imgIndex) => {
    setSelectedImgs(prev => prev.filter(i => i !== imgIndex));
    setRegeneratingSlots(prev => new Set([...prev, imgIndex]));
    // Limpiar la imagen rechazada inmediatamente para que la card vuelva al estado de
    // loading (círculo 0→100 % del componente ImageGenProgress). Sin este reset, el render
    // se queda mostrando la imagen vieja porque `img.previewUrl` aún tiene valor y el
    // primer branch del ternario gana.
    setGeneratedImages(prev => prev.map((img, i) =>
      i === imgIndex ? { ...img, previewUrl: null, cloudinaryUrl: null, loading: true } : img
    ));
    // Pick an angle that is NOT already in use by another image in the set.
    // Backend cycles AD_CREATIVE_COMBOS with angleIndex % combos.length (10 combos).
    const COMBO_COUNT = 10;
    const occupied = new Set(
      generatedImages
        .map((img, i) => (i !== imgIndex && img && img.angle ? img.angle : null))
        .filter(Boolean)
    );
    const ANGLE_BY_SLOT = [
      'Pain Point',
      'Social Proof + Testimonials',
      'Direct Result + Benefits',
      'Urgency + Price',
      'Curiosity + Lifestyle',
      'Authority + Expertise',
      'Transformation + Before/After',
      'Identity + Aspiration',
      'Comparison + This vs That',
      'Outcome + Direct Result',
    ];
    let chosenAngleIndex = (imgIndex + generatedImages.length) % COMBO_COUNT;
    for (let k = 0; k < COMBO_COUNT; k++) {
      const candidate = (chosenAngleIndex + k) % COMBO_COUNT;
      if (!occupied.has(ANGLE_BY_SLOT[candidate])) { chosenAngleIndex = candidate; break; }
    }
    try {
      const res = await authFetch('/api/generate-single-image', {
        method: 'POST',
        body: JSON.stringify({
          productImageBase64: productBase64Ref.current,
          productImageMime: productMimeRef.current,
          productImageUrl: productCloudinaryUrl,
          productImageBase64_2: productBase64Ref2.current,
          productImageMime_2: productMimeRef2.current,
          productImageUrl_2: productCloudinaryUrl2,
          productDesc: productDesc.trim(),
          objective,
          angleIndex: chosenAngleIndex,
          aspectRatio, country, language, imageModel,
          productUrl: productUrl || null,
        }),
      });
      if (res.ok) {
        const newImg = await res.json();
        setGeneratedImages(prev => prev.map((img, i) =>
          i === imgIndex ? { ...img, ...newImg, id: imgIndex, loading: false, prompt: newImg.prompt || img.prompt || null } : img
        ));
        setSelectedImgs(prev => [...prev, imgIndex]);
      }
    } catch (e) { console.warn('Regeneration failed:', e); }
    setRegeneratingSlots(prev => { const s = new Set(prev); s.delete(imgIndex); return s; });
  };

  const downloadImage = async (img, idx) => {
    try {
      const res = await authFetch(`/api/download-image?url=${encodeURIComponent(img.cloudinaryUrl || img.previewUrl)}&filename=ad-creativo-${idx + 1}`);
      const blob = await res.blob();
      const url = URL.createObjectURL(blob);
      const a = document.createElement('a');
      a.href = url; a.download = `ad-creativo-${idx + 1}.jpg`; a.click();
      URL.revokeObjectURL(url);
    } catch (e) { console.warn('Download failed:', e); }
  };

  const [downloadingAll, setDownloadingAll] = React.useState(false);
  const [downloadProgress, setDownloadProgress] = React.useState(0); // 0..100
  const [downloadMode, setDownloadMode] = React.useState('zip'); // 'zip' | 'individual'

  // Multi-CDN JSZip loader with clear error reporting.
  const loadJSZip = () => new Promise((resolve, reject) => {
    if (window.JSZip) return resolve(window.JSZip);
    const cdns = [
      'https://cdn.jsdelivr.net/npm/jszip@3.10.1/dist/jszip.min.js',
      'https://unpkg.com/jszip@3.10.1/dist/jszip.min.js',
      'https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js',
    ];
    let i = 0;
    const tryNext = () => {
      if (i >= cdns.length) return reject(new Error('All JSZip CDNs failed'));
      const s = document.createElement('script');
      s.src = cdns[i++];
      s.async = true;
      s.onload = () => window.JSZip ? resolve(window.JSZip) : tryNext();
      s.onerror = tryNext;
      document.head.appendChild(s);
    };
    tryNext();
  });

  // Preload JSZip as soon as the component mounts so the ZIP download is instant
  // and we know early if the CDN fails (shows fallback badge instead of silently switching).
  React.useEffect(() => {
    loadJSZip()
      .then(() => { /* ready */ })
      .catch(() => { setDownloadMode('individual'); });
  }, []);

  const downloadAllImages = async () => {
    const toDownload = generatedImages.filter(img => !img.loading && (img.cloudinaryUrl || img.previewUrl));
    if (!toDownload.length) return;
    setDownloadingAll(true);
    setDownloadProgress(0);

    // Path A — ZIP. Try hard to make this work; only fall back if truly impossible.
    try {
      const JSZip = await loadJSZip();
      const zip = new JSZip();
      let done = 0;

      // Fetch images sequentially to show progress, but run each fetch in the background
      // adding to the ZIP as it completes.
      const total = toDownload.length;
      const entries = await Promise.all(toDownload.map(async (img, i) => {
        const url = img.cloudinaryUrl || img.previewUrl;
        try {
          // Prefer our backend proxy for reliability; fall back to direct Cloudinary URL
          let blob = null;
          try {
            const res = await authFetch(`/api/download-image?url=${encodeURIComponent(url)}&filename=ad-creativo-${i + 1}`);
            if (res.ok) blob = await res.blob();
          } catch {}
          if (!blob) {
            // Direct fetch from Cloudinary (public URLs are CORS-enabled by default)
            const direct = await fetch(url);
            if (direct.ok) blob = await direct.blob();
          }
          done += 1;
          setDownloadProgress(Math.round((done / total) * 100));
          if (!blob) return null;
          const angle = (img.angle || 'creativo').toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/(^-|-$)/g, '');
          const name = `${String(i + 1).padStart(2, '0')}-${angle || 'creativo'}.jpg`;
          return { name, blob };
        } catch (e) {
          console.warn(`[ZIP] slot ${i} fetch failed:`, e?.message);
          done += 1;
          setDownloadProgress(Math.round((done / total) * 100));
          return null;
        }
      }));

      const okEntries = entries.filter(Boolean);
      if (okEntries.length === 0) throw new Error('No images could be fetched');
      okEntries.forEach(e => zip.file(e.name, e.blob));

      const content = await zip.generateAsync({ type: 'blob', compression: 'STORE' });
      const stamp = new Date().toISOString().slice(0, 10);
      const blobUrl = URL.createObjectURL(content);
      const a = document.createElement('a');
      a.href = blobUrl; a.download = `nexwall-creativos-${stamp}.zip`;
      document.body.appendChild(a); a.click(); document.body.removeChild(a);
      setTimeout(() => URL.revokeObjectURL(blobUrl), 5000);
      setDownloadMode('zip');
    } catch (err) {
      console.warn('[ZIP] failed, falling back to individual downloads:', err?.message);
      setDownloadMode('individual');
      // Fallback: sequential individual downloads
      for (let i = 0; i < toDownload.length; i++) {
        await downloadImage(toDownload[i], i);
        setDownloadProgress(Math.round(((i + 1) / toDownload.length) * 100));
        if (i < toDownload.length - 1) await new Promise(r => setTimeout(r, 350));
      }
    }

    setDownloadingAll(false);
    setDownloadProgress(0);
  };

  const toggleCopy = (i) => {
    setApprovedCopies(prev => {
      const next = new Set(prev);
      if (next.has(i)) { if (next.size > 1) next.delete(i); }
      else next.add(i);
      return next;
    });
  };

  const firstApprovedPrimaryText = generatedCopies[[...approvedCopies][0] ?? 0] || {};
  const firstApprovedCopy = {
    titulo: campaignTitulos[0] || firstApprovedPrimaryText.titulo || 'Tu título aquí',
    texto: firstApprovedPrimaryText.texto || 'Tu texto principal aparecerá aquí — es el texto grande arriba de la imagen.',
    descripcion: campaignDescs[0] || '',
  };
  const firstSelectedImg = generatedImages[selectedImgs[0] ?? 0];

  return (
    <NxModal onClose={!launching ? onClose : undefined} wide noBackdropClose
      // Steps with less content (loading, preview, success) get a tighter frame so the
      // modal hugs the content instead of showing wide empty bands on the sides.
      // Step 2 = loading checklist (mascot + 8 lines). Steps 5/6 = preview + launched.
      // Step 3 (image grid) and step 4 (copies grid) keep the full 1080 since they
      // genuinely need the width.
      maxWidth={
        step === 2
          ? '700px'
          : (step === 5 || step === 6)
            ? (assistantPrefill ? '700px' : '820px')
            : (step === 3 || step === 4)
              ? '960px'    // grid de imágenes/copies — más compacto que 1080 default
              : undefined
      }>
      <div style={{ padding: '28px', position: 'relative' }}>
        {!launching && onClose && (
          <button
            onClick={onClose}
            aria-label="Cerrar"
            style={{
              position: 'absolute', top: '14px', right: '14px',
              width: '32px', height: '32px', borderRadius: '50%',
              background: NX.bg3, border: `1px solid ${NX.border}`,
              color: NX.muted, fontSize: '16px', lineHeight: 1, cursor: 'pointer',
              display: 'flex', alignItems: 'center', justifyContent: 'center',
              zIndex: 10, transition: 'all 0.15s',
            }}
            onMouseEnter={e => { e.currentTarget.style.background = NX.bg4; e.currentTarget.style.color = NX.text; }}
            onMouseLeave={e => { e.currentTarget.style.background = NX.bg3; e.currentTarget.style.color = NX.muted; }}
          >×</button>
        )}
        {step === 0 && <>
          <h2 style={{ fontSize: '17px', fontWeight: 500, color: NX.text, margin: '0 0 6px' }}>{ts.steps[0].title}</h2>
          <p style={{ color: NX.muted, fontSize: '13px', margin: '0 0 20px' }}>{ts.steps[0].subtitle}</p>

          {/* Banner persistence: ya hay imágenes generadas de un intento previo */}
          {generatedImages.filter(i => !i.isOriginal).length > 0 && (
            <div style={{ padding: '12px 14px', background: 'rgba(52,211,153,0.08)', border: '1px solid rgba(52,211,153,0.25)', borderRadius: '10px', marginBottom: '16px', display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: '10px', flexWrap: 'wrap' }}>
              <div style={{ fontSize: '12px', color: NX.text, flex: 1, minWidth: 0 }}>
                <span style={{ color: NX.success, fontWeight: 600 }}>✓</span> {lang === 'en' ? `You have ${generatedImages.filter(i => !i.isOriginal).length} images from a previous generation.` : `Tienes ${generatedImages.filter(i => !i.isOriginal).length} imágenes de una generación anterior.`}
              </div>
              <div style={{ display: 'flex', gap: '6px' }}>
                <button onClick={() => setStep(3)}
                  style={{ padding: '6px 12px', background: NX.success, border: 'none', borderRadius: '8px', color: '#0a0a0a', fontSize: '11px', fontWeight: 600, cursor: 'pointer', fontFamily: "'DM Sans',sans-serif" }}>
                  {lang === 'en' ? 'Continue with them' : 'Continuar con ellas'}
                </button>
              </div>
            </div>
          )}

          <div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: '10px', marginBottom: '20px' }}>
            {objectives.map(obj => (
              <div key={obj.id} onClick={() => setObjective(obj.id)}
                style={{
                  padding: '14px 14px 12px',
                  background: objective === obj.id ? NX.bg4 : NX.bg3,
                  border: `1px solid ${objective === obj.id ? NX.accent : obj.highlight ? 'rgba(251,191,36,0.4)' : NX.border}`,
                  borderRadius: '12px', cursor: 'pointer', transition: 'all 0.15s',
                  position: 'relative',
                  display: 'flex', flexDirection: 'column', alignItems: 'flex-start', gap: '8px',
                  boxShadow: obj.highlight ? '0 0 20px rgba(251,191,36,0.08)' : 'none',
                  minHeight: '116px',
                }}>
                {obj.highlight && (
                  <span style={{ position: 'absolute', top: '-9px', left: '12px', background: 'linear-gradient(90deg,#fbbf24,#f59e0b)', borderRadius: '100px', padding: '2px 9px', fontSize: '9px', fontWeight: 700, color: '#000', letterSpacing: '0.04em' }}>
                    RECOMENDADO
                  </span>
                )}
                <div style={{ fontSize: '26px', lineHeight: 1 }}>{obj.icon}</div>
                <div style={{ flex: 1 }}>
                  <div style={{ fontSize: '13px', fontWeight: 500, color: obj.highlight ? NX.warning : NX.text, marginBottom: '3px' }}>{obj.label}</div>
                  <div style={{ fontSize: '11px', color: NX.muted, lineHeight: 1.35 }}>{obj.desc}</div>
                </div>
              </div>
            ))}
          </div>
          {/* Destination picker — per-objective required fields.
              - Ventas / Leads / Conversiones: website URL OR messaging channel
              - Tráfico: website URL OR Instagram profile URL
              - Interacción / Reconocimiento: uses the connected FB Page (no URL needed)
              - Promoción de la app: app store URLs (coming soon) */}
          {objective && ['ventas', 'conversiones', 'leads'].includes(objective) && (
            <div style={{ marginBottom: '20px', padding: '14px', background: NX.bg3, border: `1px solid ${NX.border}`, borderRadius: '12px' }}>
              <div style={{ fontSize: '13px', fontWeight: 500, color: NX.text, marginBottom: '4px' }}>
                {lang === 'en' ? 'Where does the click go?' : '¿A dónde va el cliente al hacer click?'}
              </div>
              <div style={{ fontSize: '11px', color: NX.muted, marginBottom: '12px' }}>
                {lang === 'en' ? 'You can only pick one destination type.' : 'Solo puedes elegir un tipo de destino.'}
              </div>

              {/* Mode toggle */}
              <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '8px', marginBottom: '12px' }}>
                {[
                  { id: 'web', label: lang === 'en' ? 'Website' : 'Página web', icon: '🌐' },
                  { id: 'messaging', label: lang === 'en' ? 'Messaging' : 'Mensajería', icon: '💬' },
                ].map(m => (
                  <div key={m.id} onClick={() => setDestMode(m.id)}
                    style={{ padding: '10px 12px', background: destMode === m.id ? NX.bg4 : NX.bg2, border: `1px solid ${destMode === m.id ? NX.accent : NX.border}`, borderRadius: '10px', cursor: 'pointer', textAlign: 'center', fontSize: '12px', fontWeight: destMode === m.id ? 600 : 400, color: destMode === m.id ? NX.accent : NX.muted, transition: 'all 0.15s' }}>
                    <span style={{ marginRight: '6px' }}>{m.icon}</span>{m.label}
                  </div>
                ))}
              </div>

              {/* Web: URL input */}
              {destMode === 'web' && (
                <input
                  type="url" value={productUrl} onChange={e => setProductUrl(e.target.value)}
                  placeholder={lang === 'en' ? 'https://yoursite.com/product' : 'https://tutienda.com/producto'}
                  data-flash-target="productUrl"
                  className={flashField === 'productUrl' ? 'nx-flash' : undefined}
                  style={{ width: '100%', background: NX.bg4, border: `1px solid ${productUrl ? NX.accent : NX.border}`, borderRadius: '8px', color: NX.text, fontSize: '12px', padding: '9px 12px', fontFamily: "'DM Sans',sans-serif", outline: 'none', boxSizing: 'border-box' }}/>
              )}

              {/* Messaging: channel picker — large brand-colored tiles side by side.
                  Each tile: brand icon (big) + name below. Click toggles the channel. */}
              {destMode === 'messaging' && (
                <>
                  <div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: '10px' }}>
                    {[
                      {
                        id: 'whatsapp',
                        label: 'WhatsApp',
                        color: '#25D366',
                        bg: 'rgba(37,211,102,0.08)',
                        icon: (
                          <svg width="42" height="42" viewBox="0 0 32 32" fill="#25D366">
                            <path d="M16 0C7.163 0 0 7.163 0 16c0 2.824.737 5.474 2.03 7.77L0 32l8.43-2.212A15.93 15.93 0 0016 32c8.837 0 16-7.163 16-16S24.837 0 16 0zm0 29.2c-2.515 0-4.852-.686-6.867-1.877l-.493-.293-4.988 1.31 1.332-4.867-.321-.505A13.145 13.145 0 012.8 16C2.8 8.72 8.72 2.8 16 2.8S29.2 8.72 29.2 16 23.28 29.2 16 29.2zm7.575-9.864c-.415-.207-2.454-1.21-2.834-1.349-.38-.138-.657-.207-.933.208-.276.414-1.07 1.348-1.312 1.625-.242.277-.484.312-.898.104-.415-.208-1.75-.645-3.333-2.057-1.232-1.098-2.064-2.454-2.306-2.868-.242-.415-.026-.64.182-.847.186-.186.415-.484.622-.726.207-.242.276-.415.415-.692.138-.277.069-.519-.034-.727-.104-.207-.934-2.25-1.28-3.08-.336-.81-.678-.7-.933-.712l-.795-.014c-.276 0-.726.104-1.106.519-.38.415-1.45 1.417-1.45 3.46 0 2.042 1.485 4.015 1.692 4.292.207.277 2.924 4.463 7.088 6.26 2.453 1.058 3.41 1.147 4.637.965.746-.111 2.454-1.003 2.8-1.972.346-.969.346-1.8.242-1.973-.104-.173-.38-.276-.795-.484z"/>
                          </svg>
                        ),
                      },
                      {
                        id: 'instagram',
                        label: 'Instagram DM',
                        color: '#E1306C',
                        bg: 'rgba(225,48,108,0.08)',
                        icon: (
                          <svg width="42" height="42" viewBox="0 0 24 24">
                            <defs>
                              <linearGradient id="igdmg" x1="0" y1="0" x2="24" y2="24" gradientUnits="userSpaceOnUse">
                                <stop offset="0%" stopColor="#F58529"/>
                                <stop offset="50%" stopColor="#DD2A7B"/>
                                <stop offset="100%" stopColor="#8134AF"/>
                              </linearGradient>
                            </defs>
                            <path fill="url(#igdmg)" d="M12 2.163c3.204 0 3.584.012 4.85.07 3.252.148 4.771 1.691 4.919 4.919.058 1.265.069 1.645.069 4.849 0 3.205-.012 3.584-.069 4.849-.149 3.225-1.664 4.771-4.919 4.919-1.266.058-1.644.07-4.85.07-3.204 0-3.584-.012-4.849-.07-3.26-.149-4.771-1.699-4.919-4.92-.058-1.265-.07-1.644-.07-4.849 0-3.204.013-3.583.07-4.849.149-3.227 1.664-4.771 4.919-4.919 1.266-.057 1.645-.069 4.849-.069zm0-2.163c-3.259 0-3.667.014-4.947.072-4.358.2-6.78 2.618-6.98 6.98-.059 1.281-.073 1.689-.073 4.948 0 3.259.014 3.668.072 4.948.2 4.358 2.618 6.78 6.98 6.98 1.281.058 1.689.072 4.948.072 3.259 0 3.668-.014 4.948-.072 4.354-.2 6.782-2.618 6.979-6.98.059-1.28.073-1.689.073-4.948 0-3.259-.014-3.667-.072-4.947-.196-4.354-2.617-6.78-6.979-6.98-1.281-.059-1.69-.073-4.949-.073zm0 5.838c-3.403 0-6.162 2.759-6.162 6.162s2.759 6.163 6.162 6.163 6.162-2.759 6.162-6.163c0-3.403-2.759-6.162-6.162-6.162zm0 10.162c-2.209 0-4-1.79-4-4 0-2.209 1.791-4 4-4s4 1.791 4 4c0 2.21-1.791 4-4 4zm6.406-11.845c-.796 0-1.441.645-1.441 1.44s.645 1.44 1.441 1.44c.795 0 1.439-.645 1.439-1.44s-.644-1.44-1.439-1.44z"/>
                          </svg>
                        ),
                      },
                      {
                        id: 'messenger',
                        label: 'Messenger',
                        color: '#0084FF',
                        bg: 'rgba(0,132,255,0.08)',
                        icon: (
                          <svg width="42" height="42" viewBox="0 0 32 32">
                            <defs>
                              <linearGradient id="msgrg" x1="0" y1="32" x2="32" y2="0" gradientUnits="userSpaceOnUse">
                                <stop offset="0%" stopColor="#0084FF"/>
                                <stop offset="100%" stopColor="#44BEC7"/>
                              </linearGradient>
                            </defs>
                            <path fill="url(#msgrg)" d="M16 0C7.163 0 0 6.613 0 14.778c0 4.72 2.385 8.915 6.091 11.633V32l5.565-3.057c1.48.408 3.04.626 4.656.626 8.837 0 16-6.613 16-14.778C32 6.613 24.837 0 16 0zm1.585 19.903l-4.09-4.36-7.98 4.36L14.326 10.5l4.185 4.36 7.885-4.36-8.811 9.403z"/>
                          </svg>
                        ),
                      },
                    ].map(ch => {
                      const active = destChannels[ch.id];
                      return (
                        <button key={ch.id} type="button"
                          onClick={() => setDestChannels({ whatsapp: false, instagram: false, messenger: false, [ch.id]: !active })}
                          style={{
                            padding: '18px 12px 14px',
                            background: active ? ch.bg : NX.bg3,
                            border: `1.5px solid ${active ? ch.color : NX.border}`,
                            borderRadius: '14px',
                            cursor: 'pointer',
                            display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '10px',
                            fontFamily: "'DM Sans',sans-serif",
                            transition: 'all 0.15s',
                            position: 'relative',
                            boxShadow: active ? `0 0 24px ${ch.color}22` : 'none',
                          }}
                          onMouseEnter={e => { if (!active) { e.currentTarget.style.background = NX.bg4; e.currentTarget.style.borderColor = NX.border2; } }}
                          onMouseLeave={e => { if (!active) { e.currentTarget.style.background = NX.bg3; e.currentTarget.style.borderColor = NX.border; } }}>
                          {active && (
                            <div style={{ position: 'absolute', top: '8px', right: '8px', width: '18px', height: '18px', borderRadius: '50%', background: ch.color, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
                              <svg width="10" height="10" viewBox="0 0 14 14" fill="none"><path d="M3 7l3 3 5-7" stroke="#fff" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round"/></svg>
                            </div>
                          )}
                          <div style={{ width: '48px', height: '48px', display: 'flex', alignItems: 'center', justifyContent: 'center', opacity: active ? 1 : 0.82, transition: 'opacity 0.15s' }}>
                            {ch.icon}
                          </div>
                          <div style={{ fontSize: '13px', fontWeight: 600, color: active ? ch.color : NX.text, letterSpacing: '-0.01em' }}>
                            {ch.label}
                          </div>
                        </button>
                      );
                    })}
                  </div>
                  {!metaAssets?.page_id && (
                    <div style={{ fontSize: '11px', color: NX.warning, marginTop: '10px', padding: '8px 10px', background: 'rgba(251,191,36,0.08)', border: '1px solid rgba(251,191,36,0.25)', borderRadius: '6px' }}>
                      {lang === 'en'
                        ? '⚠ Connect your Facebook Page so Meta can deliver messages to these channels.'
                        : '⚠ Conecta tu página de Facebook para que Meta entregue los mensajes a estos canales.'}
                    </div>
                  )}
                  {!Object.values(destChannels).some(Boolean) && (
                    <div style={{ fontSize: '11px', color: NX.muted, marginTop: '8px', textAlign: 'center' }}>
                      {lang === 'en' ? 'Pick at least one channel.' : 'Elige al menos un canal.'}
                    </div>
                  )}
                </>
              )}
            </div>
          )}

          {/* TRÁFICO — needs website URL OR Instagram profile URL (new path). */}
          {objective === 'trafico' && (
            <div style={{ marginBottom: '20px', padding: '14px', background: NX.bg3, border: `1px solid ${NX.border}`, borderRadius: '12px' }}>
              <div style={{ fontSize: '13px', fontWeight: 500, color: NX.text, marginBottom: '4px' }}>
                {lang === 'en' ? 'Where does the traffic go?' : '¿A dónde va el tráfico?'}
              </div>
              <div style={{ fontSize: '11px', color: NX.muted, marginBottom: '12px' }}>
                {lang === 'en' ? 'Pick the destination where users will land.' : 'Elige el destino donde aterrizarán los usuarios.'}
              </div>

              <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '8px', marginBottom: '12px' }}>
                {[
                  { id: 'web',       label: lang === 'en' ? 'Website' : 'Página web', icon: '🌐' },
                  { id: 'instagram', label: lang === 'en' ? 'Instagram profile' : 'Perfil Instagram', icon: '📸' },
                ].map(m => (
                  <div key={m.id} onClick={() => setDestMode(m.id)}
                    style={{ padding: '10px 12px', background: destMode === m.id ? NX.bg4 : NX.bg2, border: `1px solid ${destMode === m.id ? NX.accent : NX.border}`, borderRadius: '10px', cursor: 'pointer', textAlign: 'center', fontSize: '12px', fontWeight: destMode === m.id ? 600 : 400, color: destMode === m.id ? NX.accent : NX.muted, transition: 'all 0.15s' }}>
                    <span style={{ marginRight: '6px' }}>{m.icon}</span>{m.label}
                  </div>
                ))}
              </div>

              {destMode === 'web' && (
                <input
                  type="url" value={productUrl} onChange={e => setProductUrl(e.target.value)}
                  placeholder={lang === 'en' ? 'https://yoursite.com' : 'https://tusitio.com'}
                  data-flash-target="productUrl"
                  className={flashField === 'productUrl' ? 'nx-flash' : undefined}
                  style={{ width: '100%', background: NX.bg4, border: `1px solid ${productUrl ? NX.accent : NX.border}`, borderRadius: '8px', color: NX.text, fontSize: '12px', padding: '9px 12px', fontFamily: "'DM Sans',sans-serif", outline: 'none', boxSizing: 'border-box' }}/>
              )}
              {destMode === 'instagram' && (
                <>
                  <input
                    type="text" value={igProfile} onChange={e => setIgProfile(e.target.value)}
                    placeholder={lang === 'en' ? '@yourbrand or https://instagram.com/yourbrand' : '@tumarca o https://instagram.com/tumarca'}
                    data-flash-target="igProfile"
                    className={flashField === 'igProfile' ? 'nx-flash' : undefined}
                    style={{ width: '100%', background: NX.bg4, border: `1px solid ${igProfile ? NX.accent : NX.border}`, borderRadius: '8px', color: NX.text, fontSize: '12px', padding: '9px 12px', fontFamily: "'DM Sans',sans-serif", outline: 'none', boxSizing: 'border-box' }}/>
                  <div style={{ fontSize: '10px', color: NX.muted, marginTop: '6px', lineHeight: 1.4 }}>
                    {lang === 'en'
                      ? 'Tip: use the Instagram handle (@brand) or the full public URL. Meta redirects users to the profile app when they tap the ad.'
                      : 'Tip: usa el handle (@marca) o el URL público completo. Meta redirige al perfil en la app cuando el usuario toca el anuncio.'}
                  </div>
                </>
              )}
            </div>
          )}

          {/* INTERACCIÓN / RECONOCIMIENTO — page-based objectives with an IG-post-boost option.
              Two paths:
                1) Anuncio nuevo: generate creatives as usual (AI or uploaded).
                2) Publicación de Instagram: pick an existing IG post to boost.
              Both require an IG Business Account linked to the Page. */}
          {(objective === 'interaccion' || objective === 'reconocimiento') && (
            <div style={{ marginBottom: '20px', padding: '14px', background: NX.bg3, border: `1px solid ${NX.border}`, borderRadius: '12px' }}>
              <div style={{ fontSize: '13px', fontWeight: 500, color: NX.text, marginBottom: '4px' }}>
                {objective === 'interaccion'
                  ? (lang === 'en' ? 'What do you want to promote?' : '¿Qué querés promocionar?')
                  : (lang === 'en' ? 'How do you want to build awareness?' : '¿Cómo querés generar reconocimiento?')}
              </div>
              <div style={{ fontSize: '11px', color: NX.muted, marginBottom: '12px' }}>
                {metaAssets?.page_name
                  ? (lang === 'en'
                      ? <>Both options run on <b style={{ color: NX.text }}>{metaAssets.page_name}</b>.</>
                      : <>Ambas opciones corren sobre <b style={{ color: NX.text }}>{metaAssets.page_name}</b>.</>)
                  : (lang === 'en'
                      ? '⚠ Connect your Facebook Page first (Configurar activos).'
                      : '⚠ Conecta primero tu página de Facebook (Configurar activos).')}
              </div>

              <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '8px', marginBottom: '12px' }}>
                {[
                  { id: 'new', label: lang === 'en' ? 'New creative' : 'Anuncio nuevo', icon: '✨', desc: lang === 'en' ? 'Generate new image + text' : 'Genera imagen + texto nuevo' },
                  { id: 'existing_ig', label: lang === 'en' ? 'Existing IG post' : 'Publicación de Instagram', icon: '📸', desc: lang === 'en' ? 'Boost one of your IG posts' : 'Promociona un post tuyo' },
                ].map(m => (
                  <div key={m.id} onClick={() => { setPromotionMode(m.id); if (m.id === 'existing_ig') fetchIgPosts(); }}
                    style={{ padding: '12px 14px', background: promotionMode === m.id ? NX.bg4 : NX.bg2, border: `1.5px solid ${promotionMode === m.id ? NX.accent : NX.border}`, borderRadius: '12px', cursor: 'pointer', fontSize: '12px', transition: 'all 0.15s' }}>
                    <div style={{ fontSize: '18px', marginBottom: '4px' }}>{m.icon}</div>
                    <div style={{ fontWeight: promotionMode === m.id ? 600 : 500, color: promotionMode === m.id ? NX.accent : NX.text, marginBottom: '2px' }}>{m.label}</div>
                    <div style={{ fontSize: '10px', color: NX.muted, lineHeight: 1.35 }}>{m.desc}</div>
                  </div>
                ))}
              </div>

              {/* IG post gallery — shown only when user picked 'existing_ig' */}
              {promotionMode === 'existing_ig' && (
                <div>
                  {!metaAssets?.ig_id && (
                    <div style={{ fontSize: '11px', color: NX.warning, padding: '8px 10px', background: 'rgba(251,191,36,0.08)', border: '1px solid rgba(251,191,36,0.25)', borderRadius: '8px', marginBottom: '10px' }}>
                      {lang === 'en'
                        ? '⚠ No Instagram Business account linked. Configure your IG in "Configurar activos" first.'
                        : '⚠ No hay cuenta de Instagram Business vinculada. Configúrala primero en "Configurar activos".'}
                    </div>
                  )}
                  {igPostsError && (
                    <div style={{ fontSize: '11px', color: NX.danger, padding: '8px 10px', background: 'rgba(248,113,113,0.08)', border: '1px solid rgba(248,113,113,0.25)', borderRadius: '8px', marginBottom: '10px', display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: '8px' }}>
                      <span style={{ flex: 1 }}>{igPostsError}</span>
                      <button onClick={() => { setIgPostsError(null); setIgPosts([]); fetchIgPosts(); }}
                        style={{ padding: '3px 10px', background: 'transparent', border: `1px solid ${NX.danger}`, borderRadius: '6px', color: NX.danger, fontSize: '10px', cursor: 'pointer' }}>
                        {lang === 'en' ? 'Retry' : 'Reintentar'}
                      </button>
                    </div>
                  )}
                  {igPostsLoading && (
                    <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', padding: '24px', gap: '10px', color: NX.muted, fontSize: '12px' }}>
                      <span style={{ display: 'inline-block', width: '14px', height: '14px', border: '2px solid currentColor', borderTopColor: 'transparent', borderRadius: '50%', animation: 'spin 0.7s linear infinite' }}/>
                      {lang === 'en' ? 'Loading your IG posts…' : 'Cargando tus publicaciones…'}
                    </div>
                  )}
                  {!igPostsLoading && igPosts.length > 0 && (
                    <>
                      <div style={{ fontSize: '10px', color: NX.muted, fontFamily: "'DM Mono',monospace", letterSpacing: '0.1em', marginBottom: '8px', textTransform: 'uppercase' }}>
                        {lang === 'en' ? `${igPosts.length} posts — pick one` : `${igPosts.length} publicaciones — elige una`}
                      </div>
                      <div
                        data-flash-target="selectedIgPost"
                        className={flashField === 'selectedIgPost' ? 'nx-flash' : undefined}
                        style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(92px, 1fr))', gap: '8px', maxHeight: '320px', overflowY: 'auto', padding: '4px', borderRadius: '10px' }}>
                        {igPosts.map(post => {
                          const active = selectedIgPost?.id === post.id;
                          return (
                            <div key={post.id} onClick={() => setSelectedIgPost(post)}
                              title={post.caption ? post.caption.slice(0, 120) : ''}
                              style={{ position: 'relative', aspectRatio: '1 / 1', borderRadius: '10px', overflow: 'hidden', cursor: 'pointer', border: `2px solid ${active ? NX.accent : 'transparent'}`, boxShadow: active ? '0 0 20px rgba(139,92,246,0.35)' : 'none', transition: 'all 0.15s' }}>
                              <img src={post.image} alt="" style={{ width: '100%', height: '100%', objectFit: 'cover', background: NX.bg4 }}/>
                              {post.type === 'VIDEO' && (
                                <div style={{ position: 'absolute', top: '6px', right: '6px', width: '18px', height: '18px', borderRadius: '50%', background: 'rgba(0,0,0,0.6)', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
                                  <svg width="9" height="9" viewBox="0 0 12 12" fill="none"><path d="M3 2l7 4-7 4V2z" fill="#fff"/></svg>
                                </div>
                              )}
                              {post.type === 'CAROUSEL_ALBUM' && (
                                <div style={{ position: 'absolute', top: '6px', right: '6px', width: '18px', height: '18px', borderRadius: '4px', background: 'rgba(0,0,0,0.6)', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
                                  <svg width="11" height="11" viewBox="0 0 14 14" fill="none"><rect x="3" y="3" width="8" height="8" rx="1" stroke="#fff" strokeWidth="1.3"/><rect x="5" y="1" width="8" height="8" rx="1" stroke="#fff" strokeWidth="1.3"/></svg>
                                </div>
                              )}
                              {active && (
                                <div style={{ position: 'absolute', bottom: '4px', left: '4px', width: '22px', height: '22px', borderRadius: '50%', background: NX.accent, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
                                  <svg width="12" height="12" viewBox="0 0 14 14" fill="none"><path d="M3 7l3 3 5-7" stroke="#fff" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round"/></svg>
                                </div>
                              )}
                              <div style={{ position: 'absolute', bottom: 0, left: 0, right: 0, padding: '4px 6px', background: 'linear-gradient(to top, rgba(0,0,0,0.75), transparent)', color: '#fff', fontSize: '9px', fontWeight: 600, display: 'flex', gap: '8px' }}>
                                <span>♥ {post.likes}</span>
                                <span>💬 {post.comments}</span>
                              </div>
                            </div>
                          );
                        })}
                      </div>
                      {selectedIgPost && (
                        <div style={{ marginTop: '10px', fontSize: '11px', color: NX.success, display: 'flex', alignItems: 'center', gap: '6px' }}>
                          <svg width="12" height="12" viewBox="0 0 12 12" fill="none"><path d="M2 6l3 3 5-7" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round"/></svg>
                          {lang === 'en' ? 'Post selected' : 'Publicación seleccionada'} · {selectedIgPost.type === 'VIDEO' ? (lang === 'en' ? 'Video' : 'Video') : selectedIgPost.type === 'CAROUSEL_ALBUM' ? (lang === 'en' ? 'Carousel' : 'Carrusel') : (lang === 'en' ? 'Image' : 'Imagen')}
                        </div>
                      )}
                    </>
                  )}
                  {!igPostsLoading && !igPostsError && igPosts.length === 0 && metaAssets?.ig_id && (
                    <div style={{ fontSize: '11px', color: NX.muted, padding: '16px', textAlign: 'center' }}>
                      {lang === 'en' ? 'No recent posts found on your Instagram.' : 'No se encontraron publicaciones recientes en tu Instagram.'}
                    </div>
                  )}
                </div>
              )}
            </div>
          )}

          {/* APP PROMOTION — requires app store URLs + Meta App ID. Not yet fully supported. */}
          {objective === 'app_install' && (
            <div style={{ marginBottom: '20px', padding: '14px', background: 'rgba(251,191,36,0.06)', border: `1px solid rgba(251,191,36,0.3)`, borderRadius: '12px', display: 'flex', alignItems: 'flex-start', gap: '12px' }}>
              <svg width="22" height="22" viewBox="0 0 24 24" fill="none" style={{ flexShrink: 0, marginTop: '2px' }}>
                <rect x="5" y="2" width="14" height="20" rx="2" stroke={NX.warning} strokeWidth="1.6"/>
                <line x1="11" y1="18" x2="13" y2="18" stroke={NX.warning} strokeWidth="1.6" strokeLinecap="round"/>
              </svg>
              <div style={{ flex: 1 }}>
                <div style={{ fontSize: '13px', fontWeight: 500, color: NX.warning, marginBottom: '4px' }}>
                  {lang === 'en' ? 'App promotion — coming soon' : 'Promoción de apps — próximamente'}
                </div>
                <div style={{ fontSize: '11px', color: NX.muted, lineHeight: 1.5 }}>
                  {lang === 'en'
                    ? 'Requires an iOS/Android app linked to Meta plus a Meta App ID. If you need this, switch to Traffic → Website and send clicks to your app store listing.'
                    : 'Requiere una app iOS/Android vinculada a Meta y un Meta App ID. Si necesitas esto, elige Tráfico → Página web y manda clics a tu listing del app store.'}
                </div>
              </div>
            </div>
          )}

          <div style={{ display: 'flex', gap: '10px' }}>
            <NxButton variant="ghost" onClick={onClose} full>{ts.cancel}</NxButton>
            <NxButton variant="accent"
              onClick={() => {
                if (!objective) { triggerFieldFlash('objective'); return; }
                // Per-objective validation. Button stays live; on-click flashes the missing field.
                if (['ventas', 'conversiones', 'leads'].includes(objective)) {
                  if (destMode === 'web' && !productUrl.trim()) { triggerFieldFlash('productUrl'); return; }
                  if (destMode === 'messaging' && !Object.values(destChannels).some(Boolean)) { triggerFieldFlash('messaging'); return; }
                }
                if (objective === 'trafico') {
                  if (destMode === 'web' && !productUrl.trim()) { triggerFieldFlash('productUrl'); return; }
                  if (destMode === 'instagram' && !igProfile.trim()) { triggerFieldFlash('igProfile'); return; }
                }
                if (objective === 'interaccion' || objective === 'reconocimiento') {
                  if (!metaAssets?.page_id) {
                    alert(lang === 'en'
                      ? 'Connect your Facebook Page in "Configurar activos" first — this objective runs on the Page.'
                      : 'Conecta primero tu página de Facebook en "Configurar activos" — este objetivo corre sobre la Página.');
                    return;
                  }
                  if (promotionMode === 'existing_ig') {
                    if (!metaAssets?.ig_id) {
                      alert(lang === 'en'
                        ? 'Link your Instagram Business account in "Configurar activos" to boost existing posts.'
                        : 'Vincula tu cuenta de Instagram Business en "Configurar activos" para promocionar publicaciones existentes.');
                      return;
                    }
                    if (!selectedIgPost) { triggerFieldFlash('selectedIgPost'); return; }
                  }
                }
                if (objective === 'app_install') {
                  alert(lang === 'en'
                    ? 'App promotion is not yet available. Use Traffic → Website to drive users to your app store listing.'
                    : 'La promoción de apps aún no está disponible. Usa Tráfico → Página web para llevar usuarios a tu listing del app store.');
                  return;
                }
                setStep(1);
              }}
              full>{ts.continueBtn}</NxButton>
          </div>
        </>}

        {step === 1 && <>
          <h2 style={{ fontSize: '17px', fontWeight: 500, color: NX.text, margin: '0 0 2px' }}>{ts.steps[1].title}</h2>
          <p style={{ color: NX.muted, fontSize: '12px', margin: '0 0 12px' }}>{ts.steps[1].subtitle}</p>

          {/* Tabs IA/Yo — solo en modo Advanced. Si viene del Assistant, el imageSource ya
              fue elegido en el wizard, no tiene sentido re-exponerlo acá. */}
          {!assistantPrefill && (
          <div style={{ display: 'flex', gap: '20px', marginBottom: '14px', paddingBottom: '10px', borderBottom: `1px solid ${NX.border}` }}>
            {[
              { id: 'ai',       label: lang === 'en' ? 'AI creates them' : 'IA las crea',
                iconSvg: (color) => <svg width="16" height="16" viewBox="0 0 24 24" fill="none"><path d="M12 2l2.09 4.26L18.5 7l-3.5 3.41L15.91 15 12 12.77 8.09 15 9 10.41 5.5 7l4.41-.74L12 2z" fill={color}/></svg>,
                color: NX.accent2 },
              { id: 'uploaded', label: lang === 'en' ? "I'll upload mine" : 'Yo las subo',
                iconSvg: (color) => <svg width="16" height="16" viewBox="0 0 24 24" fill="none"><path d="M3 7a2 2 0 012-2h4l2 2h8a2 2 0 012 2v9a2 2 0 01-2 2H5a2 2 0 01-2-2V7z" stroke={color} strokeWidth="1.5"/><path d="M12 11v5M10 13l2-2 2 2" stroke={color} strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/></svg>,
                color: '#f59e0b' },
            ].map(s => {
              const active = imageSource === s.id;
              return (
                <button key={s.id} type="button" onClick={() => setImageSource(s.id)}
                  style={{
                    background: 'transparent', border: 'none', padding: '4px 0',
                    display: 'flex', alignItems: 'center', gap: '8px', cursor: 'pointer',
                    color: active ? s.color : NX.muted,
                    fontSize: '13px', fontWeight: active ? 600 : 500,
                    fontFamily: "'DM Sans',sans-serif",
                    borderBottom: `2px solid ${active ? s.color : 'transparent'}`,
                    transition: 'all 0.15s',
                  }}>
                  {s.iconSvg(active ? s.color : NX.muted)}
                  {s.label}
                </button>
              );
            })}
          </div>
          )}

          {/* Phase 3 layout: 2-column on desktop — form on the left, aurora preview on the right.
              Flex-wrap collapses to single column on narrow viewports (<~900px). The
              `nx-two-col` class forces single-column below 1024px (matches small laptops). */}
          <div className="nx-two-col" style={{ display: 'flex', flexWrap: 'wrap', gap: '24px', alignItems: 'flex-start' }}>
            <div style={{ flex: '1 1 540px', minWidth: 0 }}>

          {/* Image count + aspect ratio combined row — inline, minimal.
              Si viene del Assistant, estos parámetros ya fueron elegidos en el wizard:
              ocultamos Count+Ratio pero dejamos las referencias visibles (el bloque más abajo). */}
          {imageSource === 'ai' && (<>
          {!assistantPrefill && (
          <div style={{ display: 'flex', alignItems: 'center', gap: '18px', marginBottom: '12px', flexWrap: 'wrap' }}>
            {/* Count cards — Apple-style tiles with big count multiplier + badge */}
            <div style={{ display: 'flex', alignItems: 'center', gap: '10px', flexWrap: 'wrap' }}>
              <span style={{ fontSize: '11px', color: NX.muted, fontWeight: 500, marginRight: '4px' }}>{lang === 'en' ? 'Count' : 'Cantidad'}</span>
              {[
                { n: 1,  badge: lang === 'en' ? 'TEST' : 'PRUEBA', badgeStyle: 'test' },
                { n: 3,  badge: null },
                { n: 5,  badge: lang === 'en' ? '★' : '★', badgeStyle: 'recommended' },
                { n: 10, badge: 'PRO', badgeStyle: 'pro' },
              ].map(opt => {
                const active = imageCount === opt.n;
                const isTest = opt.badgeStyle === 'test';
                const isPro = opt.badgeStyle === 'pro';
                const isRec = opt.badgeStyle === 'recommended';
                return (
                  <button key={opt.n} type="button" onClick={() => setImageCount(opt.n)}
                    title={isTest ? (lang === 'en' ? 'Quick test — 1 image only, minimal cost' : 'Prueba rápida — 1 sola imagen, costo mínimo') : undefined}
                    style={{
                      position: 'relative',
                      minWidth: '96px',
                      padding: '12px 16px',
                      background: active
                        ? (isTest
                            ? 'linear-gradient(135deg, rgba(251,191,36,0.2), rgba(245,158,11,0.18))'
                            : isPro
                              ? 'linear-gradient(135deg, rgba(139,92,246,0.25), rgba(167,139,250,0.2))'
                              : 'linear-gradient(135deg, rgba(139,92,246,0.22), rgba(91,142,240,0.18))')
                        : 'rgba(26,17,40,0.55)',
                      border: `1.5px solid ${active ? (isTest ? NX.warning : NX.accent) : NX.border}`,
                      borderRadius: '14px',
                      cursor: 'pointer',
                      fontFamily: "'DM Sans',sans-serif",
                      transition: 'all 0.2s cubic-bezier(.4,0,.2,1)',
                      display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '3px',
                      boxShadow: active
                        ? (isTest ? '0 6px 22px rgba(251,191,36,0.25), inset 0 1px 0 rgba(255,255,255,0.05)' : '0 6px 22px rgba(139,92,246,0.3), inset 0 1px 0 rgba(255,255,255,0.05)')
                        : 'inset 0 1px 0 rgba(255,255,255,0.03)',
                      backdropFilter: 'blur(10px)',
                    }}
                    onMouseEnter={e => { if (!active) { e.currentTarget.style.borderColor = NX.border2; e.currentTarget.style.transform = 'translateY(-1px)'; } }}
                    onMouseLeave={e => { if (!active) { e.currentTarget.style.borderColor = NX.border; e.currentTarget.style.transform = 'translateY(0)'; } }}>
                    <div style={{
                      fontSize: '22px', fontWeight: 700, lineHeight: 1,
                      color: active ? (isTest ? NX.warning : NX.accent) : NX.text,
                      letterSpacing: '-0.03em',
                      fontFeatureSettings: '"tnum"',
                    }}>
                      <span style={{ fontSize: '13px', opacity: 0.55, fontWeight: 500, marginRight: '1px' }}>x</span>{opt.n}
                    </div>
                    {opt.badge ? (
                      <span style={{
                        fontSize: isRec ? '10px' : '9px', fontWeight: 800, letterSpacing: '0.08em', lineHeight: 1,
                        padding: '3px 7px', borderRadius: '6px',
                        background: isTest
                          ? 'linear-gradient(90deg,#fbbf24,#f59e0b)'
                          : isRec
                            ? 'linear-gradient(90deg,#22d3ee,#a78bfa)'
                            : 'linear-gradient(90deg,#8b5cf6,#a78bfa)',
                        color: '#fff',
                        textShadow: '0 1px 1px rgba(0,0,0,0.15)',
                      }}>
                        {opt.badge}
                      </span>
                    ) : (
                      <span style={{ fontSize: '10px', color: NX.muted, fontWeight: 500, letterSpacing: '0.04em', lineHeight: 1 }}>
                        {lang === 'en' ? 'imgs' : 'imgs'}
                      </span>
                    )}
                  </button>
                );
              })}
            </div>
            {/* Aspect ratio tiles — minimalist, just the shape + label.
                Both engines (Gemini Premium + gpt-image-2 Ultra HD) natively support the full
                Meta-friendly set as of gpt-image-2: 1:1, 4:5 (Feed), 9:16 (Stories/Reels),
                3:4, 4:3, 16:9. The Ultra HD backend maps each to an exact multiples-of-16
                pixel size — no cropping. */}
            <div style={{ display: 'flex', alignItems: 'center', gap: '10px', flex: 1, minWidth: '0' }}>
              <span style={{ fontSize: '11px', color: NX.muted, fontWeight: 500 }}>{lang === 'en' ? 'Format' : 'Formato'}</span>
              <div style={{ display: 'flex', gap: '10px' }}>
                {[
                  { id: '4:5',  w: 14, h: 17, label: '4:5' },
                  { id: '1:1',  w: 16, h: 16, label: '1:1' },
                  { id: '9:16', w: 11, h: 19, label: '9:16' },
                  { id: '3:4',  w: 14, h: 18, label: '3:4' },
                  { id: '4:3',  w: 18, h: 14, label: '4:3' },
                  { id: '16:9', w: 20, h: 12, label: '16:9' },
                ].map(r => {
                  const active = aspectRatio === r.id;
                  return (
                    <button key={r.id} type="button" onClick={() => setAspectRatio(r.id)} title={r.label}
                      style={{
                        background: 'transparent', border: 'none', padding: '4px',
                        display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '3px',
                        cursor: 'pointer', fontFamily: "'DM Sans',sans-serif",
                      }}>
                      <div style={{
                        width: `${r.w}px`, height: `${r.h}px`,
                        border: `1.5px solid ${active ? NX.accent : NX.muted}`,
                        background: active ? NX.accent : 'transparent',
                        borderRadius: '2px', transition: 'all 0.15s',
                      }}/>
                      <span style={{ fontSize: '9px', color: active ? NX.accent : NX.muted, fontWeight: active ? 600 : 400, lineHeight: 1 }}>{r.label}</span>
                    </button>
                  );
                })}
              </div>
            </div>
          </div>
          )}

          {/* Image upload — modern, comfortable drop zone */}
          <input ref={fileInputRef} type="file" accept="image/jpeg,image/png,image/webp" multiple onChange={handleFileSelect} style={{ display: 'none' }}/>
          {productPreviewUrl ? (
            // FILLED state: sleek horizontal card with thumbnail, meta, and actions
            <div style={{
              display: 'flex', alignItems: 'center', gap: '12px',
              background: 'linear-gradient(180deg, rgba(139,92,246,0.06), rgba(139,92,246,0.04))',
              border: `1px solid ${NX.accent}`,
              borderRadius: '14px', padding: '10px 12px', marginBottom: '10px',
              boxShadow: '0 0 0 4px rgba(139,92,246,0.06)',
              transition: 'all 0.2s',
            }}>
              <div style={{ position: 'relative', flexShrink: 0 }}>
                <img src={productPreviewUrl} alt="preview" style={{ width: '52px', height: '52px', objectFit: 'cover', borderRadius: '10px', display: 'block' }}/>
                <div style={{ position: 'absolute', bottom: '-4px', right: '-4px', width: '16px', height: '16px', borderRadius: '50%', background: NX.success, border: `2px solid ${NX.bg2}`, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
                  <svg width="8" height="8" viewBox="0 0 24 24" fill="none"><path d="M5 12l4 4 10-10" stroke="#0b0b0b" strokeWidth="3" strokeLinecap="round" strokeLinejoin="round"/></svg>
                </div>
              </div>
              <div style={{ flex: 1, minWidth: 0 }}>
                <div style={{ fontSize: '13px', color: NX.text, fontWeight: 500, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{productFile?.name}</div>
                <div style={{ fontSize: '11px', color: NX.muted, marginTop: '2px', display: 'flex', alignItems: 'center', gap: '6px' }}>
                  <span style={{ color: NX.accent, fontWeight: 500 }}>{lang === 'en' ? 'Primary reference' : 'Referencia principal'}</span>
                  <span style={{ opacity: 0.4 }}>•</span>
                  <span>{productFile?.size ? `${(productFile.size / 1024).toFixed(0)} KB` : ''}</span>
                </div>
              </div>
              {extraProductPreviews.length < MAX_EXTRA_IMAGES && (
                <button type="button" onClick={() => fileInputRef.current?.click()}
                  title={lang === 'en' ? 'Add a 2nd reference image (max 2)' : 'Agregar 2ª imagen de referencia (máx 2)'}
                  style={{ padding: '7px 12px', background: 'transparent', border: `1px solid ${NX.border2}`, borderRadius: '8px', color: NX.text, fontSize: '11px', fontWeight: 500, cursor: 'pointer', fontFamily: "'DM Sans',sans-serif", whiteSpace: 'nowrap', display: 'flex', alignItems: 'center', gap: '5px', transition: 'all 0.15s' }}
                  onMouseEnter={e => { e.currentTarget.style.background = NX.bg4; e.currentTarget.style.borderColor = NX.accent; }}
                  onMouseLeave={e => { e.currentTarget.style.background = 'transparent'; e.currentTarget.style.borderColor = NX.border2; }}>
                  <svg width="11" height="11" viewBox="0 0 24 24" fill="none"><path d="M12 5v14M5 12h14" stroke="currentColor" strokeWidth="2" strokeLinecap="round"/></svg>
                  {lang === 'en' ? '+ 2nd image' : '+ 2ª imagen'}
                </button>
              )}
              <button type="button" onClick={e => { e.stopPropagation(); setProductFile(null); setProductPreviewUrl(null); setProductCloudinaryUrl(null); setExtraProductFiles([]); setExtraProductPreviews([]); setProductCloudinaryUrl2(null); productBase64Ref2.current = null; }}
                title={lang === 'en' ? 'Remove' : 'Quitar'}
                style={{ width: '30px', height: '30px', background: 'transparent', border: 'none', borderRadius: '8px', color: NX.muted, cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center', transition: 'all 0.15s' }}
                onMouseEnter={e => { e.currentTarget.style.background = 'rgba(248,113,113,0.1)'; e.currentTarget.style.color = NX.danger; }}
                onMouseLeave={e => { e.currentTarget.style.background = 'transparent'; e.currentTarget.style.color = NX.muted; }}>
                <svg width="14" height="14" viewBox="0 0 24 24" fill="none"><path d="M3 6h18M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2M19 6l-1 14a2 2 0 01-2 2H8a2 2 0 01-2-2L5 6" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round"/></svg>
              </button>
            </div>
          ) : (
            // EMPTY state: generous drop zone with illustrated icon, hover glow, drag support
            <div
              onClick={() => fileInputRef.current?.click()}
              onDragEnter={e => { e.preventDefault(); e.stopPropagation(); setDragActive(true); }}
              onDragOver={e => { e.preventDefault(); e.stopPropagation(); setDragActive(true); }}
              onDragLeave={e => { e.preventDefault(); e.stopPropagation(); setDragActive(false); }}
              onDrop={handleFileDrop}
              style={{
                position: 'relative',
                border: `1.5px dashed ${dragActive ? NX.accent : NX.border2}`,
                background: dragActive
                  ? 'linear-gradient(180deg, rgba(139,92,246,0.10), rgba(139,92,246,0.06))'
                  : 'linear-gradient(180deg, rgba(255,255,255,0.015), rgba(255,255,255,0.005))',
                borderRadius: '14px',
                padding: '18px 16px',
                marginBottom: '10px',
                cursor: 'pointer',
                transition: 'all 0.2s',
                display: 'flex', alignItems: 'center', gap: '14px',
                boxShadow: dragActive ? '0 0 0 6px rgba(139,92,246,0.08)' : 'none',
              }}
              onMouseEnter={e => { if (!dragActive) { e.currentTarget.style.borderColor = 'rgba(139,92,246,0.5)'; e.currentTarget.style.background = 'linear-gradient(180deg, rgba(139,92,246,0.04), rgba(139,92,246,0.02))'; } }}
              onMouseLeave={e => { if (!dragActive) { e.currentTarget.style.borderColor = NX.border2; e.currentTarget.style.background = 'linear-gradient(180deg, rgba(255,255,255,0.015), rgba(255,255,255,0.005))'; } }}
            >
              {/* Illustrated icon in gradient circle */}
              <div style={{
                width: '46px', height: '46px', borderRadius: '12px',
                background: dragActive
                  ? 'linear-gradient(135deg, #8b5cf6, #a78bfa)'
                  : 'linear-gradient(135deg, rgba(139,92,246,0.15), rgba(139,92,246,0.12))',
                display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0,
                transition: 'all 0.2s',
                boxShadow: dragActive ? '0 4px 20px rgba(139,92,246,0.35)' : 'none',
              }}>
                <svg width="22" height="22" viewBox="0 0 24 24" fill="none">
                  <path d="M12 16V4m0 0l-4 4m4-4l4 4" stroke={dragActive ? '#fff' : NX.accent} strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"/>
                  <path d="M4 16v2a2 2 0 002 2h12a2 2 0 002-2v-2" stroke={dragActive ? '#fff' : NX.accent} strokeWidth="1.8" strokeLinecap="round"/>
                </svg>
              </div>
              {/* Text block */}
              <div style={{ flex: 1, minWidth: 0 }}>
                <div style={{ fontSize: '13px', color: NX.text, fontWeight: 500, marginBottom: '2px' }}>
                  {dragActive
                    ? (lang === 'en' ? 'Drop your images here' : 'Suelta tus imágenes aquí')
                    : (lang === 'en' ? 'Drag & drop or click to upload' : 'Arrastra y suelta o haz click para subir')}
                </div>
                <div style={{ fontSize: '11px', color: NX.muted, lineHeight: 1.4 }}>
                  {lang === 'en'
                    ? 'Product photo — JPG, PNG, WEBP up to 10MB (optional)'
                    : 'Foto del producto — JPG, PNG, WEBP hasta 10MB (opcional)'}
                </div>
              </div>
              {/* Primary CTA chip */}
              <div style={{
                padding: '7px 14px',
                background: 'linear-gradient(135deg, #8b5cf6, #a78bfa)',
                borderRadius: '8px',
                color: '#fff',
                fontSize: '11px',
                fontWeight: 600,
                flexShrink: 0,
                display: 'flex', alignItems: 'center', gap: '5px',
                fontFamily: "'DM Sans',sans-serif",
              }}>
                <svg width="10" height="10" viewBox="0 0 24 24" fill="none"><path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4M7 10l5-5 5 5M12 5v12" stroke="#fff" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/></svg>
                {lang === 'en' ? 'Select' : 'Elegir'}
              </div>
            </div>
          )}

          {/* Elegant "or" divider — two alternative paths */}
          {!productPreviewUrl && (
            <div style={{ display: 'flex', alignItems: 'center', gap: '10px', margin: '4px 0 10px' }}>
              <div style={{ flex: 1, height: '1px', background: NX.border }}/>
              <span style={{ fontSize: '10px', color: NX.muted, fontWeight: 500, letterSpacing: '0.1em', textTransform: 'uppercase' }}>
                {lang === 'en' ? 'or' : 'o'}
              </span>
              <div style={{ flex: 1, height: '1px', background: NX.border }}/>
            </div>
          )}

          {/* Imagen de referencia adicional (máx 1 extra → 2 total). Opus, Gemini y
              gpt-image-2 reciben ambas imágenes para entender el producto desde 2 ángulos. */}
          {extraProductPreviews.length > 0 && (
            <div style={{ marginBottom: '12px' }}>
              <div style={{ fontSize: '11px', color: NX.muted, fontWeight: 500, marginBottom: '6px' }}>
                {lang === 'en'
                  ? `2nd reference image (${extraProductPreviews.length}/${MAX_EXTRA_IMAGES}) — Opus + Gemini + gpt-image-2 will see it`
                  : `2ª imagen de referencia (${extraProductPreviews.length}/${MAX_EXTRA_IMAGES}) — Opus + Gemini + gpt-image-2 la reciben`}
              </div>
              <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill,minmax(68px,1fr))', gap: '6px' }}>
                {extraProductPreviews.map((pv, i) => (
                  <div key={i} style={{ position: 'relative', paddingBottom: '100%', background: NX.bg4, borderRadius: '8px', overflow: 'hidden' }}>
                    <img src={pv} alt="" style={{ position: 'absolute', inset: 0, width: '100%', height: '100%', objectFit: 'cover' }}/>
                    <button onClick={() => promotePrimary(i)}
                      title={lang === 'en' ? 'Make this the primary reference' : 'Hacer esta la referencia principal'}
                      style={{ position: 'absolute', bottom: '3px', left: '3px', width: '18px', height: '18px', borderRadius: '50%', background: 'rgba(0,0,0,0.7)', border: 'none', color: '#fff', cursor: 'pointer', fontSize: '11px', display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 0 }}>
                      ★
                    </button>
                    <button onClick={() => removeExtraImage(i)}
                      title={lang === 'en' ? 'Remove' : 'Eliminar'}
                      style={{ position: 'absolute', top: '3px', right: '3px', width: '18px', height: '18px', borderRadius: '50%', background: 'rgba(0,0,0,0.7)', border: 'none', color: '#fff', cursor: 'pointer', fontSize: '12px', display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 0, lineHeight: 1 }}>×</button>
                  </div>
                ))}
              </div>
            </div>
          )}

          {/* URL + import — solo Advanced. En modo Assistant ya se importó arriba en el wizard. */}
          {!assistantPrefill && (<>
          <div style={{ display: 'flex', gap: '6px', marginBottom: '8px' }}>
            <div style={{ flex: 1, display: 'flex', alignItems: 'center', background: NX.bg3, border: `1px solid ${productUrl ? NX.accent : NX.border}`, borderRadius: '11px', padding: '0 12px', boxSizing: 'border-box', transition: 'border-color 0.15s' }}>
              <svg width="13" height="13" viewBox="0 0 24 24" fill="none" style={{ flexShrink: 0, opacity: 0.7 }}><path d="M10 14a5 5 0 007 0l3-3a5 5 0 00-7-7l-1 1M14 10a5 5 0 00-7 0l-3 3a5 5 0 007 7l1-1" stroke={productUrl ? NX.accent : NX.muted} strokeWidth="1.7" strokeLinecap="round"/></svg>
              <input
                type="url"
                value={productUrl}
                onChange={e => { setProductUrl(e.target.value); setUrlError(null); }}
                onKeyDown={e => e.key === 'Enter' && fetchFromUrl()}
                placeholder={lang === 'en' ? 'Paste product URL — AI imports photo & description' : 'Pega el URL del producto — la IA importa foto y descripción'}
                style={{ flex: 1, background: 'transparent', border: 'none', color: NX.text, fontSize: '12px', padding: '11px 10px', fontFamily: "'DM Sans',sans-serif", outline: 'none' }}
              />
            </div>
            <button
              onClick={fetchFromUrl}
              disabled={!productUrl.trim() || urlLoading}
              style={{ padding: '0 16px', background: productUrl.trim() && !urlLoading ? 'linear-gradient(135deg, #8b5cf6, #a78bfa)' : NX.bg3, border: `1px solid ${productUrl.trim() ? 'transparent' : NX.border}`, borderRadius: '11px', color: productUrl.trim() ? '#fff' : NX.muted, fontSize: '12px', fontWeight: 600, cursor: productUrl.trim() && !urlLoading ? 'pointer' : 'default', whiteSpace: 'nowrap', transition: 'all 0.2s', fontFamily: "'DM Sans',sans-serif", display: 'flex', alignItems: 'center', gap: '6px', boxShadow: productUrl.trim() && !urlLoading ? '0 4px 14px rgba(139,92,246,0.25)' : 'none' }}>
              {urlLoading
                ? <svg width="12" height="12" viewBox="0 0 24 24" fill="none" style={{ animation: 'spin 0.8s linear infinite' }}><circle cx="12" cy="12" r="9" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeDasharray="14 40"/></svg>
                : <svg width="12" height="12" viewBox="0 0 24 24" fill="none"><path d="M12 2l2.09 4.26L18.5 7l-3.5 3.41L15.91 15 12 12.77 8.09 15 9 10.41 5.5 7l4.41-.74L12 2z" fill="currentColor"/></svg>}
              {urlLoading ? (lang === 'en' ? 'Importing…' : 'Importando…') : (lang === 'en' ? 'Import' : 'Importar')}
            </button>
          </div>
          {urlError && <div style={{ fontSize: '10px', color: NX.danger, marginBottom: '6px' }}>{urlError}</div>}
          </>)}

          {/* Product description — required, compact */}
          <textarea value={productDesc} onChange={e => setProductDesc(e.target.value)}
            placeholder={ts.productDesc}
            data-flash-target="productDesc"
            className={flashField === 'productDesc' ? 'nx-flash' : undefined}
            style={{ width: '100%', height: '56px', background: NX.bg3, border: `1px solid ${productDesc.trim().length > 0 ? NX.accent : NX.border}`, borderRadius: '10px', color: NX.text, fontSize: '12px', padding: '9px 12px', fontFamily: "'DM Sans',sans-serif", resize: 'none', outline: 'none', boxSizing: 'border-box', marginBottom: '8px' }}/>

          {/* Country + Language — compact inline row with icons inside inputs.
              Country field has a floating tooltip-style typo suggestion that appears anchored
              directly below the input with a small pointer arrow, like a cloud pointing to the word. */}
          <div style={{ display: 'flex', gap: '8px', marginBottom: '10px', position: 'relative' }}>
            <div style={{ flex: 1, position: 'relative' }}>
              <div
                data-flash-target="country"
                className={flashField === 'country' ? 'nx-flash' : undefined}
                style={{ display: 'flex', alignItems: 'center', background: NX.bg3, border: `1px solid ${country.trim() ? NX.accent : NX.border}`, borderRadius: '10px', padding: '0 10px', boxSizing: 'border-box' }}>
                <svg width="12" height="12" viewBox="0 0 24 24" fill="none" style={{ flexShrink: 0, opacity: 0.6 }}><circle cx="12" cy="12" r="9" stroke={NX.muted} strokeWidth="1.5"/><path d="M3 12h18M12 3a14 14 0 010 18M12 3a14 14 0 000 18" stroke={NX.muted} strokeWidth="1.5"/></svg>
                <input
                  type="text"
                  value={country}
                  onChange={e => setCountry(e.target.value)}
                  placeholder={ts.countryPlaceholder || 'Ej: Chile, Madrid, Miami…'}
                  style={{ flex: 1, background: 'transparent', border: 'none', color: NX.text, fontSize: '12px', padding: '9px 8px', fontFamily: "'DM Sans',sans-serif", outline: 'none' }}
                />
              </div>
              {/* Cloud-style typo suggestion, floats directly below the country input with an arrow pointer. */}
              {(() => {
                const suggestion = suggestCountryCorrection(country);
                if (!suggestion) return null;
                return (
                  <div style={{
                    position: 'relative',
                    marginTop: '8px',
                    display: 'inline-flex', alignItems: 'center', gap: '8px',
                    padding: '6px 8px 6px 12px',
                    background: 'linear-gradient(180deg, rgba(251,191,36,0.22), rgba(251,191,36,0.12))',
                    border: '1px solid rgba(251,191,36,0.45)',
                    borderRadius: '999px',
                    fontSize: '11px', color: NX.text,
                    boxShadow: '0 6px 20px rgba(251,191,36,0.18)',
                    backdropFilter: 'blur(8px)',
                    maxWidth: '100%',
                  }}>
                    {/* upward arrow pointing to the input */}
                    <div style={{ position: 'absolute', top: '-5px', left: '18px', width: '10px', height: '10px', background: 'rgba(251,191,36,0.16)', borderTop: '1px solid rgba(251,191,36,0.4)', borderLeft: '1px solid rgba(251,191,36,0.4)', transform: 'rotate(45deg)' }}/>
                    <svg width="12" height="12" viewBox="0 0 24 24" fill="none" style={{ flexShrink: 0 }}><path d="M12 9v4M12 17h.01" stroke={NX.warning} strokeWidth="2" strokeLinecap="round"/><circle cx="12" cy="12" r="9" stroke={NX.warning} strokeWidth="1.5"/></svg>
                    <span>{lang === 'en' ? 'Did you mean' : '¿Quisiste decir'} <b style={{ color: NX.warning }}>{suggestion}</b>?</span>
                    <button type="button" onClick={() => setCountry(suggestion)}
                      style={{ padding: '4px 12px', background: NX.warning, border: 'none', borderRadius: '999px', color: '#0b0b0b', fontSize: '11px', fontWeight: 700, cursor: 'pointer', fontFamily: "'DM Sans',sans-serif" }}>
                      {lang === 'en' ? 'Fix' : 'Corregir'}
                    </button>
                  </div>
                );
              })()}
              {/* City-only input → ask the user for the country so backend resolves to a single
                  city instead of guessing (and lets us launch ONLY in that city, not the whole country). */}
              {(() => {
                const c = country.trim();
                if (c.length < 3) return null;
                if (isCountryExactMatch(c)) return null;
                if (suggestCountryCorrection(c)) return null;
                if (c.includes(',')) return null;
                return (
                  <div style={{ marginTop: '8px', padding: '10px 12px', borderRadius: '10px', background: 'rgba(139,92,246,0.08)', border: '1px solid rgba(139,92,246,0.28)', display: 'flex', flexDirection: 'column', gap: '6px' }}>
                    <div style={{ fontSize: '11px', color: NX.text, lineHeight: 1.4 }}>
                      {lang === 'en' ? 'Looks like a city. Which country is' : 'Parece una ciudad. ¿En qué país está'} <b style={{ color: NX.accent }}>{c}</b>?
                    </div>
                    <select value="" onChange={e => { if (e.target.value) setCountry(`${c}, ${e.target.value}`); }}
                      style={{ padding: '8px 10px', borderRadius: '8px', background: 'rgba(26,17,40,0.7)', border: `1px solid ${NX.border2}`, color: NX.text, fontSize: '12px', fontFamily: "'DM Sans',sans-serif", outline: 'none', cursor: 'pointer' }}>
                      <option value="">{lang === 'en' ? 'Pick the country…' : 'Elegí el país…'}</option>
                      {KNOWN_COUNTRIES.map(opt => <option key={opt} value={opt}>{opt}</option>)}
                    </select>
                    <div style={{ fontSize: '9px', color: NX.muted, fontFamily: "'DM Mono',monospace", lineHeight: 1.4 }}>
                      {lang === 'en' ? '🎯 The campaign will run ONLY in that city.' : '🎯 La campaña va a salir SOLO en esa ciudad.'}
                    </div>
                  </div>
                );
              })()}
            </div>
            <div style={{ display: 'flex', gap: '4px', background: NX.bg3, border: `1px solid ${NX.border}`, borderRadius: '10px', padding: '3px' }}>
              {[{ id: 'es', label: 'ES' }, { id: 'en', label: 'EN' }].map(l => (
                <button key={l.id} type="button" onClick={() => setLanguage(l.id)}
                  style={{ padding: '4px 12px', background: language === l.id ? NX.accent : 'transparent', border: 'none', borderRadius: '7px', cursor: 'pointer', fontSize: '11px', fontWeight: 600, color: language === l.id ? '#fff' : NX.muted, transition: 'all 0.15s', fontFamily: "'DM Sans',sans-serif" }}>
                  {l.label}
                </button>
              ))}
            </div>
          </div>

          </>)}

          {/* USER-UPLOADED IMAGES flow */}
          {imageSource === 'uploaded' && (<>
            <input ref={userImageInputRef} type="file" accept="image/jpeg,image/png,image/webp" multiple
              onChange={e => {
                const files = Array.from(e.target.files || []).slice(0, 10 - userUploadedImages.length);
                const mapped = files.map(f => ({ file: f, previewUrl: URL.createObjectURL(f), cloudinaryUrl: null }));
                setUserUploadedImages(prev => [...prev, ...mapped]);
                e.target.value = '';
              }}
              style={{ display: 'none' }}/>

            <div onClick={() => userImageInputRef.current?.click()}
              style={{ border: `2px dashed ${userUploadedImages.length > 0 ? NX.accent : NX.border}`, borderRadius: '12px', padding: '20px', textAlign: 'center', marginBottom: '12px', cursor: 'pointer' }}>
              <svg width="32" height="32" viewBox="0 0 40 40" fill="none" style={{ margin: '0 auto 8px', display: 'block' }}><rect x="4" y="8" width="32" height="24" rx="4" stroke={NX.muted} strokeWidth="1.5"/><circle cx="14" cy="17" r="3" stroke={NX.muted} strokeWidth="1.5"/><path d="M4 28l10-8 6 5 5-4 11 7" stroke={NX.muted} strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/></svg>
              <div style={{ fontSize: '13px', color: NX.muted }}>
                {userUploadedImages.length === 0
                  ? (lang === 'en' ? 'Click to upload 1–10 images' : 'Click para subir de 1 a 10 imágenes')
                  : (lang === 'en' ? `Click to add more (${userUploadedImages.length}/10)` : `Click para agregar más (${userUploadedImages.length}/10)`)}
              </div>
              <div style={{ fontSize: '11px', color: NX.muted, marginTop: '3px' }}>{ts.uploadFormats}</div>
            </div>

            {userUploadedImages.length > 0 && (
              <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill,minmax(90px,1fr))', gap: '8px', marginBottom: '12px' }}>
                {userUploadedImages.map((img, i) => (
                  <div key={i} style={{ position: 'relative', paddingBottom: '100%', background: NX.bg4, borderRadius: '8px', overflow: 'hidden' }}>
                    <img src={img.previewUrl} alt="" style={{ position: 'absolute', inset: 0, width: '100%', height: '100%', objectFit: 'cover' }}/>
                    <button onClick={() => setUserUploadedImages(prev => prev.filter((_, idx) => idx !== i))}
                      style={{ position: 'absolute', top: '4px', right: '4px', width: '20px', height: '20px', borderRadius: '50%', background: 'rgba(0,0,0,0.7)', border: 'none', color: '#fff', cursor: 'pointer', fontSize: '12px', display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 0 }}>×</button>
                  </div>
                ))}
              </div>
            )}

            {/* Optional URL — AI reads the page and pre-fills the product description */}
            <div style={{ marginBottom: '10px' }}>
              <div style={{ fontSize: '11px', color: NX.muted, fontWeight: 500, marginBottom: '6px' }}>
                🔗 {lang === 'en' ? 'Product or service URL' : 'URL del producto o servicio'}
                <span style={{ color: NX.muted, fontWeight: 400 }}> ({lang === 'en' ? 'optional — imports the description automatically' : 'opcional — importa la descripción automáticamente'})</span>
              </div>
              <div style={{ display: 'flex', gap: '8px' }}>
                <input
                  type="url"
                  value={productUrl}
                  onChange={e => { setProductUrl(e.target.value); setUrlError(null); }}
                  onKeyDown={e => e.key === 'Enter' && fetchFromUrl()}
                  placeholder={lang === 'en' ? 'https://yoursite.com/product' : 'https://tutienda.com/producto'}
                  style={{ flex: 1, background: NX.bg3, border: `1px solid ${productUrl ? NX.accent : NX.border}`, borderRadius: '10px', color: NX.text, fontSize: '12px', padding: '10px 12px', fontFamily: "'DM Sans',sans-serif", outline: 'none', boxSizing: 'border-box' }}
                />
                <button
                  onClick={fetchFromUrl}
                  disabled={!productUrl.trim() || urlLoading}
                  style={{ padding: '0 14px', background: productUrl.trim() && !urlLoading ? 'linear-gradient(135deg, #8b5cf6, #a78bfa)' : NX.bg3, border: `1px solid ${productUrl.trim() ? NX.accent : NX.border}`, borderRadius: '10px', color: productUrl.trim() ? '#fff' : NX.muted, fontSize: '12px', fontWeight: 600, cursor: productUrl.trim() && !urlLoading ? 'pointer' : 'default', whiteSpace: 'nowrap', fontFamily: "'DM Sans',sans-serif", display: 'flex', alignItems: 'center', gap: '6px' }}>
                  <svg width="13" height="13" viewBox="0 0 24 24" fill="none"><path d="M12 2l2.09 4.26L18.5 7l-3.5 3.41L15.91 15 12 12.77 8.09 15 9 10.41 5.5 7l4.41-.74L12 2z" fill="currentColor"/></svg>
                  {urlLoading ? '...' : (lang === 'en' ? 'Import with AI' : 'Importar con IA')}
                </button>
              </div>
              {urlError && <div style={{ fontSize: '11px', color: NX.danger, marginTop: '5px' }}>{urlError}</div>}
              {!urlError && productDesc && productUrl && <div style={{ fontSize: '11px', color: NX.success, marginTop: '5px' }}>✓ {lang === 'en' ? 'Description imported — edit below' : 'Descripción importada — edítala abajo'}</div>}
            </div>

            {/* Product description still needed for copy generation */}
            <textarea value={productDesc} onChange={e => setProductDesc(e.target.value)}
              placeholder={ts.productDesc}
              data-flash-target="productDesc"
              className={flashField === 'productDesc' ? 'nx-flash' : undefined}
              style={{ width: '100%', height: '72px', background: NX.bg3, border: `1px solid ${productDesc.trim().length > 0 ? NX.accent : NX.border}`, borderRadius: '10px', color: NX.text, fontSize: '13px', padding: '12px', fontFamily: "'DM Sans',sans-serif", resize: 'none', outline: 'none', boxSizing: 'border-box', marginBottom: '12px' }}/>

            {/* Country + language for copy localization */}
            <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '10px', marginBottom: '12px', position: 'relative' }}>
              <div style={{ position: 'relative' }}>
                <input type="text" value={country} onChange={e => setCountry(e.target.value)}
                  placeholder={ts.countryPlaceholder || 'País / Ciudad'}
                  data-flash-target="country"
                  className={flashField === 'country' ? 'nx-flash' : undefined}
                  style={{ width: '100%', boxSizing: 'border-box', background: NX.bg3, border: `1px solid ${country.trim() ? NX.accent : NX.border}`, borderRadius: '10px', color: NX.text, fontSize: '12px', padding: '10px 12px', fontFamily: "'DM Sans',sans-serif", outline: 'none' }}/>
                {/* Cloud-style typo suggestion — floats below the input with a pointer arrow */}
                {(() => {
                  const suggestion = suggestCountryCorrection(country);
                  if (!suggestion) return null;
                  return (
                    <div style={{
                      position: 'absolute', top: 'calc(100% + 10px)', left: 0, zIndex: 10,
                      display: 'inline-flex', alignItems: 'center', gap: '8px',
                      padding: '6px 6px 6px 12px',
                      background: 'linear-gradient(180deg, rgba(251,191,36,0.18), rgba(251,191,36,0.08))',
                      border: '1px solid rgba(251,191,36,0.4)',
                      borderRadius: '999px',
                      fontSize: '11px', color: NX.text,
                      boxShadow: '0 6px 20px rgba(251,191,36,0.15)',
                      backdropFilter: 'blur(8px)',
                      whiteSpace: 'nowrap',
                    }}>
                      <div style={{ position: 'absolute', top: '-5px', left: '18px', width: '10px', height: '10px', background: 'rgba(251,191,36,0.16)', borderTop: '1px solid rgba(251,191,36,0.4)', borderLeft: '1px solid rgba(251,191,36,0.4)', transform: 'rotate(45deg)' }}/>
                      <svg width="12" height="12" viewBox="0 0 24 24" fill="none" style={{ flexShrink: 0 }}><path d="M12 9v4M12 17h.01" stroke={NX.warning} strokeWidth="2" strokeLinecap="round"/><circle cx="12" cy="12" r="9" stroke={NX.warning} strokeWidth="1.5"/></svg>
                      <span>{lang === 'en' ? 'Did you mean' : '¿Quisiste decir'} <b style={{ color: NX.warning }}>{suggestion}</b>?</span>
                      <button type="button" onClick={() => setCountry(suggestion)}
                        style={{ padding: '4px 12px', background: NX.warning, border: 'none', borderRadius: '999px', color: '#0b0b0b', fontSize: '11px', fontWeight: 700, cursor: 'pointer', fontFamily: "'DM Sans',sans-serif" }}>
                        {lang === 'en' ? 'Fix' : 'Corregir'}
                      </button>
                    </div>
                  );
                })()}
              </div>
              <div style={{ display: 'flex', gap: '6px' }}>
                {[{ id: 'es', label: 'Español' }, { id: 'en', label: 'English' }].map(l => (
                  <div key={l.id} onClick={() => setLanguage(l.id)}
                    style={{ flex: 1, padding: '10px 6px', background: language === l.id ? NX.bg4 : NX.bg3, border: `1px solid ${language === l.id ? NX.accent : NX.border}`, borderRadius: '10px', cursor: 'pointer', textAlign: 'center', fontSize: '12px', fontWeight: language === l.id ? 600 : 400, color: language === l.id ? NX.accent : NX.muted }}>
                    {l.label}
                  </div>
                ))}
              </div>
            </div>
          </>)}

          {uploadError && <div style={{ padding: '10px 14px', background: 'rgba(248,113,113,0.08)', border: '1px solid rgba(248,113,113,0.2)', borderRadius: '8px', fontSize: '12px', color: NX.danger, marginBottom: '12px' }}>{uploadError}</div>}

          {/* Persistence notice + discard button for AI mode */}
          {imageSource === 'ai' && generatedImages.filter(i => !i.isOriginal).length > 0 && (
            <div style={{ padding: '10px 12px', background: 'rgba(251,191,36,0.08)', border: '1px solid rgba(251,191,36,0.25)', borderRadius: '10px', marginBottom: '12px', fontSize: '11px', color: NX.warning, display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: '8px' }}>
              <span>{lang === 'en' ? `You already have ${generatedImages.filter(i => !i.isOriginal).length} generated images — regenerating will discard them.` : `Ya tienes ${generatedImages.filter(i => !i.isOriginal).length} imágenes generadas — volver a generar las descartará.`}</span>
              {confirmDiscardImgs ? (
                <button onClick={() => { setGeneratedImages([]); setSelectedImgs([]); setConfirmDiscardImgs(false); }}
                  style={{ background: NX.danger, border: 'none', borderRadius: '6px', padding: '4px 10px', color: '#fff', fontSize: '10px', fontWeight: 600, cursor: 'pointer' }}>
                  {lang === 'en' ? 'Confirm discard' : 'Confirmar descarte'}
                </button>
              ) : (
                <button onClick={() => setConfirmDiscardImgs(true)}
                  style={{ background: 'transparent', border: `1px solid ${NX.warning}`, borderRadius: '6px', padding: '3px 8px', color: NX.warning, fontSize: '10px', cursor: 'pointer' }}>
                  {lang === 'en' ? 'Discard all' : 'Borrar todo'}
                </button>
              )}
            </div>
          )}

            </div>
            {/* Phase 3: aurora preview panel — right column on desktop, below on mobile.
                The panel now acts as the PRIMARY CTA: when the form is complete, clicking the
                orb triggers image generation (AI mode) or the continue action (uploaded mode).
                Otherwise it's a visual placeholder explaining what will appear here. */}
            {(() => {
              const hasImages = generatedImages.filter(i => !i.isOriginal).length > 0;
              const hasDesc = productDesc.trim().length >= 10;
              const hasCountry = !!country.trim();
              const countryValid = hasCountry && (isCountryExactMatch(country) || !suggestCountryCorrection(country));
              const readyAi = imageSource === 'ai' && hasDesc && hasCountry && countryValid;
              const readyUploaded = imageSource === 'uploaded' && userUploadedImages.length > 0 && hasDesc && hasCountry && countryValid;
              const isReady = readyAi || readyUploaded;
              const isClickable = isReady || hasImages;
              // Orb is ALWAYS clickable — if not ready, clicking triggers the field flash so the
              // user sees exactly what's missing instead of the orb sitting silently inert.
              // Country + description ALWAYS required (even when there are stale images), because
              // the copy-generation step later needs both.
              const handleOrbClick = () => {
                if (!hasDesc) { triggerFieldFlash('productDesc'); return; }
                if (!hasCountry || !countryValid) { triggerFieldFlash('country'); return; }
                if (hasImages) { setStep(3); return; }
                if (imageSource === 'uploaded' && userUploadedImages.length === 0) { triggerFieldFlash('uploaderZone'); return; }
                if (readyAi) { setGeneratedImages([]); setConfirmDiscardImgs(false); uploadAndGenerate(); return; }
                if (readyUploaded) { submitUserUploadedImages(); return; }
              };
              const label = hasImages
                ? (lang === 'en' ? 'Your creatives are ready' : 'Tus creativos están listos')
                : isReady
                  ? (lang === 'en' ? 'Click to generate ✨' : 'Click para generar ✨')
                  : (lang === 'en' ? 'Your campaign will appear here' : 'Tu campaña aparecerá aquí');
              const helper = hasImages
                ? (lang === 'en' ? 'Click to continue with your ads' : 'Click para continuar con tus anuncios')
                : isReady
                  ? (lang === 'en' ? 'Everything is ready — hit the orb to start' : 'Todo listo — pulsa el orb para arrancar')
                  : (lang === 'en' ? 'Fill description and country, then click the orb' : 'Completa descripción y país, luego pulsa el orb');
              return (
                <div style={{
                  flex: '0 0 340px', maxWidth: '340px',
                  background: 'linear-gradient(180deg, rgba(36,24,56,0.55), rgba(18,10,30,0.55))',
                  border: `1px solid ${isClickable ? NX.accent : NX.border2}`,
                  borderRadius: '18px', padding: '24px 20px',
                  display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '14px',
                  backdropFilter: 'blur(14px)', WebkitBackdropFilter: 'blur(14px)',
                  minHeight: '420px',
                  position: 'sticky', top: '0',
                  boxShadow: isClickable ? '0 0 40px rgba(139,92,246,0.25)' : 'none',
                  transition: 'box-shadow 0.3s, border-color 0.3s',
                }}>
                  <div style={{ fontSize: '10px', color: NX.muted, fontFamily: "'DM Mono',monospace", letterSpacing: '0.12em', textTransform: 'uppercase', alignSelf: 'flex-start' }}>
                    {lang === 'en' ? 'Preview' : 'Vista previa'}
                  </div>
                  {/* Aurora orb — ALWAYS clickable. When fields are missing the orb triggers
                      the red flash on the first missing field instead of silently refusing. */}
                  <div
                    onClick={handleOrbClick}
                    role="button"
                    tabIndex={0}
                    onKeyDown={e => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); handleOrbClick(); } }}
                    title={helper}
                    style={{
                      position: 'relative', width: '220px', height: '220px', marginTop: '12px',
                      cursor: 'pointer',
                      transition: 'transform 0.2s',
                    }}
                    onMouseEnter={e => { e.currentTarget.style.transform = 'scale(1.04)'; }}
                    onMouseLeave={e => { e.currentTarget.style.transform = 'scale(1)'; }}
                  >
                    <div style={{
                      position: 'absolute', inset: '-20px', borderRadius: '50%',
                      background: 'radial-gradient(circle at 30% 40%, rgba(139,92,246,0.55) 0%, rgba(139,92,246,0) 55%), radial-gradient(circle at 70% 60%, rgba(91,142,240,0.4) 0%, rgba(91,142,240,0) 60%), radial-gradient(circle at 50% 50%, rgba(236,72,153,0.28) 0%, rgba(236,72,153,0) 65%)',
                      filter: 'blur(22px)', animation: 'nbShift 11s ease-in-out infinite',
                    }}/>
                    <div style={{
                      position: 'absolute', inset: 0, borderRadius: '50%',
                      background: 'radial-gradient(circle at 40% 40%, rgba(167,139,250,0.65), rgba(139,92,246,0.35) 45%, rgba(91,142,240,0.15) 70%, transparent 100%)',
                      boxShadow: isClickable ? '0 0 80px rgba(139,92,246,0.6), inset 0 0 60px rgba(167,139,250,0.2)' : '0 0 60px rgba(139,92,246,0.4), inset 0 0 60px rgba(167,139,250,0.15)',
                      animation: 'nbPulse 3.5s ease-in-out infinite',
                    }}/>
                    <svg viewBox="0 0 220 220" style={{ position: 'absolute', inset: 0, animation: 'spin 22s linear infinite' }}>
                      <circle cx="110" cy="110" r="92" fill="none" stroke="rgba(167,139,250,0.3)" strokeWidth="0.8" strokeDasharray="2 8"/>
                    </svg>
                    <svg viewBox="0 0 220 220" style={{ position: 'absolute', inset: 0, animation: 'spinR 14s linear infinite' }}>
                      <circle cx="110" cy="110" r="72" fill="none" stroke="rgba(139,92,246,0.22)" strokeWidth="0.6" strokeDasharray="1 6"/>
                    </svg>
                    {isClickable && (
                      <div style={{ position: 'absolute', inset: 0, display: 'flex', alignItems: 'center', justifyContent: 'center', pointerEvents: 'none' }}>
                        <svg width="42" height="42" viewBox="0 0 24 24" fill="none" style={{ filter: 'drop-shadow(0 0 8px rgba(255,255,255,0.8))' }}>
                          <path d="M8 5v14l11-7z" fill="#fff"/>
                        </svg>
                      </div>
                    )}
                  </div>
                  <div style={{ fontSize: '14px', color: isClickable ? NX.accent2 : NX.text, fontWeight: 600, marginTop: 'auto', textAlign: 'center', letterSpacing: '-0.01em' }}>
                    {label}
                  </div>
                  <div style={{ fontSize: '11px', color: NX.muted, textAlign: 'center', lineHeight: 1.4 }}>
                    {helper}
                  </div>
                </div>
              );
            })()}
          </div>

          {/* Continue button — button stays LIVE; on-click validates and flashes the missing field.
              The country typo flash is ALSO fired so the user sees exactly which field is wrong. */}
          {(() => {
            const hasDesc = productDesc.trim().length >= 10;
            const hasCountry = !!country.trim();
            const countryValid = hasCountry && (isCountryExactMatch(country) || !suggestCountryCorrection(country));
            const countryHasTypo = hasCountry && !!suggestCountryCorrection(country);
            const validateCommon = () => {
              if (!hasDesc) { triggerFieldFlash('productDesc'); return false; }
              if (!hasCountry) { triggerFieldFlash('country'); return false; }
              if (!countryValid) { triggerFieldFlash('country'); return false; }
              return true;
            };
            return (
              <>
                <div style={{ display: 'flex', gap: '10px' }}>
                  <NxButton variant="ghost" onClick={() => setStep(0)} full>{ts.back}</NxButton>
                  {(() => {
                    // Universal "use existing" detection: if the campaign was hydrated from
                    // a draft (or a previous generation cycle), generatedImages already has
                    // the creatives. Show "Continuar con imágenes anteriores →" regardless
                    // of imageSource — otherwise editing a draft on the "Yo las subo" tab
                    // gets stuck because userUploadedImages is empty.
                    const hasExistingImgs = generatedImages.filter(i => !i.isOriginal && (i.cloudinaryUrl || i.previewUrl)).length > 0 && !confirmDiscardImgs;
                    if (hasExistingImgs) {
                      return (
                        <NxButton variant="accent"
                          onClick={() => { if (!validateCommon()) return; setStep(3); }}
                          full>
                          {lang === 'en' ? 'Continue with current images →' : 'Continuar con imágenes anteriores →'}
                        </NxButton>
                      );
                    }
                    if (imageSource === 'ai') {
                      return (
                        <NxButton variant="accent"
                          onClick={() => { if (!validateCommon()) return; setGeneratedImages([]); setConfirmDiscardImgs(false); uploadAndGenerate(); }}
                          full>{ts.generateBtn}</NxButton>
                      );
                    }
                    return (
                      <NxButton variant="accent"
                        onClick={() => {
                          if (userUploadedImages.length === 0) { triggerFieldFlash('uploaderZone'); return; }
                          if (!validateCommon()) return;
                          submitUserUploadedImages();
                        }}
                        full>{lang === 'en' ? 'Continue with my images →' : 'Continuar con mis imágenes →'}</NxButton>
                    );
                  })()}
                </div>
                {/* Blocking warnings below the button row */}
                {!hasCountry && hasDesc && (
                  <div style={{ marginTop: '8px', padding: '8px 12px', background: 'rgba(251,191,36,0.08)', border: `1px solid rgba(251,191,36,0.3)`, borderRadius: '8px', fontSize: '12px', color: NX.warning, display: 'flex', alignItems: 'center', gap: '6px' }}>
                    <svg width="14" height="14" viewBox="0 0 16 16" fill="none"><circle cx="8" cy="8" r="7" stroke="currentColor" strokeWidth="1.3"/><path d="M8 4v5M8 11.5v.01" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round"/></svg>
                    {lang === 'en' ? 'Country or city is required — it determines the audience and language of the ads.' : 'El país o ciudad es obligatorio — define la audiencia y el idioma del anuncio.'}
                  </div>
                )}
                {/* The floating cloud suggestion above already shows "¿Quisiste decir X? [Corregir]".
                    The red duplicate banner was redundant and made the form look cluttered — removed. */}
              </>
            );
          })()}
        </>}

        {step === 2 && (() => {
          // Dynamic checklist scripted to roughly match the real generation pipeline
          // (analyze → references → angles → prompts → polish → designing). El paso
          // REAL más lento es generar las imágenes (60-180s por imagen, paralelo),
          // por eso "Diseñando los creativos" va de ÚLTIMO y se queda activo hasta
          // que el caller desmonta. Los pasos previos llevan pacing creciente para
          // que el usuario no se quede mirando una pantalla estática.
          const scriptedSteps = lang === 'en' ? [
            'Analyzing your product and description',
            productPreviewUrl || (extraProductFiles && extraProductFiles.length) ? 'Reading the visual reference' : 'Searching visual references',
            'Defining the creative direction',
            `Picking ${imageCount} angles for your audience${country ? ` in ${country}` : ''}`,
            'Writing the prompts for the image AI',
            'Polishing brand and color details',
            'Preparing the campaign render',
            'Designing the creatives — this is the longest step',
          ] : [
            'Analizando tu producto y descripción',
            productPreviewUrl || (extraProductFiles && extraProductFiles.length) ? 'Leyendo la imagen de referencia' : 'Buscando referencias visuales',
            'Definiendo la dirección creativa',
            `Eligiendo ${imageCount} ángulos para tu audiencia${country ? ` en ${country}` : ''}`,
            'Escribiendo los prompts para la IA de imagen',
            'Puliendo detalles de marca y color',
            'Preparando la campaña',
            'Diseñando los creativos — este es el paso más largo',
          ];
          // Per-step durations: empezamos cortos (~6s) y vamos creciendo. El último
          // ("Diseñando los creativos") se queda activo hasta que el caller desmonta.
          // Suma ≈ 95s antes del último → cubre casi toda la espera real de imgs.
          const stepDurations = [6000, 8000, 10000, 12000, 14000, 18000, 27000, 999999999];
          return (
            <div style={{ padding: '20px 0' }}>
              <AiProcessing
                label={ts.steps[2].label}
                sublabel={ts.steps[2].sublabel}
                steps={scriptedSteps}
                stepDurations={stepDurations}/>
            </div>
          );
        })()}

        {step === 3 && generatingCopies && (() => {
          // Mientras corremos N calls paralelas a Sonnet para generar 5 textos por imagen,
          // mostramos un AiProcessing fullscreen con los pasos animados — sin esto el
          // botón "Creando textos…" daba la sensación de freeze 30-60s. El paso real
          // más lento es "Redactando descripciones" (Sonnet 13-50s), por eso lo dejamos
          // al final y pacing creciente en los previos.
          const aiImgsCount = generatedImages.filter(g => !g.isOriginal).length || selectedImgs.length || 1;
          const totalVariants = aiImgsCount * 5;
          const scriptedSteps = lang === 'en' ? [
            `Reading your ${aiImgsCount} ad creative${aiImgsCount === 1 ? '' : 's'}`,
            'Analyzing the product description',
            'Studying the highest-converting ad angles',
            `Writing ${totalVariants} unique primary texts (5 per image, no duplicates)`,
            `Crafting ${totalVariants} headlines (≤40 chars each, punchy)`,
            'Polishing tone and CTAs for your country',
            `Crafting ${totalVariants} descriptions — this is the longest step`,
          ] : [
            `Leyendo tus ${aiImgsCount} creativo${aiImgsCount === 1 ? '' : 's'} publicitario${aiImgsCount === 1 ? '' : 's'}`,
            'Analizando la descripción del producto',
            'Estudiando los ángulos publicitarios que más convierten',
            `Escribiendo ${totalVariants} textos principales únicos (5 por imagen, sin duplicados)`,
            `Redactando ${totalVariants} títulos (≤40 caracteres, contundentes)`,
            'Puliendo tono y CTAs para tu país',
            `Redactando ${totalVariants} descripciones — este es el paso más largo`,
          ];
          // Total real: 13-50s para Sonnet. Suma de los 6 primeros ≈ 32s, después el
          // último ("Redactando descripciones") se queda activo hasta unmount.
          const stepDurations = [3500, 4500, 5500, 6000, 5500, 7000, 999999999];
          return (
            <div style={{ padding: '20px 0' }}>
              <AiProcessing
                label={lang === 'en' ? 'Writing your texts' : 'Escribiendo tus textos'}
                sublabel={lang === 'en'
                  ? `Generating 5 text variants per image so Meta can rotate them and find the winner`
                  : `Generando 5 variantes de texto por imagen para que Meta las rote y encuentre la ganadora`}
                steps={scriptedSteps}
                stepDurations={stepDurations}/>
            </div>
          );
        })()}

        {step === 3 && !generatingCopies && <>
          <h2 style={{ fontSize: '17px', fontWeight: 500, color: NX.text, margin: '0 0 4px' }}>{ts.steps[3].title}</h2>

          {/* Error banner when image generation failed. The raw backend message can leak
              the model name (gemini-3.1-flash-image-preview, gpt-image-2). We scrub it
              here and replace with the user-facing tier label so the dashboard never
              exposes underlying provider details. */}
          {imageGenError && (() => {
            const isUltra = /^gpt-image/i.test(imageModel || '');
            const friendly = isUltra
              ? (lang === 'en' ? 'Premium Ultra MAX' : 'Premium Ultra MAX')
              : (lang === 'en' ? 'Premium' : 'Premium');
            const sanitized = String(imageGenError)
              // Drop any "model: <name>" segments
              .replace(/\s*[—-]?\s*model:\s*[a-z0-9.\-_]+/gi, '')
              // Replace bare model identifiers with the tier name
              .replace(/gemini-?[0-9.]*-?(flash|pro)?-?image(-preview)?/gi, friendly)
              .replace(/gpt-image-?\d*/gi, friendly)
              // Collapse JSON-style {"error":"…"} → just the message
              .replace(/\{\s*"error"\s*:\s*"([^"]+)"\s*\}/g, '$1')
              // Strip lingering punctuation artifacts
              .replace(/\s{2,}/g, ' ')
              .trim();
            const generatorLabel = lang === 'en'
              ? `${friendly} image generator`
              : `Generador de Imágenes ${friendly}`;
            return (
              <div style={{ background: 'rgba(248,113,113,0.1)', border: '1px solid rgba(248,113,113,0.3)', borderRadius: '10px', padding: '10px 14px', marginBottom: '12px', fontSize: '12px', color: NX.danger }}>
                <strong>{lang === 'en' ? 'Image generation error' : 'Error al generar imagen'} ({generatorLabel}):</strong> {sanitized}
              </div>
            );
          })()}

          {/* Phase 3 polish: 2-column layout. Grid on the LEFT takes full available width,
              sidebar on the RIGHT holds reference image + approved/rejected counters + ZIP button.
              This keeps 10 images visible on-screen without scrolling. On <1024px the
              `nx-img-side` class collapses the sidebar below the grid. */}
          <div className="nx-img-side" style={{ display: 'flex', gap: '16px', alignItems: 'center', marginBottom: '12px' }}>
            <div style={{ flex: 1, minWidth: 0 }}>

          {/* AI images grid only — original excluded.
              Up to 5 images → single row. 6+ images → 5 columns × N rows (5×2 for packs of 10)
              so each card is big enough to evaluate.
              With 1-2 images (test mode) we cap the grid width so cards match the size of one
              column in a 5-col layout — otherwise a single card would stretch the full width. */}
          {(() => {
            const aiImgs = generatedImages.filter(img => !img.isOriginal);
            const gridCols = aiImgs.length <= 5 ? aiImgs.length : 5;
            // Max-width ≈ card-size × count. Uses same sizing as a 5-col card (~220px + gap) to
            // keep small counts visually consistent with larger packs.
            // Tamaños más compactos para que el grid quepa en una pantalla sin scroll
            // junto al sidebar de referencia + contadores + botones.
            const maxWidth = aiImgs.length === 1 ? '220px'
                           : aiImgs.length === 2 ? '440px'
                           : aiImgs.length === 3 ? '660px'
                           : aiImgs.length === 4 ? '880px'
                           : 'none';
            return (
          <div className="nx-img-result-grid" style={{ display: 'grid', gridTemplateColumns: `repeat(${gridCols},1fr)`, gap: '10px', maxWidth, margin: '0 auto' }}>
            {aiImgs.map((img, gridIdx) => {
              const i = generatedImages.indexOf(img); // real index for state
              const approved = selectedImgs.includes(i);
              const isRegen = regeneratingSlots.has(i);
              const isLoading = img.loading || isRegen;
              const failed = !img.previewUrl && !isLoading;
              return (
                <div key={i} style={{ borderRadius: '12px', border: `2px solid ${isLoading ? 'rgba(139,92,246,0.2)' : failed ? 'rgba(107,107,107,0.3)' : approved ? NX.success : 'rgba(248,113,113,0.35)'}`, overflow: 'hidden', background: NX.bg3, transition: 'border-color 0.3s' }}>
                  {/* Image — click to zoom */}
                  <div onClick={() => img.previewUrl && setZoomedImage(img)} style={{ position: 'relative', paddingBottom: aspectRatio === '4:5' ? '125%' : aspectRatio === '9:16' ? '177.78%' : aspectRatio === '16:9' ? '56.25%' : '100%', cursor: img.previewUrl ? 'zoom-in' : 'default', background: NX.bg4 }}>
                    {img.previewUrl ? (
                      <img src={img.previewUrl} alt={img.label} style={{ position: 'absolute', inset: 0, width: '100%', height: '100%', objectFit: 'contain' }}/>
                    ) : isLoading ? (
                      <ImageGenProgress done={false} bg={img.bg} angle={img.angle || 'GENERANDO'}
                        etaSeconds={(usedImageModel || imageModel) === 'gpt-image-2' ? 90 : 30}/>
                    ) : (
                      <div style={{ position: 'absolute', inset: 0, background: 'rgba(20,20,20,0.95)', display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', gap: '8px' }}>
                        <svg width="24" height="24" viewBox="0 0 24 24" fill="none"><circle cx="12" cy="12" r="10" stroke="rgba(248,113,113,0.5)" strokeWidth="1.5"/><path d="M12 8v4M12 16h.01" stroke="rgba(248,113,113,0.7)" strokeWidth="1.8" strokeLinecap="round"/></svg>
                        <span style={{ fontSize: '10px', color: 'rgba(248,113,113,0.7)', fontFamily: "'DM Mono',monospace", fontWeight: 600 }}>ERROR AL GENERAR</span>
                      </div>
                    )}
                    {img.previewUrl && <div style={{ position: 'absolute', inset: 0, background: 'linear-gradient(to top, rgba(0,0,0,0.55) 0%, transparent 45%)' }}/>}
                    {img.previewUrl && (
                      <div style={{ position: 'absolute', bottom: '6px', right: '8px', background: 'rgba(0,0,0,0.5)', borderRadius: '6px', padding: '3px 6px' }}>
                        <svg width="11" height="11" viewBox="0 0 16 16" fill="none"><path d="M10 2h4v4M6 14H2v-4M14 10v4h-4M2 6V2h4" stroke="rgba(255,255,255,0.7)" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/></svg>
                      </div>
                    )}
                    {img.previewUrl && (
                      <div style={{ position: 'absolute', bottom: '6px', left: '8px' }}>
                        <span style={{ fontSize: '9px', color: 'rgba(255,255,255,0.8)', fontFamily: "'DM Mono',monospace", fontWeight: 700 }}>
                          {img.angle ? img.angle.toUpperCase() : `IA ${gridIdx + 1}`}
                        </span>
                      </div>
                    )}
                    {/* Status badge — only show when image is ready (not loading) */}
                    {!failed && !isLoading && (
                      <div style={{ position: 'absolute', top: '6px', right: '6px', background: approved ? 'rgba(52,211,153,0.9)' : 'rgba(248,113,113,0.8)', borderRadius: '100px', padding: '2px 8px', display: 'flex', alignItems: 'center', gap: '4px' }}>
                        {approved
                          ? <><svg width="10" height="10" viewBox="0 0 12 12" fill="none"><path d="M2 6l3 3 5-6" stroke="white" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/></svg><span style={{ fontSize: '9px', color: '#fff', fontWeight: 700 }}>APROBADA</span></>
                          : <><svg width="8" height="8" viewBox="0 0 10 10" fill="none"><path d="M2 2l6 6M8 2l-6 6" stroke="white" strokeWidth="2" strokeLinecap="round"/></svg><span style={{ fontSize: '9px', color: '#fff', fontWeight: 700 }}>RECHAZADA</span></>}
                      </div>
                    )}
                  </div>
                  {/* Approve / Reject / Download */}
                  <div style={{ display: 'flex', borderTop: `1px solid ${NX.border}` }}>
                    {isLoading && !isRegen ? (
                      <div style={{ flex: 1, padding: '8px', display: 'flex', alignItems: 'center', justifyContent: 'center', gap: '6px' }}>
                        <span style={{ display: 'inline-block', width: '8px', height: '8px', border: '1.5px solid rgba(139,92,246,0.4)', borderTopColor: NX.accent, borderRadius: '50%', animation: 'spin 0.7s linear infinite' }}/>
                        <span style={{ fontSize: '10px', color: NX.muted, fontFamily: "'DM Sans',sans-serif" }}>Generando...</span>
                      </div>
                    ) : failed ? (
                      <button onClick={() => rejectAndRegenerate(i)} style={{ flex: 1, padding: '8px', background: 'transparent', border: 'none', cursor: 'pointer', color: NX.muted, fontSize: '11px', fontWeight: 600, fontFamily: "'DM Sans',sans-serif", display: 'flex', alignItems: 'center', justifyContent: 'center', gap: '5px' }}>
                        <svg width="11" height="11" viewBox="0 0 14 14" fill="none"><path d="M2 7a5 5 0 1 1 1.5 3.5" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round"/><path d="M2 11V7h4" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round"/></svg>
                        Reintentar
                      </button>
                    ) : (<>
                    <button onClick={() => !approved && !isRegen && toggleImg(i)}
                      style={{ flex: 1, padding: '8px', background: approved ? 'rgba(52,211,153,0.1)' : 'transparent', border: 'none', borderRight: `1px solid ${NX.border}`, cursor: (approved || isRegen) ? 'default' : 'pointer', color: approved ? NX.success : NX.muted, fontSize: '11px', fontWeight: 600, fontFamily: "'DM Sans',sans-serif", display: 'flex', alignItems: 'center', justifyContent: 'center', gap: '5px' }}>
                      <svg width="11" height="11" viewBox="0 0 12 12" fill="none"><path d="M2 6l3 3 5-6" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/></svg>
                      Me gusta
                    </button>
                    <button onClick={() => !isRegen && rejectAndRegenerate(i)}
                      style={{ flex: 1, padding: '8px', background: !approved && !isRegen ? 'rgba(248,113,113,0.08)' : 'transparent', border: 'none', borderRight: `1px solid ${NX.border}`, cursor: isRegen ? 'default' : 'pointer', color: !approved && !isRegen ? NX.danger : NX.muted, fontSize: '11px', fontWeight: 600, fontFamily: "'DM Sans',sans-serif", display: 'flex', alignItems: 'center', justifyContent: 'center', gap: '5px' }}>
                      {isRegen
                        ? <><span style={{ display: 'inline-block', width: '8px', height: '8px', border: '1.5px solid currentColor', borderTopColor: 'transparent', borderRadius: '50%', animation: 'spin 0.7s linear infinite' }}/> Generando</>
                        : <><svg width="9" height="9" viewBox="0 0 10 10" fill="none"><path d="M2 2l6 6M8 2l-6 6" stroke="currentColor" strokeWidth="2" strokeLinecap="round"/></svg> No me gusta</>}
                    </button>
                    {img.previewUrl && (
                      <button onClick={() => downloadImage(img, i)} title="Descargar imagen"
                        style={{ padding: '8px 10px', background: 'transparent', border: 'none', cursor: 'pointer', color: NX.muted, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
                        <svg width="13" height="13" viewBox="0 0 16 16" fill="none"><path d="M8 2v8M5 7l3 3 3-3" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/><path d="M3 13h10" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round"/></svg>
                      </button>
                    )}
                    </>)}
                  </div>
                </div>
              );
            })}
          </div>
            );
          })()}
            </div>
            {/* RIGHT SIDEBAR: reference image + stats + download ZIP. Sidebar is intentionally
                short so the parent flex (alignItems:center) keeps the image grid + sidebar
                vertically balanced — no big air gap below the grid before the action buttons. */}
            <div style={{ flex: '0 0 180px', display: 'flex', flexDirection: 'column', gap: '10px' }}>
              {productPreviewUrl && (
                <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '5px', padding: '10px 10px', background: 'rgba(26,17,40,0.6)', border: `1px solid ${NX.border}`, borderRadius: '12px', backdropFilter: 'blur(12px)' }}>
                  <div style={{ fontSize: '9px', color: NX.muted, fontFamily: "'DM Mono',monospace", fontWeight: 700, letterSpacing: '0.12em' }}>REFERENCIA</div>
                  <div onClick={() => fileInputRef.current?.click()} title="Haz clic para cambiar la imagen de referencia"
                    style={{ position: 'relative', width: '110px', height: '110px', borderRadius: '10px', overflow: 'hidden', border: `1.5px solid ${NX.border2}`, cursor: 'pointer', boxShadow: '0 4px 14px rgba(139,92,246,0.15)' }}>
                    <img src={productPreviewUrl} alt="Referencia" style={{ width: '100%', height: '100%', objectFit: 'contain', background: NX.bg4 }}/>
                    <div style={{ position: 'absolute', inset: 0, background: 'rgba(0,0,0,0)', display: 'flex', alignItems: 'center', justifyContent: 'center', transition: 'background 0.15s' }}
                      onMouseEnter={e => e.currentTarget.style.background='rgba(0,0,0,0.5)'}
                      onMouseLeave={e => e.currentTarget.style.background='rgba(0,0,0,0)'}>
                      <svg width="22" height="22" viewBox="0 0 20 20" fill="none" style={{ opacity: 0.9 }}>
                        <path d="M3 17l4-1 9-9a1.5 1.5 0 00-2-2L5 14l-2 3z" stroke="white" strokeWidth="1.5" strokeLinejoin="round"/>
                      </svg>
                    </div>
                  </div>
                </div>
              )}
              {/* Progress ring — fills as Gemini emits each finished image. Lives outside the
                  stats card so the percentage feels like a discrete "are we there yet?" gauge,
                  separate from the per-image approve/reject counter below. Hidden when there are
                  no AI creatives yet (e.g. boost-post mode using only the original). */}
              {(() => {
                const aiImages = generatedImages.filter(img => !img.isOriginal);
                const total = aiImages.length;
                if (total === 0) return null;
                const ready = aiImages.filter(img => !img.loading).length;
                const pct = Math.round((ready / total) * 100);
                const radius = 26;
                const circumference = 2 * Math.PI * radius;
                const dashOffset = circumference * (1 - pct / 100);
                const isDone = pct === 100;
                const labelDone = lang === 'en' ? 'Ready' : 'Listo';
                const labelGen = lang === 'en' ? 'Generating creatives…' : 'Generando creativos…';
                return (
                  <div style={{ padding: '12px 12px 10px', background: 'rgba(26,17,40,0.6)', border: `1px solid ${NX.border}`, borderRadius: '12px', backdropFilter: 'blur(12px)', display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '6px' }}>
                    <div style={{ position: 'relative', width: '64px', height: '64px' }}>
                      <svg width="64" height="64" viewBox="0 0 64 64" style={{ transform: 'rotate(-90deg)' }}>
                        <circle cx="32" cy="32" r={radius} stroke={NX.border} strokeWidth="4" fill="none"/>
                        <circle cx="32" cy="32" r={radius} stroke={isDone ? NX.success : NX.accent2} strokeWidth="4" fill="none" strokeLinecap="round" strokeDasharray={circumference} strokeDashoffset={dashOffset} style={{ transition: 'stroke-dashoffset 0.5s ease, stroke 0.3s ease' }}/>
                      </svg>
                      <div style={{ position: 'absolute', inset: 0, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
                        {isDone ? (
                          <svg width="22" height="22" viewBox="0 0 24 24" fill="none"><path d="M5 12.5l4.5 4.5L19 7.5" stroke={NX.success} strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round"/></svg>
                        ) : (
                          <span style={{ fontSize: '14px', fontWeight: 700, color: NX.text, fontFamily: "'DM Sans',sans-serif" }}>{pct}%</span>
                        )}
                      </div>
                    </div>
                    <span style={{ fontSize: '10px', color: isDone ? NX.success : NX.muted, fontFamily: "'DM Mono',monospace", fontWeight: 600, letterSpacing: '0.06em', textAlign: 'center' }}>
                      {isDone ? labelDone : labelGen}
                    </span>
                  </div>
                );
              })()}
              {/* Stats panel */}
              <div style={{ padding: '10px 12px', background: 'rgba(26,17,40,0.6)', border: `1px solid ${NX.border}`, borderRadius: '12px', backdropFilter: 'blur(12px)', display: 'flex', flexDirection: 'column', gap: '6px' }}>
                <div style={{ fontSize: '9px', color: NX.muted, fontFamily: "'DM Mono',monospace", fontWeight: 700, letterSpacing: '0.12em' }}>CREATIVOS IA</div>
                <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: '6px' }}>
                  <div style={{ display: 'flex', alignItems: 'center', gap: '6px' }}>
                    <div style={{ width: '8px', height: '8px', borderRadius: '50%', background: NX.success }}/>
                    <span style={{ fontSize: '13px', color: NX.text, fontWeight: 700 }}>{selectedImgs.length}</span>
                  </div>
                  <span style={{ fontSize: '11px', color: NX.muted }}>aprobadas</span>
                </div>
                <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: '6px' }}>
                  <div style={{ display: 'flex', alignItems: 'center', gap: '6px' }}>
                    <div style={{ width: '8px', height: '8px', borderRadius: '50%', background: NX.danger }}/>
                    <span style={{ fontSize: '13px', color: NX.text, fontWeight: 700 }}>{generatedImages.filter(img => !img.isOriginal && !img.loading).length - selectedImgs.length}</span>
                  </div>
                  <span style={{ fontSize: '11px', color: NX.muted }}>rechazadas</span>
                </div>
                <div style={{ height: '1px', background: NX.border, margin: '4px 0' }}/>
                <div style={{ fontSize: '11px', color: NX.muted, fontFamily: "'DM Mono',monospace", textAlign: 'center' }}>
                  {generatedImages.filter(img => !img.isOriginal && !img.loading).length}/{generatedImages.filter(img => !img.isOriginal).length} listas
                </div>
                {usedImageModel && (
                  <span style={{ alignSelf: 'center', fontSize: '10px', color: NX.accent, fontFamily: "'DM Mono',monospace", background: 'rgba(139,92,246,0.12)', border: '1px solid rgba(139,92,246,0.25)', borderRadius: '6px', padding: '3px 8px', textAlign: 'center' }}>
                    {(/^gpt-image/i.test(usedImageModel)) ? 'Ultra MAX' : 'Premium'}
                  </span>
                )}
              </div>
              {/* Download ZIP button */}
              {generatedImages.some(img => !img.loading && (img.cloudinaryUrl || img.previewUrl)) && (
                <button onClick={downloadAllImages} disabled={downloadingAll}
                  title={downloadMode === 'zip' ? 'Descargar todas las imágenes en un solo ZIP' : 'Descargar cada imagen individualmente'}
                  style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', gap: '8px', fontSize: '12px', color: NX.text, fontWeight: 600, background: 'rgba(26,17,40,0.6)', border: `1px solid ${NX.border2}`, borderRadius: '12px', padding: '10px 12px', cursor: downloadingAll ? 'wait' : 'pointer', opacity: downloadingAll ? 0.6 : 1, position: 'relative', overflow: 'hidden', fontFamily: "'DM Sans',sans-serif", backdropFilter: 'blur(12px)' }}>
                  {downloadingAll && (
                    <div style={{ position: 'absolute', left: 0, top: 0, bottom: 0, width: `${downloadProgress}%`, background: 'rgba(139,92,246,0.25)', transition: 'width 0.2s' }}/>
                  )}
                  <svg width="14" height="14" viewBox="0 0 20 20" fill="none" style={{ position: 'relative', zIndex: 1 }}>
                    <path d="M10 3v10m0 0l-4-4m4 4l4-4M4 17h12" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"/>
                  </svg>
                  <span style={{ position: 'relative', zIndex: 1 }}>
                    {downloadingAll
                      ? (downloadMode === 'zip' ? `ZIP ${downloadProgress}%` : `Descargando ${downloadProgress}%`)
                      : (lang === 'en' ? 'Download ZIP' : 'Descargar ZIP')}
                  </span>
                </button>
              )}
            </div>
          </div>

          <div style={{ display: 'flex', gap: '10px', alignItems: 'center' }}>
            {/* Atrás: en flow asistente vuelve a la conversación PRESERVANDO los datos
                (descripción, país, presupuesto, etc.); en avanzado vuelve al form de step 1.
                onBackToAssistant es la versión no-destructiva — usar onRestart aquí borraba
                todo lo que el usuario había escrito y obligaba a re-tipear. */}
            <NxButton variant="ghost" onClick={() => {
              if (assistantPrefill?.onBackToAssistant) { assistantPrefill.onBackToAssistant(); return; }
              if (assistantPrefill?.onRestart) { assistantPrefill.onRestart(); return; }
              setStep(1);
            }} full>{ts.back}</NxButton>
            <button
              onClick={() => {
                if (!window.confirm(lang === 'en' ? 'Delete all generated creatives? This cannot be undone.' : '¿Borrar todos los creativos generados? Esta acción no se puede deshacer.')) return;
                // Limpiar cache de imágenes y copies del producto actual — solo "Borrar todo"
                // tiene permiso para tirar el trabajo cobrado (ir Atrás al asistente NO lo hace).
                try {
                  if (imagesCacheKey) localStorage.removeItem(imagesCacheKey);
                  if (copiesCacheKey) localStorage.removeItem(copiesCacheKey);
                } catch {}
                // Si entramos por el asistente, "Borrar todo" reinicia el asistente
                // (vuelve al step 0 conversacional) en lugar de mandarte al editor avanzado.
                if (assistantPrefill?.onRestart) { assistantPrefill.onRestart(); return; }
                setGeneratedImages([]); setSelectedImgs([]); setGeneratedCopies([]); setApprovedCopies(new Set()); setStep(1);
              }}
              title={lang === 'en' ? 'Discard all creatives' : 'Borrar toda la generación'}
              style={{ flexShrink: 0, padding: '10px 14px', background: 'transparent', border: `1px solid ${NX.danger}`, borderRadius: '10px', color: NX.danger, fontSize: '12px', cursor: 'pointer', fontWeight: 600, fontFamily: "'DM Sans',sans-serif", display: 'flex', alignItems: 'center', gap: '6px' }}
              onMouseEnter={e => { e.currentTarget.style.background = 'rgba(248,113,113,0.1)'; }}
              onMouseLeave={e => { e.currentTarget.style.background = 'transparent'; }}>
              <svg width="12" height="12" viewBox="0 0 16 16" fill="none"><path d="M3 4h10M6 4V2.5A.5.5 0 016.5 2h3a.5.5 0 01.5.5V4M4.5 4l.5 9a1 1 0 001 1h4a1 1 0 001-1l.5-9" stroke="currentColor" strokeWidth="1.3" strokeLinecap="round" strokeLinejoin="round"/></svg>
              {lang === 'en' ? 'Discard all' : 'Borrar todo'}
            </button>
            <NxButton variant="accent" onClick={generateCopiesAndContinue} disabled={!selectedImgs.length || generatingCopies} full>
              {generatingCopies
                ? <><span style={{ display:'inline-block', width:'12px', height:'12px', border:'2px solid currentColor', borderTopColor:'transparent', borderRadius:'50%', animation:'spin 0.7s linear infinite', marginRight:'8px' }}/> {lang === 'en' ? 'Creating texts...' : 'Creando textos...'}</>
                : (generatedCopies && generatedCopies.length > 0
                    ? (lang === 'en' ? 'Continue with current texts →' : 'Continuar con mis textos →')
                    : <>{ts.continueWithCopies} <span style={{ marginLeft:'8px', padding:'2px 8px', borderRadius:'100px', background:'rgba(255,255,255,0.18)', fontSize:'11px', fontFamily:"'DM Mono',monospace", fontWeight:700 }}>{(window.CREDIT_COSTS?.copies_batch || 10)} cr</span></>)}
            </NxButton>
          </div>
        </>}

        {/* ── LIGHTBOX ── */}
        {zoomedImage && (
          <div onClick={() => setZoomedImage(null)} style={{ position: 'fixed', inset: 0, zIndex: 9999, background: 'rgba(0,0,0,0.92)', display: 'flex', alignItems: 'center', justifyContent: 'center', cursor: 'zoom-out', backdropFilter: 'blur(6px)' }}>
            <div onClick={e => e.stopPropagation()} style={{ position: 'relative', maxWidth: 'min(90vw, 700px)', maxHeight: '90vh', borderRadius: '16px', overflow: 'hidden', boxShadow: '0 40px 120px rgba(0,0,0,0.8)' }}>
              <img src={zoomedImage.previewUrl} alt={zoomedImage.label} style={{ display: 'block', maxWidth: '100%', maxHeight: '90vh', objectFit: 'contain' }}/>
              <button onClick={() => setZoomedImage(null)} style={{ position: 'absolute', top: '10px', right: '10px', width: '32px', height: '32px', borderRadius: '50%', background: 'rgba(0,0,0,0.6)', border: 'none', cursor: 'pointer', color: '#fff', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
                <svg width="12" height="12" viewBox="0 0 12 12" fill="none"><path d="M2 2l8 8M10 2l-8 8" stroke="white" strokeWidth="1.8" strokeLinecap="round"/></svg>
              </button>
              <div style={{ position: 'absolute', bottom: '10px', left: '12px', background: 'rgba(0,0,0,0.6)', borderRadius: '6px', padding: '4px 10px', fontSize: '11px', color: 'rgba(255,255,255,0.8)', fontFamily: "'DM Mono',monospace" }}>
                {zoomedImage.angle ? zoomedImage.angle.toUpperCase() : zoomedImage.label}
              </div>
            </div>
          </div>
        )}

        {step === 4 && <>
          {/* paddingRight reserves space for the modal's absolute close X (top-right) so the
              "Regenerar textos" button never overlaps it. */}
          <div style={{ display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between', gap: '12px', marginBottom: '4px', paddingRight: '44px' }}>
            <h2 style={{ fontSize: '17px', fontWeight: 500, color: NX.text, margin: 0 }}>{ts.steps[4].title}</h2>
            <button
              type="button"
              onClick={() => generateCopiesAndContinue({ force: true })}
              disabled={generatingCopies}
              title={lang === 'en' ? 'Generate brand-new texts (uses tokens)' : 'Generar textos nuevos (gasta tokens)'}
              style={{
                background: 'transparent', border: `1px solid ${NX.border2}`, borderRadius: '8px',
                color: NX.muted, fontSize: '11px', padding: '6px 10px', cursor: generatingCopies ? 'wait' : 'pointer',
                fontFamily: "'DM Sans',sans-serif", display: 'flex', alignItems: 'center', gap: '6px', whiteSpace: 'nowrap',
              }}>
              <svg width="11" height="11" viewBox="0 0 16 16" fill="none"><path d="M13 8a5 5 0 11-1.46-3.54M13 3v3.5h-3.5" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round" strokeLinejoin="round"/></svg>
              {generatingCopies
                ? (lang === 'en' ? 'Regenerating…' : 'Regenerando…')
                : <>{lang === 'en' ? 'Regenerate texts' : 'Regenerar textos'}
                    <span style={{ marginLeft:'5px', padding:'1px 6px', borderRadius:'100px', background:'rgba(139,92,246,0.18)', fontSize:'9px', fontFamily:"'DM Mono',monospace", fontWeight:700, color:NX.accent2 }}>{(window.CREDIT_COSTS?.copies_batch || 10)} cr</span>
                  </>}
            </button>
          </div>
          <p style={{ color: NX.muted, fontSize: '13px', margin: '0 0 8px' }}>
            {lang === 'en'
              ? 'Each image paired with its text — approve the ones you want to publish, or edit the ad text.'
              : 'Cada imagen con su texto — aprueba las que quieras publicar, o edita el texto del anuncio.'}
            <span style={{ marginLeft: '10px', color: NX.accent }}>{approvedCopies.size}/{generatedCopies.length} aprobadas</span>
          </p>
          {/* Important reminder — clarify what is and isn't editable */}
          <div style={{ padding: '8px 12px', background: 'rgba(139,92,246,0.06)', border: '1px solid rgba(139,92,246,0.2)', borderRadius: '8px', marginBottom: '14px', display: 'flex', alignItems: 'flex-start', gap: '8px' }}>
            <svg width="14" height="14" viewBox="0 0 16 16" fill="none" style={{ flexShrink: 0, marginTop: '1px' }}><circle cx="8" cy="8" r="7" stroke={NX.accent} strokeWidth="1.3"/><path d="M8 4v5M8 11.5v.01" stroke={NX.accent} strokeWidth="1.5" strokeLinecap="round"/></svg>
            <div style={{ fontSize: '11px', color: NX.text, lineHeight: 1.5 }}>
              {lang === 'en'
                ? <>You can edit the <b style={{ color: NX.accent }}>primary text, headline and description</b> — those appear around the image in the Facebook feed. <b style={{ color: NX.muted }}>The text rendered inside the image</b> was generated by AI with your reference product and cannot be edited here — regenerating it would use more tokens.</>
                : <>Puedes editar el <b style={{ color: NX.accent }}>texto principal, título y descripción</b> — esos salen alrededor de la imagen en Facebook. <b style={{ color: NX.muted }}>El texto dentro de la imagen</b> se renderizó con IA junto con tu producto y no se puede editar aquí — regenerarlo consumiría más tokens.</>}
            </div>
          </div>

          {/* Layout con paginación — re-habilitado tras la aprobación de Standard Access
              (2026-05-04). Cada imagen tiene su pool de hasta 5 variantes (texto principal +
              título + descripción) y el user navega entre imágenes y aprueba/rechaza cada
              variante. Meta rota la combinación óptima por impresión via MTO. */}
          {(() => {
            const totalImgs = generatedCopies.length;
            const i = Math.min(currentImgPage, Math.max(0, totalImgs - 1));
            const c = generatedCopies[i] || { pool: { primaryTexts: [], titulos: [], descripciones: [] } };
            const pool = c?.pool || { primaryTexts: [], titulos: [], descripciones: [] };
            const variants = Array.isArray(pool.primaryTexts) ? pool.primaryTexts.slice(0, 5) : [];
            const variantCount = Math.max(variants.length, 1);
            const img = generatedImages.filter(x => !x.isOriginal)[i] || generatedImages[i];
            const approvedInImg = Array.from({ length: variantCount }, (_, v) => v).filter(v => approvedVariants.has(`${i}:${v}`)).length;
            return (
              <div style={{ display: 'flex', flexDirection: 'column', gap: '10px', marginBottom: '14px' }}>
                {/* Image header */}
                <div style={{ display: 'flex', alignItems: 'center', gap: '10px', padding: '8px 12px', background: NX.bg3, border: `1px solid ${NX.border2}`, borderRadius: '10px' }}>
                  <div style={{ width: '40px', height: '40px', borderRadius: '8px', overflow: 'hidden', flexShrink: 0, background: NX.bg4 }}>
                    {img?.previewUrl && <img src={img.previewUrl} alt="" style={{ width: '100%', height: '100%', objectFit: 'cover' }}/>}
                  </div>
                  <div style={{ flex: 1, minWidth: 0 }}>
                    <div style={{ fontSize: '13px', fontWeight: 600, color: NX.text }}>
                      {lang === 'en' ? `Image ${i + 1} of ${totalImgs}` : `Imagen ${i + 1} de ${totalImgs}`}
                      <span style={{ marginLeft: '10px', fontSize: '10px', fontFamily: "'DM Mono',monospace", color: NX.muted, fontWeight: 400 }}>
                        {(c.angulo || '').toUpperCase()}
                      </span>
                    </div>
                    <div style={{ fontSize: '11px', color: NX.muted }}>
                      {lang === 'en'
                        ? (variantCount === 1
                            ? `${approvedInImg}/${variantCount} approved · 1 text per ad`
                            : `${approvedInImg}/${variantCount} variants approved · ${variantCount} text variants per ad — Meta rotates them`)
                        : (variantCount === 1
                            ? `${approvedInImg}/${variantCount} aprobada · 1 texto por anuncio`
                            : `${approvedInImg}/${variantCount} variantes aprobadas · ${variantCount} textos por anuncio — Meta los rota`)}
                    </div>
                  </div>
                  {/* Image-page navigator dots */}
                  <div style={{ display: 'flex', gap: '4px' }}>
                    {Array.from({ length: totalImgs }, (_, k) => (
                      <button key={k} onClick={() => setCurrentImgPage(k)}
                        title={lang === 'en' ? `Go to image ${k + 1}` : `Ir a imagen ${k + 1}`}
                        style={{ width: k === i ? '20px' : '8px', height: '8px', borderRadius: '4px', background: k === i ? NX.accent : NX.bg4, border: `1px solid ${k === i ? NX.accent : NX.border}`, cursor: 'pointer', padding: 0, transition: 'all 0.15s' }}/>
                    ))}
                  </div>
                </div>

                {/* Variantes en una sola fila — exactamente N columnas siempre, sin scroll
                    horizontal. Para que las 5 entren en pantallas comunes (1280px+), las cards
                    son compactas. En mobile angosto el modal no permite scroll horizontal así
                    que las cards se aprietan pero siguen visibles. */}
                <div className="nx-variants-grid" style={{ display: 'grid', gridTemplateColumns: `repeat(${variantCount}, minmax(0, 1fr))`, gap: '8px' }}>
                  {variants.map((rawPt, v) => {
                    const variantText = typeof rawPt === 'string' ? rawPt : (rawPt?.texto || rawPt?.text || '');
                    const variantTitle = (pool.titulos && pool.titulos[v]) || '';
                    const variantDesc  = (pool.descripciones && pool.descripciones[v]) || '';
                    const variantKey = `${i}:${v}`;
                    const approved = approvedVariants.has(variantKey);
                    const isRegen = regeneratingVariant.has(variantKey);
                    const isEditing = editingVariant && editingVariant.i === i && editingVariant.v === v;
                    const toggleVar = () => {
                      setApprovedVariants(prev => {
                        const next = new Set(prev);
                        if (next.has(variantKey)) next.delete(variantKey); else next.add(variantKey);
                        return next;
                      });
                    };
                    return (
                      <div key={`v-${i}-${v}`} style={{
                        position: 'relative',
                        borderRadius: '10px',
                        border: `2px solid ${isRegen ? NX.accent : (approved ? NX.success : 'rgba(248,113,113,0.3)')}`,
                        background: NX.bg3,
                        overflow: 'hidden',
                        display: 'flex', flexDirection: 'column',
                        transition: 'border-color 0.2s',
                        opacity: approved ? 1 : 0.55,
                      }}>
                        {/* Regen overlay */}
                        {isRegen && (
                          <div style={{ position: 'absolute', inset: 0, background: 'rgba(8,8,10,0.86)', backdropFilter: 'blur(3px)', display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', gap: '10px', zIndex: 6 }}>
                            <svg width="20" height="20" viewBox="0 0 24 24" fill="none" style={{ animation: 'spin 1s linear infinite' }}>
                              <circle cx="12" cy="12" r="9" stroke={NX.accent} strokeWidth="2.5" strokeLinecap="round" strokeDasharray="14 40"/>
                            </svg>
                            <div style={{ fontSize: '9px', color: NX.accent, fontFamily: "'DM Mono',monospace", letterSpacing: '0.14em', textTransform: 'uppercase' }}>
                              {lang === 'en' ? 'Regenerating…' : 'Regenerando…'}
                            </div>
                          </div>
                        )}
                        {/* Variant tag + edit toggle */}
                        <div style={{ padding: '5px 8px', background: NX.bg4, borderBottom: `1px solid ${NX.border}`, display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
                          <span style={{ fontSize: '8px', fontFamily: "'DM Mono',monospace", color: approved ? NX.accent : NX.muted, fontWeight: 700, letterSpacing: '0.08em' }}>
                            {lang === 'en' ? `VARIANT ${v + 1}` : `VARIANTE ${v + 1}`}
                          </span>
                          <button onClick={() => setEditingVariant(isEditing ? null : { i, v })}
                            style={{ background: 'transparent', border: 'none', cursor: 'pointer', color: isEditing ? NX.accent : NX.muted, padding: '1px 3px', display: 'flex', alignItems: 'center', gap: '3px', fontSize: '9px', fontFamily: "'DM Sans',sans-serif" }}>
                            <svg width="9" height="9" viewBox="0 0 16 16" fill="none"><path d="M3 13l1.5-4 7-7a1.5 1.5 0 012 2l-7 7-4 1.5z" stroke="currentColor" strokeWidth="1.5" strokeLinejoin="round"/></svg>
                            {isEditing ? (lang === 'en' ? 'Done' : 'Listo') : (lang === 'en' ? 'Edit' : 'Editar')}
                          </button>
                        </div>
                        {/* Primary text — editable inline */}
                        <div style={{ padding: '6px 8px', background: '#fff', color: '#050505' }}>
                          {isEditing ? (
                            <textarea value={variantText} onChange={e => updateVariantField(i, v, 'texto', e.target.value)}
                              style={{ width: '100%', minHeight: '54px', background: '#F0F2F5', border: `1px solid ${NX.accent}`, borderRadius: '5px', padding: '5px 7px', fontSize: '9px', fontFamily: "'DM Sans',sans-serif", color: '#050505', resize: 'vertical', outline: 'none', boxSizing: 'border-box' }}/>
                          ) : (
                            <p style={{ fontSize: '9px', color: '#050505', lineHeight: 1.4, margin: 0, maxHeight: '64px', overflow: 'auto', whiteSpace: 'pre-wrap', wordBreak: 'break-word' }}>{variantText || '—'}</p>
                          )}
                        </div>
                        {/* Image — same for all 5 variants */}
                        <div style={{ width: '100%', aspectRatio: '4 / 5', position: 'relative', background: '#000', maxHeight: '38vh' }}>
                          {img?.previewUrl
                            ? <img src={img.previewUrl} alt="" style={{ position: 'absolute', inset: 0, width: '100%', height: '100%', objectFit: 'contain' }}/>
                            : <div style={{ position: 'absolute', inset: 0, background: img?.bg || 'linear-gradient(135deg,#1e1e2e,#2d1b69)' }}/>}
                        </div>
                        {/* Footer: title + description + CTA — editable inline */}
                        <div style={{ padding: '6px 8px', background: '#F0F2F5', display: 'flex', alignItems: 'center', gap: '5px' }}>
                          <div style={{ flex: 1, minWidth: 0 }}>
                            {isEditing ? (
                              <>
                                <input type="text" value={variantTitle} onChange={e => updateVariantField(i, v, 'titulo', e.target.value)} maxLength={40}
                                  placeholder={lang === 'en' ? 'Headline' : 'Título'}
                                  style={{ width: '100%', background: '#fff', border: `1px solid ${NX.accent}`, borderRadius: '4px', padding: '3px 5px', fontSize: '10px', color: '#050505', fontFamily: "'DM Sans',sans-serif", fontWeight: 700, outline: 'none', marginBottom: '3px', boxSizing: 'border-box' }}/>
                                <input type="text" value={variantDesc} onChange={e => updateVariantField(i, v, 'descripcion', e.target.value)} maxLength={30}
                                  placeholder={lang === 'en' ? 'Description' : 'Descripción'}
                                  style={{ width: '100%', background: '#fff', border: `1px solid ${NX.accent}`, borderRadius: '4px', padding: '3px 5px', fontSize: '8px', color: '#050505', fontFamily: "'DM Sans',sans-serif", outline: 'none', boxSizing: 'border-box' }}/>
                              </>
                            ) : (
                              <>
                                <div style={{ fontSize: '10px', fontWeight: 700, color: '#050505', lineHeight: 1.2, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{variantTitle || '—'}</div>
                                {variantDesc && (
                                  <div style={{ fontSize: '8px', color: '#65676B', lineHeight: 1.25, marginTop: '1px', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{variantDesc}</div>
                                )}
                              </>
                            )}
                          </div>
                          <div style={{ background: '#E4E6EB', color: '#050505', fontSize: '8px', fontWeight: 700, padding: '4px 6px', borderRadius: '3px', whiteSpace: 'nowrap', flexShrink: 0 }}>
                            {ctaLabel(ctaButton)}
                          </div>
                        </div>
                        {/* Approve / 👎 No me gusta (regenerar) buttons */}
                        <div style={{ display: 'flex', borderTop: `1px solid ${NX.border}` }}>
                          <button onClick={toggleVar}
                            style={{ flex: 1, padding: '6px', background: approved ? 'rgba(52,211,153,0.12)' : 'transparent', border: 'none', borderRight: `1px solid ${NX.border}`, cursor: 'pointer', color: approved ? NX.success : NX.muted, fontSize: '10px', fontWeight: 600, fontFamily: "'DM Sans',sans-serif", display: 'flex', alignItems: 'center', justifyContent: 'center', gap: '4px' }}>
                            <span style={{ fontSize: '11px' }}>👍</span>
                            {lang === 'en' ? (approved ? 'Approved' : 'Approve') : (approved ? 'Aprobada' : 'Aprobar')}
                          </button>
                          <button onClick={() => regenerateVariant(i, v)}
                            disabled={isRegen}
                            title={lang === 'en' ? 'Generate a new text for this variant' : 'Generar un nuevo texto para esta variante'}
                            style={{ flex: 1, padding: '6px', background: 'transparent', border: 'none', cursor: isRegen ? 'wait' : 'pointer', color: NX.muted, fontSize: '10px', fontWeight: 600, fontFamily: "'DM Sans',sans-serif", display: 'flex', alignItems: 'center', justifyContent: 'center', gap: '4px', transition: 'color 0.15s, background 0.15s' }}
                            onMouseEnter={e => { if (!isRegen) { e.currentTarget.style.color = NX.danger; e.currentTarget.style.background = 'rgba(248,113,113,0.08)'; } }}
                            onMouseLeave={e => { if (!isRegen) { e.currentTarget.style.color = NX.muted; e.currentTarget.style.background = 'transparent'; } }}>
                            <span style={{ fontSize: '11px' }}>👎</span>
                            {lang === 'en' ? 'Regen' : 'Regenerar'}
                          </button>
                        </div>
                      </div>
                    );
                  })}
                </div>
              </div>
            );
          })()}

          {/* Grid paginado SINGLE-TEXT — DESHABILITADO tras volver a 5 variantes por imagen.
              El bloque de arriba (paginado por imagen con 5 variantes) ya cubre el flujo. */}
          {false && (() => {
            const PAGE_SIZE = 5;
            const totalPages = Math.max(1, Math.ceil(generatedCopies.length / PAGE_SIZE));
            const safePage = Math.min(currentImgPage, totalPages - 1);
            const startIdx = safePage * PAGE_SIZE;
            const endIdx = Math.min(startIdx + PAGE_SIZE, generatedCopies.length);
            const pageItems = generatedCopies.slice(startIdx, endIdx);
            return (
              <div>
                <div style={{
                  display: 'grid',
                  gridTemplateColumns: `repeat(${Math.min(pageItems.length, PAGE_SIZE)}, minmax(0, 1fr))`,
                  gap: '10px',
                  // Con 1 sola imagen el grid de 1 columna ocupaba el modal completo (~960px) y la
                  // tarjeta con aspect-ratio 4/5 renderizaba la imagen a ~1200px de alto, saliéndose
                  // del viewport. Cap el ancho para que cards individuales tengan tamaño razonable
                  // y queden centradas; con 2+ imágenes mantiene el comportamiento anterior.
                  maxWidth: pageItems.length === 1 ? '320px' : pageItems.length === 2 ? '640px' : '100%',
                  margin: pageItems.length < 3 ? '0 auto' : '0',
                }}>
                  {pageItems.map((rawC, pageI) => {
                    const i = startIdx + pageI; // índice GLOBAL (no de la página)
                    return ((rawC, i) => {
                // Defensive: if Claude returns fewer/malformed items, keep rendering with an empty
                // shell instead of crashing the whole flow (used to cause a black screen).
                const c = (rawC && typeof rawC === 'object') ? rawC : { texto: '', titulo: '', angulo: 'Variante', emojis: false };
                const approved = approvedCopies.has(i);
                const img = generatedImages.filter(x => !x.isOriginal)[i] || generatedImages[i];
                const isEditing = editingCopyIdx === i;
                const titulo = campaignTitulos[i] || c.titulo || '';
                const descripcion = campaignDescs[i] || '';
                const isRegenerating = regeneratingCopyIdx.has(i);
                return (
                  <div key={i} style={{
                    position: 'relative',
                    borderRadius: '12px',
                    border: `2px solid ${isRegenerating ? NX.accent : (approved ? NX.success : 'rgba(248,113,113,0.3)')}`,
                    background: NX.bg3,
                    overflow: 'hidden',
                    display: 'flex', flexDirection: 'column',
                    transition: 'border-color 0.2s',
                    boxShadow: isRegenerating ? '0 0 24px rgba(139,92,246,0.35)' : 'none',
                  }}>
                    {/* Loader overlay — covers the card while the single copy is being
                        regenerated. Shimmer bar + status label, no spinner so the action
                        feels lightweight. */}
                    {isRegenerating && (
                      <div style={{ position: 'absolute', inset: 0, background: 'rgba(8,8,10,0.86)', backdropFilter: 'blur(3px)', display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', gap: '12px', zIndex: 6 }}>
                        <svg width="22" height="22" viewBox="0 0 24 24" fill="none" style={{ animation: 'spin 1s linear infinite' }}>
                          <circle cx="12" cy="12" r="9" stroke={NX.accent} strokeWidth="2.5" strokeLinecap="round" strokeDasharray="14 40"/>
                        </svg>
                        <div style={{ width: '70%', height: '4px', borderRadius: '2px', background: 'linear-gradient(90deg, rgba(139,92,246,0.06), rgba(139,92,246,0.55), rgba(139,92,246,0.06))', backgroundSize: '200% 100%', animation: 'nxShimmer 1.4s linear infinite' }}/>
                        <div style={{ fontSize: '10px', color: NX.accent, fontFamily: "'DM Mono',monospace", letterSpacing: '0.14em', textTransform: 'uppercase' }}>
                          {lang === 'en' ? 'Regenerating text…' : 'Regenerando texto…'}
                        </div>
                      </div>
                    )}
                    {/* Header: angle + edit toggle */}
                    <div style={{ padding: '7px 10px', background: NX.bg4, borderBottom: `1px solid ${NX.border}`, display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
                      <span style={{ fontSize: '9px', fontFamily: "'DM Mono',monospace", color: NX.accent, fontWeight: 700, letterSpacing: '0.08em' }}>{(c.angulo || `VARIANTE ${i + 1}`).toUpperCase()}</span>
                      <button onClick={() => setEditingCopyIdx(isEditing ? null : i)}
                        title={lang === 'en' ? 'Edit the ad text (primary text, headline, description). The text inside the image cannot be edited.' : 'Editar el texto del anuncio (texto principal, título, descripción). El texto dentro de la imagen no se puede editar.'}
                        style={{ background: 'transparent', border: 'none', cursor: 'pointer', color: isEditing ? NX.accent : NX.muted, padding: '2px 4px', display: 'flex', alignItems: 'center', gap: '3px', fontSize: '10px', fontFamily: "'DM Sans',sans-serif" }}>
                        <svg width="10" height="10" viewBox="0 0 16 16" fill="none"><path d="M3 13l1.5-4 7-7a1.5 1.5 0 012 2l-7 7-4 1.5z" stroke="currentColor" strokeWidth="1.5" strokeLinejoin="round"/></svg>
                        {isEditing ? (lang === 'en' ? 'Done' : 'Listo') : (lang === 'en' ? 'Edit text' : 'Editar texto')}
                      </button>
                    </div>

                    {/* FB-style primary text (arriba de la imagen) */}
                    <div style={{ padding: '8px 10px', background: '#fff', color: '#050505' }}>
                      {isEditing ? (
                        <textarea
                          value={c.texto || ''}
                          onChange={e => updateCopyField(i, 'texto', e.target.value)}
                          style={{ width: '100%', minHeight: '60px', background: '#F0F2F5', border: `1px solid ${NX.accent}`, borderRadius: '6px', padding: '6px 8px', fontSize: '10px', fontFamily: "'DM Sans',sans-serif", color: '#050505', resize: 'vertical', outline: 'none', boxSizing: 'border-box' }}/>
                      ) : (
                        <p style={{ fontSize: '10px', color: '#050505', lineHeight: 1.4, margin: 0, maxHeight: '54px', overflow: 'auto' }}>{c.texto || '—'}</p>
                      )}
                    </div>

                    {/* Image — NOT editable. Rendered by Gemini with the reference product. */}
                    <div style={{ width: '100%', aspectRatio: '4 / 5', position: 'relative', background: '#000' }}>
                      {img?.previewUrl
                        ? <img src={img.previewUrl} alt="" style={{ position: 'absolute', inset: 0, width: '100%', height: '100%', objectFit: 'contain' }}/>
                        : <div style={{ position: 'absolute', inset: 0, background: img?.bg || 'linear-gradient(135deg,#1e1e2e,#2d1b69)' }}/>}
                      {isEditing && (
                        <div style={{ position: 'absolute', top: '6px', left: '6px', right: '6px', background: 'rgba(0,0,0,0.72)', borderRadius: '6px', padding: '5px 8px', display: 'flex', alignItems: 'center', gap: '5px', backdropFilter: 'blur(4px)' }}>
                          <svg width="11" height="11" viewBox="0 0 16 16" fill="none" style={{ flexShrink: 0 }}>
                            <rect x="2" y="4" width="12" height="8" rx="1.5" stroke="#fbbf24" strokeWidth="1.3"/>
                            <path d="M5 7.5l2 2 4-4" stroke="#fbbf24" strokeWidth="1.3" strokeLinecap="round" strokeLinejoin="round"/>
                          </svg>
                          <span style={{ fontSize: '8px', color: 'rgba(255,255,255,0.92)', lineHeight: 1.3, fontFamily: "'DM Sans',sans-serif" }}>
                            {lang === 'en' ? 'Image not editable — rendered with AI' : 'Imagen no editable — renderizada con IA'}
                          </span>
                        </div>
                      )}
                    </div>

                    {/* FB-style link preview footer: anatomía REAL de Facebook
                        ┌───────────────────────────────────┬─────────┐
                        │ HOSTNAME (uppercase tiny gray)    │  [CTA]  │
                        │ Título (BOLD)                     │ button  │
                        │ Descripción gris debajo           │         │
                        └───────────────────────────────────┴─────────┘
                        Título y descripción a la izquierda, botón vertical-centered a la derecha. */}
                    <div style={{ padding: '7px 9px', background: '#F0F2F5', borderBottom: `1px solid ${NX.border}` }}>
                      {isEditing ? (
                        <>
                          {/* Title primero arriba (bold), luego descripción abajo en gris.
                              Mismo orden que la vista renderizada. */}
                          <input type="text" value={titulo} onChange={e => updateCopyField(i, 'titulo', e.target.value)} maxLength={40}
                            placeholder={lang === 'en' ? 'Headline (≤40 chars)' : 'Título (≤40 cars)'}
                            style={{ width: '100%', background: '#fff', border: `1px solid ${NX.accent}`, borderRadius: '5px', padding: '4px 6px', fontSize: '10px', color: '#050505', fontFamily: "'DM Sans',sans-serif", fontWeight: 600, outline: 'none', marginBottom: '4px', boxSizing: 'border-box' }}/>
                          <input type="text" value={descripcion} onChange={e => updateCopyField(i, 'descripcion', e.target.value)} maxLength={30}
                            placeholder={lang === 'en' ? 'Description (≤30 chars)' : 'Descripción (≤30 cars)'}
                            style={{ width: '100%', background: '#fff', border: `1px solid ${NX.accent}`, borderRadius: '5px', padding: '4px 6px', fontSize: '9px', color: '#050505', fontFamily: "'DM Sans',sans-serif", outline: 'none', boxSizing: 'border-box' }}/>
                        </>
                      ) : (
                        <div style={{ display: 'flex', alignItems: 'center', gap: '6px' }}>
                          <div style={{ flex: 1, minWidth: 0 }}>
                            {/* Hostname opcional (si tenemos URL del producto) */}
                            {productUrl && (
                              <div style={{ fontSize: '7px', color: '#65676B', textTransform: 'uppercase', letterSpacing: '0.05em', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', marginBottom: '1px' }}>
                                {productUrl.replace(/^https?:\/\//, '').split('/')[0]}
                              </div>
                            )}
                            {/* Título primero (bold) */}
                            <div style={{ fontSize: '11px', fontWeight: 700, color: '#050505', lineHeight: 1.2, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{titulo || '—'}</div>
                            {/* Descripción debajo del título en gris */}
                            {descripcion && (
                              <div style={{ fontSize: '8px', color: '#65676B', lineHeight: 1.25, marginTop: '1px', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{descripcion}</div>
                            )}
                          </div>
                          {/* Botón CTA al lado derecho del título */}
                          <div style={{ background: '#E4E6EB', color: '#050505', fontSize: '9px', fontWeight: 700, padding: '5px 8px', borderRadius: '4px', whiteSpace: 'nowrap', flexShrink: 0 }}>
                            {ctaLabel(ctaButton)}
                          </div>
                        </div>
                      )}
                    </div>

                    {/* Approve / Reject bottom bar.
                        - "Aprobar/Aprobada": toggles the approval flag (skip without regen).
                        - "Rechazar": triggers single-slot regeneration — shimmer overlays the
                          card, the copy is replaced when /api/generate-copies returns. */}
                    <div style={{ display: 'flex', borderTop: `1px solid ${NX.border}` }}>
                      <button onClick={() => !isRegenerating && toggleCopy(i)}
                        disabled={isRegenerating}
                        style={{ flex: 1, padding: '7px', background: approved ? 'rgba(52,211,153,0.12)' : 'transparent', border: 'none', borderRight: `1px solid ${NX.border}`, cursor: isRegenerating ? 'wait' : 'pointer', color: approved ? NX.success : NX.muted, fontSize: '10px', fontWeight: 600, fontFamily: "'DM Sans',sans-serif", display: 'flex', alignItems: 'center', justifyContent: 'center', gap: '4px' }}>
                        <svg width="10" height="10" viewBox="0 0 12 12" fill="none"><path d="M2 6l3 3 5-6" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/></svg>
                        {lang === 'en' ? (approved ? 'Approved' : 'Approve') : (approved ? 'Aprobada' : 'Aprobar')}
                      </button>
                      <button onClick={() => !isRegenerating && regenerateSingleCopy(i)}
                        disabled={isRegenerating}
                        title={lang === 'en' ? 'Reject this text and generate a new one' : 'Rechazar este texto y generar uno nuevo'}
                        style={{ flex: 1, padding: '7px', background: 'transparent', border: 'none', cursor: isRegenerating ? 'wait' : 'pointer', color: NX.muted, fontSize: '10px', fontWeight: 600, fontFamily: "'DM Sans',sans-serif", display: 'flex', alignItems: 'center', justifyContent: 'center', gap: '4px', transition: 'color 0.15s, background 0.15s' }}
                        onMouseEnter={e => { if (!isRegenerating) { e.currentTarget.style.color = NX.danger; e.currentTarget.style.background = 'rgba(248,113,113,0.08)'; } }}
                        onMouseLeave={e => { if (!isRegenerating) { e.currentTarget.style.color = NX.muted; e.currentTarget.style.background = 'transparent'; } }}>
                        {isRegenerating
                          ? <><svg width="10" height="10" viewBox="0 0 24 24" fill="none" style={{ animation: 'spin 1s linear infinite' }}><circle cx="12" cy="12" r="9" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeDasharray="14 40"/></svg> {lang === 'en' ? 'Regenerating…' : 'Regenerando…'}</>
                          : <><svg width="10" height="10" viewBox="0 0 16 16" fill="none"><path d="M13 8a5 5 0 11-1.46-3.54M13 3v3.5h-3.5" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round" strokeLinejoin="round"/></svg> {lang === 'en' ? 'Reject & regen' : 'Rechazar'}</>}
                      </button>
                    </div>
                  </div>
                );
              })(rawC, i);
                  })}
                </div>
                {/* Paginación: solo si hay más de PAGE_SIZE imágenes */}
                {totalPages > 1 && (
                  <div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', gap: '10px', marginTop: '14px' }}>
                    <button onClick={() => setCurrentImgPage(Math.max(0, safePage - 1))}
                      disabled={safePage === 0}
                      style={{ padding: '6px 12px', background: safePage === 0 ? NX.bg3 : NX.bg4, color: safePage === 0 ? NX.muted : NX.text, border: `1px solid ${NX.border}`, borderRadius: '8px', cursor: safePage === 0 ? 'not-allowed' : 'pointer', fontSize: '12px', fontFamily: "'DM Sans',sans-serif", fontWeight: 600 }}>
                      ← {lang === 'en' ? 'Previous 5' : 'Anteriores 5'}
                    </button>
                    <div style={{ display: 'flex', gap: '6px' }}>
                      {Array.from({ length: totalPages }, (_, p) => (
                        <button key={p} onClick={() => setCurrentImgPage(p)}
                          style={{ width: p === safePage ? '24px' : '10px', height: '10px', borderRadius: '5px', background: p === safePage ? NX.accent : NX.bg4, border: `1px solid ${p === safePage ? NX.accent : NX.border}`, cursor: 'pointer', padding: 0, transition: 'all 0.15s' }}/>
                      ))}
                    </div>
                    <button onClick={() => setCurrentImgPage(Math.min(totalPages - 1, safePage + 1))}
                      disabled={safePage >= totalPages - 1}
                      style={{ padding: '6px 12px', background: safePage >= totalPages - 1 ? NX.bg3 : NX.bg4, color: safePage >= totalPages - 1 ? NX.muted : NX.text, border: `1px solid ${NX.border}`, borderRadius: '8px', cursor: safePage >= totalPages - 1 ? 'not-allowed' : 'pointer', fontSize: '12px', fontFamily: "'DM Sans',sans-serif", fontWeight: 600 }}>
                      {lang === 'en' ? `Next ${Math.min(PAGE_SIZE, generatedCopies.length - (safePage + 1) * PAGE_SIZE)}` : `Siguientes ${Math.min(PAGE_SIZE, generatedCopies.length - (safePage + 1) * PAGE_SIZE)}`} →
                    </button>
                  </div>
                )}
              </div>
            );
          })()}

          {/* Step 4 footer: atrás + (cuando hay >1 imagen) aprobar imagen y siguiente + ver preview */}
          {(() => {
            const totalImgs = generatedCopies.length;
            const safeI = Math.min(currentImgPage, Math.max(0, totalImgs - 1));
            const isLastImage = safeI >= totalImgs - 1;
            const hasMultipleImgs = totalImgs > 1;
            // "Aprobar imagen actual y siguiente": aprueba TODAS las variantes de la imagen
            // actual + avanza al próximo índice. Útil cuando el user quiere ir imagen por imagen
            // sin clickear cada variante. En la última imagen, este botón se reemplaza por
            // el de "Ver preview (aprobar todas)".
            const approveCurrentAndNext = () => {
              const curImg = generatedCopies[safeI];
              const variantCount = Array.isArray(curImg?.pool?.primaryTexts)
                ? Math.min(curImg.pool.primaryTexts.length, 5)
                : 1;
              // Aprobar todas las variantes de la imagen actual
              setApprovedVariants(prev => {
                const next = new Set(prev);
                for (let v = 0; v < variantCount; v++) next.add(`${safeI}:${v}`);
                return next;
              });
              setApprovedCopies(prev => new Set([...prev, safeI]));
              // Avanzar a la próxima imagen
              setCurrentImgPage(safeI + 1);
            };
            return (
              <div style={{ display: 'flex', gap: '10px', flexWrap: 'wrap', marginTop: '14px' }}>
                <NxButton variant="ghost" onClick={() => setStep(3)} full>{ts.back}</NxButton>
                {hasMultipleImgs && !isLastImage && (
                  <NxButton variant="accent" onClick={approveCurrentAndNext} full>
                    {lang === 'en'
                      ? `✓ Approve image ${safeI + 1} & next →`
                      : `✓ Aprobar imagen ${safeI + 1} y siguiente →`}
                  </NxButton>
                )}
                <NxButton variant="accent" onClick={goToPreviewApprovingAll} full>
                  {lang === 'en'
                    ? (hasMultipleImgs && isLastImage ? '✓ Approve all & view preview' : '✓ View ad preview (approve all)')
                    : (hasMultipleImgs && isLastImage ? '✓ Aprobar todas y ver preview' : '✓ Ver preview del anuncio (aprobar todas)')}
                </NxButton>
              </div>
            );
          })()}
        </>}

        {step === 5 && <>
          {/* Constrain step 5 to a narrower centered column so the confirm screen reads as
              a square card instead of a stretched landscape band. Assistant variant uses a
              tighter cap (640) since it has fewer panels; advanced uses 760 to fit the
              FB/IG mockup tabs + CTA picker comfortably. */}
          <div style={{ maxWidth: assistantPrefill ? '640px' : '760px', margin: '0 auto', width: '100%' }}>
          {/* Header row with title + strategy details button. paddingRight reserves space for the
              modal's absolute close button (×) at top-right so they don't overlap.
              When entered via the assistant we show a simplified header — the user already
              reviewed images + copies; this screen is purely a confirmation gate. */}
          <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', gap: '12px', marginBottom: '10px', flexWrap: 'wrap', paddingRight: '48px' }}>
            <div>
              <h2 style={{ fontSize: '15px', fontWeight: 500, color: NX.text, margin: '0 0 2px' }}>
                {assistantPrefill ? (lang === 'en' ? 'Confirm and launch' : 'Confirma y lanza') : ts.steps[5].title}
              </h2>
              <p style={{ color: NX.muted, fontSize: '12px', margin: 0 }}>
                {assistantPrefill ? (lang === 'en' ? 'Review your investment and publish mode, then create the strategy and launch.' : 'Revisa tu inversión y modo de publicación, luego crea la estrategia y lanza.') : ts.steps[5].subtitle}
              </p>
            </div>
            {!assistantPrefill && (
              <button onClick={() => { setStrategyOpen(true); if (!strategy && !strategyLoading) generateStrategy(); }} disabled={strategyLoading && !strategy}
                style={{ flexShrink: 0, padding: '8px 14px', background: 'linear-gradient(135deg, #8b5cf6, #a78bfa)', border: 'none', borderRadius: '10px', color: '#fff', fontSize: '12px', fontWeight: 600, cursor: strategyLoading && !strategy ? 'wait' : 'pointer', fontFamily: "'DM Sans',sans-serif", display: 'flex', alignItems: 'center', gap: '6px', boxShadow: '0 0 16px rgba(139,92,246,0.3)' }}>
                {strategyLoading && !strategy
                  ? <><span style={{ display: 'inline-block', width: '10px', height: '10px', border: '1.5px solid #fff', borderTopColor: 'transparent', borderRadius: '50%', animation: 'spin 0.7s linear infinite' }}/> {lang === 'en' ? 'Preparing…' : 'Preparando…'}</>
                  : <><svg width="13" height="13" viewBox="0 0 24 24" fill="none"><path d="M9 11l3-8L22 9l-6 3 2 10-8-5-9 4 8-10z" fill="#fff"/></svg>{lang === 'en' ? 'Strategy details' : 'Ver detalles de estrategia'}</>}
              </button>
            )}
          </div>

          {/* ── ALL APPROVED CREATIVES AS MINI FB + IG MOCKUPS — hidden for assistant flow ── */}
          {!assistantPrefill && (() => {
            // Collect all approved creatives (AI images selected by user, excluding the original reference)
            const approvedImgs = selectedImgs
              .map(i => generatedImages[i])
              .filter(img => img && !img.isOriginal && (img.cloudinaryUrl || img.previewUrl));
            const creatives = approvedImgs.length > 0 ? approvedImgs : (firstSelectedImg ? [firstSelectedImg] : []);
            const pageHandle = (metaAssets?.page_name || 'nexwallcorp').toLowerCase().replace(/\s+/g, '');
            const pageName = metaAssets?.page_name || 'NexWall Corp AI';
            const pageInitials = (metaAssets?.page_name || 'NW').slice(0, 2).toUpperCase();
            // Per-creative copy resolution. Each ad gets its OWN texto/titulo/descripcion
            // from generatedCopies[i] / campaignTitulos[i] / campaignDescs[i] (which now hold
            // per-image pools — variant 0 of each pool is used as the preview). Falls back to
            // firstApprovedCopy when index data is missing (legacy / single-copy case).
            const copyForIndex = (i) => {
              const c = generatedCopies[i] || generatedCopies[0] || firstApprovedCopy || {};
              const pool = c?.pool || {};
              return {
                primaryText: c.texto || (pool.primaryTexts && (typeof pool.primaryTexts[0] === 'string' ? pool.primaryTexts[0] : pool.primaryTexts[0]?.texto)) || firstApprovedCopy.texto || '',
                titulo:      campaignTitulos[i] || (pool.titulos && pool.titulos[0]) || c.titulo || firstApprovedCopy.titulo || '',
                descripcion: campaignDescs[i]   || (pool.descripciones && pool.descripciones[0]) || c.descripcion || firstApprovedCopy.descripcion || '',
              };
            };
            const urlHost = (productUrl && productUrl.replace(/^https?:\/\//, '').split('/')[0]) || 'nexwallcorp.com';

            const renderFacebookMini = (img, i) => {
              const { primaryText: ptI, titulo: tlI, descripcion: dcI } = copyForIndex(i);
              return (
              <div key={`fb-${i}`} style={{ background: '#fff', borderRadius: '8px', overflow: 'hidden', border: `1px solid ${NX.border}`, color: '#050505', fontFamily: 'system-ui, -apple-system, sans-serif', display: 'flex', flexDirection: 'column' }}>
                {/* Top bar */}
                <div style={{ padding: '6px 8px', display: 'flex', alignItems: 'center', gap: '6px', borderBottom: '1px solid #E4E6EB' }}>
                  <div style={{ width: '20px', height: '20px', borderRadius: '50%', background: 'linear-gradient(135deg, #8b5cf6, #a78bfa)', display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: '8px', fontWeight: 700, color: '#fff', flexShrink: 0 }}>{pageInitials}</div>
                  <div style={{ flex: 1, minWidth: 0 }}>
                    <div style={{ fontSize: '9px', fontWeight: 600, color: '#050505', lineHeight: 1.1, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{pageName}</div>
                    <div style={{ fontSize: '8px', color: '#65676B' }}>{language === 'en' ? 'Sponsored' : 'Publicidad'}</div>
                  </div>
                </div>
                {/* Primary text — el copy del producto, multi-line con "...Ver más" si excede */}
                {ptI && (
                  <div style={{ padding: '6px 8px', fontSize: '9px', color: '#050505', lineHeight: 1.4, maxHeight: '54px', overflow: 'hidden', whiteSpace: 'pre-wrap', wordBreak: 'break-word' }}>{ptI}</div>
                )}
                {/* Image */}
                <div style={{ width: '100%', aspectRatio: '4 / 5', position: 'relative', background: '#000' }}>
                  <img src={img.previewUrl || img.cloudinaryUrl} alt="" style={{ position: 'absolute', inset: 0, width: '100%', height: '100%', objectFit: 'contain' }}/>
                </div>
                {/* Link preview footer — TÍTULO grande+bold + DESCRIPCIÓN gris debajo + CTA a la derecha */}
                <div style={{ padding: '7px 8px', background: '#F0F2F5', display: 'flex', alignItems: 'center', gap: '6px' }}>
                  <div style={{ flex: 1, minWidth: 0 }}>
                    {urlHost && <div style={{ fontSize: '7px', color: '#65676B', textTransform: 'uppercase', letterSpacing: '0.04em', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{urlHost}</div>}
                    {tlI && <div style={{ fontSize: '11px', fontWeight: 700, color: '#050505', lineHeight: 1.2, marginTop: '1px', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{tlI}</div>}
                    {dcI && <div style={{ fontSize: '8px', color: '#65676B', lineHeight: 1.25, marginTop: '1px', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{dcI}</div>}
                  </div>
                  <button onClick={() => setCtaPickerOpen(true)} title={lang === 'en' ? 'Click to change' : 'Click para cambiar'}
                    style={{ background: '#E4E6EB', color: '#050505', fontSize: '9px', fontWeight: 700, padding: '5px 8px', borderRadius: '4px', whiteSpace: 'nowrap', flexShrink: 0, border: '1px dashed transparent', cursor: 'pointer', fontFamily: 'inherit' }}
                    onMouseEnter={e => e.currentTarget.style.borderColor = NX.accent}
                    onMouseLeave={e => e.currentTarget.style.borderColor = 'transparent'}>
                    {ctaLabel(ctaButton)}
                  </button>
                </div>
              </div>
              );
            };

            const renderInstagramMini = (img, i) => {
              const { primaryText: ptI } = copyForIndex(i);
              return (
              <div key={`ig-${i}`} style={{ background: '#fff', borderRadius: '8px', overflow: 'hidden', border: `1px solid ${NX.border}`, color: '#050505', fontFamily: 'system-ui, -apple-system, sans-serif', display: 'flex', flexDirection: 'column' }}>
                {/* Top bar */}
                <div style={{ padding: '6px 8px', display: 'flex', alignItems: 'center', gap: '6px', borderBottom: '1px solid #DBDBDB' }}>
                  <div style={{ width: '20px', height: '20px', borderRadius: '50%', padding: '1px', background: 'linear-gradient(135deg,#F58529,#DD2A7B,#8134AF)', flexShrink: 0 }}>
                    <div style={{ width: '100%', height: '100%', borderRadius: '50%', background: '#fff', display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: '7px', fontWeight: 700, color: '#050505' }}>{pageInitials}</div>
                  </div>
                  <div style={{ flex: 1, minWidth: 0 }}>
                    <div style={{ fontSize: '9px', fontWeight: 600, color: '#050505', lineHeight: 1.1, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{pageHandle}</div>
                    <div style={{ fontSize: '8px', color: '#65676B' }}>{language === 'en' ? 'Sponsored' : 'Publicidad'}</div>
                  </div>
                </div>
                {/* Image */}
                <div style={{ width: '100%', aspectRatio: '4 / 5', position: 'relative', background: '#000' }}>
                  <img src={img.previewUrl || img.cloudinaryUrl} alt="" style={{ position: 'absolute', inset: 0, width: '100%', height: '100%', objectFit: 'contain' }}/>
                </div>
                {/* CTA strip — flecha derecha estilo Instagram */}
                <button onClick={() => setCtaPickerOpen(true)} title={lang === 'en' ? 'Click to change' : 'Click para cambiar'}
                  style={{ width: '100%', padding: '6px 8px', background: '#FAFAFA', border: 'none', borderBottom: '1px dashed transparent', display: 'flex', alignItems: 'center', justifyContent: 'space-between', cursor: 'pointer', fontFamily: 'inherit', textAlign: 'left' }}
                  onMouseEnter={e => e.currentTarget.style.borderBottomColor = NX.accent}
                  onMouseLeave={e => e.currentTarget.style.borderBottomColor = 'transparent'}>
                  <div style={{ fontSize: '9px', fontWeight: 600, color: '#050505', flex: 1, minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{ctaLabel(ctaButton)}</div>
                  <svg width="9" height="9" viewBox="0 0 24 24" fill="none" stroke="#050505" strokeWidth="2.5"><path d="M9 5l7 7-7 7"/></svg>
                </button>
                {/* Caption — primary text per imagen */}
                <div style={{ padding: '4px 8px 7px', fontSize: '9px', color: '#050505', lineHeight: 1.35, maxHeight: '38px', overflow: 'hidden', whiteSpace: 'pre-wrap', wordBreak: 'break-word' }}>
                  <span style={{ fontWeight: 600 }}>{pageHandle}</span>{' '}
                  {ptI}
                </div>
              </div>
              );
            };

            const cols = Math.min(creatives.length || 1, 5);
            const gridTemplate = `repeat(${cols}, minmax(0, 1fr))`;

            if (creatives.length === 0) {
              return (
                <div style={{ padding: '30px', textAlign: 'center', color: NX.muted, fontSize: '13px', background: NX.bg3, borderRadius: '12px', marginBottom: '14px' }}>
                  {lang === 'en' ? 'No creatives selected yet. Approve some in the previous step.' : 'Aún no hay creativos aprobados. Vuelve al paso anterior y aprueba algunos.'}
                </div>
              );
            }

            const FB_COLOR = '#1877F2';
            const IG_GRADIENT = 'linear-gradient(135deg, #F58529 0%, #DD2A7B 50%, #8134AF 100%)';
            const isFb = previewPlatform === 'facebook';

            // Phase 3 UX: instead of rendering the full preview grid inline (which pushes the budget
            // + launch config off-screen), show compact "Ver en Facebook" / "Ver en Instagram" buttons.
            // Clicking one opens a dedicated popup that contains the full-size preview of all creatives
            // for that platform. The final launch config stays visible without scroll.
            return (
              <div style={{ display: 'flex', flexDirection: 'column', gap: '10px', marginBottom: '10px' }}>
                <div style={{ fontSize: '10px', color: NX.muted, fontFamily: "'DM Mono',monospace", letterSpacing: '0.1em', textTransform: 'uppercase' }}>
                  {lang === 'en' ? 'Preview' : 'Vista previa'} · {creatives.length} {creatives.length === 1 ? (lang === 'en' ? 'creative' : 'creativo') : (lang === 'en' ? 'creatives' : 'creativos')}
                </div>
                <div style={{ display: 'flex', gap: '10px', flexWrap: 'wrap' }}>
                  <button
                    onClick={() => { setPreviewPlatform('facebook'); setPreviewPopupOpen(true); }}
                    style={{
                      flex: '1 1 200px', padding: '12px 16px', borderRadius: '12px',
                      border: `1px solid ${FB_COLOR}`,
                      background: 'rgba(24,119,242,0.08)', color: NX.text, cursor: 'pointer',
                      fontFamily: "'DM Sans',sans-serif", fontSize: '13px', fontWeight: 600,
                      display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: '10px',
                      transition: 'all 0.15s',
                    }}
                    onMouseEnter={e => { e.currentTarget.style.background = 'rgba(24,119,242,0.16)'; }}
                    onMouseLeave={e => { e.currentTarget.style.background = 'rgba(24,119,242,0.08)'; }}>
                    <span style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
                      <svg width="16" height="16" viewBox="0 0 24 24" fill={FB_COLOR}><path d="M24 12.073c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.99 4.388 10.954 10.125 11.854v-8.385H7.078v-3.47h3.047V9.43c0-3.007 1.792-4.669 4.533-4.669 1.312 0 2.686.235 2.686.235v2.953H15.83c-1.491 0-1.956.925-1.956 1.874v2.25h3.328l-.532 3.47h-2.796v8.385C19.612 23.027 24 18.062 24 12.073z"/></svg>
                      Facebook
                    </span>
                    <span style={{ display: 'flex', alignItems: 'center', gap: '6px', fontSize: '11px', color: FB_COLOR, fontWeight: 500 }}>
                      <svg width="12" height="12" viewBox="0 0 16 16" fill="none"><path d="M1 8s2.5-5 7-5 7 5 7 5-2.5 5-7 5-7-5-7-5z" stroke={FB_COLOR} strokeWidth="1.3"/><circle cx="8" cy="8" r="2" stroke={FB_COLOR} strokeWidth="1.3"/></svg>
                      {lang === 'en' ? 'View' : 'Ver'}
                    </span>
                  </button>
                  <button
                    onClick={() => { setPreviewPlatform('instagram'); setPreviewPopupOpen(true); }}
                    style={{
                      flex: '1 1 200px', padding: '12px 16px', borderRadius: '12px',
                      border: 'none',
                      background: IG_GRADIENT,
                      color: '#fff', cursor: 'pointer',
                      fontFamily: "'DM Sans',sans-serif", fontSize: '13px', fontWeight: 700,
                      display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: '10px',
                      transition: 'all 0.15s',
                      boxShadow: '0 4px 14px rgba(221,42,123,0.25)',
                    }}
                    onMouseEnter={e => { e.currentTarget.style.boxShadow = '0 6px 20px rgba(221,42,123,0.5)'; e.currentTarget.style.transform = 'translateY(-1px)'; }}
                    onMouseLeave={e => { e.currentTarget.style.boxShadow = '0 4px 14px rgba(221,42,123,0.25)'; e.currentTarget.style.transform = 'translateY(0)'; }}>
                    <span style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
                      {/* White Instagram icon on gradient bg — solid white for clean contrast */}
                      <svg width="16" height="16" viewBox="0 0 24 24" fill="#fff"><path d="M12 2.163c3.204 0 3.584.012 4.85.07 3.252.148 4.771 1.691 4.919 4.919.058 1.265.069 1.645.069 4.849 0 3.205-.012 3.584-.069 4.849-.149 3.225-1.664 4.771-4.919 4.919-1.266.058-1.644.07-4.85.07-3.204 0-3.584-.012-4.849-.07-3.26-.149-4.771-1.699-4.919-4.92-.058-1.265-.07-1.644-.07-4.849 0-3.204.013-3.583.07-4.849.149-3.227 1.664-4.771 4.919-4.919 1.266-.057 1.645-.069 4.849-.069zm0-2.163c-3.259 0-3.667.014-4.947.072-4.358.2-6.78 2.618-6.98 6.98-.059 1.281-.073 1.689-.073 4.948 0 3.259.014 3.668.072 4.948.2 4.358 2.618 6.78 6.98 6.98 1.281.058 1.689.072 4.948.072 3.259 0 3.668-.014 4.948-.072 4.354-.2 6.782-2.618 6.979-6.98.059-1.28.073-1.689.073-4.948 0-3.259-.014-3.667-.072-4.947-.196-4.354-2.617-6.78-6.979-6.98-1.281-.059-1.69-.073-4.949-.073zm0 5.838c-3.403 0-6.162 2.759-6.162 6.162s2.759 6.163 6.162 6.163 6.162-2.759 6.162-6.163c0-3.403-2.759-6.162-6.162-6.162zm0 10.162c-2.209 0-4-1.79-4-4 0-2.209 1.791-4 4-4s4 1.791 4 4c0 2.21-1.791 4-4 4zm6.406-11.845c-.796 0-1.441.645-1.441 1.44s.645 1.44 1.441 1.44c.795 0 1.439-.645 1.439-1.44s-.644-1.44-1.439-1.44z"/></svg>
                      <span style={{ color: '#fff', textShadow: '0 1px 2px rgba(0,0,0,0.2)' }}>Instagram</span>
                    </span>
                    <span style={{ display: 'flex', alignItems: 'center', gap: '6px', fontSize: '11px', fontWeight: 600, color: '#fff', background: 'rgba(0,0,0,0.18)', borderRadius: '999px', padding: '3px 10px' }}>
                      <svg width="12" height="12" viewBox="0 0 16 16" fill="none"><path d="M1 8s2.5-5 7-5 7 5 7 5-2.5 5-7 5-7-5-7-5z" stroke="#fff" strokeWidth="1.4"/><circle cx="8" cy="8" r="2" stroke="#fff" strokeWidth="1.4"/></svg>
                      {lang === 'en' ? 'View' : 'Ver'}
                    </span>
                  </button>
                </div>

                {/* Preview popup — opens when user clicks "Ver" on either platform button */}
                {previewPopupOpen && (
                  <div onClick={() => setPreviewPopupOpen(false)}
                    style={{ position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.82)', backdropFilter: 'blur(10px)', zIndex: 2000, display: 'flex', alignItems: 'center', justifyContent: 'center', padding: '24px' }}>
                    <div onClick={e => e.stopPropagation()}
                      style={{
                        background: NX.bg2, border: `1px solid ${NX.border2}`, borderRadius: '20px',
                        maxWidth: '1280px', width: '100%', maxHeight: '92vh', overflow: 'auto',
                        padding: '24px', boxShadow: '0 30px 80px rgba(10,6,16,0.8)',
                        animation: 'modalIn 0.2s ease',
                      }}>
                      {/* Header with tabs FB / IG + close */}
                      <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: '12px', marginBottom: '18px' }}>
                        <div style={{ display: 'flex', gap: '8px', padding: '4px', background: NX.bg3, border: `1px solid ${NX.border}`, borderRadius: '100px' }}>
                          <button onClick={() => setPreviewPlatform('facebook')}
                            style={{
                              padding: '7px 16px', borderRadius: '100px', border: 'none', cursor: 'pointer',
                              background: isFb ? FB_COLOR : 'transparent',
                              color: isFb ? '#fff' : NX.muted,
                              fontSize: '12px', fontWeight: 600, fontFamily: "'DM Sans',sans-serif",
                              display: 'flex', alignItems: 'center', gap: '6px', transition: 'all 0.2s',
                            }}>
                            <svg width="13" height="13" viewBox="0 0 24 24" fill={isFb ? '#fff' : NX.muted}><path d="M24 12.073c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.99 4.388 10.954 10.125 11.854v-8.385H7.078v-3.47h3.047V9.43c0-3.007 1.792-4.669 4.533-4.669 1.312 0 2.686.235 2.686.235v2.953H15.83c-1.491 0-1.956.925-1.956 1.874v2.25h3.328l-.532 3.47h-2.796v8.385C19.612 23.027 24 18.062 24 12.073z"/></svg>
                            Facebook
                          </button>
                          <button onClick={() => setPreviewPlatform('instagram')}
                            style={{
                              padding: '7px 16px', borderRadius: '100px', border: 'none', cursor: 'pointer',
                              background: !isFb ? IG_GRADIENT : 'transparent',
                              color: !isFb ? '#fff' : NX.muted,
                              fontSize: '12px', fontWeight: 600, fontFamily: "'DM Sans',sans-serif",
                              display: 'flex', alignItems: 'center', gap: '6px', transition: 'all 0.2s',
                            }}>
                            <svg width="13" height="13" viewBox="0 0 24 24" fill={!isFb ? '#fff' : NX.muted}><path d="M12 2.163c3.204 0 3.584.012 4.85.07 3.252.148 4.771 1.691 4.919 4.919.058 1.265.069 1.645.069 4.849 0 3.205-.012 3.584-.069 4.849-.149 3.225-1.664 4.771-4.919 4.919-1.266.058-1.644.07-4.85.07-3.204 0-3.584-.012-4.849-.07-3.26-.149-4.771-1.699-4.919-4.92-.058-1.265-.07-1.644-.07-4.849 0-3.204.013-3.583.07-4.849.149-3.227 1.664-4.771 4.919-4.919 1.266-.057 1.645-.069 4.849-.069zm0-2.163c-3.259 0-3.667.014-4.947.072-4.358.2-6.78 2.618-6.98 6.98-.059 1.281-.073 1.689-.073 4.948 0 3.259.014 3.668.072 4.948.2 4.358 2.618 6.78 6.98 6.98 1.281.058 1.689.072 4.948.072 3.259 0 3.668-.014 4.948-.072 4.354-.2 6.782-2.618 6.979-6.98.059-1.28.073-1.689.073-4.948 0-3.259-.014-3.667-.072-4.947-.196-4.354-2.617-6.78-6.979-6.98-1.281-.059-1.69-.073-4.949-.073zm0 5.838c-3.403 0-6.162 2.759-6.162 6.162s2.759 6.163 6.162 6.163 6.162-2.759 6.162-6.163c0-3.403-2.759-6.162-6.162-6.162zm0 10.162c-2.209 0-4-1.79-4-4 0-2.209 1.791-4 4-4s4 1.791 4 4c0 2.21-1.791 4-4 4zm6.406-11.845c-.796 0-1.441.645-1.441 1.44s.645 1.44 1.441 1.44c.795 0 1.439-.645 1.439-1.44s-.644-1.44-1.439-1.44z"/></svg>
                            Instagram
                          </button>
                        </div>
                        <button onClick={() => setPreviewPopupOpen(false)}
                          aria-label={lang === 'en' ? 'Close' : 'Cerrar'}
                          style={{ width: '36px', height: '36px', borderRadius: '50%', background: NX.bg3, border: `1px solid ${NX.border}`, color: NX.text, fontSize: '16px', cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>×</button>
                      </div>

                      {/* Full-size creatives grid inside the popup.
                          Layout: fixed 5 columns; wraps to 2 rows for 10 creatives. No scroll —
                          cards resize via aspect-ratio so everything fits on the same screen. */}
                      <div style={{
                        padding: '16px',
                        background: isFb ? 'rgba(24,119,242,0.05)' : 'rgba(221,42,123,0.05)',
                        border: isFb ? `1.5px solid ${FB_COLOR}` : '1.5px solid transparent',
                        borderRadius: '14px',
                        ...(isFb ? {} : { backgroundImage: `${IG_GRADIENT}, linear-gradient(#0d0d0d, #0d0d0d)`, backgroundOrigin: 'border-box', backgroundClip: 'border-box, padding-box' }),
                      }}>
                        <div style={{ fontSize: '11px', fontFamily: "'DM Mono',monospace", letterSpacing: '0.12em', marginBottom: '14px', fontWeight: 700 }}>
                          {isFb ? (
                            <span style={{ color: FB_COLOR }}>FACEBOOK · {creatives.length} {creatives.length === 1 ? 'CREATIVO' : 'CREATIVOS'}</span>
                          ) : (
                            /* IG label with dark chip bg for legibility (the gradient border + dark page
                               was washing out the gradient-clipped text). */
                            <span style={{ display: 'inline-block', background: 'rgba(10,6,16,0.85)', padding: '4px 10px', borderRadius: '999px' }}>
                              <span style={{ background: IG_GRADIENT, WebkitBackgroundClip: 'text', WebkitTextFillColor: 'transparent', backgroundClip: 'text', fontWeight: 800 }}>INSTAGRAM</span>
                              <span style={{ color: '#fff', marginLeft: '6px' }}>· {creatives.length} {creatives.length === 1 ? 'CREATIVO' : 'CREATIVOS'}</span>
                            </span>
                          )}
                        </div>
                        <div style={{
                          display: 'grid',
                          gridTemplateColumns: `repeat(${Math.min(creatives.length, 5)}, minmax(0, 1fr))`,
                          gap: '12px',
                        }}>
                          {creatives.map((img, i) => isFb ? renderFacebookMini(img, i) : renderInstagramMini(img, i))}
                        </div>
                      </div>
                    </div>
                  </div>
                )}
              </div>
            );
          })()}

          {/* ── Call-to-action row: label + dropdown + live button sample. Hidden for the
              assistant flow — the AI picks the CTA based on objective + destination. ── */}
          {!assistantPrefill && (
          <div style={{ marginBottom: '14px', padding: '14px 16px', background: 'rgba(139,92,246,0.05)', border: `1px solid rgba(139,92,246,0.2)`, borderRadius: '14px', display: 'flex', alignItems: 'center', gap: '14px', flexWrap: 'wrap' }}>
            <div style={{ display: 'flex', alignItems: 'center', gap: '8px', flexShrink: 0 }}>
              <svg width="16" height="16" viewBox="0 0 24 24" fill="none"><path d="M13 2L3 14h7l-1 8 10-12h-7l1-8z" fill={NX.accent}/></svg>
              <span style={{ fontSize: '13px', fontWeight: 500, color: NX.text }}>
                {lang === 'en' ? 'Call to action' : 'Llamada a la acción'}
              </span>
            </div>

            {/* Dropdown-style button — opens the CTA picker modal on click */}
            <button onClick={() => setCtaPickerOpen(true)}
              style={{ flex: '1 1 220px', padding: '10px 14px', background: NX.bg3, border: `1px solid ${NX.border2}`, borderRadius: '10px', color: NX.text, fontSize: '13px', fontWeight: 500, cursor: 'pointer', fontFamily: "'DM Sans',sans-serif", display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: '10px', transition: 'all 0.15s' }}
              onMouseEnter={e => { e.currentTarget.style.borderColor = NX.accent; e.currentTarget.style.background = NX.bg4; }}
              onMouseLeave={e => { e.currentTarget.style.borderColor = NX.border2; e.currentTarget.style.background = NX.bg3; }}>
              <span>{ctaLabel(ctaButton)}</span>
              <svg width="12" height="12" viewBox="0 0 12 12" fill="none">
                <path d="M3 4.5l3 3 3-3" stroke={NX.muted} strokeWidth="1.4" strokeLinecap="round"/>
              </svg>
            </button>

            {/* Live sample — mimics how Meta renders the CTA button inside the ad */}
            <div style={{ display: 'flex', alignItems: 'center', gap: '8px', flexShrink: 0 }}>
              <span style={{ fontSize: '10px', color: NX.muted, fontFamily: "'DM Mono',monospace", letterSpacing: '0.08em', textTransform: 'uppercase' }}>
                {lang === 'en' ? 'Preview' : 'Ejemplo'}
              </span>
              <div style={{ background: '#E4E6EB', color: '#050505', fontSize: '12px', fontWeight: 700, padding: '7px 14px', borderRadius: '6px', whiteSpace: 'nowrap', fontFamily: "'DM Sans',sans-serif" }}>
                {ctaLabel(ctaButton)}
              </div>
            </div>
          </div>
          )}

          {/* ── CTA PICKER MODAL ── */}
          {ctaPickerOpen && (
            <div onClick={() => setCtaPickerOpen(false)}
              style={{ position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.75)', backdropFilter: 'blur(6px)', zIndex: 2000, display: 'flex', alignItems: 'center', justifyContent: 'center', padding: '24px' }}>
              <div onClick={e => e.stopPropagation()}
                style={{ background: NX.bg2, border: `1px solid ${NX.border}`, borderRadius: '16px', maxWidth: '520px', width: '100%', maxHeight: '80vh', overflow: 'auto', padding: '24px' }}>
                <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '6px' }}>
                  <h2 style={{ fontSize: '16px', fontWeight: 500, color: NX.text, margin: 0 }}>
                    {lang === 'en' ? 'Choose the action button' : 'Elige el botón de acción'}
                  </h2>
                  <button onClick={() => setCtaPickerOpen(false)}
                    style={{ background: 'transparent', border: 'none', color: NX.muted, fontSize: '20px', cursor: 'pointer', lineHeight: 1 }}>×</button>
                </div>
                <p style={{ fontSize: '12px', color: NX.muted, margin: '0 0 16px', lineHeight: 1.5 }}>
                  {lang === 'en' ? 'Meta only allows these preset buttons in its ads.' : 'Meta solo permite estos botones en sus anuncios.'}
                </p>
                <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill,minmax(160px,1fr))', gap: '8px' }}>
                  {META_CTA_OPTIONS.map(o => {
                    const active = ctaButton === o.id;
                    return (
                      <button key={o.id}
                        onClick={() => { setCtaButton(o.id); setCtaPickerOpen(false); }}
                        style={{
                          padding: '12px 14px', background: active ? NX.bg4 : NX.bg3,
                          border: `1.5px solid ${active ? NX.accent : NX.border}`,
                          borderRadius: '10px', color: active ? NX.accent : NX.text,
                          fontSize: '13px', fontWeight: active ? 600 : 500, cursor: 'pointer',
                          fontFamily: "'DM Sans',sans-serif", textAlign: 'left',
                          transition: 'all 0.15s',
                          display: 'flex', alignItems: 'center', gap: '8px',
                        }}
                        onMouseEnter={e => { if (!active) e.currentTarget.style.borderColor = NX.border2; }}
                        onMouseLeave={e => { if (!active) e.currentTarget.style.borderColor = NX.border; }}>
                        {active && <svg width="13" height="13" viewBox="0 0 14 14" fill="none"><path d="M2.5 7l3 3 6-7" stroke={NX.accent} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/></svg>}
                        {language === 'en' ? o.labelEn : o.labelEs}
                      </button>
                    );
                  })}
                </div>
              </div>
            </div>
          )}
          {/* Budget + launch mode */}
          <div style={{ marginBottom: '10px' }}>
            <div style={{ fontSize: '12px', fontWeight: 500, color: NX.text, marginBottom: '8px' }}>Configuración de campaña</div>

            {/* Daily budget */}
            {/* Compact horizontal layout: [CLP] [budget input] [Pausa] [Ahora] in one row.
                Yellow tint for paused (pre-publish caution), green tint for active (live now).
                Default selection stays paused. */}
            <div style={{ display: 'flex', gap: '6px', alignItems: 'stretch', flexWrap: 'wrap' }}>
              {/* Currency — fixed from ad account (not editable) */}
              <div title="La moneda se toma de tu cuenta publicitaria de Meta y no se puede cambiar."
                style={{ background: NX.bg3, border: `1px solid ${NX.border}`, borderRadius: '8px', color: NX.text, fontSize: '12px', padding: '7px 10px', fontFamily: "'DM Sans',sans-serif", minWidth: '60px', display: 'flex', alignItems: 'center', justifyContent: 'center', fontWeight: 600, cursor: 'not-allowed', opacity: 0.9 }}>
                {currency}
              </div>
              {/* Budget input — narrower so the row holds the launch modes too */}
              <input
                type="number" min={minDailyForCurrency} value={dailyBudget} onChange={e => setDailyBudget(e.target.value)}
                placeholder={`Mín: ${minDailyForCurrency.toLocaleString()}`}
                data-flash-target="budget"
                className={flashField === 'budget' ? 'nx-flash' : undefined}
                style={{ flex: '1 1 140px', minWidth: '120px', background: NX.bg3, border: `1px solid ${dailyBudget ? NX.accent : NX.border}`, borderRadius: '8px', color: NX.text, fontSize: '13px', padding: '7px 10px', fontFamily: "'DM Sans',sans-serif", outline: 'none', boxSizing: 'border-box' }}
              />
              {/* Launch mode — pill buttons inline. Subtle yellow for paused, green for active.
                  When the user clicks "Lanzar campaña" without picking, the buttons SHAKE
                  RED via the existing fieldFlash keyframe (.nx-flash). The launchFlashKey
                  remounts the pills so the CSS animation restarts even on repeated clicks. */}
              {[
                { id: 'paused', icon: '⏸', label: lang === 'en' ? 'Pausa' : 'Pausa',  tone: NX.warning, toneBg: 'rgba(251,191,36,0.10)', toneBgActive: 'rgba(251,191,36,0.18)' },
                { id: 'active', icon: '▶', label: lang === 'en' ? 'Ahora' : 'Ahora',  tone: NX.success, toneBg: 'rgba(52,211,153,0.10)', toneBgActive: 'rgba(52,211,153,0.18)' },
              ].map(opt => {
                const active = launchMode === opt.id;
                const errorRing = launchModeError && launchMode == null;
                const shouldFlash = errorRing && launchFlashKey > 0;
                return (
                  <button key={`${opt.id}-${shouldFlash ? launchFlashKey : 'idle'}`} type="button"
                    onClick={() => { setLaunchMode(opt.id); setLaunchModeError(false); }}
                    title={opt.id === 'paused' ? (lang === 'en' ? 'Publish paused — activate later from Ads Manager' : 'Publicar en pausa — actívala después desde Ads Manager') : (lang === 'en' ? 'Publish now — starts running immediately' : 'Publicar ahora — empieza a correr de inmediato')}
                    className={shouldFlash ? 'nx-flash' : undefined}
                    style={{
                      flex: '0 0 auto',
                      padding: '7px 14px',
                      background: active ? opt.toneBgActive : opt.toneBg,
                      border: `1.5px solid ${errorRing ? NX.danger : (active ? opt.tone : `${opt.tone}55`)}`,
                      borderRadius: '8px',
                      color: active ? opt.tone : NX.text,
                      fontSize: '12px', fontWeight: 600,
                      cursor: 'pointer',
                      fontFamily: "'DM Sans',sans-serif",
                      display: 'inline-flex', alignItems: 'center', gap: '6px',
                      transition: 'all 0.15s',
                      whiteSpace: 'nowrap',
                      boxShadow: errorRing ? '0 0 0 3px rgba(248,113,113,0.18)' : 'none',
                    }}>
                    <span style={{ fontSize: '12px' }}>{opt.icon}</span>
                    <span>{opt.label}</span>
                  </button>
                );
              })}
            </div>
            {launchModeError && launchMode == null && (
              <div style={{ marginTop: '6px', fontSize: '11px', color: NX.danger, fontFamily:"'DM Sans',sans-serif", fontWeight: 500 }}>
                ⚠ {lang === 'en' ? 'Pick "Pausa" or "Ahora" before launching the campaign.' : 'Elige "Pausa" o "Ahora" antes de lanzar la campaña.'}
              </div>
            )}
            {/* Currency hint below the compact row */}
            <div style={{ fontSize: '10px', color: NX.muted, marginTop: '6px' }}>
              {metaAssets?.ad_account_name
                ? <>Moneda de <span style={{ color: NX.text }}>{metaAssets.ad_account_name}</span>. Mínimo Meta Ads: <span style={{ color: NX.text }}>{minDailyForCurrency.toLocaleString()} {currency}</span>/día.</>
                : <>Completa primero "Configurar activos" para detectar la moneda de tu cuenta. Mínimo: {minDailyForCurrency.toLocaleString()} {currency}/día.</>}
            </div>
          </div>

          {/* Model used badge */}
          {usedImageModel && (
            <div style={{ fontSize: '10px', color: NX.muted, fontFamily: "'DM Mono',monospace", marginBottom: '12px', textAlign: 'center' }}>
              Imágenes generadas con <span style={{ color: NX.accent }}>NexWall Corp AI</span> · {(/^gpt-image/i.test(usedImageModel)) ? 'Premium Ultra MAX' : 'Premium'}
            </div>
          )}

          {publishError && (() => {
            const isPermissionIssue = /ads_management|permiso|permission|advanced access|scope/i.test(publishError);
            const appId = '964537206532097';
            return (
              <div style={{ margin: '12px 0', padding: '12px 14px', background: 'rgba(248,113,113,0.08)', border: `1px solid rgba(248,113,113,0.25)`, borderRadius: '10px' }}>
                <div style={{ fontSize: '12px', fontWeight: 600, color: NX.danger, marginBottom: '6px' }}>✗ {lang === 'en' ? 'Could not publish on Meta' : 'No se pudo publicar en Meta'}</div>
                <div style={{ fontSize: '11px', color: NX.muted, lineHeight: 1.5, marginBottom: isPermissionIssue ? '10px' : 0 }}>{publishError}</div>
                {isPermissionIssue && (
                  <div style={{ display: 'flex', gap: '8px', flexWrap: 'wrap' }}>
                    <a href={`https://developers.facebook.com/apps/${appId}/app-review/permissions/`} target="_blank" rel="noopener noreferrer"
                      style={{ flex: 1, minWidth: '180px', padding: '8px 12px', background: 'rgba(139,92,246,0.1)', border: `1px solid ${NX.accent}`, borderRadius: '8px', color: NX.accent, fontSize: '11px', fontWeight: 600, textDecoration: 'none', display: 'flex', alignItems: 'center', gap: '6px', justifyContent: 'center' }}>
                      <svg width="12" height="12" viewBox="0 0 16 16" fill="none"><path d="M6 3h-3v10h10v-3M10 3h3v3M13 3l-6 6" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/></svg>
                      {lang === 'en' ? 'Request ads_management permission' : 'Solicitar permiso ads_management'}
                    </a>
                    <button
                      onClick={async () => {
                        try { await authFetch('/auth/disconnect', { method: 'POST' }); } catch {}
                        window.location.href = `${API}/auth/meta?token=${encodeURIComponent(localStorage.getItem('nw_token') || '')}`;
                      }}
                      style={{ flex: 1, minWidth: '180px', padding: '8px 12px', background: 'linear-gradient(135deg, #8b5cf6, #a78bfa)', border: 'none', borderRadius: '8px', color: '#fff', fontSize: '11px', fontWeight: 600, cursor: 'pointer', fontFamily: "'DM Sans',sans-serif" }}>
                      {lang === 'en' ? 'Reconnect Meta (get a new token)' : 'Reconectar Meta (token nuevo)'}
                    </button>
                  </div>
                )}
              </div>
            );
          })()}
          {!metaAssets?.ad_account_id && (
            <div style={{ margin: '12px 0', padding: '12px 14px', background: 'rgba(251,191,36,0.08)', border: `1px solid rgba(251,191,36,0.25)`, borderRadius: '10px' }}>
              <div style={{ fontSize: '12px', fontWeight: 600, color: NX.warning, marginBottom: '4px' }}>⚠ Configura tus activos de Meta</div>
              <div style={{ fontSize: '11px', color: NX.muted, lineHeight: 1.5 }}>Debes completar "Configurar activos" antes de poder publicar.</div>
            </div>
          )}
          <div style={{ display: 'flex', gap: '8px', flexWrap: 'wrap' }}>
            <NxButton variant="ghost" onClick={() => setStep(4)} full>{ts.edit}</NxButton>
            <NxButton
              variant="primary"
              disabled={launching}
              onClick={saveDraft}
              full>
              {lang === 'en' ? 'Save draft' : 'Guardar campaña'}
            </NxButton>
            <NxButton
              variant="accent"
              disabled={launching}
              onClick={() => {
                // Instead of silently disabling, keep the button live and guide the user to the
                // missing field with a flash animation. The field shakes red + auto-scrolls into view.
                if (!dailyBudget || Number(dailyBudget) < minDailyForCurrency) {
                  triggerFieldFlash('budget');
                  return;
                }
                if (!metaAssets?.ad_account_id) {
                  // Meta assets missing — different error, already shown as banner above.
                  return;
                }
                if (!launchMode) {
                  // Force the user to pick "Pausa" or "Ahora" — never silently default to one.
                  // Trigger the red shake animation on the pills so the user sees clearly
                  // that those buttons need attention. Key bump restarts the CSS keyframe
                  // even on repeated clicks.
                  setLaunchModeError(true);
                  setLaunchFlashKey(k => k + 1);
                  return;
                }
                // Open the confirmation modal so the user explicitly acknowledges that the
                // budget will be charged by Meta (Facebook Ads), not by NexWall.
                setConfirmLaunchOpen(true);
                return;
              }}
              full>
              {assistantPrefill
                ? (lang === 'en' ? 'Create strategy & launch campaign' : 'Crear estrategia y lanzar campaña')
                : (lang === 'en' ? 'Launch campaign' : 'Lanzar campaña')}
            </NxButton>
          </div>
          {/* Costo en créditos visible antes del click. Cuando assistantPrefill está activo,
              el CTA dispara generación de estrategia + N imágenes + lote de copies. Si no, ya
              se generó todo, así que el publish es gratis (Meta cobra los ads aparte). */}
          {(() => {
            const isRelaunch = !!(draftCampaign?.id && draftCampaign.status !== 'draft');
            const estimate = assistantPrefill
              ? (window.estimateCampaignCredits
                  ? window.estimateCampaignCredits({ numImages: selectedImgs.length || 5, imageModel })
                  : null)
              : (isRelaunch ? (window.CREDIT_COSTS?.campaign_relaunch || 5) : 0);
            if (estimate === null) return null;
            return (
              <div style={{ fontSize: '10px', color: NX.accent2, textAlign: 'center', marginTop: '4px', fontFamily: "'DM Mono',monospace" }}>
                {estimate === 0
                  ? (lang === 'en' ? '✦ No credits — launching is free' : '✦ Sin costo — lanzar es gratis')
                  : (lang === 'en' ? `✦ Estimated cost: ${estimate} credits` : `✦ Costo estimado: ${estimate} créditos`)}
              </div>
            );
          })()}
          <div style={{ fontSize: '10px', color: NX.muted, textAlign: 'center', marginTop: '6px' }}>
            {lang === 'en'
              ? 'Draft keeps everything saved without charging Meta. You can publish it later from My campaigns.'
              : 'Guardar deja todo listo sin cobrarte en Meta. Puedes publicarla después desde Mis Estrategias.'}
          </div>

          {/* ── LAUNCH CONFIRMATION MODAL ── */}
          {confirmLaunchOpen && (() => {
            const cur = currency;
            const dailyN = Number(dailyBudget) || 0;
            const monthlyEstimate = dailyN * 30;
            const isActive = launchMode === 'active';
            const fmtMoneyAt = (amount) => {
              try { return new Intl.NumberFormat(undefined, { style: 'currency', currency: cur, maximumFractionDigits: 0 }).format(amount); }
              catch { return `${cur} ${Number(amount).toLocaleString()}`; }
            };
            return (
              <div onClick={() => !launching && setConfirmLaunchOpen(false)}
                style={{ position:'fixed', inset:0, background:'rgba(0,0,0,0.82)', backdropFilter:'blur(10px)', zIndex:3500, display:'flex', alignItems:'center', justifyContent:'center', padding:'24px' }}>
                <div onClick={e => e.stopPropagation()}
                  style={{ background: NX.bg2, border: `1px solid ${NX.border2}`, borderRadius:'18px', maxWidth:'520px', width:'100%', padding:'28px 26px', animation:'modalIn 0.22s ease', boxShadow:'0 30px 80px rgba(10,6,16,0.85), 0 0 60px rgba(139,92,246,0.18)', textAlign:'center' }}>
                  <div style={{ display:'flex', alignItems:'center', justifyContent:'center', gap:'10px', marginBottom:'14px' }}>
                    <div style={{ width:'34px', height:'34px', borderRadius:'10px', background:'rgba(251,191,36,0.14)', border:`1px solid rgba(251,191,36,0.32)`, display:'flex', alignItems:'center', justifyContent:'center' }}>
                      <svg width="18" height="18" viewBox="0 0 24 24" fill="none"><path d="M12 9v4M12 17h.01M5.07 19h13.86a2 2 0 001.74-3L13.74 4a2 2 0 00-3.48 0L3.33 16a2 2 0 001.74 3z" stroke={NX.warning} strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round"/></svg>
                    </div>
                    <h2 style={{ fontSize:'17px', fontWeight:600, color: NX.text, margin:0, letterSpacing:'-0.01em' }}>
                      {lang === 'en' ? 'Confirm launch' : 'Confirmar lanzamiento'}
                    </h2>
                  </div>

                  <div style={{ background:'rgba(18,10,30,0.5)', border:`1px solid ${NX.border}`, borderRadius:'12px', padding:'14px 16px', marginBottom:'14px', textAlign:'center' }}>
                    <div style={{ fontSize:'11px', color: NX.muted, fontFamily:"'DM Mono',monospace", letterSpacing:'0.1em', textTransform:'uppercase', marginBottom:'4px' }}>
                      {lang === 'en' ? 'Daily budget' : 'Presupuesto diario'}
                    </div>
                    <div style={{ fontSize:'22px', fontWeight:700, color: NX.text, letterSpacing:'-0.02em', fontFeatureSettings:'"tnum"' }}>
                      {fmtMoneyAt(dailyN)} <span style={{ fontSize:'12px', color: NX.muted, fontWeight:500 }}>/ {lang === 'en' ? 'day' : 'día'}</span>
                    </div>
                    <div style={{ fontSize:'11px', color: NX.muted, marginTop:'6px' }}>
                      {lang === 'en' ? 'Estimated monthly cap (30 days)' : 'Estimado mensual (30 días)'}: <strong style={{ color: NX.text }}>{fmtMoneyAt(monthlyEstimate)}</strong>
                    </div>
                    <div style={{ fontSize:'11px', color: isActive ? NX.success : NX.warning, marginTop:'8px', fontWeight:600, display:'flex', alignItems:'center', justifyContent:'center', gap:'6px' }}>
                      <span>{isActive ? '▶' : '⏸'}</span>
                      {isActive
                        ? (lang === 'en' ? 'Will publish ACTIVE — starts spending immediately' : 'Se publicará ACTIVA — empieza a gastar de inmediato')
                        : (lang === 'en' ? 'Will publish PAUSED — no spend until you activate it' : 'Se publicará PAUSADA — no gasta hasta que la actives')}
                    </div>
                  </div>

                  <p style={{ fontSize:'13px', color: NX.text, lineHeight:1.6, margin:'0 0 14px', textAlign:'center' }}>
                    {lang === 'en'
                      ? <>I authorize spending up to <strong>{fmtMoneyAt(dailyN)}/day</strong> on this campaign, and I understand that <strong style={{ color: NX.warning }}>the charges are billed by Meta (Facebook Ads) directly to the payment method on my ad account</strong> — NexWall Corp AI does NOT process this payment. I am responsible for the resulting charges.</>
                      : <>Confirmo que autorizo gastar hasta <strong>{fmtMoneyAt(dailyN)}/día</strong> en esta campaña, y entiendo que <strong style={{ color: NX.warning }}>el cobro lo realiza Meta (Facebook Ads) directamente desde el método de pago configurado en mi cuenta publicitaria</strong> — NexWall Corp AI no procesa ese cobro. Soy responsable de los cargos generados.</>}
                  </p>
                  <p style={{ fontSize:'11px', color: NX.muted, lineHeight:1.55, margin:'0 0 18px', textAlign:'center' }}>
                    {lang === 'en'
                      ? 'You can pause or change the budget anytime from "My campaigns". Meta optimizes within ±25% of the daily budget per day.'
                      : 'Puedes pausar o cambiar el presupuesto cuando quieras desde "Mis estrategias". Meta optimiza con un margen de ±25 % diario sobre tu presupuesto.'}
                  </p>

                  <div style={{ display:'flex', gap:'10px', justifyContent:'center' }}>
                    <NxButton variant="ghost" onClick={() => setConfirmLaunchOpen(false)} disabled={launching} full>
                      {lang === 'en' ? 'Cancel' : 'Cancelar'}
                    </NxButton>
                    <NxButton variant="accent" disabled={launching}
                      onClick={() => {
                        setConfirmLaunchOpen(false);
                        publishCampaign();
                      }}
                      full>
                      {lang === 'en' ? 'I confirm — launch campaign' : 'Confirmo — lanzar campaña'}
                    </NxButton>
                  </div>
                </div>
              </div>
            );
          })()}
          </div>{/* /max-width wrapper for assistant flow */}

          {/* ═════ PUBLISH PROGRESS MODAL ═════ NexWall robot + live status from SSE */}
          {progressOpen && (
            <div style={{ position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.9)', backdropFilter: 'blur(10px)', zIndex: 3000, display: 'flex', alignItems: 'center', justifyContent: 'center', padding: '24px' }}>
              <div style={{ background: NX.bg2, border: `1px solid ${NX.border}`, borderRadius: '20px', maxWidth: '520px', width: '100%', padding: '36px 28px', textAlign: 'center', boxShadow: '0 20px 60px rgba(139,92,246,0.15)' }}>
                <div style={{ position: 'relative', width: '180px', height: '180px', margin: '0 auto 20px', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
                  {/* Back nebula glows */}
                  <div style={{ position: 'absolute', width: '180px', height: '180px', borderRadius: '50%', background: 'radial-gradient(circle, rgba(139,92,246,0.38) 0%, rgba(139,92,246,0.18) 50%, transparent 72%)', animation: 'nwNebula 2.4s ease-in-out infinite', filter: 'blur(6px)' }}/>
                  <div style={{ position: 'absolute', width: '220px', height: '220px', borderRadius: '50%', background: 'radial-gradient(circle, rgba(139,92,246,0.26) 0%, rgba(139,92,246,0.1) 60%, transparent 80%)', animation: 'nwNebula2 3.1s ease-in-out infinite', filter: 'blur(10px)' }}/>
                  {/* Orbit ring 1 — 4 dots rotating clockwise */}
                  <div style={{ position: 'absolute', width: '150px', height: '150px', animation: 'nwOrbitCW 4s linear infinite' }}>
                    <div style={{ position: 'absolute', top: '-3px', left: '50%', transform: 'translateX(-50%)', width: '7px', height: '7px', borderRadius: '50%', background: '#8b5cf6', boxShadow: '0 0 10px #8b5cf6, 0 0 20px rgba(139,92,246,0.6)' }}/>
                    <div style={{ position: 'absolute', right: '-3px', top: '50%', transform: 'translateY(-50%)', width: '5px', height: '5px', borderRadius: '50%', background: '#8b5cf6', boxShadow: '0 0 8px #8b5cf6' }}/>
                    <div style={{ position: 'absolute', bottom: '-3px', left: '50%', transform: 'translateX(-50%)', width: '6px', height: '6px', borderRadius: '50%', background: '#8b5cf6', boxShadow: '0 0 10px #8b5cf6' }}/>
                    <div style={{ position: 'absolute', left: '-3px', top: '50%', transform: 'translateY(-50%)', width: '4px', height: '4px', borderRadius: '50%', background: '#8b5cf6', boxShadow: '0 0 8px #8b5cf6' }}/>
                  </div>
                  {/* Orbit ring 2 — 3 dots rotating counter-clockwise, slightly larger */}
                  <div style={{ position: 'absolute', width: '180px', height: '180px', animation: 'nwOrbitCCW 6s linear infinite' }}>
                    <div style={{ position: 'absolute', top: '10%', right: '10%', width: '4px', height: '4px', borderRadius: '50%', background: '#a78bfa', boxShadow: '0 0 8px #a78bfa' }}/>
                    <div style={{ position: 'absolute', bottom: '15%', left: '12%', width: '5px', height: '5px', borderRadius: '50%', background: '#8b5cf6', boxShadow: '0 0 10px #8b5cf6' }}/>
                    <div style={{ position: 'absolute', top: '55%', right: '5%', width: '3px', height: '3px', borderRadius: '50%', background: '#8b5cf6', boxShadow: '0 0 6px #8b5cf6' }}/>
                  </div>
                  {/* Blurred halo behind the icon */}
                  <img src="/icono500x500.png" alt="" aria-hidden="true" style={{ width: '110px', height: '110px', objectFit: 'contain', position: 'absolute', zIndex: 1, filter: 'blur(14px) saturate(1.4)', opacity: 0.75 }}/>
                  {/* The logo itself */}
                  <img src="/icono500x500.png" alt="NexWall Corp AI" style={{ width: '108px', height: '108px', objectFit: 'contain', position: 'relative', zIndex: 3, animation: 'nwIconPulse 2s ease-in-out infinite' }}/>
                </div>
                <div style={{ fontSize: '10px', color: NX.muted, fontFamily: "'DM Mono',monospace", letterSpacing: '0.15em', marginBottom: '8px' }}>NEXWALL CORP AI</div>
                <h2 style={{ fontSize: '20px', fontWeight: 500, color: NX.text, margin: '0 0 6px' }}>
                  {lang === 'en' ? 'Publishing your campaign…' : 'Publicando tu campaña…'}
                </h2>
                <div style={{ fontSize: '13px', color: NX.muted, marginBottom: '24px' }}>
                  {lang === 'en' ? 'Creating everything in Meta Ads. This can take up to a minute.' : 'Estamos creando todo en Meta Ads. Puede tomar hasta un minuto.'}
                </div>
                <div style={{ padding: '14px 16px', background: NX.bg3, border: `1px solid ${NX.border}`, borderRadius: '12px', marginBottom: '16px', display: 'flex', alignItems: 'center', gap: '12px', justifyContent: 'center', minHeight: '52px' }}>
                  <div style={{ width: '8px', height: '8px', borderRadius: '50%', background: NX.accent, animation: 'nwBlink 1s ease-in-out infinite' }}/>
                  <div style={{ fontSize: '14px', color: NX.text, fontWeight: 500 }}>
                    {progressCurrent || (lang === 'en' ? 'Starting…' : 'Empezando…')}
                  </div>
                </div>
                {progressSteps.length > 1 && (
                  <div style={{ textAlign: 'left', maxHeight: '140px', overflow: 'auto', padding: '10px 14px', background: NX.bg3, border: `1px solid ${NX.border}`, borderRadius: '10px' }}>
                    {progressSteps.slice(0, -1).slice(-6).map((s, i) => (
                      <div key={i} style={{ fontSize: '11px', color: NX.muted, marginBottom: '4px', display: 'flex', alignItems: 'center', gap: '6px' }}>
                        <span style={{ color: NX.success }}>✓</span>{s.msg}
                      </div>
                    ))}
                  </div>
                )}
                <style>{`
                  @keyframes nwNebula  { 0%,100% { transform: scale(1); opacity: 0.9; } 50% { transform: scale(1.12); opacity: 1; } }
                  @keyframes nwNebula2 { 0%,100% { transform: scale(1.05); opacity: 0.6; } 50% { transform: scale(0.95); opacity: 0.9; } }
                  @keyframes nwIconPulse { 0%,100% { transform: scale(1); filter: drop-shadow(0 0 18px rgba(139,92,246,0.45)); } 50% { transform: scale(1.05); filter: drop-shadow(0 0 28px rgba(139,92,246,0.6)); } }
                  @keyframes nwOrbitCW { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
                  @keyframes nwOrbitCCW { from { transform: rotate(360deg); } to { transform: rotate(0deg); } }
                  @keyframes nwBlink { 0%,100% { opacity: 1; } 50% { opacity: 0.3; } }
                `}</style>
              </div>
            </div>
          )}

          {/* ═════ STRATEGY MODAL ═════ generated by paid-ads skill + Opus */}
          {strategyOpen && (
            <div onClick={() => setStrategyOpen(false)}
              style={{ position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.82)', backdropFilter: 'blur(8px)', zIndex: 2000, display: 'flex', alignItems: 'center', justifyContent: 'center', padding: '24px' }}>
              <div onClick={e => e.stopPropagation()}
                style={{ background: NX.bg2, border: `1px solid ${NX.border}`, borderRadius: '16px', maxWidth: '820px', width: '100%', maxHeight: '90vh', overflow: 'auto', padding: '28px' }}>
                <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', marginBottom: '20px', gap: '12px' }}>
                  <div style={{ minWidth: 0, flex: 1 }}>
                    <div style={{ fontSize: '10px', color: NX.muted, fontFamily: "'DM Mono',monospace", letterSpacing: '0.1em', marginBottom: '4px', display: 'flex', alignItems: 'center', gap: '6px' }}>
                      <svg width="10" height="10" viewBox="0 0 24 24" fill="none"><path d="M9 11l3-8 10 6-6 3 2 10-8-5-9 4 8-10z" fill={NX.accent}/></svg>
                      NEXWALL CORP AI
                    </div>
                    <h2 style={{ fontSize: '20px', fontWeight: 500, color: NX.text, margin: 0 }}>
                      {lang === 'en' ? 'Your Meta Ads strategy' : 'Tu estrategia para Meta Ads'}
                    </h2>
                  </div>
                  <div style={{ display: 'flex', alignItems: 'center', gap: '8px', flexShrink: 0 }}>
                    {/* Regenerate strategy — bypasses the cache and re-runs the AI. The current
                        strategy is replaced when the new one arrives; the modal stays open. */}
                    <button onClick={() => !strategyLoading && generateStrategy({ force: true })}
                      disabled={strategyLoading}
                      title={lang === 'en' ? 'Generate a new strategy (uses tokens)' : 'Generar una nueva estrategia (gasta tokens)'}
                      style={{ background: strategyLoading ? 'rgba(139,92,246,0.18)' : 'linear-gradient(135deg, #7c3aed 0%, #8b5cf6 50%, #a78bfa 100%)', border: 'none', borderRadius: '10px', color: '#fff', fontSize: '12px', fontWeight: 600, padding: '8px 14px', cursor: strategyLoading ? 'wait' : 'pointer', fontFamily: "'DM Sans',sans-serif", display: 'flex', alignItems: 'center', gap: '6px', boxShadow: strategyLoading ? 'none' : '0 6px 18px rgba(139,92,246,0.35)', whiteSpace: 'nowrap', transition: 'all 0.18s' }}>
                      {strategyLoading
                        ? <><svg width="11" height="11" viewBox="0 0 24 24" fill="none" style={{ animation: 'spin 1s linear infinite' }}><circle cx="12" cy="12" r="9" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeDasharray="14 40"/></svg> {lang === 'en' ? 'Generating…' : 'Generando…'}</>
                        : <><svg width="11" height="11" viewBox="0 0 16 16" fill="none"><path d="M13 8a5 5 0 11-1.46-3.54M13 3v3.5h-3.5" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round" strokeLinejoin="round"/></svg> {lang === 'en' ? 'New strategy' : 'Generar nueva estrategia'}</>}
                    </button>
                    <button onClick={() => setStrategyOpen(false)}
                      style={{ background: NX.bg3, border: `1px solid ${NX.border}`, width: '30px', height: '30px', borderRadius: '50%', color: NX.text, cursor: 'pointer', fontSize: '16px' }}>×</button>
                  </div>
                </div>

                {strategyLoading && (
                  <div style={{ padding: '40px', textAlign: 'center' }}>
                    <div style={{ display: 'inline-block', width: '24px', height: '24px', border: `2px solid ${NX.accent}`, borderTopColor: 'transparent', borderRadius: '50%', animation: 'spin 0.7s linear infinite', marginBottom: '14px' }}/>
                    <div style={{ fontSize: '14px', color: NX.text, fontWeight: 500, marginBottom: '4px' }}>{lang === 'en' ? 'NexWall Corp AI is building your strategy' : 'NexWall Corp AI está creando tu estrategia'}</div>
                    <div style={{ fontSize: '12px', color: NX.muted }}>{lang === 'en' ? 'Analyzing your product, audience and market…' : 'Analizando tu producto, audiencia y mercado…'}</div>
                  </div>
                )}

                {strategy?.error && (
                  <div style={{ padding: '14px', background: 'rgba(248,113,113,0.08)', border: `1px solid ${NX.danger}`, borderRadius: '10px', color: NX.danger, fontSize: '13px' }}>{strategy.error}</div>
                )}

                {strategy && !strategy.error && !strategyLoading && (
                  <StrategyDetails strategy={strategy} lang={language}/>
                )}
              </div>
            </div>
          )}
        </>}

        {launching && (
          <div style={{ padding: '20px 0' }}>
            <AiProcessing label={ts.launching.label} sublabel={ts.launching.sublabel}/>
          </div>
        )}

        {step === 6 && <>
          {/* Same compact column treatment as step 5 — keeps the success screen looking like
              a centered card instead of a stretched landscape banner. Max-width applied
              universally (advanced flow benefits too — the launched-state has no wide
              content that would justify 1080 px). */}
          <div style={{ maxWidth: '640px', margin: '0 auto', width: '100%' }}>
          <div style={{ textAlign: 'center', padding: '20px 0' }}>
            <div style={{ width: '72px', height: '72px', borderRadius: '50%', background: finalCampaign?.savedAsDraft ? 'rgba(251,191,36,0.12)' : 'rgba(52,211,153,0.1)', border: `1px solid ${finalCampaign?.savedAsDraft ? 'rgba(251,191,36,0.3)' : 'rgba(52,211,153,0.25)'}`, display: 'flex', alignItems: 'center', justifyContent: 'center', margin: '0 auto 20px', fontSize: '28px' }}>
              {finalCampaign?.savedAsDraft ? '📝' : '🚀'}
            </div>
            <h2 style={{ fontSize: '20px', fontWeight: 500, color: NX.text, margin: '0 0 8px' }}>
              {finalCampaign?.savedAsDraft
                ? (lang === 'en' ? 'Draft saved' : 'Borrador guardado')
                : ts.steps[6].title}
            </h2>
            <p style={{ color: NX.muted, fontSize: '13px', marginBottom: '24px', lineHeight: 1.6 }}>
              {finalCampaign?.savedAsDraft
                ? (lang === 'en'
                    ? 'Your campaign is saved in My campaigns. Images, ad texts and all settings are kept. Launch it when you are ready.'
                    : 'Tu campaña está guardada en Mis Estrategias. Las imágenes, los textos y toda la configuración se mantienen. Lánzala cuando estés listo.')
                : ts.steps[6].subtitle}
            </p>
            <NxCard padding="16px" style={{ textAlign: 'left', marginBottom: '20px' }}>
              <div style={{ display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between', gap: '10px', marginBottom: '8px' }}>
                <span style={{ fontSize: '12px', fontWeight: 500, color: NX.text, fontFamily: "'DM Mono',monospace", letterSpacing: '0.01em', lineHeight: 1.4, wordBreak: 'break-word' }}>
                  {metaPublishInfo?.meta_campaign_name || firstApprovedCopy?.titulo || firstApprovedCopy?.title || '—'}
                </span>
                <NxBadge color={finalCampaign?.savedAsDraft ? 'warning' : launchMode === 'active' ? 'success' : 'default'}>
                  {finalCampaign?.savedAsDraft ? (lang === 'en' ? 'Draft' : 'Borrador') : launchMode === 'active' ? 'Activa' : 'Pausada'}
                </NxBadge>
              </div>
              <div style={{ fontSize: '12px', color: NX.muted, marginBottom: '6px' }}>Meta Ads · {objectives.find(o=>o.id===objective)?.label}</div>
              {dailyBudget && <div style={{ fontSize: '12px', color: NX.muted }}>{lang === 'en' ? 'Budget' : 'Presupuesto'}: <span style={{ color: NX.text, fontWeight: 500 }}>{currency} {dailyBudget}/{lang === 'en' ? 'day' : 'día'}</span></div>}
            </NxCard>
            {/* Meta publish status — only for non-draft flow */}
            {!finalCampaign?.savedAsDraft && (metaPublishInfo ? (<>
              <div style={{ background: 'rgba(52,211,153,0.06)', border: `1px solid rgba(52,211,153,0.25)`, borderRadius: '10px', padding: '12px 14px', marginBottom: '16px', textAlign: 'left' }}>
                <div style={{ fontSize: '12px', fontWeight: 600, color: NX.success, marginBottom: '6px' }}>✓ Publicada en Meta Ads</div>
                <div style={{ fontSize: '11px', color: NX.muted, lineHeight: 1.6 }}>
                  {lang === 'en' ? 'Your campaign is' : 'Tu campaña está'} {launchMode === 'active' ? <span style={{ color: NX.success }}>{lang === 'en' ? 'active' : 'activa'}</span> : <span style={{ color: NX.accent }}>{lang === 'en' ? 'paused' : 'pausada'}</span>} {lang === 'en' ? 'in Ads Manager.' : 'en Ads Manager.'}<br/>
                  <span style={{ fontFamily: "'DM Mono',monospace", fontSize: '10px' }}>Campaign ID: {metaPublishInfo.meta_campaign_id}</span><br/>
                  <span style={{ fontFamily: "'DM Mono',monospace", fontSize: '10px' }}>
                    {metaPublishInfo.ads_created ? `${metaPublishInfo.ads_created} ads / ${metaPublishInfo.adsets_created || 1} adset(s)` : `Ad ID: ${metaPublishInfo.meta_ad_id}`}
                  </span>
                </div>
              </div>
              {/* Per-channel failures (e.g. WhatsApp without WABA) — published partially. */}
              {Array.isArray(metaPublishInfo.failed_adsets) && metaPublishInfo.failed_adsets.length > 0 && (
                <div style={{ background:'rgba(251,191,36,0.07)', border:`1px solid rgba(251,191,36,0.32)`, borderRadius:'10px', padding:'12px 14px', marginBottom:'16px', textAlign:'left' }}>
                  <div style={{ fontSize:'12px', fontWeight:600, color: NX.warning, marginBottom:'6px' }}>
                    ⚠ {metaPublishInfo.failed_adsets.length === 1
                      ? (lang === 'en' ? '1 channel was rejected by Meta' : '1 canal fue rechazado por Meta')
                      : (lang === 'en' ? `${metaPublishInfo.failed_adsets.length} channels were rejected by Meta` : `${metaPublishInfo.failed_adsets.length} canales fueron rechazados por Meta`)}
                  </div>
                  <div style={{ fontSize:'11px', color: NX.muted, lineHeight:1.55 }}>
                    {metaPublishInfo.failed_adsets.map((f, i) => (
                      <div key={i} style={{ marginBottom:'4px' }}>
                        <strong style={{ color: NX.text, textTransform:'uppercase', letterSpacing:'0.04em', fontSize:'10px', fontFamily:"'DM Mono',monospace" }}>
                          {f.channel || (lang === 'en' ? 'unknown' : 'desconocido')}:
                        </strong>{' '}
                        {f.message}
                      </div>
                    ))}
                    <div style={{ marginTop:'6px', fontSize:'10px', opacity:0.8 }}>
                      {lang === 'en'
                        ? 'The campaign was published with the channels that worked. To enable the rejected one, fix the issue and create a new campaign.'
                        : 'La campaña se publicó con los canales que funcionaron. Para habilitar el rechazado, soluciona el problema y crea una nueva campaña.'}
                    </div>
                  </div>
                </div>
              )}
            </>) : (
              <div style={{ background: 'rgba(248,113,113,0.06)', border: `1px solid rgba(248,113,113,0.2)`, borderRadius: '10px', padding: '12px 14px', marginBottom: '16px', textAlign: 'left' }}>
                <div style={{ fontSize: '12px', fontWeight: 600, color: NX.danger, marginBottom: '4px' }}>✗ {lang === 'en' ? 'Not published on Meta Ads' : 'No se publicó en Meta Ads'}</div>
                <div style={{ fontSize: '11px', color: NX.muted, lineHeight: 1.5 }}>{lang === 'en' ? 'The campaign was saved in NexWall but could not be sent to Meta. Review the error in the previous step and try again.' : 'La campaña está guardada en NexWall pero no se envió a Meta. Revisa el error en el paso anterior e intenta de nuevo.'}</div>
              </div>
            ))}

            {/* Ads Manager link + View in NexWall — stacked buttons for non-draft success */}
            {!finalCampaign?.savedAsDraft && metaPublishInfo?.meta_campaign_id && metaAssets?.ad_account_id && (() => {
              const acct = String(metaAssets.ad_account_id).replace(/^act_/, '');
              const adsManagerUrl = `https://adsmanager.facebook.com/adsmanager/manage/campaigns?act=${acct}&selected_campaign_ids=${metaPublishInfo.meta_campaign_id}`;
              return (
                <a href={adsManagerUrl} target="_blank" rel="noopener noreferrer"
                  style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', gap: '8px', padding: '11px 16px', background: '#1877F2', color: '#fff', borderRadius: '10px', fontSize: '13px', fontWeight: 600, textDecoration: 'none', marginBottom: '8px', fontFamily: "'DM Sans',sans-serif", transition: 'all 0.15s' }}
                  onMouseEnter={e => { e.currentTarget.style.filter = 'brightness(1.1)'; }}
                  onMouseLeave={e => { e.currentTarget.style.filter = 'brightness(1)'; }}>
                  <svg width="14" height="14" viewBox="0 0 24 24" fill="#fff"><path d="M22 12A10 10 0 1010 22v-7H7v-3h3v-2.3c0-3 1.8-4.7 4.5-4.7 1.3 0 2.6.2 2.6.2v3h-1.5c-1.5 0-1.9.9-1.9 1.8V12h3.3l-.5 3h-2.8v7A10 10 0 0022 12z"/></svg>
                  {lang === 'en' ? 'View in Meta Ads Manager' : 'Ver en Meta Ads Manager'}
                  <svg width="11" height="11" viewBox="0 0 16 16" fill="none"><path d="M6 3h-3v10h10v-3M10 3h3v3M13 3l-6 6" stroke="#fff" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/></svg>
                </a>
              );
            })()}
            <NxButton variant="accent" onClick={saveCampaign} full>{ts.launched.viewBtn}</NxButton>

            {/* Integration attribution — functional, NOT a co-branding lockup */}
            {!finalCampaign?.savedAsDraft && metaPublishInfo && (
              <div style={{ marginTop: '16px', padding: '8px 10px', display: 'flex', alignItems: 'center', justifyContent: 'center', gap: '10px', fontSize: '10px', color: NX.muted, fontFamily: "'DM Mono',monospace", letterSpacing: '0.06em' }}>
                <span>POWERED BY</span>
                <span style={{ color: NX.accent, fontWeight: 600 }}>NEXWALL CORP AI</span>
                <span style={{ opacity: 0.5 }}>×</span>
                <span style={{ color: NX.muted }}>META MARKETING API</span>
              </div>
            )}
          </div>
          </div>{/* /max-width wrapper for step 6 */}
        </>}
      </div>
    </NxModal>
  );
};

// ══════════════════════════════════════════
// SCREEN: MIS ESTRATEGIAS
// ══════════════════════════════════════════
// Reusable strategy renderer — used in StrategyFlow step 5 modal AND in MisEstrategias detail view.
// Accepts the strategy JSON structure produced by /api/strategy/generate.
const StrategyDetails = ({ strategy, lang = 'es', campaign = null }) => {
  if (!strategy) return null;
  // Days elapsed since the campaign was launched on Meta. When `campaign` is null (we're in
  // the pre-launch preview), buttons stay disabled with a hint that they activate after launch.
  const daysSinceLaunch = campaign?.created_at
    ? Math.floor((Date.now() - new Date(campaign.created_at).getTime()) / 86400000)
    : null;
  const [applyingPhase, setApplyingPhase] = React.useState(null); // 'mid' | 'late' | null
  const [applyResult, setApplyResult] = React.useState(null); // { phase, data | error }
  // Confirm modal state — replaces window.confirm so we can show the financial detail
  // (current daily → new daily) before charging Meta.
  const [confirmPhase, setConfirmPhase] = React.useState(null); // 'mid' | 'late' | null
  // Local mirror of last_scaled_at so the 24h countdown updates immediately after a
  // successful scale (without needing a refetch from /api/campaigns).
  const [localLastScaledAt, setLocalLastScaledAt] = React.useState(campaign?.last_scaled_at || null);
  // Tick every minute so the countdown re-renders without explicit invalidation.
  const [, setNowTick] = React.useState(0);
  React.useEffect(() => {
    const t = setInterval(() => setNowTick(n => n + 1), 60000);
    return () => clearInterval(t);
  }, []);

  // 24h gating for the "late" phase. Engagement-pausing (mid) has no rate limit.
  const lateLockedUntil = (() => {
    if (!localLastScaledAt) return null;
    const ms = new Date(localLastScaledAt).getTime() + 24 * 3600 * 1000;
    return ms > Date.now() ? ms : null;
  })();
  const lateRemainingLabel = lateLockedUntil ? (() => {
    const remMs = lateLockedUntil - Date.now();
    const hrs = Math.floor(remMs / 3600000);
    const mins = Math.floor((remMs % 3600000) / 60000);
    return hrs >= 1 ? `${hrs}h ${mins}m` : `${mins}m`;
  })() : null;

  const fmtMoneyLocal = (n) => {
    const cur = campaign?.currency || 'USD';
    try { return new Intl.NumberFormat(lang === 'en' ? 'en-US' : 'es-CL', { style: 'currency', currency: cur, maximumFractionDigits: 0 }).format(Number(n) || 0); }
    catch { return `${cur} ${Math.round(Number(n) || 0)}`; }
  };

  const runApply = async (phase) => {
    if (!campaign?.id || !campaign?.meta_campaign_id) return;
    setApplyingPhase(phase);
    setApplyResult(null);
    try {
      const res = await authFetch(`/api/campaigns/${campaign.id}/apply-optimization`, {
        method: 'POST', body: JSON.stringify({ phase }),
      });
      const data = await res.json();
      if (!res.ok) throw new Error(data.error || 'Error al aplicar');
      setApplyResult({ phase, data });
      // Persist the rate-limit timestamp locally so the button disables right away.
      if (phase === 'late' && data.next_allowed_at) {
        setLocalLastScaledAt(new Date(new Date(data.next_allowed_at).getTime() - 24 * 3600 * 1000).toISOString());
      }
    } catch (e) {
      setApplyResult({ phase, error: e.message || 'Error' });
    }
    setApplyingPhase(null);
  };
  const applyOptimization = (phase) => {
    if (!campaign?.id || !campaign?.meta_campaign_id) return;
    if (phase === 'late' && lateLockedUntil) return; // 24h lock — show error via UI gate
    setConfirmPhase(phase); // open the dedicated confirm modal
  };
  return (
    <div style={{ display: 'flex', flexDirection: 'column', gap: '20px' }}>
      {/* Audiencia */}
      {strategy.audiencia && (
        <section style={{ background: NX.bg3, border: `1px solid ${NX.accent}`, borderRadius: '12px', padding: '16px 18px' }}>
          <div style={{ display: 'flex', alignItems: 'center', gap: '8px', marginBottom: '10px' }}>
            <svg width="16" height="16" viewBox="0 0 24 24" fill="none"><circle cx="9" cy="7" r="4" stroke={NX.accent} strokeWidth="1.6"/><path d="M3 21v-1a6 6 0 0112 0v1M16 3a5 5 0 011 9.8M21 21v-1a6 6 0 00-4-5.65" stroke={NX.accent} strokeWidth="1.6" strokeLinecap="round"/></svg>
            <h3 style={{ fontSize: '14px', fontWeight: 600, color: NX.accent, margin: 0 }}>{lang === 'en' ? 'Target audience' : 'Audiencia objetivo'}</h3>
          </div>
          {strategy.audiencia.resumen && <p style={{ fontSize: '13px', color: NX.text, margin: '0 0 10px', lineHeight: 1.5 }}>{strategy.audiencia.resumen}</p>}
          <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit,minmax(160px,1fr))', gap: '10px', marginBottom: '10px' }}>
            {strategy.audiencia.edad && <div><div style={{ fontSize: '10px', color: NX.muted, textTransform: 'uppercase', marginBottom: '2px' }}>Edad</div><div style={{ fontSize: '14px', color: NX.text, fontWeight: 600 }}>{strategy.audiencia.edad.min}–{strategy.audiencia.edad.max}</div></div>}
            {strategy.audiencia.genero && <div><div style={{ fontSize: '10px', color: NX.muted, textTransform: 'uppercase', marginBottom: '2px' }}>Género</div><div style={{ fontSize: '14px', color: NX.text, fontWeight: 600 }}>{strategy.audiencia.genero === 'all' ? 'Todos' : strategy.audiencia.genero === 'male' ? 'Hombres' : 'Mujeres'}</div></div>}
          </div>
          {strategy.audiencia.intereses_detallados?.length > 0 && (
            <div style={{ marginBottom: '10px' }}>
              <div style={{ fontSize: '10px', color: NX.muted, textTransform: 'uppercase', marginBottom: '4px' }}>Intereses</div>
              <div style={{ display: 'flex', flexWrap: 'wrap', gap: '6px' }}>
                {strategy.audiencia.intereses_detallados.map((t, i) => <span key={i} style={{ fontSize: '11px', padding: '4px 10px', background: 'rgba(139,92,246,0.12)', color: NX.accent, borderRadius: '100px' }}>{t}</span>)}
              </div>
            </div>
          )}
          {strategy.audiencia.comportamientos?.length > 0 && (
            <div style={{ marginBottom: '10px' }}>
              <div style={{ fontSize: '10px', color: NX.muted, textTransform: 'uppercase', marginBottom: '4px' }}>Comportamientos</div>
              <div style={{ display: 'flex', flexWrap: 'wrap', gap: '6px' }}>
                {strategy.audiencia.comportamientos.map((t, i) => <span key={i} style={{ fontSize: '11px', padding: '4px 10px', background: 'rgba(139,92,246,0.12)', color: NX.accent2, borderRadius: '100px' }}>{t}</span>)}
              </div>
            </div>
          )}
          {strategy.audiencia.exclusiones?.length > 0 && (
            <div style={{ marginBottom: '10px' }}>
              <div style={{ fontSize: '10px', color: NX.muted, textTransform: 'uppercase', marginBottom: '4px' }}>Excluir</div>
              <div style={{ display: 'flex', flexWrap: 'wrap', gap: '6px' }}>
                {strategy.audiencia.exclusiones.map((t, i) => <span key={i} style={{ fontSize: '11px', padding: '4px 10px', background: 'rgba(248,113,113,0.1)', color: NX.danger, borderRadius: '100px', textDecoration: 'line-through' }}>{t}</span>)}
              </div>
            </div>
          )}
          {strategy.audiencia.por_que && <div style={{ fontSize: '12px', color: NX.muted, lineHeight: 1.5, padding: '8px 10px', background: NX.bg4, borderRadius: '8px', fontStyle: 'italic' }}>💡 {strategy.audiencia.por_que}</div>}
        </section>
      )}
      {/* Ad Sets — one card per adset the strategy prescribed */}
      {Array.isArray(strategy.adsets) && strategy.adsets.length > 0 && (
        <section style={{ background: NX.bg3, border: `1px solid ${NX.accent2}`, borderRadius: '12px', padding: '16px 18px' }}>
          <div style={{ display: 'flex', alignItems: 'center', gap: '8px', marginBottom: '12px' }}>
            <svg width="16" height="16" viewBox="0 0 24 24" fill="none"><rect x="3" y="3" width="7" height="7" rx="1.5" stroke={NX.accent2} strokeWidth="1.6"/><rect x="14" y="3" width="7" height="7" rx="1.5" stroke={NX.accent2} strokeWidth="1.6"/><rect x="3" y="14" width="7" height="7" rx="1.5" stroke={NX.accent2} strokeWidth="1.6"/></svg>
            <h3 style={{ fontSize: '14px', fontWeight: 600, color: NX.accent2, margin: 0 }}>
              {lang === 'en' ? `Ad Sets (${strategy.adsets.length})` : `Conjuntos de anuncios (${strategy.adsets.length})`}
            </h3>
          </div>
          <div style={{ fontSize: '11px', color: NX.muted, marginBottom: '12px', lineHeight: 1.5 }}>
            {lang === 'en'
              ? 'Every ad set below runs the full creative pool — Meta will identify the winning creative × audience pair.'
              : 'Cada ad set corre todos los creativos elegidos — Meta identifica la mejor combinación creativo × audiencia.'}
          </div>
          <div style={{ display: 'flex', flexDirection: 'column', gap: '10px' }}>
            {strategy.adsets.map((as, i) => (
              <div key={i} style={{ padding: '12px 14px', background: NX.bg4, border: `1px solid ${NX.border}`, borderRadius: '10px' }}>
                <div style={{ display: 'flex', alignItems: 'center', gap: '8px', marginBottom: '8px', flexWrap: 'wrap' }}>
                  <span style={{ fontSize: '11px', fontFamily: "'DM Mono',monospace", color: NX.muted }}>#{i + 1}</span>
                  <span style={{ fontSize: '13px', fontWeight: 600, color: NX.text }}>{as.nombre || as.tipo || `Set ${i + 1}`}</span>
                  {as.tipo && <span style={{ fontSize: '10px', padding: '2px 8px', background: 'rgba(139,92,246,0.12)', color: NX.accent2, borderRadius: '100px', textTransform: 'uppercase', letterSpacing: '0.06em' }}>{as.tipo}</span>}
                </div>
                <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit,minmax(120px,1fr))', gap: '8px', marginBottom: '8px' }}>
                  {as.edad && <div><div style={{ fontSize: '9px', color: NX.muted, textTransform: 'uppercase', marginBottom: '2px' }}>Edad</div><div style={{ fontSize: '12px', color: NX.text, fontWeight: 600 }}>{as.edad.min}–{as.edad.max}</div></div>}
                  {as.genero && <div><div style={{ fontSize: '9px', color: NX.muted, textTransform: 'uppercase', marginBottom: '2px' }}>{lang === 'en' ? 'Gender' : 'Género'}</div><div style={{ fontSize: '12px', color: NX.text, fontWeight: 600 }}>{as.genero === 'all' ? (lang === 'en' ? 'All' : 'Todos') : as.genero === 'male' ? (lang === 'en' ? 'Men' : 'Hombres') : (lang === 'en' ? 'Women' : 'Mujeres')}</div></div>}
                </div>
                {as.intereses_detallados?.length > 0 && (
                  <div style={{ marginBottom: '6px' }}>
                    <div style={{ fontSize: '9px', color: NX.muted, textTransform: 'uppercase', marginBottom: '3px' }}>{lang === 'en' ? 'Interests' : 'Intereses'}</div>
                    <div style={{ display: 'flex', flexWrap: 'wrap', gap: '5px' }}>
                      {as.intereses_detallados.map((t, j) => <span key={j} style={{ fontSize: '10px', padding: '3px 8px', background: 'rgba(139,92,246,0.12)', color: NX.accent, borderRadius: '100px' }}>{t}</span>)}
                    </div>
                  </div>
                )}
                {as.comportamientos?.length > 0 && (
                  <div style={{ marginBottom: '6px' }}>
                    <div style={{ fontSize: '9px', color: NX.muted, textTransform: 'uppercase', marginBottom: '3px' }}>{lang === 'en' ? 'Behaviors' : 'Comportamientos'}</div>
                    <div style={{ display: 'flex', flexWrap: 'wrap', gap: '5px' }}>
                      {as.comportamientos.map((t, j) => <span key={j} style={{ fontSize: '10px', padding: '3px 8px', background: 'rgba(139,92,246,0.12)', color: NX.accent2, borderRadius: '100px' }}>{t}</span>)}
                    </div>
                  </div>
                )}
                {as.exclusiones?.length > 0 && (
                  <div style={{ marginBottom: '6px' }}>
                    <div style={{ fontSize: '9px', color: NX.muted, textTransform: 'uppercase', marginBottom: '3px' }}>{lang === 'en' ? 'Exclude' : 'Excluir'}</div>
                    <div style={{ display: 'flex', flexWrap: 'wrap', gap: '5px' }}>
                      {as.exclusiones.map((t, j) => <span key={j} style={{ fontSize: '10px', padding: '3px 8px', background: 'rgba(248,113,113,0.1)', color: NX.danger, borderRadius: '100px', textDecoration: 'line-through' }}>{t}</span>)}
                    </div>
                  </div>
                )}
                {as.por_que && <div style={{ fontSize: '11px', color: NX.muted, lineHeight: 1.5, padding: '6px 8px', background: NX.bg3, borderRadius: '6px', fontStyle: 'italic', marginTop: '6px' }}>💡 {as.por_que}</div>}
              </div>
            ))}
          </div>
        </section>
      )}
      {/* Presupuesto */}
      {strategy.presupuesto && (
        <section style={{ background: NX.bg3, border: `1px solid ${NX.border}`, borderRadius: '12px', padding: '16px 18px' }}>
          <h3 style={{ fontSize: '14px', fontWeight: 600, color: NX.success, margin: '0 0 10px' }}>💰 {lang === 'en' ? 'Budget plan' : 'Plan de presupuesto'}</h3>
          <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit,minmax(140px,1fr))', gap: '10px', marginBottom: '10px' }}>
            <div><div style={{ fontSize: '10px', color: NX.muted, textTransform: 'uppercase', marginBottom: '2px' }}>Diario recomendado</div><div style={{ fontSize: '16px', color: NX.text, fontWeight: 600, fontFamily: "'DM Mono',monospace" }}>{strategy.presupuesto.moneda} {strategy.presupuesto.diario_recomendado?.toLocaleString()}</div></div>
            <div><div style={{ fontSize: '10px', color: NX.muted, textTransform: 'uppercase', marginBottom: '2px' }}>Fase testing</div><div style={{ fontSize: '14px', color: NX.text, fontWeight: 600 }}>{strategy.presupuesto.fase_testing_dias} días</div></div>
          </div>
          {strategy.presupuesto.distribucion && <p style={{ fontSize: '12px', color: NX.text, margin: '0 0 8px', lineHeight: 1.5 }}>{strategy.presupuesto.distribucion}</p>}
          {strategy.presupuesto.cuando_escalar && <div style={{ fontSize: '11px', color: NX.muted, padding: '8px 10px', background: NX.bg4, borderRadius: '6px' }}>📈 <b>Escalar cuando:</b> {strategy.presupuesto.cuando_escalar}</div>}
        </section>
      )}
      {/* Creativos */}
      {strategy.creativos && (
        <section style={{ background: NX.bg3, border: `1px solid ${NX.border}`, borderRadius: '12px', padding: '16px 18px' }}>
          <h3 style={{ fontSize: '14px', fontWeight: 600, color: NX.accent2, margin: '0 0 10px' }}>🎨 {lang === 'en' ? 'Creatives' : 'Creativos'}</h3>
          {strategy.creativos.angulos_prioritarios?.length > 0 && (
            <div style={{ marginBottom: '12px' }}>
              <div style={{ fontSize: '10px', color: NX.muted, textTransform: 'uppercase', marginBottom: '6px' }}>Ángulos a priorizar</div>
              <ol style={{ margin: 0, paddingLeft: '20px', fontSize: '12px', color: NX.text, lineHeight: 1.6 }}>
                {strategy.creativos.angulos_prioritarios.map((a, i) => <li key={i}>{a}</li>)}
              </ol>
            </div>
          )}
          {strategy.creativos.hook_recomendado && (
            <div style={{ padding: '10px 12px', background: NX.bg4, borderRadius: '8px', marginBottom: '10px' }}>
              <div style={{ fontSize: '10px', color: NX.muted, textTransform: 'uppercase', marginBottom: '3px' }}>Hook sugerido</div>
              <div style={{ fontSize: '14px', color: NX.text, fontWeight: 600, fontStyle: 'italic' }}>"{strategy.creativos.hook_recomendado}"</div>
            </div>
          )}
        </section>
      )}
      {/* Métricas */}
      {strategy.metricas_objetivo && (
        <section style={{ background: NX.bg3, border: `1px solid ${NX.border}`, borderRadius: '12px', padding: '16px 18px' }}>
          <h3 style={{ fontSize: '14px', fontWeight: 600, color: NX.warning, margin: '0 0 10px' }}>📊 {lang === 'en' ? 'Target metrics' : 'Métricas objetivo'}</h3>
          <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit,minmax(140px,1fr))', gap: '10px' }}>
            {strategy.metricas_objetivo.CTR_min && <div><div style={{ fontSize: '10px', color: NX.muted, textTransform: 'uppercase' }}>CTR mín</div><div style={{ fontSize: '14px', color: NX.text, fontWeight: 600, fontFamily: "'DM Mono',monospace" }}>{strategy.metricas_objetivo.CTR_min}</div></div>}
            {strategy.metricas_objetivo.CPC_rango && <div><div style={{ fontSize: '10px', color: NX.muted, textTransform: 'uppercase' }}>CPC</div><div style={{ fontSize: '13px', color: NX.text, fontWeight: 600 }}>{strategy.metricas_objetivo.CPC_rango}</div></div>}
            {strategy.metricas_objetivo.CPM_rango && <div><div style={{ fontSize: '10px', color: NX.muted, textTransform: 'uppercase' }}>CPM</div><div style={{ fontSize: '13px', color: NX.text, fontWeight: 600 }}>{strategy.metricas_objetivo.CPM_rango}</div></div>}
            {strategy.metricas_objetivo.alcance_diario_estimado && <div style={{ gridColumn: '1 / -1' }}><div style={{ fontSize: '10px', color: NX.muted, textTransform: 'uppercase' }}>Alcance diario estimado</div><div style={{ fontSize: '13px', color: NX.text }}>{strategy.metricas_objetivo.alcance_diario_estimado}</div></div>}
          </div>
        </section>
      )}
      {/* Optimización */}
      {strategy.optimizacion?.decisiones_por_etapa?.length > 0 && (
        <section style={{ background: NX.bg3, border: `1px solid ${NX.border}`, borderRadius: '12px', padding: '16px 18px' }}>
          <h3 style={{ fontSize: '14px', fontWeight: 600, color: NX.text, margin: '0 0 10px' }}>🔄 {lang === 'en' ? 'Optimization roadmap' : 'Plan de optimización'}</h3>
          <div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
            {strategy.optimizacion.decisiones_por_etapa.map((d, i) => {
              // Parse the day range ("4-7" → start=4) so we know when each phase becomes
              // actionable. The first card (Día 1-3) is informational only — Meta is in
              // learning phase, no automation should run yet.
              const startDay = parseInt(String(d.dia || '').match(/\d+/)?.[0] || '0', 10);
              const phase = startDay >= 8 ? 'late' : startDay >= 4 ? 'mid' : null;
              const isPreLaunch = daysSinceLaunch === null;
              const isLateLocked = phase === 'late' && !!lateLockedUntil;
              const isReady = !isPreLaunch && phase && daysSinceLaunch >= startDay && !isLateLocked;
              const isBusy = applyingPhase === phase;
              const wasApplied = applyResult && applyResult.phase === phase && !applyResult.error;
              return (
                <div key={i} style={{ display: 'flex', flexDirection: 'column', gap: '8px', padding: '8px 10px', background: NX.bg4, borderRadius: '8px' }}>
                  <div style={{ display: 'flex', gap: '10px', fontSize: '12px' }}>
                    <div style={{ color: NX.accent, fontWeight: 700, fontFamily: "'DM Mono',monospace", flexShrink: 0, minWidth: '55px' }}>Día {d.dia}</div>
                    <div style={{ color: NX.text, flex: 1 }}>{d.accion}</div>
                  </div>
                  {phase && campaign?.meta_campaign_id && (
                    <div style={{ display: 'flex', alignItems: 'center', gap: '10px', paddingLeft: '65px' }}>
                      <button
                        onClick={() => isReady && !isBusy && applyOptimization(phase)}
                        disabled={!isReady || isBusy || wasApplied}
                        title={
                          isPreLaunch
                            ? (lang === 'en' ? 'Available after the campaign is launched' : 'Disponible después de lanzar la campaña')
                            : isLateLocked
                              ? (lang === 'en' ? `Already scaled today — wait ${lateRemainingLabel} before scaling again` : `Ya se escaló hoy — espera ${lateRemainingLabel} antes de volver a escalar`)
                              : !isReady
                                ? (lang === 'en' ? `Available on day ${startDay} (in ${startDay - daysSinceLaunch} day(s))` : `Disponible el día ${startDay} (en ${startDay - daysSinceLaunch} día(s))`)
                                : phase === 'mid'
                                  ? (lang === 'en' ? 'Pause ads with CTR<1% and >1000 impressions' : 'Pausar anuncios con CTR<1% y >1000 impresiones')
                                  : (lang === 'en' ? 'Scale active ad sets +25%' : 'Escalar +25% los conjuntos activos')
                        }
                        style={{
                          padding: '6px 12px',
                          background: wasApplied
                            ? 'rgba(52,211,153,0.18)'
                            : isReady
                              ? 'linear-gradient(135deg, #7c3aed 0%, #8b5cf6 50%, #a78bfa 100%)'
                              : 'rgba(139,92,246,0.10)',
                          border: `1px solid ${wasApplied ? NX.success : isReady ? 'transparent' : 'rgba(139,92,246,0.25)'}`,
                          borderRadius: '8px',
                          color: wasApplied ? NX.success : isReady ? '#fff' : NX.muted,
                          fontSize: '11px', fontWeight: 600,
                          cursor: !isReady ? 'not-allowed' : isBusy ? 'wait' : 'pointer',
                          opacity: !isReady && !wasApplied ? 0.55 : 1,
                          fontFamily: "'DM Sans',sans-serif",
                          display: 'inline-flex', alignItems: 'center', gap: '6px',
                          boxShadow: isReady && !wasApplied ? '0 4px 14px rgba(139,92,246,0.30)' : 'none',
                          transition: 'all 0.15s',
                        }}>
                        {isBusy
                          ? <><svg width="10" height="10" viewBox="0 0 24 24" fill="none" style={{ animation: 'spin 1s linear infinite' }}><circle cx="12" cy="12" r="9" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeDasharray="14 40"/></svg> {lang === 'en' ? 'Applying…' : 'Aplicando…'}</>
                          : wasApplied
                            ? <><svg width="10" height="10" viewBox="0 0 12 12" fill="none"><path d="M2 6l3 3 5-7" stroke="currentColor" strokeWidth="2" strokeLinecap="round"/></svg> {lang === 'en' ? 'Applied' : 'Aplicado'}</>
                            : <><svg width="10" height="10" viewBox="0 0 16 16" fill="none"><path d="M3 8l3 3 7-8" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"/></svg> {lang === 'en' ? 'Apply plan' : 'Aplicar plan'}</>}
                      </button>
                      {isLateLocked && !wasApplied && (
                        <span style={{ fontSize: '10px', color: NX.warning, fontFamily: "'DM Mono',monospace" }}>
                          {lang === 'en' ? `🔒 24h lock — ${lateRemainingLabel} left` : `🔒 Bloqueado 24h — ${lateRemainingLabel} restantes`}
                        </span>
                      )}
                      {!isReady && !wasApplied && !isPreLaunch && !isLateLocked && (
                        <span style={{ fontSize: '10px', color: NX.muted, fontFamily: "'DM Mono',monospace" }}>
                          {lang === 'en' ? `Available in ${startDay - daysSinceLaunch} d` : `Disponible en ${startDay - daysSinceLaunch} d`}
                        </span>
                      )}
                      {isPreLaunch && (
                        <span style={{ fontSize: '10px', color: NX.muted, fontStyle: 'italic' }}>
                          {lang === 'en' ? 'After launch' : 'Tras el lanzamiento'}
                        </span>
                      )}
                    </div>
                  )}
                  {/* Result feedback for this phase */}
                  {applyResult && applyResult.phase === phase && (
                    <div style={{ paddingLeft: '65px', fontSize: '11px', color: applyResult.error ? NX.danger : NX.success, lineHeight: 1.5 }}>
                      {applyResult.error
                        ? `⚠ ${applyResult.error}`
                        : applyResult.phase === 'mid'
                          ? (lang === 'en' ? `${applyResult.data?.paused?.length || 0} ad(s) paused (${applyResult.data?.total_evaluated || 0} evaluated)` : `${applyResult.data?.paused?.length || 0} anuncio(s) pausado(s) (${applyResult.data?.total_evaluated || 0} evaluados)`)
                          : (lang === 'en' ? `${applyResult.data?.scaled?.length || 0} ad set(s) scaled +25%` : `${applyResult.data?.scaled?.length || 0} conjunto(s) escalado(s) +25%`)}
                    </div>
                  )}
                </div>
              );
            })}
          </div>
          {strategy.optimizacion.senales_de_alerta?.length > 0 && (
            <div style={{ marginTop: '10px' }}>
              <div style={{ fontSize: '10px', color: NX.danger, textTransform: 'uppercase', marginBottom: '4px' }}>⚠ Señales para pausar</div>
              <ul style={{ margin: 0, paddingLeft: '18px', fontSize: '12px', color: NX.muted, lineHeight: 1.6 }}>
                {strategy.optimizacion.senales_de_alerta.map((s, i) => <li key={i}>{s}</li>)}
              </ul>
            </div>
          )}
        </section>
      )}
      {/* Riesgos */}
      {strategy.riesgos?.length > 0 && (
        <section style={{ background: 'rgba(248,113,113,0.05)', border: `1px solid rgba(248,113,113,0.3)`, borderRadius: '12px', padding: '14px 16px' }}>
          <h3 style={{ fontSize: '13px', fontWeight: 600, color: NX.danger, margin: '0 0 8px' }}>⚠ {lang === 'en' ? 'Risks to watch' : 'Riesgos a vigilar'}</h3>
          <ul style={{ margin: 0, paddingLeft: '18px', fontSize: '12px', color: NX.text, lineHeight: 1.55 }}>
            {strategy.riesgos.map((r, i) => <li key={i}>{r}</li>)}
          </ul>
        </section>
      )}

      {/* Confirm modal — replaces window.confirm. Shows the financial impact (current daily
          → new daily, monthly estimate) so the user understands what Meta will charge before
          authorizing. Mid phase has no money impact (only pauses), so its modal is simpler. */}
      {confirmPhase && (() => {
        const isLate = confirmPhase === 'late';
        const currentDaily = Number(campaign?.daily_budget) || 0;
        const newDaily = isLate ? Math.round(currentDaily * 1.25) : currentDaily;
        const monthlyEstimate = newDaily * 30;
        return (
          <div onClick={() => setConfirmPhase(null)}
            style={{ position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.78)', backdropFilter: 'blur(8px)', zIndex: 3000, display: 'flex', alignItems: 'center', justifyContent: 'center', padding: '24px' }}>
            <div onClick={e => e.stopPropagation()}
              style={{ background: NX.bg2, border: `1px solid ${isLate ? NX.warning : NX.accent}`, borderRadius: '16px', maxWidth: '460px', width: '100%', padding: '24px', animation: 'modalIn 0.18s ease', boxShadow: '0 30px 80px rgba(0,0,0,0.65), 0 0 40px rgba(139,92,246,0.18)' }}>
              <div style={{ display: 'flex', alignItems: 'center', gap: '10px', marginBottom: '12px' }}>
                <div style={{ width: '36px', height: '36px', borderRadius: '50%', background: isLate ? 'rgba(251,191,36,0.16)' : 'rgba(139,92,246,0.14)', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
                  {isLate
                    ? <svg width="18" height="18" viewBox="0 0 24 24" fill="none"><path d="M12 9v4M12 17.5v.01M3.5 19.5h17l-8.5-15-8.5 15z" stroke={NX.warning} strokeWidth="1.7" strokeLinecap="round" strokeLinejoin="round"/></svg>
                    : <svg width="18" height="18" viewBox="0 0 24 24" fill="none"><path d="M9 12l2 2 4-4M21 12a9 9 0 11-18 0 9 9 0 0118 0z" stroke={NX.accent} strokeWidth="1.7" strokeLinecap="round" strokeLinejoin="round"/></svg>}
                </div>
                <h3 style={{ fontSize: '17px', fontWeight: 600, color: NX.text, margin: 0 }}>
                  {isLate
                    ? (lang === 'en' ? 'Confirm budget scaling +25%' : 'Confirmar escalado +25% de presupuesto')
                    : (lang === 'en' ? 'Pause low-CTR ads' : 'Pausar anuncios con bajo CTR')}
                </h3>
              </div>

              {isLate ? (<>
                <div style={{ background: NX.bg3, border: `1px solid ${NX.border}`, borderRadius: '10px', padding: '14px', marginBottom: '12px' }}>
                  <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '8px' }}>
                    <span style={{ fontSize: '11px', color: NX.muted, fontFamily: "'DM Mono',monospace", letterSpacing: '0.08em', textTransform: 'uppercase' }}>{lang === 'en' ? 'Current daily' : 'Diario actual'}</span>
                    <span style={{ fontSize: '14px', color: NX.text, fontWeight: 600 }}>{fmtMoneyLocal(currentDaily)}</span>
                  </div>
                  <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '8px', paddingTop: '8px', borderTop: `1px dashed ${NX.border}` }}>
                    <span style={{ fontSize: '11px', color: NX.warning, fontFamily: "'DM Mono',monospace", letterSpacing: '0.08em', textTransform: 'uppercase' }}>{lang === 'en' ? 'New daily' : 'Diario nuevo'}</span>
                    <span style={{ fontSize: '17px', color: NX.warning, fontWeight: 700 }}>{fmtMoneyLocal(newDaily)}</span>
                  </div>
                  <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', fontSize: '11px', color: NX.muted }}>
                    <span>{lang === 'en' ? 'Estimated monthly' : 'Estimación mensual'}</span>
                    <span style={{ fontWeight: 600, color: NX.text }}>≈ {fmtMoneyLocal(monthlyEstimate)}</span>
                  </div>
                </div>
                <div style={{ background: 'rgba(251,191,36,0.06)', border: `1px solid rgba(251,191,36,0.3)`, borderRadius: '10px', padding: '12px 14px', marginBottom: '14px' }}>
                  <p style={{ fontSize: '12px', color: NX.text, margin: 0, lineHeight: 1.55 }}>
                    {lang === 'en'
                      ? <><b style={{ color: NX.warning }}>⚠ Meta will charge this new daily rate immediately</b> on your ad account, every day, until you change it again. NexWall does NOT process this payment — billing is direct to your Meta payment method.</>
                      : <><b style={{ color: NX.warning }}>⚠ Meta cobrará esta nueva tarifa diaria de inmediato</b> en tu cuenta publicitaria, cada día, hasta que la vuelvas a cambiar. NexWall NO procesa este cobro — el cargo lo realiza Meta directamente desde tu método de pago.</>}
                  </p>
                </div>
                <div style={{ background: 'rgba(139,92,246,0.06)', border: `1px solid rgba(139,92,246,0.25)`, borderRadius: '8px', padding: '8px 12px', marginBottom: '14px', display: 'flex', alignItems: 'center', gap: '8px' }}>
                  <svg width="14" height="14" viewBox="0 0 24 24" fill="none"><circle cx="12" cy="12" r="10" stroke={NX.accent} strokeWidth="1.5"/><path d="M12 7v5l3 2" stroke={NX.accent} strokeWidth="1.5" strokeLinecap="round"/></svg>
                  <span style={{ fontSize: '11px', color: NX.muted, lineHeight: 1.4 }}>
                    {lang === 'en' ? 'Bonus protection: only 1 scale per 24 hours per campaign — no accidental compounding.' : 'Protección extra: solo 1 escalado cada 24 horas por campaña — sin compounding accidental.'}
                  </span>
                </div>
              </>) : (
                <p style={{ fontSize: '13px', color: NX.text, lineHeight: 1.6, margin: '0 0 14px' }}>
                  {lang === 'en'
                    ? 'NexWall will pause every ad with CTR below 1% AND more than 1000 impressions. This stops wasted spend without changing your daily budget.'
                    : 'NexWall pausará cada anuncio con CTR menor al 1% Y más de 1000 impresiones. Esto detiene el gasto desperdiciado sin cambiar tu presupuesto diario.'}
                </p>
              )}

              <div style={{ display: 'flex', gap: '10px' }}>
                <button onClick={() => setConfirmPhase(null)}
                  style={{ flex: 1, padding: '11px 14px', background: 'transparent', border: `1px solid ${NX.border}`, borderRadius: '10px', color: NX.muted, fontSize: '13px', fontWeight: 500, cursor: 'pointer', fontFamily: "'DM Sans',sans-serif" }}>
                  {lang === 'en' ? 'Cancel' : 'Cancelar'}
                </button>
                <button onClick={() => { const p = confirmPhase; setConfirmPhase(null); runApply(p); }}
                  style={{ flex: 2, padding: '11px 14px',
                    background: isLate ? 'linear-gradient(135deg, #f59e0b, #fbbf24)' : 'linear-gradient(135deg, #7c3aed 0%, #8b5cf6 50%, #a78bfa 100%)',
                    border: 'none', borderRadius: '10px', color: '#fff', fontSize: '13px', fontWeight: 600, cursor: 'pointer', fontFamily: "'DM Sans',sans-serif",
                    boxShadow: isLate ? '0 8px 24px rgba(251,191,36,0.35)' : '0 8px 24px rgba(139,92,246,0.35)' }}>
                  {isLate
                    ? (lang === 'en' ? `Confirm — scale to ${fmtMoneyLocal(newDaily)}/day` : `Confirmar — escalar a ${fmtMoneyLocal(newDaily)}/día`)
                    : (lang === 'en' ? 'Confirm — pause low-CTR ads' : 'Confirmar — pausar anuncios')}
                </button>
              </div>
            </div>
          </div>
        );
      })()}
    </div>
  );
};

// Resumen de créditos consumidos en una campaña — se muestra en el modal de detalle.
// Filtra el ledger por ref_type='campaign' AND ref_id=campaignId. El backend ya devuelve
// metadata por transacción, así que agrupamos por action en cliente.
const CampaignCreditsSummary = ({ campaignId, lang='es' }) => {
  const [data, setData] = React.useState(null);
  React.useEffect(() => {
    if (!campaignId) return;
    let cancelled = false;
    (async () => {
      try {
        const res = await authFetch('/api/credits/transactions?limit=200');
        if (!res.ok) return;
        const j = await res.json();
        const matches = (j.transactions || []).filter(t => t.ref_type === 'campaign' && String(t.ref_id) === String(campaignId));
        const total = matches.reduce((s, t) => s + (t.delta < 0 ? -t.delta : 0), 0);
        const refunded = matches.reduce((s, t) => s + (t.action === 'refund' ? t.delta : 0), 0);
        const breakdown = {};
        matches.forEach(t => {
          if (t.delta < 0) {
            breakdown[t.action] = (breakdown[t.action] || 0) + (-t.delta);
          }
        });
        if (!cancelled) setData({ total, refunded, breakdown, count: matches.length });
      } catch {}
    })();
    return () => { cancelled = true; };
  }, [campaignId]);

  if (!data || data.count === 0) return null;
  const labelByAction = lang === 'en' ? {
    strategy_gen: 'Strategy', image_premium: 'Premium images', image_ultra_max: 'Ultra MAX images',
    copies_batch: 'Copies', text_regen: 'Text regen', brand_dna: 'Brand identity generation', campaign_relaunch: 'Re-launch',
  } : {
    strategy_gen: 'Estrategia', image_premium: 'Imágenes Premium', image_ultra_max: 'Imágenes Ultra MAX',
    copies_batch: 'Copies', text_regen: 'Regen texto', brand_dna: 'Generación de identidad de tu marca', campaign_relaunch: 'Re-lanzar',
  };
  const net = data.total - (data.refunded || 0);
  return (
    <div style={{ marginTop: '12px', padding: '10px 14px', background: 'rgba(139,92,246,0.08)', border: `1px solid ${NX.border2}`, borderRadius: '10px', display: 'flex', alignItems: 'center', gap: '14px', flexWrap: 'wrap' }}>
      <div style={{ display: 'flex', alignItems: 'baseline', gap: '6px' }}>
        <span style={{ fontSize: '11px', color: NX.muted, fontFamily: "'DM Mono',monospace", letterSpacing: '0.08em', textTransform: 'uppercase' }}>
          {lang === 'en' ? 'Credits used' : 'Créditos consumidos'}
        </span>
        <span style={{ fontSize: '15px', color: NX.accent2, fontWeight: 600, fontFamily: "'DM Mono',monospace" }}>{net}</span>
        {data.refunded > 0 && (
          <span style={{ fontSize: '10px', color: NX.success, fontFamily: "'DM Mono',monospace" }}>
            ({lang === 'en' ? 'incl. ' : 'incl. '}{data.refunded} {lang === 'en' ? 'refunded' : 'reembolsados'})
          </span>
        )}
      </div>
      <div style={{ display: 'flex', gap: '10px', fontSize: '11px', color: NX.muted, flexWrap: 'wrap' }}>
        {Object.entries(data.breakdown).map(([action, amount]) => (
          <span key={action}>
            <span style={{ color: NX.text }}>{labelByAction[action] || action}</span>: {amount}
          </span>
        ))}
      </div>
    </div>
  );
};

const MisEstrategias = ({ campaigns, setCampaigns, setView, lang='es', onLaunchDraft }) => {
  const te = (I18N && I18N[lang]) ? I18N[lang].estrategias : I18N.es.estrategias;
  // Tabs: 'active' (running en Meta), 'paused' (pausadas en Meta), 'archived' (soft-deleted)
  // y 'all' (drafts y todo lo demás). El user puede navegar entre las 3 vistas.
  const [activeTab, setActiveTab] = React.useState('active');
  // Filter campaigns por tab. Soft-delete: status='archived' → tab "borradas".
  // Pause: status='paused' → tab "pausadas". Active: status='active' → tab "activas".
  // Drafts y otros aparecen en "activas" (son borradores que no han ido a Meta).
  const filteredCampaigns = React.useMemo(() => {
    return campaigns.filter(c => {
      if (activeTab === 'active') return c.status === 'active' || c.status === 'draft' || (!c.status);
      if (activeTab === 'paused') return c.status === 'paused';
      if (activeTab === 'archived') return c.status === 'archived';
      return true;
    });
  }, [campaigns, activeTab]);
  const tabCounts = React.useMemo(() => ({
    active:   campaigns.filter(c => c.status === 'active' || c.status === 'draft' || !c.status).length,
    paused:   campaigns.filter(c => c.status === 'paused').length,
    archived: campaigns.filter(c => c.status === 'archived').length,
  }), [campaigns]);
  const n = filteredCampaigns.length;
  const countLabel = `${n} ${n !== 1 ? te.campaignPlural : te.campaign} ${n !== 1 ? te.activePlural : te.active}`;
  const [deletingId, setDeletingId] = React.useState(null);
  const [confirmId, setConfirmId] = React.useState(null);
  const [restoringId, setRestoringId] = React.useState(null);
  const [confirmHardId, setConfirmHardId] = React.useState(null);
  const [hardDeletingId, setHardDeletingId] = React.useState(null);
  const [detailCampaign, setDetailCampaign] = React.useState(null);
  const isMobile = useIsMobile(680);
  const [openMenuId, setOpenMenuId] = React.useState(null);
  const tx = lang === 'en'
    ? { delete: 'Archive', analyze: 'Analyze campaign', confirm: 'Yes, archive', cancel: 'Cancel', view: 'View details', creatives: 'Campaign details', creativesSection: 'Ad creatives', noCreatives: 'No creatives saved for this campaign', product: 'Product description', copy: 'Ad text', cta: 'CTA', close: 'Close', strategy: 'Strategy', restore: 'Restore', hardDelete: 'Delete forever', confirmHard: 'Yes, delete forever' }
    : { delete: 'Archivar', analyze: 'Analizar campaña', confirm: 'Sí, archivar', cancel: 'Cancelar', view: 'Ver detalles', creatives: 'Detalles de la campaña', creativesSection: 'Creativos del anuncio', noCreatives: 'No hay creativos guardados para esta campaña', product: 'Descripción del producto', copy: 'Texto del anuncio', cta: 'CTA', close: 'Cerrar', strategy: 'Estrategia', restore: 'Restaurar', hardDelete: 'Borrar para siempre', confirmHard: 'Sí, borrar para siempre' };

  const [togglingId, setTogglingId] = React.useState(null);
  // Two-click confirmation for pause/resume — same pattern as the delete button. First click
  // arms ("¿Pausar?"/"¿Reanudar?"), second click commits. Auto-disarms in 4s. Prevents
  // accidental taps that would change the campaign's run state on Meta.
  const [confirmToggleId, setConfirmToggleId] = React.useState(null);
  const armToggle = (campaignId) => {
    setConfirmToggleId(campaignId);
    setTimeout(() => setConfirmToggleId(prev => (prev === campaignId ? null : prev)), 4000);
  };
  // ──────────── Optimization actions (per-card "Aplicar plan" button) ────────────
  // Dynamic state per card based on (a) days since launch and (b) last_scaled_at rate-limit.
  // The "applyOptCampaign" state holds whichever campaign currently has its confirm modal
  // open. "applyingOptId" prevents double-clicks while the API call is in flight.
  const [applyOptCampaign, setApplyOptCampaign] = React.useState(null); // { campaign, phase }
  const [applyingOptId, setApplyingOptId] = React.useState(null);
  const [optResultByCampaign, setOptResultByCampaign] = React.useState({}); // { [id]: { phase, paused/scaled, error } }
  // Live KPI status fetched when the late-phase modal opens — lets the user see whether the
  // strategy criteria are met BEFORE confirming, instead of finding out after a 412.
  const [optStatus, setOptStatus] = React.useState(null); // { loading, eligible, ctr, ctr_min, ... }
  React.useEffect(() => {
    if (!applyOptCampaign || applyOptCampaign.phase !== 'late') { setOptStatus(null); return; }
    let cancelled = false;
    setOptStatus({ loading: true });
    (async () => {
      try {
        const res = await authFetch(`/api/campaigns/${applyOptCampaign.campaign.id}/optimization-status`);
        const data = await res.json();
        if (!cancelled) setOptStatus({ ...data, loading: false });
      } catch (e) {
        if (!cancelled) setOptStatus({ loading: false, error: e.message || 'Error' });
      }
    })();
    return () => { cancelled = true; };
  }, [applyOptCampaign]);
  // Tick every minute so the late-phase countdown re-renders without manual invalidation.
  const [, setOptNowTick] = React.useState(0);
  React.useEffect(() => {
    const t = setInterval(() => setOptNowTick(n => n + 1), 60000);
    return () => clearInterval(t);
  }, []);
  // Returns the action state for a given campaign card based on date-elapsed + rate-limit.
  // Possible states: 'pre-launch' | 'wait' | 'mid' | 'late-locked' | 'late' | 'draft'
  const optimizationStateFor = (c) => {
    if (c.status === 'draft' || !c.meta_campaign_id) return { kind: 'draft' };
    if (!c.created_at) return { kind: 'pre-launch' };
    const days = Math.floor((Date.now() - new Date(c.created_at).getTime()) / 86400000);
    if (days < 4) return { kind: 'wait', daysLeft: 4 - days };
    if (days < 8) return { kind: 'mid', days };
    // days >= 8: late phase (24h-rate-limited)
    if (c.last_scaled_at) {
      const remMs = new Date(c.last_scaled_at).getTime() + 86400000 - Date.now();
      if (remMs > 0) {
        const hrs = Math.floor(remMs / 3600000);
        const mins = Math.floor((remMs % 3600000) / 60000);
        return { kind: 'late-locked', label: hrs >= 1 ? `${hrs}h ${mins}m` : `${mins}m` };
      }
    }
    return { kind: 'late', days };
  };
  const runOptimization = async (campaign, phase) => {
    setApplyingOptId(campaign.id);
    try {
      const res = await authFetch(`/api/campaigns/${campaign.id}/apply-optimization`, {
        method: 'POST', body: JSON.stringify({ phase }),
      });
      const data = await res.json();
      if (!res.ok) {
        // Surface CRITERIA_NOT_MET (412) with its diagnostics intact so the inline feedback
        // can show the actual CTR/impressions/conversions numbers. Same with RATE_LIMIT (429).
        const err = new Error(data.error || 'Error al aplicar');
        err.code = data.code;
        err.diagnostics = data.diagnostics;
        throw err;
      }
      // For 'late' phase update the local list with the new daily_budget total + last_scaled_at
      // so the UI reflects the change without refetch. Sum the scaled new budgets back to major.
      if (phase === 'late' && Array.isArray(data.scaled) && data.scaled.length) {
        const ZERO_DECIMAL = new Set(['CLP','JPY','KRW','VND','PYG','XAF','XOF','UGX','RWF','MGA','KMF','DJF','BIF','GNF','VUV']);
        const cur = (campaign.currency || 'USD').toUpperCase();
        const newMinor = data.scaled.reduce((sum, s) => sum + Number(s.to || 0), 0);
        const newMajor = ZERO_DECIMAL.has(cur) ? newMinor : newMinor / 100;
        const lastScaledAt = data.next_allowed_at
          ? new Date(new Date(data.next_allowed_at).getTime() - 86400000).toISOString()
          : new Date().toISOString();
        if (setCampaigns) {
          setCampaigns(prev => prev.map(x => x.id === campaign.id
            ? { ...x, daily_budget: newMajor, last_scaled_at: lastScaledAt }
            : x));
        }
      }
      setOptResultByCampaign(prev => ({ ...prev, [campaign.id]: { phase, data } }));
    } catch (e) {
      setOptResultByCampaign(prev => ({ ...prev, [campaign.id]: { phase, error: e.message || 'Error', code: e.code, diagnostics: e.diagnostics } }));
    }
    setApplyingOptId(null);
  };
  const fmtMoneyCard = (n, currency) => {
    const cur = currency || 'USD';
    try { return new Intl.NumberFormat(lang === 'en' ? 'en-US' : 'es-CL', { style: 'currency', currency: cur, maximumFractionDigits: 0 }).format(Number(n) || 0); }
    catch { return `${cur} ${Math.round(Number(n) || 0)}`; }
  };

  // ──────────── Analyze campaign (popup con recomendaciones + selección manual) ────────────
  // Estado completo del flow Analyze. El user abre el modal con setAnalyzeCampaign(c),
  // el effect dispara POST /analyze, renderiza ganadores/perdedores/recomendaciones,
  // el user marca checkboxes y aplica con POST /apply-analysis-actions.
  const [analyzeCampaign, setAnalyzeCampaign] = React.useState(null);
  const [analyzeData, setAnalyzeData] = React.useState(null);
  const [analyzing, setAnalyzing] = React.useState(false);
  const [analyzeError, setAnalyzeError] = React.useState(null);   // { msg, code, impressions?, impressions_min? }
  const [selectedActions, setSelectedActions] = React.useState({}); // { [actionKey]: true }
  const [applyingActions, setApplyingActions] = React.useState(false);
  const [applyResult, setApplyResult] = React.useState(null);     // { results: [...], succeeded, failed } | null
  // Modo del modal: null inicial / 'picker' (lista de análisis guardados + opción nuevo) /
  // 'saved' (viendo uno cacheado) / 'fresh' (corriendo uno nuevo en Meta).
  const [analyzeMode, setAnalyzeMode] = React.useState(null);
  const [savedAnalyses, setSavedAnalyses] = React.useState([]); // array DESC ordenado por created_at
  const [viewingSavedIndex, setViewingSavedIndex] = React.useState(null); // índice en savedAnalyses cuando mode='saved'
  const [checkingSaved, setCheckingSaved] = React.useState(false);
  // Cooldown 24h entre /analyze. Sincronizado vía GET /analyses + actualizado al
  // recibir 429 ANALYZE_COOLDOWN. Tick every minute para refrescar el countdown.
  const [cooldown, setCooldown] = React.useState({ active: false, next_allowed_at: null, remaining_ms: 0 });
  const [, setCooldownTick] = React.useState(0);
  React.useEffect(() => {
    if (!cooldown.active || !cooldown.next_allowed_at) return;
    const t = setInterval(() => setCooldownTick(n => n + 1), 60000);
    return () => clearInterval(t);
  }, [cooldown.active, cooldown.next_allowed_at]);
  // Computed remaining derived from next_allowed_at (recalculates on each tick).
  const cooldownRemainingMs = cooldown.next_allowed_at
    ? Math.max(0, new Date(cooldown.next_allowed_at).getTime() - Date.now())
    : 0;
  const cooldownActive = cooldown.active && cooldownRemainingMs > 0;
  const fmtCooldown = (ms) => {
    const total = Math.ceil(ms / 60000);
    if (total >= 60) {
      const h = Math.floor(total / 60);
      const m = total % 60;
      return lang === 'en' ? `${h}h ${m}m` : `${h}h ${m}m`;
    }
    return lang === 'en' ? `${total}m` : `${total}m`;
  };

  // Helper: corre el flow fresh (POST /analyze) — invocable desde el picker o un retry.
  // Al éxito, anteponemos el nuevo análisis al array de guardados para mantener orden DESC.
  const runFreshAnalyze = React.useCallback(async (campaignId) => {
    setAnalyzeMode('fresh');
    setViewingSavedIndex(null);
    setAnalyzing(true); setAnalyzeError(null); setAnalyzeData(null);
    setSelectedActions({}); setApplyResult(null);
    try {
      const res = await authFetch(`/api/campaigns/${campaignId}/analyze`, {
        method: 'POST',
        body: JSON.stringify({ lang }),
      });
      const data = await res.json().catch(() => ({}));
      if (!res.ok) {
        // Cooldown: sincronizar el estado para que la UI muestre el countdown
        if (data.code === 'ANALYZE_COOLDOWN' && data.next_allowed_at) {
          setCooldown({ active: true, next_allowed_at: data.next_allowed_at, remaining_ms: data.remaining_ms || 0 });
        }
        setAnalyzeError({
          msg: data.error || (lang === 'en' ? 'Could not analyze the campaign' : 'No se pudo analizar la campaña'),
          code: data.code,
          impressions: data.impressions,
          impressions_min: data.impressions_min,
          next_allowed_at: data.next_allowed_at,
        });
      } else {
        setAnalyzeData(data);
        setSavedAnalyses(prev => [{ ...data, age_seconds: 0 }, ...prev]);
        // Acabamos de generar uno nuevo → cooldown empieza otra vez (24h desde ahora)
        setCooldown({
          active: true,
          next_allowed_at: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(),
          remaining_ms: 24 * 60 * 60 * 1000,
        });
      }
    } catch (e) {
      setAnalyzeError({ msg: e.message || 'Error' });
    }
    setAnalyzing(false);
  }, [lang]);

  React.useEffect(() => {
    if (!analyzeCampaign) {
      setAnalyzeData(null); setAnalyzing(false); setAnalyzeError(null);
      setSelectedActions({}); setApplyResult(null); setApplyingActions(false);
      setAnalyzeMode(null); setSavedAnalyses([]); setViewingSavedIndex(null); setCheckingSaved(false);
      setCooldown({ active: false, next_allowed_at: null, remaining_ms: 0 });
      return;
    }
    let cancelled = false;
    setCheckingSaved(true); setAnalyzeMode(null);
    setAnalyzing(false); setAnalyzeError(null); setAnalyzeData(null);
    setSelectedActions({}); setApplyResult(null); setSavedAnalyses([]); setViewingSavedIndex(null);
    setCooldown({ active: false, next_allowed_at: null, remaining_ms: 0 });
    (async () => {
      try {
        const res = await authFetch(`/api/campaigns/${analyzeCampaign.id}/analyses`);
        if (cancelled) return;
        if (res.ok) {
          const data = await res.json();
          const list = Array.isArray(data.analyses) ? data.analyses : [];
          if (data.cooldown) setCooldown(data.cooldown);
          if (list.length > 0) {
            setSavedAnalyses(list);
            setAnalyzeMode('picker');
            setCheckingSaved(false);
            return;
          }
        }
      } catch {}
      if (cancelled) return;
      setCheckingSaved(false);
      // Sin guardados → fresh directo (no hay cooldown que respetar)
      runFreshAnalyze(analyzeCampaign.id);
    })();
    return () => { cancelled = true; };
  }, [analyzeCampaign, lang, runFreshAnalyze]);

  // Selecciona un análisis del historial para verlo (sin tocar Meta, sin créditos).
  const viewSavedAnalysis = (idx) => {
    const a = savedAnalyses[idx];
    if (!a) return;
    setAnalyzeData(a);
    setViewingSavedIndex(idx);
    setAnalyzeMode('saved');
    setSelectedActions({}); setApplyResult(null);
  };

  // Format relative age — "hace 2 min" / "hace 3 h" / "hace 2 d".
  const fmtAge = (ageSeconds) => {
    const s = Number(ageSeconds || 0);
    if (s < 60) return lang === 'en' ? 'just now' : 'recién';
    if (s < 3600) {
      const m = Math.floor(s / 60);
      return lang === 'en' ? `${m} min ago` : `hace ${m} min`;
    }
    if (s < 86400) {
      const h = Math.floor(s / 3600);
      return lang === 'en' ? `${h}h ago` : `hace ${h}h`;
    }
    const d = Math.floor(s / 86400);
    return lang === 'en' ? `${d}d ago` : `hace ${d}d`;
  };

  const toggleAction = (key) => setSelectedActions(prev => ({ ...prev, [key]: !prev[key] }));
  const selectedActionCount = Object.values(selectedActions).filter(Boolean).length;

  const applySelectedAnalysisActions = async () => {
    if (!analyzeData || !analyzeCampaign) return;
    const actions = [];
    (analyzeData.losers || []).forEach((l, i) => {
      if (selectedActions[`loser_${i}`] && l.ad_id) actions.push({ type: 'pause_ad', id: l.ad_id });
    });
    (analyzeData.budget_actions || []).forEach((b, i) => {
      if (!selectedActions[`budget_${i}`] || !b.id || !b.to) return;
      const type = b.type === 'scale_campaign' ? 'scale_campaign'
        : (b.type === 'reduce_adset' ? 'reduce_adset' : 'scale_adset');
      actions.push({ type, id: b.id, to: b.to });
    });
    if (actions.length === 0) return;
    setApplyingActions(true);
    try {
      const res = await authFetch(`/api/campaigns/${analyzeCampaign.id}/apply-analysis-actions`, {
        method: 'POST',
        body: JSON.stringify({ actions }),
      });
      const data = await res.json().catch(() => ({}));
      if (!res.ok) {
        setApplyResult({ ok: false, error: data.error || 'Error', results: [] });
      } else {
        setApplyResult(data);
        // Mirror local: budget si cambió, y el nuevo meta_campaign_name (con emoji 🔍)
        // que el backend regeneró si al menos 1 acción funcionó. Así la card y los
        // dropdowns reflejan el nuevo estado sin esperar a refetch.
        if (setCampaigns) {
          const budgetWin = (data.results || []).find(r => r.ok && (r.type === 'scale_campaign' || r.type === 'scale_adset' || r.type === 'reduce_adset'));
          let mirrorPatch = {};
          if (budgetWin) {
            const ZERO_DECIMAL = new Set(['CLP','JPY','KRW','VND','PYG','XAF','XOF','UGX','RWF','MGA','KMF','DJF','BIF','GNF','VUV']);
            const cur = (analyzeCampaign.currency || 'USD').toUpperCase();
            const totalMinor = (data.results || []).filter(r => r.ok && r.type !== 'pause_ad').reduce((sum, r) => sum + Number(r.to || 0), 0);
            if (totalMinor > 0) mirrorPatch.daily_budget = ZERO_DECIMAL.has(cur) ? totalMinor : totalMinor / 100;
          }
          if (data.meta_campaign_name) mirrorPatch.meta_campaign_name = data.meta_campaign_name;
          if (data.last_analyzed_at) mirrorPatch.last_analyzed_at = data.last_analyzed_at;
          if (Object.keys(mirrorPatch).length) {
            setCampaigns(prev => prev.map(x => x.id === analyzeCampaign.id ? { ...x, ...mirrorPatch } : x));
          }
        }
      }
    } catch (e) {
      setApplyResult({ ok: false, error: e.message || 'Error', results: [] });
    }
    setApplyingActions(false);
  };
  const [editingBudgetId, setEditingBudgetId] = React.useState(null); // campaign id whose budget is in inline edit
  const [editingBudgetValue, setEditingBudgetValue] = React.useState('');
  const [savingBudgetId, setSavingBudgetId] = React.useState(null);
  const [budgetError, setBudgetError] = React.useState(null);
  const [confirmingBudget, setConfirmingBudget] = React.useState(false); // armed second-click commits

  const startEditBudget = (c) => {
    setEditingBudgetId(c.id);
    setEditingBudgetValue(String(Number(c.daily_budget) || ''));
    setBudgetError(null);
    setConfirmingBudget(false);
  };
  const cancelEditBudget = () => {
    setEditingBudgetId(null);
    setEditingBudgetValue('');
    setBudgetError(null);
    setConfirmingBudget(false);
  };
  // Two-step save: first click validates and arms confirmation; second click commits.
  // Lets the user see exactly what will be sent to Meta before any spend change happens.
  const handleSavePress = async (c) => {
    const newAmount = Number(editingBudgetValue);
    if (!isFinite(newAmount) || newAmount <= 0) {
      setBudgetError(lang === 'en' ? 'Enter a valid amount' : 'Ingresa un monto válido');
      return;
    }
    if (Number(c.daily_budget) === newAmount) { cancelEditBudget(); return; }
    if (!confirmingBudget) { setBudgetError(null); setConfirmingBudget(true); return; }
    // Confirmed — commit
    setSavingBudgetId(c.id);
    setBudgetError(null);
    try {
      const res = await authFetch(`/api/campaigns/${c.id}/update-budget`, {
        method: 'POST',
        body: JSON.stringify({ daily_budget: newAmount }),
      });
      const data = await res.json().catch(() => ({}));
      if (!res.ok) {
        setBudgetError(data.error || (lang === 'en' ? 'Could not update on Meta' : 'No se pudo actualizar en Meta'));
        setSavingBudgetId(null);
        setConfirmingBudget(false);
        return;
      }
      // Optimistic refresh of the local list — the backend already persisted the new figure.
      if (setCampaigns) {
        setCampaigns(prev => prev.map(x => x.id === c.id ? { ...x, daily_budget: newAmount } : x));
      }
      cancelEditBudget();
    } catch (e) {
      setBudgetError(e.message || 'Error');
    }
    setSavingBudgetId(null);
  };

  // ──────────── Rename manual de campaña ────────────
  // Patrón inline igual que el editor de budget: click ✏️ junto al nombre → input
  // editable → Enter / ✓ guarda en local DB y sincroniza a Meta. Esc cancela.
  const [editingNameId, setEditingNameId] = React.useState(null);
  const [editingNameValue, setEditingNameValue] = React.useState('');
  const [savingNameId, setSavingNameId] = React.useState(null);
  const [nameError, setNameError] = React.useState(null);
  const startEditName = (c) => {
    setEditingNameId(c.id);
    setEditingNameValue(c.meta_campaign_name || c.title || '');
    setNameError(null);
  };
  const cancelEditName = () => {
    setEditingNameId(null);
    setEditingNameValue('');
    setNameError(null);
  };
  const saveName = async (c) => {
    const newName = String(editingNameValue || '').trim();
    if (!newName) {
      setNameError(lang === 'en' ? 'Name cannot be empty' : 'El nombre no puede estar vacío');
      return;
    }
    if (newName === (c.meta_campaign_name || c.title)) { cancelEditName(); return; }
    if (newName.length > 200) {
      setNameError(lang === 'en' ? 'Too long (max 200 chars)' : 'Muy largo (máx 200 caracteres)');
      return;
    }
    setSavingNameId(c.id);
    setNameError(null);
    try {
      const res = await authFetch(`/api/campaigns/${c.id}/rename`, {
        method: 'POST',
        body: JSON.stringify({ name: newName }),
      });
      const data = await res.json().catch(() => ({}));
      if (!res.ok) {
        setNameError(data.error || (lang === 'en' ? 'Could not rename on Meta' : 'No se pudo renombrar en Meta'));
        setSavingNameId(null);
        return;
      }
      if (setCampaigns) {
        setCampaigns(prev => prev.map(x => x.id === c.id ? { ...x, meta_campaign_name: newName } : x));
      }
      cancelEditName();
    } catch (e) {
      setNameError(e.message || 'Error');
    }
    setSavingNameId(null);
  };

  // SOFT-DELETE: archiva la campaña (status='archived') y la pausa en Meta. Preserva data
  // histórica (impressions, clicks, conversions, spend). El user puede verla en la pestaña
  // "Borradas" y restaurarla o borrarla permanentemente desde ahí.
  const handleDelete = async (id) => {
    if (!id) return;
    setDeletingId(id);
    try {
      const res = await authFetch(`/api/campaigns/${id}`, { method: 'DELETE' });
      if (res.ok) {
        const data = await res.json().catch(() => ({}));
        // En vez de quitarla del listado, cambiamos su status a 'archived' para que aparezca
        // en la pestaña "Borradas" automáticamente.
        if (setCampaigns) setCampaigns(prev => prev.map(c => c.id === id ? { ...c, status: 'archived' } : c));
        if (data.metaCampaignId && !data.metaPaused && data.metaPauseError) {
          alert(
            lang === 'en'
              ? `Campaign archived in NexWall, but Meta reported: "${data.metaPauseError}". Verify the campaign is paused in Ads Manager.`
              : `Campaña archivada en NexWall, pero Meta respondió: "${data.metaPauseError}". Verifica en Ads Manager que esté pausada.`
          );
        }
      }
    } catch {}
    setDeletingId(null);
    setConfirmId(null);
  };

  // RESTORE: revierte el soft-delete. Vuelve la campaña a status='paused' (no la reanuda
  // automáticamente — el user debe usar el toggle para que Meta vuelva a gastar).
  const handleRestore = async (id) => {
    if (!id) return;
    setRestoringId(id);
    try {
      const res = await authFetch(`/api/campaigns/${id}/restore`, { method: 'POST' });
      if (res.ok) {
        const data = await res.json().catch(() => ({}));
        if (setCampaigns) setCampaigns(prev => prev.map(c => c.id === id ? { ...c, status: data.status || 'paused' } : c));
      } else {
        const err = await res.json().catch(() => ({}));
        alert(err.error || (lang === 'en' ? 'Could not restore' : 'No se pudo restaurar'));
      }
    } catch {}
    setRestoringId(null);
  };

  // HARD-DELETE: borra permanentemente de NexWall y de Meta. Solo disponible desde "Borradas".
  // Esto SÍ pierde la data histórica — el user debe confirmar dos veces.
  const handleHardDelete = async (id) => {
    if (!id) return;
    setHardDeletingId(id);
    try {
      const res = await authFetch(`/api/campaigns/${id}/hard`, { method: 'DELETE' });
      if (res.ok) {
        if (setCampaigns) setCampaigns(prev => prev.filter(c => c.id !== id));
      } else {
        const err = await res.json().catch(() => ({}));
        alert(err.error || (lang === 'en' ? 'Could not delete permanently' : 'No se pudo borrar permanentemente'));
      }
    } catch {}
    setHardDeletingId(null);
    setConfirmHardId(null);
  };

  const handleToggleStatus = async (c) => {
    if (!c?.id || togglingId) return;
    const nextStatus = c.status === 'active' ? 'PAUSED' : 'ACTIVE';
    setTogglingId(c.id);
    try {
      const res = await authFetch(`/api/campaigns/${c.id}/toggle-meta-status`, {
        method: 'POST',
        body: JSON.stringify({ status: nextStatus }),
      });
      if (res.ok) {
        const data = await res.json();
        if (setCampaigns) {
          setCampaigns(prev => prev.map(x => x.id === c.id ? { ...x, status: data.status } : x));
        }
      } else {
        const err = await res.json().catch(() => ({}));
        alert(err.error || (lang === 'en' ? 'Failed to update campaign status' : 'No se pudo actualizar el estado de la campaña'));
      }
    } catch (e) {
      alert(e.message || 'Error');
    }
    setTogglingId(null);
  };

  // Country → ISO code + display name. Match is tolerant (first city/region word works too).
  // Flags render as PNG from flagcdn.com because Windows doesn't support emoji country flags
  // (it shows the regional-indicator letters instead, e.g. "AR" for 🇦🇷).
  const flagFor = (rawCountry) => {
    if (!rawCountry) return null;
    const key = String(rawCountry).toLowerCase();
    const table = [
      { match: /chile|santiago|viña|concepci/,                iso: 'cl', name: 'Chile' },
      { match: /argent|buenos aires|córdoba|cordoba|rosario/, iso: 'ar', name: 'Argentina' },
      { match: /m[eé]xico|mexico|cdmx|guadalajara|monterrey/, iso: 'mx', name: 'México' },
      { match: /colomb|bogot[aá]|medell[ií]n|cali/,           iso: 'co', name: 'Colombia' },
      { match: /per[uú]|lima|arequipa/,                       iso: 'pe', name: 'Perú' },
      { match: /espa[ñn]a|madrid|barcelona|valencia|sevilla/, iso: 'es', name: 'España' },
      { match: /brasil|brazil|s[aã]o paulo|rio/,              iso: 'br', name: 'Brasil' },
      { match: /uruguay|montevideo/,                          iso: 'uy', name: 'Uruguay' },
      { match: /paraguay|asunci/,                             iso: 'py', name: 'Paraguay' },
      { match: /bolivia|la paz|santa cruz/,                   iso: 'bo', name: 'Bolivia' },
      { match: /venezuela|caracas/,                           iso: 've', name: 'Venezuela' },
      { match: /ecuador|quito|guayaquil/,                     iso: 'ec', name: 'Ecuador' },
      { match: /panam[aá]|panama/,                            iso: 'pa', name: 'Panamá' },
      { match: /costa rica|san jos[eé]/,                      iso: 'cr', name: 'Costa Rica' },
      { match: /guatemala/,                                   iso: 'gt', name: 'Guatemala' },
      { match: /honduras|tegucigalpa/,                        iso: 'hn', name: 'Honduras' },
      { match: /nicaragua|managua/,                           iso: 'ni', name: 'Nicaragua' },
      { match: /el salvador|salvador/,                        iso: 'sv', name: 'El Salvador' },
      { match: /rep[uú]blica dominicana|dominican|santo domingo/, iso: 'do', name: 'Rep. Dominicana' },
      { match: /puerto rico|san juan/,                        iso: 'pr', name: 'Puerto Rico' },
      { match: /cuba|havana|habana/,                          iso: 'cu', name: 'Cuba' },
      { match: /miami|new york|los angeles|usa|united states|eeuu|estados unidos/, iso: 'us', name: 'USA' },
      { match: /canad[aá]|toronto|montreal/,                  iso: 'ca', name: 'Canadá' },
      { match: /united kingdom|london|inglaterra|reino unido/,iso: 'gb', name: 'Reino Unido' },
      { match: /france|paris|francia/,                        iso: 'fr', name: 'Francia' },
      { match: /portugal|lisboa|lisbon/,                      iso: 'pt', name: 'Portugal' },
      { match: /italia|italy|roma|milan/,                     iso: 'it', name: 'Italia' },
      { match: /alemania|germany|berlin/,                     iso: 'de', name: 'Alemania' },
    ];
    const hit = table.find(t => t.match.test(key));
    return hit
      ? { url: `https://flagcdn.com/w40/${hit.iso}.png`, name: hit.name }
      : { url: null, name: rawCountry };
  };
  // Spending dashboard — period selector + totals computed from each campaign's daily_budget.
  // We don't yet pull actuals from Meta Insights, so the totals here represent the COMMITTED
  // spend (sum of dailies × period multiplier) for currently-active campaigns. Drafts and
  // paused campaigns are excluded from "active spend" so the user sees what's currently
  // burning real money on Meta. Per-card we still show that campaign's daily/period budget
  // regardless of status so the user can plan resumes.
  const [period, setPeriod] = React.useState('day'); // 'yesterday' | 'day' | 'week' | 'this_month' | 'last_month'
  // Multiplicador para el "spend estimado" (presupuesto diario × días del período).
  // Ayer = 1 día, Día = 1, Semana = 7, Mes actual / mes anterior = 30 (aproximación).
  const periodMultiplier = period === 'week' ? 7 : (period === 'this_month' || period === 'last_month') ? 30 : 1;
  const periodLabel = lang === 'en'
    ? (period === 'yesterday' ? 'yesterday'
        : period === 'day' ? 'today'
        : period === 'week' ? 'this week'
        : period === 'this_month' ? 'this month'
        : 'last month')
    : (period === 'yesterday' ? 'ayer'
        : period === 'day' ? 'hoy'
        : period === 'week' ? 'esta semana'
        : period === 'this_month' ? 'este mes'
        : 'mes anterior');

  // Insights reales desde Meta — gasto efectivo + conversiones por campaña en el período.
  // Se pide en cada cambio de period y al montar. Si Meta tarda, mostramos los placeholders
  // de "—" hasta que llegue. Cache local en memoria por (biz_id, period) para evitar refetch
  // al cambiar entre tabs Activas/Pausadas/Borradas dentro de la misma sesión.
  const [insightsSummary, setInsightsSummary] = React.useState(null);
  const [insightsLoading, setInsightsLoading] = React.useState(false);
  React.useEffect(() => {
    let cancelled = false;
    setInsightsLoading(true);
    (async () => {
      try {
        const bizId = localStorage.getItem('nw_active_biz_id') || '1';
        const res = await authFetch(`/api/campaigns/insights-summary?period=${period}&biz_id=${encodeURIComponent(bizId)}`);
        if (!res.ok) { if (!cancelled) { setInsightsSummary(null); setInsightsLoading(false); } return; }
        const data = await res.json();
        if (!cancelled) { setInsightsSummary(data); setInsightsLoading(false); }
      } catch { if (!cancelled) { setInsightsSummary(null); setInsightsLoading(false); } }
    })();
    return () => { cancelled = true; };
  }, [period, campaigns.length]); // refetch al sumarse o quitarse campañas también

  // Group dailies by currency — ad accounts are locked to a single currency on Meta, but
  // a user might have campaigns across multiple businesses with different currencies.
  // We render each currency total separately so we never silently sum CLP + MXN.
  const totalsByCurrency = {};
  const activeTotalsByCurrency = {};
  campaigns.forEach(c => {
    if (!c) return;
    const cur = c.currency || 'USD';
    const daily = Number(c.daily_budget) || 0;
    totalsByCurrency[cur] = (totalsByCurrency[cur] || 0) + daily;
    const isActive = c.status === 'active' || c.status === undefined || c.status === null;
    if (isActive && c.status !== 'draft') {
      activeTotalsByCurrency[cur] = (activeTotalsByCurrency[cur] || 0) + daily;
    }
  });
  const fmtMoney = (amount, cur) => {
    try { return new Intl.NumberFormat(undefined, { style: 'currency', currency: cur, maximumFractionDigits: 0 }).format(amount); }
    catch { return `${cur} ${amount.toLocaleString()}`; }
  };

  const activeCount = campaigns.filter(c => c && c.status !== 'draft' && (c.status === 'active' || c.status === undefined || c.status === null)).length;
  const pausedCount = campaigns.filter(c => c && c.status === 'paused').length;
  const draftCount  = campaigns.filter(c => c && c.status === 'draft').length;

  return (
  <div style={{ height: '100%', overflowY: 'auto', overflowX: 'hidden', padding: isMobile ? '20px 14px' : '32px', maxWidth: '100%' }}>
    <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '20px', flexWrap: 'wrap', gap: '12px' }}>
      <div>
        <h1 style={{ fontSize: '24px', fontWeight: 400, letterSpacing: '-0.02em', color: NX.text, margin: '0 0 4px' }}>{te.title}</h1>
        <p style={{ color: NX.muted, fontSize: '14px', margin: 0 }}>{countLabel}</p>
      </div>
      <NxButton variant="accent" onClick={() => setView('crear-estrategia')}
        icon={<svg width="14" height="14" viewBox="0 0 14 14" fill="none"><path d="M7 1v12M1 7h12" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round"/></svg>}>
        {te.newBtn}
      </NxButton>
    </div>

    {/* Spending dashboard */}
    {campaigns.length > 0 && (
      <div style={{ marginBottom: '14px', padding: '14px 16px', background: 'rgba(26,17,40,0.62)', border: `1px solid ${NX.border2}`, borderRadius: '12px', backdropFilter: 'blur(16px)', WebkitBackdropFilter: 'blur(16px)', boxShadow: 'inset 0 1px 0 rgba(255,255,255,0.04)' }}>
        <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '12px', flexWrap: 'wrap', gap: '10px' }}>
          <div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
            <svg width="16" height="16" viewBox="0 0 24 24" fill="none"><path d="M12 2v4M12 18v4M4.93 4.93l2.83 2.83M16.24 16.24l2.83 2.83M2 12h4M18 12h4M4.93 19.07l2.83-2.83M16.24 7.76l2.83-2.83" stroke={NX.accent} strokeWidth="1.5" strokeLinecap="round"/></svg>
            <span style={{ fontSize: '14px', fontWeight: 600, color: NX.text }}>
              {lang === 'en' ? 'Spend overview' : 'Resumen de inversión'}
            </span>
            <span style={{ fontSize: '11px', color: NX.muted, fontFamily: "'DM Mono',monospace", padding: '2px 8px', borderRadius: '100px', background: 'rgba(139,92,246,0.10)' }}>{periodLabel}</span>
          </div>
          {/* Period selector — orden: Ayer | Día | Semana | Mes actual | Mes anterior */}
          <div style={{ display: 'flex', gap: '3px', padding: '3px', background: 'rgba(18,10,30,0.5)', border: `1px solid ${NX.border}`, borderRadius: '100px', flexWrap: 'wrap' }}>
            {[
              { id: 'yesterday',  label: lang === 'en' ? 'Yesterday'  : 'Ayer' },
              { id: 'day',        label: lang === 'en' ? 'Today'      : 'Hoy' },
              { id: 'week',       label: lang === 'en' ? 'Week'       : 'Semana' },
              { id: 'this_month', label: lang === 'en' ? 'This month' : 'Mes actual' },
              { id: 'last_month', label: lang === 'en' ? 'Last month' : 'Mes anterior' },
            ].map(p => (
              <button key={p.id} type="button" onClick={() => setPeriod(p.id)}
                style={{
                  padding: '5px 12px', borderRadius: '100px', border: 'none', cursor: 'pointer',
                  background: period === p.id ? NX.accent : 'transparent',
                  color: period === p.id ? '#fff' : NX.muted,
                  fontSize: '12px', fontWeight: 600, fontFamily: "'DM Sans',sans-serif",
                  transition: 'all 0.15s',
                }}>
                {p.label}
              </button>
            ))}
          </div>
        </div>
        {/* Stats grid — tiles con ícono gradiente a la izquierda + content a la derecha (rediseño 2026-04-30) */}
        {(() => {
          // Helpers locales para mantener todos los tiles con el mismo lenguaje visual.
          const Tile = ({ accent, label, value, subtitle, icon, highlight }) => (
            <div style={{
              padding: '14px',
              background: highlight ? `rgba(${accent.rgb},0.05)` : 'rgba(18,10,30,0.5)',
              border: `1px solid ${highlight ? `rgba(${accent.rgb},0.30)` : NX.border}`,
              borderRadius: '12px',
              display: 'flex',
              alignItems: 'flex-start',
              gap: '12px',
              minWidth: 0,
            }}>
              <div style={{
                flexShrink: 0,
                width: '63px',
                height: '63px',
                borderRadius: '14px',
                background: `linear-gradient(135deg, rgba(${accent.rgb},0.22), rgba(${accent.rgb},0.06))`,
                border: `1px solid rgba(${accent.rgb},0.30)`,
                display: 'flex',
                alignItems: 'center',
                justifyContent: 'center',
                color: accent.solid,
              }}>
                {icon}
              </div>
              <div style={{ flex: 1, minWidth: 0 }}>
                <div style={{ fontSize: '11px', color: accent.solid, fontFamily: "'DM Mono',monospace", letterSpacing: '0.1em', marginBottom: '4px', textTransform: 'uppercase', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
                  {label}
                </div>
                <div style={{ fontSize: '20px', fontWeight: 700, color: NX.text, letterSpacing: '-0.02em', fontFeatureSettings: '"tnum"', lineHeight: 1.15 }}>
                  {value}
                </div>
                {subtitle && (
                  <div style={{ fontSize: '12px', color: NX.muted, marginTop: '4px', lineHeight: 1.35 }}>
                    {subtitle}
                  </div>
                )}
              </div>
            </div>
          );
          const palette = {
            green:  { rgb: '52,211,153',  solid: NX.success },
            purple: { rgb: '139,92,246',  solid: NX.accent2 },
            blue:   { rgb: '91,142,240',  solid: NX.accent },
            yellow: { rgb: '251,191,36',  solid: NX.warning },
            violet: { rgb: '167,139,250', solid: NX.accent2 },
            muted:  { rgb: '107,107,107', solid: NX.muted   },
          };
          const Icons = {
            coins: (
              <svg width="33" height="33" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.7" strokeLinecap="round" strokeLinejoin="round">
                <ellipse cx="12" cy="6" rx="7" ry="2.4"/>
                <path d="M5 6v4c0 1.3 3.1 2.4 7 2.4s7-1.1 7-2.4V6"/>
                <path d="M5 12v4c0 1.3 3.1 2.4 7 2.4s7-1.1 7-2.4v-4"/>
                <path d="M16 4l2-2m0 0h-2m2 0v2"/>
              </svg>
            ),
            shield: (
              <svg width="33" height="33" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.7" strokeLinecap="round" strokeLinejoin="round">
                <path d="M12 3l8 3v6c0 4.5-3.4 8.4-8 9.5C7.4 20.4 4 16.5 4 12V6l8-3z"/>
                <path d="M12 8.5v7M9.5 10c0-1 1.1-1.5 2.5-1.5s2.5.5 2.5 1.5-1.2 1.5-2.5 1.5-2.5.5-2.5 1.5 1.1 1.5 2.5 1.5 2.5-.5 2.5-1.5"/>
              </svg>
            ),
            wallet: (
              <svg width="33" height="33" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.7" strokeLinecap="round" strokeLinejoin="round">
                <rect x="3" y="6" width="18" height="13" rx="2.2"/>
                <path d="M3 9h14a2 2 0 012 2v3a2 2 0 01-2 2H3"/>
                <circle cx="16.5" cy="12.5" r="1.1" fill="currentColor" stroke="none"/>
                <path d="M5.5 6V4.5a1.5 1.5 0 011.5-1.5h9.5"/>
              </svg>
            ),
            chartUp: (
              <svg width="33" height="33" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.7" strokeLinecap="round" strokeLinejoin="round">
                <path d="M3 20h18"/>
                <rect x="5"  y="13" width="3" height="6"/>
                <rect x="10.5" y="9"  width="3" height="10"/>
                <rect x="16" y="5"  width="3" height="14"/>
                <path d="M14 4l3-1 1 3"/>
              </svg>
            ),
            target: (
              <svg width="33" height="33" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.7" strokeLinecap="round" strokeLinejoin="round">
                <circle cx="12" cy="12" r="9"/>
                <circle cx="12" cy="12" r="5"/>
                <circle cx="12" cy="12" r="1.5" fill="currentColor" stroke="none"/>
              </svg>
            ),
          };
          return (
            <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))', gap: '10px' }}>
              {/* Active spend per currency — main number */}
              {Object.keys(activeTotalsByCurrency).length === 0 ? (
                <Tile
                  accent={palette.muted}
                  label={lang === 'en' ? 'Active spend' : 'Inversión activa'}
                  value="—"
                  subtitle={lang === 'en' ? 'No active campaigns' : 'Sin campañas activas'}
                  icon={Icons.coins}
                />
              ) : (
                Object.entries(activeTotalsByCurrency).map(([cur, daily]) => (
                  <Tile
                    key={`active-${cur}`}
                    accent={palette.green}
                    highlight
                    label={`${lang === 'en' ? 'Active spend' : 'Inversión activa'} · ${cur}`}
                    value={fmtMoney(daily * periodMultiplier, cur)}
                    subtitle={`${fmtMoney(daily, cur)}/${lang === 'en' ? 'day' : 'día'}${periodMultiplier === 1 ? '' : ` × ${periodMultiplier} ${lang === 'en' ? 'days' : 'días'}`}`}
                    icon={Icons.coins}
                  />
                ))
              )}
              {/* Total committed spend (active + paused) per currency */}
              {Object.entries(totalsByCurrency).map(([cur, daily]) => (
                <Tile
                  key={`total-${cur}`}
                  accent={palette.purple}
                  label={`${lang === 'en' ? 'Total committed' : 'Total comprometido'} · ${cur}`}
                  value={fmtMoney(daily * periodMultiplier, cur)}
                  subtitle={lang === 'en' ? 'Active + paused (resume spend)' : 'Activas + Pausadas (gasto por reanudar)'}
                  icon={Icons.shield}
                />
              ))}
              {/* Real spend per currency — viene de Meta Insights */}
              {insightsSummary && Object.entries(insightsSummary.totals_by_currency || {}).map(([cur, t]) => (
                <Tile
                  key={`real-${cur}`}
                  accent={palette.blue}
                  label={`${lang === 'en' ? 'Real spend' : 'Invertido real'} · ${cur}`}
                  value={fmtMoney(Number(t.spend) || 0, cur)}
                  subtitle={lang === 'en' ? `From Meta · ${periodLabel}` : `Desde Meta · ${periodLabel}`}
                  icon={Icons.wallet}
                />
              ))}
              {/* Total ventas/leads — SIEMPRE visible. Si Meta aún no respondió muestra "—".
                  Label primario por destination_mode dominante (web→Ventas, messaging→Leads),
                  fallback al tipo de conversión más numeroso de los insights. */}
              {(() => {
                const allTypes = {};
                let total = 0;
                const totalsByCur = insightsSummary?.totals_by_currency || {};
                for (const cur of Object.values(totalsByCur)) {
                  total += Number(cur.conversions || 0);
                  for (const [t, v] of Object.entries(cur.by_type || {})) {
                    allTypes[t] = (allTypes[t] || 0) + Number(v || 0);
                  }
                }
                const activeCampaignsForLabel = campaigns.filter(c => c && c.status !== 'draft' && c.status !== 'archived');
                const messagingCount = activeCampaignsForLabel.filter(c => c.destination_mode === 'messaging').length;
                const webCount = activeCampaignsForLabel.length - messagingCount;
                let dominantType;
                if (messagingCount > webCount) dominantType = 'leads';
                else if (webCount > 0) dominantType = 'sales';
                else dominantType = Object.entries(allTypes).sort((a,b) => b[1]-a[1])[0]?.[0] || 'sales';
                const labelMap = lang === 'en'
                  ? { sales: 'Sales', leads: 'Leads', messages: 'Messages', clicks: 'Clicks', conversions: 'Conversions' }
                  : { sales: 'Ventas', leads: 'Leads', messages: 'Mensajes', clicks: 'Clics', conversions: 'Conversiones' };
                const breakdown = Object.entries(allTypes).filter(([,v]) => v > 0).map(([t, v]) => `${Math.round(v)} ${labelMap[t] || t}`).join(' · ');
                const value = insightsLoading ? '…' : insightsSummary ? Math.round(total).toLocaleString() : '—';
                const subtitle = insightsLoading
                  ? (lang === 'en' ? 'Reading from Meta…' : 'Leyendo desde Meta…')
                  : !insightsSummary
                    ? (lang === 'en' ? 'Connect Meta to see results' : 'Conecta Meta para ver resultados')
                    : breakdown
                      ? breakdown
                      : (lang === 'en' ? 'No conversions yet in this period' : 'Aún sin conversiones en este período');
                return (
                  <Tile
                    accent={palette.yellow}
                    label={`${labelMap[dominantType] || labelMap.sales} · ${periodLabel}`}
                    value={value}
                    subtitle={subtitle}
                    icon={Icons.chartUp}
                  />
                );
              })()}
              {/* Campaign counts — total + breakdown vertical (matching mockup) */}
              <div style={{
                padding: '14px',
                background: 'rgba(18,10,30,0.5)',
                border: `1px solid ${NX.border}`,
                borderRadius: '12px',
                display: 'flex',
                alignItems: 'flex-start',
                gap: '12px',
                minWidth: 0,
              }}>
                <div style={{
                  flexShrink: 0,
                  width: '63px',
                  height: '63px',
                  borderRadius: '14px',
                  background: `linear-gradient(135deg, rgba(${palette.violet.rgb},0.22), rgba(${palette.violet.rgb},0.06))`,
                  border: `1px solid rgba(${palette.violet.rgb},0.30)`,
                  display: 'flex',
                  alignItems: 'center',
                  justifyContent: 'center',
                  color: palette.violet.solid,
                }}>
                  {Icons.target}
                </div>
                <div style={{ flex: 1, minWidth: 0 }}>
                  <div style={{ fontSize: '11px', color: palette.violet.solid, fontFamily: "'DM Mono',monospace", letterSpacing: '0.1em', marginBottom: '4px', textTransform: 'uppercase' }}>
                    {lang === 'en' ? 'Campaigns' : 'Campañas'}
                  </div>
                  <div style={{ fontSize: '20px', fontWeight: 700, color: NX.text, letterSpacing: '-0.02em', fontFeatureSettings: '"tnum"', lineHeight: 1.15, marginBottom: '6px' }}>
                    {activeCount + pausedCount + draftCount}
                  </div>
                  <div style={{ display: 'flex', flexDirection: 'column', gap: '3px' }}>
                    <div style={{ display: 'flex', alignItems: 'center', gap: '6px', fontSize: '12px', color: NX.muted }}>
                      <span style={{ width: '6px', height: '6px', borderRadius: '50%', background: NX.success }}/>
                      <span style={{ color: NX.text, fontWeight: 600, fontFeatureSettings: '"tnum"' }}>{activeCount}</span>
                      <span>{lang === 'en' ? 'Active' : 'Activas'}</span>
                    </div>
                    <div style={{ display: 'flex', alignItems: 'center', gap: '6px', fontSize: '12px', color: NX.muted }}>
                      <span style={{ width: '6px', height: '6px', borderRadius: '50%', background: NX.warning }}/>
                      <span style={{ color: NX.text, fontWeight: 600, fontFeatureSettings: '"tnum"' }}>{pausedCount}</span>
                      <span>{lang === 'en' ? 'Paused' : 'Pausadas'}</span>
                    </div>
                    {draftCount > 0 && (
                      <div style={{ display: 'flex', alignItems: 'center', gap: '6px', fontSize: '12px', color: NX.muted }}>
                        <span style={{ width: '6px', height: '6px', borderRadius: '50%', background: NX.muted }}/>
                        <span style={{ color: NX.text, fontWeight: 600, fontFeatureSettings: '"tnum"' }}>{draftCount}</span>
                        <span>{lang === 'en' ? 'Drafts' : 'Borradores'}</span>
                      </div>
                    )}
                  </div>
                </div>
              </div>
            </div>
          );
        })()}
        <div style={{ marginTop: '10px', fontSize: '11px', color: NX.muted, fontFamily: "'DM Sans',sans-serif", lineHeight: 1.5 }}>
          {lang === 'en'
            ? 'Estimates based on daily budgets configured in NexWall. Actual Meta-reported spend may differ slightly (Meta optimizes within ±25% per day).'
            : 'Estimaciones basadas en los presupuestos diarios configurados en NexWall. El gasto real reportado por Meta puede variar ligeramente (Meta optimiza dentro de ±25% diario).'}
        </div>
      </div>
    )}
    {/* TABS: Activas / Pausadas / Borradas. Permite filtrar campañas según su estado en
        Meta + NexWall. "Borradas" muestra las soft-deleted (status='archived'). */}
    {campaigns.length > 0 && (
      <div style={{ display: 'flex', gap: '4px', padding: '4px', background: 'rgba(18,10,30,0.5)', border: `1px solid ${NX.border}`, borderRadius: '100px', marginBottom: '20px', width: 'fit-content', maxWidth: '100%', overflowX: 'auto' }}>
        {[
          { id: 'active',   label: lang === 'en' ? 'Active'   : 'Activas',  count: tabCounts.active,   color: NX.success },
          { id: 'paused',   label: lang === 'en' ? 'Paused'   : 'Pausadas', count: tabCounts.paused,   color: NX.warning },
          { id: 'archived', label: lang === 'en' ? 'Archived' : 'Borradas', count: tabCounts.archived, color: NX.muted },
        ].map(t => {
          const isActive = activeTab === t.id;
          return (
            <button key={t.id} type="button" onClick={() => setActiveTab(t.id)}
              style={{ padding: '7px 16px', borderRadius: '100px', background: isActive ? NX.bg4 : 'transparent', color: isActive ? NX.text : NX.muted, border: 'none', cursor: 'pointer', fontSize: '13px', fontWeight: isActive ? 600 : 500, fontFamily: "'DM Sans',sans-serif", display: 'flex', alignItems: 'center', gap: '8px', transition: 'all 0.15s', whiteSpace: 'nowrap' }}>
              <span style={{ width: '8px', height: '8px', borderRadius: '50%', background: t.color, opacity: isActive ? 1 : 0.45 }}/>
              {t.label}
              <span style={{ fontSize: '11px', color: isActive ? t.color : NX.muted, fontFamily: "'DM Mono',monospace", padding: '1px 6px', borderRadius: '100px', background: isActive ? 'rgba(255,255,255,0.06)' : 'transparent' }}>{t.count}</span>
            </button>
          );
        })}
      </div>
    )}
    {filteredCampaigns.length === 0 ? (
      <div style={{ textAlign: 'center', padding: '80px 40px' }}>
        <div style={{ width: '64px', height: '64px', borderRadius: '50%', background: NX.bg3, border: `1px solid ${NX.border}`, display: 'flex', alignItems: 'center', justifyContent: 'center', margin: '0 auto 20px' }}>
          <svg width="28" height="28" viewBox="0 0 28 28" fill="none"><path d="M3 24l5-12 5 8 4-6 6 10" stroke={NX.muted} strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/></svg>
        </div>
        <h3 style={{ fontSize: '16px', fontWeight: 400, color: NX.text, margin: '0 0 8px' }}>
          {campaigns.length === 0
            ? te.empty.title
            : (activeTab === 'archived'
                ? (lang === 'en' ? 'No archived campaigns' : 'Sin campañas archivadas')
                : activeTab === 'paused'
                ? (lang === 'en' ? 'No paused campaigns' : 'Sin campañas pausadas')
                : (lang === 'en' ? 'No active campaigns' : 'Sin campañas activas'))}
        </h3>
        {campaigns.length === 0 && <p style={{ color: NX.muted, fontSize: '14px', margin: '0 0 24px' }}>{te.empty.subtitle}</p>}
        {campaigns.length === 0 && <NxButton variant="accent" onClick={() => setView('onboarding')}>{te.empty.btn}</NxButton>}
      </div>
    ) : (
      <div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
        {filteredCampaigns.map((c, i) => (
          <NxCard key={c.id || i} padding={isMobile ? '14px' : '24px'} style={Object.assign(
            { position: 'relative', maxWidth: '100%', overflow: 'hidden' },
            isMobile
              ? { display: 'flex', flexWrap: 'wrap', alignItems: 'center', gap: '10px', rowGap: '10px' }
              // Desktop: ahora también con flexWrap. Sin esto, el banner de resultado de
              // "Aplicar estrategia" (flexBasis:100%) se metía a la fuerza en la misma
              // fila que la status pill + budget + botones, comprimiéndolos y haciendo
              // que la pill "Activa" se solapara con la pill del presupuesto.
              : { display: 'flex', flexWrap: 'wrap', alignItems: 'center', gap: '18px', rowGap: '12px' }
          )}>
            {/* Quality badge — DESKTOP: esquina top-right DENTRO del frame del card.
                MOBILE: se renderiza inline debajo del título (ver más abajo) para que no
                tape el nombre de la campaña. Premium = azul; Ultra MAX = violeta. */}
            {!isMobile && c.image_model && (() => {
              const isUltra = /^gpt-image/i.test(c.image_model);
              return (
                <span title={isUltra ? 'Premium Ultra MAX' : 'Premium'}
                  style={{
                    position: 'absolute', top: '8px', right: '10px', zIndex: 2,
                    fontFamily: "'DM Mono',monospace", fontSize: '8px', fontWeight: 700, letterSpacing: '0.10em',
                    padding: '2px 7px', borderRadius: '999px',
                    background: isUltra
                      ? 'linear-gradient(135deg, #7c3aed 0%, #a78bfa 100%)'
                      : 'linear-gradient(135deg, #1e3a8a 0%, #5b8ef0 100%)',
                    border: `1px solid ${isUltra ? '#c4b5fd' : '#93c5fd'}`,
                    color: '#fff',
                    textTransform: 'uppercase',
                    boxShadow: isUltra
                      ? '0 0 10px rgba(139,92,246,0.55)'
                      : '0 0 6px rgba(91,142,240,0.40)',
                    whiteSpace: 'nowrap',
                    display: 'inline-flex', alignItems: 'center', gap: '3px',
                  }}>
                  {isUltra ? '✨ ULTRA MAX' : '⚡ PREMIUM'}
                </span>
              );
            })()}
            <div
              onClick={() => c.selected_image_url && setDetailCampaign(c)}
              title={c.selected_image_url ? (lang === 'en' ? 'Click to view full creative' : 'Click para ver el creativo completo') : ''}
              style={{ width: isMobile ? '64px' : '88px', height: isMobile ? '64px' : '88px', borderRadius: '12px', flexShrink: 0, overflow: 'hidden', background: 'linear-gradient(135deg,#1e1e2e,#2d1b69)', cursor: c.selected_image_url ? 'zoom-in' : 'default', border: `1px solid ${NX.border}`, boxShadow: '0 4px 14px rgba(10,6,16,0.45)' }}>
              {c.selected_image_url && <img src={c.selected_image_url} alt="" style={{ width: '100%', height: '100%', objectFit: 'cover' }}/>}
            </div>
            <div style={{ flex: 1, minWidth: 0, maxWidth: '100%' }}>
              {/* Título principal. En MOBILE el título queda solo en su fila (ellipsis),
                  y debajo va una mini-fila con [bandera país] + [badge calidad] para que
                  el badge nunca pise el nombre. En DESKTOP la bandera va inline al lado
                  del título y el badge sigue en su esquina absoluta. */}
              <div style={{ fontSize: '13px', fontWeight: 500, color: NX.text, fontFamily: "'DM Sans', sans-serif", letterSpacing: '-0.01em', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis', display: 'flex', alignItems: 'center', gap: '6px', maxWidth: '100%' }}>
                {/* Antes: el span del título tenía `flex: 1`, lo que estiraba la cajita
                    del título hasta llenar todo el ancho disponible y empujaba la pill
                    de bandera al borde derecho de la fila. Ahora `flex: 0 1 auto`
                    (default) hace que el título tome solo el ancho de su contenido y
                    la bandera quede pegada al final del nombre. Con overflow+ellipsis
                    sigue cortando bien cuando el nombre es muy largo. */}
                {editingNameId === c.id ? (
                  // Editor inline: input del mismo tamaño/tipografía que el nombre + botones ✓ ✕.
                  // Enter guarda, Esc cancela. Sincroniza a Meta y a la DB local.
                  <span style={{ display: 'inline-flex', alignItems: 'center', gap: '6px', flex: '0 1 auto', minWidth: 0, maxWidth: '100%' }}>
                    <input
                      autoFocus
                      type="text"
                      value={editingNameValue}
                      maxLength={200}
                      // El atributo HTML `size` ajusta el ancho del input al número
                      // de caracteres del texto. Le sumamos 1 para que no quede
                      // pegado al borde derecho mientras se escribe. Min 10 evita
                      // que se vea minúsculo al borrar todo. Max sigue limitado por
                      // maxWidth del padre + maxLength=200.
                      size={Math.max(10, (editingNameValue || '').length + 1)}
                      onChange={(e) => setEditingNameValue(e.target.value)}
                      onKeyDown={(e) => {
                        if (e.key === 'Enter') { e.preventDefault(); saveName(c); }
                        else if (e.key === 'Escape') { e.preventDefault(); cancelEditName(); }
                      }}
                      disabled={savingNameId === c.id}
                      style={{
                        // width: 'auto' deja que el atributo `size` controle el ancho.
                        width: 'auto', maxWidth: '100%', minWidth: 0,
                        fontFamily: "'DM Sans',sans-serif", fontSize: '13px',
                        fontWeight: 500, color: NX.text, padding: '4px 8px',
                        background: NX.bg2, border: `1px solid ${NX.accent}`, borderRadius: '6px',
                        outline: 'none', letterSpacing: '-0.01em',
                      }}
                    />
                    <button type="button" onClick={(e) => { e.stopPropagation(); saveName(c); }} disabled={savingNameId === c.id}
                      title={lang === 'en' ? 'Save (also renames in Meta)' : 'Guardar (también renombra en Meta)'}
                      style={{
                        width: '26px', height: '26px', padding: 0, borderRadius: '8px',
                        display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
                        cursor: 'pointer', flexShrink: 0, transition: 'all 0.15s',
                        background: 'rgba(52,211,153,0.15)', border: `1px solid ${NX.success}`, color: NX.success,
                      }}>
                      {savingNameId === c.id
                        ? <svg width="13" height="13" viewBox="0 0 24 24" fill="none" style={{ animation: 'spin 1s linear infinite' }}><circle cx="12" cy="12" r="9" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeDasharray="14 40"/></svg>
                        : <svg width="13" height="13" viewBox="0 0 16 16" fill="none"><path d="M3 8.5l3 3 7-7" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"/></svg>}
                    </button>
                    <button type="button" onClick={(e) => { e.stopPropagation(); cancelEditName(); }} disabled={savingNameId === c.id}
                      title={lang === 'en' ? 'Cancel' : 'Cancelar'}
                      style={{
                        width: '26px', height: '26px', padding: 0, borderRadius: '8px',
                        display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
                        cursor: 'pointer', flexShrink: 0, transition: 'all 0.15s',
                        background: 'transparent', border: `1px solid ${NX.border}`, color: NX.muted,
                      }}>
                      <svg width="11" height="11" viewBox="0 0 16 16" fill="none"><path d="M3 3l10 10M13 3L3 13" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round"/></svg>
                    </button>
                    {nameError && (
                      <span style={{ fontSize: '11px', color: NX.danger, whiteSpace: 'nowrap', flexShrink: 0 }}>{nameError}</span>
                    )}
                  </span>
                ) : (
                  <>
                    <span style={{ whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis', minWidth: 0, flex: '0 1 auto' }}>{c.meta_campaign_name || c.title}</span>
                    {/* Lápiz para editar el nombre — solo visible si la campaña no está archivada/draft. */}
                    {c.status !== 'archived' && c.status !== 'draft' && (
                      <button type="button" onClick={(e) => { e.stopPropagation(); startEditName(c); }}
                        title={lang === 'en' ? 'Rename (also updates in Meta)' : 'Renombrar (también actualiza en Meta)'}
                        aria-label={lang === 'en' ? 'Rename campaign' : 'Renombrar campaña'}
                        style={{
                          flexShrink: 0, width: '20px', height: '20px', display: 'inline-flex',
                          alignItems: 'center', justifyContent: 'center', padding: 0, cursor: 'pointer',
                          background: 'transparent', border: 'none', color: NX.muted, opacity: 0.6,
                          borderRadius: '4px', transition: 'opacity 0.15s, color 0.15s',
                        }}
                        onMouseEnter={(e) => { e.currentTarget.style.opacity = '1'; e.currentTarget.style.color = NX.accent; }}
                        onMouseLeave={(e) => { e.currentTarget.style.opacity = '0.6'; e.currentTarget.style.color = NX.muted; }}>
                        <svg width="12" height="12" viewBox="0 0 16 16" fill="none"><path d="M11.5 2.5a1.5 1.5 0 012.121 2.121l-8.5 8.5-2.829.707.707-2.828 8.5-8.5z" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round" strokeLinejoin="round"/></svg>
                      </button>
                    )}
                  </>
                )}
                {!isMobile && c.country && editingNameId !== c.id && (() => {
                  const f = flagFor(c.country);
                  if (!f) return null;
                  return (
                    <span title={c.country} style={{
                      flexShrink: 0,
                      fontFamily: "'DM Sans',sans-serif",
                      fontSize: '8px',
                      fontWeight: 500,
                      padding: '1px 5px 1px 2px',
                      borderRadius: '4px',
                      background: 'rgba(255,255,255,0.04)',
                      border: `1px solid ${NX.border}`,
                      color: NX.text,
                      display: 'inline-flex', alignItems: 'center', gap: '4px',
                      letterSpacing: '0.02em',
                    }}>
                      {f.url
                        ? <img src={f.url} alt="" width="18" height="13" style={{ display: 'block', borderRadius: '2px', objectFit: 'cover', boxShadow: '0 0 0 1px rgba(255,255,255,0.08)' }}/>
                        : <span style={{ fontSize: '11px' }}>🌎</span>}
                      <span>{f.name}</span>
                    </span>
                  );
                })()}
              </div>
              {/* Mobile: meta-row con bandera + badge calidad (Premium / Ultra MAX) debajo
                  del nombre. Antes el badge iba absolute top-right y se solapaba con el
                  texto del título cuando este era largo. */}
              {isMobile && (c.country || c.image_model) && (
                <div style={{ marginTop: '5px', display: 'flex', alignItems: 'center', gap: '6px', flexWrap: 'wrap' }}>
                  {c.country && (() => {
                    const f = flagFor(c.country);
                    if (!f) return null;
                    return (
                      <span title={c.country} style={{
                        flexShrink: 0,
                        fontFamily: "'DM Sans',sans-serif",
                        fontSize: '9px',
                        fontWeight: 500,
                        padding: '2px 6px 2px 3px',
                        borderRadius: '5px',
                        background: 'rgba(255,255,255,0.04)',
                        border: `1px solid ${NX.border}`,
                        color: NX.text,
                        display: 'inline-flex', alignItems: 'center', gap: '5px',
                        letterSpacing: '0.02em',
                      }}>
                        {f.url
                          ? <img src={f.url} alt="" width="16" height="12" style={{ display: 'block', borderRadius: '2px', objectFit: 'cover' }}/>
                          : <span style={{ fontSize: '11px' }}>🌎</span>}
                        <span>{f.name}</span>
                      </span>
                    );
                  })()}
                  {c.image_model && (() => {
                    const isUltra = /^gpt-image/i.test(c.image_model);
                    return (
                      <span title={isUltra ? 'Premium Ultra MAX' : 'Premium'}
                        style={{
                          fontFamily: "'DM Mono',monospace", fontSize: '8px', fontWeight: 700, letterSpacing: '0.10em',
                          padding: '2px 7px', borderRadius: '999px',
                          background: isUltra
                            ? 'linear-gradient(135deg, #7c3aed 0%, #a78bfa 100%)'
                            : 'linear-gradient(135deg, #1e3a8a 0%, #5b8ef0 100%)',
                          border: `1px solid ${isUltra ? '#c4b5fd' : '#93c5fd'}`,
                          color: '#fff',
                          textTransform: 'uppercase',
                          boxShadow: isUltra
                            ? '0 0 8px rgba(139,92,246,0.45)'
                            : '0 0 6px rgba(91,142,240,0.35)',
                          whiteSpace: 'nowrap',
                          display: 'inline-flex', alignItems: 'center', gap: '3px',
                          flexShrink: 0,
                        }}>
                        {isUltra ? '✨ ULTRA MAX' : '⚡ PREMIUM'}
                      </span>
                    );
                  })()}
                </div>
              )}
              {/* Subtítulo "Upgrade Your MOZA Today · Meta Ads · ventas · 20 anuncios · 27 abr 2026 23:52"
                  removido por pedido del usuario — info ya redundante con el título y el badge de
                  Premium/Ultra MAX. */}
              {/* Per-campaign spend pill — daily + selected period total. Inline editable:
                  click the pencil to change the daily budget directly (calls
                  /api/campaigns/:id/update-budget which updates Meta + DB). Currency stays
                  fixed because Meta does not allow changing the ad-account currency. */}
              {(Number(c.daily_budget) > 0 || editingBudgetId === c.id) && (() => {
                const cur = c.currency || 'USD';
                const daily = Number(c.daily_budget);
                const isEditing = editingBudgetId === c.id;
                const isSaving  = savingBudgetId === c.id;
                const canEdit = !!c.meta_campaign_id && Array.isArray(c.meta_adset_ids) && c.meta_adset_ids.length > 0;
                if (isEditing) {
                  const newAmount = Number(editingBudgetValue);
                  const validNew  = isFinite(newAmount) && newAmount > 0;
                  return (
                    <div style={{ marginTop: '6px', display: 'flex', alignItems: 'center', gap: '6px', flexWrap: 'wrap' }}>
                      <span style={{ fontSize: '10px', color: NX.muted, fontFamily: "'DM Mono',monospace", fontWeight: 700, padding: '3px 8px', borderRadius: '6px', background: NX.bg3, border: `1px solid ${NX.border}` }}>{cur}</span>
                      <input
                        type="number" min={1} value={editingBudgetValue}
                        onChange={e => { setEditingBudgetValue(e.target.value); setBudgetError(null); setConfirmingBudget(false); }}
                        onKeyDown={e => { if (e.key === 'Enter') handleSavePress(c); if (e.key === 'Escape') cancelEditBudget(); }}
                        autoFocus
                        disabled={isSaving}
                        style={{ width: '110px', padding: '5px 9px', background: NX.bg3, border: `1.5px solid ${budgetError ? NX.danger : NX.accent}`, borderRadius: '8px', color: NX.text, fontSize: '12px', fontFamily: "'DM Mono',monospace", outline: 'none' }}
                      />
                      <button type="button" onClick={() => handleSavePress(c)} disabled={isSaving}
                        style={{
                          padding: '5px 12px',
                          background: confirmingBudget ? 'linear-gradient(135deg, #f59e0b, #fbbf24)' : NX.accentGrad,
                          border: 'none', borderRadius: '8px', color: '#fff',
                          fontSize: '11px', fontWeight: 700,
                          cursor: isSaving ? 'wait' : 'pointer',
                          fontFamily: "'DM Sans',sans-serif",
                          display: 'inline-flex', alignItems: 'center', gap: '5px',
                          boxShadow: confirmingBudget ? '0 0 14px rgba(245,158,11,0.45)' : '0 4px 14px rgba(139,92,246,0.30)',
                          letterSpacing: '0.02em',
                        }}>
                        {isSaving
                          ? <><span style={{ display:'inline-block', width:'8px', height:'8px', border:'1.5px solid #fff', borderTopColor:'transparent', borderRadius:'50%', animation:'spin 0.7s linear infinite' }}/> {lang === 'en' ? 'Saving…' : 'Guardando…'}</>
                          : confirmingBudget
                            ? (lang === 'en'
                                ? `Confirm — ${validNew ? fmtMoney(newAmount, cur) : ''}/day`
                                : `Confirmar — ${validNew ? fmtMoney(newAmount, cur) : ''}/día`)
                            : (lang === 'en' ? 'Save' : 'Guardar')}
                      </button>
                      <button type="button" onClick={cancelEditBudget} disabled={isSaving}
                        style={{ padding: '5px 10px', background: 'transparent', border: `1px solid ${NX.border}`, borderRadius: '8px', color: NX.muted, fontSize: '11px', cursor: 'pointer', fontFamily: "'DM Sans',sans-serif" }}>
                        {lang === 'en' ? 'Cancel' : 'Cancelar'}
                      </button>
                      {confirmingBudget && validNew && (
                        <span style={{ fontSize: '10px', color: NX.warning, fontFamily:"'DM Sans',sans-serif", flexBasis: '100%', marginTop: '2px' }}>
                          {lang === 'en'
                            ? `This will update the daily budget on Meta from ${fmtMoney(daily, cur)} to ${fmtMoney(newAmount, cur)}. Click again to confirm.`
                            : `Esto actualizará el presupuesto en Meta de ${fmtMoney(daily, cur)} a ${fmtMoney(newAmount, cur)}. Click de nuevo para confirmar.`}
                        </span>
                      )}
                      {budgetError && <span style={{ fontSize: '10px', color: NX.danger, flexBasis: '100%' }}>{budgetError}</span>}
                    </div>
                  );
                }
                return (
                  <div style={{ marginTop: '8px', display: 'flex', alignItems: 'center', gap: '8px', flexWrap: 'wrap', maxWidth: '100%', minWidth: 0 }}>
                    {/* Pill de presupuesto diario — el dato más importante de la card. Antes
                        era un pill discreto violeta con fontSize 11px; ahora va con el accent
                        success (verde) y fontSize 14px para que destaque a primera vista.
                        Click → edita el presupuesto en Meta (mismo flow). */}
                    <button type="button"
                      onClick={() => canEdit && startEditBudget(c)}
                      disabled={!canEdit}
                      title={canEdit
                        ? (lang === 'en' ? 'Click to change the daily budget on Meta' : 'Click para cambiar el presupuesto diario en Meta')
                        : (lang === 'en' ? 'Not published on Meta yet' : 'Aún no publicada en Meta')}
                      style={{
                        fontSize: isMobile ? '13px' : '14px', fontFamily: "'DM Mono',monospace", fontWeight: 700,
                        padding: isMobile ? '5px 10px' : '6px 12px', borderRadius: '10px',
                        background: 'rgba(52,211,153,0.12)',
                        border: `1px solid rgba(52,211,153,0.45)`,
                        color: NX.success,
                        display: 'inline-flex', alignItems: 'center', gap: '6px',
                        cursor: canEdit ? 'pointer' : 'not-allowed',
                        opacity: canEdit ? 1 : 0.5,
                        transition: 'all 0.15s',
                        boxShadow: '0 2px 10px rgba(52,211,153,0.15)',
                        maxWidth: '100%', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis',
                      }}
                      onMouseEnter={e => { if (canEdit) { e.currentTarget.style.background = 'rgba(52,211,153,0.22)'; e.currentTarget.style.borderColor = NX.success; e.currentTarget.style.boxShadow = '0 3px 14px rgba(52,211,153,0.30)'; } }}
                      onMouseLeave={e => { if (canEdit) { e.currentTarget.style.background = 'rgba(52,211,153,0.12)'; e.currentTarget.style.borderColor = 'rgba(52,211,153,0.45)'; e.currentTarget.style.boxShadow = '0 2px 10px rgba(52,211,153,0.15)'; } }}>
                      <svg width="13" height="13" viewBox="0 0 16 16" fill="none"><path d="M8 2v12M5 4.5h4.5a2 2 0 010 4h-3a2 2 0 000 4H11" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round"/></svg>
                      {fmtMoney(daily, cur)}<span style={{ fontSize:'10px', opacity:0.75, fontWeight:500 }}>/{lang === 'en' ? 'day' : 'día'}</span>
                      {canEdit && (
                        <svg width="11" height="11" viewBox="0 0 16 16" fill="none" style={{ marginLeft:'2px', opacity:0.65 }}>
                          <path d="M11.5 2.5a1.5 1.5 0 012.121 2.121l-8.5 8.5-2.829.707.707-2.828 8.5-8.5z" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round" strokeLinejoin="round"/>
                        </svg>
                      )}
                    </button>
                    <span title={lang === 'en' ? `Estimated spend ${periodLabel}` : `Inversión estimada ${periodLabel}`}
                      style={{ fontSize: '11px', color: NX.muted, fontFamily: "'DM Mono',monospace" }}>
                      ≈ {fmtMoney(daily * periodMultiplier, cur)} {periodMultiplier === 1 ? '' : `/ ${periodLabel}`}
                    </span>
                    {/* Gasto real + conversiones del PERÍODO seleccionado en el toggle (día/semana/mes).
                        Mismo tamaño visual que la pill verde de presupuesto diario para que sea
                        igualmente prominente — los dos números más importantes de la card.
                        Color azul accent para distinguir "lo que ya gastó" del "lo que va a gastar".
                        Anteriormente esto mostraba lifetime y por eso no variaba con el toggle. */}
                    {(() => {
                      const ins = insightsSummary?.by_campaign?.[c.id];
                      if (!ins) return null;
                      const periodSpend = Number(ins.period?.spend) || 0;
                      const conv = ins.period?.conversion || {};
                      const convVal = Math.round(Number(conv.value) || 0);
                      const linkClicks = Math.round(Number(ins.period?.link_clicks) || 0);
                      const labelMap = lang === 'en'
                        ? { sales: 'sales', leads: 'leads', messages: 'msgs', clicks: 'clicks', conversions: 'conv.' }
                        : { sales: 'ventas', leads: 'leads', messages: 'msjs', clicks: 'clics', conversions: 'conv.' };
                      const convLabel = labelMap[conv.type] || labelMap.conversions;
                      const clicksLabel = labelMap.clicks;
                      // Mostrar la métrica primaria (ventas/leads) Y los clics simultáneamente.
                      // Cuando la primaria YA es clicks (porque no hay purchase ni leads en el
                      // período) no duplicamos el bloque de clicks.
                      const showClicksToo = conv.type !== 'clicks';
                      const periodTitle = lang === 'en'
                        ? (period === 'yesterday' ? 'Spent and conversions yesterday'
                            : period === 'day' ? 'Spent and conversions today'
                            : period === 'week' ? 'Spent and conversions in the last 7 days'
                            : period === 'this_month' ? 'Spent and conversions this month'
                            : 'Spent and conversions last month')
                        : (period === 'yesterday' ? 'Gastado y conversiones de ayer'
                            : period === 'day' ? 'Gastado y conversiones de hoy'
                            : period === 'week' ? 'Gastado y conversiones de los últimos 7 días'
                            : period === 'this_month' ? 'Gastado y conversiones de este mes'
                            : 'Gastado y conversiones del mes anterior');
                      return (
                        <span title={periodTitle}
                          style={{
                            // Mismas dimensiones que la pill verde de presupuesto diario
                            // para alinear visualmente los dos números clave de la card.
                            fontSize: isMobile ? '13px' : '14px', fontFamily: "'DM Mono',monospace", fontWeight: 700,
                            padding: isMobile ? '5px 10px' : '6px 12px', borderRadius: '10px',
                            background: 'rgba(91,142,240,0.12)',
                            border: `1px solid rgba(91,142,240,0.45)`,
                            color: NX.accent,
                            display: 'inline-flex', alignItems: 'center', gap: '6px',
                            boxShadow: '0 2px 10px rgba(91,142,240,0.15)',
                            maxWidth: '100%', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis',
                            flexShrink: 1, minWidth: 0,
                          }}>
                          <svg width="13" height="13" viewBox="0 0 16 16" fill="none">
                            <path d="M8 1.5v13M3 5.5l5-5 5 5M3 11l5 5 5-5" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round" strokeLinejoin="round" opacity="0.85"/>
                          </svg>
                          <span>
                            <b style={{ color: NX.text }}>{fmtMoney(periodSpend, ins.currency || cur)}</b>
                            <span style={{ fontSize: '10px', opacity: 0.75, fontWeight: 500, marginLeft: '4px' }}>{lang === 'en' ? 'spent' : 'gastado'}</span>
                          </span>
                          <span style={{ color: NX.muted, opacity: 0.55 }}>·</span>
                          <span style={{ color: convVal > 0 ? NX.success : NX.muted }}>
                            <b style={{ color: convVal > 0 ? NX.text : NX.muted }}>{convVal}</b>
                            <span style={{ fontSize: '10px', opacity: 0.75, fontWeight: 500, marginLeft: '4px' }}>{convLabel}</span>
                          </span>
                          {showClicksToo && (
                            <>
                              <span style={{ color: NX.muted, opacity: 0.55 }}>·</span>
                              <span style={{ color: linkClicks > 0 ? NX.text : NX.muted }}>
                                <b style={{ color: linkClicks > 0 ? NX.text : NX.muted }}>{linkClicks}</b>
                                <span style={{ fontSize: '10px', opacity: 0.75, fontWeight: 500, marginLeft: '4px' }}>{clicksLabel}</span>
                              </span>
                            </>
                          )}
                        </span>
                      );
                    })()}
                  </div>
                );
              })()}
            </div>
            {/* Wrap-forcing spacer: on mobile, makes the status pill + actions cluster
                drop to row 2 of the card so they never overlap the budget pill. */}
            {isMobile && <div style={{ flexBasis: '100%', height: 0, margin: 0 }}/>}
            {c.status === 'archived' ? (
              <NxBadge color="muted">{lang === 'en' ? 'Archived' : 'Archivada'}</NxBadge>
            ) : c.status === 'draft' ? (
              <NxBadge color="warning">{lang === 'en' ? 'Draft' : 'Borrador'}</NxBadge>
            ) : (() => {
              const isActive = c.status === 'active' || c.status === undefined || c.status === null;
              const isBusy = togglingId === c.id;
              const isArmed = confirmToggleId === c.id;
              // When armed, the pill flips its visual to the OPPOSITE-color preview so the
              // user sees what the result of the second click will be (active→amber preview,
              // paused→green preview). Click anywhere else (or wait 4s) and it disarms.
              const color = isArmed
                ? (isActive ? NX.warning : NX.success)
                : (isActive ? NX.success : NX.warning);
              const bg = isArmed
                ? (isActive ? 'rgba(251,191,36,0.18)' : 'rgba(52,211,153,0.18)')
                : (isActive ? 'rgba(52,211,153,0.12)' : 'rgba(251,191,36,0.12)');
              const label = isBusy
                ? (lang === 'en' ? 'Saving…' : 'Guardando…')
                : isArmed
                  ? (isActive
                      ? (lang === 'en' ? 'Pause? Tap again' : '¿Pausar? Toca de nuevo')
                      : (lang === 'en' ? 'Resume? Tap again' : '¿Reanudar? Toca de nuevo'))
                  : (isActive ? (lang === 'en' ? 'Active' : 'Activa') : (lang === 'en' ? 'Paused' : 'Pausada'));
              const title = isBusy
                ? ''
                : isArmed
                  ? (lang === 'en' ? 'Tap again to confirm — auto-cancels in 4s' : 'Toca de nuevo para confirmar — se cancela en 4s')
                  : (isActive
                      ? (lang === 'en' ? 'Tap to pause this campaign on Meta — requires confirmation' : 'Toca para pausar en Meta — pedirá confirmación')
                      : (lang === 'en' ? 'Tap to resume this campaign on Meta — requires confirmation' : 'Toca para reanudar en Meta — pedirá confirmación'));
              const onPillClick = () => {
                if (isBusy || !c.meta_campaign_id) return;
                if (isArmed) {
                  setConfirmToggleId(null);
                  handleToggleStatus(c);
                } else {
                  armToggle(c.id);
                }
              };
              return (
                <button
                  type="button"
                  onClick={onPillClick}
                  disabled={isBusy || !c.meta_campaign_id}
                  title={!c.meta_campaign_id ? (lang === 'en' ? 'Not published on Meta yet' : 'Aún no publicada en Meta') : title}
                  style={{
                    padding: isMobile ? '4px 9px' : '5px 11px',
                    borderRadius: '100px',
                    background: bg,
                    border: `1px solid ${color}`,
                    color: color,
                    fontSize: isMobile ? '10px' : '11px',
                    fontWeight: 600,
                    cursor: (isBusy || !c.meta_campaign_id) ? 'default' : 'pointer',
                    fontFamily: "'DM Sans',sans-serif",
                    display: 'inline-flex', alignItems: 'center', gap: isMobile ? '4px' : '5px',
                    transition: 'all 0.15s',
                    opacity: !c.meta_campaign_id ? 0.5 : 1,
                    flexShrink: 0,
                    boxShadow: isArmed ? `0 0 0 3px ${color}33` : 'none',
                  }}
                >
                  {isBusy
                    ? <svg width="10" height="10" viewBox="0 0 24 24" fill="none" style={{ animation: 'spin 0.8s linear infinite' }}><circle cx="12" cy="12" r="9" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeDasharray="14 40"/></svg>
                    : isArmed
                      // While armed show the icon of the action that will happen (pause bars
                      // when going to PAUSED, play triangle when going to ACTIVE).
                      ? (isActive
                          ? <svg width="9" height="9" viewBox="0 0 12 12" fill="none"><rect x="2" y="1.5" width="2.5" height="9" rx="0.5" fill="currentColor"/><rect x="7.5" y="1.5" width="2.5" height="9" rx="0.5" fill="currentColor"/></svg>
                          : <svg width="10" height="10" viewBox="0 0 12 12" fill="none"><path d="M3 2l7 4-7 4V2z" fill="currentColor"/></svg>)
                      : (isActive
                          ? <span style={{ width: '6px', height: '6px', borderRadius: '50%', background: 'currentColor' }}/>
                          : <svg width="9" height="9" viewBox="0 0 12 12" fill="none"><rect x="2" y="1.5" width="2.5" height="9" rx="0.5" fill="currentColor"/><rect x="7.5" y="1.5" width="2.5" height="9" rx="0.5" fill="currentColor"/></svg>)}
                  {label}
                </button>
              );
            })()}
            {(() => {
              let metaAdsUrl = null;
              if (c.meta_campaign_id) {
                let acct = '';
                try {
                  const cached = JSON.parse(localStorage.getItem('nw_assets_' + (c.biz_id || '')) || 'null');
                  if (cached?.ad_account_id) acct = String(cached.ad_account_id).replace(/^act_/, '');
                } catch {}
                metaAdsUrl = acct
                  ? `https://adsmanager.facebook.com/adsmanager/manage/campaigns?act=${acct}&selected_campaign_ids=${encodeURIComponent(c.meta_campaign_id)}`
                  : `https://adsmanager.facebook.com/adsmanager/manage/campaigns?selected_campaign_ids=${encodeURIComponent(c.meta_campaign_id)}`;
              }
              // Estilo común para los botones-icono de la fila de acciones. Mobile usa
              // 36×36 (más compacto) y desktop 38×38. TODOS los botones de acción
              // (launch, opt, view, more, etc.) comparten este tamaño para que la fila
              // se vea pareja sin botones más grandes/chicos.
              const ICON_SIZE = isMobile ? 36 : 38;
              const ICON_BTN = { width: `${ICON_SIZE}px`, height: `${ICON_SIZE}px`, padding: 0, borderRadius: '9px', display: 'inline-flex', alignItems: 'center', justifyContent: 'center', cursor: 'pointer', flexShrink: 0, transition: 'all 0.15s', fontFamily: "'DM Sans',sans-serif" };
              const launchBtn = c.status === 'draft' && onLaunchDraft ? (
                <button key="launch" onClick={() => onLaunchDraft(c)}
                  title={lang === 'en' ? 'Review and launch this draft' : 'Revisar y lanzar este borrador'}
                  aria-label={lang === 'en' ? 'Launch' : 'Lanzar'}
                  style={{ ...ICON_BTN, background: 'linear-gradient(135deg, #8b5cf6, #a78bfa)', border: 'none', color: '#fff', boxShadow: '0 2px 10px rgba(139,92,246,0.35)' }}
                  onMouseEnter={e => { e.currentTarget.style.transform = 'translateY(-1px)'; e.currentTarget.style.boxShadow = '0 4px 14px rgba(139,92,246,0.45)'; }}
                  onMouseLeave={e => { e.currentTarget.style.transform = 'translateY(0)'; e.currentTarget.style.boxShadow = '0 2px 10px rgba(139,92,246,0.35)'; }}>
                  <svg width="15" height="15" viewBox="0 0 12 12" fill="none"><path d="M3 2l7 4-7 4V2z" fill="currentColor"/></svg>
                </button>
              ) : null;
              const reuseBtn = c.status !== 'draft' && onLaunchDraft ? (
                <button key="reuse" onClick={() => onLaunchDraft(c)}
                  title={lang === 'en' ? 'Start a new campaign reusing this one' : 'Crear una campaña nueva reusando esta'}
                  aria-label={lang === 'en' ? 'Reuse' : 'Reutilizar'}
                  style={{ ...ICON_BTN, background: 'transparent', border: `1px solid ${NX.accent2}`, color: NX.accent2 }}
                  onMouseEnter={e => { e.currentTarget.style.background = 'rgba(139,92,246,0.12)'; }}
                  onMouseLeave={e => { e.currentTarget.style.background = 'transparent'; }}>
                  <svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M2 8a6 6 0 1 1 1.76 4.24M2 13V9h4" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/></svg>
                </button>
              ) : null;
              // Dynamic "Aplicar plan" button — its label, color and behavior change with the
              // campaign's age + last_scaled_at. Hidden for drafts and campaigns not yet on Meta.
              const optState = optimizationStateFor(c);
              const optResult = optResultByCampaign[c.id];
              const isOptBusy = applyingOptId === c.id;
              // Helper: construye un botón icon-only para una fase específica ('mid' | 'late').
              // Tooltip nativo, color/icono según fase. Devuelve null si la fase no aplica al
              // estado actual de la campaña (p.ej. late aún no disponible, draft, etc.).
              const renderOptIcon = (iconKind) => {
                if (isOptBusy) return <svg width="16" height="16" viewBox="0 0 24 24" fill="none" style={{ animation: 'spin 1s linear infinite' }}><circle cx="12" cy="12" r="9" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeDasharray="14 40"/></svg>;
                if (iconKind === 'clock') return <svg width="16" height="16" viewBox="0 0 16 16" fill="none"><circle cx="8" cy="8" r="6.2" stroke="currentColor" strokeWidth="1.4"/><path d="M8 4.5V8l2.4 1.5" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round"/></svg>;
                if (iconKind === 'lock') return <svg width="16" height="16" viewBox="0 0 16 16" fill="none"><rect x="3" y="7.5" width="10" height="6.5" rx="1.2" stroke="currentColor" strokeWidth="1.4"/><path d="M5 7.5V5a3 3 0 016 0v2.5" stroke="currentColor" strokeWidth="1.4"/></svg>;
                if (iconKind === 'broom') return <svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M11 1l4 4-7 7-4 0 0-4 7-7zM3 13l-2 2M5 13l-2 2M7 13l-2 2" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round" strokeLinejoin="round"/></svg>;
                if (iconKind === 'lightning') return <svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor"><path d="M9 1L3 9h4l-1 6 6-8H8l1-6z"/></svg>;
                return <svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M8 1.5L9.2 6.5L14 7.8L9.2 9.1L8 14.1L6.8 9.1L2 7.8L6.8 6.5L8 1.5Z" fill="currentColor"/></svg>;
              };
              const buildOptBtn = (phase) => {
                // El estado del card es: pre-launch | wait | mid | late-locked | late | draft.
                // Reglas: el botón "mid" (limpieza) está disponible desde el día 4 — y SE
                // MANTIENE visible aunque la campaña entre en fase late, para que el usuario
                // pueda seguir limpiando ads de bajo CTR en paralelo al escalado. El botón
                // "late" (escalar +25%) aparece a partir del día 8 y respeta el lock de 24h.
                let title, color, bg, border, action, disabled = false, iconKind, badge = null, key;
                if (phase === 'mid') {
                  key = 'opt-mid';
                  if (optState.kind === 'pre-launch') {
                    title = lang === 'en' ? 'Apply cleanup · publish first' : 'Aplicar limpieza · lanza primero';
                    color = NX.muted; bg = 'transparent'; border = NX.border; disabled = true; iconKind = 'broom';
                  } else if (optState.kind === 'wait') {
                    title = lang === 'en'
                      ? `Apply cleanup · available in ${optState.daysLeft}d`
                      : `Aplicar limpieza · disponible en ${optState.daysLeft}d`;
                    color = NX.muted; bg = 'transparent'; border = NX.border; disabled = true; iconKind = 'clock';
                    badge = `${optState.daysLeft}d`;
                  } else {
                    // mid | late | late-locked → cleanup siempre disponible si la campaña pasó
                    // el día 4 y está publicada en Meta.
                    title = lang === 'en' ? '🧹 Apply cleanup · pause low-CTR ads' : '🧹 Aplicar limpieza · pausar ads de bajo CTR';
                    color = '#fff'; bg = 'linear-gradient(135deg, #7c3aed 0%, #8b5cf6 50%, #a78bfa 100%)'; border = 'transparent';
                    action = () => setApplyOptCampaign({ campaign: c, phase: 'mid' });
                    iconKind = 'broom';
                  }
                } else { // 'late'
                  key = 'opt-late';
                  // El late solo se renderiza cuando aplica (>= día 8).
                  if (optState.kind === 'late-locked') {
                    title = lang === 'en' ? `Already scaled · wait ${optState.label}` : `Ya escalado · espera ${optState.label}`;
                    color = NX.warning; bg = 'rgba(251,191,36,0.10)'; border = NX.warning; disabled = true; iconKind = 'lock';
                  } else if (optState.kind === 'late') {
                    title = lang === 'en' ? '⚡ Apply scale · +25% daily budget' : '⚡ Aplicar escalado · +25% presupuesto';
                    color = '#fff'; bg = 'linear-gradient(135deg, #f59e0b, #fbbf24)'; border = 'transparent';
                    action = () => setApplyOptCampaign({ campaign: c, phase: 'late' });
                    iconKind = 'lightning';
                  } else {
                    return null; // late aún no disponible
                  }
                }
                return (
                  <button key={key}
                    onClick={() => !disabled && !isOptBusy && action && action()}
                    disabled={disabled || isOptBusy}
                    title={title}
                    aria-label={title}
                    style={{
                      ...ICON_BTN,
                      background: isOptBusy ? 'rgba(139,92,246,0.18)' : bg,
                      border: `1px solid ${border}`,
                      color,
                      cursor: disabled ? 'not-allowed' : isOptBusy ? 'wait' : 'pointer',
                      boxShadow: !disabled && !isOptBusy
                        ? (phase === 'mid' ? '0 3px 12px rgba(139,92,246,0.35)' : '0 3px 12px rgba(251,191,36,0.35)')
                        : 'none',
                      position: 'relative',
                    }}>
                    {renderOptIcon(iconKind)}
                    {badge && (
                      <span style={{ position:'absolute', bottom:-3, right:-3, fontSize:'8px', fontFamily:"'DM Mono',monospace", fontWeight:700, padding:'1px 4px', borderRadius:'8px', background:NX.bg2, border:`1px solid ${NX.border2}`, color:NX.muted, lineHeight:1 }}>{badge}</span>
                    )}
                  </button>
                );
              };
              // Drafts no muestran ningún botón. Para todos los demás estados:
              // - mid siempre se renderiza (placeholder pre-launch / wait, o activo).
              // - late se renderiza solo cuando la campaña entró en fase late (≥día 8).
              const optButtons = optState.kind === 'draft'
                ? null
                : <>{buildOptBtn('mid')}{buildOptBtn('late')}</>;
              if (isMobile) {
                const isOpen = openMenuId === c.id;
                return (
                  <div style={{ display: 'flex', gap: '8px', alignItems: 'center', marginLeft: 'auto', flexWrap: 'nowrap', justifyContent: 'flex-end', flexShrink: 0 }}>
                    {launchBtn}
                    {optButtons}
                    <button
                      onClick={() => setOpenMenuId(isOpen ? null : c.id)}
                      title={lang === 'en' ? 'More actions' : 'Más acciones'}
                      aria-label={lang === 'en' ? 'More actions' : 'Más acciones'}
                      style={{ ...ICON_BTN, background: isOpen ? NX.bg4 : 'transparent', border: `1px solid ${isOpen ? NX.accent : NX.border}`, color: isOpen ? NX.accent : NX.text }}>
                      <svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor"><circle cx="3" cy="8" r="1.5"/><circle cx="8" cy="8" r="1.5"/><circle cx="13" cy="8" r="1.5"/></svg>
                    </button>
                  </div>
                );
              }
              return (
                <div style={{ display: 'flex', gap: '8px', alignItems: 'center', flexWrap: 'wrap' }}>
                  {launchBtn}
                  {optButtons}
                  {/* Ver detalles — colocado inmediatamente después de Aplicar estrategia
                      por pedido del usuario, así la acción primaria queda flanqueada por
                      su contexto (estrategia + detalles del creativo). */}
                  <button onClick={() => setDetailCampaign(c)} title={tx.view} aria-label={tx.view}
                    style={{ ...ICON_BTN, background: 'transparent', border: `1px solid ${NX.border}`, color: NX.text }}
                    onMouseEnter={e => { e.currentTarget.style.borderColor = NX.accent; e.currentTarget.style.color = NX.accent; }}
                    onMouseLeave={e => { e.currentTarget.style.borderColor = NX.border; e.currentTarget.style.color = NX.text; }}>
                    <svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M1 8s2.5-5 7-5 7 5 7 5-2.5 5-7 5-7-5-7-5z" stroke="currentColor" strokeWidth="1.3"/><circle cx="8" cy="8" r="2" stroke="currentColor" strokeWidth="1.3"/></svg>
                  </button>
                  {reuseBtn}
                  {metaAdsUrl && (
                    <a href={metaAdsUrl} target="_blank" rel="noopener noreferrer"
                      title={lang === 'en' ? 'Open in Meta Ads Manager (new tab)' : 'Abrir en Facebook Ads Manager (nueva pestaña)'}
                      aria-label="Meta Ads Manager"
                      style={{ ...ICON_BTN, background: 'rgba(24,119,242,0.10)', border: `1px solid #1877F2`, color: '#4f9bff', textDecoration: 'none', position:'relative' }}
                      onMouseEnter={e => { e.currentTarget.style.background = 'rgba(24,119,242,0.20)'; }}
                      onMouseLeave={e => { e.currentTarget.style.background = 'rgba(24,119,242,0.10)'; }}>
                      <svg width="16" height="16" viewBox="0 0 24 24" fill="#1877F2"><path d="M24 12.073c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.99 4.388 10.954 10.125 11.854v-8.385H7.078v-3.47h3.047V9.43c0-3.007 1.792-4.669 4.533-4.669 1.312 0 2.686.235 2.686.235v2.953H15.83c-1.491 0-1.956.925-1.956 1.874v2.25h3.328l-.532 3.47h-2.796v8.385C19.612 23.027 24 18.062 24 12.073z"/></svg>
                      <svg width="8" height="8" viewBox="0 0 16 16" fill="none" style={{ position:'absolute', top:6, right:6, opacity:0.65 }}><path d="M6 3h-3v10h10v-3M10 3h3v3M13 3l-6 6" stroke="#4f9bff" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round"/></svg>
                    </a>
                  )}
                  <button onClick={() => setAnalyzeCampaign(c)}
                    title={lang === 'en' ? '🔍 Analyze campaign — applied campaigns get the 🔍 tag' : '🔍 Analizar campaña — al aplicar queda marcada con 🔍'}
                    aria-label={tx.analyze}
                    disabled={c.status === 'draft' || !c.meta_campaign_id}
                    style={{ ...ICON_BTN, background: 'transparent', border: `1px solid ${NX.border}`, color: (c.status === 'draft' || !c.meta_campaign_id) ? NX.muted : NX.text, opacity: (c.status === 'draft' || !c.meta_campaign_id) ? 0.45 : 1, cursor: (c.status === 'draft' || !c.meta_campaign_id) ? 'not-allowed' : 'pointer', fontSize: '16px', lineHeight: 1 }}
                    onMouseEnter={e => { if (c.status === 'draft' || !c.meta_campaign_id) return; e.currentTarget.style.borderColor = NX.accent; }}
                    onMouseLeave={e => { e.currentTarget.style.borderColor = NX.border; }}>
                    <span style={{ filter: (c.status === 'draft' || !c.meta_campaign_id) ? 'grayscale(1)' : 'none' }}>🔍</span>
                  </button>
                  {/* Si la campaña está ARCHIVADA: mostramos Restaurar + Borrar para siempre.
                      Si está activa/pausada/draft: mostramos Archivar (soft-delete). */}
                  {c.status === 'archived' ? (
                    <>
                      <button onClick={() => handleRestore(c.id)} disabled={restoringId === c.id}
                        title={lang === 'en' ? 'Restore as paused — use the toggle to resume on Meta' : 'Restaurar como pausada — usá el toggle para reanudar en Meta'}
                        aria-label={tx.restore}
                        style={{ ...ICON_BTN, background: 'transparent', border: `1px solid ${NX.border2}`, color: NX.success, cursor: restoringId === c.id ? 'wait' : 'pointer' }}
                        onMouseEnter={e => { if (restoringId !== c.id) { e.currentTarget.style.borderColor = NX.success; e.currentTarget.style.background = 'rgba(52,211,153,0.10)'; } }}
                        onMouseLeave={e => { if (restoringId !== c.id) { e.currentTarget.style.borderColor = NX.border2; e.currentTarget.style.background = 'transparent'; } }}>
                        {restoringId === c.id
                          ? <svg width="16" height="16" viewBox="0 0 24 24" fill="none" style={{ animation: 'spin 1s linear infinite' }}><circle cx="12" cy="12" r="9" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeDasharray="14 40"/></svg>
                          : <svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M2 8a6 6 0 1112 0M2 8L4 6M2 8L4 10" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round" strokeLinejoin="round"/></svg>}
                      </button>
                      {confirmHardId === c.id ? (
                        <>
                          <button onClick={() => handleHardDelete(c.id)} disabled={hardDeletingId === c.id}
                            title={tx.confirmHard} aria-label={tx.confirmHard}
                            style={{ ...ICON_BTN, background: NX.danger, border: 'none', color: '#fff' }}>
                            {hardDeletingId === c.id
                              ? <svg width="16" height="16" viewBox="0 0 24 24" fill="none" style={{ animation: 'spin 1s linear infinite' }}><circle cx="12" cy="12" r="9" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeDasharray="14 40"/></svg>
                              : <svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M3 8l3 3 7-8" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"/></svg>}
                          </button>
                          <button onClick={() => setConfirmHardId(null)} title={tx.cancel} aria-label={tx.cancel}
                            style={{ ...ICON_BTN, background: 'transparent', border: `1px solid ${NX.border}`, color: NX.muted }}>
                            <svg width="15" height="15" viewBox="0 0 16 16" fill="none"><path d="M4 4l8 8M12 4l-8 8" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round"/></svg>
                          </button>
                        </>
                      ) : (
                        <button onClick={() => setConfirmHardId(c.id)} title={tx.hardDelete} aria-label={tx.hardDelete}
                          style={{ ...ICON_BTN, background: 'transparent', border: `1px solid ${NX.border}`, color: NX.muted }}
                          onMouseEnter={e => { e.currentTarget.style.borderColor = 'rgba(248,113,113,0.4)'; e.currentTarget.style.color = NX.danger; }}
                          onMouseLeave={e => { e.currentTarget.style.borderColor = NX.border; e.currentTarget.style.color = NX.muted; }}>
                          <svg width="15" height="15" viewBox="0 0 16 16" fill="none"><path d="M3 4h10M6 4V2.5A.5.5 0 016.5 2h3a.5.5 0 01.5.5V4M4.5 4l.5 9a1 1 0 001 1h4a1 1 0 001-1l.5-9M7 7v5M9 7v5" stroke="currentColor" strokeWidth="1.3" strokeLinecap="round" strokeLinejoin="round"/></svg>
                        </button>
                      )}
                    </>
                  ) : confirmId === c.id ? (
                    <>
                      <button onClick={() => handleDelete(c.id)} disabled={deletingId === c.id}
                        title={lang === 'en' ? 'Pauses on Meta and moves to Archived. Data preserved.' : 'Pausa en Meta y mueve a Archivadas. Data preservada.'}
                        aria-label={tx.confirm}
                        style={{ ...ICON_BTN, background: NX.warning, border: 'none', color: '#000' }}>
                        {deletingId === c.id
                          ? <svg width="16" height="16" viewBox="0 0 24 24" fill="none" style={{ animation: 'spin 1s linear infinite' }}><circle cx="12" cy="12" r="9" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeDasharray="14 40"/></svg>
                          : <svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M3 8l3 3 7-8" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"/></svg>}
                      </button>
                      <button onClick={() => setConfirmId(null)} title={tx.cancel} aria-label={tx.cancel}
                        style={{ ...ICON_BTN, background: 'transparent', border: `1px solid ${NX.border}`, color: NX.muted }}>
                        <svg width="15" height="15" viewBox="0 0 16 16" fill="none"><path d="M4 4l8 8M12 4l-8 8" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round"/></svg>
                      </button>
                    </>
                  ) : (
                    <button onClick={() => setConfirmId(c.id)} title={lang === 'en' ? 'Archive — pauses on Meta but preserves all data' : 'Archivar — pausa en Meta pero preserva toda la data'}
                      aria-label={lang === 'en' ? 'Archive' : 'Archivar'}
                      style={{ ...ICON_BTN, background: 'transparent', border: `1px solid ${NX.border}`, color: NX.muted }}
                      onMouseEnter={e => { e.currentTarget.style.borderColor = 'rgba(251,191,36,0.4)'; e.currentTarget.style.color = NX.warning; }}
                      onMouseLeave={e => { e.currentTarget.style.borderColor = NX.border; e.currentTarget.style.color = NX.muted; }}>
                      <svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M2 4h12M2 4v9a1 1 0 001 1h10a1 1 0 001-1V4M6 7h4" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round" strokeLinejoin="round"/></svg>
                    </button>
                  )}
                </div>
              );
            })()}
            {/* Result feedback for the optimization action (per-card). Renders inline below
                the action row so the user sees what was paused/scaled without opening a modal.
                CRITERIA_NOT_MET errors expand to show the actual CTR/impressions/conversions
                so the user understands WHY the scale was blocked, not just that it was. */}
            {(() => {
              const r = optResultByCampaign[c.id];
              if (!r) return null;
              const isCriteria = r.code === 'CRITERIA_NOT_MET';
              const tone = r.error
                ? (isCriteria ? 'criteria' : 'error')
                : 'ok';
              const palette = tone === 'ok'
                ? { bg: 'rgba(52,211,153,0.10)', border: 'rgba(52,211,153,0.32)', text: NX.success, icon: '✓' }
                : tone === 'criteria'
                  ? { bg: 'rgba(251,191,36,0.08)', border: 'rgba(251,191,36,0.35)', text: NX.warning, icon: '🚫' }
                  : { bg: 'rgba(248,113,113,0.08)', border: 'rgba(248,113,113,0.30)', text: NX.danger, icon: '⚠' };
              return (
                <div style={{ flexBasis: '100%', marginTop: '6px', padding: '10px 12px', borderRadius: '8px', fontSize: '11px', lineHeight: 1.5, background: palette.bg, border: `1px solid ${palette.border}`, color: palette.text, display: 'flex', flexDirection: 'column', gap: '6px' }}>
                  <div style={{ display: 'flex', alignItems: 'flex-start', gap: '8px' }}>
                    <span style={{ flexShrink: 0 }}>{palette.icon}</span>
                    <span style={{ flex: 1, fontWeight: isCriteria ? 600 : 400 }}>
                      {isCriteria
                        ? (lang === 'en' ? "Can't scale yet — strategy criteria not met" : 'Aún no se puede escalar — no se cumplen los criterios de la estrategia')
                        : r.error
                          ? r.error
                          : r.phase === 'mid'
                            ? (() => {
                                const p = r.data?.paused?.length || 0;
                                const b = r.data?.budget_reduced?.length || 0;
                                const evals = r.data?.total_evaluated || 0;
                                if (lang === 'en') {
                                  if (p === 0 && b === 0) return `No action — none of the ${evals} ad(s) met the criteria (CTR<1% AND frequency>2.5) and no adset was 2× worse than another.`;
                                  return `${p} ad(s) paused${b ? `, ${b} adset(s) budget reduced 30%` : ''} (${evals} ad(s) evaluated)`;
                                } else {
                                  if (p === 0 && b === 0) return `Sin acción — ninguno de los ${evals} anuncio(s) cumplió los criterios (CTR<1% Y frecuencia>2.5) y ningún adset fue 2× peor que otro.`;
                                  return `${p} anuncio(s) pausado(s)${b ? `, ${b} adset(s) con presupuesto -30%` : ''} (${evals} anuncio(s) evaluados)`;
                                }
                              })()
                            : (lang === 'en'
                                ? `${r.data?.scaled?.length || 0} ad set(s) scaled +25% — Meta will charge the new daily rate`
                                : `${r.data?.scaled?.length || 0} conjunto(s) escalado(s) +25% — Meta cobrará la nueva tarifa diaria`)}
                    </span>
                    <button onClick={() => setOptResultByCampaign(prev => { const n = { ...prev }; delete n[c.id]; return n; })}
                      style={{ background: 'transparent', border: 'none', color: 'inherit', cursor: 'pointer', padding: '2px 6px', opacity: 0.6, fontSize: '12px' }}>×</button>
                  </div>
                  {isCriteria && r.diagnostics && (
                    <div style={{ paddingLeft: '20px', display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(140px, 1fr))', gap: '4px 12px', fontSize: '10px', fontFamily: "'DM Mono',monospace", color: NX.text }}>
                      <span>
                        <span style={{ color: NX.muted }}>CTR:</span>{' '}
                        <span style={{ color: r.diagnostics.ctr >= r.diagnostics.ctr_min ? NX.success : NX.warning }}>
                          {Number(r.diagnostics.ctr).toFixed(2)}%
                        </span>
                        <span style={{ color: NX.muted }}> / min {r.diagnostics.ctr_min}%</span>
                      </span>
                      <span>
                        <span style={{ color: NX.muted }}>Impresiones:</span>{' '}
                        <span style={{ color: r.diagnostics.impressions >= r.diagnostics.impressions_min ? NX.success : NX.warning }}>
                          {r.diagnostics.impressions}
                        </span>
                        <span style={{ color: NX.muted }}> / min {r.diagnostics.impressions_min}</span>
                      </span>
                      {r.diagnostics.requires_conversion && (
                        <span>
                          <span style={{ color: NX.muted }}>Conversiones:</span>{' '}
                          <span style={{ color: r.diagnostics.conversions >= 1 ? NX.success : NX.warning }}>
                            {r.diagnostics.conversions}
                          </span>
                          <span style={{ color: NX.muted }}> / min 1</span>
                        </span>
                      )}
                    </div>
                  )}
                </div>
              );
            })()}
          </NxCard>
        ))}
      </div>
    )}

    {/* Confirm modal for the dynamic "Aplicar plan" button (per-card). Mirrors the financial
        detail of the StrategyDetails modal but is mounted at the MisEstrategias root so any
        card can open it. Phase 'mid' has no money impact (pause-only); phase 'late' shows the
        current → new daily comparison + the bold Meta-charges disclaimer. */}
    {applyOptCampaign && (() => {
      const { campaign: oc, phase: op } = applyOptCampaign;
      const isLate = op === 'late';
      const currentDaily = Number(oc?.daily_budget) || 0;
      const newDaily = isLate ? Math.round(currentDaily * 1.25) : currentDaily;
      const monthlyEstimate = newDaily * 30;
      return (
        <div onClick={() => setApplyOptCampaign(null)}
          style={{ position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.78)', backdropFilter: 'blur(8px)', zIndex: 3000, display: 'flex', alignItems: 'center', justifyContent: 'center', padding: '24px' }}>
          <div onClick={e => e.stopPropagation()}
            style={{ background: NX.bg2, border: `1px solid ${isLate ? NX.warning : NX.accent}`, borderRadius: '16px', maxWidth: '460px', width: '100%', padding: '24px', animation: 'modalIn 0.18s ease', boxShadow: '0 30px 80px rgba(0,0,0,0.65), 0 0 40px rgba(139,92,246,0.18)' }}>
            <div style={{ display: 'flex', alignItems: 'center', gap: '10px', marginBottom: '12px' }}>
              <div style={{ width: '36px', height: '36px', borderRadius: '50%', background: isLate ? 'rgba(251,191,36,0.16)' : 'rgba(139,92,246,0.14)', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
                {isLate
                  ? <svg width="18" height="18" viewBox="0 0 24 24" fill="none"><path d="M12 9v4M12 17.5v.01M3.5 19.5h17l-8.5-15-8.5 15z" stroke={NX.warning} strokeWidth="1.7" strokeLinecap="round" strokeLinejoin="round"/></svg>
                  : <svg width="18" height="18" viewBox="0 0 24 24" fill="none"><path d="M9 12l2 2 4-4M21 12a9 9 0 11-18 0 9 9 0 0118 0z" stroke={NX.accent} strokeWidth="1.7" strokeLinecap="round" strokeLinejoin="round"/></svg>}
              </div>
              <div style={{ minWidth: 0, flex: 1 }}>
                <h3 style={{ fontSize: '16px', fontWeight: 600, color: NX.text, margin: 0 }}>
                  {isLate
                    ? (lang === 'en' ? 'Confirm budget scaling +25%' : 'Confirmar escalado +25% de presupuesto')
                    : (lang === 'en' ? 'Pause low-CTR ads' : 'Pausar anuncios con bajo CTR')}
                </h3>
                <div style={{ fontSize: '11px', color: NX.muted, fontFamily: "'DM Mono',monospace", letterSpacing: '0.06em', marginTop: '2px', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
                  {oc?.campaign_seq ? `ID-${String(oc.campaign_seq).padStart(2, '0')} · ` : ''}{oc?.title || '—'}
                </div>
              </div>
            </div>

            {isLate ? (<>
              <div style={{ background: NX.bg3, border: `1px solid ${NX.border}`, borderRadius: '10px', padding: '14px', marginBottom: '12px' }}>
                <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '8px' }}>
                  <span style={{ fontSize: '11px', color: NX.muted, fontFamily: "'DM Mono',monospace", letterSpacing: '0.08em', textTransform: 'uppercase' }}>{lang === 'en' ? 'Current daily' : 'Diario actual'}</span>
                  <span style={{ fontSize: '14px', color: NX.text, fontWeight: 600 }}>{fmtMoneyCard(currentDaily, oc?.currency)}</span>
                </div>
                <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '8px', paddingTop: '8px', borderTop: `1px dashed ${NX.border}` }}>
                  <span style={{ fontSize: '11px', color: NX.warning, fontFamily: "'DM Mono',monospace", letterSpacing: '0.08em', textTransform: 'uppercase' }}>{lang === 'en' ? 'New daily' : 'Diario nuevo'}</span>
                  <span style={{ fontSize: '17px', color: NX.warning, fontWeight: 700 }}>{fmtMoneyCard(newDaily, oc?.currency)}</span>
                </div>
                <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', fontSize: '11px', color: NX.muted }}>
                  <span>{lang === 'en' ? 'Estimated monthly' : 'Estimación mensual'}</span>
                  <span style={{ fontWeight: 600, color: NX.text }}>≈ {fmtMoneyCard(monthlyEstimate, oc?.currency)}</span>
                </div>
              </div>
              <div style={{ background: 'rgba(251,191,36,0.06)', border: `1px solid rgba(251,191,36,0.3)`, borderRadius: '10px', padding: '12px 14px', marginBottom: '14px' }}>
                <p style={{ fontSize: '12px', color: NX.text, margin: 0, lineHeight: 1.55 }}>
                  {lang === 'en'
                    ? <><b style={{ color: NX.warning }}>⚠ Meta will charge this new daily rate immediately</b> on your ad account, every day, until you change it again. NexWall does NOT process this payment — billing is direct to your Meta payment method.</>
                    : <><b style={{ color: NX.warning }}>⚠ Meta cobrará esta nueva tarifa diaria de inmediato</b> en tu cuenta publicitaria, cada día, hasta que la vuelvas a cambiar. NexWall NO procesa este cobro — el cargo lo realiza Meta directamente desde tu método de pago.</>}
                </p>
              </div>
              <div style={{ background: 'rgba(139,92,246,0.06)', border: `1px solid rgba(139,92,246,0.25)`, borderRadius: '8px', padding: '8px 12px', marginBottom: '8px', display: 'flex', alignItems: 'center', gap: '8px' }}>
                <svg width="14" height="14" viewBox="0 0 24 24" fill="none"><circle cx="12" cy="12" r="10" stroke={NX.accent} strokeWidth="1.5"/><path d="M12 7v5l3 2" stroke={NX.accent} strokeWidth="1.5" strokeLinecap="round"/></svg>
                <span style={{ fontSize: '11px', color: NX.muted, lineHeight: 1.4 }}>
                  {lang === 'en' ? 'Bonus protection: only 1 scale per 24 hours per campaign — no accidental compounding.' : 'Protección extra: solo 1 escalado cada 24 horas por campaña — sin compounding accidental.'}
                </span>
              </div>
              {/* Live KPI panel — fetched from /optimization-status when the modal opens.
                  Each criterion shows the real number vs the strategy minimum with a
                  pass/fail icon. The Confirmar button below is gated by eligible. */}
              {(() => {
                if (!optStatus || optStatus.loading) {
                  return (
                    <div style={{ background: NX.bg3, border: `1px solid ${NX.border}`, borderRadius: '10px', padding: '14px', marginBottom: '14px', display: 'flex', alignItems: 'center', gap: '10px', color: NX.muted, fontSize: '12px' }}>
                      <svg width="14" height="14" viewBox="0 0 24 24" fill="none" style={{ animation: 'spin 1s linear infinite' }}><circle cx="12" cy="12" r="9" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeDasharray="14 40"/></svg>
                      {lang === 'en' ? 'Reading current KPIs from Meta…' : 'Leyendo KPIs actuales desde Meta…'}
                    </div>
                  );
                }
                if (optStatus.error) {
                  return (
                    <div style={{ background: 'rgba(248,113,113,0.08)', border: `1px solid rgba(248,113,113,0.30)`, borderRadius: '10px', padding: '12px', marginBottom: '14px', fontSize: '12px', color: NX.danger }}>
                      ⚠ {optStatus.error}
                    </div>
                  );
                }
                const eligible = optStatus.eligible;
                const ctrPass = optStatus.ctr >= optStatus.ctr_min;
                const impPass = optStatus.impressions >= optStatus.impressions_min;
                const convPass = !optStatus.requires_conversion || optStatus.conversions >= 1;
                const Row = ({ ok, label, value, target }) => (
                  <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '6px 0', fontSize: '12px' }}>
                    <span style={{ display: 'flex', alignItems: 'center', gap: '8px', color: NX.text }}>
                      {ok
                        ? <svg width="14" height="14" viewBox="0 0 16 16" fill="none"><circle cx="8" cy="8" r="7" fill="rgba(52,211,153,0.18)"/><path d="M5 8l2 2 4-5" stroke={NX.success} strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"/></svg>
                        : <svg width="14" height="14" viewBox="0 0 16 16" fill="none"><circle cx="8" cy="8" r="7" fill="rgba(248,113,113,0.18)"/><path d="M5 5l6 6M11 5l-6 6" stroke={NX.danger} strokeWidth="1.8" strokeLinecap="round"/></svg>}
                      {label}
                    </span>
                    <span style={{ fontFamily: "'DM Mono',monospace", fontSize: '11px' }}>
                      <span style={{ color: ok ? NX.success : NX.danger, fontWeight: 600 }}>{value}</span>
                      <span style={{ color: NX.muted }}> / {target}</span>
                    </span>
                  </div>
                );
                return (
                  <div style={{ background: NX.bg3, border: `1px solid ${eligible ? 'rgba(52,211,153,0.32)' : 'rgba(248,113,113,0.30)'}`, borderRadius: '10px', padding: '12px 14px', marginBottom: '14px' }}>
                    <div style={{ fontSize: '10px', color: eligible ? NX.success : NX.danger, fontWeight: 700, fontFamily: "'DM Mono',monospace", letterSpacing: '0.1em', textTransform: 'uppercase', marginBottom: '6px' }}>
                      {eligible
                        ? (lang === 'en' ? '✓ Strategy criteria met — ready to scale' : '✓ Cumple los criterios de la estrategia')
                        : (lang === 'en' ? '✗ Strategy criteria NOT met' : '✗ No cumple los criterios de la estrategia')}
                    </div>
                    <Row ok={ctrPass}
                      label="CTR"
                      value={`${Number(optStatus.ctr || 0).toFixed(2)}%`}
                      target={`≥ ${optStatus.ctr_min}%`}/>
                    <Row ok={impPass}
                      label={lang === 'en' ? 'Impressions' : 'Impresiones'}
                      value={Number(optStatus.impressions || 0).toLocaleString()}
                      target={`≥ ${(optStatus.impressions_min || 0).toLocaleString()}`}/>
                    {optStatus.requires_conversion && (
                      <Row ok={convPass}
                        label={lang === 'en' ? 'Conversions' : 'Conversiones'}
                        value={String(optStatus.conversions || 0)}
                        target="≥ 1"/>
                    )}
                    {!eligible && (
                      <div style={{ marginTop: '6px', paddingTop: '8px', borderTop: `1px dashed ${NX.border}`, fontSize: '11px', color: NX.muted, lineHeight: 1.5 }}>
                        {lang === 'en'
                          ? 'Per the strategy, scaling +25% is only safe when these KPIs are met. NexWall blocks the scale until the campaign reaches the targets.'
                          : 'Según la estrategia, escalar +25% solo es seguro cuando estos KPIs se cumplen. NexWall bloquea el escalado hasta que la campaña alcance los objetivos.'}
                      </div>
                    )}
                  </div>
                );
              })()}
            </>) : (() => {
              // Descripción de la fase mid en formato de filas compactas. Antes era un párrafo
              // largo con info desactualizada ("CTR<1% AND >1000 impresiones"). Ahora cada
              // acción ocupa su propia fila con icono + criterio claro + qué hace.
              const Row = ({ icon, title, detail, accent }) => (
                <div style={{ display:'flex', alignItems:'flex-start', gap:'10px', padding:'10px 12px', background: NX.bg3, border:`1px solid ${NX.border}`, borderRadius:'9px' }}>
                  <div style={{ flexShrink:0, width:'28px', height:'28px', borderRadius:'7px', background: `${accent}1f`, color: accent, display:'flex', alignItems:'center', justifyContent:'center' }}>{icon}</div>
                  <div style={{ flex:1, minWidth:0 }}>
                    <div style={{ fontSize:'12px', fontWeight:600, color: NX.text, lineHeight:1.35 }}>{title}</div>
                    <div style={{ fontSize:'11px', color: NX.muted, fontFamily:"'DM Mono',monospace", letterSpacing:'0.02em', marginTop:'2px', lineHeight:1.45 }}>{detail}</div>
                  </div>
                </div>
              );
              return (
                <div style={{ display:'flex', flexDirection:'column', gap:'8px', marginBottom:'14px' }}>
                  <Row
                    accent={NX.danger}
                    icon={<svg width="14" height="14" viewBox="0 0 16 16" fill="none"><rect x="4" y="2.5" width="2.5" height="11" rx="0.7" fill="currentColor"/><rect x="9.5" y="2.5" width="2.5" height="11" rx="0.7" fill="currentColor"/></svg>}
                    title={lang === 'en' ? 'Pause underperforming ads' : 'Pausar anuncios perdedores'}
                    detail={lang === 'en' ? 'CTR < 1%  AND  frequency > 2.5' : 'CTR < 1%  Y  frecuencia > 2.5'}
                  />
                  <Row
                    accent={NX.warning}
                    icon={<svg width="14" height="14" viewBox="0 0 16 16" fill="none"><path d="M8 3v9M4 9l4 4 4-4" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round"/></svg>}
                    title={lang === 'en' ? 'Reduce 30% budget on the worst adset' : 'Reducir 30% el presupuesto del adset peor'}
                    detail={lang === 'en' ? 'When CPA/CPC ≥ 2× the best adset' : 'Cuando CPA/CPC ≥ 2× el mejor adset'}
                  />
                  <Row
                    accent={NX.success}
                    icon={<svg width="14" height="14" viewBox="0 0 16 16" fill="none"><path d="M3 8l3 3 7-8" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"/></svg>}
                    title={lang === 'en' ? 'Daily budget unchanged' : 'Presupuesto diario sin cambios'}
                    detail={lang === 'en' ? 'No surprise charges from Meta' : 'Sin cobros sorpresa de Meta'}
                  />
                </div>
              );
            })()}

            <div style={{ display: 'flex', gap: '10px' }}>
              <button onClick={() => setApplyOptCampaign(null)}
                style={{ flex: 1, padding: '11px 14px', background: 'transparent', border: `1px solid ${NX.border}`, borderRadius: '10px', color: NX.muted, fontSize: '13px', fontWeight: 500, cursor: 'pointer', fontFamily: "'DM Sans',sans-serif" }}>
                {lang === 'en' ? 'Cancel' : 'Cancelar'}
              </button>
              {(() => {
                // Late requires the gate to pass; mid has no gate so it's always confirmable
                const lateBlocked = isLate && (!optStatus || optStatus.loading || !optStatus.eligible);
                const confirmDisabled = lateBlocked;
                return (
                  <button onClick={() => { if (confirmDisabled) return; const target = applyOptCampaign; setApplyOptCampaign(null); runOptimization(target.campaign, target.phase); }}
                    disabled={confirmDisabled}
                    title={confirmDisabled ? (lang === 'en' ? 'Cannot scale until KPIs are met' : 'No se puede escalar hasta cumplir los KPIs') : ''}
                    style={{ flex: 2, padding: '11px 14px',
                      background: confirmDisabled
                        ? 'rgba(139,92,246,0.18)'
                        : isLate ? 'linear-gradient(135deg, #f59e0b, #fbbf24)' : 'linear-gradient(135deg, #7c3aed 0%, #8b5cf6 50%, #a78bfa 100%)',
                      border: 'none', borderRadius: '10px', color: '#fff', fontSize: '13px', fontWeight: 600,
                      cursor: confirmDisabled ? 'not-allowed' : 'pointer',
                      opacity: confirmDisabled ? 0.55 : 1,
                      fontFamily: "'DM Sans',sans-serif",
                      boxShadow: confirmDisabled ? 'none' : (isLate ? '0 8px 24px rgba(251,191,36,0.35)' : '0 8px 24px rgba(139,92,246,0.35)') }}>
                    {isLate
                      ? (confirmDisabled
                          ? (lang === 'en' ? 'Cannot scale yet' : 'Aún no se puede escalar')
                          : (lang === 'en' ? `Confirm — scale to ${fmtMoneyCard(newDaily, oc?.currency)}/day` : `Confirmar — escalar a ${fmtMoneyCard(newDaily, oc?.currency)}/día`))
                      : (lang === 'en' ? 'Confirm — pause low-CTR ads' : 'Confirmar — pausar anuncios')}
                  </button>
                );
              })()}
            </div>
          </div>
        </div>
      );
    })()}

    {/* ──────────── Analyze campaign modal ──────────────────────────────────────
        Triggered by the "Analizar campaña" button on each campaign card. Renders
        Sonnet 4.6 analysis: winners (read-only), losers (checkbox → pause), budget
        actions (checkbox → scale/reduce), creative recommendations (read-only),
        warnings. User picks which actions to apply with one click. */}
    {analyzeCampaign && (() => {
      const ac = analyzeCampaign;
      const healthColor = analyzeData?.health === 'good' ? NX.success
        : analyzeData?.health === 'critical' ? NX.danger
        : NX.warning;
      const healthLabel = lang === 'en'
        ? { good: 'Healthy', warning: 'Warning', critical: 'Critical' }
        : { good: 'Saludable', warning: 'Atención', critical: 'Crítico' };
      const ZERO_DECIMAL = new Set(['CLP','JPY','KRW','VND','PYG','XAF','XOF','UGX','RWF','MGA','KMF','DJF','BIF','GNF','VUV']);
      const fmtBudget = (minor) => {
        const cur = (ac.currency || 'USD').toUpperCase();
        const major = ZERO_DECIMAL.has(cur) ? Number(minor) : Number(minor) / 100;
        return fmtMoneyCard(major, cur);
      };
      const onClose = () => { if (!applyingActions) setAnalyzeCampaign(null); };

      return (
        <div
          style={{ position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.78)', backdropFilter: 'blur(8px)', zIndex: 3000, display: 'flex', alignItems: 'center', justifyContent: 'center', padding: '24px' }}>
          <div onClick={e => e.stopPropagation()}
            style={{ background: NX.bg2, border: `1px solid ${NX.border}`, borderRadius: '16px', maxWidth: '640px', width: '100%', maxHeight: '88vh', overflow: 'auto', animation: 'modalIn 0.18s ease', boxShadow: '0 30px 80px rgba(0,0,0,0.65)' }}>

            {/* Header */}
            <div style={{ padding: '20px 24px 14px', borderBottom: `1px solid ${NX.border}`, position: 'sticky', top: 0, background: NX.bg2, zIndex: 2 }}>
              <div style={{ display: 'flex', alignItems: 'flex-start', gap: '12px' }}>
                <div style={{ width: '40px', height: '40px', borderRadius: '50%', background: `${healthColor}22`, display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0 }}>
                  <svg width="20" height="20" viewBox="0 0 16 16" fill="none"><path d="M2 13l3-6 3 3 3-7 3 10" stroke={healthColor} strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round"/></svg>
                </div>
                <div style={{ flex: 1, minWidth: 0 }}>
                  <h3 style={{ fontSize: '17px', fontWeight: 600, color: NX.text, margin: 0 }}>
                    🔍 {lang === 'en' ? 'Campaign analysis' : 'Análisis de campaña'}
                  </h3>
                  <div style={{ fontSize: '11px', color: NX.muted, fontFamily: "'DM Mono',monospace", letterSpacing: '0.06em', marginTop: '3px', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
                    {ac.campaign_seq ? `ID-${String(ac.campaign_seq).padStart(2, '0')} · ` : ''}{ac.title || ac.meta_campaign_name}
                  </div>
                </div>
                <button onClick={onClose} disabled={applyingActions}
                  style={{ background: NX.bg3, border: `1px solid ${NX.border}`, width: '30px', height: '30px', borderRadius: '50%', color: NX.text, cursor: applyingActions ? 'not-allowed' : 'pointer', opacity: applyingActions ? 0.4 : 1, display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0 }}>
                  <svg width="12" height="12" viewBox="0 0 14 14" fill="none"><path d="M1 1l12 12M13 1L1 13" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round"/></svg>
                </button>
              </div>
            </div>

            <div style={{ padding: '18px 24px 22px' }}>

              {/* Loading state */}
              {/* Check inicial: estamos preguntando si hay análisis guardado */}
              {checkingSaved && (
                <div style={{ padding: '32px 16px', textAlign: 'center' }}>
                  <div style={{ display: 'inline-block', width: '40px', height: '40px', marginBottom: '16px' }}>
                    <svg width="40" height="40" viewBox="0 0 24 24" fill="none" style={{ animation: 'spin 1.2s linear infinite' }}>
                      <circle cx="12" cy="12" r="9" stroke={NX.muted} strokeWidth="2.5" strokeLinecap="round" strokeDasharray="14 40"/>
                    </svg>
                  </div>
                  <div style={{ fontSize: '12px', color: NX.muted }}>
                    {lang === 'en' ? 'Loading…' : 'Cargando…'}
                  </div>
                </div>
              )}

              {/* Picker: lista del historial (DESC por fecha) + opción de generar uno nuevo */}
              {analyzeMode === 'picker' && !applyResult && (
                <div style={{ padding: '4px 0 8px' }}>
                  <div style={{ fontSize: '13px', color: NX.text, marginBottom: '16px', lineHeight: 1.55 }}>
                    {lang === 'en'
                      ? <>You have <b>{savedAnalyses.length}</b> saved {savedAnalyses.length === 1 ? 'analysis' : 'analyses'} for this campaign. View any of them (free) or generate a new one with the latest Meta data.</>
                      : <>Tenés <b>{savedAnalyses.length}</b> análisis guardado{savedAnalyses.length === 1 ? '' : 's'} de esta campaña. Podés ver cualquiera (gratis) o generar uno nuevo con la última data de Meta.</>}
                  </div>

                  {/* Generar nuevo — primero, prominente. Disabled si cooldown activo. */}
                  <button onClick={() => { if (!cooldownActive) runFreshAnalyze(analyzeCampaign.id); }}
                    disabled={cooldownActive}
                    title={cooldownActive ? (lang === 'en' ? `Available in ${fmtCooldown(cooldownRemainingMs)}` : `Disponible en ${fmtCooldown(cooldownRemainingMs)}`) : ''}
                    style={{ display: 'flex', alignItems: 'center', gap: '14px', padding: '14px 16px',
                      background: cooldownActive ? NX.bg3 : 'rgba(139,92,246,0.10)',
                      border: `1px solid ${cooldownActive ? NX.border : 'rgba(139,92,246,0.35)'}`,
                      borderRadius: '12px',
                      cursor: cooldownActive ? 'not-allowed' : 'pointer',
                      textAlign: 'left', fontFamily: "'DM Sans',sans-serif",
                      color: cooldownActive ? NX.muted : NX.text,
                      transition: 'border 0.15s, background 0.15s',
                      width: '100%', marginBottom: '18px',
                      opacity: cooldownActive ? 0.7 : 1 }}
                    onMouseEnter={e => { if (cooldownActive) return; e.currentTarget.style.borderColor = NX.accent2; e.currentTarget.style.background = 'rgba(139,92,246,0.18)'; }}
                    onMouseLeave={e => { if (cooldownActive) return; e.currentTarget.style.borderColor = 'rgba(139,92,246,0.35)'; e.currentTarget.style.background = 'rgba(139,92,246,0.10)'; }}>
                    <div style={{ width: '40px', height: '40px', borderRadius: '50%', background: cooldownActive ? 'rgba(107,107,107,0.18)' : 'rgba(139,92,246,0.20)', display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0, fontSize: '20px', filter: cooldownActive ? 'grayscale(1)' : 'none' }}>
                      {cooldownActive ? '⏳' : '🔍'}
                    </div>
                    <div style={{ flex: 1, minWidth: 0 }}>
                      <div style={{ fontSize: '14px', fontWeight: 600, marginBottom: '2px' }}>
                        {cooldownActive
                          ? (lang === 'en' ? 'Cooldown active' : 'En espera (cooldown 24h)')
                          : (lang === 'en' ? 'Generate new analysis' : 'Generar análisis nuevo')}
                      </div>
                      <div style={{ fontSize: '11px', color: NX.muted, fontFamily: "'DM Mono',monospace" }}>
                        {cooldownActive
                          ? (lang === 'en' ? `Available in ${fmtCooldown(cooldownRemainingMs)}` : `Disponible en ${fmtCooldown(cooldownRemainingMs)}`)
                          : (lang === 'en' ? `Fresh Meta data · ${window.CREDIT_COSTS?.analyze_campaign || 10} credits` : `Data fresca de Meta · ${window.CREDIT_COSTS?.analyze_campaign || 10} créditos`)}
                      </div>
                    </div>
                    {!cooldownActive && (
                      <svg width="14" height="14" viewBox="0 0 16 16" fill="none" style={{ color: NX.accent2, flexShrink: 0 }}><path d="M6 3l5 5-5 5" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/></svg>
                    )}
                  </button>

                  {/* Lista de análisis guardados (DESC por fecha, max 50) */}
                  <div style={{ fontSize: '11px', color: NX.muted, fontFamily: "'DM Mono',monospace", letterSpacing: '0.1em', textTransform: 'uppercase', marginBottom: '8px' }}>
                    📂 {lang === 'en' ? 'History' : 'Historial'} · {savedAnalyses.length}
                  </div>
                  <div style={{ display: 'flex', flexDirection: 'column', gap: '6px', maxHeight: '380px', overflowY: 'auto', paddingRight: '4px' }}>
                    {savedAnalyses.map((a, idx) => {
                      const isLatest = idx === 0;
                      const healthC = a.health === 'good' ? NX.success : a.health === 'critical' ? NX.danger : NX.warning;
                      const summary = (a.summary || '').slice(0, 160);
                      const winnersN = (a.winners || []).length;
                      const losersN = (a.losers || []).length;
                      const budgetN = (a.budget_actions || []).length;
                      return (
                        <button key={a.analysis_id || idx} onClick={() => viewSavedAnalysis(idx)}
                          style={{ display: 'flex', flexDirection: 'column', alignItems: 'stretch', gap: '6px', padding: '12px 14px', background: NX.bg3, border: `1px solid ${isLatest ? `${healthC}55` : NX.border}`, borderRadius: '10px', cursor: 'pointer', textAlign: 'left', fontFamily: "'DM Sans',sans-serif", color: NX.text, transition: 'border 0.15s, background 0.15s', position: 'relative' }}
                          onMouseEnter={e => { e.currentTarget.style.background = 'rgba(91,142,240,0.06)'; e.currentTarget.style.borderColor = NX.accent; }}
                          onMouseLeave={e => { e.currentTarget.style.background = NX.bg3; e.currentTarget.style.borderColor = isLatest ? `${healthC}55` : NX.border; }}>
                          <div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
                            <span style={{ display: 'inline-flex', alignItems: 'center', padding: '2px 7px', background: `${healthC}1f`, border: `1px solid ${healthC}55`, borderRadius: '999px', fontSize: '9px', fontWeight: 700, color: healthC, fontFamily: "'DM Mono',monospace", letterSpacing: '0.08em', textTransform: 'uppercase' }}>
                              {a.health === 'good' ? (lang === 'en' ? 'Healthy' : 'Saludable') : a.health === 'critical' ? (lang === 'en' ? 'Critical' : 'Crítico') : (lang === 'en' ? 'Warning' : 'Atención')}
                            </span>
                            <span style={{ fontSize: '11px', color: NX.muted, fontFamily: "'DM Mono',monospace", flex: 1 }}>
                              {fmtAge(a.age_seconds)}
                            </span>
                            {isLatest && (
                              <span style={{ fontSize: '9px', color: NX.accent, background: 'rgba(91,142,240,0.14)', padding: '2px 6px', borderRadius: '4px', fontWeight: 700, fontFamily: "'DM Mono',monospace", letterSpacing: '0.08em' }}>
                                {lang === 'en' ? 'LATEST' : 'ÚLTIMO'}
                              </span>
                            )}
                          </div>
                          {summary && (
                            <div style={{ fontSize: '12px', color: NX.text, lineHeight: 1.45, opacity: 0.85 }}>
                              {summary}{(a.summary || '').length > 160 ? '…' : ''}
                            </div>
                          )}
                          <div style={{ display: 'flex', gap: '10px', fontSize: '10px', color: NX.muted, fontFamily: "'DM Mono',monospace" }}>
                            <span>✓ {winnersN} {lang === 'en' ? 'winners' : 'gan'}</span>
                            <span>⚠ {losersN} {lang === 'en' ? 'losers' : 'perd'}</span>
                            <span>⚡ {budgetN} {lang === 'en' ? 'budget' : 'budget'}</span>
                          </div>
                        </button>
                      );
                    })}
                  </div>
                </div>
              )}

              {/* Loading: análisis fresh corriendo */}
              {analyzing && (
                <div style={{ padding: '32px 16px', textAlign: 'center' }}>
                  <div style={{ display: 'inline-block', width: '40px', height: '40px', marginBottom: '16px' }}>
                    <svg width="40" height="40" viewBox="0 0 24 24" fill="none" style={{ animation: 'spin 1.2s linear infinite' }}>
                      <circle cx="12" cy="12" r="9" stroke={NX.accent} strokeWidth="2.5" strokeLinecap="round" strokeDasharray="14 40"/>
                    </svg>
                  </div>
                  <div style={{ fontSize: '14px', color: NX.text, marginBottom: '4px', fontWeight: 500 }}>
                    {lang === 'en' ? 'NexWall AI is studying your campaign…' : 'NexWall AI está estudiando tu campaña…'}
                  </div>
                  <div style={{ fontSize: '12px', color: NX.muted }}>
                    {lang === 'en' ? 'Applying our performance marketing playbook to find what to scale and what to pause.' : 'Aplicando nuestro playbook de marketing de performance para detectar qué escalar y qué pausar.'}
                  </div>
                </div>
              )}

              {/* Error states */}
              {analyzeError && !analyzing && (() => {
                if (analyzeError.code === 'NEEDS_DATA') {
                  const imp = Number(analyzeError.impressions || 0);
                  const min = Number(analyzeError.impressions_min || 1000);
                  const pct = Math.min(100, Math.round((imp / min) * 100));
                  return (
                    <div style={{ padding: '8px 4px' }}>
                      <div style={{ display: 'flex', alignItems: 'center', gap: '10px', marginBottom: '12px' }}>
                        <svg width="20" height="20" viewBox="0 0 24 24" fill="none"><circle cx="12" cy="12" r="10" stroke={NX.warning} strokeWidth="1.7"/><path d="M12 7v6M12 16v.01" stroke={NX.warning} strokeWidth="1.7" strokeLinecap="round"/></svg>
                        <div style={{ fontSize: '14px', color: NX.text, fontWeight: 600 }}>
                          {lang === 'en' ? 'Not enough data yet' : 'Aún no hay suficiente data'}
                        </div>
                      </div>
                      <div style={{ fontSize: '13px', color: NX.muted, lineHeight: 1.55, marginBottom: '14px' }}>
                        {lang === 'en'
                          ? `An analysis needs at least ${min.toLocaleString()} impressions to be statistically meaningful. Your campaign currently has ${imp.toLocaleString()}.`
                          : `Para que el análisis sea confiable se necesitan al menos ${min.toLocaleString()} impresiones. Tu campaña lleva ${imp.toLocaleString()}.`}
                      </div>
                      <div style={{ background: NX.bg3, border: `1px solid ${NX.border}`, borderRadius: '10px', padding: '14px' }}>
                        <div style={{ display: 'flex', justifyContent: 'space-between', fontSize: '11px', fontFamily: "'DM Mono',monospace", letterSpacing: '0.04em', color: NX.muted, marginBottom: '8px', textTransform: 'uppercase' }}>
                          <span>{lang === 'en' ? 'Progress' : 'Progreso'}</span>
                          <span style={{ color: NX.text, fontWeight: 600 }}>{pct}%</span>
                        </div>
                        <div style={{ height: '8px', background: NX.bg, borderRadius: '4px', overflow: 'hidden' }}>
                          <div style={{ width: `${pct}%`, height: '100%', background: `linear-gradient(90deg, ${NX.accent2}, ${NX.accent})`, transition: 'width 0.3s ease' }}/>
                        </div>
                        <div style={{ fontSize: '11px', color: NX.muted, marginTop: '8px' }}>
                          {lang === 'en'
                            ? `${(min - imp).toLocaleString()} more impressions needed.`
                            : `Faltan ${(min - imp).toLocaleString()} impresiones.`}
                        </div>
                      </div>
                      <div style={{ marginTop: '12px', fontSize: '11px', color: NX.muted, lineHeight: 1.5 }}>
                        {lang === 'en' ? 'No credits were charged. Try again when the campaign has more reach.' : 'No se cobraron créditos. Vuelve a intentar cuando la campaña tenga más alcance.'}
                      </div>
                    </div>
                  );
                }
                if (analyzeError.code === 'INSUFFICIENT_CREDITS') {
                  return (
                    <div style={{ padding: '8px 4px' }}>
                      <div style={{ fontSize: '14px', color: NX.danger, fontWeight: 600, marginBottom: '10px' }}>
                        {lang === 'en' ? 'Not enough credits' : 'No tienes suficientes créditos'}
                      </div>
                      <div style={{ fontSize: '13px', color: NX.muted, lineHeight: 1.55 }}>
                        {lang === 'en' ? `This analysis costs ${window.CREDIT_COSTS?.analyze_campaign || 10} credits.` : `Este análisis cuesta ${window.CREDIT_COSTS?.analyze_campaign || 10} créditos.`}
                      </div>
                    </div>
                  );
                }
                return (
                  <div style={{ padding: '8px 4px' }}>
                    <div style={{ fontSize: '14px', color: NX.danger, fontWeight: 600, marginBottom: '8px' }}>
                      {lang === 'en' ? 'Could not run analysis' : 'No se pudo analizar'}
                    </div>
                    <div style={{ fontSize: '12px', color: NX.muted, lineHeight: 1.5 }}>{analyzeError.msg}</div>
                  </div>
                );
              })()}

              {/* Result view */}
              {analyzeData && !analyzing && !analyzeError && analyzeMode !== 'picker' && !checkingSaved && (() => {
                const losers = analyzeData.losers || [];
                const winners = analyzeData.winners || [];
                const budgetActions = analyzeData.budget_actions || [];
                const creativeRecs = analyzeData.creative_recommendations || [];
                const warnings = analyzeData.warnings || [];

                // After applying actions, replace the body with the per-action report.
                if (applyResult) {
                  const results = applyResult.results || [];
                  return (
                    <div>
                      <div style={{ display: 'flex', alignItems: 'center', gap: '10px', marginBottom: '14px' }}>
                        <div style={{ width: '32px', height: '32px', borderRadius: '50%', background: applyResult.failed === 0 ? `${NX.success}22` : `${NX.warning}22`, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
                          {applyResult.failed === 0
                            ? <svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M3 8l3 3 7-8" stroke={NX.success} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/></svg>
                            : <svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M8 5v4M8 12v.01" stroke={NX.warning} strokeWidth="2" strokeLinecap="round"/></svg>}
                        </div>
                        <div>
                          <div style={{ fontSize: '14px', fontWeight: 600, color: NX.text }}>
                            {lang === 'en'
                              ? `${applyResult.succeeded}/${applyResult.total} actions applied`
                              : `${applyResult.succeeded}/${applyResult.total} acciones aplicadas`}
                          </div>
                          {applyResult.failed > 0 && (
                            <div style={{ fontSize: '11px', color: NX.warning }}>
                              {lang === 'en' ? `${applyResult.failed} failed` : `${applyResult.failed} fallaron`}
                            </div>
                          )}
                        </div>
                      </div>
                      <div style={{ display: 'flex', flexDirection: 'column', gap: '6px', maxHeight: '320px', overflowY: 'auto' }}>
                        {results.map((r, i) => (
                          <div key={i} style={{ display: 'flex', alignItems: 'center', gap: '10px', padding: '10px 12px', background: NX.bg3, border: `1px solid ${r.ok ? NX.border : 'rgba(248,113,113,0.30)'}`, borderRadius: '8px' }}>
                            {r.ok
                              ? <svg width="14" height="14" viewBox="0 0 16 16" fill="none" style={{ flexShrink: 0 }}><circle cx="8" cy="8" r="7" fill={`${NX.success}28`}/><path d="M5 8l2 2 4-5" stroke={NX.success} strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"/></svg>
                              : <svg width="14" height="14" viewBox="0 0 16 16" fill="none" style={{ flexShrink: 0 }}><circle cx="8" cy="8" r="7" fill={`${NX.danger}28`}/><path d="M5 5l6 6M11 5l-6 6" stroke={NX.danger} strokeWidth="1.8" strokeLinecap="round"/></svg>}
                            <div style={{ flex: 1, minWidth: 0 }}>
                              <div style={{ fontSize: '12px', color: NX.text, fontFamily: "'DM Mono',monospace", overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
                                {r.type === 'pause_ad' ? (lang === 'en' ? `Pause ad ${r.id}` : `Pausar ad ${r.id}`)
                                  : r.type === 'scale_campaign' ? (lang === 'en' ? `Scale campaign → ${fmtBudget(r.to)}` : `Subir campaña → ${fmtBudget(r.to)}`)
                                  : r.type === 'scale_adset' ? (lang === 'en' ? `Scale adset ${r.id} → ${fmtBudget(r.to)}` : `Subir adset ${r.id} → ${fmtBudget(r.to)}`)
                                  : (lang === 'en' ? `Reduce adset ${r.id} → ${fmtBudget(r.to)}` : `Reducir adset ${r.id} → ${fmtBudget(r.to)}`)}
                              </div>
                              {!r.ok && r.error && <div style={{ fontSize: '11px', color: NX.danger, marginTop: '2px' }}>{r.error}</div>}
                            </div>
                          </div>
                        ))}
                      </div>
                      <button onClick={() => setAnalyzeCampaign(null)}
                        style={{ marginTop: '16px', width: '100%', padding: '12px', background: NX.bg3, border: `1px solid ${NX.border}`, borderRadius: '10px', color: NX.text, fontSize: '13px', fontWeight: 600, cursor: 'pointer', fontFamily: "'DM Sans',sans-serif" }}>
                        {lang === 'en' ? 'Close' : 'Cerrar'}
                      </button>
                    </div>
                  );
                }

                return (
                  <div>
                    {/* Saved-mode banner: cuál análisis estás viendo (#N de M) + acciones */}
                    {analyzeMode === 'saved' && viewingSavedIndex !== null && !applyResult && (() => {
                      const total = savedAnalyses.length;
                      const ageSec = savedAnalyses[viewingSavedIndex]?.age_seconds;
                      const positionLabel = total > 1
                        ? (lang === 'en' ? `#${total - viewingSavedIndex} of ${total}` : `#${total - viewingSavedIndex} de ${total}`)
                        : '';
                      return (
                        <div style={{ display: 'flex', alignItems: 'center', gap: '10px', padding: '8px 12px', background: 'rgba(91,142,240,0.08)', border: `1px solid rgba(91,142,240,0.25)`, borderRadius: '10px', marginBottom: '14px', flexWrap: 'wrap' }}>
                          <svg width="14" height="14" viewBox="0 0 16 16" fill="none" style={{ flexShrink: 0 }}><path d="M3 8a5 5 0 1 0 1.5-3.5M3 4v3h3" stroke={NX.accent} strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/></svg>
                          <div style={{ flex: 1, minWidth: '120px', fontSize: '11px', color: NX.text, lineHeight: 1.4 }}>
                            {lang === 'en'
                              ? <>Viewing <b>saved analysis {positionLabel}</b> · {fmtAge(ageSec)}</>
                              : <>Viendo <b>análisis guardado {positionLabel}</b> · {fmtAge(ageSec)}</>}
                          </div>
                          {total > 1 && (
                            <button onClick={() => { setAnalyzeMode('picker'); setViewingSavedIndex(null); }}
                              style={{ background: 'transparent', border: `1px solid ${NX.border}`, color: NX.muted, padding: '5px 10px', borderRadius: '7px', fontSize: '10px', fontWeight: 600, cursor: 'pointer', fontFamily: "'DM Mono',monospace", letterSpacing: '0.05em', textTransform: 'uppercase', whiteSpace: 'nowrap' }}>
                              {lang === 'en' ? '← History' : '← Historial'}
                            </button>
                          )}
                          <button onClick={() => { if (!cooldownActive) runFreshAnalyze(analyzeCampaign.id); }}
                            disabled={cooldownActive}
                            title={cooldownActive ? (lang === 'en' ? `Cooldown — available in ${fmtCooldown(cooldownRemainingMs)}` : `Cooldown — disponible en ${fmtCooldown(cooldownRemainingMs)}`) : ''}
                            style={{ background: 'transparent', border: `1px solid ${cooldownActive ? NX.border : 'rgba(91,142,240,0.40)'}`, color: cooldownActive ? NX.muted : NX.accent, padding: '5px 10px', borderRadius: '7px', fontSize: '10px', fontWeight: 600, cursor: cooldownActive ? 'not-allowed' : 'pointer', fontFamily: "'DM Mono',monospace", letterSpacing: '0.05em', textTransform: 'uppercase', whiteSpace: 'nowrap', opacity: cooldownActive ? 0.55 : 1 }}>
                            {cooldownActive
                              ? (lang === 'en' ? `⏳ ${fmtCooldown(cooldownRemainingMs)}` : `⏳ ${fmtCooldown(cooldownRemainingMs)}`)
                              : (lang === 'en' ? 'Refresh' : 'Refrescar')}
                          </button>
                        </div>
                      );
                    })()}
                    {/* Health badge + summary */}
                    <div style={{ display: 'flex', alignItems: 'center', gap: '8px', marginBottom: '10px' }}>
                      <span style={{ display: 'inline-flex', alignItems: 'center', padding: '4px 10px', background: `${healthColor}1f`, border: `1px solid ${healthColor}55`, borderRadius: '999px', fontSize: '10px', fontWeight: 700, color: healthColor, fontFamily: "'DM Mono',monospace", letterSpacing: '0.1em', textTransform: 'uppercase' }}>
                        {healthLabel[analyzeData.health]}
                      </span>
                      <span style={{ fontSize: '11px', color: NX.muted, fontFamily: "'DM Mono',monospace" }}>
                        {analyzeData.meta?.days_running ? `${analyzeData.meta.days_running}d · ` : ''}
                        {Number(analyzeData.meta?.campaign_impressions || 0).toLocaleString()} {lang === 'en' ? 'impr' : 'impr'}
                      </span>
                    </div>
                    <div style={{ fontSize: '13px', color: NX.text, lineHeight: 1.6, marginBottom: '18px', padding: '12px 14px', background: NX.bg3, borderRadius: '10px', border: `1px solid ${NX.border}` }}>
                      {analyzeData.summary}
                    </div>

                    {/* Winners (read-only) */}
                    {winners.length > 0 && (
                      <div style={{ marginBottom: '16px' }}>
                        <div style={{ fontSize: '11px', fontWeight: 700, color: NX.success, fontFamily: "'DM Mono',monospace", letterSpacing: '0.1em', textTransform: 'uppercase', marginBottom: '8px' }}>
                          ✓ {lang === 'en' ? `Top performers (${winners.length})` : `Anuncios ganadores (${winners.length})`}
                        </div>
                        <div style={{ display: 'flex', flexDirection: 'column', gap: '6px' }}>
                          {winners.map((w, i) => (
                            <div key={i} style={{ padding: '10px 12px', background: 'rgba(52,211,153,0.06)', border: `1px solid rgba(52,211,153,0.20)`, borderRadius: '8px' }}>
                              <div style={{ fontSize: '12px', color: NX.text, fontWeight: 600 }}>{w.name}</div>
                              <div style={{ fontSize: '11px', color: NX.muted, marginTop: '2px', lineHeight: 1.45 }}>{w.why}</div>
                            </div>
                          ))}
                        </div>
                      </div>
                    )}

                    {/* Losers (checkbox → pause) */}
                    {losers.length > 0 && (
                      <div style={{ marginBottom: '16px' }}>
                        <div style={{ fontSize: '11px', fontWeight: 700, color: NX.danger, fontFamily: "'DM Mono',monospace", letterSpacing: '0.1em', textTransform: 'uppercase', marginBottom: '8px' }}>
                          ⚠ {lang === 'en' ? `Recommended to pause (${losers.length})` : `Recomendado pausar (${losers.length})`}
                        </div>
                        <div style={{ display: 'flex', flexDirection: 'column', gap: '6px' }}>
                          {losers.map((l, i) => {
                            const k = `loser_${i}`;
                            const checked = !!selectedActions[k];
                            return (
                              <label key={i} style={{ display: 'flex', alignItems: 'flex-start', gap: '10px', padding: '10px 12px', background: checked ? 'rgba(248,113,113,0.10)' : NX.bg3, border: `1px solid ${checked ? 'rgba(248,113,113,0.45)' : NX.border}`, borderRadius: '8px', cursor: 'pointer', transition: 'background 0.15s, border 0.15s' }}>
                                <input type="checkbox" checked={checked} onChange={() => toggleAction(k)}
                                  style={{ marginTop: '2px', cursor: 'pointer', accentColor: NX.danger }}/>
                                <div style={{ flex: 1, minWidth: 0 }}>
                                  <div style={{ fontSize: '12px', color: NX.text, fontWeight: 600, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{l.name}</div>
                                  <div style={{ fontSize: '11px', color: NX.muted, marginTop: '2px', lineHeight: 1.45 }}>{l.why}</div>
                                  <div style={{ fontSize: '10px', color: NX.muted, fontFamily: "'DM Mono',monospace", marginTop: '4px' }}>
                                    CTR {Number(l.ctr || 0).toFixed(2)}% · {lang === 'en' ? 'freq' : 'freq'} {Number(l.frequency || 0).toFixed(2)} · {Number(l.impressions || 0).toLocaleString()} {lang === 'en' ? 'impr' : 'impr'}
                                  </div>
                                </div>
                              </label>
                            );
                          })}
                        </div>
                      </div>
                    )}

                    {/* Budget actions (checkbox) */}
                    {budgetActions.length > 0 && (
                      <div style={{ marginBottom: '16px' }}>
                        <div style={{ fontSize: '11px', fontWeight: 700, color: NX.warning, fontFamily: "'DM Mono',monospace", letterSpacing: '0.1em', textTransform: 'uppercase', marginBottom: '8px' }}>
                          ⚡ {lang === 'en' ? `Budget actions (${budgetActions.length})` : `Acciones de presupuesto (${budgetActions.length})`}
                        </div>
                        <div style={{ display: 'flex', flexDirection: 'column', gap: '6px' }}>
                          {budgetActions.map((b, i) => {
                            const k = `budget_${i}`;
                            const checked = !!selectedActions[k];
                            const isScale = b.type === 'scale_campaign' || b.type === 'scale_adset';
                            const arrow = isScale ? '↑' : '↓';
                            const arrowColor = isScale ? NX.success : NX.warning;
                            return (
                              <label key={i} style={{ display: 'flex', alignItems: 'flex-start', gap: '10px', padding: '10px 12px', background: checked ? `${arrowColor}10` : NX.bg3, border: `1px solid ${checked ? `${arrowColor}55` : NX.border}`, borderRadius: '8px', cursor: 'pointer', transition: 'background 0.15s, border 0.15s' }}>
                                <input type="checkbox" checked={checked} onChange={() => toggleAction(k)}
                                  style={{ marginTop: '2px', cursor: 'pointer', accentColor: arrowColor }}/>
                                <div style={{ flex: 1, minWidth: 0 }}>
                                  <div style={{ display: 'flex', alignItems: 'center', gap: '6px', fontSize: '12px', color: NX.text, fontWeight: 600 }}>
                                    <span style={{ color: arrowColor, fontSize: '14px' }}>{arrow}</span>
                                    <span>
                                      {b.type === 'scale_campaign' ? (lang === 'en' ? 'Scale campaign budget' : 'Subir presupuesto de campaña')
                                        : b.type === 'reduce_adset' ? (lang === 'en' ? 'Reduce adset budget' : 'Reducir presupuesto de adset')
                                        : (lang === 'en' ? 'Scale adset budget' : 'Subir presupuesto de adset')}
                                    </span>
                                  </div>
                                  <div style={{ fontSize: '11px', color: NX.muted, marginTop: '3px', lineHeight: 1.45 }}>{b.why}</div>
                                  <div style={{ fontSize: '11px', color: NX.text, fontFamily: "'DM Mono',monospace", marginTop: '4px' }}>
                                    {fmtBudget(b.from)} <span style={{ color: NX.muted }}>→</span> <span style={{ color: arrowColor, fontWeight: 700 }}>{fmtBudget(b.to)}</span>
                                    {b.ratio && <span style={{ color: NX.muted, marginLeft: '6px' }}>({Math.round((b.ratio - 1) * 100 > 0 ? (b.ratio - 1) * 100 : (1 - b.ratio) * -100)}%)</span>}
                                  </div>
                                </div>
                              </label>
                            );
                          })}
                        </div>
                      </div>
                    )}

                    {/* Creative recommendations (read-only) */}
                    {creativeRecs.length > 0 && (
                      <div style={{ marginBottom: '16px' }}>
                        <div style={{ fontSize: '11px', fontWeight: 700, color: NX.accent2, fontFamily: "'DM Mono',monospace", letterSpacing: '0.1em', textTransform: 'uppercase', marginBottom: '8px' }}>
                          ✦ {lang === 'en' ? 'Creative ideas' : 'Ideas creativas'}
                        </div>
                        <ul style={{ margin: 0, padding: '0 0 0 20px', display: 'flex', flexDirection: 'column', gap: '6px' }}>
                          {creativeRecs.map((r, i) => (
                            <li key={i} style={{ fontSize: '12px', color: NX.muted, lineHeight: 1.55 }}>{r}</li>
                          ))}
                        </ul>
                      </div>
                    )}

                    {/* Warnings */}
                    {warnings.length > 0 && (
                      <div style={{ marginBottom: '16px' }}>
                        <div style={{ fontSize: '11px', fontWeight: 700, color: NX.warning, fontFamily: "'DM Mono',monospace", letterSpacing: '0.1em', textTransform: 'uppercase', marginBottom: '8px' }}>
                          ⚠ {lang === 'en' ? 'Watch out' : 'Atención'}
                        </div>
                        <div style={{ display: 'flex', flexDirection: 'column', gap: '6px' }}>
                          {warnings.map((w, i) => (
                            <div key={i} style={{ padding: '8px 12px', background: 'rgba(251,191,36,0.06)', border: `1px solid rgba(251,191,36,0.25)`, borderRadius: '8px', fontSize: '12px', color: NX.text, lineHeight: 1.5 }}>
                              {w}
                            </div>
                          ))}
                        </div>
                      </div>
                    )}

                    {/* Empty state */}
                    {losers.length === 0 && winners.length === 0 && budgetActions.length === 0 && creativeRecs.length === 0 && warnings.length === 0 && (
                      <div style={{ padding: '20px', textAlign: 'center', color: NX.muted, fontSize: '13px' }}>
                        {lang === 'en' ? 'Nothing to recommend right now — keep monitoring.' : 'Nada que recomendar por ahora — sigue monitoreando.'}
                      </div>
                    )}

                    {/* Footer: apply button */}
                    {(losers.length > 0 || budgetActions.length > 0) && (
                      <div style={{ display: 'flex', gap: '10px', marginTop: '8px', paddingTop: '14px', borderTop: `1px solid ${NX.border}` }}>
                        <button onClick={onClose} disabled={applyingActions}
                          style={{ flex: 1, padding: '11px 14px', background: 'transparent', border: `1px solid ${NX.border}`, borderRadius: '10px', color: NX.muted, fontSize: '13px', fontWeight: 500, cursor: applyingActions ? 'not-allowed' : 'pointer', fontFamily: "'DM Sans',sans-serif" }}>
                          {lang === 'en' ? 'Close' : 'Cerrar'}
                        </button>
                        <button onClick={applySelectedAnalysisActions}
                          disabled={selectedActionCount === 0 || applyingActions}
                          style={{ flex: 2, padding: '11px 14px',
                            background: (selectedActionCount === 0 || applyingActions) ? 'rgba(139,92,246,0.18)' : 'linear-gradient(135deg, #7c3aed 0%, #8b5cf6 50%, #a78bfa 100%)',
                            border: 'none', borderRadius: '10px', color: '#fff', fontSize: '13px', fontWeight: 600,
                            cursor: (selectedActionCount === 0 || applyingActions) ? 'not-allowed' : 'pointer',
                            opacity: (selectedActionCount === 0 || applyingActions) ? 0.55 : 1,
                            fontFamily: "'DM Sans',sans-serif",
                            boxShadow: (selectedActionCount === 0 || applyingActions) ? 'none' : '0 8px 24px rgba(139,92,246,0.35)' }}>
                          {applyingActions
                            ? (lang === 'en' ? 'Applying…' : 'Aplicando…')
                            : selectedActionCount === 0
                              ? (lang === 'en' ? 'Select at least one action' : 'Selecciona al menos una acción')
                              : (lang === 'en'
                                  ? `Apply ${selectedActionCount} action${selectedActionCount > 1 ? 's' : ''}`
                                  : `Aplicar ${selectedActionCount} ${selectedActionCount > 1 ? 'acciones' : 'acción'}`)}
                        </button>
                      </div>
                    )}
                  </div>
                );
              })()}

              {/* Generic close button when no result yet (error states already inside) */}
              {!analyzing && !analyzeData && analyzeError && (
                <div style={{ marginTop: '18px', paddingTop: '14px', borderTop: `1px solid ${NX.border}` }}>
                  <button onClick={onClose}
                    style={{ width: '100%', padding: '11px 14px', background: NX.bg3, border: `1px solid ${NX.border}`, borderRadius: '10px', color: NX.text, fontSize: '13px', fontWeight: 600, cursor: 'pointer', fontFamily: "'DM Sans',sans-serif" }}>
                    {lang === 'en' ? 'Close' : 'Cerrar'}
                  </button>
                </div>
              )}
            </div>
          </div>
        </div>
      );
    })()}

    {/* Mobile bottom-sheet for the kebab menu. Renders at component level (not inside the
        card) so it floats above the bottom nav and stays accessible regardless of scroll
        position — the previous absolute popover got clipped under the bottom nav when the
        card was near the end of the list. */}
    {isMobile && openMenuId && (() => {
      const c = campaigns.find(x => x.id === openMenuId);
      if (!c) return null;
      let metaAdsUrl = null;
      if (c.meta_campaign_id) {
        let acct = '';
        try {
          const cached = JSON.parse(localStorage.getItem('nw_assets_' + (c.biz_id || '')) || 'null');
          if (cached?.ad_account_id) acct = String(cached.ad_account_id).replace(/^act_/, '');
        } catch {}
        metaAdsUrl = acct
          ? `https://adsmanager.facebook.com/adsmanager/manage/campaigns?act=${acct}&selected_campaign_ids=${encodeURIComponent(c.meta_campaign_id)}`
          : `https://adsmanager.facebook.com/adsmanager/manage/campaigns?selected_campaign_ids=${encodeURIComponent(c.meta_campaign_id)}`;
      }
      return (
        <div onClick={() => setOpenMenuId(null)}
          style={{ position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.6)', backdropFilter: 'blur(6px)', zIndex: 1100, display: 'flex', alignItems: 'flex-end', justifyContent: 'center', animation: 'modalIn 0.18s ease' }}>
          <div onClick={e => e.stopPropagation()}
            style={{ width: '100%', maxWidth: '520px', background: 'rgba(14,14,16,0.98)', backdropFilter: 'blur(28px)', borderTopLeftRadius: '18px', borderTopRightRadius: '18px', borderTop: `1px solid ${NX.border}`, padding: '8px 8px calc(env(safe-area-inset-bottom, 0px) + 76px)', boxShadow: '0 -16px 48px rgba(0,0,0,0.6)' }}>
            <div style={{ display: 'flex', justifyContent: 'center', padding: '6px 0 8px' }}>
              <div style={{ width: '40px', height: '4px', borderRadius: '2px', background: NX.border }}/>
            </div>
            <div style={{ padding: '4px 12px 10px', fontSize: '11px', color: NX.muted, fontFamily: "'DM Mono',monospace", letterSpacing: '0.1em', textTransform: 'uppercase', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
              {c.campaign_seq ? `ID-${String(c.campaign_seq).padStart(2, '0')} · ` : ''}{c.title || c.meta_campaign_name}
            </div>
            {c.status !== 'draft' && onLaunchDraft && (
              <button onClick={() => { setOpenMenuId(null); onLaunchDraft(c); }}
                style={{ display: 'flex', alignItems: 'center', gap: '12px', width: '100%', padding: '14px 14px', borderRadius: '10px', border: 'none', background: 'transparent', color: NX.accent2, fontSize: '15px', fontFamily: "'DM Sans',sans-serif", fontWeight: 600, cursor: 'pointer', textAlign: 'left' }}>
                <svg width="18" height="18" viewBox="0 0 16 16" fill="none"><path d="M2 8a6 6 0 1 1 1.76 4.24M2 13V9h4" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/></svg>
                {lang === 'en' ? 'Reuse' : 'Reutilizar'}
              </button>
            )}
            {metaAdsUrl && (
              <a href={metaAdsUrl} target="_blank" rel="noopener noreferrer"
                onClick={() => setOpenMenuId(null)}
                style={{ display: 'flex', alignItems: 'center', gap: '12px', width: '100%', padding: '14px 14px', borderRadius: '10px', color: '#4f9bff', fontSize: '15px', fontFamily: "'DM Sans',sans-serif", fontWeight: 600, textDecoration: 'none' }}>
                <svg width="18" height="18" viewBox="0 0 24 24" fill="#1877F2"><path d="M24 12.073c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.99 4.388 10.954 10.125 11.854v-8.385H7.078v-3.47h3.047V9.43c0-3.007 1.792-4.669 4.533-4.669 1.312 0 2.686.235 2.686.235v2.953H15.83c-1.491 0-1.956.925-1.956 1.874v2.25h3.328l-.532 3.47h-2.796v8.385C19.612 23.027 24 18.062 24 12.073z"/></svg>
                {lang === 'en' ? 'Open in Meta Ads Manager' : 'Ver en Meta Ads Manager'}
              </a>
            )}
            <button onClick={() => { setOpenMenuId(null); setDetailCampaign(c); }}
              style={{ display: 'flex', alignItems: 'center', gap: '12px', width: '100%', padding: '14px 14px', borderRadius: '10px', border: 'none', background: 'transparent', color: NX.text, fontSize: '15px', fontFamily: "'DM Sans',sans-serif", cursor: 'pointer', textAlign: 'left' }}>
              <svg width="18" height="18" viewBox="0 0 16 16" fill="none"><path d="M1 8s2.5-5 7-5 7 5 7 5-2.5 5-7 5-7-5-7-5z" stroke="currentColor" strokeWidth="1.3"/><circle cx="8" cy="8" r="2" stroke="currentColor" strokeWidth="1.3"/></svg>
              {tx.view}
            </button>
            {c.status !== 'draft' && c.meta_campaign_id && (
              <button onClick={() => { setOpenMenuId(null); setAnalyzeCampaign(c); }}
                style={{ display: 'flex', alignItems: 'center', gap: '12px', width: '100%', padding: '14px 14px', borderRadius: '10px', border: 'none', background: 'transparent', color: NX.text, fontSize: '15px', fontFamily: "'DM Sans',sans-serif", cursor: 'pointer', textAlign: 'left' }}>
                <span style={{ fontSize: '18px', lineHeight: 1, width: '18px', textAlign: 'center' }}>🔍</span>
                {tx.analyze}
              </button>
            )}
            <div style={{ height: '1px', background: NX.border, margin: '6px 12px' }}/>
            {confirmId === c.id ? (
              <button onClick={() => { handleDelete(c.id); setOpenMenuId(null); }}
                disabled={deletingId === c.id}
                style={{ display: 'flex', alignItems: 'center', gap: '12px', width: '100%', padding: '14px 14px', borderRadius: '10px', border: 'none', background: 'rgba(248,113,113,0.18)', color: NX.danger, fontSize: '15px', fontFamily: "'DM Sans',sans-serif", fontWeight: 700, cursor: 'pointer', textAlign: 'left' }}>
                <svg width="18" height="18" viewBox="0 0 16 16" fill="none"><path d="M3 4h10M6 4V2.5A.5.5 0 016.5 2h3a.5.5 0 01.5.5V4M4.5 4l.5 9a1 1 0 001 1h4a1 1 0 001-1l.5-9" stroke="currentColor" strokeWidth="1.3" strokeLinecap="round" strokeLinejoin="round"/></svg>
                {deletingId === c.id ? '…' : tx.confirm}
              </button>
            ) : (
              <button onClick={() => setConfirmId(c.id)}
                style={{ display: 'flex', alignItems: 'center', gap: '12px', width: '100%', padding: '14px 14px', borderRadius: '10px', border: 'none', background: 'transparent', color: NX.muted, fontSize: '15px', fontFamily: "'DM Sans',sans-serif", cursor: 'pointer', textAlign: 'left' }}>
                <svg width="18" height="18" viewBox="0 0 16 16" fill="none"><path d="M3 4h10M6 4V2.5A.5.5 0 016.5 2h3a.5.5 0 01.5.5V4M4.5 4l.5 9a1 1 0 001 1h4a1 1 0 001-1l.5-9" stroke="currentColor" strokeWidth="1.3" strokeLinecap="round" strokeLinejoin="round"/></svg>
                {tx.delete}
              </button>
            )}
            <button onClick={() => setOpenMenuId(null)}
              style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', gap: '8px', width: '100%', padding: '12px 14px', marginTop: '6px', borderRadius: '10px', border: `1px solid ${NX.border}`, background: 'transparent', color: NX.muted, fontSize: '13px', fontFamily: "'DM Sans',sans-serif", cursor: 'pointer' }}>
              {tx.cancel}
            </button>
          </div>
        </div>
      );
    })()}

    {detailCampaign && (
      <div
        onClick={() => setDetailCampaign(null)}
        style={{ position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.78)', backdropFilter: 'blur(6px)', zIndex: 1000, display: 'flex', alignItems: 'center', justifyContent: 'center', padding: '40px' }}
      >
        <div
          onClick={e => e.stopPropagation()}
          style={{ background: NX.bg2, border: `1px solid ${NX.border}`, borderRadius: '16px', maxWidth: '1100px', width: '100%', maxHeight: '88vh', overflow: 'auto', position: 'relative' }}
        >
          <button
            onClick={() => setDetailCampaign(null)}
            aria-label={tx.close}
            style={{ position: 'absolute', top: '16px', right: '16px', background: NX.bg3, border: `1px solid ${NX.border}`, width: '32px', height: '32px', borderRadius: '50%', color: NX.text, cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 2 }}
          >
            <svg width="14" height="14" viewBox="0 0 14 14" fill="none"><path d="M1 1l12 12M13 1L1 13" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round"/></svg>
          </button>

          <div style={{ padding: '32px' }}>
            <div style={{ marginBottom: '24px' }}>
              <div style={{ fontSize: '12px', color: NX.muted, fontFamily: "'DM Mono',monospace", letterSpacing: '0.08em', marginBottom: '6px' }}>{tx.creatives.toUpperCase()}</div>
              <h2 style={{ fontSize: '22px', fontWeight: 500, color: NX.text, margin: '0 0 8px' }}>{detailCampaign.title}</h2>
              <div style={{ fontSize: '12px', color: NX.muted }}>Meta Ads · {detailCampaign.objective} · {detailCampaign.created_at ? new Date(detailCampaign.created_at).toLocaleDateString() : ''}</div>
              <CampaignCreditsSummary campaignId={detailCampaign.id} lang={lang}/>
            </div>

            {detailCampaign.product_desc && (
              <div style={{ marginBottom: '20px', padding: '14px 16px', background: NX.bg3, border: `1px solid ${NX.border}`, borderRadius: '10px' }}>
                <div style={{ fontSize: '11px', color: NX.muted, fontFamily: "'DM Mono',monospace", letterSpacing: '0.08em', marginBottom: '6px' }}>{tx.product.toUpperCase()}</div>
                <div style={{ fontSize: '13px', color: NX.text, lineHeight: 1.5, whiteSpace: 'pre-wrap' }}>{detailCampaign.product_desc}</div>
              </div>
            )}

            {(detailCampaign.copy_title || detailCampaign.copy_desc || detailCampaign.copy_cta) && (
              <div style={{ marginBottom: '24px', padding: '14px 16px', background: NX.bg3, border: `1px solid ${NX.border}`, borderRadius: '10px' }}>
                <div style={{ fontSize: '11px', color: NX.muted, fontFamily: "'DM Mono',monospace", letterSpacing: '0.08em', marginBottom: '8px' }}>{tx.copy.toUpperCase()}</div>
                {detailCampaign.copy_title && <div style={{ fontSize: '14px', color: NX.text, fontWeight: 500, marginBottom: '4px' }}>{detailCampaign.copy_title}</div>}
                {detailCampaign.copy_desc && <div style={{ fontSize: '13px', color: NX.muted, lineHeight: 1.5, marginBottom: '6px' }}>{detailCampaign.copy_desc}</div>}
                {detailCampaign.copy_cta && <div style={{ display: 'inline-block', fontSize: '11px', color: NX.accent, border: `1px solid ${NX.accent}`, borderRadius: '999px', padding: '3px 10px', fontWeight: 500 }}>{tx.cta}: {detailCampaign.copy_cta}</div>}
              </div>
            )}

            {detailCampaign.strategy && (
              <div style={{ marginBottom: '28px' }}>
                <div style={{ fontSize: '11px', color: NX.muted, fontFamily: "'DM Mono',monospace", letterSpacing: '0.08em', marginBottom: '12px' }}>{tx.strategy.toUpperCase()}</div>
                <StrategyDetails strategy={detailCampaign.strategy} lang={lang} campaign={detailCampaign}/>
              </div>
            )}

            <div style={{ fontSize: '11px', color: NX.muted, fontFamily: "'DM Mono',monospace", letterSpacing: '0.08em', marginBottom: '12px' }}>{tx.creativesSection.toUpperCase()}</div>
            {(() => {
              const urls = Array.isArray(detailCampaign.creative_image_urls) ? detailCampaign.creative_image_urls : [];
              if (urls.length === 0) {
                return (
                  <div style={{ padding: '40px', textAlign: 'center', color: NX.muted, fontSize: '13px', background: NX.bg3, borderRadius: '10px', border: `1px dashed ${NX.border}` }}>
                    {tx.noCreatives}
                  </div>
                );
              }
              // Mini CTA label map (same enums as StrategyFlow)
              const ctaOptions = {
                LEARN_MORE: lang === 'en' ? 'Learn More' : 'Más información',
                SHOP_NOW: lang === 'en' ? 'Shop Now' : 'Comprar ahora',
                SIGN_UP: lang === 'en' ? 'Sign Up' : 'Registrarse',
                SUBSCRIBE: lang === 'en' ? 'Subscribe' : 'Suscribirse',
                BOOK_TRAVEL: lang === 'en' ? 'Book Now' : 'Reservar',
                CONTACT_US: lang === 'en' ? 'Contact Us' : 'Contáctanos',
                DOWNLOAD: lang === 'en' ? 'Download' : 'Descargar',
                GET_OFFER: lang === 'en' ? 'Get Offer' : 'Obtener oferta',
                GET_QUOTE: lang === 'en' ? 'Get Quote' : 'Solicitar cotización',
                APPLY_NOW: lang === 'en' ? 'Apply Now' : 'Solicitar',
                ORDER_NOW: lang === 'en' ? 'Order Now' : 'Pedir ahora',
                SEND_MESSAGE: lang === 'en' ? 'Send Message' : 'Enviar mensaje',
                WHATSAPP_MESSAGE: lang === 'en' ? 'Send WhatsApp' : 'Enviar WhatsApp',
              };
              const ctaText = ctaOptions[detailCampaign.cta_button] || ctaOptions.LEARN_MORE;
              // All creatives of a campaign share the same final copy (Meta serves one ad set with the shared copy)
              const primaryText = detailCampaign.copy_desc || '';
              const headline = detailCampaign.copy_title || '';
              const description = detailCampaign.copy_cta || '';
              return (
                <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill,minmax(260px,1fr))', gap: '14px' }}>
                  {urls.map((url, i) => (
                    <div key={i} style={{ background: '#ffffff', border: `1px solid ${NX.border}`, borderRadius: '12px', overflow: 'hidden', display: 'flex', flexDirection: 'column', color: '#050505' }}>
                      {/* Primary text (arriba de la imagen como en FB) */}
                      {primaryText && (
                        <div style={{ padding: '8px 10px', fontSize: '10px', lineHeight: 1.4, color: '#050505', maxHeight: '60px', overflow: 'hidden' }}>
                          {primaryText.length > 140 ? primaryText.slice(0, 140) + '…' : primaryText}
                        </div>
                      )}
                      {/* Image */}
                      <div style={{ background: '#000', aspectRatio: '4 / 5', position: 'relative' }}>
                        <img src={url} alt={`creative-${i + 1}`} style={{ position: 'absolute', inset: 0, width: '100%', height: '100%', objectFit: 'contain' }}/>
                      </div>
                      {/* Link preview (descripción overline + título + CTA) */}
                      <div style={{ padding: '8px 10px', background: '#F0F2F5', display: 'flex', alignItems: 'center', gap: '8px' }}>
                        <div style={{ flex: 1, minWidth: 0 }}>
                          {description && (
                            <div style={{ fontSize: '9px', color: '#65676B', textTransform: 'uppercase', letterSpacing: '0.05em', marginBottom: '2px', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{description}</div>
                          )}
                          <div style={{ fontSize: '11px', fontWeight: 700, color: '#050505', lineHeight: 1.25, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{headline || '—'}</div>
                        </div>
                        <div style={{ background: '#E4E6EB', color: '#050505', fontSize: '10px', fontWeight: 700, padding: '6px 9px', borderRadius: '5px', whiteSpace: 'nowrap', flexShrink: 0 }}>
                          {ctaText}
                        </div>
                      </div>
                    </div>
                  ))}
                </div>
              );
            })()}
          </div>
        </div>
      </div>
    )}
  </div>
  );
};

// ══════════════════════════════════════════
// SCREEN: INTEGRACIONES
// ══════════════════════════════════════════
const Integraciones = ({ connected, lang='es' }) => {
  const ti = (I18N && I18N[lang]) ? I18N[lang].integraciones : I18N.es.integraciones;
  const [resetting, setResetting] = React.useState(false);
  const [confirmReset, setConfirmReset] = React.useState(false);
  const [diagnosing, setDiagnosing] = React.useState(false);
  const [diagResult, setDiagResult] = React.useState(null);
  const [waDiagnosing, setWaDiagnosing] = React.useState(false);
  const [waDiagResult, setWaDiagResult] = React.useState(null);

  const runWhatsAppDiag = async () => {
    setWaDiagnosing(true);
    setWaDiagResult(null);
    try {
      const bizId = localStorage.getItem('nw_active_biz_id') || '1';
      const res = await authFetch(`/api/meta/whatsapp-diag?biz_id=${encodeURIComponent(bizId)}`);
      const data = await res.json();
      setWaDiagResult(data);
    } catch (e) {
      setWaDiagResult({ ok: false, error: e.message || 'Error' });
    }
    setWaDiagnosing(false);
  };

  const runDiagnostic = async () => {
    setDiagnosing(true);
    setDiagResult(null);
    try {
      // 1) Get stored token scopes
      const tokenRes = await authFetch('/api/meta/debug-token');
      const tokenData = await tokenRes.json();

      // 2) Get current business's ad account
      const bizId = localStorage.getItem('nw_active_biz_id') || '1';
      const assetsRes = await authFetch(`/api/meta/assets?biz_id=${encodeURIComponent(bizId)}`);
      const assets = assetsRes.ok ? await assetsRes.json() : null;

      // 3) Check if NexWall app is linked to the ad account
      let linkedApps = [];
      let linkedAppsError = null;
      if (assets?.ad_account_id) {
        try {
          const appsRes = await authFetch(`/api/meta/adaccounts/${encodeURIComponent(assets.ad_account_id)}/apps`);
          linkedApps = appsRes.ok ? await appsRes.json() : [];
        } catch (e) { linkedAppsError = e.message; }
      }

      const requiredScopes = ['ads_management', 'ads_read', 'business_management', 'pages_show_list'];
      const hasScopes = requiredScopes.filter(s => (tokenData.scopes || []).includes(s));
      const missingScopes = requiredScopes.filter(s => !(tokenData.scopes || []).includes(s));
      const nexWallAppId = '964537206532097';
      const appLinked = linkedApps.some(a => String(a.id) === nexWallAppId);

      setDiagResult({
        tokenValid: tokenData.is_valid,
        tokenScopes: tokenData.scopes || [],
        hasScopes, missingScopes,
        adAccountId: assets?.ad_account_id || null,
        adAccountName: assets?.ad_account_name || null,
        pageId: assets?.page_id || null,
        linkedApps, appLinked, linkedAppsError,
      });
    } catch (e) {
      setDiagResult({ error: e.message || 'Error' });
    }
    setDiagnosing(false);
  };

  const handleReset = async () => {
    setResetting(true);
    const bizId = localStorage.getItem('nw_active_biz_id') || '1';
    try {
      await authFetch('/api/meta/reset', {
        method: 'POST',
        body: JSON.stringify({ biz_id: bizId }),
      });
      // Wipe local cache for this business
      localStorage.removeItem(`nw_assets_${bizId}`);
      const steps = JSON.parse(localStorage.getItem(`nw_steps_${bizId}`) || '[]');
      const filtered = Array.isArray(steps) ? steps.filter(i => i !== 0 && i !== 1) : [];
      localStorage.setItem(`nw_steps_${bizId}`, JSON.stringify(filtered));
      // Notify listeners so PrimerosPasos / StrategyFlow refetch and the red banner recalculates
      window.dispatchEvent(new Event('nw-assets-refresh'));
      // Reload so the whole app picks up the new state cleanly
      setTimeout(() => window.location.reload(), 600);
    } catch (e) {
      alert(e.message || 'Error');
      setResetting(false);
    }
  };

  const platformIcons = [
    <svg width="22" height="22" viewBox="0 0 24 24" fill="#1877F2"><path d="M24 12.073c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.99 4.388 10.954 10.125 11.854v-8.385H7.078v-3.47h3.047V9.43c0-3.007 1.792-4.669 4.533-4.669 1.312 0 2.686.235 2.686.235v2.953H15.83c-1.491 0-1.956.925-1.956 1.874v2.25h3.328l-.532 3.47h-2.796v8.385C19.612 23.027 24 18.062 24 12.073z"/></svg>,
    <svg width="22" height="22" viewBox="0 0 24 24"><path d="M12.48 10.92v3.28h7.84c-.24 1.84-.853 3.187-1.787 4.133-1.147 1.147-2.933 2.4-6.053 2.4-4.827 0-8.6-3.893-8.6-8.72s3.773-8.72 8.6-8.72c2.6 0 4.507 1.027 5.907 2.347l2.307-2.307C18.747 1.44 16.133 0 12.48 0 5.867 0 .307 5.387.307 12s5.56 12 12.173 12c3.573 0 6.267-1.173 8.373-3.36 2.16-2.16 2.84-5.213 2.84-7.667 0-.76-.053-1.467-.173-2.053H12.48z" fill="#4285F4"/></svg>,
    <svg width="22" height="22" viewBox="0 0 24 24" fill="white"><path d="M19.59 6.69a4.83 4.83 0 01-3.77-4.25V2h-3.45v13.67a2.89 2.89 0 01-2.88 2.5 2.89 2.89 0 01-2.89-2.89 2.89 2.89 0 012.89-2.89c.28 0 .54.04.79.1V9.01a6.33 6.33 0 00-.79-.05 6.34 6.34 0 00-6.34 6.34 6.34 6.34 0 006.34 6.34 6.34 6.34 0 006.33-6.34V8.69a8.24 8.24 0 004.82 1.55V6.79a4.85 4.85 0 01-1.05-.1z"/></svg>,
    <svg width="22" height="22" viewBox="0 0 24 24" fill="#25D366"><path d="M17.472 14.382c-.297-.149-1.758-.867-2.03-.967-.273-.099-.471-.148-.67.15-.197.297-.767.966-.94 1.164-.173.199-.347.223-.644.075-.297-.15-1.255-.463-2.39-1.475-.883-.788-1.48-1.761-1.653-2.059-.173-.297-.018-.458.13-.606.134-.133.298-.347.446-.52.149-.174.198-.298.298-.497.099-.198.05-.371-.025-.52-.075-.149-.669-1.612-.916-2.207-.242-.579-.487-.5-.669-.51-.173-.008-.371-.01-.57-.01-.198 0-.52.074-.792.372-.272.297-1.04 1.016-1.04 2.479 0 1.462 1.065 2.875 1.213 3.074.149.198 2.096 3.2 5.077 4.487.709.306 1.262.489 1.694.625.712.227 1.36.195 1.871.118.571-.085 1.758-.719 2.006-1.413.248-.694.248-1.289.173-1.413-.074-.124-.272-.198-.57-.347m-5.421 7.403h-.004a9.87 9.87 0 01-5.031-1.378l-.361-.214-3.741.982.998-3.648-.235-.374a9.86 9.86 0 01-1.51-5.26c.001-5.45 4.436-9.884 9.888-9.884 2.64 0 5.122 1.03 6.988 2.898a9.825 9.825 0 012.893 6.994c-.003 5.45-4.437 9.884-9.885 9.884m8.413-18.297A11.815 11.815 0 0012.05 0C5.495 0 .16 5.335.157 11.892c0 2.096.547 4.142 1.588 5.945L.057 24l6.305-1.654a11.882 11.882 0 005.683 1.448h.005c6.554 0 11.89-5.335 11.893-11.893a11.821 11.821 0 00-3.48-8.413z"/></svg>,
  ];
  return (
  <div style={{ height: '100%', overflow: 'auto', padding: '32px' }}>
    <h1 style={{ fontSize: '24px', fontWeight: 400, letterSpacing: '-0.02em', color: NX.text, margin: '0 0 6px' }}>{ti.title}</h1>
    <p style={{ color: NX.muted, fontSize: '14px', margin: '0 0 32px' }}>{ti.subtitle}</p>
    <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit,minmax(240px,1fr))', gap: '16px' }}>
      {ti.platforms.map((int, i) => {
        // i === 0 is Meta Ads (Facebook + Instagram). The other rows (Google, TikTok,
        // WhatsApp Business) are placeholder integrations marked "Próximamente" — they
        // don't have an OAuth flow wired yet, so the button stays informational.
        const isMeta = i === 0;
        const onConnect = isMeta
          ? () => {
              const token = localStorage.getItem('nw_token');
              if (!token) { alert(lang === 'en' ? 'You must be logged in.' : 'Debes iniciar sesión.'); return; }
              window.location.href = `${API}/auth/meta?token=${encodeURIComponent(token)}`;
            }
          : () => alert(lang === 'en' ? 'Coming soon — this integration is not yet available.' : 'Próximamente — esta integración aún no está disponible.');
        return (
          <NxCard key={i} style={{ display: 'flex', alignItems: 'center', gap: '14px' }}>
            <div className="nx-platform-icon" style={{ width: '44px', height: '44px', borderRadius: '10px', background: NX.bg, border: `1px solid ${NX.border}`, display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0 }}>{platformIcons[i]}</div>
            <div style={{ flex: 1 }}>
              <div style={{ fontSize: '14px', fontWeight: 500, color: NX.text, marginBottom: '2px' }}>{int.name}</div>
              <div style={{ fontSize: '12px', color: NX.muted }}>{int.desc}</div>
            </div>
            {(isMeta && connected)
              ? <NxBadge color="success">{ti.connected}</NxBadge>
              : <NxButton variant="ghost" size="sm" onClick={onConnect}>{ti.connectBtn}</NxButton>}
          </NxCard>
        );
      })}
    </div>

    {/* Diagnostic card — check why Meta publish is failing */}
    <div style={{ marginTop: '24px', padding: '18px 20px', background: 'rgba(139,92,246,0.05)', border: '1px solid rgba(139,92,246,0.25)', borderRadius: '12px' }}>
      <div style={{ display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between', gap: '16px', flexWrap: 'wrap' }}>
        <div style={{ flex: 1, minWidth: '240px' }}>
          <div style={{ display: 'flex', alignItems: 'center', gap: '8px', marginBottom: '6px' }}>
            <svg width="16" height="16" viewBox="0 0 24 24" fill="none"><circle cx="11" cy="11" r="8" stroke={NX.accent} strokeWidth="1.8"/><path d="M21 21l-5-5" stroke={NX.accent} strokeWidth="1.8" strokeLinecap="round"/></svg>
            <span style={{ fontSize: '14px', fontWeight: 600, color: NX.text }}>
              {lang === 'en' ? 'Diagnose Meta connection' : 'Diagnosticar conexión con Meta'}
            </span>
          </div>
          <div style={{ fontSize: '12px', color: NX.muted, lineHeight: 1.55 }}>
            {lang === 'en'
              ? 'Checks what scopes your stored token has and whether the NexWall app is linked to your ad account. Helps troubleshoot publish errors.'
              : 'Revisa qué scopes tiene tu token guardado y si la app NexWall está vinculada a tu cuenta publicitaria. Útil para depurar errores al publicar.'}
          </div>
        </div>
        <button onClick={runDiagnostic} disabled={diagnosing}
          style={{ padding: '10px 18px', background: 'transparent', border: `1px solid ${NX.accent}`, borderRadius: '10px', color: NX.accent, fontSize: '13px', cursor: diagnosing ? 'wait' : 'pointer', fontWeight: 600, fontFamily: "'DM Sans',sans-serif", display: 'flex', alignItems: 'center', gap: '8px' }}>
          {diagnosing && <span style={{ display: 'inline-block', width: '12px', height: '12px', border: '1.5px solid currentColor', borderTopColor: 'transparent', borderRadius: '50%', animation: 'spin 0.7s linear infinite' }}/>}
          {diagnosing ? (lang === 'en' ? 'Checking…' : 'Revisando…') : (lang === 'en' ? 'Run diagnostic' : 'Ejecutar diagnóstico')}
        </button>
      </div>

      {diagResult && (
        <div style={{ marginTop: '14px', padding: '12px 14px', background: NX.bg3, border: `1px solid ${NX.border}`, borderRadius: '10px' }}>
          {diagResult.error ? (
            <div style={{ fontSize: '12px', color: NX.danger }}>{diagResult.error}</div>
          ) : (
            <div style={{ display: 'flex', flexDirection: 'column', gap: '10px', fontSize: '12px', color: NX.text }}>
              <div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
                <span style={{ fontSize: '14px' }}>{diagResult.tokenValid ? '✓' : '✗'}</span>
                <span style={{ color: diagResult.tokenValid ? NX.success : NX.danger, fontWeight: 600 }}>
                  {lang === 'en' ? 'Token stored' : 'Token guardado'}: {diagResult.tokenValid ? (lang === 'en' ? 'valid' : 'válido') : (lang === 'en' ? 'invalid / expired' : 'inválido / expirado')}
                </span>
              </div>
              <div>
                <div style={{ fontSize: '10px', color: NX.muted, fontFamily: "'DM Mono',monospace", marginBottom: '4px' }}>{lang === 'en' ? 'SCOPES IN TOKEN' : 'SCOPES DEL TOKEN'}</div>
                <div style={{ display: 'flex', flexWrap: 'wrap', gap: '4px' }}>
                  {diagResult.tokenScopes.map(s => (
                    <span key={s} style={{ fontSize: '10px', padding: '2px 7px', borderRadius: '4px', background: diagResult.hasScopes.includes(s) ? 'rgba(52,211,153,0.15)' : NX.bg4, color: diagResult.hasScopes.includes(s) ? NX.success : NX.muted, fontFamily: "'DM Mono',monospace" }}>{s}</span>
                  ))}
                </div>
                {diagResult.missingScopes.length > 0 && (
                  <div style={{ marginTop: '6px', fontSize: '11px', color: NX.danger }}>
                    {lang === 'en' ? 'Missing required scopes' : 'Faltan scopes obligatorios'}: <b>{diagResult.missingScopes.join(', ')}</b>
                  </div>
                )}
              </div>
              <div>
                <div style={{ fontSize: '10px', color: NX.muted, fontFamily: "'DM Mono',monospace", marginBottom: '4px' }}>{lang === 'en' ? 'AD ACCOUNT' : 'CUENTA PUBLICITARIA'}</div>
                <div style={{ fontSize: '12px', color: NX.text }}>
                  {diagResult.adAccountId
                    ? <>{diagResult.adAccountName || '—'} <span style={{ color: NX.muted, fontFamily: "'DM Mono',monospace", fontSize: '10px' }}>({diagResult.adAccountId})</span></>
                    : <span style={{ color: NX.danger }}>{lang === 'en' ? 'Not configured' : 'No configurada'}</span>}
                </div>
              </div>
              <div>
                <div style={{ fontSize: '10px', color: NX.muted, fontFamily: "'DM Mono',monospace", marginBottom: '4px' }}>{lang === 'en' ? 'NEXWALL APP LINKED TO AD ACCOUNT' : 'APP NEXWALL VINCULADA A LA CUENTA'}</div>
                <div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
                  <span style={{ fontSize: '14px' }}>{diagResult.appLinked ? '✓' : '✗'}</span>
                  <span style={{ color: diagResult.appLinked ? NX.success : NX.danger, fontWeight: 600, fontSize: '12px' }}>
                    {diagResult.appLinked
                      ? (lang === 'en' ? 'Linked — should publish correctly' : 'Vinculada — debería publicar bien')
                      : (lang === 'en' ? 'NOT linked — this is why Meta rejects your campaigns' : 'NO vinculada — por esto Meta rechaza tus campañas')}
                  </span>
                </div>
                {!diagResult.appLinked && diagResult.adAccountId && (
                  <div style={{ marginTop: '8px', padding: '10px 12px', background: 'rgba(248,113,113,0.08)', border: '1px solid rgba(248,113,113,0.2)', borderRadius: '8px', fontSize: '11px', color: NX.text, lineHeight: 1.55 }}>
                    <b style={{ color: NX.danger }}>{lang === 'en' ? 'Fix:' : 'Solución:'}</b> {lang === 'en'
                      ? <>Go to <a href="https://business.facebook.com/settings" target="_blank" rel="noopener noreferrer" style={{ color: NX.accent }}>Business Manager</a> → Accounts → Ad accounts → open your account → <b>Apps</b> tab → click <b>Add</b> and select <b>NexWall Corp AI</b>. After that, come back here and run the diagnostic again.</>
                      : <>Ve a <a href="https://business.facebook.com/settings" target="_blank" rel="noopener noreferrer" style={{ color: NX.accent }}>Business Manager</a> → Cuentas → Cuentas publicitarias → abre tu cuenta → pestaña <b>Apps</b> → click <b>Agregar</b> y elige <b>NexWall Corp AI</b>. Luego vuelve aquí y ejecuta el diagnóstico de nuevo.</>}
                  </div>
                )}
              </div>
            </div>
          )}
        </div>
      )}
    </div>

    {/* WhatsApp Ads readiness diagnostic — checks WABA + phone + page-link */}
    <div style={{ marginTop: '16px', padding: '18px 20px', background: 'rgba(37,211,102,0.06)', border: '1px solid rgba(37,211,102,0.28)', borderRadius: '12px' }}>
      <div style={{ display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between', gap: '16px', flexWrap: 'wrap' }}>
        <div style={{ flex: 1, minWidth: '240px' }}>
          <div style={{ display: 'flex', alignItems: 'center', gap: '8px', marginBottom: '6px' }}>
            <svg width="18" height="18" viewBox="0 0 24 24" fill="#25D366"><path d="M17.472 14.382c-.297-.149-1.758-.867-2.03-.967-.273-.099-.471-.148-.67.15-.197.297-.767.966-.94 1.164-.173.199-.347.223-.644.075-.297-.15-1.255-.463-2.39-1.475-.883-.788-1.48-1.761-1.653-2.059-.173-.297-.018-.458.13-.606.134-.133.298-.347.446-.52.149-.174.198-.298.298-.497.099-.198.05-.371-.025-.52-.075-.149-.669-1.612-.916-2.207-.242-.579-.487-.5-.669-.51-.173-.008-.371-.01-.57-.01-.198 0-.52.074-.792.372-.272.297-1.04 1.016-1.04 2.479 0 1.462 1.065 2.875 1.213 3.074.149.198 2.096 3.2 5.077 4.487.709.306 1.262.489 1.694.625.712.227 1.36.195 1.871.118.571-.085 1.758-.719 2.006-1.413.248-.694.248-1.289.173-1.413-.074-.124-.272-.198-.57-.347m-5.421 7.403h-.004a9.87 9.87 0 01-5.031-1.378l-.361-.214-3.741.982.998-3.648-.235-.374a9.86 9.86 0 01-1.51-5.26c.001-5.45 4.436-9.884 9.888-9.884 2.64 0 5.122 1.03 6.988 2.898a9.825 9.825 0 012.893 6.994c-.003 5.45-4.437 9.884-9.885 9.884m8.413-18.297A11.815 11.815 0 0012.05 0C5.495 0 .16 5.335.157 11.892c0 2.096.547 4.142 1.588 5.945L.057 24l6.305-1.654a11.882 11.882 0 005.683 1.448h.005c6.554 0 11.89-5.335 11.893-11.893a11.821 11.821 0 00-3.48-8.413z"/></svg>
            <span style={{ fontSize: '14px', fontWeight: 600, color: NX.text }}>
              {lang === 'en' ? 'WhatsApp Ads readiness' : 'WhatsApp Ads — diagnóstico'}
            </span>
          </div>
          <div style={{ fontSize: '12px', color: NX.muted, lineHeight: 1.55 }}>
            {lang === 'en'
              ? 'Checks if your WABA, phone number and page link are properly set up so Meta accepts click-to-WhatsApp campaigns. Tells you exactly which step is missing.'
              : 'Revisa si tu WABA, número de teléfono y vinculación con la página están bien configurados para que Meta acepte campañas click-to-WhatsApp. Te dice qué paso falta.'}
          </div>
        </div>
        <button onClick={runWhatsAppDiag} disabled={waDiagnosing}
          style={{ padding: '10px 18px', background: 'transparent', border: '1px solid #25D366', borderRadius: '10px', color: '#25D366', fontSize: '13px', cursor: waDiagnosing ? 'wait' : 'pointer', fontWeight: 600, fontFamily: "'DM Sans',sans-serif", display: 'flex', alignItems: 'center', gap: '8px', flexShrink: 0 }}>
          {waDiagnosing && <span style={{ display: 'inline-block', width: '10px', height: '10px', border: '1.5px solid #25D366', borderTopColor: 'transparent', borderRadius: '50%', animation: 'spin 0.7s linear infinite' }}/>}
          {waDiagnosing ? (lang === 'en' ? 'Checking…' : 'Verificando…') : (lang === 'en' ? 'Run diagnostic' : 'Ejecutar diagnóstico')}
        </button>
      </div>

      {waDiagResult && (
        <div style={{ marginTop: '14px', padding: '14px', background: NX.bg3, border: `1px solid ${NX.border}`, borderRadius: '10px' }}>
          {waDiagResult.error ? (
            <div style={{ fontSize: '12px', color: NX.danger }}>{waDiagResult.error}</div>
          ) : !waDiagResult.ok ? (
            <div style={{ fontSize: '12px', color: NX.warning }}>{waDiagResult.message || waDiagResult.reason}</div>
          ) : (
            <div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
              <div style={{ fontSize: '12px', fontWeight: 600, color: waDiagResult.ready_for_ads ? NX.success : NX.warning }}>
                {waDiagResult.summary}
              </div>
              {/* Scope-limited disclaimer: when our token can't see WABAs, tell the user that
                  the diagnostic is INCOMPLETE rather than implying nothing exists. */}
              {waDiagResult.scope_limited && (
                <div style={{ padding: '10px 12px', background: 'rgba(251,191,36,0.08)', border: `1px solid ${NX.warning}33`, borderRadius: '8px', fontSize: '11px', color: NX.text, lineHeight: 1.55 }}>
                  <div style={{ fontWeight: 600, color: NX.warning, marginBottom: '4px' }}>
                    {lang === 'en' ? 'Diagnostic limited by missing scope' : 'Diagnóstico limitado por scope faltante'}
                  </div>
                  {lang === 'en'
                    ? <>NexWall does not have the <code style={{ fontFamily: "'DM Mono',monospace", background: NX.bg2, padding: '1px 4px', borderRadius: '3px' }}>whatsapp_business_management</code> scope (requires Meta Tech Provider status, in progress). Without it, Meta hides your WABAs from the API even if they exist. Verify your setup directly at <a href="https://business.facebook.com/wa/manage" target="_blank" rel="noopener noreferrer" style={{ color: NX.accent }}>WhatsApp Manager</a>.</>
                    : <>NexWall no tiene el scope <code style={{ fontFamily: "'DM Mono',monospace", background: NX.bg2, padding: '1px 4px', borderRadius: '3px' }}>whatsapp_business_management</code> (requiere status de Tech Provider de Meta — en proceso). Sin ese scope, Meta esconde tus WABAs vía API aunque existan. Verificá tu setup directo en <a href="https://business.facebook.com/wa/manage" target="_blank" rel="noopener noreferrer" style={{ color: NX.accent }}>WhatsApp Manager</a>.</>}
                </div>
              )}
              {(waDiagResult.steps || []).map((step, i) => (
                <div key={step.id} style={{ display: 'flex', alignItems: 'flex-start', gap: '10px', padding: '10px 12px', background: step.done ? 'rgba(52,211,153,0.06)' : step.blocked_by ? 'rgba(255,255,255,0.02)' : 'rgba(251,191,36,0.06)', border: `1px solid ${step.done ? 'rgba(52,211,153,0.25)' : step.blocked_by ? NX.border : 'rgba(251,191,36,0.25)'}`, borderRadius: '8px', opacity: step.blocked_by ? 0.55 : 1 }}>
                  <span style={{ fontSize: '14px', flexShrink: 0, marginTop: '1px' }}>
                    {step.done ? '✅' : step.blocked_by ? '⏸' : '⚠️'}
                  </span>
                  <div style={{ flex: 1, minWidth: 0 }}>
                    <div style={{ fontSize: '13px', fontWeight: 600, color: NX.text, marginBottom: '3px' }}>
                      {i + 1}. {step.label}
                    </div>
                    <div style={{ fontSize: '11px', color: NX.muted, lineHeight: 1.5 }}>
                      {step.details}
                    </div>
                    {!step.done && !step.blocked_by && step.action_url && (
                      <a href={step.action_url} target="_blank" rel="noopener noreferrer"
                        style={{ display: 'inline-flex', alignItems: 'center', gap: '4px', marginTop: '6px', fontSize: '11px', color: NX.accent, textDecoration: 'none', fontWeight: 600 }}>
                        {lang === 'en' ? 'Open in Business Manager' : 'Abrir en Business Manager'} →
                      </a>
                    )}
                  </div>
                </div>
              ))}
              {/* Show raw WABAs if found, for debugging */}
              {Array.isArray(waDiagResult.wabas) && waDiagResult.wabas.length > 0 && (
                <details style={{ fontSize: '11px', color: NX.muted, marginTop: '4px' }}>
                  <summary style={{ cursor: 'pointer', userSelect: 'none' }}>
                    {lang === 'en' ? 'Technical details' : 'Detalles técnicos'}
                  </summary>
                  <pre style={{ marginTop: '6px', padding: '8px', background: NX.bg2, borderRadius: '6px', fontSize: '10px', overflow: 'auto', maxHeight: '200px' }}>
{JSON.stringify({ wabas: waDiagResult.wabas, page_waba: waDiagResult.page_waba, page_legacy_number: waDiagResult.page_legacy_number }, null, 2)}
                  </pre>
                </details>
              )}
            </div>
          )}
        </div>
      )}
    </div>

    {/* Reset connections card — destructive, clears current business Meta connections */}
    <div style={{ marginTop: '32px', padding: '18px 20px', background: 'rgba(248,113,113,0.05)', border: '1px solid rgba(248,113,113,0.25)', borderRadius: '12px' }}>
      <div style={{ display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between', gap: '16px', flexWrap: 'wrap' }}>
        <div style={{ flex: 1, minWidth: '240px' }}>
          <div style={{ display: 'flex', alignItems: 'center', gap: '8px', marginBottom: '6px' }}>
            <svg width="16" height="16" viewBox="0 0 24 24" fill="none"><path d="M21 2v6h-6M3 12a9 9 0 0115-6.7L21 8M3 22v-6h6M21 12a9 9 0 01-15 6.7L3 16" stroke={NX.danger} strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"/></svg>
            <span style={{ fontSize: '14px', fontWeight: 600, color: NX.text }}>
              {lang === 'en' ? 'Reset all Meta connections' : 'Resetear todas las conexiones de Meta'}
            </span>
          </div>
          <div style={{ fontSize: '12px', color: NX.muted, lineHeight: 1.55 }}>
            {lang === 'en'
              ? 'Disconnects your Facebook login AND clears the Business Manager, Ad Account, Page, Instagram, WhatsApp and Pixel selected for the CURRENT business. You can connect again from scratch. Other businesses are unaffected.'
              : 'Desconecta tu login de Facebook Y limpia el Business Manager, cuenta publicitaria, página, Instagram, WhatsApp y Pixel del negocio activo. Podrás reconectar desde cero. Otros negocios no se ven afectados.'}
          </div>
        </div>
        {confirmReset ? (
          <div style={{ display: 'flex', gap: '8px', flexShrink: 0 }}>
            <button onClick={() => setConfirmReset(false)} disabled={resetting}
              style={{ padding: '8px 14px', background: 'transparent', border: `1px solid ${NX.border}`, borderRadius: '8px', color: NX.muted, fontSize: '12px', cursor: 'pointer', fontFamily: "'DM Sans',sans-serif" }}>
              {lang === 'en' ? 'Cancel' : 'Cancelar'}
            </button>
            <button onClick={handleReset} disabled={resetting}
              style={{ padding: '8px 16px', background: NX.danger, border: 'none', borderRadius: '8px', color: '#fff', fontSize: '12px', cursor: 'pointer', fontWeight: 600, fontFamily: "'DM Sans',sans-serif", display: 'flex', alignItems: 'center', gap: '6px' }}>
              {resetting && <span style={{ display: 'inline-block', width: '10px', height: '10px', border: '1.5px solid #fff', borderTopColor: 'transparent', borderRadius: '50%', animation: 'spin 0.7s linear infinite' }}/>}
              {resetting ? (lang === 'en' ? 'Resetting…' : 'Reseteando…') : (lang === 'en' ? 'Yes, reset everything' : 'Sí, resetear todo')}
            </button>
          </div>
        ) : (
          <button onClick={() => setConfirmReset(true)}
            style={{ padding: '10px 18px', background: 'transparent', border: `1px solid ${NX.danger}`, borderRadius: '10px', color: NX.danger, fontSize: '13px', cursor: 'pointer', fontWeight: 600, fontFamily: "'DM Sans',sans-serif", display: 'flex', alignItems: 'center', gap: '8px', flexShrink: 0 }}
            onMouseEnter={e => { e.currentTarget.style.background = 'rgba(248,113,113,0.1)'; }}
            onMouseLeave={e => { e.currentTarget.style.background = 'transparent'; }}>
            <svg width="13" height="13" viewBox="0 0 24 24" fill="none"><path d="M21 2v6h-6M3 12a9 9 0 0115-6.7L21 8" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"/></svg>
            {lang === 'en' ? 'Reset connections' : 'Resetear conexiones'}
          </button>
        )}
      </div>
    </div>
  </div>
  );
};

// ══════════════════════════════════════════
// SCREEN: MARCA
// ══════════════════════════════════════════
const PencilIcon = () => (
  <svg width="13" height="13" viewBox="0 0 16 16" fill="none">
    <path d="M11.5 2.5a1.5 1.5 0 012.121 2.121l-8.5 8.5-2.829.707.707-2.828 8.5-8.5z" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round" strokeLinejoin="round"/>
  </svg>
);

const EditableRow = ({ fieldKey, label, value, wide, last, editingKey, editValue, setEditValue, savedKey, onConfirm, onCancel, onStartEdit }) => {
  const isEditing = editingKey === fieldKey;
  const isArray = Array.isArray(value);
  const isSaved = savedKey === fieldKey;

  return (
    <div style={{ display: 'flex', gap: '12px', padding: '8px 0', borderBottom: last ? 'none' : `1px solid ${NX.border}`, alignItems: isEditing ? 'flex-start' : 'center', minHeight: '38px' }}>
      <span style={{ fontSize: '12px', color: NX.muted, width: wide ? '110px' : '90px', flexShrink: 0, paddingTop: isEditing ? '4px' : 0 }}>{label}</span>
      {isEditing ? (
        <div style={{ flex: 1, display: 'flex', gap: '6px', alignItems: 'flex-start' }}>
          <textarea
            autoFocus
            value={editValue}
            onChange={e => setEditValue(e.target.value)}
            onKeyDown={e => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); onConfirm(fieldKey); } if (e.key === 'Escape') onCancel(); }}
            style={{ flex: 1, background: NX.bg3, border: `1px solid ${NX.accent}`, borderRadius: '7px', color: NX.text, fontSize: '13px', padding: '6px 10px', fontFamily: "'DM Sans',sans-serif", resize: 'none', outline: 'none', lineHeight: 1.5, minHeight: isArray ? '60px' : '36px' }}
          />
          <div style={{ display: 'flex', flexDirection: 'column', gap: '4px', flexShrink: 0 }}>
            <button onClick={() => onConfirm(fieldKey)} style={{ width: '28px', height: '28px', borderRadius: '7px', border: 'none', background: NX.success, color: '#fff', cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
              <svg width="12" height="12" viewBox="0 0 12 12" fill="none"><path d="M2 6l3 3 5-6" stroke="white" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"/></svg>
            </button>
            <button onClick={onCancel} style={{ width: '28px', height: '28px', borderRadius: '7px', border: 'none', background: NX.bg4, color: NX.muted, cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
              <svg width="10" height="10" viewBox="0 0 10 10" fill="none"><path d="M2 2l6 6M8 2l-6 6" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round"/></svg>
            </button>
          </div>
        </div>
      ) : (
        <div style={{ flex: 1, display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: '8px' }}>
          <span style={{ fontSize: '13px', color: NX.text, lineHeight: 1.5, flexWrap: 'wrap', display: 'flex', gap: '4px', alignItems: 'center' }}>
            {isArray
              ? (value || []).map((t, i) => <NxBadge key={i} color="accent">{t}</NxBadge>)
              : (value || <span style={{ color: NX.muted, fontStyle: 'italic' }}>—</span>)}
          </span>
          <button onClick={() => onStartEdit(fieldKey, value)}
            title={isArray ? 'Editar tags (separados por coma)' : 'Editar campo'}
            style={{ flexShrink: 0, padding: '5px 7px', borderRadius: '6px', border: `1px solid ${isSaved ? NX.success : NX.border}`, background: isSaved ? 'rgba(52,211,153,0.08)' : 'transparent', color: isSaved ? NX.success : NX.muted, cursor: 'pointer', display: 'flex', alignItems: 'center', gap: '4px', transition: 'all 0.15s' }}>
            {isSaved
              ? <svg width="12" height="12" viewBox="0 0 12 12" fill="none"><path d="M2 6l3 3 5-6" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"/></svg>
              : <PencilIcon />}
          </button>
        </div>
      )}
    </div>
  );
};

const MarcaScreen = ({ brandDNA, onSave, lang='es' }) => {
  const tm = (I18N && I18N[lang]) ? I18N[lang].marca : I18N.es.marca;
  const [localDNA, setLocalDNA] = React.useState(brandDNA);
  const [editingKey, setEditingKey] = React.useState(null);
  const [editValue, setEditValue] = React.useState('');
  const [saving, setSaving] = React.useState(false);
  const [savedKey, setSavedKey] = React.useState(null);

  React.useEffect(() => { setLocalDNA(brandDNA); }, [brandDNA]);

  const startEdit = (key, value) => {
    setEditingKey(key);
    setEditValue(Array.isArray(value) ? value.join(', ') : (value || ''));
  };

  const cancelEdit = () => { setEditingKey(null); setEditValue(''); };

  const confirmEdit = async (key) => {
    const parsed = key === 'personalidad'
      ? editValue.split(',').map(s => s.trim()).filter(Boolean)
      : editValue.trim();
    const updated = { ...localDNA, [key]: parsed };
    setLocalDNA(updated);
    setEditingKey(null);
    setSaving(true);
    try {
      const bizId = localStorage.getItem('nw_active_biz_id') || '1';
      await fetch(`${API}/api/brand-dna/${bizId}`, {
        method: 'PUT',
        headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${localStorage.getItem('nw_token')}` },
        body: JSON.stringify({ dna: updated }),
      });
      onSave && onSave(updated);
      setSavedKey(key);
      setTimeout(() => setSavedKey(null), 2000);
    } catch (e) { console.warn('Save error:', e); }
    setSaving(false);
  };

  const rowProps = { editingKey, editValue, setEditValue, savedKey, onConfirm: confirmEdit, onCancel: cancelEdit, onStartEdit: startEdit };

  return (
  <div style={{ height: '100%', overflow: 'auto', padding: '32px' }}>
    <div style={{ display: 'flex', alignItems: 'flex-end', justifyContent: 'space-between', marginBottom: '32px' }}>
      <div>
        <h1 style={{ fontSize: '24px', fontWeight: 400, letterSpacing: '-0.02em', color: NX.text, margin: '0 0 6px' }}>{tm.title}</h1>
        <p style={{ color: NX.muted, fontSize: '14px', margin: 0 }}>{tm.subtitle}</p>
      </div>
      {saving && <span style={{ fontSize: '12px', color: NX.muted }}>Guardando...</span>}
    </div>

    {!localDNA ? (
      <div style={{ textAlign: 'center', padding: '80px 40px' }}>
        <NxOrbIcon size={48}/>
        <h3 style={{ fontSize: '16px', fontWeight: 400, color: NX.text, margin: '20px 0 8px' }}>{tm.notConfigured.title}</h3>
        <p style={{ color: NX.muted, fontSize: '14px' }}>{tm.notConfigured.subtitle}</p>
      </div>
    ) : typeof localDNA === 'object' ? (
      <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '16px' }}>
        <NxCard>
          <div style={{ fontSize: '11px', fontFamily: "'DM Mono',monospace", color: NX.accent, marginBottom: '14px', letterSpacing: '0.1em' }}>VOZ & PERSONALIDAD</div>
          <EditableRow fieldKey="voz"          label="Voz"          value={localDNA.voz}          {...rowProps} />
          <EditableRow fieldKey="tono"         label="Tono"         value={localDNA.tono}         {...rowProps} />
          <EditableRow fieldKey="personalidad" label="Personalidad" value={localDNA.personalidad} {...rowProps} last />
        </NxCard>
        <NxCard>
          <div style={{ fontSize: '11px', fontFamily: "'DM Mono',monospace", color: NX.accent, marginBottom: '14px', letterSpacing: '0.1em' }}>AUDIENCIA & MERCADO</div>
          <EditableRow fieldKey="audiencia" label="Audiencia" value={localDNA.audiencia} {...rowProps} />
          <EditableRow fieldKey="industria" label="Industria" value={localDNA.industria} {...rowProps} last />
        </NxCard>
        <NxCard style={{ gridColumn: '1 / -1' }}>
          <div style={{ fontSize: '11px', fontFamily: "'DM Mono',monospace", color: NX.accent, marginBottom: '14px', letterSpacing: '0.1em' }}>PROPUESTA DE VALOR</div>
          <EditableRow fieldKey="propuesta"     label="Propuesta"     value={localDNA.propuesta}     wide {...rowProps} />
          <EditableRow fieldKey="productos"     label="Productos"     value={localDNA.productos}     wide {...rowProps} />
          <EditableRow fieldKey="diferenciador" label="Diferenciador" value={localDNA.diferenciador} wide {...rowProps} last />
        </NxCard>
      </div>
    ) : (
      <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '16px' }}>
        {tm.sections.map((sec, i) => (
          <NxCard key={i}>
            <div style={{ fontSize: '11px', fontFamily: "'DM Mono',monospace", color: NX.accent, marginBottom: '16px', letterSpacing: '0.1em' }}>{sec.title.toUpperCase()}</div>
            {sec.items.map((item, j) => (
              <div key={j} style={{ display: 'flex', gap: '12px', padding: '8px 0', borderBottom: j < sec.items.length-1 ? `1px solid ${NX.border}` : 'none' }}>
                <span style={{ fontSize: '12px', color: NX.muted, width: '80px', flexShrink: 0 }}>{item.k}</span>
                <span style={{ fontSize: '13px', color: NX.text }}>{item.v}</span>
              </div>
            ))}
          </NxCard>
        ))}
      </div>
    )}
  </div>
  );
};

// ══════════════════════════════════════════
// SCREEN: PRECIOS
// ══════════════════════════════════════════
const PreciosScreen = ({ lang='es', currentUser=null }) => {
  const tp = (I18N && I18N[lang]) ? I18N[lang].precios : I18N.es.precios;
  const [billing, setBilling] = React.useState('annual'); // 'monthly' | 'annual'
  // Plan activo del user — preferimos lo que viene del backend vía /api/credits/balance
  // (incluye plan_billing_cycle, más fresco que currentUser que se carga 1 vez al login).
  // currentUser.plan es el fallback inmediato sin esperar al fetch.
  const [activePlan, setActivePlan] = React.useState(currentUser?.plan || null);
  const [activeCycle, setActiveCycle] = React.useState(null);
  // Fecha del próximo cobro (ISO string). Lo trae /api/credits/balance como `period_end`,
  // que se renueva en cada invoice.payment_succeeded → resetPlanCredits.
  const [periodEnd, setPeriodEnd] = React.useState(null);
  // 'business' | 'business_max' | null — id del plan que está esperando redirección a Stripe.
  // Solo bloqueamos el botón clickeado, no toda la pantalla, para evitar que un click accidental
  // tape la otra opción.
  const [redirectingPlan, setRedirectingPlan] = React.useState(null);
  // Loading state separado para la apertura del Customer Portal (gestionar/cambiar plan).
  const [openingPortal, setOpeningPortal] = React.useState(false);
  // Maintenance mode: backend reporta si está bloqueando registros/pagos nuevos. Cuando
  // está activo, ocultamos los CTAs de compra y mostramos un mensaje claro. Los users
  // con plan ya activo igual ven "✓ Tu plan actual" + el portal (esos endpoints NO se bloquean).
  const [maintenance, setMaintenance] = React.useState(false);

  React.useEffect(() => {
    let cancelled = false;
    // Refresca plan + cycle desde backend. `authFetch` está definido globalmente en
    // NexWall App.html y maneja API base + Authorization header. La respuesta incluye
    // plan_billing_cycle gracias al cambio en /api/credits/balance.
    if (typeof authFetch === 'function') {
      authFetch('/api/credits/balance')
        .then(r => r.ok ? r.json() : null)
        .then(b => {
          if (cancelled || !b) return;
          if (b.plan) setActivePlan(b.plan);
          if (b.plan_billing_cycle) setActiveCycle(b.plan_billing_cycle);
          if (b.period_end) setPeriodEnd(b.period_end);
        })
        .catch(() => {});
    }
    // Status del sistema (público, no requiere auth). Si maintenance=true ocultamos los
    // CTAs de compra. Si falla la fetch, mantenemos comportamiento normal (no asumimos
    // mantenimiento por error de red — preferimos servir UI normal).
    authFetch('/api/system/status')
      .then(r => r.ok ? r.json() : null)
      .then(s => { if (!cancelled && s && s.maintenance) setMaintenance(true); })
      .catch(() => {});
    return () => { cancelled = true; };
  }, []);

  // Click handler para los botones de plan. Llama al backend, recibe la URL hosted de Stripe
  // Checkout y redirige. Errores se muestran como alert (consistente con otros flows que aún
  // no migraron a un Toast component dedicado).
  const handleSelectPlan = async (planId) => {
    if (redirectingPlan) return;
    if (!localStorage.getItem('nw_token')) {
      alert(lang === 'en' ? 'Please log in first.' : 'Iniciá sesión primero.');
      return;
    }
    setRedirectingPlan(planId);
    try {
      const res = await authFetch('/api/billing/create-checkout-session', {
        method: 'POST',
        body: JSON.stringify({ plan: planId, cycle: billing }),
      });
      const data = await res.json().catch(() => ({}));
      if (!res.ok) {
        // 409 = already_subscribed; 503 = stripe no configurado; otros = error genérico
        alert(data.error || tp.checkoutError);
        setRedirectingPlan(null);
        return;
      }
      if (data.url) {
        window.location.href = data.url;
      } else {
        alert(tp.checkoutError);
        setRedirectingPlan(null);
      }
    } catch (e) {
      console.error('[checkout]', e);
      alert(tp.checkoutError);
      setRedirectingPlan(null);
    }
  };

  // Abre el Customer Portal de Stripe. Usado tanto para "Gestionar suscripción" (botón del
  // plan actual) como para "Cambiar a este plan" (botón del otro plan cuando ya hay sub).
  // Una vez en el portal, el user cambia/cancela y Stripe dispara webhook que actualiza la DB.
  const handleOpenPortal = async () => {
    if (openingPortal) return;
    if (!localStorage.getItem('nw_token')) {
      alert(lang === 'en' ? 'Please log in first.' : 'Iniciá sesión primero.');
      return;
    }
    setOpeningPortal(true);
    try {
      const res = await authFetch('/api/billing/create-portal-session', { method: 'POST' });
      const data = await res.json().catch(() => ({}));
      if (!res.ok) {
        alert(data.error || tp.portalError);
        setOpeningPortal(false);
        return;
      }
      if (data.url) {
        window.location.href = data.url;
      } else {
        alert(tp.portalError);
        setOpeningPortal(false);
      }
    } catch (e) {
      console.error('[portal]', e);
      alert(tp.portalError);
      setOpeningPortal(false);
    }
  };

  const MAX_COLOR = '#f59e0b'; // vivid gold — "MAX" highlight
  const planMeta = [
    { id:'business',     label:'BUSINESS',     maxSuffix: null,  labelColor: NX.accent2, highlight: false, soon: false, monthly: 59,  annual: 49 },
    { id:'business_max', label:'BUSINESS',     maxSuffix: 'MAX', labelColor: NX.accent2, highlight: true,  soon: false, monthly: 119, annual: 99 },
  ];
  const plans = planMeta.map((m, i) => ({ ...m, desc: tp.plans[i].desc, features: tp.plans[i].features }));

  const Check = () => (
    <svg width="14" height="14" viewBox="0 0 14 14" fill="none" style={{ flexShrink: 0 }}>
      <path d="M2.5 7l3 3L11.5 4" stroke={NX.success} strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"/>
    </svg>
  );

  return (
    <div style={{ height: '100%', overflow: 'auto', padding: '32px' }}>
      {/* Header */}
      <div style={{ textAlign: 'center', marginBottom: '32px' }}>
        <h1 style={{ fontSize: '26px', fontWeight: 300, letterSpacing: '-0.03em', color: NX.text, margin: '0 0 8px' }}>
          {tp.title}
        </h1>

        {/* Billing toggle */}
        <div style={{ display: 'inline-flex', alignItems: 'center', gap: '0', background: NX.bg3, border: `1px solid ${NX.border}`, borderRadius: '100px', padding: '4px', marginTop: '16px' }}>
          {['monthly', 'annual'].map(b => (
            <button key={b} onClick={() => setBilling(b)} style={{
              padding: '7px 18px', borderRadius: '100px', border: 'none', cursor: 'pointer',
              background: billing === b ? NX.bg4 : 'transparent',
              color: billing === b ? NX.text : NX.muted,
              fontSize: '13px', fontWeight: billing === b ? 500 : 400,
              fontFamily: "'DM Sans', sans-serif",
              display: 'flex', alignItems: 'center', gap: '8px',
              transition: 'all 0.2s',
            }}>
              {b === 'monthly' ? tp.monthly : tp.annual}
              {b === 'annual' && (
                <span style={{ background: 'rgba(52,211,153,0.15)', color: NX.success, fontSize: '10px', fontWeight: 600, padding: '2px 6px', borderRadius: '100px' }}>
                  {tp.save}
                </span>
              )}
            </button>
          ))}
        </div>
      </div>

      {/* Maintenance banner — visible cuando MAINTENANCE_MODE=true en backend. Aparece
          arriba de todos los plan cards y top-up packs para que sea imposible que un user
          intente comprar sin entender que está bloqueado. Users con plan activo siguen
          viendo "Tu plan actual" + portal funciona normal. */}
      {maintenance && (
        <div style={{
          maxWidth:'720px', margin:'0 auto 24px', padding:'14px 20px',
          background:'rgba(251,191,36,0.08)', border:`1px solid rgba(251,191,36,0.28)`,
          borderRadius:'12px', display:'flex', alignItems:'flex-start', gap:'12px',
        }}>
          <svg width="20" height="20" viewBox="0 0 20 20" fill="none" style={{ flexShrink: 0, marginTop: 1 }}>
            <path d="M10 6.25v4.5M10 13.5h0" stroke="#fbbf24" strokeWidth="1.7" strokeLinecap="round"/>
            <circle cx="10" cy="10" r="8" stroke="#fbbf24" strokeWidth="1.4"/>
          </svg>
          <div style={{ flex: 1, fontSize:'13px', color:'#f0eeec', lineHeight: 1.55 }}>
            {tp.maintenance}
          </div>
        </div>
      )}

      {/* Plan cards */}
      <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(260px, 1fr))', gap: '16px', maxWidth: '960px', margin: '0 auto' }}>
        {plans.map(plan => (
          <div key={plan.id} style={{
            position: 'relative',
            background: NX.bg3,
            border: `1px solid ${plan.highlight ? NX.accent2 : NX.border}`,
            borderRadius: '16px', padding: '24px',
            display: 'flex', flexDirection: 'column',
            boxShadow: plan.highlight ? '0 0 40px rgba(139,92,246,0.12)' : 'none',
          }}>
            {plan.highlight && (
              <div style={{ position: 'absolute', top: '-13px', left: '50%', transform: 'translateX(-50%)', background: 'linear-gradient(90deg,#f59e0b,#ec4899)', borderRadius: '100px', padding: '3px 12px', fontSize: '11px', fontWeight: 700, color: '#fff', whiteSpace: 'nowrap' }}>
                {tp.popular}
              </div>
            )}

            {/* Plan label */}
            <div style={{ fontSize: '11px', fontWeight: 700, color: plan.labelColor, letterSpacing: '0.1em', marginBottom: '12px' }}>
              {plan.label}
              {plan.maxSuffix && (
                <span style={{
                  marginLeft: '6px',
                  color: MAX_COLOR,
                  background: 'rgba(245,158,11,0.12)',
                  padding: '2px 7px',
                  borderRadius: '4px',
                  letterSpacing: '0.12em',
                  textShadow: '0 0 12px rgba(245,158,11,0.5)',
                }}>{plan.maxSuffix}</span>
              )}
            </div>

            {/* Price */}
            {plan.soon ? (
              <div style={{ fontSize: '24px', fontWeight: 300, color: NX.muted, marginBottom: '8px' }}>{tp.soon}</div>
            ) : (
              <div style={{ marginBottom: '8px' }}>
                {billing === 'annual' && (
                  <div style={{ fontSize: '13px', color: NX.muted, textDecoration: 'line-through', fontFamily: "'DM Mono',monospace" }}>${plan.monthly}{tp.perMonth}</div>
                )}
                <div style={{ display: 'flex', alignItems: 'flex-end', gap: '4px' }}>
                  <span style={{ fontSize: '36px', fontWeight: 600, color: plan.highlight ? NX.accent2 : NX.text, fontFamily: "'DM Mono',monospace", lineHeight: 1 }}>
                    ${billing === 'annual' ? plan.annual : plan.monthly}
                  </span>
                  <span style={{ fontSize: '13px', color: NX.muted, marginBottom: '4px' }}>{tp.perMonth}</span>
                </div>
              </div>
            )}

            <p style={{ fontSize: '13px', color: NX.muted, margin: '0 0 20px', lineHeight: 1.5 }}>{plan.desc}</p>

            {(() => {
              // Estados del CTA:
              //   0) maintenance && !isThisActive → "Próximamente" disabled (sitio cerrado a nuevos pagos)
              //   1) plan.soon          → "Próximamente" disabled
              //   2) activePlan === id  → "Tu plan actual" + sub-link "Gestionar" → portal
              //   3) activePlan != id   → "Cambiar a este plan" → portal (Stripe handles switch + prorating)
              //   4) sin plan activo    → "Seleccionar Plan" clickeable (Stripe Checkout)
              const isThisActive  = activePlan === plan.id;
              const hasOtherActive = !!activePlan && !isThisActive && ['business','business_max'].includes(activePlan);
              const isLoading = redirectingPlan === plan.id;
              if (maintenance && !isThisActive) {
                return <NxButton variant="ghost" full disabled style={{ marginBottom:'20px' }}>{tp.comingSoon}</NxButton>;
              }
              if (plan.soon) {
                return <NxButton variant="ghost" full disabled style={{ marginBottom:'20px' }}>{tp.soon}</NxButton>;
              }
              if (isThisActive) {
                // Format period_end como "18 de junio de 2026" / "June 18, 2026". Cae a
                // null si no hay fecha o si Intl.DateTimeFormat falla (browsers viejos).
                let renewDate = null;
                if (periodEnd) {
                  try {
                    const d = new Date(periodEnd);
                    if (!isNaN(d.getTime())) {
                      renewDate = new Intl.DateTimeFormat(lang === 'en' ? 'en-US' : 'es-ES',
                        { year:'numeric', month:'long', day:'numeric' }).format(d);
                    }
                  } catch {}
                }
                return (
                  <div style={{ marginBottom:'20px' }}>
                    <NxButton variant="success" full disabled>✓ {tp.currentPlan}</NxButton>
                    {renewDate && (
                      <div style={{
                        marginTop:'10px', fontSize:'11px', color: NX.muted, textAlign:'center',
                        fontFamily: "'DM Sans',sans-serif",
                      }}>
                        {tp.nextBilling}: <span style={{ color: NX.text, fontWeight: 500 }}>{renewDate}</span>
                      </div>
                    )}
                    <button
                      onClick={handleOpenPortal}
                      disabled={openingPortal}
                      style={{
                        marginTop: renewDate ? '6px' : '8px', width:'100%', background:'transparent', border:'none',
                        color: NX.muted, fontSize:'12px', cursor: openingPortal ? 'wait' : 'pointer',
                        padding:'6px', textDecoration:'underline', fontFamily: "'DM Sans',sans-serif",
                      }}
                    >
                      {openingPortal ? tp.openingPortal : tp.managePlan + ' ↗'}
                    </button>
                  </div>
                );
              }
              if (hasOtherActive) {
                return (
                  <NxButton
                    variant={plan.highlight ? 'accent' : 'ghost'}
                    full
                    disabled={openingPortal}
                    onClick={handleOpenPortal}
                    style={{ marginBottom:'20px' }}
                  >
                    {openingPortal ? tp.openingPortal : tp.switchPlan + ' ↗'}
                  </NxButton>
                );
              }
              return (
                <NxButton
                  variant={plan.highlight ? 'accent' : 'ghost'}
                  full
                  disabled={isLoading}
                  onClick={() => handleSelectPlan(plan.id)}
                  style={{ marginBottom:'20px' }}
                >
                  {isLoading ? tp.processing : tp.selectPlan}
                </NxButton>
              );
            })()}

            {plan.features.length > 0 && <>
              <div style={{ fontSize: '10px', fontWeight: 700, color: NX.muted, letterSpacing: '0.1em', marginBottom: '12px' }}>{tp.includes}</div>
              <div style={{ display: 'flex', flexDirection: 'column', gap: '10px', flex: 1 }}>
                {plan.features.map((f, i) => (
                  <div key={i} style={{ display: 'flex', alignItems: 'flex-start', gap: '8px', fontSize: '13px', color: NX.text, lineHeight: 1.4 }}>
                    <Check/>
                    {f}
                  </div>
                ))}
              </div>
            </>}

            {plan.soon && (
              <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', flex: 1, opacity: 0.3 }}>
                <svg width="48" height="48" viewBox="0 0 48 48" fill="none"><rect x="10" y="22" width="28" height="20" rx="4" stroke={NX.muted} strokeWidth="2"/><path d="M16 22v-6a8 8 0 0116 0v6" stroke={NX.muted} strokeWidth="2" strokeLinecap="round"/></svg>
              </div>
            )}
          </div>
        ))}
      </div>

      {/* Annual savings note */}
      {billing === 'annual' && (
        <p style={{ textAlign: 'center', color: NX.muted, fontSize: '12px', marginTop: '20px' }}>
          {tp.annualSavings}
        </p>
      )}

      {/* All sales final — aviso prominente arriba del fold de TopUpPacks. Stripe + buenas
          prácticas SaaS: el user TIENE que ver esto antes de comprar para evitar disputas
          y chargebacks. Link a /refund para política completa. */}
      <div style={{
        maxWidth:'720px', margin:'24px auto 0', padding:'12px 18px',
        background:'rgba(248,113,113,0.06)', border:`1px solid rgba(248,113,113,0.22)`,
        borderRadius:'10px', display:'flex', alignItems:'center', gap:'12px',
      }}>
        <svg width="18" height="18" viewBox="0 0 18 18" fill="none" style={{ flexShrink: 0 }}>
          <circle cx="9" cy="9" r="7.5" stroke="#f87171" strokeWidth="1.4"/>
          <path d="M9 5.5v4M9 12h0" stroke="#f87171" strokeWidth="1.6" strokeLinecap="round"/>
        </svg>
        <div style={{ flex: 1, fontSize:'12px', color:'#f0eeec', lineHeight: 1.5 }}>
          {tp.finalSale}{' '}
          <a href="/refund" target="_blank" rel="noopener noreferrer"
             style={{ color:'#f87171', textDecoration:'underline', fontWeight: 500 }}>
            {tp.finalSaleLink} →
          </a>
        </div>
      </div>

      {/* Costo por acción — qué consume cada operación de IA. Coincide 1:1 con CREDIT_COSTS
          en backend/server.js. Aquí no leemos el catálogo del backend para evitar un fetch
          extra; los strings vienen de i18n y los números de window.CREDIT_COSTS. */}
      <CreditCostsTable lang={lang} />

      {/* Top-up packs — compra de créditos extra vía Stripe one-time. Requiere plan activo
          (los créditos solo son usables con suscripción — los packs son SUPLEMENTARIOS).
          maintenance prop = ocultamos cards y mostramos "Próximamente" cuando MAINTENANCE_MODE=true. */}
      <TopUpPacks lang={lang} hasActivePlan={!!activePlan && ['business','business_max'].includes(activePlan)} maintenance={maintenance} />
    </div>
  );
};

// Compra de packs de créditos one-time vía Stripe Checkout (Phase 2). Cada card es clickeable
// y dispara /api/billing/create-pack-checkout → redirect a Stripe. Requiere plan activo en
// backend (sino devuelve 402 PLAN_REQUIRED) porque requireCredits exige plan para gastar.
const TopUpPacks = ({ lang='es', hasActivePlan=false, maintenance=false }) => {
  // pack_id en compra (loading state per-card) | null
  const [buyingPack, setBuyingPack] = React.useState(null);
  const packs = [
    { id: 'pack_200',  credits: 200,  price: 19,  highlight: false },
    { id: 'pack_1000', credits: 1000, price: 79,  highlight: true  },
    { id: 'pack_5000', credits: 5000, price: 349, highlight: false },
  ];
  const t = lang === 'en' ? {
    title: 'Need more credits?',
    subtitle: 'Buy extra credit packs anytime — they never expire and stack on top of your plan balance.',
    save: 'BEST VALUE',
    perCredit: 'per credit',
    buy: 'Buy now',
    loading: 'Redirecting…',
    requiresPlan: 'Subscribe to a plan first to buy credit packs.',
    error: 'Could not start payment. Try again or contact support.',
  } : {
    title: '¿Necesitas más créditos?',
    subtitle: 'Compra packs adicionales cuando quieras — no expiran y se suman al saldo de tu plan.',
    save: 'MEJOR VALOR',
    perCredit: 'por crédito',
    buy: 'Comprar',
    loading: 'Redirigiendo…',
    requiresPlan: 'Suscribite a un plan primero para comprar packs de créditos.',
    error: 'No se pudo iniciar el pago. Intentá de nuevo o contactá soporte.',
  };

  const handleBuyPack = async (packId) => {
    if (buyingPack) return;
    if (maintenance) { /* card disabled, no debería poder clickear, pero por seguridad */ return; }
    if (!hasActivePlan) { alert(t.requiresPlan); return; }
    if (!localStorage.getItem('nw_token')) {
      alert(lang === 'en' ? 'Please log in first.' : 'Iniciá sesión primero.');
      return;
    }
    setBuyingPack(packId);
    try {
      const res = await authFetch('/api/billing/create-pack-checkout', {
        method: 'POST',
        body: JSON.stringify({ pack: packId }),
      });
      const data = await res.json().catch(() => ({}));
      if (!res.ok) {
        alert(data.error || t.error);
        setBuyingPack(null);
        return;
      }
      if (data.url) {
        window.location.href = data.url;
      } else {
        alert(t.error);
        setBuyingPack(null);
      }
    } catch (e) {
      console.error('[pack checkout]', e);
      alert(t.error);
      setBuyingPack(null);
    }
  };

  return (
    <div style={{ maxWidth: '720px', margin: '24px auto 0', padding: '24px', background: 'rgba(26,17,40,0.55)', border: `1px solid ${NX.border}`, borderRadius: '16px' }}>
      <div style={{ display: 'flex', alignItems: 'baseline', justifyContent: 'space-between', flexWrap: 'wrap', gap: '8px', marginBottom: '14px' }}>
        <div>
          <h3 style={{ fontSize: '15px', fontWeight: 500, color: NX.text, margin: '0 0 4px' }}>{t.title}</h3>
          <p style={{ fontSize: '12px', color: NX.muted, margin: 0 }}>{t.subtitle}</p>
        </div>
      </div>
      {!hasActivePlan && (
        <div style={{ marginBottom:'12px', padding:'10px 12px', background:'rgba(251,191,36,0.08)', border:`1px solid rgba(251,191,36,0.25)`, borderRadius:'8px', fontSize:'12px', color:'#fbbf24', textAlign:'center' }}>
          ⚠ {t.requiresPlan}
        </div>
      )}
      <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(180px, 1fr))', gap: '10px' }}>
        {packs.map(p => {
          const isLoading = buyingPack === p.id;
          const disabled = maintenance || !hasActivePlan || (buyingPack && buyingPack !== p.id);
          return (
            <button
              key={p.id}
              type="button"
              onClick={() => handleBuyPack(p.id)}
              disabled={disabled}
              style={{
                position: 'relative',
                padding: '16px 14px',
                background: p.highlight ? `linear-gradient(135deg, ${NX.accent}1a 0%, rgba(26,17,40,0.85) 100%)` : 'rgba(26,17,40,0.5)',
                border: `1px solid ${p.highlight ? NX.accent2 : NX.border}`,
                borderRadius: '12px',
                display: 'flex', flexDirection: 'column', gap: '8px',
                cursor: disabled ? 'not-allowed' : 'pointer',
                opacity: disabled && !isLoading ? 0.55 : 1,
                textAlign: 'left',
                fontFamily: "'DM Sans',sans-serif",
                transition: 'transform 0.15s, box-shadow 0.15s',
              }}
              onMouseEnter={e => { if (!disabled) { e.currentTarget.style.transform = 'translateY(-2px)'; e.currentTarget.style.boxShadow = `0 8px 24px rgba(139,92,246,0.18)`; } }}
              onMouseLeave={e => { e.currentTarget.style.transform = ''; e.currentTarget.style.boxShadow = ''; }}
            >
              {p.highlight && (
                <span style={{ position: 'absolute', top: '-8px', right: '10px', fontSize: '8px', fontWeight: 800, letterSpacing: '0.1em', padding: '2px 8px', borderRadius: '100px', background: NX.accentGrad, color: '#fff' }}>★ {t.save}</span>
              )}
              <div style={{ fontSize: '22px', fontWeight: 700, color: NX.accent2, fontFamily: "'DM Mono',monospace", lineHeight: 1 }}>
                {p.credits.toLocaleString()} <span style={{ fontSize: '11px', color: NX.muted, fontWeight: 500 }}>cr</span>
              </div>
              <div style={{ fontSize: '18px', fontWeight: 600, color: NX.text, fontFamily: "'DM Mono',monospace" }}>
                ${p.price}
                <span style={{ fontSize: '10px', color: NX.muted, fontWeight: 400, marginLeft: '4px' }}>USD</span>
              </div>
              <div style={{ fontSize: '10px', color: NX.muted }}>
                ${(p.price / p.credits).toFixed(3)} {t.perCredit}
              </div>
              <div style={{
                marginTop:'6px', padding:'7px 10px', borderRadius:'8px', textAlign:'center',
                background: maintenance ? 'rgba(255,255,255,0.04)' : (p.highlight ? 'rgba(139,92,246,0.20)' : 'rgba(255,255,255,0.04)'),
                border: `1px solid ${maintenance ? NX.border : (p.highlight ? NX.accent2 : NX.border)}`,
                fontSize:'12px', fontWeight:500, color: maintenance ? NX.muted : (p.highlight ? NX.accent2 : NX.text),
              }}>
                {maintenance ? (lang === 'en' ? 'Coming soon' : 'Próximamente') : (isLoading ? t.loading : t.buy + ' →')}
              </div>
            </button>
          );
        })}
      </div>
    </div>
  );
};

const CreditCostsTable = ({ lang='es' }) => {
  const c = window.CREDIT_COSTS || { strategy_gen:30, image_premium:25, image_ultra_max:100, copies_batch:10, text_regen:2, brand_dna:50, campaign_relaunch:5 };
  const t = lang === 'en' ? {
    title: 'Credit cost per action',
    subtitle: 'What each AI action consumes. Failed generations are auto-refunded.',
    actions: [
      { label: 'Generate strategy',          cost: c.strategy_gen },
      { label: 'Premium image (Gemini)',     cost: c.image_premium },
      { label: 'Ultra Premium MAX image',    cost: c.image_ultra_max },
      { label: 'Generate 5 copies',          cost: c.copies_batch },
      { label: 'Regenerate 1 text',          cost: c.text_regen },
      { label: 'Brand identity generation',  cost: c.brand_dna },
      { label: 'Re-launch existing campaign', cost: c.campaign_relaunch },
      { label: 'Launch new campaign',        cost: 0 },
    ],
    free: 'FREE',
    credits: 'credits',
  } : {
    title: 'Costo por acción',
    subtitle: 'Lo que consume cada operación con IA. Si la generación falla, se reembolsa automáticamente.',
    actions: [
      { label: 'Generar estrategia',         cost: c.strategy_gen },
      { label: 'Imagen Premium (Gemini)',    cost: c.image_premium },
      { label: 'Imagen Ultra Premium MAX',   cost: c.image_ultra_max },
      { label: 'Generar 5 copies',           cost: c.copies_batch },
      { label: 'Regenerar 1 texto',          cost: c.text_regen },
      { label: 'Generación de identidad de tu marca', cost: c.brand_dna },
      { label: 'Re-lanzar campaña existente', cost: c.campaign_relaunch },
      { label: 'Lanzar campaña nueva',       cost: 0 },
    ],
    free: 'GRATIS',
    credits: 'créditos',
  };
  return (
    <div style={{ maxWidth: '720px', margin: '40px auto 0', padding: '24px', background: 'rgba(26,17,40,0.55)', border: `1px solid ${NX.border}`, borderRadius: '16px' }}>
      <h3 style={{ fontSize: '15px', fontWeight: 500, color: NX.text, margin: '0 0 4px' }}>{t.title}</h3>
      <p style={{ fontSize: '12px', color: NX.muted, margin: '0 0 18px' }}>{t.subtitle}</p>
      <div style={{ display: 'flex', flexDirection: 'column', gap: '6px' }}>
        {t.actions.map((a, i) => (
          <div key={i} style={{ display: 'flex', alignItems: 'baseline', justifyContent: 'space-between', padding: '10px 14px', background: i % 2 ? 'transparent' : 'rgba(139,92,246,0.04)', borderRadius: '8px' }}>
            <span style={{ fontSize: '13px', color: NX.text }}>{a.label}</span>
            <span style={{ fontSize: '13px', fontWeight: 600, fontFamily: "'DM Mono', monospace", color: a.cost === 0 ? NX.success : NX.accent2 }}>
              {a.cost === 0 ? t.free : `${a.cost} ${t.credits}`}
            </span>
          </div>
        ))}
      </div>
    </div>
  );
};

// ══════════════════════════════════════════
// SCREEN: HISTORIAL DE CRÉDITOS
// Lista cronológica de todas las transacciones del usuario: consumos por acción
// (imagen Premium, imagen Ultra MAX, estrategia, copies, regen, brand DNA, relaunch),
// resets de plan, otorgamientos del admin y reembolsos. Cada cuenta solo ve la suya.
// ══════════════════════════════════════════
const HistorialScreen = ({ lang='es' }) => {
  const [transactions, setTransactions] = React.useState(null);
  const [filter, setFilter] = React.useState('all');
  const [loading, setLoading] = React.useState(true);

  const load = React.useCallback(async () => {
    setLoading(true);
    try {
      const res = await authFetch('/api/credits/transactions?limit=200');
      if (!res.ok) { setTransactions([]); return; }
      const j = await res.json();
      setTransactions(j.transactions || []);
    } catch { setTransactions([]); }
    setLoading(false);
  }, []);
  React.useEffect(() => { load(); }, [load]);

  // Mapeo acción → label legible + ícono + color
  const ACTION_META = lang === 'en' ? {
    image_premium:     { label: 'Premium image (Gemini)',         color: '#22d3ee', icon: '⚡' },
    image_ultra_max:   { label: 'Premium Ultra MAX image',         color: '#f472b6', icon: '✦' },
    strategy_gen:      { label: 'Strategy generation',             color: NX.accent, icon: '⊹' },
    copies_batch:      { label: 'Texts (5 copies)',                color: '#a78bfa', icon: '✎' },
    text_regen:        { label: 'Single text regeneration',        color: '#a78bfa', icon: '↻' },
    brand_dna:         { label: 'Brand identity generation',       color: '#f59e0b', icon: '⬡' },
    campaign_relaunch: { label: 'Campaign re-launch',              color: '#fbbf24', icon: '↺' },
    plan_reset:        { label: 'Plan period renewed',             color: NX.success, icon: '+' },
    admin_grant:       { label: 'Manual grant',                    color: NX.success, icon: '+' },
    refund:            { label: 'Auto refund (gen failed)',        color: NX.success, icon: '↶' },
  } : {
    image_premium:     { label: 'Imagen Premium (Gemini)',         color: '#22d3ee', icon: '⚡' },
    image_ultra_max:   { label: 'Imagen Premium Ultra MAX',        color: '#f472b6', icon: '✦' },
    strategy_gen:      { label: 'Generación de estrategia',        color: NX.accent, icon: '⊹' },
    copies_batch:      { label: 'Textos (5 copies)',               color: '#a78bfa', icon: '✎' },
    text_regen:        { label: 'Regenerar 1 texto',               color: '#a78bfa', icon: '↻' },
    brand_dna:         { label: 'Generación de identidad de marca', color: '#f59e0b', icon: '⬡' },
    campaign_relaunch: { label: 'Re-lanzar campaña',               color: '#fbbf24', icon: '↺' },
    plan_reset:        { label: 'Período del plan renovado',       color: NX.success, icon: '+' },
    admin_grant:       { label: 'Otorgamiento manual',             color: NX.success, icon: '+' },
    refund:            { label: 'Reembolso automático (gen falló)', color: NX.success, icon: '↶' },
  };

  const list = transactions || [];

  // Resumen agregado por acción (solo consumos, delta < 0)
  const summary = React.useMemo(() => {
    const out = {};
    for (const t of list) {
      if (t.delta >= 0) continue;
      const key = t.action;
      if (!out[key]) out[key] = { count: 0, total: 0 };
      out[key].count += 1;
      out[key].total += -t.delta;
    }
    return out;
  }, [list]);

  const totalSpent = React.useMemo(
    () => list.filter(t => t.delta < 0).reduce((s, t) => s + (-t.delta), 0),
    [list]
  );
  const totalGranted = React.useMemo(
    () => list.filter(t => t.delta > 0 && t.action !== 'refund').reduce((s, t) => s + t.delta, 0),
    [list]
  );
  const totalRefunded = React.useMemo(
    () => list.filter(t => t.action === 'refund').reduce((s, t) => s + t.delta, 0),
    [list]
  );

  const filterOptions = [
    { id: 'all',     label: lang === 'en' ? 'All'        : 'Todo' },
    { id: 'spend',   label: lang === 'en' ? 'Spent'      : 'Consumo' },
    { id: 'income',  label: lang === 'en' ? 'Granted'    : 'Otorgados' },
    { id: 'refund',  label: lang === 'en' ? 'Refunded'   : 'Reembolsos' },
  ];
  const filtered = list.filter(t => {
    if (filter === 'all') return true;
    if (filter === 'spend') return t.delta < 0;
    if (filter === 'income') return t.delta > 0 && t.action !== 'refund';
    if (filter === 'refund') return t.action === 'refund';
    return true;
  });

  const fmtDate = (iso) => {
    try {
      const d = new Date(iso);
      return d.toLocaleString(lang === 'en' ? 'en-US' : 'es-CL', {
        day: '2-digit', month: 'short', hour: '2-digit', minute: '2-digit',
      });
    } catch { return iso; }
  };

  return (
    <div style={{ height: '100%', overflow: 'auto', padding: '32px', maxWidth: '960px', margin: '0 auto' }}>
      <div style={{ marginBottom: '24px' }}>
        <h1 style={{ fontSize: '24px', fontWeight: 300, letterSpacing: '-0.02em', color: NX.text, margin: '0 0 6px' }}>
          {lang === 'en' ? 'Credits history' : 'Historial de créditos'}
        </h1>
        <p style={{ fontSize: '13px', color: NX.muted, margin: 0 }}>
          {lang === 'en'
            ? 'Detailed log of how every credit was spent — per action, per generation, per campaign.'
            : 'Registro detallado de cómo se gastó cada crédito — por acción, por generación, por campaña.'}
        </p>
      </div>

      {/* Tarjetas resumen */}
      <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(160px, 1fr))', gap: '12px', marginBottom: '20px' }}>
        <div style={{ padding: '16px', background: 'rgba(248,113,113,0.08)', border: '1px solid rgba(248,113,113,0.22)', borderRadius: '12px' }}>
          <div style={{ fontSize: '10px', color: NX.muted, fontFamily: "'DM Mono',monospace", letterSpacing: '0.08em', textTransform: 'uppercase', marginBottom: '6px' }}>
            {lang === 'en' ? 'Total spent' : 'Total gastado'}
          </div>
          <div style={{ fontSize: '22px', fontWeight: 700, color: NX.danger, fontFamily: "'DM Mono',monospace" }}>
            {totalSpent.toLocaleString()} <span style={{ fontSize: '11px', color: NX.muted, fontWeight: 400 }}>cr</span>
          </div>
        </div>
        <div style={{ padding: '16px', background: 'rgba(52,211,153,0.08)', border: '1px solid rgba(52,211,153,0.22)', borderRadius: '12px' }}>
          <div style={{ fontSize: '10px', color: NX.muted, fontFamily: "'DM Mono',monospace", letterSpacing: '0.08em', textTransform: 'uppercase', marginBottom: '6px' }}>
            {lang === 'en' ? 'Total granted' : 'Total otorgado'}
          </div>
          <div style={{ fontSize: '22px', fontWeight: 700, color: NX.success, fontFamily: "'DM Mono',monospace" }}>
            {totalGranted.toLocaleString()} <span style={{ fontSize: '11px', color: NX.muted, fontWeight: 400 }}>cr</span>
          </div>
        </div>
        <div style={{ padding: '16px', background: 'rgba(167,139,250,0.08)', border: '1px solid rgba(167,139,250,0.22)', borderRadius: '12px' }}>
          <div style={{ fontSize: '10px', color: NX.muted, fontFamily: "'DM Mono',monospace", letterSpacing: '0.08em', textTransform: 'uppercase', marginBottom: '6px' }}>
            {lang === 'en' ? 'Refunded' : 'Reembolsado'}
          </div>
          <div style={{ fontSize: '22px', fontWeight: 700, color: NX.accent2, fontFamily: "'DM Mono',monospace" }}>
            {totalRefunded.toLocaleString()} <span style={{ fontSize: '11px', color: NX.muted, fontWeight: 400 }}>cr</span>
          </div>
        </div>
        <div style={{ padding: '16px', background: 'rgba(139,92,246,0.08)', border: '1px solid rgba(139,92,246,0.22)', borderRadius: '12px' }}>
          <div style={{ fontSize: '10px', color: NX.muted, fontFamily: "'DM Mono',monospace", letterSpacing: '0.08em', textTransform: 'uppercase', marginBottom: '6px' }}>
            {lang === 'en' ? 'Transactions' : 'Movimientos'}
          </div>
          <div style={{ fontSize: '22px', fontWeight: 700, color: NX.text, fontFamily: "'DM Mono',monospace" }}>
            {list.length}
          </div>
        </div>
      </div>

      {/* Desglose por acción (solo consumos) */}
      {Object.keys(summary).length > 0 && (
        <div style={{ marginBottom: '20px', padding: '20px', background: 'rgba(26,17,40,0.55)', border: `1px solid ${NX.border}`, borderRadius: '14px' }}>
          <h3 style={{ fontSize: '13px', fontWeight: 600, color: NX.text, margin: '0 0 14px', letterSpacing: '0.04em', textTransform: 'uppercase', fontFamily: "'DM Mono',monospace" }}>
            {lang === 'en' ? 'Breakdown by action' : 'Desglose por acción'}
          </h3>
          <div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
            {Object.entries(summary)
              .sort((a, b) => b[1].total - a[1].total)
              .map(([action, data]) => {
                const meta = ACTION_META[action] || { label: action, color: NX.muted, icon: '·' };
                const pct = totalSpent > 0 ? (data.total / totalSpent) * 100 : 0;
                return (
                  <div key={action}>
                    <div style={{ display: 'flex', alignItems: 'baseline', justifyContent: 'space-between', marginBottom: '4px' }}>
                      <div style={{ display: 'flex', alignItems: 'center', gap: '8px', minWidth: 0 }}>
                        <span style={{ fontSize: '14px', color: meta.color, width: '18px', textAlign: 'center' }}>{meta.icon}</span>
                        <span style={{ fontSize: '13px', color: NX.text }}>{meta.label}</span>
                        <span style={{ fontSize: '10px', color: NX.muted, fontFamily: "'DM Mono',monospace" }}>×{data.count}</span>
                      </div>
                      <span style={{ fontSize: '13px', fontWeight: 700, fontFamily: "'DM Mono',monospace", color: meta.color }}>
                        {data.total.toLocaleString()} cr
                        <span style={{ fontSize: '10px', color: NX.muted, marginLeft: '6px', fontWeight: 400 }}>{pct.toFixed(0)}%</span>
                      </span>
                    </div>
                    <div style={{ height: '4px', background: NX.bg3, borderRadius: '4px', overflow: 'hidden' }}>
                      <div style={{ height: '100%', width: `${pct}%`, background: meta.color, opacity: 0.55, transition: 'width 0.3s' }}/>
                    </div>
                  </div>
                );
              })}
          </div>
        </div>
      )}

      {/* Filtros */}
      <div style={{ display: 'flex', gap: '6px', marginBottom: '12px', flexWrap: 'wrap' }}>
        {filterOptions.map(opt => (
          <button key={opt.id} onClick={() => setFilter(opt.id)}
            style={{
              padding: '6px 12px', borderRadius: '100px', border: `1px solid ${filter === opt.id ? NX.accent2 : NX.border}`,
              background: filter === opt.id ? 'rgba(139,92,246,0.18)' : 'transparent',
              color: filter === opt.id ? NX.accent2 : NX.muted,
              fontSize: '12px', fontWeight: 500, fontFamily: "'DM Sans',sans-serif",
              cursor: 'pointer', transition: 'all 0.15s',
            }}>
            {opt.label}
          </button>
        ))}
      </div>

      {/* Lista de transacciones */}
      <div style={{ background: 'rgba(26,17,40,0.55)', border: `1px solid ${NX.border}`, borderRadius: '14px', overflow: 'hidden' }}>
        {loading && (
          <div style={{ padding: '40px', textAlign: 'center', color: NX.muted, fontSize: '13px' }}>
            {lang === 'en' ? 'Loading…' : 'Cargando…'}
          </div>
        )}
        {!loading && filtered.length === 0 && (
          <div style={{ padding: '40px', textAlign: 'center', color: NX.muted, fontSize: '13px' }}>
            {lang === 'en' ? 'No transactions yet — generate a strategy or image to see your usage here.' : 'Aún no hay movimientos — genera una estrategia o imagen para ver tu consumo aquí.'}
          </div>
        )}
        {!loading && filtered.map((tx, idx) => {
          const meta = ACTION_META[tx.action] || { label: tx.action, color: NX.muted, icon: '·' };
          const isCredit = tx.delta > 0;
          const refType = tx.ref_type
            ? (tx.ref_type === 'campaign' ? (lang === 'en' ? 'Campaign' : 'Campaña')
              : tx.ref_type === 'image' ? (lang === 'en' ? 'Image' : 'Imagen')
              : tx.ref_type === 'image_batch' ? (lang === 'en' ? 'Image batch' : 'Lote de imágenes')
              : tx.ref_type === 'copies' ? (lang === 'en' ? 'Copies' : 'Copies')
              : tx.ref_type === 'strategy' ? (lang === 'en' ? 'Strategy' : 'Estrategia')
              : tx.ref_type === 'brand_dna' ? 'Brand DNA'
              : tx.ref_type === 'plan' ? (lang === 'en' ? 'Plan' : 'Plan')
              : tx.ref_type === 'admin' ? 'Admin'
              : tx.ref_type)
            : '';
          return (
            <div key={tx.id || idx} style={{
              display: 'grid',
              gridTemplateColumns: '32px 1fr auto',
              gap: '12px', alignItems: 'center',
              padding: '12px 16px',
              borderTop: idx === 0 ? 'none' : `1px solid ${NX.border}`,
            }}>
              <div style={{ width: '32px', height: '32px', borderRadius: '8px', background: `${meta.color}1a`, border: `1px solid ${meta.color}33`, display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: '14px', color: meta.color }}>
                {meta.icon}
              </div>
              <div style={{ minWidth: 0 }}>
                <div style={{ fontSize: '13px', color: NX.text, fontWeight: 500, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
                  {meta.label}
                </div>
                <div style={{ fontSize: '11px', color: NX.muted, fontFamily: "'DM Mono',monospace", display: 'flex', gap: '8px', flexWrap: 'wrap' }}>
                  <span>{fmtDate(tx.created_at)}</span>
                  {refType && tx.ref_id && <span>· {refType} #{tx.ref_id}</span>}
                  {refType && !tx.ref_id && <span>· {refType}</span>}
                </div>
              </div>
              <div style={{ textAlign: 'right', fontFamily: "'DM Mono',monospace" }}>
                <div style={{ fontSize: '15px', fontWeight: 700, color: isCredit ? NX.success : NX.danger }}>
                  {isCredit ? '+' : '−'}{Math.abs(tx.delta).toLocaleString()}
                </div>
                <div style={{ fontSize: '10px', color: NX.muted }}>
                  {lang === 'en' ? 'balance:' : 'saldo:'} {Number(tx.balance_after).toLocaleString()}
                </div>
              </div>
            </div>
          );
        })}
      </div>

      {!loading && filtered.length >= 200 && (
        <div style={{ textAlign: 'center', marginTop: '12px', fontSize: '11px', color: NX.muted }}>
          {lang === 'en' ? 'Showing the latest 200 transactions.' : 'Mostrando los 200 movimientos más recientes.'}
        </div>
      )}
    </div>
  );
};

// ══════════════════════════════════════════
// SCREEN: ADMIN DASHBOARD (only visible when currentUser.is_admin)
// ══════════════════════════════════════════
const AdminScreen = ({ lang = 'es' }) => {
  const t = lang === 'en'
    ? { title: 'Admin dashboard', subtitle: 'Users, subscriptions and platform health',
        users: 'Users', total: 'Total', verified: 'Verified', admins: 'Admins', new30: 'New (30 d)',
        plans: 'Active subscriptions by plan', free: 'Free',
        campaigns: 'Campaigns', activeCampaigns: 'Active', storage: 'Cloudinary storage', ofPlan: 'of free tier', openCloudinary: 'Open Cloudinary console',
        topUsers: 'Top 10 users by campaigns', empty: 'No data yet.', loading: 'Loading…', error: 'Could not load stats',
        retry: 'Retry',
        tokens: 'AI token consumption', tokensToday: 'Today', tokensWeek: 'Last 7 days', tokensMonth: 'Last 30 days', calls: 'calls', cost: 'Est. cost',
        revenue: 'Subscription revenue', mrr: 'Monthly recurring revenue (paid)', perCycle: 'Only counts paid plans — admin comps excluded',
        active: 'Active users', active24h: '24h', active7d: '7d', active30d: '30d',
        quickActions: 'Quick actions', openRailway: 'Railway status', openNeon: 'Neon console', openResend: 'Resend logs',
        userMgmt: 'User management', search: 'Search by name or email…', email: 'Email', name: 'Name', plan: 'Plan', status: 'Status', source: 'Source', camps: 'Camps', actions: 'Actions',
        makeAdmin: 'Make admin', removeAdmin: 'Remove admin', noPlan: 'No plan', paid: 'Paid', comp: 'Admin (comp)',
        delete: 'Delete', confirmDelete: 'Delete this user and all their data?', yesDelete: 'Yes, delete', cancel: 'Cancel',
        saving: 'Saving…', saved: 'Saved' }
    : { title: 'Panel de administrador', subtitle: 'Usuarios, suscripciones y estado de la plataforma',
        users: 'Usuarios', total: 'Total', verified: 'Verificados', admins: 'Admins', new30: 'Nuevos (30 d)',
        plans: 'Suscripciones activas por plan', free: 'Free',
        campaigns: 'Campañas', activeCampaigns: 'Activas', storage: 'Almacenamiento Cloudinary', ofPlan: 'del free tier', openCloudinary: 'Abrir consola Cloudinary',
        topUsers: 'Top 10 usuarios por campañas', empty: 'Sin datos aún.', loading: 'Cargando…', error: 'No se pudo cargar',
        retry: 'Reintentar',
        tokens: 'Consumo de tokens IA', tokensToday: 'Hoy', tokensWeek: 'Últimos 7 días', tokensMonth: 'Últimos 30 días', calls: 'llamadas', cost: 'Costo estimado',
        revenue: 'Ingresos por suscripciones', mrr: 'Ingreso recurrente mensual (pagado)', perCycle: 'Solo cuenta planes pagados — cortesías admin excluidas',
        active: 'Usuarios activos', active24h: '24 h', active7d: '7 d', active30d: '30 d',
        quickActions: 'Accesos rápidos', openRailway: 'Estado de Railway', openNeon: 'Consola Neon', openResend: 'Logs de Resend',
        userMgmt: 'Gestión de usuarios', search: 'Buscar por nombre o email…', email: 'Email', name: 'Nombre', plan: 'Plan', status: 'Estado', source: 'Origen', camps: 'Camp.', actions: 'Acciones',
        makeAdmin: 'Hacer admin', removeAdmin: 'Quitar admin', noPlan: 'Sin plan', paid: 'Pagado', comp: 'Admin (cortesía)',
        delete: 'Eliminar', confirmDelete: '¿Eliminar este usuario y todos sus datos?', yesDelete: 'Sí, eliminar', cancel: 'Cancelar',
        saving: 'Guardando…', saved: 'Guardado' };

  const [stats, setStats] = React.useState(null);
  const [storage, setStorage] = React.useState(null);
  const [users, setUsers] = React.useState([]);
  const [loading, setLoading] = React.useState(true);
  const [error, setError] = React.useState(null);
  const [search, setSearch] = React.useState('');
  const [busyUserId, setBusyUserId] = React.useState(null);
  const [deleteConfirmId, setDeleteConfirmId] = React.useState(null);

  const load = React.useCallback(async () => {
    setLoading(true); setError(null);
    try {
      const [s, st, us] = await Promise.all([
        authFetch('/api/admin/stats').then(r => r.ok ? r.json() : Promise.reject(new Error(r.status))),
        authFetch('/api/admin/storage').then(r => r.ok ? r.json() : null).catch(() => null),
        authFetch('/api/admin/users').then(r => r.ok ? r.json() : []).catch(() => []),
      ]);
      setStats(s); setStorage(st); setUsers(Array.isArray(us) ? us : []);
    } catch (e) { setError(e.message || 'error'); }
    setLoading(false);
  }, []);
  React.useEffect(() => { load(); }, [load]);

  const patchUser = async (id, patch) => {
    setBusyUserId(id);
    try {
      const res = await authFetch(`/api/admin/users/${id}`, { method: 'PATCH', body: JSON.stringify(patch) });
      if (!res.ok) {
        const d = await res.json().catch(() => ({}));
        alert(d.error || 'Error');
      } else {
        const updated = await res.json();
        setUsers(prev => prev.map(u => u.id === id ? { ...u, ...updated } : u));
      }
    } catch (e) { alert(e.message || 'Error'); }
    setBusyUserId(null);
  };

  const deleteUser = async (id) => {
    setBusyUserId(id);
    try {
      const res = await authFetch(`/api/admin/users/${id}`, { method: 'DELETE' });
      if (!res.ok) {
        const d = await res.json().catch(() => ({}));
        alert(d.error || 'Error');
      } else {
        setUsers(prev => prev.filter(u => u.id !== id));
      }
    } catch (e) { alert(e.message || 'Error'); }
    setBusyUserId(null);
    setDeleteConfirmId(null);
  };

  const changePassword = async (id, email) => {
    const newPwd = window.prompt(
      lang === 'en'
        ? `Set a new password for ${email} (min 6 chars):`
        : `Nueva contraseña para ${email} (mínimo 6 caracteres):`
    );
    if (!newPwd) return;
    if (newPwd.length < 6) { alert(lang === 'en' ? 'Too short (min 6 chars)' : 'Muy corta (mínimo 6 caracteres)'); return; }
    setBusyUserId(id);
    try {
      const res = await authFetch(`/api/admin/users/${id}/password`, {
        method: 'POST',
        body: JSON.stringify({ password: newPwd }),
      });
      if (!res.ok) {
        const d = await res.json().catch(() => ({}));
        alert(d.error || 'Error');
      } else {
        alert(lang === 'en' ? `✓ Password updated for ${email}` : `✓ Contraseña actualizada para ${email}`);
      }
    } catch (e) { alert(e.message || 'Error'); }
    setBusyUserId(null);
  };

  const resetUser = async (id, email) => {
    // Doble confirmación: primero un confirm() con la lista exacta de lo que se borra, luego
    // un prompt() que exige escribir el email para evitar clicks accidentales en producción.
    const confirmMsg = lang === 'en'
      ? `⚠️ FULL RESET of ${email}\n\nThis will permanently delete:\n• All campaigns\n• Brand DNA\n• Meta connection + assets\n• Businesses\n• Credit transactions history\n• API usage history\n• Subscription events\n\nAnd reset:\n• Plan → none\n• Credits balance → 0\n• Period → cleared\n\nThe login (email + password) is preserved. Continue?`
      : `⚠️ RESET TOTAL de ${email}\n\nEsto borrará permanentemente:\n• Todas las campañas\n• Brand DNA\n• Conexión Meta + assets\n• Negocios\n• Historial de créditos consumidos\n• Historial de uso de API\n• Eventos de suscripción\n\nY reseteará:\n• Plan → ninguno\n• Saldo de créditos → 0\n• Período → vacío\n\nEl login (email + contraseña) se conserva. ¿Continuar?`;
    if (!window.confirm(confirmMsg)) return;
    const typed = window.prompt(
      lang === 'en'
        ? `Type the email "${email}" to confirm:`
        : `Escribe el email "${email}" para confirmar:`,
      ''
    );
    if (!typed || typed.trim().toLowerCase() !== String(email).trim().toLowerCase()) {
      alert(lang === 'en' ? 'Email did not match — reset cancelled.' : 'El email no coincide — reset cancelado.');
      return;
    }
    setBusyUserId(id);
    try {
      const res = await authFetch(`/api/admin/users/${id}/reset`, { method: 'POST' });
      if (!res.ok) {
        const d = await res.json().catch(() => ({}));
        alert(d.error || 'Error');
      } else {
        alert(lang === 'en' ? '✓ User reset — they can log in as a fresh account' : '✓ Cuenta reseteada — el usuario entra como cuenta nueva');
        if (typeof load === 'function') { try { load(); } catch {} }
      }
    } catch (e) { alert(e.message || 'Error'); }
    setBusyUserId(null);
  };

  // Otorga créditos arbitrarios. Suma al balance, no resetea período.
  const grantCredits = async (id, email) => {
    const promptMsg = lang === 'en'
      ? `Grant credits to ${email}. Enter amount (positive number):`
      : `Otorgar créditos a ${email}. Ingresa cantidad (número positivo):`;
    const raw = window.prompt(promptMsg, '100');
    if (!raw) return;
    const amount = Math.round(Number(raw));
    if (!amount || amount <= 0) { alert(lang === 'en' ? 'Invalid amount' : 'Cantidad inválida'); return; }
    const reasonMsg = lang === 'en' ? 'Reason (optional):' : 'Motivo (opcional):';
    const reason = window.prompt(reasonMsg, '') || '';
    setBusyUserId(id);
    try {
      const res = await authFetch('/api/admin/credits/grant', { method: 'POST', body: JSON.stringify({ user_id: id, amount, reason }) });
      const d = await res.json().catch(() => ({}));
      if (!res.ok) alert(d.error || 'Error');
      else {
        alert((lang === 'en' ? '✓ Granted. New balance: ' : '✓ Otorgado. Nuevo balance: ') + d.balance);
        load();
      }
    } catch (e) { alert(e.message || 'Error'); }
    setBusyUserId(null);
  };

  // Fuerza el reset del período: balance vuelve al monto del plan + extiende period_end 30d.
  const resetCreditsPeriod = async (id, email) => {
    const confirmMsg = lang === 'en'
      ? `Force credits reset for ${email}? Balance will be restored to plan allocation and the period restarted (30 days).`
      : `¿Forzar reset de créditos para ${email}? Balance vuelve al monto del plan y el período se reinicia (30 días).`;
    if (!window.confirm(confirmMsg)) return;
    setBusyUserId(id);
    try {
      const res = await authFetch(`/api/admin/credits/reset/${id}`, { method: 'POST' });
      const d = await res.json().catch(() => ({}));
      if (!res.ok || !d.ok) alert(d.error || d.reason || 'Error');
      else {
        alert((lang === 'en' ? '✓ Reset. Balance: ' : '✓ Reseteado. Balance: ') + d.balance);
        load();
      }
    } catch (e) { alert(e.message || 'Error'); }
    setBusyUserId(null);
  };

  const filteredUsers = users.filter(u => {
    if (!search.trim()) return true;
    const s = search.toLowerCase();
    return (u.email || '').toLowerCase().includes(s) || (u.name || '').toLowerCase().includes(s);
  });

  const planCount = (plan) => {
    if (!stats?.plans) return 0;
    return stats.plans
      .filter(p => (p.plan === plan) && p.status === 'active')
      .reduce((s, p) => s + p.count, 0);
  };

  const MAX_COLOR = '#f59e0b';
  const renderBusinessMaxLabel = () => (
    <span>BUSINESS <span style={{ color: MAX_COLOR, background: 'rgba(245,158,11,0.12)', padding: '1px 6px', borderRadius: '4px', letterSpacing: '0.1em', marginLeft: '4px', fontSize: '10px' }}>MAX</span></span>
  );

  const Stat = ({ label, value, accent }) => (
    <div style={{ flex: 1, minWidth: '140px', background: NX.bg3, border: `1px solid ${NX.border}`, borderRadius: '12px', padding: '16px 18px' }}>
      <div style={{ fontSize: '11px', color: NX.muted, fontFamily: "'DM Mono',monospace", letterSpacing: '0.08em', marginBottom: '8px', textTransform: 'uppercase' }}>{label}</div>
      <div style={{ fontSize: '28px', fontWeight: 400, color: accent || NX.text, fontFamily: "'DM Mono',monospace" }}>{value ?? '—'}</div>
    </div>
  );

  return (
    <div style={{ height: '100%', overflow: 'auto', padding: '32px 32px 60px' }}>
      <div style={{ marginBottom: '24px' }}>
        <h1 style={{ fontSize: '24px', fontWeight: 400, letterSpacing: '-0.02em', color: NX.text, margin: '0 0 4px' }}>{t.title}</h1>
        <p style={{ color: NX.muted, fontSize: '14px', margin: 0 }}>{t.subtitle}</p>
      </div>

      {loading && <div style={{ padding: '40px', color: NX.muted, textAlign: 'center' }}>{t.loading}</div>}
      {error && !loading && (
        <div style={{ padding: '24px', border: `1px solid ${NX.danger}`, borderRadius: '12px', color: NX.danger, fontSize: '13px' }}>
          {t.error} · <button onClick={load} style={{ marginLeft: '8px', background: 'transparent', border: `1px solid ${NX.danger}`, color: NX.danger, padding: '4px 10px', borderRadius: '6px', cursor: 'pointer' }}>{t.retry}</button>
        </div>
      )}

      {stats && !loading && !error && (
        <>
          {/* USERS */}
          <div style={{ marginBottom: '28px' }}>
            <div style={{ fontSize: '11px', color: NX.muted, fontFamily: "'DM Mono',monospace", letterSpacing: '0.08em', marginBottom: '10px', textTransform: 'uppercase' }}>{t.users}</div>
            <div style={{ display: 'flex', flexWrap: 'wrap', gap: '12px' }}>
              <Stat label={t.total} value={stats.users.total} accent={NX.accent}/>
              <Stat label={t.verified} value={stats.users.verified}/>
              <Stat label={t.new30} value={stats.users.newLast30d} accent={NX.success}/>
              <Stat label={t.admins} value={stats.users.admins}/>
            </div>
          </div>

          {/* PLANS */}
          <div style={{ marginBottom: '28px' }}>
            <div style={{ fontSize: '11px', color: NX.muted, fontFamily: "'DM Mono',monospace", letterSpacing: '0.08em', marginBottom: '10px', textTransform: 'uppercase' }}>{t.plans}</div>
            <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit,minmax(180px,1fr))', gap: '12px' }}>
              <div style={{ background: NX.bg3, border: `1px solid ${NX.border}`, borderRadius: '12px', padding: '16px 18px' }}>
                <div style={{ fontSize: '11px', color: NX.muted, marginBottom: '6px' }}>{t.free}</div>
                <div style={{ fontSize: '24px', color: NX.text, fontFamily: "'DM Mono',monospace" }}>{planCount(null) + planCount('free')}</div>
              </div>
              <div style={{ background: NX.bg3, border: `1px solid ${NX.accent2}`, borderRadius: '12px', padding: '16px 18px' }}>
                <div style={{ fontSize: '11px', color: NX.accent2, marginBottom: '6px', fontWeight: 600, letterSpacing: '0.1em' }}>BUSINESS</div>
                <div style={{ fontSize: '24px', color: NX.text, fontFamily: "'DM Mono',monospace" }}>{planCount('business')}</div>
              </div>
              <div style={{ background: NX.bg3, border: `1px solid ${MAX_COLOR}`, borderRadius: '12px', padding: '16px 18px', boxShadow: '0 0 30px rgba(245,158,11,0.08)' }}>
                <div style={{ fontSize: '11px', marginBottom: '6px', fontWeight: 600, letterSpacing: '0.1em' }}>{renderBusinessMaxLabel()}</div>
                <div style={{ fontSize: '24px', color: NX.text, fontFamily: "'DM Mono',monospace" }}>{planCount('business_max')}</div>
              </div>
            </div>
          </div>

          {/* CAMPAIGNS + ACTIVE USERS in one row */}
          <div style={{ marginBottom: '28px', display: 'grid', gridTemplateColumns: 'repeat(auto-fit,minmax(320px,1fr))', gap: '20px' }}>
            <div>
              <div style={{ fontSize: '11px', color: NX.muted, fontFamily: "'DM Mono',monospace", letterSpacing: '0.08em', marginBottom: '10px', textTransform: 'uppercase' }}>{t.campaigns}</div>
              <div style={{ display: 'flex', flexWrap: 'wrap', gap: '12px' }}>
                <Stat label={t.total} value={stats.campaigns.total}/>
                <Stat label={t.activeCampaigns} value={stats.campaigns.active} accent={NX.success}/>
                <Stat label={t.new30} value={stats.campaigns.newLast30d}/>
              </div>
            </div>
            {stats.activeUsers && (
              <div>
                <div style={{ fontSize: '11px', color: NX.muted, fontFamily: "'DM Mono',monospace", letterSpacing: '0.08em', marginBottom: '10px', textTransform: 'uppercase' }}>{t.active}</div>
                <div style={{ display: 'flex', flexWrap: 'wrap', gap: '12px' }}>
                  <Stat label={t.active24h} value={stats.activeUsers.active_day} accent={NX.success}/>
                  <Stat label={t.active7d} value={stats.activeUsers.active_week}/>
                  <Stat label={t.active30d} value={stats.activeUsers.active_month}/>
                </div>
              </div>
            )}
          </div>

          {/* AI TOKENS */}
          {stats.tokens && stats.tokens.length > 0 && (
            <div style={{ marginBottom: '28px' }}>
              <div style={{ fontSize: '11px', color: NX.muted, fontFamily: "'DM Mono',monospace", letterSpacing: '0.08em', marginBottom: '10px', textTransform: 'uppercase' }}>{t.tokens}</div>
              <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit,minmax(300px,1fr))', gap: '12px' }}>
                {stats.tokens.map(row => {
                  const providerLabel = row.provider === 'anthropic' ? 'OPUS / CLAUDE'
                                      : row.provider === 'gemini'    ? 'GEMINI · NANO BANANA'
                                      : row.provider === 'openai'    ? 'OPENAI · GPT-IMAGE-2'
                                      : row.provider.toUpperCase();
                  const color = row.provider === 'anthropic' ? '#f59e0b'
                              : row.provider === 'openai'    ? '#10a37f'
                              : NX.accent2;
                  const fmt = (n) => Number(n || 0).toLocaleString('en-US');
                  const money = (n) => '$' + Number(n || 0).toFixed(4);
                  return (
                    <div key={row.provider} style={{ background: NX.bg3, border: `1px solid ${NX.border}`, borderLeft: `3px solid ${color}`, borderRadius: '12px', padding: '18px' }}>
                      <div style={{ fontSize: '11px', color, fontWeight: 700, letterSpacing: '0.12em', marginBottom: '14px' }}>{providerLabel}</div>
                      {[
                        { label: t.tokensToday, tokens: row.tokens_day, calls: row.calls_day, cost: row.cost_day },
                        { label: t.tokensWeek,  tokens: row.tokens_week, calls: row.calls_week, cost: row.cost_week },
                        { label: t.tokensMonth, tokens: row.tokens_month, calls: row.calls_month, cost: row.cost_month },
                      ].map((b, i) => (
                        <div key={i} style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'baseline', padding: '8px 0', borderTop: i > 0 ? `1px dashed ${NX.border}` : 'none' }}>
                          <span style={{ fontSize: '11px', color: NX.muted, fontFamily: "'DM Mono',monospace", letterSpacing: '0.06em' }}>{b.label}</span>
                          <div style={{ textAlign: 'right' }}>
                            <div style={{ fontSize: '13px', color: NX.text, fontFamily: "'DM Mono',monospace" }}>{fmt(b.tokens)} tok · {b.calls} {t.calls}</div>
                            <div style={{ fontSize: '11px', color: NX.success, fontFamily: "'DM Mono',monospace" }}>{money(b.cost)}</div>
                          </div>
                        </div>
                      ))}
                    </div>
                  );
                })}
              </div>
            </div>
          )}

          {/* REVENUE */}
          <div style={{ marginBottom: '28px' }}>
            <div style={{ fontSize: '11px', color: NX.muted, fontFamily: "'DM Mono',monospace", letterSpacing: '0.08em', marginBottom: '10px', textTransform: 'uppercase' }}>{t.revenue}</div>
            <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit,minmax(200px,1fr))', gap: '12px' }}>
              <Stat label={t.tokensToday}  value={`$${Number(stats.revenue.day).toFixed(2)}`}/>
              <Stat label={t.tokensWeek}   value={`$${Number(stats.revenue.week).toFixed(2)}`}/>
              <Stat label={t.tokensMonth}  value={`$${Number(stats.revenue.month).toFixed(2)}`} accent={NX.success}/>
              <div style={{ background: NX.bg3, border: `1px solid ${MAX_COLOR}`, borderRadius: '12px', padding: '16px 18px', boxShadow: '0 0 30px rgba(245,158,11,0.08)' }}>
                <div style={{ fontSize: '11px', color: MAX_COLOR, fontFamily: "'DM Mono',monospace", letterSpacing: '0.08em', marginBottom: '8px', textTransform: 'uppercase', fontWeight: 700 }}>{t.mrr}</div>
                <div style={{ fontSize: '28px', fontWeight: 400, color: NX.text, fontFamily: "'DM Mono',monospace" }}>${Number(stats.revenue.mrrEstimate || 0).toLocaleString()}</div>
                <div style={{ fontSize: '10px', color: NX.muted, marginTop: '4px' }}>{t.perCycle}</div>
              </div>
            </div>
          </div>

          {/* STORAGE (clickable to Cloudinary console) */}
          {storage && (
            <div style={{ marginBottom: '28px' }}>
              <div style={{ fontSize: '11px', color: NX.muted, fontFamily: "'DM Mono',monospace", letterSpacing: '0.08em', marginBottom: '10px', textTransform: 'uppercase' }}>{t.storage}</div>
              <a
                href="https://console.cloudinary.com/"
                target="_blank" rel="noopener noreferrer"
                style={{ display: 'block', textDecoration: 'none', background: NX.bg3, border: `1px solid ${NX.border}`, borderRadius: '12px', padding: '20px', transition: 'all 0.2s', cursor: 'pointer' }}
                onMouseEnter={e => { e.currentTarget.style.borderColor = NX.accent; e.currentTarget.style.background = NX.bg4; }}
                onMouseLeave={e => { e.currentTarget.style.borderColor = NX.border; e.currentTarget.style.background = NX.bg3; }}
              >
                <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '12px' }}>
                  <div style={{ fontSize: '14px', color: NX.text }}>
                    <span style={{ fontFamily: "'DM Mono',monospace", fontSize: '22px' }}>{storage.totalGigabytes} GB</span>
                    <span style={{ color: NX.muted, fontSize: '13px', marginLeft: '8px' }}>/ {storage.freeTierLimitGB} GB</span>
                  </div>
                  <div style={{ fontSize: '13px', color: storage.pctOfFreeTier > 70 ? NX.danger : storage.pctOfFreeTier > 50 ? NX.warning : NX.success }}>
                    {storage.pctOfFreeTier}% {t.ofPlan}
                  </div>
                </div>
                <div style={{ height: '8px', background: NX.bg4, borderRadius: '4px', overflow: 'hidden', marginBottom: '10px' }}>
                  <div style={{
                    width: `${Math.min(100, storage.pctOfFreeTier)}%`,
                    height: '100%',
                    background: storage.pctOfFreeTier > 70 ? NX.danger : storage.pctOfFreeTier > 50 ? NX.warning : NX.accent,
                    transition: 'width 0.3s',
                  }}/>
                </div>
                <div style={{ fontSize: '11px', color: NX.accent, display: 'flex', alignItems: 'center', gap: '6px', fontFamily: "'DM Sans',sans-serif" }}>
                  {t.openCloudinary}
                  <svg width="10" height="10" viewBox="0 0 10 10" fill="none"><path d="M2 2h6v6M8 2L2 8" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round"/></svg>
                </div>
              </a>
            </div>
          )}

          {/* QUICK ACTIONS */}
          <div style={{ marginBottom: '28px' }}>
            <div style={{ fontSize: '11px', color: NX.muted, fontFamily: "'DM Mono',monospace", letterSpacing: '0.08em', marginBottom: '10px', textTransform: 'uppercase' }}>{t.quickActions}</div>
            <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit,minmax(200px,1fr))', gap: '10px' }}>
              {[
                { label: t.openRailway, href: 'https://status.railway.com' },
                { label: t.openNeon, href: 'https://console.neon.tech' },
                { label: t.openResend, href: 'https://resend.com/emails' },
              ].map((a, i) => (
                <a key={i} href={a.href} target="_blank" rel="noopener noreferrer"
                  style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: '8px', background: NX.bg3, border: `1px solid ${NX.border}`, borderRadius: '10px', padding: '12px 14px', color: NX.text, textDecoration: 'none', fontSize: '13px', transition: 'all 0.15s' }}
                  onMouseEnter={e => { e.currentTarget.style.borderColor = NX.accent; e.currentTarget.style.color = NX.accent; }}
                  onMouseLeave={e => { e.currentTarget.style.borderColor = NX.border; e.currentTarget.style.color = NX.text; }}
                >
                  <span>{a.label}</span>
                  <svg width="12" height="12" viewBox="0 0 10 10" fill="none"><path d="M2 2h6v6M8 2L2 8" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round"/></svg>
                </a>
              ))}
            </div>
          </div>

          {/* USER MANAGEMENT */}
          <div style={{ marginBottom: '28px' }}>
            <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: '10px', gap: '12px', flexWrap: 'wrap' }}>
              <div style={{ fontSize: '11px', color: NX.muted, fontFamily: "'DM Mono',monospace", letterSpacing: '0.08em', textTransform: 'uppercase' }}>{t.userMgmt}</div>
              <input
                type="text" value={search} onChange={e => setSearch(e.target.value)} placeholder={t.search}
                style={{ background: NX.bg3, border: `1px solid ${NX.border}`, borderRadius: '8px', color: NX.text, padding: '7px 12px', fontSize: '12px', minWidth: '240px', fontFamily: "'DM Sans',sans-serif", outline: 'none' }}
              />
            </div>

            <div style={{ background: NX.bg3, border: `1px solid ${NX.border}`, borderRadius: '12px', overflow: 'hidden' }}>
              {/* Header */}
              <div style={{ display: 'grid', gridTemplateColumns: 'minmax(220px,1.8fr) 130px 100px 80px 50px minmax(420px,auto)', gap: '12px', padding: '10px 16px', background: NX.bg4, borderBottom: `1px solid ${NX.border}`, fontSize: '10px', color: NX.muted, fontFamily: "'DM Mono',monospace", letterSpacing: '0.08em', textTransform: 'uppercase' }}>
                <div>{t.email}</div>
                <div>{t.plan}</div>
                <div>{t.status}</div>
                <div>{t.source}</div>
                <div style={{ textAlign: 'right' }}>{t.camps}</div>
                <div style={{ textAlign: 'right' }}>{t.actions}</div>
              </div>

              {filteredUsers.length === 0 && (
                <div style={{ padding: '32px', textAlign: 'center', color: NX.muted, fontSize: '13px' }}>{t.empty}</div>
              )}

              {filteredUsers.map(u => {
                const busy = busyUserId === u.id;
                const confirm = deleteConfirmId === u.id;
                const planColor = u.plan === 'business_max' ? MAX_COLOR : u.plan === 'business' ? NX.accent2 : u.plan === 'reviewer' ? NX.accent : NX.muted;
                const sourceLabel = u.plan_source === 'admin' ? t.comp : u.plan_source === 'stripe' ? t.paid : '—';
                const sourceColor = u.plan_source === 'admin' ? NX.warning : u.plan_source === 'stripe' ? NX.success : NX.muted;
                return (
                  <div key={u.id} style={{ display: 'grid', gridTemplateColumns: 'minmax(220px,1.8fr) 130px 100px 80px 50px minmax(420px,auto)', gap: '12px', padding: '12px 16px', borderTop: `1px solid ${NX.border}`, alignItems: 'center', opacity: busy ? 0.6 : 1 }}>
                    {/* Email + name + credits */}
                    <div style={{ minWidth: 0 }}>
                      <div style={{ display: 'flex', alignItems: 'center', gap: '6px' }}>
                        <span style={{ fontSize: '13px', color: NX.text, fontWeight: 500, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{u.email}</span>
                        {u.is_admin && <span style={{ fontSize: '9px', background: 'rgba(245,158,11,0.15)', color: MAX_COLOR, padding: '1px 6px', borderRadius: '4px', letterSpacing: '0.08em', fontWeight: 700 }}>ADMIN</span>}
                      </div>
                      <div style={{ display: 'flex', gap: '8px', alignItems: 'baseline' }}>
                        <span style={{ fontSize: '11px', color: NX.muted, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{u.name || '—'}</span>
                        {u.plan && (
                          <span style={{ fontSize: '10px', color: NX.accent2, fontFamily: "'DM Mono',monospace" }}>
                            {Number(u.credits_balance || 0).toLocaleString()} cr
                            {u.credits_period_end && ' · ' + new Date(u.credits_period_end).toLocaleDateString(lang === 'en' ? 'en-US' : 'es-CL', { day: 'numeric', month: 'short' })}
                          </span>
                        )}
                      </div>
                    </div>

                    {/* Plan selector */}
                    <select
                      value={u.plan || ''} disabled={busy}
                      onChange={e => patchUser(u.id, { plan: e.target.value || null })}
                      style={{ background: NX.bg4, border: `1px solid ${planColor}`, color: planColor, borderRadius: '6px', padding: '5px 8px', fontSize: '11px', cursor: 'pointer', fontFamily: "'DM Sans',sans-serif", outline: 'none' }}>
                      <option value="">{t.noPlan}</option>
                      <option value="business">BUSINESS</option>
                      <option value="business_max">BUSINESS MAX</option>
                      <option value="reviewer">REVIEWER</option>
                    </select>

                    {/* Status selector */}
                    <select
                      value={u.plan_status || 'active'} disabled={busy || !u.plan}
                      onChange={e => patchUser(u.id, { plan_status: e.target.value })}
                      style={{ background: NX.bg4, border: `1px solid ${NX.border}`, color: u.plan_status === 'active' ? NX.success : u.plan_status === 'canceled' ? NX.danger : NX.muted, borderRadius: '6px', padding: '5px 8px', fontSize: '11px', cursor: u.plan ? 'pointer' : 'not-allowed', fontFamily: "'DM Sans',sans-serif", outline: 'none' }}>
                      <option value="active">active</option>
                      <option value="canceled">canceled</option>
                      <option value="paused">paused</option>
                    </select>

                    {/* Source label */}
                    <div style={{ fontSize: '11px', color: sourceColor, fontFamily: "'DM Mono',monospace", overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{sourceLabel}</div>

                    {/* Campaigns count */}
                    <div style={{ textAlign: 'right', fontSize: '12px', color: NX.muted, fontFamily: "'DM Mono',monospace" }}>{u.campaigns || 0}</div>

                    {/* Actions */}
                    <div style={{ display: 'flex', gap: '5px', justifyContent: 'flex-end', flexWrap: 'wrap', alignItems: 'center' }}>
                      <button
                        disabled={busy}
                        onClick={() => patchUser(u.id, { is_admin: !u.is_admin })}
                        title={u.is_admin ? t.removeAdmin : t.makeAdmin}
                        style={{ background: 'transparent', border: `1px solid ${u.is_admin ? NX.border : MAX_COLOR}`, borderRadius: '6px', padding: '5px 10px', color: u.is_admin ? NX.muted : MAX_COLOR, fontSize: '10px', cursor: busy ? 'default' : 'pointer', fontFamily: "'DM Sans',sans-serif", whiteSpace: 'nowrap' }}>
                        {u.is_admin ? t.removeAdmin : t.makeAdmin}
                      </button>
                      <button
                        disabled={busy}
                        onClick={() => resetUser(u.id, u.email)}
                        title={lang === 'en' ? 'Reset — delete campaigns, Meta connection, brand DNA, businesses (keeps login and plan)' : 'Resetear — borra campañas, conexión Meta, brand DNA y negocios (deja login y plan)'}
                        style={{ background: 'transparent', border: `1px solid ${NX.warning}`, borderRadius: '6px', padding: '5px 10px', color: NX.warning, fontSize: '10px', cursor: busy ? 'default' : 'pointer', fontFamily: "'DM Sans',sans-serif", whiteSpace: 'nowrap' }}>
                        {lang === 'en' ? 'Reset' : 'Resetear'}
                      </button>
                      <button
                        disabled={busy}
                        onClick={() => changePassword(u.id, u.email)}
                        title={lang === 'en' ? 'Set a new password for this user' : 'Cambiar la contraseña de este usuario'}
                        style={{ background: 'transparent', border: `1px solid ${NX.accent2}`, borderRadius: '6px', padding: '5px 10px', color: NX.accent2, fontSize: '10px', cursor: busy ? 'default' : 'pointer', fontFamily: "'DM Sans',sans-serif", whiteSpace: 'nowrap' }}>
                        {lang === 'en' ? 'Password' : 'Contraseña'}
                      </button>
                      {u.plan && (
                        <>
                          <button
                            disabled={busy}
                            onClick={() => grantCredits(u.id, u.email)}
                            title={lang === 'en' ? 'Grant credits manually (does not reset the period)' : 'Otorgar créditos manualmente (no resetea el período)'}
                            style={{ background: 'transparent', border: `1px solid ${NX.success}`, borderRadius: '6px', padding: '5px 10px', color: NX.success, fontSize: '10px', cursor: busy ? 'default' : 'pointer', fontFamily: "'DM Sans',sans-serif", whiteSpace: 'nowrap' }}>
                            {lang === 'en' ? '+ Credits' : '+ Créditos'}
                          </button>
                          <button
                            disabled={busy}
                            onClick={() => resetCreditsPeriod(u.id, u.email)}
                            title={lang === 'en' ? 'Force credits period reset (balance back to plan allocation, period +30d)' : 'Forzar reset del período (balance vuelve al monto del plan, +30d)'}
                            style={{ background: 'transparent', border: `1px solid ${NX.accent2}`, borderRadius: '6px', padding: '5px 10px', color: NX.accent2, fontSize: '10px', cursor: busy ? 'default' : 'pointer', fontFamily: "'DM Sans',sans-serif", whiteSpace: 'nowrap' }}>
                            {lang === 'en' ? 'Reset cr.' : 'Reset cr.'}
                          </button>
                        </>
                      )}
                      {confirm ? (
                        <>
                          <button disabled={busy} onClick={() => deleteUser(u.id)}
                            style={{ background: NX.danger, border: 'none', borderRadius: '6px', padding: '5px 10px', color: '#fff', fontSize: '10px', cursor: 'pointer', fontFamily: "'DM Sans',sans-serif", whiteSpace: 'nowrap' }}>
                            {t.yesDelete}
                          </button>
                          <button onClick={() => setDeleteConfirmId(null)}
                            style={{ background: 'transparent', border: `1px solid ${NX.border}`, borderRadius: '6px', padding: '5px 10px', color: NX.muted, fontSize: '10px', cursor: 'pointer', fontFamily: "'DM Sans',sans-serif" }}>
                            {t.cancel}
                          </button>
                        </>
                      ) : (
                        <button disabled={busy} onClick={() => setDeleteConfirmId(u.id)}
                          title={t.delete}
                          style={{ background: 'transparent', border: `1px solid ${NX.border}`, borderRadius: '6px', padding: '5px 8px', color: NX.muted, cursor: busy ? 'default' : 'pointer', display: 'flex', alignItems: 'center' }}
                          onMouseEnter={e => { e.currentTarget.style.borderColor = 'rgba(248,113,113,0.4)'; e.currentTarget.style.color = NX.danger; }}
                          onMouseLeave={e => { e.currentTarget.style.borderColor = NX.border; e.currentTarget.style.color = NX.muted; }}>
                          <svg width="12" height="12" viewBox="0 0 16 16" fill="none"><path d="M3 4h10M6 4V2.5A.5.5 0 016.5 2h3a.5.5 0 01.5.5V4M4.5 4l.5 9a1 1 0 001 1h4a1 1 0 001-1l.5-9" stroke="currentColor" strokeWidth="1.3" strokeLinecap="round" strokeLinejoin="round"/></svg>
                        </button>
                      )}
                    </div>
                  </div>
                );
              })}
            </div>
          </div>

          {/* TOP USERS */}
          {stats.topUsers && stats.topUsers.length > 0 && (
            <div>
              <div style={{ fontSize: '11px', color: NX.muted, fontFamily: "'DM Mono',monospace", letterSpacing: '0.08em', marginBottom: '10px', textTransform: 'uppercase' }}>{t.topUsers}</div>
              <div style={{ background: NX.bg3, border: `1px solid ${NX.border}`, borderRadius: '12px', overflow: 'hidden' }}>
                {stats.topUsers.map((u, i) => (
                  <div key={u.id} style={{ display: 'flex', alignItems: 'center', gap: '12px', padding: '12px 16px', borderTop: i > 0 ? `1px solid ${NX.border}` : 'none' }}>
                    <div style={{ width: '28px', color: NX.muted, fontFamily: "'DM Mono',monospace", fontSize: '12px' }}>#{i + 1}</div>
                    <div style={{ flex: 1, minWidth: 0 }}>
                      <div style={{ fontSize: '13px', color: NX.text, fontWeight: 500, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{u.name || u.email}</div>
                      <div style={{ fontSize: '11px', color: NX.muted }}>{u.email}</div>
                    </div>
                    <div style={{ fontSize: '13px', color: NX.accent, fontFamily: "'DM Mono',monospace" }}>{u.campaigns}</div>
                  </div>
                ))}
              </div>
            </div>
          )}
        </>
      )}
    </div>
  );
};

Object.assign(window, {
  PrimerosPasos, ConnectFlow, AssetsFlow, BrandFlow, StrategyFlow, QualityPicker,
  StrategyModePicker, AssistantFlow,
  MisEstrategias, Integraciones, MarcaScreen, PreciosScreen, AdminScreen,
});
