/*
 * AltText.jsx — Alt Text Generator (SA Command panel)
 * v1.0.5 — 2026-05-15
 *   v1.0.0 (initial)
 *   v1.0.1 (URL normalization, theme-var-only colors, ghost button)
 *   v1.0.2 (Surface server `detail` field; mobile responsive; Brain AI hook)
 *   v1.0.3 (Render per-image `reason` field for self-diagnostic errors)
 *   v1.0.4 (Generic placeholders only — Design Standards rule #7)
 *   v1.0.5 (Saved-runs persistence wire-up. On mount, fetches
 *           /sa/alt-text/projects and shows a "Previous runs (N)" picker
 *           above the form. Click a saved run to reload its results
 *           without regeneration (no AI cost). After successful generation,
 *           list refreshes so the new run appears immediately. Form fields
 *           repopulate from saved metadata when a run is loaded so user
 *           sees what config produced those results.)
 *
 * Wraps POST /sa/alt-text/process-page + new persistence endpoints
 * (server.js v1.7.5+).
 *
 * Auth: window.WPSB.getToken() — defined in index.html bootstrap.
 */
(function() {
  const { useState, useEffect } = window.React;
  const RAILWAY = (window.WPSB_CONFIG && window.WPSB_CONFIG.RAILWAY_URL)
    || 'https://wpsitebeam-railway-api-production.up.railway.app';

  const VOICE_PRESETS = {
    healthcare:           'Warm, accessible, and patient-centered. Plain language without clinical jargon. Inclusive of all demographics including uninsured and sliding-scale patients. Reassuring and professional, never marketing-heavy.',
    community_nonprofit:  'Mission-driven, inclusive, and friendly. Speaks to community members and supporters with warmth and respect. Emphasizes service and impact over self-promotion.',
    professional_services:'Confident, expert, and authoritative without being cold. Clear and consultative tone. Demonstrates expertise through clarity, not jargon.',
    ecommerce:            'Energetic and benefit-focused. Speaks to shoppers with enthusiasm. Names products and use cases clearly. Conversion-aware but never pushy.',
    hospitality:          'Inviting, sensory, and evocative. Describes experiences and atmosphere. Appeals to taste, comfort, and discovery. Distinctive personality.',
    agency:               'Bold, narrative-driven, distinctive. Shows creative point of view. Confident in expression. Treats the audience as design-literate.',
    legal:                'Precise, formal, and measured. Careful about jurisdictional accuracy. No exaggerated claims. Professional restraint.',
    default:              'Clear, factual, and descriptive. No industry-specific tone. Reads naturally to a general audience.',
    custom:               '',
  };

  const PRESET_LABELS = {
    healthcare:           'Healthcare — warm, accessible, plain language',
    community_nonprofit:  'Community / nonprofit — inclusive, mission-driven',
    professional_services:'Professional services — confident, expert',
    ecommerce:            'E-commerce — energetic, benefit-focused',
    hospitality:          'Hospitality / restaurant — inviting, evocative',
    agency:               'Agency / creative — bold, distinctive',
    legal:                'Legal / financial — precise, formal',
    default:              'Generic — neutral, descriptive',
    custom:               'Custom — write your own',
  };

  /* URL normalization — same forgiving behavior as the core scanner.
     Accepts: kpchc.org, www.kpchc.org, http://kpchc.org, https://kpchc.org/path
     Returns the canonical https:// form. Returns null if it can't be salvaged. */
  function normalizeUrl(input) {
    if (!input) return null;
    let s = String(input).trim();
    if (!s) return null;
    /* If it already has a protocol, leave it */
    if (/^https?:\/\//i.test(s)) return s;
    /* Strip any leading '//' or '/' */
    s = s.replace(/^\/+/, '');
    /* Prepend https:// */
    s = 'https://' + s;
    /* Validate it parses */
    try {
      const u = new URL(s);
      if (!u.hostname || !u.hostname.includes('.')) return null;
      return u.href;
    } catch (_) {
      return null;
    }
  }

  function AltText() {
    const [pageUrl, setPageUrl]       = useState('');
    const [preset, setPreset]         = useState('healthcare');
    const [brandName, setBrandName]   = useState('');
    const [brandIndustry, setBrandIndustry] = useState('');
    const [voiceText, setVoiceText]   = useState('');
    const [keywords, setKeywords]     = useState('');
    const [intent, setIntent]         = useState('balanced');
    const [loading, setLoading]       = useState(false);
    const [error, setError]           = useState(null);
    const [data, setData]             = useState(null);
    const [edits, setEdits]           = useState({});
    const [savedProjects, setSavedProjects] = useState([]);
    const [loadingSaved, setLoadingSaved]   = useState(false);

    /* Mobile responsive: collapse 2-col grids to 1-col below 700px. */
    const [isNarrow, setIsNarrow] = useState(
      typeof window !== 'undefined' && window.matchMedia('(max-width: 700px)').matches
    );
    useEffect(() => {
      const mq = window.matchMedia('(max-width: 700px)');
      const handler = e => setIsNarrow(e.matches);
      mq.addEventListener('change', handler);
      return () => mq.removeEventListener('change', handler);
    }, []);

    /* On mount, fetch saved projects so user can load previous runs (v1.0.5) */
    useEffect(() => {
      let cancelled = false;
      (async () => {
        const token = window.WPSB && window.WPSB.getToken && window.WPSB.getToken();
        if (!token) return;
        try {
          const r = await fetch(RAILWAY + '/sa/alt-text/projects', {
            headers: { 'Authorization': 'Bearer ' + token }
          });
          if (!r.ok) return;
          const j = await r.json();
          if (cancelled) return;
          setSavedProjects(j.projects || []);
        } catch (_) { /* silent — feature is enhancement, not critical */ }
      })();
      return () => { cancelled = true; };
    }, []);

    async function loadSavedProject(projectId) {
      const token = window.WPSB && window.WPSB.getToken && window.WPSB.getToken();
      if (!token) { setError('Not authenticated.'); return; }
      setLoadingSaved(true);
      setError(null);
      try {
        const r = await fetch(RAILWAY + '/sa/alt-text/projects/' + projectId, {
          headers: { 'Authorization': 'Bearer ' + token }
        });
        const j = await r.json();
        if (!r.ok) throw new Error(j.error || ('HTTP ' + r.status));
        const p = j.project;
        /* Reconstruct the data shape that renderResults expects */
        setData({
          ok: true,
          page: { url: p.page_url, h1: p.page_h1 },
          applied_seo_context: { target_keywords: p.applied_keywords || [], source: 'saved' },
          intent: p.intent,
          results: p.results || [],
          summary: {
            total: p.image_count,
            generated: p.generated_count,
            skipped: (p.results || []).filter(r => r.flags?.some(f => /skipped/.test(f))).length,
            failed:  (p.results || []).filter(r => r.flags?.some(f => /failed|error/.test(f))).length,
            flagged: (p.results || []).filter(r => r.flags?.some(f => /over_length|too_short|starts_with_image_of/.test(f))).length,
            truncated: false,
          },
        });
        setEdits({});
        /* Repopulate the form so user knows what config this run used */
        setPageUrl(p.page_url || '');
        setIntent(p.intent || 'balanced');
        setBrandName(p.brand_name || '');
        setVoiceText(p.brand_voice || '');
        setKeywords((p.applied_keywords || []).join(', '));
      } catch (e) {
        setError('Could not load saved run: ' + e.message);
      } finally {
        setLoadingSaved(false);
      }
    }

    function resolveVoice() {
      const userText = voiceText.trim();
      if (preset === 'custom') return userText;
      const presetText = VOICE_PRESETS[preset] || '';
      return userText ? presetText + ' ' + userText : presetText;
    }

    async function run() {
      setError(null);
      setData(null);
      setEdits({});

      const normalizedUrl = normalizeUrl(pageUrl);
      if (!normalizedUrl) { setError('Page URL is required. Examples that work: kpchc.org, www.kpchc.org, https://kpchc.org/about'); return; }

      const token = window.WPSB && window.WPSB.getToken && window.WPSB.getToken();
      if (!token) { setError('Not authenticated. Refresh the page after logging in.'); return; }

      setLoading(true);
      try {
        const body = {
          page_url: normalizedUrl,
          intent,
          brand_context: {
            name: brandName.trim() || null,
            voice: resolveVoice() || null,
            industry: brandIndustry.trim() || null,
          },
          seo_context: {
            target_keywords: keywords.split(',').map(s => s.trim()).filter(Boolean),
          },
        };
        const r = await fetch(RAILWAY + '/sa/alt-text/process-page', {
          method: 'POST',
          headers: { 'Authorization': 'Bearer ' + token, 'Content-Type': 'application/json' },
          body: JSON.stringify(body),
        });
        const j = await r.json();
        if (!r.ok) {
          /* Server returns { error, detail } on failures — surface both so the
             user can copy/paste the actual cause when reporting. */
          const msg = j.error || ('HTTP ' + r.status);
          throw new Error(j.detail ? msg + ' — ' + j.detail : msg);
        }
        if (!j.ok) throw new Error(j.error || 'Unknown error');
        setData(j);

        /* v1.0.5: refresh saved-projects list so the just-saved run appears in the picker */
        try {
          const listR = await fetch(RAILWAY + '/sa/alt-text/projects', {
            headers: { 'Authorization': 'Bearer ' + token }
          });
          if (listR.ok) {
            const listJ = await listR.json();
            setSavedProjects(listJ.projects || []);
          }
        } catch (_) { /* silent */ }
      } catch (e) {
        setError(e.message);
      } finally {
        setLoading(false);
      }
    }

    function exportCSV() {
      if (!data?.results) return;
      const headers = ['image_url', 'filename', 'existing_alt', 'suggested_alt', 'flags', 'functional_role'];
      const rows = [headers.join(',')];
      data.results.forEach((r, i) => {
        const finalAlt = (edits[i] != null) ? edits[i] : (r.suggested_alt || '');
        const row = [
          r.image_url, r.filename || '', r.existing_alt || '',
          finalAlt, (r.flags || []).join(';'), r.functional_role || ''
        ].map(v => '"' + String(v).replace(/"/g, '""') + '"').join(',');
        rows.push(row);
      });
      downloadBlob(rows.join('\n'), 'text/csv;charset=utf-8;', 'csv');
    }

    function exportJSON() {
      if (!data?.results) return;
      const merged = data.results.map((r, i) => ({
        ...r,
        suggested_alt: (edits[i] != null) ? edits[i] : r.suggested_alt
      }));
      const out = {
        page: data.page,
        applied_seo_context: data.applied_seo_context,
        results: merged,
        exported_at: new Date().toISOString(),
      };
      downloadBlob(JSON.stringify(out, null, 2), 'application/json', 'json');
    }

    function downloadBlob(content, mime, ext) {
      const blob = new Blob([content], { type: mime });
      const url = URL.createObjectURL(blob);
      const a = document.createElement('a');
      a.href = url;
      const slug = (data?.page?.url || 'export').replace(/^https?:\/\//, '').replace(/[^a-z0-9]/gi, '-').slice(0, 40);
      a.download = 'alt-text-' + slug + '-' + Date.now() + '.' + ext;
      document.body.appendChild(a); a.click(); document.body.removeChild(a);
      URL.revokeObjectURL(url);
    }

    function flagClass(flag) {
      if (/skipped/.test(flag)) return 'skip';
      if (/error|failed/.test(flag)) return 'error';
      if (/over_length|too_short|starts_with/.test(flag)) return 'warn';
      return 'ok';
    }

    function clearAll() {
      setData(null); setError(null); setEdits({});
    }

    /* Width fix 2026-05-18 — removed `padding:20px 24px, maxWidth:1200`
       to match Brand Profile / Account / Billing standard width.
       The .main grid container handles overall width + padding. */
    return (
      <div>
        <div style={{ marginBottom: 18, display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between', gap: 12 }}>
          <div>
            <h1 style={{ fontSize: '1.5rem', fontWeight: 700, margin: 0, color: 'var(--text)' }}>Alt Text Generator</h1>
            <div style={{ fontSize: '.85rem', color: 'var(--dim)', marginTop: 4 }}>
              Vision-AI alt text for any page · ADA + SEO + brand voice · CSV export.
            </div>
          </div>
          {/* Brain AI panel-help hook (Design Standards #6 — structural reserve).
              Wires to Brain AI scoped to this panel's docs when kb_articles has
              the "Alt Text Generator" article. Currently shows a tooltip-only
              affordance with no action so the UI accommodates it without breaking. */}
          <button
            type="button"
            title="Panel help — coming soon"
            aria-label="Panel help"
            style={{
              flexShrink: 0,
              width: 32, height: 32, padding: 0,
              background: 'transparent',
              color: 'var(--dim)',
              border: '1px solid var(--border)',
              borderRadius: '50%',
              fontFamily: 'inherit', fontSize: '.95rem', fontWeight: 600,
              cursor: 'default',
              opacity: 0.5,
            }}
          >?</button>
        </div>

        {/* Previous runs picker (v1.0.5) — show only if any saved projects exist */}
        {savedProjects.length > 0 && (
          <div style={{
            background: 'rgba(0,207,239,0.06)',
            border: '1px solid rgba(0,207,239,0.25)',
            borderRadius: 8, padding: '10px 14px', marginBottom: 14,
            fontSize: '.82rem', color: 'var(--text)'
          }}>
            <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', flexWrap: 'wrap', gap: 8 }}>
              <div>
                <strong style={{ color: 'var(--beam, #00cfef)' }}>Previous runs ({savedProjects.length}):</strong>{' '}
                <span style={{ color: 'var(--dim)', fontSize: '.74rem' }}>
                  click to load any prior alt-text run instead of regenerating (no AI cost)
                </span>
              </div>
            </div>
            <div style={{ display: 'flex', flexWrap: 'wrap', gap: 6, marginTop: 8 }}>
              {savedProjects.slice(0, 10).map(p => (
                <button
                  key={p.id}
                  type="button"
                  onClick={() => loadSavedProject(p.id)}
                  disabled={loadingSaved}
                  style={{
                    padding: '4px 10px',
                    fontSize: '.74rem',
                    background: 'rgba(0,207,239,0.12)',
                    color: 'var(--text)',
                    border: '1px solid rgba(0,207,239,0.3)',
                    borderRadius: 11,
                    cursor: loadingSaved ? 'not-allowed' : 'pointer',
                    fontFamily: 'inherit',
                    opacity: loadingSaved ? 0.5 : 1,
                  }}
                  title={`${p.image_count || 0} images, ${p.generated_count || 0} generated · ${new Date(p.updated_at).toLocaleString()}`}
                >
                  {(() => {
                    try {
                      const u = new URL(p.page_url);
                      return u.hostname + (u.pathname === '/' ? '' : u.pathname);
                    } catch (_) { return p.page_url; }
                  })()}
                </button>
              ))}
              {savedProjects.length > 10 && (
                <span style={{ color: 'var(--dim)', fontSize: '.72rem', alignSelf: 'center' }}>
                  +{savedProjects.length - 10} more
                </span>
              )}
            </div>
          </div>
        )}

        {/* 1. Page URL */}
        <Card>
          <CardHead>1. Page</CardHead>
          <FieldLabel>Page URL</FieldLabel>
          <input type="text" value={pageUrl} onChange={e => setPageUrl(e.target.value)}
            placeholder="example.com   or   https://www.example.com/page"
            autoComplete="off" style={inputStyle()}/>
        </Card>

        {/* 2. Brand voice */}
        <Card>
          <CardHead>2. Brand voice</CardHead>

          <FieldLabel>Voice preset</FieldLabel>
          <select value={preset} onChange={e => setPreset(e.target.value)} style={inputStyle()}>
            {Object.entries(PRESET_LABELS).map(([k, label]) => (
              <option key={k} value={k}>{label}</option>
            ))}
          </select>

          <div style={{ display: 'grid', gridTemplateColumns: isNarrow ? '1fr' : '1fr 1fr', gap: 14, marginBottom: 4 }}>
            <div>
              <FieldLabel>Brand name <span style={optStyle}>(optional)</span></FieldLabel>
              <input type="text" value={brandName} onChange={e => setBrandName(e.target.value)}
                placeholder="Acme Inc" autoComplete="off" style={inputStyle()}/>
            </div>
            <div>
              <FieldLabel>Industry <span style={optStyle}>(optional)</span></FieldLabel>
              <input type="text" value={brandIndustry} onChange={e => setBrandIndustry(e.target.value)}
                placeholder="professional services" autoComplete="off" style={inputStyle()}/>
            </div>
          </div>

          <FieldLabel>
            Brand voice text <span style={optStyle}>
              {preset === 'custom' ? '(write the voice from scratch)' : '(optional refinement to the preset)'}
            </span>
          </FieldLabel>
          <textarea value={voiceText} onChange={e => setVoiceText(e.target.value)}
            placeholder={preset === 'custom'
              ? 'Write the brand voice in your own words.'
              : 'Optional refinement appended to the preset. Leave blank to use the preset as-is.'}
            style={{ ...inputStyle(), minHeight: 80, resize: 'vertical', fontFamily: 'inherit' }}/>
        </Card>

        {/* 3. SEO keywords */}
        <Card>
          <CardHead>3. SEO keywords</CardHead>

          <FieldLabel>Target keywords <span style={optStyle}>(optional — leave blank to auto-detect)</span></FieldLabel>
          <input type="text" value={keywords} onChange={e => setKeywords(e.target.value)}
            placeholder="e.g. primary keyword, secondary keyword, local term"
            autoComplete="off" style={inputStyle()}/>
          <div style={{ fontSize: '.74rem', color: 'var(--dim)', marginTop: -6, marginBottom: 12 }}>
            If blank, server reads meta keywords, OG title, H1/H2 to derive 2-3 candidates. You'll see what was applied below.
          </div>

          <FieldLabel>Intent mode</FieldLabel>
          <select value={intent} onChange={e => setIntent(e.target.value)} style={inputStyle()}>
            <option value="balanced">Balanced — ADA-first, natural keyword inclusion</option>
            <option value="ada_focus">ADA Focus — pure accessibility, ignore keywords</option>
            <option value="seo_focus">SEO Focus — more aggressive keyword inclusion</option>
          </select>
        </Card>

        {/* Run controls */}
        <div style={{ display: 'flex', gap: 8, marginBottom: 14, flexWrap: 'wrap' }}>
          <button onClick={run} disabled={loading} style={btnPrimary(loading)}>
            {loading ? 'Generating…' : 'Scan page + generate alt text'}
          </button>
          <button onClick={clearAll} style={btnGhost()}>Clear results</button>
        </div>

        {/* Error */}
        {error && (
          <div style={alertStyle('error')}>
            <strong>Failed:</strong> {error}
          </div>
        )}

        {/* Loading hint */}
        {loading && (
          <div style={alertStyle('info')}>
            Scanning page + generating alt text via Claude Sonnet 4.6 vision… 30–60 seconds depending on image count.
          </div>
        )}

        {/* Results */}
        {data && (
          <Card>
            <CardHead>Results — review &amp; edit before export</CardHead>

            {/* Applied context */}
            <div style={{
              padding: '10px 12px',
              background: 'rgba(0,207,239,0.06)',
              border: '1px solid rgba(0,207,239,0.25)',
              borderRadius: 6,
              fontSize: '.78rem', marginBottom: 12, color: 'var(--text)'
            }}>
              <strong style={{ color: 'var(--beam, #00cfef)' }}>Keywords applied:</strong>{' '}
              {(data.applied_seo_context?.target_keywords || []).length === 0 ? (
                <em style={{ color: 'var(--dim)' }}>none</em>
              ) : data.applied_seo_context.target_keywords.map((k, i) => (
                <span key={i} style={{
                  display: 'inline-block', padding: '2px 8px', margin: '2px 4px 2px 0',
                  background: 'rgba(0,207,239,0.15)', borderRadius: 11, fontSize: '.72rem'
                }}>{k}</span>
              ))}
              <span style={{ color: 'var(--dim)', marginLeft: 6 }}>
                ({({
                  explicit: 'from your input',
                  derived:  'auto-detected from page metadata',
                  none:     'none used'
                })[data.applied_seo_context?.source] || data.applied_seo_context?.source})
              </span>
              {data.page?.h1 && (
                <div style={{ marginTop: 6 }}>
                  <strong style={{ color: 'var(--beam, #00cfef)' }}>Page H1:</strong> {data.page.h1}
                </div>
              )}
            </div>

            {/* Summary */}
            <div style={{
              display: 'flex', flexWrap: 'wrap', gap: 16,
              padding: 12,
              background: 'rgba(0,0,0,0.12)',
              border: '1px solid var(--border)',
              borderRadius: 6, marginBottom: 12,
              fontSize: '.85rem', color: 'var(--text)'
            }}>
              <Stat label="total" value={data.summary.total}/>
              <Stat label="generated" value={data.summary.generated}/>
              <Stat label="skipped" value={data.summary.skipped}/>
              <Stat label="failed" value={data.summary.failed}/>
              <Stat label="flagged" value={data.summary.flagged}/>
              {data.summary.truncated && (
                <div style={{ color: 'var(--warn, #f0a830)' }}>⚠ truncated at 50</div>
              )}
            </div>

            {/* Results table */}
            <div style={{ overflowX: 'auto' }}>
              <table style={{ width: '100%', borderCollapse: 'collapse', fontSize: '.82rem' }}>
                <thead>
                  <tr>
                    <Th style={{ width: 90 }}>Image</Th>
                    <Th>Filename / URL</Th>
                    <Th>Suggested alt text (editable)</Th>
                    <Th>Flags</Th>
                  </tr>
                </thead>
                <tbody>
                  {data.results.map((r, i) => {
                    const currentValue = (edits[i] != null) ? edits[i] : (r.suggested_alt || '');
                    return (
                      <tr key={i} style={{ borderBottom: '1px solid var(--border)' }}>
                        <Td>
                          <img src={r.image_url} alt="" onError={e => e.target.style.opacity = 0.3}
                            style={{ maxWidth: 80, maxHeight: 60, borderRadius: 4, display: 'block' }}/>
                        </Td>
                        <Td style={{ fontSize: '.68rem', color: 'var(--dim)', wordBreak: 'break-all', maxWidth: 200 }}>
                          <div style={{ color: 'var(--text)' }}>{r.filename || ''}</div>
                          <div style={{ opacity: 0.7, fontSize: '.62rem' }}>{r.image_url}</div>
                          {r.existing_alt && (
                            <div style={{ marginTop: 4, opacity: 0.8 }}>
                              <strong>existing:</strong> {r.existing_alt}
                            </div>
                          )}
                        </Td>
                        <Td>
                          <input type="text" value={currentValue}
                            onChange={e => setEdits({ ...edits, [i]: e.target.value })}
                            style={tableInputStyle()}/>
                        </Td>
                        <Td>
                          {(r.flags && r.flags.length > 0) ? r.flags.map((f, j) => (
                            <FlagBadge key={j} kind={flagClass(f)}>{f}</FlagBadge>
                          )) : <FlagBadge kind="ok">clean</FlagBadge>}
                          {r.reason && (
                            <div style={{
                              marginTop: 6, fontSize: '.68rem',
                              color: 'var(--dim)', maxWidth: 240,
                              wordBreak: 'break-word', lineHeight: 1.4
                            }}>{r.reason}</div>
                          )}
                        </Td>
                      </tr>
                    );
                  })}
                </tbody>
              </table>
            </div>

            <div style={{ display: 'flex', gap: 8, marginTop: 14, flexWrap: 'wrap' }}>
              <button onClick={exportCSV} style={btnPrimary(false)}>Export CSV</button>
              <button onClick={exportJSON} style={btnGhost()}>Export JSON</button>
              {/* 2026-05-19 T2.2 integration: apply generated alt text directly
                  to the WP site via plugin write-back. site_url derived from
                  data.page.url (the scanned page). Plugin handler resolves
                  each media_url → attachment ID, updates _wp_attachment_image_alt
                  meta. Only includes results with non-empty alt text. */}
              {window.QueueOperationButton && data?.page?.url && (
                React.createElement(window.QueueOperationButton, {
                  operationType: 'alt_text_apply',
                  payload: {
                    items: data.results
                      .map((r, i) => ({
                        media_url: r.image_url,
                        alt_text: (edits[i] != null) ? edits[i] : (r.suggested_alt || ''),
                      }))
                      .filter(it => it.alt_text && it.alt_text.trim().length > 0),
                  },
                  siteUrl: (() => {
                    try {
                      const u = new URL(data.page.url);
                      return `${u.protocol}//${u.hostname}`;
                    } catch(e) { return ''; }
                  })(),
                  label: `Apply ${data.results.filter((r, i) => {
                    const alt = (edits[i] != null) ? edits[i] : (r.suggested_alt || '');
                    return alt && alt.trim().length > 0;
                  }).length} alt texts to WP`,
                })
              )}
            </div>
          </Card>
        )}

        {/* Notes */}
        <div style={{
          background: 'var(--panel)', border: '1px solid var(--border)', borderRadius: 10,
          padding: 18, marginTop: 14, fontSize: '.82rem', color: 'var(--dim)', lineHeight: 1.7
        }}>
          <div style={{ fontSize: '.92rem', fontWeight: 600, marginBottom: 8, color: 'var(--text)' }}>Notes</div>
          <ul style={{ paddingLeft: 18, margin: 0 }}>
            <li>Max 50 images per page run. Pages with more will be truncated.</li>
            <li>Decorative images (role=presentation, aria-hidden) are skipped without AI spend.</li>
            <li>Each generated alt text costs ~$0.005–$0.008 in vision API spend. Tracked under <code style={codeStyle}>module='alt_text_gen'</code> in the AI Usage dashboard.</li>
            <li>To apply CSV to WordPress: use a bulk-edit plugin (e.g. <em>Bulk Image Title and Alt Text Editor</em>) or wp-cli.</li>
            <li>Or click <strong>Apply N alt texts to WP</strong> above to push directly via WPSiteBeam plugin write-back (requires plugin v1.7.0+ installed on the target site).</li>
          </ul>
        </div>
      </div>
    );
  }

  /* ────────────────────────── Helper components ────────────────────────── */

  function Card({ children }) {
    return (
      <div style={{
        background: 'var(--panel)', border: '1px solid var(--border)',
        borderRadius: 10, padding: 18, marginBottom: 14
      }}>{children}</div>
    );
  }

  function CardHead({ children }) {
    return (
      <div style={{
        fontSize: '.92rem', fontWeight: 600, marginBottom: 12,
        color: 'var(--accent, var(--beam, #f0a830))'
      }}>{children}</div>
    );
  }

  function FieldLabel({ children }) {
    return (
      <div style={{
        fontSize: '.72rem', fontWeight: 600, textTransform: 'uppercase',
        letterSpacing: '.06em', color: 'var(--dim)', marginBottom: 6
      }}>{children}</div>
    );
  }

  function Stat({ label, value }) {
    return (
      <div style={{ color: 'var(--dim)' }}>
        <strong style={{ color: 'var(--beam, #00cfef)' }}>{value}</strong> {label}
      </div>
    );
  }

  function Th({ children, style }) {
    return (
      <th style={{
        textAlign: 'left', padding: '10px 8px', fontSize: '.7rem',
        textTransform: 'uppercase', letterSpacing: '.06em',
        color: 'var(--dim)', borderBottom: '1px solid var(--border)',
        background: 'rgba(0,0,0,0.08)', ...style
      }}>{children}</th>
    );
  }

  function Td({ children, style }) {
    return (
      <td style={{ padding: '10px 8px', verticalAlign: 'top', ...style }}>{children}</td>
    );
  }

  function FlagBadge({ children, kind }) {
    const colors = {
      skip:  { bg: 'rgba(150,150,150,0.2)', fg: 'var(--dim)' },
      error: { bg: 'rgba(255,80,80,0.2)',   fg: 'var(--red, #ff8080)' },
      warn:  { bg: 'rgba(240,168,48,0.2)',  fg: 'var(--warn, #f0a830)' },
      ok:    { bg: 'rgba(80,255,150,0.15)', fg: 'var(--green, #80ffa0)' },
    }[kind] || { bg: 'rgba(150,150,150,0.2)', fg: 'var(--dim)' };
    return (
      <span style={{
        display: 'inline-block', padding: '2px 6px', borderRadius: 3,
        fontSize: '.62rem', fontWeight: 600,
        marginRight: 4, marginBottom: 2,
        background: colors.bg, color: colors.fg
      }}>{children}</span>
    );
  }

  /* ────────────────────────── Helper styles ────────────────────────── */

  function inputStyle() {
    return {
      width: '100%', padding: '10px 12px',
      background: 'rgba(0,0,0,0.18)',
      border: '1px solid var(--border)',
      borderRadius: 6, color: 'var(--text)', fontSize: '.85rem',
      fontFamily: 'inherit', marginBottom: 14, boxSizing: 'border-box',
      outline: 'none',
    };
  }
  function tableInputStyle() {
    return {
      width: '100%', padding: '6px 8px', fontSize: '.82rem',
      background: 'rgba(0,0,0,0.15)',
      border: '1px solid var(--border)', borderRadius: 4,
      color: 'var(--text)', fontFamily: 'inherit', outline: 'none',
    };
  }
  const optStyle = { color: 'var(--dim)', fontWeight: 400, textTransform: 'none', letterSpacing: 0 };
  const codeStyle = {
    fontFamily: 'var(--font-mono, monospace)', fontSize: '.78rem',
    background: 'rgba(0,0,0,0.18)', padding: '1px 5px', borderRadius: 3,
    color: 'var(--text)'
  };

  function btnPrimary(disabled) {
    return {
      padding: '10px 18px',
      background: 'var(--accent, #f0a830)',
      color: 'var(--accent-fg, #1a0f00)',
      border: 'none', borderRadius: 6,
      fontWeight: 600, fontSize: '.9rem',
      cursor: disabled ? 'not-allowed' : 'pointer',
      opacity: disabled ? 0.5 : 1, fontFamily: 'inherit',
    };
  }
  /* Ghost button — transparent bg + theme border + theme text — adapts to dark/light */
  function btnGhost() {
    return {
      padding: '10px 18px',
      background: 'transparent',
      color: 'var(--text)',
      border: '1px solid var(--border)',
      borderRadius: 6, fontWeight: 600, fontSize: '.9rem',
      cursor: 'pointer', fontFamily: 'inherit',
    };
  }

  function alertStyle(kind) {
    const styles = {
      info:  { bg: 'rgba(0,207,239,0.10)', border: 'rgba(0,207,239,0.30)', fg: 'var(--beam, #00cfef)' },
      error: { bg: 'rgba(255,80,80,0.10)', border: 'rgba(255,80,80,0.30)', fg: 'var(--red, #ff5050)' },
      ok:    { bg: 'rgba(80,255,150,0.10)', border: 'rgba(80,255,150,0.30)', fg: 'var(--green, #4ec9b0)' },
    }[kind] || { bg: 'transparent', border: 'var(--border)', fg: 'var(--text)' };
    return {
      padding: '12px 14px', borderRadius: 6, marginBottom: 14, fontSize: '.85rem',
      background: styles.bg, border: '1px solid ' + styles.border, color: styles.fg,
    };
  }

  window.AltText = AltText;
  console.log('[WPSB] AltText v1.0.5 loaded (SA Alt Text generator panel)');
})();
