const { useState, useMemo, useEffect, useRef } = React;

const LIVE_SOURCES = ['foreplay', 'adspy', 'brandsearch'];
const MANUAL_SEARCH_ALL_COMPETITORS_ID = '__all_competitors__';
const AR_TARGET_MIN = 1;
const AR_TARGET_MAX = window.RESILIA_LIMITS?.autoresearchPerDirectionMax || 1000;
const SEARCH_ADS_PER_DIRECTION_MAX = window.RESILIA_LIMITS?.searchAdsPerDirectionMax || 10000;
const SEARCH_ADS_PER_PLATFORM_MAX = window.RESILIA_LIMITS?.searchAdsPerPlatformMax || 100000;
const AUTORESEARCH_PAGE_SIZE = 30;
const AUTORESEARCH_FAIL_DISPLAY_LIMIT = 500;
const AUTO_REMODEL_COUNT_MAX = window.RESILIA_LIMITS?.autoRemodelCountMax || 10000;
const AUTO_REMODEL_PAGE_SIZE = 20;
const AUTO_REMODEL_PREVIOUS_PAGE_SIZE = 10;
const PIPELINE_PAGE_SIZE = 20;
// Background pollers: 12s cadence matches the user's "ads come winning after
// ~10s" expectation while keeping DB pool pressure low. Pollers are also
// gated on document.hidden so a backgrounded tab does not hammer the API.
const AR_POLL_INTERVAL_MS = 12000;
const AR_ACTIVE_POLL_INTERVAL_MS = 5000;
const AR_PREFILL_POLL_INTERVAL_MS = 2000;
const REMODEL_POLL_INTERVAL_MS = 12000;
const FILTER_POLL_INTERVAL_MS = 12000;
const COMPLIANCE_POLL_INTERVAL_MS = 12000;
const TAB_STORAGE_KEY = 'resilia.dashboard.activeTab';
const showAutoRemodelUI = () => window.RESILIA_UI?.showAutoRemodel === true;
const normalizeDashboardTab = (nextTab, fallback = 'research') => {
  const resolved = DASHBOARD_TABS.has(nextTab) ? nextTab : fallback;
  if (resolved === 'autoRemodel' && !showAutoRemodelUI()) return 'autoSearch';
  return resolved;
};
const ACTIVE_SEARCH_RUNS_STORAGE_KEY = 'resilia.dashboard.activeSearchRuns';
const REMODEL_STRATEGY_MAX = 4000;
const REMODEL_VARIANTS_MAX = 10;
const REMODEL_METHODS = [
  { id: 'updated', label: 'Updated', short: 'U', defaultTemperature: 0.5 },
  { id: 'traditional', label: 'Traditional', short: 'T', defaultTemperature: 0.5 },
  { id: 'hybrid', label: 'Hybrid', short: 'H', defaultTemperature: 0.5 },
];
const REMODEL_METHOD_BY_ID = Object.fromEntries(REMODEL_METHODS.map(method => [method.id, method]));
const normalizeRemodelMethod = (value) => (REMODEL_METHOD_BY_ID[value] ? value : 'updated');
const remodelMethodMeta = (value) => REMODEL_METHOD_BY_ID[normalizeRemodelMethod(value)];
const clampRemodelTemperature = (value, fallback = 0.5) => {
  if (value === null || value === undefined || value === '') return fallback;
  const n = Number(value);
  return Math.max(0, Math.min(1, Number.isFinite(n) ? n : fallback));
};
const formatRemodelTemperature = (value) => clampRemodelTemperature(value).toFixed(2);
const remodelPlanRow = (method = 'updated', temperature = null) => {
  const meta = remodelMethodMeta(method);
  return {
    remodelMethod: meta.id,
    temperature: clampRemodelTemperature(
      temperature,
      meta.defaultTemperature,
    ),
  };
};
const remodelPlanFromVariants = (variants = []) => {
  const rows = (variants || []).map(v => remodelPlanRow(v.remodelMethod, v.temperature ?? 0.5));
  return rows.length ? rows.slice(0, REMODEL_VARIANTS_MAX) : [remodelPlanRow()];
};
const remodelVariantLabel = (variant, fallbackIndex = 0) => {
  const meta = remodelMethodMeta(variant?.remodelMethod);
  const temp = variant?.temperature;
  const tempLabel = temp === null || temp === undefined ? '' : ` · temp ${formatRemodelTemperature(temp)}`;
  return `Variant ${fallbackIndex + 1} · ${meta.short}${tempLabel}`;
};
const remodelPendingEntryForPlan = (variantPlan = [remodelPlanRow()]) => ({
  status: 'pending',
  expectedVariants: variantPlan.length,
  variants: variantPlan.map((row, index) => ({
    jobId: null,
    variantIdx: index + 1,
    pending: true,
    raw: '',
    lines: [{ tag: 'REMODELING', body: 'Claude Sonnet is generating this remodel output.' }],
    remodelMethod: normalizeRemodelMethod(row.remodelMethod),
    temperature: clampRemodelTemperature(row.temperature),
    promotedAt: null,
  })),
});
const DASHBOARD_TABS = new Set(['staticAds', 'autoRemodel', 'autoSearch', 'research', 'filter', 'remodel', 'compliance', 'video', 'accessRequests', 'adminDirections', 'adminWinningCriteria', 'adminGlobalCrawler', 'adminCosts', 'adminEvaluationQuotas', 'adminWorkspaces', 'adminUsers']);
const STATIC_ADS_POLL_INTERVAL_MS = 12000;
const passThresholdForAd = (ad) => {
  const threshold = Number(ad?.passThreshold ?? ad?.pass_threshold ?? ad?.scoreThreshold ?? ad?.score_threshold);
  return Number.isFinite(threshold) ? threshold : null;
};
const sourcePlatformForAd = (ad) => {
  const raw = ad?.raw && typeof ad.raw === 'object' ? ad.raw : {};
  const platform = String(
    ad?.sourcePlatform
    || ad?.source_platform
    || raw.source_platform
    || raw.source
    || ad?.platform
    || 'foreplay',
  ).trim().toLowerCase();
  return ['foreplay', 'adspy', 'brandsearch'].includes(platform) ? platform : 'foreplay';
};
const adMeetsPassThreshold = (ad, unknownFallback = false) => {
  if (typeof ad?.passes === 'boolean') return ad.passes;
  const score = Number(ad?.score);
  const threshold = passThresholdForAd(ad);
  if (Number.isFinite(score) && threshold !== null) return score >= threshold;
  return unknownFallback;
};
const savedDashboardTab = () => {
  try {
    const saved = window.localStorage.getItem(TAB_STORAGE_KEY);
    return saved && DASHBOARD_TABS.has(saved) ? normalizeDashboardTab(saved) : null;
  } catch {
    return null;
  }
};
const boundedPositiveIntOrNull = (value, max = AR_TARGET_MAX) => {
  if (value === null || value === undefined || value === '') return null;
  const n = Number(value);
  if (!Number.isFinite(n)) return null;
  return Math.max(AR_TARGET_MIN, Math.min(max, Math.floor(n)));
};
const schedulerClockIntOrNull = (value, max) => {
  if (value === null || value === undefined || value === '') return null;
  const n = Number(value);
  if (!Number.isFinite(n)) return null;
  return Math.max(0, Math.min(max, Math.floor(n)));
};
const sanitizeSchedulerClockInput = (value, max) => {
  if (value === '') return '';
  const n = schedulerClockIntOrNull(value, max);
  return n === null ? '' : String(n);
};
const SCHEDULER_TIMEZONE_OPTIONS = [
  'Asia/Karachi',
  'UTC',
  'America/New_York',
  'America/Los_Angeles',
  'Europe/London',
  'Asia/Dubai',
  'Asia/Singapore',
];
const COST_FLOW_ORDER = ['search', 'autoresearch', 'auto_remodel'];
const COST_FLOW_LABELS = {
  search: 'Manual Search',
  autoresearch: 'AutoSearch',
  auto_remodel: 'Auto-Remodel',
};
const DEDUPE_DEFAULTS = { scope_mode: 'platform_search_type', match_strategy: 'exact' };
const DEDUPE_SCOPE_LABELS = {
  platform_search_type: 'Both: platform + search type',
  platform: 'Platform only',
  search_type: 'Search type only',
  overall: 'Everywhere',
};
const costMoney = (value) => new Intl.NumberFormat('en-US', {
  style: 'currency', currency: 'USD', minimumFractionDigits: 2, maximumFractionDigits: 4,
}).format(Number(value || 0));
const compactNumber = (value) => new Intl.NumberFormat('en-US').format(Number(value || 0));
const sanitizeArTargetInput = (value, max = AR_TARGET_MAX) => {
  if (value === '') return '';
  const n = boundedPositiveIntOrNull(value, max);
  return n === null ? '' : String(n);
};
const resolveAutoresearchProgressMeta = (runState, summary, { looksComplete, useRunState }) => {
  const prefilledCount = Number(runState?.prefilled_count || runState?.result_count || 0);
  const remainingTarget = Number(runState?.remaining_target || 0);
  const runPhase = runState?.run_phase || null;
  const poolOnlyComplete = Boolean(
    runState?.shared_pool_prefill_only
    || runPhase === 'shared_pool_ready'
    || runState?.status === 'done'
  );
  if (looksComplete || poolOnlyComplete) {
    return {
      crawlState: 'done',
      runPhase: runPhase || 'done',
      prefilledCount,
      remainingTarget,
    };
  }
  if (runPhase) {
    return { crawlState: runPhase, runPhase, prefilledCount, remainingTarget };
  }
  const crawlState = useRunState
    ? (runState?.crawl_status || runState?.job_status || runState?.status || 'running')
    : summary?.crawl_state;
  return { crawlState, runPhase: crawlState, prefilledCount, remainingTarget };
};
const sourceState = (selected = ['foreplay']) => {
  const active = new Set(Array.isArray(selected) ? selected : [selected]);
  return {
    foreplay: active.has('foreplay'),
    adspy: active.has('adspy'),
    brandsearch: active.has('brandsearch'),
    metaads: false,
  };
};
const allSourceState = () => sourceState(LIVE_SOURCES);
const resultKey = (ad) => (
  ad?.resultUuid
  || ad?.result_uuid
  || ad?.cardKey
  || [ad?._arRunId, ad?._arBatchId, ad?.searchRunId, ad?.searchBatchId, ad?.runId, ad?.id]
    .filter(Boolean)
    .join(':')
);
const stableRandomRank = (value, seed = '') => {
  const text = `${seed}:${value || ''}`;
  let hash = 2166136261;
  for (let i = 0; i < text.length; i += 1) {
    hash ^= text.charCodeAt(i);
    hash = Math.imul(hash, 16777619);
  }
  return hash >>> 0;
};
const normalizedCreativeKey = (ad) => {
  const raw = ad?.creativeUrl
    || ad?.creative_url
    || ad?.raw?.creative_url
    || ad?.raw?.creativeUrl
    || ad?.raw?.video_url
    || ad?.raw?.videoUrl
    || ad?.thumbnailUrl
    || ad?.thumbnail_url
    || ad?.thumb
    || ad?.raw?.thumbnail_url
    || ad?.raw?.thumbnail
    || ad?.raw?.thumb_url
    || ad?.raw?.image_url
    || ad?.raw?.image
    || ad?.url
    || null;
  if (!raw || typeof raw !== 'string') return null;
  try {
    const base = typeof window !== 'undefined' ? window.location.origin : 'https://resilia.local';
    const parsed = new URL(raw, base);
    [...parsed.searchParams.keys()].forEach(key => {
      const lower = key.toLowerCase();
      if (lower.startsWith('utm_') || ['fbclid', 'gclid', 'msclkid'].includes(lower)) {
        parsed.searchParams.delete(key);
      }
    });
    parsed.hash = '';
    return parsed.href.toLowerCase();
  } catch {
    return raw.trim().toLowerCase();
  }
};
const transcriptTextKey = (ad) => {
  const explicitHash = ad?.transcriptHash
    || ad?.transcript_hash
    || ad?.raw?.transcript_hash
    || null;
  if (explicitHash) return String(explicitHash).trim().toLowerCase();
  const text = ad?.transcriptRaw
    || ad?.full_transcription
    || ad?.raw?.full_transcription
    || ad?.raw?.transcript
    || ad?.raw?.transcription
    || (typeof ad?.transcript === 'string' ? ad.transcript : null);
  if (!text || typeof text !== 'string') return null;
  return text.replace(/\s+/g, ' ').trim().toLowerCase() || null;
};
const visibleCopyKey = (ad) => {
  const text = ad?.copy
    || ad?.raw?.copy
    || ad?.raw?.text
    || ad?.raw?.body
    || ad?.raw?.title
    || null;
  if (!text || typeof text !== 'string') return null;
  const brand = ad?.brand || ad?.raw?.brand || ad?.raw?.page_name || ad?.raw?.name || '';
  const normalized = text.replace(/\s+/g, ' ').trim().toLowerCase();
  return normalized ? `${String(brand).trim().toLowerCase()}:${normalized}` : null;
};
const campaignInstanceKey = (ad) => {
  const raw = ad?.raw && typeof ad.raw === 'object' ? ad.raw : {};
  const source = ad?.sourcePlatform || ad?.source_platform || raw.source_platform || raw.source || ad?.platform || '';
  const brand = ad?.brand || raw.brand || raw.page_name || raw.name || '';
  const publisher = ad?.publisherPlatform || ad?.publisher_platform || raw.publisher_platform || raw.platform || '';
  const started = ad?.startedRunningMs
    || ad?.started_running_ms
    || raw.started_running_ms
    || raw.started_running
    || null;
  const link = ad?.linkUrl || ad?.link_url || raw.link_url || raw.url || '';
  if (!source || !brand || !started || !link) return null;
  return [
    String(source).trim().toLowerCase(),
    String(brand).trim().toLowerCase(),
    String(publisher).trim().toLowerCase(),
    String(started),
    String(link).trim().toLowerCase(),
  ].join(':');
};
const autoresearchDisplayKey = (ad) => {
  const campaign = campaignInstanceKey(ad);
  if (campaign) return `campaign:${campaign}`;
  const creative = normalizedCreativeKey(ad);
  if (creative) return `creative:${creative}`;
  const transcript = transcriptTextKey(ad);
  if (transcript) return `transcript:${transcript}`;
  const copy = visibleCopyKey(ad);
  if (copy) return `copy:${copy}`;
  return resultKey(ad) || ad?.id;
};
const uniqueAutoresearchAds = (items) => {
  const seen = new Set();
  const out = [];
  for (const ad of items || []) {
    const key = autoresearchDisplayKey(ad);
    if (seen.has(key)) continue;
    seen.add(key);
    out.push(ad);
  }
  return out;
};
const uniqueSearchAds = (items) => {
  const seen = new Set();
  const out = [];
  for (const ad of items || []) {
    const key = autoresearchDisplayKey(ad);
    if (seen.has(key)) continue;
    seen.add(key);
    out.push(ad);
  }
  return out;
};
const autoRemodelDisplayKey = (ad) => autoresearchDisplayKey(ad);
const normaliseDedupeSettings = (settings = {}) => ({
  scope_mode: DEDUPE_SCOPE_LABELS[settings?.scope_mode] ? settings.scope_mode : DEDUPE_DEFAULTS.scope_mode,
  match_strategy: DEDUPE_DEFAULTS.match_strategy,
});
const normaliseDedupePlatform = (value) => {
  if (Array.isArray(value)) {
    return [...new Set(value.map(normaliseDedupePlatform).filter(Boolean))].sort().join(',');
  }
  if (value === null || value === undefined) return null;
  const parts = String(value)
    .split(/[,/|]/)
    .map(part => part.trim().toLowerCase().replace(/\s+/g, '_'))
    .filter(Boolean);
  return parts.length ? [...new Set(parts)].sort().join(',') : null;
};
const autoRemodelPublisherPlatform = (ad) => {
  const raw = ad?.raw && typeof ad.raw === 'object' ? ad.raw : {};
  const source = normaliseDedupePlatform(ad?.sourcePlatform || ad?.source_platform || ad?.source || raw.source_platform || raw.source);
  return normaliseDedupePlatform(
    ad?.publisherPlatform
    || ad?.publisher_platform
    || ad?.platform
    || raw.publisher_platform
    || raw.platform,
  ) || source || 'unknown';
};
const autoRemodelDedupeScopePrefix = (ad, settings = DEDUPE_DEFAULTS) => {
  const resolved = normaliseDedupeSettings(settings);
  const platform = autoRemodelPublisherPlatform(ad);
  if (resolved.scope_mode === 'platform_search_type') return `platform_search_type:${platform}:auto_remodel`;
  if (resolved.scope_mode === 'platform') return `platform:${platform}`;
  if (resolved.scope_mode === 'search_type') return 'search_type:auto_remodel';
  return 'overall';
};
const autoRemodelDedupeKeys = (ad, settings = DEDUPE_DEFAULTS) => {
  const resolved = normaliseDedupeSettings(settings);
  const scope = autoRemodelDedupeScopePrefix(ad, resolved);
  const creative = normalizedCreativeKey(ad);
  const transcript = transcriptTextKey(ad);
  const keys = [];
  if (creative) keys.push(`${scope}:url:${creative}`);
  if (transcript && !creative) {
    keys.push(`${scope}:transcript:${transcript}`);
  }
  if (!keys.length) keys.push(`display:${autoRemodelDisplayKey(ad)}`);
  return keys;
};
const hasAutoRemodelDedupeOverlap = (keySet, ad, settings = DEDUPE_DEFAULTS) => (
  autoRemodelDedupeKeys(ad, settings).some(key => keySet.has(key))
);
const addAutoRemodelDedupeKeys = (keySet, ad, settings = DEDUPE_DEFAULTS) => {
  autoRemodelDedupeKeys(ad, settings).forEach(key => keySet.add(key));
};
const autoRemodelDedupeKeySet = (items, settings = DEDUPE_DEFAULTS) => {
  const keySet = new Set();
  (items || []).forEach(ad => addAutoRemodelDedupeKeys(keySet, ad, settings));
  return keySet;
};
const uniqueAutoRemodelAds = (items, settings = DEDUPE_DEFAULTS) => {
  const seen = new Set();
  const out = [];
  for (const ad of items || []) {
    if (hasAutoRemodelDedupeOverlap(seen, ad, settings)) continue;
    addAutoRemodelDedupeKeys(seen, ad, settings);
    out.push(ad);
  }
  return out;
};
const scriptEntryForAd = (scripts, ad) => {
  const key = resultKey(ad);
  if (key && scripts[key]) return scripts[key];
  const hasInstanceKey = !!(ad?.resultUuid || ad?.result_uuid || ad?.cardKey);
  return hasInstanceKey ? undefined : scripts[ad?.id];
};
const scriptLinesFromText = (text) => String(text || '')
  .split(/\n\s*\n/)
  .map(p => p.trim())
  .filter(Boolean)
  .map(body => ({ tag: '', body }));
const remodelOutputWithComplianceScript = (fullOutput, replacementScript) => {
  const full = String(fullOutput || '').trim();
  const replacement = String(replacementScript || '').trim();
  if (!full) return replacement ? `### REMODELED SCRIPT\n\n${replacement}` : '';
  if (!replacement) return full;
  const block = `### REMODELED SCRIPT\n\n${replacement}`;
  const scriptSection = /###\s*REMODELED\s+SCRIPT\s*\n[\s\S]*?(?=\n###\s|$)/i;
  if (scriptSection.test(full)) return full.replace(scriptSection, block);
  return `${full}\n\n${block}`;
};
const capRemodelInstruction = (value) => String(value || '').slice(0, REMODEL_STRATEGY_MAX);
const mergeRemodelInstructions = (pageInstruction, adInstruction) => {
  const parts = [
    capRemodelInstruction(pageInstruction).trim(),
    capRemodelInstruction(adInstruction).trim(),
  ].filter(Boolean);
  return capRemodelInstruction(parts.join('\n\n')).trim();
};
const hasTranscriptContent = (ad) => !!(
  (ad?.transcriptRaw && String(ad.transcriptRaw).trim())
  || (typeof ad?.transcript === 'string' && ad.transcript.trim())
  || (Array.isArray(ad?.transcript) && ad.transcript.length > 0)
);
const mergeRichAdDetails = (existing, incoming) => {
  if (!existing) return incoming;
  if (!incoming) return existing;
  const incomingHasTranscript = hasTranscriptContent(incoming);
  const existingHasTranscript = hasTranscriptContent(existing);
  const raw = (existing.raw || incoming.raw)
    ? { ...(existing.raw || {}), ...(incoming.raw || {}) }
    : undefined;
  const thumbnailUrl = incoming.thumbnailUrl || existing.thumbnailUrl || null;
  const creativeUrl = incoming.creativeUrl || existing.creativeUrl || null;
  return {
    ...existing,
    ...incoming,
    raw,
    creativeUrl,
    thumbnailUrl,
    thumb: thumbnailUrl
      ? (incoming.thumbnailUrl ? incoming.thumb : existing.thumb || incoming.thumb)
      : (incoming.thumb || existing.thumb),
    transcript: incomingHasTranscript || !existingHasTranscript ? incoming.transcript : existing.transcript,
    transcriptRaw: incomingHasTranscript || !existingHasTranscript ? incoming.transcriptRaw : existing.transcriptRaw,
    transcriptStatus: incoming.transcriptStatus || existing.transcriptStatus || null,
    transcriptSource: incoming.transcriptSource || existing.transcriptSource || null,
    isVoiceTranscript: typeof incoming.isVoiceTranscript === 'boolean'
      ? incoming.isVoiceTranscript
      : existing.isVoiceTranscript,
    fallbackCopy: incoming.fallbackCopy || existing.fallbackCopy || null,
    videoUrl: incoming.videoUrl || existing.videoUrl || creativeUrl,
    reasoning: incoming.reasoning || existing.reasoning || null,
    copy: incoming.copy || existing.copy || '',
  };
};
const toggleSourceState = (current, key) => {
  if (!LIVE_SOURCES.includes(key)) return current || sourceState();
  const next = { ...(current || sourceState()) };
  next[key] = !next[key];
  next.metaads = false;
  return next;
};
const defaultSearchFilters = () => ({
  fpFormat: 'video', fpPublisherPlatforms: '', fpLanguages: 'en', fpLiveStatus: '',
  fpMinDays: '', fpOrder: '',
  minLikes: '', maxLikes: '', minDailyLikes: '', maxDailyLikes: '', orderBy: '',
  adspySiteType: '', adspyMediaType: 'video', adspyGender: '',
  adspyAgeMin: '', adspyAgeMax: '', adspyCountries: '', adspyLang: 'eng',
  adspyCreatedFrom: '', adspyCreatedTo: '', adspySeenFrom: '', adspySeenTo: '',
  adspyUsername: '', adspyUserId: '', adspyAffNetwork: '', adspyAffId: '',
  adspyOfferId: '', adspyTech: '', adspyButtons: '', adspyCtaMode: 'all', adspyUrl: '', adspyLandingUrl: '',
  bsPlatform: '', bsMinViews: '', bsMinLikes: '', bsMinEngagement: '',
  bsMinReach: '', bsMinSpend: '', bsMaxSpend: '',
  foreplayUrl: '', brandsearchUrl: '',
});

const DIRECTION_PLATFORM_DEFAULT_FILTERS = {
  foreplay: {
    display_format: ['video'],
    languages: ['en'],
    running_duration_min_days: 15,
    live: true,
    order: 'longest_running',
  },
  adspy: { mediaType: 'video' },
  brandsearch: { display_format: ['video'], brandsearch_status: 'active' },
};
const cloneDirectionPlatformDefaults = (platform) => Object.fromEntries(
  Object.entries(DIRECTION_PLATFORM_DEFAULT_FILTERS[platform] || {})
    .map(([key, value]) => [key, Array.isArray(value) ? [...value] : value]),
);
const emptyDirectionPlatformFilters = () => Object.fromEntries(
  LIVE_SOURCES.map(platform => [platform, cloneDirectionPlatformDefaults(platform)]),
);
const DIRECTION_FILTER_FIELDS = {
  foreplay: [
    { key: 'display_format', label: 'Format', type: 'select', array: true, options: [['', 'Any format'], ['video', 'Video'], ['image', 'Image'], ['carousel', 'Carousel']] },
    { key: 'publisher_platform', label: 'Publisher', type: 'list', placeholder: 'facebook, instagram' },
    { key: 'languages', label: 'Lang', type: 'list', placeholder: 'en' },
    { key: 'running_duration_min_days', label: 'Min days', type: 'number', min: 0, placeholder: '14' },
    { key: 'live', label: 'Status', type: 'boolean', emptyLabel: 'Any status', trueLabel: 'Live only', falseLabel: 'Inactive only' },
    { key: 'order', label: 'Order', type: 'select', options: [['', 'Default'], ['longest_running', 'Longest'], ['saved_newest', 'Newest'], ['saved_oldest', 'Oldest']] },
  ],
  adspy: [
    { key: 'siteType', label: 'Network', type: 'select', options: [['', 'FB+IG'], ['facebook', 'Facebook'], ['instagram', 'Instagram']] },
    { key: 'mediaType', label: 'Format', type: 'select', options: [['', 'Any format'], ['video', 'Video'], ['photo', 'Photo']] },
    { key: 'gender', label: 'Gender', type: 'select', options: [['', 'Any gender'], ['female', 'Female'], ['male', 'Male']] },
    { key: 'ages', label: 'Age', type: 'range', min: 18, max: 64, startPlaceholder: '18', endPlaceholder: '64' },
    { key: 'totalLikes', label: 'Likes', type: 'range', min: 0, max: 100000, startPlaceholder: '0', endPlaceholder: '100000' },
    { key: 'dailyLikes', label: 'Daily', type: 'range', min: 0, max: 1000, startPlaceholder: '0', endPlaceholder: '1000' },
    { key: 'orderBy', label: 'Sort', type: 'select', options: [['', 'Default'], ['total_likes', 'Likes'], ['total_shares', 'Shares'], ['created_on_asc', 'Oldest']] },
    { key: 'countries', label: 'Geo', type: 'list', placeholder: 'US, GB' },
    { key: 'lang', label: 'Lang', type: 'text', placeholder: 'eng' },
    { key: 'username', label: 'User', type: 'text', placeholder: 'username' },
    { key: 'userId', label: 'Page ID', type: 'number', min: 1, placeholder: 'id' },
    { key: 'affNetwork', label: 'Net', type: 'number', min: 1, placeholder: 'id' },
    { key: 'affId', label: 'Aff', type: 'text', placeholder: 'aff id' },
    { key: 'offerId', label: 'Offer', type: 'text', placeholder: 'offer id' },
    { key: 'buttons', label: 'CTA', type: 'list', placeholder: 'Shop Now' },
    { key: 'createdBetween', label: 'Created', type: 'dateRange' },
    { key: 'seenBetween', label: 'Seen', type: 'dateRange' },
  ],
  brandsearch: [
    { key: 'brandsearch_platforms', label: 'Network', type: 'select', array: true, options: [['', 'All BS'], ['meta', 'Meta'], ['tiktok', 'TikTok'], ['instagram', 'Instagram']] },
    { key: 'display_format', label: 'Format', type: 'select', array: true, options: [['', 'Any format'], ['video', 'Video'], ['image', 'Image']] },
    { key: 'brandsearch_status', label: 'Status', type: 'select', options: [['', 'Any status'], ['active', 'Live only'], ['inactive', 'Inactive only']] },
    { key: 'brandsearch_min_views', label: 'Views', type: 'number', min: 0, placeholder: '0' },
    { key: 'brandsearch_min_likes', label: 'Likes', type: 'number', min: 0, placeholder: '0' },
    { key: 'brandsearch_min_engagement_rate', label: 'ER%', type: 'number', min: 0, step: 0.1, placeholder: '0' },
    { key: 'brandsearch_min_reach', label: 'Reach', type: 'number', min: 0, placeholder: '0' },
    { key: 'brandsearch_min_spend', label: 'Min spend', type: 'number', min: 0, placeholder: '0' },
    { key: 'brandsearch_max_spend', label: 'Max spend', type: 'number', min: 0, placeholder: '0' },
  ],
};

const asFilterList = (value) => {
  if (value === null || value === undefined || value === '') return [];
  if (Array.isArray(value)) return value.filter(item => item !== null && item !== undefined && item !== '');
  if (typeof value === 'string') return value.split(',').map(item => item.trim()).filter(Boolean);
  return [value];
};
const listText = (value) => asFilterList(value).join(', ');
const setIfMissing = (out, key, value) => {
  if (out[key] === undefined && value !== undefined && value !== null && value !== '') out[key] = value;
};
const normalizeDirectionApiFilters = (platform, filters = {}) => {
  const out = {
    ...cloneDirectionPlatformDefaults(platform),
    ...(filters && typeof filters === 'object' && !Array.isArray(filters) ? filters : {}),
  };
  if (platform === 'adspy') {
    setIfMissing(out, 'siteType', out.adspy_site_type);
    setIfMissing(out, 'mediaType', out.adspy_media_type);
    setIfMissing(out, 'gender', out.adspy_gender);
    setIfMissing(out, 'orderBy', out.adspy_order);
    setIfMissing(out, 'username', out.adspy_username);
    setIfMissing(out, 'userId', out.adspy_user_id);
    setIfMissing(out, 'affNetwork', out.adspy_aff_network);
    setIfMissing(out, 'affId', out.adspy_aff_id);
    setIfMissing(out, 'offerId', out.adspy_offer_id);
    setIfMissing(out, 'buttons', out.adspy_buttons);
    setIfMissing(out, 'lang', Array.isArray(out.languages) ? out.languages[0] : out.languages);
    if (out.ages === undefined && (out.adspy_age_min || out.adspy_age_max)) out.ages = [out.adspy_age_min, out.adspy_age_max].filter(Boolean);
    if (out.totalLikes === undefined && (out.adspy_min_likes || out.adspy_max_likes)) out.totalLikes = [out.adspy_min_likes, out.adspy_max_likes].filter(Boolean);
    if (out.dailyLikes === undefined && (out.adspy_min_daily_likes || out.adspy_max_daily_likes)) out.dailyLikes = [out.adspy_min_daily_likes, out.adspy_max_daily_likes].filter(Boolean);
    if (out.createdBetween === undefined && (out.adspy_created_from || out.adspy_created_to)) out.createdBetween = [out.adspy_created_from, out.adspy_created_to].filter(Boolean);
    if (out.seenBetween === undefined && (out.adspy_seen_from || out.adspy_seen_to)) out.seenBetween = [out.adspy_seen_from, out.adspy_seen_to].filter(Boolean);
  }
  if (platform === 'brandsearch') {
    setIfMissing(out, 'brandsearch_platforms', out.brandsearch_platform);
  }
  return out;
};
const cleanApiFilters = (filters = {}) => Object.fromEntries(
  Object.entries(filters || {}).flatMap(([key, value]) => {
    if (value === null || value === undefined || value === '') return [];
    if (Array.isArray(value)) {
      const list = value.map(item => (typeof item === 'string' ? item.trim() : item)).filter(item => item !== null && item !== undefined && item !== '');
      return list.length ? [[key, list]] : [];
    }
    if (typeof value === 'string') {
      const text = value.trim();
      return text ? [[key, text]] : [];
    }
    return [[key, value]];
  }),
);

function App() {
  const [authState, setAuthState] = useState({ status: 'loading', user: null });
  const queryTab = new URLSearchParams(window.location.search).get('tab');
  const initialTab = window.location.pathname === '/admin/access-requests'
    ? 'accessRequests'
    : normalizeDashboardTab(DASHBOARD_TABS.has(queryTab) ? queryTab : (savedDashboardTab() || 'research'));
  const [tab, setTabState] = useState(initialTab);
  const setTab = (nextTab) => setTabState(normalizeDashboardTab(nextTab, tab));

  // search rows (manual keyword / competitor search)
  const [rows, setRows] = useState([
    { id: 1, query: '', competitorId: null, count: 20,
      filters: defaultSearchFilters(),
      platforms: sourceState() },
  ]);
  const [competitors, setCompetitors] = useState([]);
  const [directions, setDirections] = useState([]);
  const directionsRef = useRef([]);
  const arDefaultDirectionsLoadedRef = useRef(false);

  // pipeline stages
  const [researchAds, setResearchAds] = useState([]);
  const [filteredAds, setFilteredAds] = useState([]);
  const [remodeledAds, setRemodeledAds] = useState([]);
  const [filterPage, setFilterPage] = useState({ loaded: 0, hasMore: false, loading: false });
  const [remodelPage, setRemodelPage] = useState({ loaded: 0, hasMore: false, loading: false });
  const [complianceJobs, setComplianceJobs] = useState([]);
  const [compliancePage, setCompliancePage] = useState({ loaded: 0, hasMore: false, loading: false });
  const [complianceStrictness, setComplianceStrictness] = useState('strict');
  const [complianceBusy, setComplianceBusy] = useState(false);
  const [complianceRunState, setComplianceRunState] = useState(null);
  const [autoRemodelAds, setAutoRemodelAds] = useState([]);
  const researchAdsRef = useRef([]);
  const autoRemodelAdsRef = useRef([]);
  const [searchCacheLoading, setSearchCacheLoading] = useState(false);
  const [remodelScripts, setRemodelScripts] = useState({}); // { [adId]: [{tag, body}] | 'pending' | 'error' }
  const [remodelPageInstruction, setRemodelPageInstruction] = useState('');
  const [remodelAdInstructions, setRemodelAdInstructions] = useState({});
  const [autoRemodelPageInstruction, setAutoRemodelPageInstruction] = useState('');
  const [autoRemodelAdInstructions, setAutoRemodelAdInstructions] = useState({});
  const remodelPollRef = useRef(null);
  const remodelStartPendingRef = useRef(false);
  const compliancePollRef = useRef(null);
  const [convertedAds, setConvertedAds] = useState([]);
  const [autoresearchActive, setAutoresearchActive] = useState(false);  // true if last Research view should show winners-only badge
  const setInstructionForAd = (setter) => (adId, value) => {
    setter(prev => {
      const next = { ...prev };
      const cleaned = capRemodelInstruction(value);
      if (cleaned.trim()) next[adId] = cleaned;
      else delete next[adId];
      return next;
    });
  };
  const setManualAdInstruction = setInstructionForAd(setRemodelAdInstructions);
  const setAutoAdInstruction = setInstructionForAd(setAutoRemodelAdInstructions);

  const [selResearch, setSelResearch] = useState(new Set());
  const [selFilter, setSelFilter] = useState(new Set());
  const [selRemodel, setSelRemodel] = useState(new Set());
  const [selAutoRemodel, setSelAutoRemodel] = useState(new Set());
  const [selRemodelVariants, setSelRemodelVariants] = useState(new Set());
  const [selAutoRemodelVariants, setSelAutoRemodelVariants] = useState(new Set());
  const [selCompliance, setSelCompliance] = useState(new Set());
  const pipelineFocusRef = useRef({ tab: null, ids: [], tick: 0 });
  const [pipelineFocusTick, setPipelineFocusTick] = useState(0);

  // autoresearch controls
  const [arSelected, setArSelected] = useState(new Set());
  const [arTarget, setArTarget] = useState('1');
  const setBoundedArTarget = (value) => setArTarget(sanitizeArTargetInput(value));
  const [arMaxAdsPerDirection, setArMaxAdsPerDirection] = useState('100');
  const [arMaxAdsPerPlatform, setArMaxAdsPerPlatform] = useState('');
  const setBoundedArMaxAdsPerDirection = (value) =>
    setArMaxAdsPerDirection(sanitizeArTargetInput(value, SEARCH_ADS_PER_DIRECTION_MAX));
  const setBoundedArMaxAdsPerPlatform = (value) =>
    setArMaxAdsPerPlatform(sanitizeArTargetInput(value, SEARCH_ADS_PER_PLATFORM_MAX));
  const [arNumDirections, setArNumDirections] = useState('');
  const [arPickedDirections, setArPickedDirections] = useState(new Set());
  const [arRunning, setArRunning] = useState(false);
  const [arProgress, setArProgress] = useState(null); // { totalScored, winners, directionsSatisfied, directionsTotal, crawlState }
  const [arSources, setArSources] = useState(sourceState());
  const [arFilters, setArFilters] = useState(defaultSearchFilters());
  const [autoresearchPage, setAutoresearchPage] = useState({ loaded: 0, total: null, hasMore: false, loading: false });
  const autoresearchLoadedIdsRef = useRef(new Set());
  const autoresearchCurrentIdsRef = useRef(new Set());
  const autoresearchLoadedPlatformCountsRef = useRef({});
  const autoresearchTargetRef = useRef(null);
  const arRunningRef = useRef(false);
  const autoresearchActiveRef = useRef(false);
  const autoresearchWinnersInFlightRef = useRef(new Map());
  // Bug retention — each Autoresearch run is tagged with a monotonically
  // increasing batch id so the UI can show new winners on top while older
  // batches stay below as history.
  const autoresearchBatchIdRef = useRef(null);
  // Platform-level credit balances surfaced in the top bar. Foreplay is live;
  // the other three are visual placeholders until their backends land.
  const [platformCredits, setPlatformCredits] = useState({
    foreplay: null, adspy: null, brandsearch: null, metaads: null,
  });
  const arPollRef = useRef(null);
  const arPollPhaseRef = useRef(null);
  // Monotonic token so late pollWinners responses from a prior click cannot
  // mark the current run complete or clear visible ads.
  const autoresearchRunGenerationRef = useRef(0);

  // ---- Static Ads state -------------------------------------------------
  // Static Ads is a standalone product surface — it does not share any
  // pipeline state with the video-ads flow. App only holds the run snapshot
  // (from /api/static_ads/state), the history list, and the in-flight flag.
  const [saActiveRun, setSaActiveRun] = useState(null);
  const [saStatus, setSaStatus] = useState(null);
  const [saState, setSaState] = useState(null);
  const [saRunning, setSaRunning] = useState(false);
  const [saRuns, setSaRuns] = useState([]);
  const [saStarting, setSaStarting] = useState(false);
  const [saError, setSaError] = useState(null);
  const saPollRef = useRef(null);
  // /state only returns *active* runs; once a run is terminal it drops out.
  // We remember the current run id so the finished run (and its gallery) stays
  // on screen until the user starts another or deletes it.
  const saCurrentRunIdRef = useRef(null);

  // ---- Auto-Remodel state -----------------------------------------------
  // The orchestrator's directions picker is shared with manual autoresearch
  // — both pull from the same full direction pool. We keep it in App so the
  // panel + the autoresearch panel can stay independent components without
  // losing user picks when one re-renders.
  const [arOrchPickedDirections, setArOrchPickedDirections] = useState(new Set());
  const toggleArOrchPickedDirection = (id) =>
    setArOrchPickedDirections(s => {
      const n = new Set(s);
      n.has(id) ? n.delete(id) : n.add(id);
      return n;
    });
  const clearArOrchPickedDirections = () => setArOrchPickedDirections(new Set());

  const [arRunId, setArRunId] = useState(null);
  const [arStage, setArStage] = useState(null);
  const [arLastEvent, setArLastEvent] = useState(null);
  const [arPickedIds, setArPickedIds] = useState([]);
  const [arStartPending, setArStartPending] = useState(false);
  // Refs that mirror the orchestrator state — the SSE callback below is
  // installed once via useEffect([]), so any state value referenced inside
  // its closure is captured at mount-time and goes stale. Refs let the
  // handler read the *current* value when an event arrives.
  const arRunIdRef = useRef(null);
  const arStageRef = useRef(null);
  const arPickedIdsRef = useRef([]);
  const arTargetAdsRef = useRef(0);
  const arAcceptedAdIdsRef = useRef(new Set());
  const arStartPendingRef = useRef(false);
  // Live progress counters — fed by SSE so the panel meta line + progress
  // bar tick exactly like the autoresearch panel does. ``arNVariants`` is
  // captured at Run time so we know the variants_total denominator.
  const [arDirectionsDone, setArDirectionsDone] = useState(0);
  const [arDirectionsTotal, setArDirectionsTotal] = useState(0);
  const [arAdsFound, setArAdsFound] = useState(0);
  const [arTargetAds, setArTargetAds] = useState(0);
  const [arVariantsDone, setArVariantsDone] = useState(0);
  const [arNVariants, setArNVariants] = useState(1);
  const [arVariantsTotalOverride, setArVariantsTotalOverride] = useState(null);
  const [autoRemodelHydration, setAutoRemodelHydration] = useState({ total: 0, loaded: 0, loading: false });
  const [autoRemodelRestoring, setAutoRemodelRestoring] = useState(false);
  const autoRemodelLoadedIdsRef = useRef(new Set());
  const autoRemodelHydratingPageRef = useRef(false);
  const arNVariantsRef = useRef(1);
  const arRunningOrch = !!arRunId && !['done', 'failed', 'cancelled'].includes(arStage);
  const [searchPassesOnly, setSearchPassesOnly] = useState(true);
  const searchPassesOnlyRef = useRef(searchPassesOnly);

  useEffect(() => { arRunIdRef.current = arRunId; }, [arRunId]);
  useEffect(() => { arStageRef.current = arStage; }, [arStage]);
  useEffect(() => { arPickedIdsRef.current = arPickedIds; }, [arPickedIds]);
  useEffect(() => { arTargetAdsRef.current = arTargetAds; }, [arTargetAds]);
  useEffect(() => { researchAdsRef.current = researchAds; }, [researchAds]);
  useEffect(() => { autoRemodelAdsRef.current = autoRemodelAds; }, [autoRemodelAds]);
  useEffect(() => { arNVariantsRef.current = arNVariants; }, [arNVariants]);
  useEffect(() => { arRunningRef.current = arRunning; }, [arRunning]);
  useEffect(() => { autoresearchActiveRef.current = autoresearchActive; }, [autoresearchActive]);
  useEffect(() => { searchPassesOnlyRef.current = searchPassesOnly; }, [searchPassesOnly]);

  // Platform toggles for the Auto-Remodel panel. Kept separate from
  // ``arSources`` (which the manual autoresearch panel owns) so the user
  // can configure each panel independently.
  const [arOrchSources, setArOrchSources] = useState(allSourceState());
  const toggleArOrchSource = (key) => {
    if (!LIVE_SOURCES.includes(key)) return;
    setArOrchSources(s => toggleSourceState(s, key));
  };
  const selectAllArOrchSources = () => setArOrchSources(allSourceState());

  const toggleArPickedDirection = (id) =>
    setArPickedDirections(s => {
      const n = new Set(s);
      n.has(id) ? n.delete(id) : n.add(id);
      return n;
    });
  const clearArPickedDirections = () => setArPickedDirections(new Set());
  const selectAllArPickedDirections = () => setArPickedDirections(new Set(directionsRef.current.map(d => d.id)));

  const toggleArSource = (key) => {
    if (!LIVE_SOURCES.includes(key)) return;
    setArSources(s => toggleSourceState(s, key));
  };
  const selectAllArSources = () => setArSources(allSourceState());

  const exclusiveAdSelection = (adSetter, variantSetter) => (id) => {
    if (!id) return;
    variantSetter(new Set());
    adSetter(prev => {
      const next = new Set(prev);
      next.has(id) ? next.delete(id) : next.add(id);
      return next;
    });
  };

  const exclusiveVariantSelection = (variantSetter, adSetter) => (jobId) => {
    if (!jobId) return;
    adSetter(new Set());
    variantSetter(prev => {
      const next = new Set(prev);
      next.has(jobId) ? next.delete(jobId) : next.add(jobId);
      return next;
    });
  };

  const toggleRemodelAdSelection = exclusiveAdSelection(setSelRemodel, setSelRemodelVariants);
  const toggleAutoRemodelAdSelection = exclusiveAdSelection(setSelAutoRemodel, setSelAutoRemodelVariants);
  const toggleRemodelVariantSelection = exclusiveVariantSelection(setSelRemodelVariants, setSelRemodel);
  const toggleAutoRemodelVariantSelection = exclusiveVariantSelection(setSelAutoRemodelVariants, setSelAutoRemodel);

  const [tweaks, setTweaks] = useState({ theme: 'light', density: 'comfortable', cardStyle: 'standard', accent: 'teal' });
  const [tweaksOpen, setTweaksOpen] = useState(false);
  const [toast, setToast] = useState(null);
  const [dedupeSettings, setDedupeSettings] = useState(DEDUPE_DEFAULTS);
  const dedupeSettingsRef = useRef(DEDUPE_DEFAULTS);
  const [dedupeSaving, setDedupeSaving] = useState(false);
  useEffect(() => { dedupeSettingsRef.current = dedupeSettings; }, [dedupeSettings]);
  const [searching, setSearching] = useState(false);
  const [searchingRowIds, setSearchingRowIds] = useState(new Set());
  const activeSearchRunIdsRef = useRef(new Map());
  const searchStopRequestedRef = useRef(false);
  const searchResumeStartedRef = useRef(false);
  // High-severity per-platform warnings from the last manual Search run
  // (e.g. "no AdSpy ads matched 'oregano oil' \u2014 showing top 5 instead").
  // Rendered as a red banner above the search results panels.
  const [searchWarnings, setSearchWarnings] = useState([]);
  const [latestSearchRun, setLatestSearchRun] = useState(null);
  const [latestAutoresearchRun, setLatestAutoresearchRun] = useState(null);
  const [filterRunState, setFilterRunState] = useState(null);
  const [remodelRunState, setRemodelRunState] = useState(null);
  const [filterBusy, setFilterBusy] = useState(false);
  const [remodelBusy, setRemodelBusy] = useState(false);
  const [expandedAd, setExpandedAd] = useState(null);
  const [pendingAccessCount, setPendingAccessCount] = useState(0);
  const [managementCollapsed, setManagementCollapsed] = useState(false);
  const filterPollRef = useRef(null);
  // Active-poller callback handles. Stored so that visibilitychange can fire
  // each active poller once when the user returns to the tab (catch-up) and
  // so each setInterval body can read the latest closure via the ref.
  const arPollCallbackRef = useRef(null);
  const remodelPollCallbackRef = useRef(null);
  const filterPollCallbackRef = useRef(null);
  const compliancePollCallbackRef = useRef(null);
  const saPollCallbackRef = useRef(null);
  // Tracks tabs whose first page has been lazy-loaded so re-entering a tab
  // does not refetch on every click. Reset by manual user action / logout.
  const tabLoadedRef = useRef(new Set());
  const searchCacheRestoredRef = useRef(false);
  const [phaseAComplete, setPhaseAComplete] = useState(false);
  const [autoRemodelHistoryLoaded, setAutoRemodelHistoryLoaded] = useState(false);
  const [autoRemodelHistoryLoading, setAutoRemodelHistoryLoading] = useState(false);

  const startVisibilityAwarePoll = (refKey, callback, intervalMs) => {
    const callbackRef = (
      refKey === 'ar' ? arPollCallbackRef
      : refKey === 'remodel' ? remodelPollCallbackRef
      : refKey === 'filter' ? filterPollCallbackRef
      : refKey === 'compliance' ? compliancePollCallbackRef
      : refKey === 'staticAds' ? saPollCallbackRef
      : null
    );
    const intervalRef = (
      refKey === 'ar' ? arPollRef
      : refKey === 'remodel' ? remodelPollRef
      : refKey === 'filter' ? filterPollRef
      : refKey === 'compliance' ? compliancePollRef
      : refKey === 'staticAds' ? saPollRef
      : null
    );
    if (!callbackRef || !intervalRef) return null;
    if (intervalRef.current) clearInterval(intervalRef.current);
    callbackRef.current = callback;
    intervalRef.current = setInterval(() => {
      if (typeof document !== 'undefined' && document.hidden) return;
      const fn = callbackRef.current;
      if (typeof fn === 'function') fn();
    }, intervalMs);
    return intervalRef.current;
  };

  const stopPoll = (refKey) => {
    const callbackRef = (
      refKey === 'ar' ? arPollCallbackRef
      : refKey === 'remodel' ? remodelPollCallbackRef
      : refKey === 'filter' ? filterPollCallbackRef
      : refKey === 'compliance' ? compliancePollCallbackRef
      : refKey === 'staticAds' ? saPollCallbackRef
      : null
    );
    const intervalRef = (
      refKey === 'ar' ? arPollRef
      : refKey === 'remodel' ? remodelPollRef
      : refKey === 'filter' ? filterPollRef
      : refKey === 'compliance' ? compliancePollRef
      : refKey === 'staticAds' ? saPollRef
      : null
    );
    if (intervalRef && intervalRef.current) {
      clearInterval(intervalRef.current);
      intervalRef.current = null;
    }
    if (callbackRef) callbackRef.current = null;
  };

  // When the tab becomes visible again, run each active poller once so the UI
  // catches up immediately instead of waiting up to 12s for the next tick.
  useEffect(() => {
    if (typeof document === 'undefined') return undefined;
    const onVisibilityChange = () => {
      if (document.hidden) return;
      try { arPollCallbackRef.current?.(); } catch (e) { console.warn('ar poll catchup failed', e); }
      try { remodelPollCallbackRef.current?.(); } catch (e) { console.warn('remodel poll catchup failed', e); }
      try { filterPollCallbackRef.current?.(); } catch (e) { console.warn('filter poll catchup failed', e); }
      try { compliancePollCallbackRef.current?.(); } catch (e) { console.warn('compliance poll catchup failed', e); }
      try { saPollCallbackRef.current?.(); } catch (e) { console.warn('static ads poll catchup failed', e); }
    };
    document.addEventListener('visibilitychange', onVisibilityChange);
    return () => document.removeEventListener('visibilitychange', onVisibilityChange);
  }, []);

  useEffect(() => {
    let cancelled = false;
    const loadAuth = async () => {
      try {
        const me = await RESILIA_API.authMe();
        if (cancelled) return;
        if (me && me.authenticated) {
          setAuthState({ status: 'authenticated', user: me.user });
        } else {
          setAuthState({ status: 'signed_out', user: null });
        }
      } catch (e) {
        if (!cancelled) setAuthState({ status: 'signed_out', user: null });
      }
    };
    const onUnauthorized = () => setAuthState({ status: 'signed_out', user: null });
    window.addEventListener('resilia:unauthorized', onUnauthorized);
    loadAuth();
    return () => {
      cancelled = true;
      window.removeEventListener('resilia:unauthorized', onUnauthorized);
    };
  }, []);

  useEffect(() => {
    if (authState.status !== 'authenticated') return;
    let cancelled = false;
    RESILIA_API.getDedupeSettings()
      .then(settings => {
        if (!cancelled) setDedupeSettings({ ...DEDUPE_DEFAULTS, ...(settings || {}) });
      })
      .catch(() => {
        if (!cancelled) setDedupeSettings(DEDUPE_DEFAULTS);
      });
    return () => { cancelled = true; };
  }, [authState.status]);

  // edit-mode host protocol
  useEffect(() => {
    const onMsg = (e) => {
      if (e.data?.type === '__activate_edit_mode') setTweaksOpen(true);
      if (e.data?.type === '__deactivate_edit_mode') setTweaksOpen(false);
    };
    window.addEventListener('message', onMsg);
    window.parent.postMessage({ type: '__edit_mode_available' }, '*');
    return () => window.removeEventListener('message', onMsg);
  }, []);

  useEffect(() => { document.documentElement.setAttribute('data-theme', tweaks.theme); }, [tweaks.theme]);
  useEffect(() => { document.documentElement.setAttribute('data-accent', tweaks.accent); }, [tweaks.accent]);
  useEffect(() => { directionsRef.current = directions; }, [directions]);
  useEffect(() => {
    if (!directions.length || arDefaultDirectionsLoadedRef.current) return;
    const allDirectionIds = directions.map(direction => direction.id).filter(Boolean);
    if (!allDirectionIds.length) return;
    setArPickedDirections(new Set(allDirectionIds));
    setArNumDirections(String(allDirectionIds.length));
    arDefaultDirectionsLoadedRef.current = true;
  }, [directions]);

  const restoreSearchCacheOnce = async () => {
    if (searchCacheRestoredRef.current) return;
    searchCacheRestoredRef.current = true;
    setSearchCacheLoading(true);
    try {
      const cached = await RESILIA_API.getCache('last_search_results', {
        limit: 2000,
        runLimit: 20,
      });
      const cachedAds = cached && cached.payload && Array.isArray(cached.payload.ads) ? cached.payload.ads : [];
      if (cachedAds.length) {
        const latestRunId = cached?.payload?.run_id ? String(cached.payload.run_id) : null;
        const cachedHistoryAds = Array.isArray(cached?.payload?.history_ads)
          ? cached.payload.history_ads
          : [];
        const mappedAds = [...cachedAds, ...cachedHistoryAds].map(a => {
          const adapted = RESILIA_ADAPT.adaptSearchAd(a, 'foreplay');
          const runId = adapted.runId ? String(adapted.runId) : null;
          return {
            ...adapted,
            source: 'search',
            currentSearchRun: latestRunId ? runId === latestRunId : false,
          };
        });
        const seenCacheKeys = new Set();
        const ads = mappedAds.filter(ad => {
          const key = resultKey(ad);
          if (seenCacheKeys.has(key)) return false;
          seenCacheKeys.add(key);
          return true;
        });
        setResearchAds(prev => {
          const existingKeys = new Set(ads.map(resultKey));
          const retained = prev.filter(a => a.source === 'autoresearch' || !existingKeys.has(resultKey(a)));
          const retainedSearchAds = retained
            .filter(a => a.source !== 'autoresearch')
            .map(a => ({ ...a, currentSearchRun: false }));
          return [...retained.filter(a => a.source === 'autoresearch'), ...ads, ...retainedSearchAds];
        });
        if (latestRunId) {
          const latestAds = ads.filter(a => a.currentSearchRun);
          setLatestSearchRun({
            id: latestRunId,
            count: latestAds.length,
            labels: [cached.payload.query || cached.payload.competitor_id || 'Restored manual search'].filter(Boolean),
            duplicatesHidden: Number(cached.payload.duplicates_hidden || 0),
          });
        }
      }
    } catch (e) {
      console.warn('restore search cache failed', e);
    } finally {
      setSearchCacheLoading(false);
    }
  };

  const persistActiveSearchRuns = () => {
    try {
      const runs = [...activeSearchRunIdsRef.current.entries()]
        .map(([rowKey, runId]) => ({ rowKey, runId }))
        .filter(item => item.runId);
      if (runs.length) {
        window.localStorage.setItem(ACTIVE_SEARCH_RUNS_STORAGE_KEY, JSON.stringify(runs));
      } else {
        window.localStorage.removeItem(ACTIVE_SEARCH_RUNS_STORAGE_KEY);
      }
    } catch {
      // localStorage can be unavailable in private/hardened contexts.
    }
  };

  const loadPersistedSearchRuns = () => {
    try {
      const raw = window.localStorage.getItem(ACTIVE_SEARCH_RUNS_STORAGE_KEY);
      const parsed = raw ? JSON.parse(raw) : [];
      return Array.isArray(parsed)
        ? parsed.filter(item => item && typeof item.runId === 'string' && item.runId)
        : [];
    } catch {
      return [];
    }
  };

  const applyResumedSearchResponse = (runId, rowKey, resp) => {
    const seedArrivedAtMs = Date.now();
    const ads = (resp?.ads || []).map(a => {
      const platform = a?.source_platform || a?.raw?.source_platform || 'foreplay';
      const adapted = RESILIA_ADAPT.adaptSearchAd(a, platform);
      return {
        ...adapted,
        source: 'search',
        searchReturnLabel: 'Resumed manual search',
        searchRunId: runId,
        searchBatchId: 'resumed_manual',
        searchRowId: rowKey,
        currentSearchRun: true,
        arrivedAtMs: adapted.arrivedAtMs || seedArrivedAtMs,
      };
    });
    const seen = new Set();
    const deduped = [];
    for (const ad of ads) {
      if (seen.has(ad.id)) continue;
      seen.add(ad.id);
      deduped.push(ad);
    }
    const historyAds = (resp?.history_ads || []).map(a => ({
      ...RESILIA_ADAPT.adaptSearchAd(a, 'foreplay'),
      source: 'search',
      searchReturnLabel: 'Duplicate history',
      currentSearchRun: false,
    }));
    if (deduped.length) {
      setResearchAds(prev => {
        const keys = new Set([...deduped, ...historyAds].map(resultKey));
        const remaining = prev
          .filter(ad => ad.searchRunId !== runId && !keys.has(resultKey(ad)))
          .map(ad => (
            ad.source === 'autoresearch' ? ad : { ...ad, currentSearchRun: false }
          ));
        return [...deduped, ...historyAds, ...remaining];
      });
    } else if (historyAds.length) {
      setResearchAds(prev => {
        const keys = new Set(historyAds.map(resultKey));
        const remaining = prev
          .filter(ad => !keys.has(resultKey(ad)))
          .map(ad => (
            ad.source === 'autoresearch' ? ad : { ...ad, currentSearchRun: false }
          ));
        return [...historyAds, ...remaining];
      });
    }
    setLatestSearchRun({
      id: runId,
      count: deduped.length,
      labels: ['Resumed manual search'],
      duplicatesHidden: Number(resp?.duplicates_hidden || 0),
      progress: resp?.progress || {},
    });
  };

  const refreshDirectionCatalog = async () => {
    const [comps, dirs] = await Promise.all([
      RESILIA_API.listCompetitors(),
      RESILIA_API.listDirections(),
    ]);
    const loadedDirections = dirs || [];
    directionsRef.current = loadedDirections;
    setCompetitors(comps || []);
    setDirections(loadedDirections);
    return loadedDirections;
  };

  useEffect(() => {
    try {
      window.localStorage.setItem(TAB_STORAGE_KEY, tab);
    } catch {
      // localStorage can be unavailable in hardened/private browser contexts.
    }
  }, [tab]);

  useEffect(() => {
    if (authState.status !== 'authenticated') return;
    if (searchResumeStartedRef.current) return;
    const runs = loadPersistedSearchRuns();
    if (!runs.length || !RESILIA_API.waitForSearchRun) return;
    searchResumeStartedRef.current = true;
    setSearching(true);
    runs.forEach(({ rowKey, runId }) => {
      const key = rowKey || `resumed-${runId}`;
      activeSearchRunIdsRef.current.set(key, runId);
      persistActiveSearchRuns();
      RESILIA_API.waitForSearchRun(runId, {
        onProgress: progress => applyResumedSearchResponse(runId, key, progress),
      }).then(resp => {
        applyResumedSearchResponse(runId, key, resp);
      }).catch(e => {
        console.warn('resumed search polling failed', runId, e);
      }).finally(() => {
        activeSearchRunIdsRef.current.delete(key);
        persistActiveSearchRuns();
        if (activeSearchRunIdsRef.current.size === 0) setSearching(false);
      });
    });
  }, [authState.status]);

  useEffect(() => {
    if (authState.status === 'authenticated' && !authState.user?.is_admin && ['accessRequests', 'adminDirections', 'adminWinningCriteria', 'adminGlobalCrawler', 'adminCosts', 'adminEvaluationQuotas', 'adminWorkspaces', 'adminUsers'].includes(tab)) {
      setTab('research');
    }
  }, [authState.status, authState.user?.is_admin, tab]);

  // Startup bootstrap: one backend request returns the critical direction
  // catalog plus lightweight state snapshots. Heavy tab lists and cached
  // search ads are still deferred to the lazy tab loader below.
  useEffect(() => {
    if (authState.status !== 'authenticated') return undefined;
    let cancelled = false;
    setPhaseAComplete(false);
    tabLoadedRef.current = new Set();
    searchCacheRestoredRef.current = false;

    (async () => {
      let loadedDirections = [];
      const restoreAutoRemodel = showAutoRemodelUI();
      if (restoreAutoRemodel && !cancelled) setAutoRemodelRestoring(true);
      // Claim ownership of Auto-Remodel restore so the lazy loader does not
      // race this bootstrap request with a second state/snapshot fetch.
      if (restoreAutoRemodel) tabLoadedRef.current.add('autoRemodel');
      try {
        const bootstrap = await RESILIA_API.dashboardBootstrap();
        if (cancelled) return;
        if (!bootstrap) throw new Error('dashboard bootstrap unavailable');

        loadedDirections = Array.isArray(bootstrap.directions) ? bootstrap.directions : [];
        directionsRef.current = loadedDirections;
        setCompetitors(Array.isArray(bootstrap.competitors) ? bootstrap.competitors : []);
        setDirections(loadedDirections);

        const summary = bootstrap.summary || {};
        if (typeof summary.credits_remaining === 'number') {
          setPlatformCredits(pc => ({ ...pc, foreplay: summary.credits_remaining }));
        }

        const restoredAutoresearchState = bootstrap.autoresearch || null;
        const state = restoredAutoresearchState?.state || {};
        if (typeof state.target_ads === 'number' && state.target_ads > 0) {
          autoresearchTargetRef.current = state.target_ads;
          setArTarget(String(state.target_ads));
        }
        if (restoredAutoresearchState?.running) {
          const runId = state.run_id || `ar-restored-${Date.now()}`;
          autoresearchBatchIdRef.current = runId;
          autoresearchLoadedIdsRef.current = new Set();
          autoresearchCurrentIdsRef.current = new Set();
          setArRunning(true);
          setAutoresearchActive(true);
          setArSelected(new Set(state.selected || []));
          setLatestAutoresearchRun({
            id: runId,
            count: 0,
            status: 'running',
            labels: state.sources || [],
          });
          setArProgress({
            crawlState: state.run_phase || state.status || 'running',
            runPhase: state.run_phase || state.status || 'running',
            prefilledCount: Number(state.prefilled_count || state.result_count || 0),
            remainingTarget: Number(state.remaining_target || 0),
            directionsSatisfied: state.run_directions_done || 0,
            directionsTotal: state.run_directions_total || 0,
            platformAdsFetched: state.platform_ads_fetched || {},
            platformProgress: state.platform_progress || {},
            directionProgress: state.direction_progress || {},
            coverageTotal: state.coverage_total || 0,
            coverageTouched: state.coverage_touched || 0,
            platformCounts: state.run_platform_counts || {},
          });
          startVisibilityAwarePoll('ar', pollWinners, AR_ACTIVE_POLL_INTERVAL_MS);
          pollWinners();
        }

        const filterState = bootstrap.filter || null;
        if (filterState) {
          setFilterRunState(filterState);
          setFilterBusy(!!filterState.running);
          if (filterState.running) startFilterStatePolling();
        }

        const remodelState = bootstrap.remodel || null;
        if (remodelState) {
          setRemodelRunState(remodelState);
          setRemodelBusy(!!remodelState.running);
          const restoreAdIds = remodelState?.state?.ad_ids || remodelState?.run?.state?.ad_ids || [];
          if (remodelState.running && restoreAdIds.length) {
            startVisibilityAwarePoll('remodel', () => {
              pollRemodelJobs(restoreAdIds, { source: 'manual' });
              refreshRemodelRunState();
            }, REMODEL_POLL_INTERVAL_MS);
            pollRemodelJobs(restoreAdIds, { source: 'manual' });
          }
        }

        const auto = bootstrap.auto_remodel || null;
        if (restoreAutoRemodel && auto && auto.status === 'ok' && auto.state) {
          const restored = await applyAutoRemodelCurrentPayload(auto, loadedDirections);
          if (!restored) {
            await hydrateAutoRemodelPickedState(auto.state, loadedDirections).catch(err => {
              console.warn('bootstrap auto-remodel pre-hydrate failed', err);
              return false;
            });
          }
        } else if (restoreAutoRemodel) {
          tabLoadedRef.current.delete('autoRemodel');
        }
      } catch (e) {
        console.warn('dashboard bootstrap failed', e);
        showToast('Failed to load dashboard startup data: ' + e.message, 'Error');
        tabLoadedRef.current.delete('autoRemodel');
      } finally {
        if (!cancelled) {
          setAutoRemodelRestoring(false);
          setPhaseAComplete(true);
        }
      }
      // Phase C list calls (loadAutoresearchWinnersPage, loadFilterPage,
      // loadRemodelPage, hydrateAutoRemodelPickedState, history fetches) are
      // intentionally NOT awaited here — the lazy tab loader effect below
      // fires the right one based on the active tab.
    })();
    return () => {
      cancelled = true;
      stopPoll('ar');
      stopPoll('filter');
      stopPoll('remodel');
      stopPoll('compliance');
    };
  }, [authState.status]);

  // Lazy first-page loader. Fires whenever the active tab changes (or once
  // Phase A finishes for the initial tab). Each tab loads its own first page
  // exactly once per session — re-entering a loaded tab does not refetch
  // unless the user explicitly clicks Refresh / Load older.
  const loadTabIfNeeded = async (currentTab) => {
    if (!currentTab) return;
    if (tabLoadedRef.current.has(currentTab)) return;
    const dirs = directionsRef.current;
    tabLoadedRef.current.add(currentTab);
    try {
      if (currentTab === 'research' || currentTab === 'autoSearch') {
        if (currentTab === 'research') await restoreSearchCacheOnce();
        const includeFailed = !searchPassesOnlyRef.current;
        await loadAutoresearchWinnersPage({
          reset: true,
          directionSource: dirs,
          lightweight: !includeFailed,
          includeFailed,
          pageSize: includeFailed ? AUTORESEARCH_FAIL_DISPLAY_LIMIT : AUTORESEARCH_PAGE_SIZE,
        });
      } else if (currentTab === 'filter') {
        await loadFilterPage({ reset: true });
      } else if (currentTab === 'remodel') {
        await loadRemodelPage({ reset: true });
      } else if (currentTab === 'compliance') {
        const [, state] = await Promise.all([
          loadCompliancePage({ reset: true }),
          refreshComplianceRunState(),
        ]);
        if (state?.running) startCompliancePolling();
      } else if (currentTab === 'staticAds') {
        const [, snap] = await Promise.all([
          loadStaticAdsRuns(),
          refreshStaticAdsState(),
        ]);
        if (snap?.running) startStaticAdsPolling();
      } else if (currentTab === 'autoRemodel' && showAutoRemodelUI()) {
        const auto = RESILIA_API.getAutoRemodelCurrent
          ? await RESILIA_API.getAutoRemodelCurrent()
          : await RESILIA_API.getAutoRemodelState();
        if (auto && auto.status === 'ok' && auto.state) {
          const restored = await applyAutoRemodelCurrentPayload(auto, dirs);
          if (!restored) await hydrateAutoRemodelPickedState(auto.state, dirs);
        }
      }
    } catch (e) {
      console.warn(`tab ${currentTab} lazy load failed`, e);
      tabLoadedRef.current.delete(currentTab);
    }
  };

  useEffect(() => {
    if (authState.status !== 'authenticated') return;
    if (!phaseAComplete) return;
    loadTabIfNeeded(tab);
    // tab + phaseAComplete trigger; loadTabIfNeeded reads the latest closures.
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [tab, phaseAComplete, authState.status]);

  const showToast = (msg, badge) => {
    setToast({ msg, badge });
    setTimeout(() => setToast(null), 3200);
  };

  const updateDedupeSettings = async (patch) => {
    const next = { ...dedupeSettings, ...patch };
    setDedupeSettings(next);
    setDedupeSaving(true);
    try {
      const saved = await RESILIA_API.updateDedupeSettings(next);
      setDedupeSettings({ ...DEDUPE_DEFAULTS, ...(saved || {}) });
      showToast('Dedupe preference saved.', 'Dedupe');
    } catch (e) {
      showToast(`Could not save dedupe preference: ${e.message}`, 'Error');
    } finally {
      setDedupeSaving(false);
    }
  };

  const directionMetaMap = (items) => Object.fromEntries((items || []).map(d => {
    const keywords = Array.isArray(d.keywords) ? d.keywords.filter(Boolean) : [];
    const label = d.display_name || d.brand_name || d.id;
    const searchLabel = keywords.length
      ? keywords.slice(0, 4).join(', ')
      : (d.brand_name || d.display_name || d.id);
    return [d.id, { label, searchLabel }];
  }));

  const attachDirectionMeta = (ad, metaMap) => {
    const meta = metaMap[ad.directionId];
    ad.directionLabel = meta?.label || ad.directionId || null;
    ad.directionSearchLabel = meta?.searchLabel || ad.directionLabel || null;
    return ad;
  };

  const adaptAutoresearchRows = (rows, directionSource = directionsRef.current) => {
    const dirMap = directionMetaMap(directionSource);
    const ads = [];
    const seen = new Set();
    for (const row of rows || []) {
      if (!row?.ad_id) continue;
      const adapted = RESILIA_ADAPT.adaptEvaluatedAd(row);
      attachDirectionMeta(adapted, dirMap);
      const key = autoresearchDisplayKey(adapted);
      if (seen.has(key)) continue;
      seen.add(key);
      ads.push({
        ...adapted,
        source: 'autoresearch',
        autoresearchRunId: row.autoresearch_run_id || row.run_id || null,
        currentAutoresearchRun: row.current_autoresearch_run === true,
        currentRunLabel: row.current_autoresearch_run === true ? 'Current' : null,
      });
    }
    return ads;
  };

  const mergeRecentAds = (incoming, existing = []) => {
    const out = [];
    const seen = new Set();
    [...(incoming || []), ...(existing || [])].forEach(ad => {
      if (!ad?.id || seen.has(ad.id)) return;
      seen.add(ad.id);
      out.push(ad);
    });
    return out;
  };

  const mergeCurrentPipelineAds = (incoming, existing = [], batchId = null) => {
    const currentIds = new Set((incoming || []).map(ad => ad?.id).filter(Boolean));
    return mergeRecentAds(
      (incoming || []).map(ad => ({
        ...ad,
        currentPipelineMove: true,
        pipelineMoveBatchId: batchId,
        currentRunLabel: 'Current',
      })),
      (existing || []).map(ad => (
        currentIds.has(ad.id)
          ? ad
          : { ...ad, currentPipelineMove: false, currentRunLabel: null }
      )),
    );
  };

  const focusPipelineAds = (targetTab, ids) => {
    const cleanIds = [...new Set((ids || []).filter(Boolean))];
    if (!cleanIds.length) return;
    pipelineFocusRef.current = {
      tab: targetTab,
      ids: cleanIds,
      tick: Date.now(),
    };
    setPipelineFocusTick(pipelineFocusRef.current.tick);
  };

  const appendOlderAds = (existing = [], incoming = []) => {
    const out = [];
    const seen = new Set();
    [...(existing || []), ...(incoming || [])].forEach(ad => {
      if (!ad?.id || seen.has(ad.id)) return;
      seen.add(ad.id);
      out.push(ad);
    });
    return out;
  };

  const pipelineFallbackAd = (id, source = 'manual') => ({
    id,
    brand: id || 'Recovered ad',
    platform: 'foreplay',
    copy: '',
    score: null,
    reasoning: null,
    daysRunning: 0,
    views: '—',
    thumb: RESILIA_ADAPT.gradFor(id || source),
    duration: '',
    scrapedAt: '—',
    cta: null,
    transcript: [],
    transcriptRaw: '',
    source,
    raw: { ad_id: id, source },
  });

  const adaptPipelineRows = (rows, source = 'manual') => (rows || [])
    .filter(row => row?.ad_id)
    .map(row => {
      const adapted = RESILIA_ADAPT.adaptEvaluatedAd(row);
      const pipelineAt = row.filter_verdict?.filtered_at || row.saved_at || null;
      const pipelineAtMs = pipelineAt ? Date.parse(pipelineAt) : NaN;
      return {
        ...adapted,
        source,
        filterVerdict: row.filter_verdict || null,
        pipelineAt,
        arrivedAtMs: Number.isFinite(pipelineAtMs)
          ? pipelineAtMs
          : (adapted.arrivedAtMs || 0),
      };
    });

  const remodelEntryFromJobs = (jobs) => {
    const allRows = Array.isArray(jobs) ? jobs : [];
    const latestRunId = allRows.find(row => row?.run_id)?.run_id || null;
    const rows = latestRunId ? allRows.filter(row => row.run_id === latestRunId) : allRows;
    if (!rows.length) return { status: 'pending', variants: [] };
    const done = rows.filter(j => j.status === 'done' && j.response_text);
    const failed = rows.filter(j => j.status === 'failed');
    const pending = rows.filter(j => j.status !== 'done' && j.status !== 'failed');
    const variantFromJob = (j, extra = {}) => ({
      jobId: j.id,
      variantIdx: j.variant_idx || 0,
      remodelMethod: normalizeRemodelMethod(j.remodel_method),
      temperature: j.temperature,
      promotedAt: j.promoted_at || null,
      ...extra,
    });
    if (done.length > 0 || failed.length > 0) {
      const variants = [
        ...done.map(j => variantFromJob(j, {
          raw: j.response_text,
          lines: parseRemodelScript(j.response_text),
        })),
        ...failed.map(j => variantFromJob(j, {
          raw: '',
          lines: [{ tag: 'ERROR', body: j.error || 'Generation failed' }],
        })),
        ...pending.map(j => variantFromJob(j, {
          pending: true,
          raw: '',
          lines: [{ tag: 'REMODELING', body: 'Claude Sonnet is generating this remodel output.' }],
        })),
      ].sort((a, b) => (a.variantIdx || 0) - (b.variantIdx || 0));
      return {
        status: pending.length ? 'pending' : 'ready',
        expectedVariants: variants.length,
        variants,
      };
    }
    if (failed.length === rows.length) {
      return {
        status: 'error',
        variants: [],
        error: failed[0]?.error || 'Generation failed',
      };
    }
    return { status: 'pending', variants: [] };
  };

  const groupJobsByAd = (jobs) => {
    const grouped = new Map();
    (jobs || []).forEach(job => {
      if (!job?.ad_id) return;
      const key = job.payload?.result_uuid || job.payload?.resultUuid || job.ad_id;
      if (!grouped.has(key)) grouped.set(key, []);
      grouped.get(key).push(job);
    });
    return grouped;
  };

  // ---- Autoresearch -------------------------------------------------------
  const loadAutoresearchWinnersPage = async ({
    reset = false,
    offset = null,
    pageSize = AUTORESEARCH_PAGE_SIZE,
    directionSource = directionsRef.current,
    lightweight = false,
    includeFailed = false,
    runId = null,
  } = {}) => {
    const perDir = autoresearchTargetRef.current || boundedPositiveIntOrNull(arTarget) || 1;
    const serverNextOffset = Number(autoresearchPage?.nextOffset);
    const pageOffset = offset === null
      ? (reset ? 0 : (Number.isFinite(serverNextOffset) ? serverNextOffset : autoresearchLoadedIdsRef.current.size))
      : Math.max(0, parseInt(offset, 10) || 0);
    const resolvedRunId = [runId, autoresearchBatchIdRef.current].find(
      (candidate) => typeof candidate === 'string' && candidate.startsWith('autoresearch-'),
    ) || null;
    const requestKey = JSON.stringify({
      pageSize, perDir, pageOffset, lightweight, includeFailed, runId: resolvedRunId,
    });
    const inFlight = autoresearchWinnersInFlightRef.current.get(requestKey);
    if (inFlight) return inFlight;
    const request = (async () => {
    setAutoresearchPage(prev => ({ ...prev, loading: true }));
    try {
      const page = await RESILIA_API.autoresearchWinners(
        pageSize,
        perDir,
        { offset: pageOffset, paged: true, lightweight, includeFailed, runId: resolvedRunId },
      );
      const rows = Array.isArray(page) ? page : (Array.isArray(page?.ads) ? page.ads : []);
      const historyRows = Array.isArray(page?.history_ads) ? page.history_ads : [];
      const currentBatchId = autoresearchBatchIdRef.current || 'latest-autoresearch';
      const currentRunId = page?.current_run_id || currentBatchId;
      const pageAds = adaptAutoresearchRows(rows, directionSource);
      const currentPageAds = pageAds.filter(ad => (
        ad.currentAutoresearchRun || (currentRunId && ad.autoresearchRunId === currentRunId)
      ));
      const historyPageAds = pageAds.filter(ad => !currentPageAds.includes(ad));
      const historyAds = [
        ...adaptAutoresearchRows(historyRows, directionSource),
        ...historyPageAds,
      ].map(ad => ({
        ...ad,
        _arBatchId: ad._arBatchId || 'history',
        currentAutoresearchRun: false,
        currentRunLabel: null,
      }));
      const ads = currentPageAds.map(ad => ({
        ...ad,
        _arBatchId: currentBatchId,
        currentAutoresearchRun: true,
        currentRunLabel: 'Current',
      }));
      const loadedIds = reset ? new Set() : new Set(autoresearchLoadedIdsRef.current);
      const currentIds = reset ? new Set() : new Set(autoresearchCurrentIdsRef.current);
      const loadedPlatformCounts = reset ? {} : { ...autoresearchLoadedPlatformCountsRef.current };
      [...ads, ...historyAds].forEach(ad => {
        const key = autoresearchDisplayKey(ad);
        const isNewLoadedCard = !loadedIds.has(key);
        loadedIds.add(key);
        if (!ad.currentAutoresearchRun) return;
        const isNewCurrentCard = !currentIds.has(key);
        currentIds.add(key);
        if (isNewLoadedCard && isNewCurrentCard) {
          const platform = sourcePlatformForAd(ad);
          loadedPlatformCounts[platform] = Number(loadedPlatformCounts[platform] || 0) + 1;
        }
      });
      autoresearchLoadedIdsRef.current = loadedIds;
      autoresearchCurrentIdsRef.current = currentIds;
      autoresearchLoadedPlatformCountsRef.current = loadedPlatformCounts;
      setResearchAds(prev => {
        const searchAds = prev.filter(a => a.source !== 'autoresearch');
        const priorAutoAds = prev
          .filter(a => (
            a.source === 'autoresearch'
            && (!a.currentAutoresearchRun || (a._arBatchId && a._arBatchId !== currentBatchId))
          ))
          .map(a => ({ ...a, currentAutoresearchRun: false, currentRunLabel: null }));
        const currentAutoAds = reset
          ? []
          : prev.filter(a => (
              a.source === 'autoresearch'
              && a.currentAutoresearchRun
              && (a._arBatchId || currentBatchId) === currentBatchId
            ));
        const byId = new Map(currentAutoAds.map(ad => [autoresearchDisplayKey(ad), ad]));
        ads.forEach(ad => byId.set(autoresearchDisplayKey(ad), ad));
        const currentKeys = new Set(Array.from(byId.values()).map(autoresearchDisplayKey));
        const priorByKey = new Map(priorAutoAds.map(ad => [autoresearchDisplayKey(ad), ad]));
        historyAds.forEach(ad => {
          const key = autoresearchDisplayKey(ad);
          if (!currentKeys.has(key)) priorByKey.set(key, ad);
        });
        // Layout: search ads on top (Bug 11 quick fix), then current batch
        // of autoresearch winners, then older autoresearch batches as
        // history beneath.
        const nextAds = [...searchAds, ...Array.from(byId.values()), ...Array.from(priorByKey.values())];
        return nextAds;
      });
      const hasMore = Array.isArray(page) ? ads.length >= AUTORESEARCH_PAGE_SIZE : !!page?.has_more;
      const total = Array.isArray(page) ? null : (page?.total ?? null);
      const currentRunTotal = Array.isArray(page)
        ? currentIds.size
        : Number(page?.current_run_total ?? currentIds.size);
      const currentRunPlatformTotals = (
        !Array.isArray(page) && page?.current_run_platform_totals && typeof page.current_run_platform_totals === 'object'
      ) ? page.current_run_platform_totals : loadedPlatformCounts;
      const serverNextPageOffset = Number(page?.next_offset);
      const nextState = {
        loaded: loadedIds.size,
        total,
        hasMore,
        loading: false,
        nextOffset: Number.isFinite(serverNextPageOffset) ? serverNextPageOffset : null,
      };
      setAutoresearchPage(nextState);
      setLatestAutoresearchRun(prev => ({
        id: currentBatchId,
        count: currentRunTotal,
        total: total ?? prev?.total ?? null,
        status: prev?.status || 'restored',
        labels: prev?.labels || [],
      }));
      setArProgress(prev => {
        if (!prev) return prev;
        const platformProgress = { ...(prev.platformProgress || {}) };
        Object.entries(currentRunPlatformTotals).forEach(([platform, count]) => {
          const entry = { ...(platformProgress[platform] || { source: platform }) };
          entry.winners = Number(count || 0);
          entry.visible_winners = Number(count || 0);
          platformProgress[platform] = entry;
        });
        return {
          ...prev,
          winners: currentRunTotal,
          loadedWinners: currentIds.size,
          totalWinners: currentRunTotal,
          platformProgress,
        };
      });
      if (loadedIds.size > 0) setAutoresearchActive(true);
      return { ads, ...nextState };
    } finally {
      autoresearchWinnersInFlightRef.current.delete(requestKey);
      setAutoresearchPage(prev => ({ ...prev, loading: false }));
    }
    })();
    autoresearchWinnersInFlightRef.current.set(requestKey, request);
    return request;
  };

  const handleSearchPassesOnlyChange = (nextPassesOnly) => {
    setSearchPassesOnly(nextPassesOnly);
    searchPassesOnlyRef.current = nextPassesOnly;
    if (nextPassesOnly) return;
    loadAutoresearchWinnersPage({
      reset: true,
      pageSize: AUTORESEARCH_FAIL_DISPLAY_LIMIT,
      directionSource: directionsRef.current,
      includeFailed: true,
    }).catch(err => showToast('Failed to load failing AutoSearch ads: ' + err.message, 'Error'));
  };

  const loadFilterPage = async ({ reset = false } = {}) => {
    const offset = reset ? 0 : filterPage.loaded;
    setFilterPage(prev => ({ ...prev, loading: true }));
    try {
      const rows = await RESILIA_API.listFilterVerdicts({
        passesOnly: false,
        source: 'manual',
        limit: PIPELINE_PAGE_SIZE,
        offset,
      });
      const ads = adaptPipelineRows(rows, 'filter');
      setFilteredAds(prev => reset ? mergeRecentAds(ads, []) : appendOlderAds(prev, ads));
      const nextState = {
        loaded: offset + rows.length,
        hasMore: rows.length >= PIPELINE_PAGE_SIZE,
        loading: false,
      };
      setFilterPage(nextState);
      return { ads, ...nextState };
    } finally {
      setFilterPage(prev => ({ ...prev, loading: false }));
    }
  };

  const loadRemodelPage = async ({ reset = false } = {}) => {
    const offset = reset ? 0 : remodelPage.loaded;
    setRemodelPage(prev => ({ ...prev, loading: true }));
    try {
      const jobs = [];
      const grouped = new Map();
      let nextOffset = offset;
      let hasMore = false;
      let pageReads = 0;
      while (grouped.size < PIPELINE_PAGE_SIZE && pageReads < REMODEL_VARIANTS_MAX) {
        const pageJobs = await RESILIA_API.listRemodelJobs({
          source: 'manual',
          limit: PIPELINE_PAGE_SIZE,
          offset: nextOffset,
        });
        pageReads += 1;
        if (!pageJobs.length) {
          hasMore = false;
          break;
        }
        jobs.push(...pageJobs);
        nextOffset += pageJobs.length;
        groupJobsByAd(pageJobs).forEach((rows, key) => {
          const existing = grouped.get(key) || [];
          grouped.set(key, [...existing, ...rows]);
        });
        hasMore = pageJobs.length >= PIPELINE_PAGE_SIZE;
        if (!hasMore) break;
      }
      const adIds = [...new Set((jobs || []).map(job => job.ad_id).filter(Boolean))];
      const swipeRows = adIds.length ? await RESILIA_API.getSwipesBatch(adIds) : [];
      const rowsById = new Map((swipeRows || []).map(row => [row.ad_id, row]));
      const ads = [...grouped.entries()].map(([_key, rows]) => {
        const firstJob = rows[0] || {};
        const payload = firstJob.payload && typeof firstJob.payload === 'object' ? firstJob.payload : null;
        const row = payload || rowsById.get(firstJob.ad_id);
        return row ? { ...RESILIA_ADAPT.adaptEvaluatedAd(row), source: 'remodel' } : pipelineFallbackAd(firstJob.ad_id, 'remodel');
      });
      setRemodelScripts(prev => {
        const next = { ...prev };
        grouped.forEach((rows, key) => { next[key] = remodelEntryFromJobs(rows); });
        return next;
      });
      setRemodeledAds(prev => {
        if (!reset) return appendOlderAds(prev, ads);
        const currentSeeds = prev.filter(ad => ad.currentPipelineMove);
        return mergeRecentAds(ads, currentSeeds);
      });
      const nextState = {
        loaded: nextOffset,
        hasMore,
        loading: false,
      };
      setRemodelPage(nextState);
      return { ads, ...nextState };
    } finally {
      setRemodelPage(prev => ({ ...prev, loading: false }));
    }
  };

  const loadCompliancePage = async ({ reset = false } = {}) => {
    const offset = reset ? 0 : compliancePage.loaded;
    setCompliancePage(prev => ({ ...prev, loading: true }));
    try {
      const jobs = await RESILIA_API.listComplianceJobs({
        limit: PIPELINE_PAGE_SIZE,
        offset,
      });
      setComplianceJobs(prev => reset ? jobs : [...prev, ...jobs]);
      const nextState = {
        loaded: offset + jobs.length,
        hasMore: jobs.length >= PIPELINE_PAGE_SIZE,
        loading: false,
      };
      setCompliancePage(nextState);
      return { jobs, ...nextState };
    } finally {
      setCompliancePage(prev => ({ ...prev, loading: false }));
    }
  };

  const refreshFilterRunState = async () => {
    const state = await RESILIA_API.getFilterState();
    if (state) {
      setFilterRunState(state);
      setFilterBusy(!!state.running);
      if (!state.running && filterPollRef.current) {
        stopPoll('filter');
      }
    }
    return state;
  };

  const refreshComplianceRunState = async () => {
    const state = await RESILIA_API.getComplianceState();
    if (state) {
      setComplianceRunState(state);
      setComplianceBusy(!!state.running);
      if (!state.running && compliancePollRef.current) {
        stopPoll('compliance');
      }
    }
    return state;
  };

  const startCompliancePolling = () => {
    startVisibilityAwarePoll('compliance', async () => {
      try {
        await Promise.all([
          loadCompliancePage({ reset: true }),
          refreshComplianceRunState(),
        ]);
      } catch (e) {
        console.warn('compliance poll failed', e);
      }
    }, COMPLIANCE_POLL_INTERVAL_MS);
  };

  const startFilterStatePolling = () => {
    startVisibilityAwarePoll('filter', async () => {
      try {
        await Promise.all([
          loadFilterPage({ reset: true }),
          refreshFilterRunState(),
        ]);
      } catch (e) {
        console.warn('filter state poll failed', e);
      }
    }, FILTER_POLL_INTERVAL_MS);
  };

  // ---- Static Ads (standalone image-ad surface) -------------------------
  // /state returns the active run (with its variants) in one call; /runs is
  // the owner-scoped history. Polling stops once the active run is terminal.
  const refreshStaticAdsState = async () => {
    const snap = await RESILIA_API.getStaticAdsState();
    if (!snap) return null;
    let run = snap.run || null;
    if (run) {
      saCurrentRunIdRef.current = run.run_id;
    } else if (saCurrentRunIdRef.current) {
      // No active run, but one just finished — re-fetch it so the completed
      // result + gallery stay visible (orchestrator emits variants only at done).
      run = await RESILIA_API.getStaticAdsRun(saCurrentRunIdRef.current);
    }
    setSaActiveRun(run);
    setSaStatus(run?.status || null);
    setSaState(run?.state || snap.state || null);
    setSaRunning(!!snap.running);
    if (!snap.running && saPollRef.current) stopPoll('staticAds');
    return snap;
  };

  const loadStaticAdsRuns = async () => {
    try {
      const out = await RESILIA_API.listStaticAdsRuns({ limit: 50 });
      setSaRuns(Array.isArray(out?.runs) ? out.runs : []);
    } catch (e) {
      console.warn('static ads runs load failed', e);
    }
  };

  const startStaticAdsPolling = () => {
    startVisibilityAwarePoll('staticAds', async () => {
      try {
        await Promise.all([refreshStaticAdsState(), loadStaticAdsRuns()]);
      } catch (e) {
        console.warn('static ads poll failed', e);
      }
    }, STATIC_ADS_POLL_INTERVAL_MS);
  };

  const handleRunStaticAds = async ({ keyword, brand, nVariants, strictness }) => {
    setSaError(null);
    setSaStarting(true);
    try {
      const res = await RESILIA_API.runStaticAds({ keyword, brand, nVariants, strictness });
      if (res?.run_id) saCurrentRunIdRef.current = res.run_id;
      await Promise.all([refreshStaticAdsState(), loadStaticAdsRuns()]);
      startStaticAdsPolling();
    } catch (e) {
      setSaError(e.message || 'Failed to start run');
    } finally {
      setSaStarting(false);
    }
  };

  const handleStopStaticAds = async (runId) => {
    const id = runId || saActiveRun?.run_id;
    if (!id) return;
    try {
      await RESILIA_API.stopStaticAdsRun(id);
      await Promise.all([refreshStaticAdsState(), loadStaticAdsRuns()]);
    } catch (e) {
      console.warn('static ads stop failed', e);
    }
  };

  const handleDeleteStaticAdsRun = async (runId) => {
    if (!runId) return;
    try {
      await RESILIA_API.deleteStaticAdsRun(runId);
      if (saCurrentRunIdRef.current === runId) saCurrentRunIdRef.current = null;
      if (saActiveRun?.run_id === runId) {
        setSaActiveRun(null);
        setSaStatus(null);
        setSaState(null);
        setSaRunning(false);
      }
      await loadStaticAdsRuns();
    } catch (e) {
      console.warn('static ads delete failed', e);
    }
  };

  // --- per-variant actions (#221) ---
  const handleSaveStaticAdsVariant = async (runId, variantId, saved) => {
    if (!runId || variantId == null) return;
    try {
      await RESILIA_API.saveStaticAdsVariant(runId, variantId, saved);
      await Promise.all([refreshStaticAdsState(), loadStaticAdsRuns()]);
    } catch (e) {
      console.warn('static ads variant save failed', e);
      setSaError(e.message || 'Failed to save variant');
    }
  };

  const handleRegenerateStaticAdsVariant = async (runId, variantId, promptOverride) => {
    if (!runId || variantId == null) return;
    setSaError(null);
    try {
      const res = await RESILIA_API.regenerateStaticAdsVariant(runId, variantId, promptOverride);
      if (res?.run_id) saCurrentRunIdRef.current = res.run_id;
      // Run flips to "running" server-side; resume polling so the new sibling appears.
      await Promise.all([refreshStaticAdsState(), loadStaticAdsRuns()]);
      startStaticAdsPolling();
    } catch (e) {
      console.warn('static ads variant regenerate failed', e);
      setSaError(e.message || 'Failed to regenerate variant');
    }
  };

  const handleDeleteStaticAdsVariant = async (runId, variantId) => {
    if (!runId || variantId == null) return;
    try {
      await RESILIA_API.deleteStaticAdsVariant(runId, variantId);
      await Promise.all([refreshStaticAdsState(), loadStaticAdsRuns()]);
    } catch (e) {
      console.warn('static ads variant delete failed', e);
      setSaError(e.message || 'Failed to delete variant');
    }
  };

  const refreshStaticAds = async () => {
    await Promise.all([refreshStaticAdsState(), loadStaticAdsRuns()]);
  };

  const refreshRemodelRunState = async () => {
    const state = await RESILIA_API.getRemodelState();
    if (state) {
      setRemodelRunState(state);
      setRemodelBusy(!!state.running);
      if (!state.running && remodelPollRef.current) {
        stopPoll('remodel');
      }
      if (!state.running) {
        remodelStartPendingRef.current = false;
      }
    }
    return state;
  };

  const autoRemodelFallbackAd = (id, job = {}) => ({
    id,
    brand: job.ad_id || id,
    platform: job.platform || 'foreplay',
    copy: '',
    score: null,
    reasoning: null,
    daysRunning: 0,
    directionLabel: 'Auto-Remodel',
    directionSearchLabel: 'Auto-Remodel',
    transcript: [],
    transcriptRaw: null,
    source: 'auto_remodel',
  });

  const syncAutoRemodelState = (state) => {
    if (!state) return;
    setArRunId(state.run_id || null);
    arRunIdRef.current = state.run_id || null;
    setArStage(state.stage || null);
    arStageRef.current = state.stage || null;
    setArLastEvent({ ...state, run_id: state.run_id, stage: state.stage });
    if (typeof state.directions_done === 'number') setArDirectionsDone(state.directions_done);
    if (typeof state.directions_total === 'number') setArDirectionsTotal(state.directions_total);
    if (typeof state.ads_found === 'number') setArAdsFound(state.ads_found);
    if (typeof state.target_ads === 'number') {
      setArTargetAds(state.target_ads);
      arTargetAdsRef.current = state.target_ads;
    }
    if (typeof state.n_variants === 'number') {
      setArNVariants(state.n_variants);
      arNVariantsRef.current = state.n_variants;
    }
    if (Object.prototype.hasOwnProperty.call(state, 'strategy')) {
      setAutoRemodelPageInstruction(capRemodelInstruction(state.strategy || ''));
    }
    const completedVariants = (parseInt(state.completed, 10) || 0) + (parseInt(state.failed, 10) || 0);
    if (completedVariants > 0) setArVariantsDone(completedVariants);
    const startedVariants = parseInt(state.jobs_started ?? state.jobs_created, 10);
    if (Number.isFinite(startedVariants) && startedVariants > 0) {
      setArVariantsTotalOverride(startedVariants);
    } else if (state.stage === 'cancelled' || state.stage === 'failed') {
      setArVariantsTotalOverride(null);
      setArVariantsDone(0);
    }
    if (Array.isArray(state.picked_ad_ids)) {
      setArPickedIds(state.picked_ad_ids);
      arPickedIdsRef.current = state.picked_ad_ids;
    }
  };

  const searchFiltersPayload = (filters = {}) => {
    const out = {};
    const minLikes = parseInt(filters.minLikes, 10);
    const maxLikes = parseInt(filters.maxLikes, 10);
    const minDailyLikes = parseInt(filters.minDailyLikes, 10);
    const maxDailyLikes = parseInt(filters.maxDailyLikes, 10);
    const adspyAgeMin = parseInt(filters.adspyAgeMin, 10);
    const adspyAgeMax = parseInt(filters.adspyAgeMax, 10);
    const adspyUserId = parseInt(filters.adspyUserId, 10);
    const adspyAffNetwork = parseInt(filters.adspyAffNetwork, 10);
    const fpMinDays = parseInt(filters.fpMinDays, 10);
    const bsMinViews = parseInt(filters.bsMinViews, 10);
    const bsMinLikes = parseInt(filters.bsMinLikes, 10);
    const bsMinEngagement = parseFloat(filters.bsMinEngagement);
    const bsMinReach = parseInt(filters.bsMinReach, 10);
    const bsMinSpend = parseInt(filters.bsMinSpend, 10);
    const bsMaxSpend = parseInt(filters.bsMaxSpend, 10);
    if (filters.fpFormat) out.foreplay_display_format = [filters.fpFormat];
    if ((filters.fpPublisherPlatforms || '').trim()) out.foreplay_publisher_platform = filters.fpPublisherPlatforms.split(',').map(s => s.trim()).filter(Boolean);
    if ((filters.fpLanguages || '').trim()) out.foreplay_languages = filters.fpLanguages.split(',').map(s => s.trim()).filter(Boolean);
    if (filters.fpLiveStatus === 'live') out.foreplay_live = true;
    else if (filters.fpLiveStatus === 'inactive') out.foreplay_live = false;
    if (Number.isFinite(fpMinDays) && fpMinDays > 0) out.foreplay_running_duration_min_days = fpMinDays;
    if (filters.fpOrder) out.foreplay_order = filters.fpOrder;
    if (Number.isFinite(minLikes) && minLikes > 0) out.adspy_min_likes = minLikes;
    if (Number.isFinite(maxLikes) && maxLikes > 0 && maxLikes < 100000) out.adspy_max_likes = maxLikes;
    if (Number.isFinite(minDailyLikes) && minDailyLikes > 0) out.adspy_min_daily_likes = minDailyLikes;
    if (Number.isFinite(maxDailyLikes) && maxDailyLikes > 0 && maxDailyLikes < 1000) out.adspy_max_daily_likes = maxDailyLikes;
    if (filters.orderBy) out.adspy_order = filters.orderBy;
    if (filters.adspySiteType) out.adspy_site_type = filters.adspySiteType;
    if (filters.adspyMediaType) out.adspy_media_type = filters.adspyMediaType;
    if (filters.adspyGender) out.adspy_gender = filters.adspyGender;
    if (Number.isFinite(adspyAgeMin) && adspyAgeMin > 0) out.adspy_age_min = adspyAgeMin;
    if (Number.isFinite(adspyAgeMax) && adspyAgeMax > 0) out.adspy_age_max = adspyAgeMax;
    if ((filters.adspyCountries || '').trim()) out.countries = filters.adspyCountries.split(',').map(s => s.trim()).filter(Boolean);
    if ((filters.adspyLang || '').trim()) out.languages = [filters.adspyLang.trim()];
    if ((filters.adspyCreatedFrom || '').trim()) out.adspy_created_from = filters.adspyCreatedFrom.trim();
    if ((filters.adspyCreatedTo || '').trim()) out.adspy_created_to = filters.adspyCreatedTo.trim();
    if ((filters.adspySeenFrom || '').trim()) out.adspy_seen_from = filters.adspySeenFrom.trim();
    if ((filters.adspySeenTo || '').trim()) out.adspy_seen_to = filters.adspySeenTo.trim();
    if ((filters.adspyUsername || '').trim()) out.adspy_username = filters.adspyUsername.trim();
    if (Number.isFinite(adspyUserId) && adspyUserId > 0) out.adspy_user_id = adspyUserId;
    if (Number.isFinite(adspyAffNetwork) && adspyAffNetwork > 0) out.adspy_aff_network = adspyAffNetwork;
    if ((filters.adspyAffId || '').trim()) out.adspy_aff_id = filters.adspyAffId.trim();
    if ((filters.adspyOfferId || '').trim()) out.adspy_offer_id = filters.adspyOfferId.trim();
    if ((filters.adspyTech || '').trim()) out.adspy_tech = filters.adspyTech.split(',').map(s => s.trim()).filter(Boolean);
    if ((filters.adspyButtons || '').trim()) out.adspy_buttons = filters.adspyButtons.trim();
    if ((filters.adspyLandingUrl || '').trim()) out.adspy_url_search_type = 'lp_urls';
    else if ((filters.adspyUrl || '').trim()) out.adspy_url_search_type = 'urls';
    if (filters.bsPlatform) out.brandsearch_platforms = [filters.bsPlatform];
    if (Number.isFinite(bsMinViews) && bsMinViews > 0) out.brandsearch_min_views = bsMinViews;
    if (Number.isFinite(bsMinLikes) && bsMinLikes > 0) out.brandsearch_min_likes = bsMinLikes;
    if (Number.isFinite(bsMinEngagement) && bsMinEngagement > 0) out.brandsearch_min_engagement_rate = bsMinEngagement;
    if (Number.isFinite(bsMinReach) && bsMinReach > 0) out.brandsearch_min_reach = bsMinReach;
    if (Number.isFinite(bsMinSpend) && bsMinSpend > 0) out.brandsearch_min_spend = bsMinSpend;
    if (Number.isFinite(bsMaxSpend) && bsMaxSpend > 0) out.brandsearch_max_spend = bsMaxSpend;
    return Object.keys(out).length ? out : null;
  };

  const searchSourceUrlsPayload = (filters = {}) => {
    const out = {};
    const foreplayUrl = (filters.foreplayUrl || '').trim();
    const adspyUrl = (filters.adspyUrl || '').trim();
    const adspyLandingUrl = (filters.adspyLandingUrl || '').trim();
    const brandsearchUrl = (filters.brandsearchUrl || '').trim();
    if (foreplayUrl) out.foreplay = foreplayUrl;
    if (adspyUrl || adspyLandingUrl) out.adspy = adspyUrl || adspyLandingUrl;
    if (brandsearchUrl) out.brandsearch = brandsearchUrl;
    return Object.keys(out).length ? out : null;
  };

  const expandAd = (ad, opts = {}) => {
    const drawerAd = opts.autoPlay ? { ...ad, __autoPlay: true } : ad;
    setExpandedAd(drawerAd);
    if (!ad) return;
    // Bug: the dashboard used to only hydrate transcripts for BrandSearch
    // ads. AdSpy and Foreplay video ads now also Whisper-transcribe on
    // demand server-side (search_service.get_ad_transcript), so trigger the
    // same hydration whenever the card has no real transcript yet.
    const hasTranscript = ad.isVoiceTranscript !== false
      && ad.transcriptSource !== 'copy_fallback'
      && ((ad.transcriptRaw && String(ad.transcriptRaw).trim())
        || (Array.isArray(ad.transcript) && ad.transcript.length > 0));
    if (hasTranscript) return;
    RESILIA_API.fetchTranscript(ad.id).then(detail => {
      if (!detail || !detail.transcript) return;
      const patch = (item) => {
        if (item.id !== ad.id) return item;
        const adapted = RESILIA_ADAPT.adaptSearchAd({
          ...(item.raw || {}),
          ad_id: item.id,
          brand: item.brand,
          platform: item.publisherPlatform,
          source_platform: item.sourcePlatform || item.source_platform || item.raw?.source_platform || item.platform,
          score: item.score,
          reasoning: item.reasoning,
          running_days: item.daysRunning,
          creative_url: item.creativeUrl,
          thumbnail_url: item.thumbnailUrl,
          audio_duration: detail.audio_duration,
          video_duration: item.raw?.video_duration || detail.raw?.video_duration || detail.audio_duration,
          transcript: detail.transcript,
          transcript_segments: detail.transcript_segments,
          transcript_status: detail.transcript_status,
          transcript_source: detail.transcript_source,
          is_voice_transcript: detail.is_voice_transcript,
          fallback_copy: detail.fallback_copy,
          transcript_provider: detail.transcript_provider,
          transcript_model: detail.transcript_model,
          transcript_word_count: detail.transcript_word_count,
          creative_url: detail.creative_url || detail.video_url || item.creativeUrl,
          raw: { ...(item.raw || {}), ...(detail.raw || {}) },
          metrics: item.raw?.metrics || detail.raw?.metrics || {},
        }, item.sourcePlatform || item.source_platform || item.raw?.source_platform || item.platform);
        adapted.directionId = item.directionId;
        adapted.directionLabel = item.directionLabel;
        adapted.directionSearchLabel = item.directionSearchLabel;
        return {
          ...item,
          ...adapted,
          source: item.source,
          resultUuid: item.resultUuid,
          cardKey: item.cardKey,
          runId: item.runId,
          arrivedAtMs: item.arrivedAtMs || adapted.arrivedAtMs || 0,
        };
      };
      setResearchAds(prev => prev.map(patch));
      setFilteredAds(prev => prev.map(patch));
      setRemodeledAds(prev => prev.map(patch));
      setAutoRemodelAds(prev => prev.map(patch));
      setExpandedAd(prev => prev && prev.id === ad.id ? patch(prev) : prev);
    }).catch(e => console.warn('brandsearch transcript hydration failed', e));
  };

  // ---- Manual search ------------------------------------------------------
  const manualSearchLabelForRow = (row) => {
    if (row.competitorId) {
      if (row.competitorId === MANUAL_SEARCH_ALL_COMPETITORS_ID) return 'All competitors';
      const competitor = competitors.find(c => String(c.id) === String(row.competitorId));
      return competitor?.name || row.competitorId;
    }
    const keyword = String(row.query || '').trim();
    if (keyword) return keyword;
    const urls = searchSourceUrlsPayload(row.filters);
    if (urls?.foreplay_url) return urls.foreplay_url;
    if (urls?.adspy_url) return urls.adspy_url;
    if (urls?.adspy_landing_url) return urls.adspy_landing_url;
    if (urls?.brandsearch_url) return urls.brandsearch_url;
    return null;
  };

  const runSearchRows = async (targetRows, { global = false } = {}) => {
    searchStopRequestedRef.current = false;
    const activeRows = (targetRows || []).filter(row => {
      const sourceUrls = searchSourceUrlsPayload(row.filters);
      return row.query || row.competitorId || sourceUrls;
    });
    if (!activeRows.length) {
      showToast('No searchable row configured. Add a keyword, competitor, or source URL.', 'Search');
      return [];
    }
    const activeRowIds = activeRows.map(row => row.id).filter(id => id !== null && id !== undefined);
    const searchBatchId = `manual_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
    const searchRunLabels = [...new Set(activeRows.map(manualSearchLabelForRow).filter(Boolean))];
    if (global) setSearching(true);
    setSearchingRowIds(prev => {
      const next = new Set(prev);
      activeRowIds.forEach(id => next.add(id));
      return next;
    });
    setResearchAds(prev => prev.map(ad => (
      ad.source === 'autoresearch' ? ad : { ...ad, currentSearchRun: false }
    )));
    setSearchWarnings([]);
    const collectedWarnings = [];
    const rowResults = new Map();
    const rowDuplicatesHidden = new Map();
    const rowProgress = new Map();
    const combinedProgress = () => {
      const out = {};
      for (const progress of rowProgress.values()) {
        if (!progress || typeof progress !== 'object') continue;
        for (const [source, value] of Object.entries(progress)) {
          out[source] = value;
        }
      }
      return out;
    };
    const updateSearchSummary = (meta = {}) => {
      const count = [...rowResults.values()].reduce((n, ads) => n + ads.length, 0);
      const hidden = [...rowDuplicatesHidden.values()].reduce((n, value) => n + Number(value || 0), 0);
      setLatestSearchRun(prev => {
        const nextSearchPhase = meta.searchPhase ?? prev?.searchPhase ?? null;
        return {
          id: searchBatchId,
          count,
          labels: searchRunLabels,
          progress: combinedProgress(),
          sharedPoolPrefill: nextSearchPhase === 'live_search'
            ? null
            : meta.sharedPoolPrefill ?? prev?.sharedPoolPrefill ?? null,
          searchPhase: nextSearchPhase,
          duplicatesHidden: hidden,
        };
      });
    };
    const runOneRow = async (row) => {
      const rowKey = row.id ?? `${searchBatchId}_${rowResults.size}`;
      const rowRunId = `${searchBatchId}_${rowKey}`;
      const sourceUrls = searchSourceUrlsPayload(row.filters);
      const searchReturnLabel = manualSearchLabelForRow(row);
      const platforms = Object.keys(row.platforms).filter(k => row.platforms[k]);
      const platformList = platforms.length ? platforms : ['foreplay'];
      const basePayload = {
        query: row.query,
        competitorId: row.competitorId,
        count: Math.min(row.count, 100),
        platforms: platformList,
        filters: searchFiltersPayload(row.filters),
        sourceUrls,
      };
      const isCompetitorSearch = !!row.competitorId;
      const seedSearchProgress = () => {
        const seeded = {};
        for (const platformKey of platformList) {
          if (isCompetitorSearch) {
            seeded[platformKey] = {
              source: platformKey,
              status: 'queued',
              stage: 'queued',
              unit: 'shared_pool',
              kept: 0,
              target: basePayload.count,
              target_kind: 'winning',
              message: `Waiting to check ${platformKey} shared pool.`,
            };
          } else if (platformKey === 'brandsearch') {
            seeded[platformKey] = {
              source: platformKey,
              status: 'queued',
              stage: 'queued',
              unit: 'ads',
              kept: 0,
              target: basePayload.count,
              target_kind: 'winning',
              ads_checked: 0,
              message: `Waiting to search ${platformKey}.`,
            };
          } else {
            seeded[platformKey] = {
              source: platformKey,
              status: 'queued',
              stage: 'queued',
              unit: 'pages',
              kept: 0,
              target: basePayload.count,
              target_kind: 'winning',
              pages_fetched: 0,
              raw_ads_seen: 0,
              message: `Waiting to search ${platformKey}.`,
            };
          }
        }
        rowProgress.set(rowKey, seeded);
        updateSearchSummary({
          searchPhase: isCompetitorSearch ? 'queued' : 'live_search',
          sharedPoolPrefill: null,
        });
      };
      const applySearchResponse = (resp, { partial = false } = {}) => {
        const seedArrivedAtMs = Date.now();
        const ads = (resp?.ads || []).map(a => {
          const platform = a?.source_platform || a?.raw?.source_platform || 'foreplay';
          const adapted = RESILIA_ADAPT.adaptSearchAd(a, platform);
          return {
            ...adapted,
            source: 'search',
            searchReturnLabel,
            searchRunId: rowRunId,
            searchBatchId,
            searchRowId: rowKey,
            currentSearchRun: true,
            arrivedAtMs: adapted.arrivedAtMs || seedArrivedAtMs,
          };
        });
        const historyAds = (resp?.history_ads || []).map(a => ({
          ...RESILIA_ADAPT.adaptSearchAd(a, a?.source_platform || 'foreplay'),
          source: 'search',
          searchReturnLabel: 'Duplicate history',
          currentSearchRun: false,
        }));
        const deduped = uniqueSearchAds(ads);
        if (resp && typeof resp.duplicates_hidden === 'number') {
          rowDuplicatesHidden.set(rowKey, resp.duplicates_hidden);
        }
        if (resp && resp.progress && typeof resp.progress === 'object') {
          rowProgress.set(rowKey, { ...(rowProgress.get(rowKey) || {}), ...resp.progress });
        }
        if (partial && !deduped.length && !historyAds.length) {
          if (resp && (resp.progress || typeof resp.duplicates_hidden === 'number')) {
            setLatestSearchRun({
              id: searchBatchId,
              count: (rowResults.get(rowKey) || []).length,
              labels: searchRunLabels,
              duplicatesHidden: Number(rowDuplicatesHidden.get(rowKey) || 0),
              progress: combinedProgress(),
              sharedPoolPrefill: resp?.shared_pool_prefill ?? null,
              searchPhase: resp?.search_phase ?? null,
            });
          } else {
            updateSearchSummary();
          }
          return rowResults.get(rowKey) || [];
        }
        if (!partial || deduped.length) {
          rowResults.set(rowKey, deduped);
        }
        setResearchAds(prev => {
          const autoresearchAds = prev.filter(a => a.source === 'autoresearch');
          const priorSearchAds = prev.filter(a => a.source !== 'autoresearch');
          const merged = partial && !deduped.length
            ? (rowResults.get(rowKey) || [])
            : deduped;
          const newKeys = new Set([...merged, ...historyAds].map(autoresearchDisplayKey));
          const remainingPrior = priorSearchAds
            .filter(a => !newKeys.has(autoresearchDisplayKey(a)))
            .map(a => (
              a.searchRowId === rowKey
                ? { ...a, currentSearchRun: false }
                : a
            ));
          return [...merged, ...historyAds, ...remainingPrior, ...autoresearchAds];
        });
        updateSearchSummary({
          sharedPoolPrefill: resp?.shared_pool_prefill ?? undefined,
          searchPhase: resp?.search_phase ?? undefined,
        });
        return rowResults.get(rowKey) || deduped;
      };
      const activeKey = String(rowKey);
      try {
        seedSearchProgress();
        let resp = await RESILIA_API.search(basePayload);
        if (resp && resp.status === 'started' && resp.run_id && RESILIA_API.waitForSearchRun) {
          activeSearchRunIdsRef.current.set(activeKey, resp.run_id);
          persistActiveSearchRuns();
          if (searchStopRequestedRef.current && RESILIA_API.cancelSearchRun) {
            await RESILIA_API.cancelSearchRun(resp.run_id);
          }
          resp = await RESILIA_API.waitForSearchRun(resp.run_id, {
            onProgress: progress => applySearchResponse(progress, { partial: true }),
          });
        }
        const deduped = applySearchResponse(resp);
        if (resp && typeof resp.credits_remaining === 'number') {
          setPlatformCredits(pc => ({ ...pc, foreplay: resp.credits_remaining }));
        }
        if (resp && Array.isArray(resp.warnings) && resp.warnings.length) {
          for (const w of resp.warnings) {
            if (w && typeof w === 'object') {
              collectedWarnings.push({ ...w, query: row.query || null });
            }
          }
          const labels = resp.warnings.map(w => w.source).filter(Boolean).join(', ');
          showToast(`Search completed with ${labels || resp.warnings.length} warning(s).`, 'Warn');
        }
        return deduped;
      } catch (e) {
        updateSearchSummary();
        if (searchStopRequestedRef.current || String(e.message || '').includes('cancelled')) {
          showToast(`Search stopped for ${row.query || row.competitorId}.`, 'Search');
        } else {
          showToast(`Search failed for ${row.query || row.competitorId}: ${e.message}`, 'Error');
        }
        return rowResults.get(rowKey) || [];
      } finally {
        activeSearchRunIdsRef.current.delete(activeKey);
        persistActiveSearchRuns();
        if (row.id !== null && row.id !== undefined) {
          setSearchingRowIds(prev => {
            const next = new Set(prev);
            next.delete(row.id);
            return next;
          });
        }
      }
    };
    try {
      const results = (await Promise.all(activeRows.map(runOneRow))).flat();
      // dedupe by ad id across rows
      const seen = new Set();
      const deduped = [];
      for (const a of results) {
        if (seen.has(a.id)) continue;
        seen.add(a.id);
        deduped.push(a);
      }
      const hidden = [...rowDuplicatesHidden.values()].reduce((n, count) => n + Number(count || 0), 0);
      setLatestSearchRun({
        id: searchBatchId,
        count: deduped.length,
        labels: searchRunLabels,
        duplicatesHidden: hidden,
        progress: combinedProgress(),
      });
      if (searchStopRequestedRef.current) {
        showToast(`${deduped.length} ads kept from stopped search.`, 'Search');
      } else if (deduped.length === 0) {
        showToast('No ads returned. Check your query or competitor.', 'Search');
      } else {
        showToast(
          hidden ? `${deduped.length} ads fetched, ${hidden} duplicate(s) hidden.` : `${deduped.length} ads fetched (scored).`,
          'Search',
        );
      }
      setSearchWarnings(collectedWarnings);
      return deduped;
    } finally {
      if (global) setSearching(false);
      if (activeSearchRunIdsRef.current.size === 0) {
        searchStopRequestedRef.current = false;
      }
      setSearchingRowIds(prev => {
        const next = new Set(prev);
        activeRowIds.forEach(id => next.delete(id));
        return next;
      });
    }
  };

  const runSearch = async () => {
    await runSearchRows(rows, { global: true });
  };

  const runSearchRow = async (rowId) => {
    const row = rows.find(item => item.id === rowId);
    if (!row) return;
    await runSearchRows([row]);
  };

  const stopSearch = async () => {
    searchStopRequestedRef.current = true;
    const runIds = [...new Set([...activeSearchRunIdsRef.current.values()].filter(Boolean))];
    if (runIds.length === 0) {
      setSearching(false);
      setSearchingRowIds(new Set());
      showToast('Search stopping…', 'Search');
      return;
    }
    await Promise.all(runIds.map(runId => (
      RESILIA_API.cancelSearchRun(runId).catch(e => console.warn('search cancel failed', runId, e))
    )));
    setSearching(false);
    setSearchingRowIds(new Set());
    showToast('Search stopping…', 'Search');
  };

  // ---- Autoresearch -------------------------------------------------------
  const pollWinners = async () => {
    const pollGeneration = autoresearchRunGenerationRef.current;
    const pollStillCurrent = () => pollGeneration === autoresearchRunGenerationRef.current;
    try {
      // Always refresh from offset 0 so current-run ads stream in while prefill
      // grows. Incremental offsets belong to scroll load-more only (see
      // loadMoreAutoresearchWinners) — advancing offset during poll skips the
      // current-run slice once loadedIds > current_run_total.
      const pollOffset = 0;
      const activeRunId = [autoresearchBatchIdRef.current].find(
        (candidate) => typeof candidate === 'string' && candidate.startsWith('autoresearch-'),
      ) || null;
      const [summary, activeState, page] = await Promise.all([
        RESILIA_API.summary(),
        RESILIA_API.getAutoresearchState(),
        loadAutoresearchWinnersPage({
          reset: autoresearchLoadedIdsRef.current.size === 0,
          offset: pollOffset,
          pageSize: AUTORESEARCH_PAGE_SIZE,
          directionSource: directionsRef.current,
          includeFailed: !searchPassesOnlyRef.current,
          lightweight: true,
          runId: activeRunId,
        }),
      ]);
      if (!pollStillCurrent()) return;

      const loadedWinners = page.loaded ?? autoresearchCurrentIdsRef.current.size;
      const currentRunWinners = autoresearchCurrentIdsRef.current.size;

      const activeRunState = activeState?.state || {};
      const backendRunning = !!activeState?.running;
      const useRunState = backendRunning
        || Number(activeRunState.run_directions_total || 0) > 0
        || Object.keys(activeRunState.platform_ads_fetched || {}).length > 0
        || Object.keys(activeRunState.platform_progress || {}).length > 0
        || activeRunState?.shared_pool_prefill_only
        || activeRunState?.run_phase === 'shared_pool_ready'
        || activeRunState?.run_phase === 'prefilling_shared_pool';
      const effectiveRunTotal = useRunState
        ? Number(activeRunState.run_directions_total || 0)
        : Number(summary?.run_directions_total || 0);
      const effectiveRunDone = Number(activeRunState.run_directions_done || 0);
      const poolOnlyComplete = Boolean(
        activeRunState?.shared_pool_prefill_only
        || activeRunState?.run_phase === 'shared_pool_ready'
        || activeState?.run?.status === 'done'
      );
      const runLooksComplete = !backendRunning && (
        poolOnlyComplete
        || (effectiveRunTotal > 0 && effectiveRunDone >= effectiveRunTotal)
      );
      const effectiveMeta = resolveAutoresearchProgressMeta(activeRunState, summary, {
        looksComplete: runLooksComplete,
        useRunState,
      });
      const effectiveCrawlState = effectiveMeta.crawlState;
      if (runLooksComplete || poolOnlyComplete || effectiveCrawlState === 'done') {
        setArRunning(false);
        setAutoresearchActive(false);
      }
      const effectiveCoverageTouched = Math.max(
        Number(activeRunState.coverage_touched || 0),
        Number(summary?.coverage_touched || 0),
        Number(summary?.run_directions_touched || 0),
      );
      if (summary) {
        const poolWinners = Number(
          activeRunState.result_count || activeRunState.prefilled_count || 0,
        );
        const usePoolCounts = poolOnlyComplete || poolWinners > 0;
        const runTotal = effectiveRunTotal || 0;
        const runTotalScored = usePoolCounts
          ? Number(summary.run_total_scored ?? poolWinners)
          : runTotal > 0
            ? (summary.run_total_scored ?? 0)
            : summary.total_scored;
        setArProgress({
          totalScored: runTotalScored,
          winners: currentRunWinners,
          loadedWinners,
          totalWinners: currentRunWinners > 0
            ? currentRunWinners
            : usePoolCounts
              ? Math.max(Number(summary.run_winners ?? 0), poolWinners)
              : runTotal > 0
                ? Number(summary.run_winners ?? 0)
                : summary.winners,
          // Prefer the per-run counter while a crawl is active so the bar
          // tracks "directions finished this run", not lifetime DB state.
          directionsSatisfied: useRunState
            ? Number(activeRunState.run_directions_done || 0)
            : (runTotal > 0 ? summary.run_directions_done : summary.directions_satisfied),
          directionsTotal: runTotal > 0 ? runTotal : summary.directions_total,
          crawlState: effectiveCrawlState,
          runPhase: effectiveMeta.runPhase,
          prefilledCount: effectiveMeta.prefilledCount,
          remainingTarget: effectiveMeta.remainingTarget,
          creditsRemaining: summary.credits_remaining,
          platformAdsFetched: useRunState
            ? (activeRunState.platform_ads_fetched || {})
            : (summary.platform_ads_fetched || {}),
          platformProgress: useRunState
            ? (activeRunState.platform_progress || {})
            : (summary.platform_progress || {}),
          directionProgress: useRunState
            ? (activeRunState.direction_progress || {})
            : (summary.direction_progress || {}),
          coverageTotal: useRunState
            ? Number(activeRunState.coverage_total || 0)
            : (summary.coverage_total || 0),
          coverageTouched: useRunState
            ? effectiveCoverageTouched
            : effectiveCoverageTouched,
          platformCounts: summary.run_platform_counts || {},
        });
        restartAutoresearchPoll(effectiveMeta.runPhase);
        if (typeof summary.credits_remaining === 'number') {
          setPlatformCredits(pc => ({ ...pc, foreplay: summary.credits_remaining }));
        }
      }
      if (summary && !runLooksComplete && !poolOnlyComplete && effectiveCrawlState !== 'idle' && effectiveCrawlState !== 'done') {
        setArRunning(true);
        setAutoresearchActive(true);
        setLatestAutoresearchRun(prev => prev ? ({
          ...prev,
          status: 'running',
          count: autoresearchCurrentIdsRef.current.size,
        }) : prev);
      }
      if (summary && (runLooksComplete || poolOnlyComplete || effectiveCrawlState === 'idle' || effectiveCrawlState === 'done') && !backendRunning) {
        const stillSavingPages = Boolean(
          page.hasMore
          && !poolOnlyComplete
          && effectiveCrawlState !== 'done'
          && effectiveMeta.runPhase !== 'done',
        );
        if (stillSavingPages) {
          setArProgress(prev => prev ? ({ ...prev, crawlState: 'saving' }) : prev);
          setLatestAutoresearchRun(prev => prev ? ({
            ...prev,
            status: 'saving results',
            count: autoresearchCurrentIdsRef.current.size,
          }) : prev);
        } else {
          setArRunning(false);
          setAutoresearchActive(false);
          setArProgress(prev => {
            if (!prev) return prev;
            const total = Number(prev.directionsTotal || 0);
            return {
              ...prev,
              crawlState: 'done',
              directionsSatisfied: total > 0 ? total : prev.directionsSatisfied,
            };
          });
          setLatestAutoresearchRun(prev => prev ? ({ ...prev, status: 'done', count: currentRunWinners }) : prev);
          stopPoll('ar');
      showToast(`AutoSearch complete. ${currentRunWinners} ads loaded.`, 'AutoSearch');
        }
      }
    } catch (e) {
      console.warn('poll winners failed', e);
    }
  };

  const restartAutoresearchPoll = (runPhase) => {
    const normalized = runPhase || 'running';
    if (normalized === 'done' || normalized === 'shared_pool_ready') {
      stopPoll('ar');
      arPollPhaseRef.current = null;
      return;
    }
    if (arPollPhaseRef.current === normalized && arPollRef.current) return;
    arPollPhaseRef.current = normalized;
    const intervalMs = normalized === 'prefilling_shared_pool'
      ? AR_PREFILL_POLL_INTERVAL_MS
      : AR_ACTIVE_POLL_INTERVAL_MS;
    startVisibilityAwarePoll('ar', pollWinners, intervalMs);
  };

  const runAutoresearch = async () => {
    const directionPool = directions;
    if (directionPool.length === 0) {
      showToast('No directions configured for the selected sources.', 'AutoSearch');
      return;
    }
    // Direction selection: explicit checkbox picks win; otherwise run the
    // full direction pool. Autoresearch should never silently sample only a
    // few directions when the user expects the whole catalog.
    let ids;
    if (arPickedDirections.size > 0) {
      ids = directions.filter(d => arPickedDirections.has(d.id)).map(d => d.id);
    } else {
      ids = directionPool.map(d => d.id);
      setArPickedDirections(new Set(ids));
      setArNumDirections(String(ids.length));
    }
    if (ids.length === 0) {
      showToast('No directions resolved — check the picker or the cap.', 'AutoSearch');
      return;
    }
    const sources = Object.keys(arSources).filter(k => arSources[k]);
    if (sources.length === 0) {
      showToast('Pick at least one source (Foreplay, AdSpy, …).', 'AutoSearch');
      return;
    }
    setArSelected(new Set(ids));
    setAutoresearchActive(true);
    setArProgress({
      crawlState: 'prefilling_shared_pool',
      runPhase: 'prefilling_shared_pool',
      winners: 0,
      prefilledCount: 0,
      totalScored: 0,
      directionsSatisfied: 0,
      directionsTotal: ids.length * sources.length,
      platformProgress: {},
      directionProgress: {},
    });
    // Bug retention — keep prior autoresearch winners on screen as a history
    // strip below the new batch. Stamp untagged old ads with a 'previous'
    // batch id, then bump the current batch id so new ads land on top.
    setResearchAds(prev => prev.map(ad => (
      ad.source === 'autoresearch' && !ad._arBatchId
        ? { ...ad, _arBatchId: 'previous', currentAutoresearchRun: false, currentRunLabel: null }
        : ad.source === 'autoresearch'
          ? { ...ad, currentAutoresearchRun: false, currentRunLabel: null }
          : ad
    )));
    const newBatchId = `ar-${Date.now()}`;
    autoresearchBatchIdRef.current = newBatchId;
    autoresearchLoadedIdsRef.current = new Set();
    autoresearchCurrentIdsRef.current = new Set();
          autoresearchLoadedPlatformCountsRef.current = {};
    autoresearchLoadedPlatformCountsRef.current = {};
    setAutoresearchPage({ loaded: 0, total: null, hasMore: false, loading: false });
    setLatestAutoresearchRun({
      id: newBatchId,
      count: 0,
      total: null,
      status: 'starting',
      labels: sources,
    });
    arPollPhaseRef.current = null;
    stopPoll('ar');
    autoresearchRunGenerationRef.current += 1;
    setArRunning(true);
    try {
      const perDirectionTarget = boundedPositiveIntOrNull(arTarget);
      if (arTarget && String(perDirectionTarget) !== arTarget) {
        setArTarget(perDirectionTarget === null ? '' : String(perDirectionTarget));
      }
      autoresearchTargetRef.current = perDirectionTarget;
      const maxAdsPerDirection = boundedPositiveIntOrNull(
        arMaxAdsPerDirection,
        SEARCH_ADS_PER_DIRECTION_MAX,
      );
      const maxAdsPerPlatform = boundedPositiveIntOrNull(
        arMaxAdsPerPlatform,
        SEARCH_ADS_PER_PLATFORM_MAX,
      );
      const startResult = await RESILIA_API.startAutoresearch({
        directionIds: ids,
        perDirectionTarget,
        platforms: sources,
        filters: searchFiltersPayload(arFilters),
        maxAdsPerDirection,
        maxAdsPerPlatform,
        force: true,
      });
      if (startResult?.status === 'already_running') {
        const restored = startResult.state || {};
        const state = restored.state || {};
        const runId = state.run_id || restored.run?.run_id || newBatchId;
        const restoredSources = state.sources || restored.run?.platforms || sources;
        const restoredTarget = state.target_ads || state.max_total_winners || perDirectionTarget;
        if (restoredTarget) {
          autoresearchTargetRef.current = Number(restoredTarget);
          setArTarget(String(restoredTarget));
        }
        autoresearchBatchIdRef.current = runId;
        setArRunning(true);
        setAutoresearchActive(true);
        setArSelected(new Set(state.selected || restored.run?.direction_ids || ids));
        setLatestAutoresearchRun(prev => prev ? ({
          ...prev,
          id: runId,
          status: state.job_status || state.crawl_status || state.status || 'running',
          labels: restoredSources,
        }) : {
          id: runId,
          count: 0,
          total: null,
          status: state.job_status || state.crawl_status || state.status || 'running',
          labels: restoredSources,
        });
        setArProgress({
          crawlState: state.run_phase || state.status || state.job_status || state.crawl_status || 'running',
          runPhase: state.run_phase || state.crawl_status || state.job_status || state.status || 'running',
          prefilledCount: Number(state.prefilled_count || state.result_count || 0),
          remainingTarget: Number(state.remaining_target || 0),
          winners: Number(state.result_count || 0),
          totalScored: 0,
          directionsSatisfied: state.run_directions_done || 0,
          directionsTotal: state.run_directions_total || 0,
          platformAdsFetched: state.platform_ads_fetched || {},
          platformProgress: state.platform_progress || {},
          directionProgress: state.direction_progress || {},
          coverageTotal: state.coverage_total || 0,
          coverageTouched: state.coverage_touched || 0,
          platformCounts: state.run_platform_counts || {},
        });
        showToast('AutoSearch is already running. Showing the active run controls.', 'AutoSearch');
        restartAutoresearchPoll(state.run_phase || state.crawl_status || 'running');
        pollWinners();
        return;
      }
      const backendRunId = startResult.autoresearch_run_id || newBatchId;
      autoresearchBatchIdRef.current = backendRunId;
      const prefilledCount = Number(startResult.prefilled_count || 0);
      if (startResult?.completed) {
        setArRunning(false);
        setAutoresearchActive(false);
        setArProgress(prev => prev ? ({
          ...prev,
          crawlState: 'done',
          runPhase: startResult.run_phase || 'shared_pool_ready',
          prefilledCount,
          winners: prefilledCount,
          totalWinners: prefilledCount,
          totalScored: prefilledCount,
        }) : prev);
        setLatestAutoresearchRun(prev => prev ? ({
          ...prev,
          id: backendRunId,
          count: prefilledCount,
          total: prefilledCount,
          status: 'done',
        }) : {
          id: backendRunId,
          count: prefilledCount,
          total: prefilledCount,
          status: 'done',
          labels: sources,
        });
        await loadAutoresearchWinnersPage({
          reset: true,
          directionSource: directionsRef.current,
          lightweight: true,
          runId: backendRunId,
        });
        pollWinners();
        showToast(
          prefilledCount
            ? `AutoSearch filled ${prefilledCount} ad(s) from the shared pool.`
            : 'AutoSearch completed with no new ads from the shared pool.',
          'AutoSearch',
        );
        return;
      }
      setArRunning(true);
      setLatestAutoresearchRun(prev => prev ? ({
        ...prev,
        id: backendRunId,
        count: prefilledCount,
        status: 'running',
      }) : prev);
      setArProgress(prev => ({
        ...(prev || {}),
        crawlState: startResult.run_phase || (prefilledCount > 0 ? 'awaiting_worker' : 'running'),
        runPhase: startResult.run_phase || prev?.runPhase || 'running',
        prefilledCount,
        remainingTarget: Number(startResult.remaining_target || 0),
        winners: prefilledCount,
        totalWinners: prefilledCount,
      }));
      const startToast = startResult.run_phase === 'prefilling_shared_pool'
        ? `AutoSearch started — loading ads from the shared pool…`
        : prefilledCount
          ? `AutoSearch started — ${prefilledCount} ad(s) loaded from the shared pool.`
          : `AutoSearch started for ${ids.length} direction(s) across ${sources.length} source(s).`;
      showToast(startToast, 'AutoSearch');
      restartAutoresearchPoll(startResult.run_phase || 'prefilling_shared_pool');
      pollWinners();
    } catch (e) {
      stopPoll('ar');
      arPollPhaseRef.current = null;
      setArRunning(false);
      setAutoresearchActive(false);
      setArProgress(prev => prev ? ({ ...prev, crawlState: 'idle' }) : prev);
      setLatestAutoresearchRun(prev => prev ? ({ ...prev, status: 'failed' }) : prev);
      showToast('AutoSearch failed to start: ' + e.message, 'Error');
    }
  };

  const loadMoreAutoresearchWinners = async () => {
    try {
      const includeFailed = !searchPassesOnlyRef.current;
      const page = await loadAutoresearchWinnersPage({
        includeFailed,
        lightweight: !includeFailed,
        pageSize: includeFailed ? AUTORESEARCH_FAIL_DISPLAY_LIMIT : AUTORESEARCH_PAGE_SIZE,
      });
      if (page && !page.hasMore && page.loaded > 0) {
        showToast('No more AutoSearch ads to load.', 'AutoSearch');
      }
    } catch (e) {
      showToast('Failed to load more AutoSearch ads: ' + e.message, 'Error');
    }
  };

  const stopAutoresearch = async () => {
    autoresearchRunGenerationRef.current += 1;
    setArRunning(false);
    setAutoresearchActive(false);
    stopPoll('ar');
    try {
      await RESILIA_API.stopAutoresearch();
      setLatestAutoresearchRun(prev => prev ? ({ ...prev, status: 'cancelled' }) : prev);
      setArProgress(prev => prev ? ({
        ...prev,
        crawlState: 'idle',
        runPhase: 'cancelled',
      }) : prev);
      showToast('AutoSearch stopped.', 'AutoSearch');
    } catch (e) {
      showToast('Failed to stop: ' + e.message, 'Error');
    }
  };

  // ---- Auto-Remodel orchestrator ----------------------------------------
  // Once the orchestrator picks ads, hydrate the swipe rows for them and
  // push them through the existing Research → Filter → Remodel views as the
  // pipeline transitions stages. This reuses the same panels the manual
  // flow uses (no duplicate UI to maintain).
  const hydratePickedAds = async (adIds, directionSource = directionsRef.current) => {
    if (!adIds || adIds.length === 0) return [];
    const dirMap = directionMetaMap(directionSource);
    const uniqueIds = [...new Set(adIds.filter(Boolean))];
    // Retry budget kept low (2 attempts × 500ms) so this Phase C path does
    // not block the user-perceived load when called from the lazy tab
    // loader. Auto-Remodel SSE will fill in any swipe rows that arrive after
    // we give up here.
    const getSwipeWithRetry = async (id) => {
      for (let attempt = 0; attempt < 2; attempt += 1) {
        const swipe = await RESILIA_API.getSwipe(id);
        if (swipe) return swipe;
        await new Promise(resolve => setTimeout(resolve, 500));
      }
      return null;
    };
    const getSwipesWithRetry = async (ids) => {
      const rowsById = new Map();
      try {
        for (let attempt = 0; attempt < 2 && rowsById.size < ids.length; attempt += 1) {
          const missingIds = ids.filter(id => !rowsById.has(id));
          const rows = await RESILIA_API.getSwipesBatch(missingIds);
          rows.forEach(row => { if (row?.ad_id) rowsById.set(row.ad_id, row); });
          if (rowsById.size >= ids.length) break;
          await new Promise(resolve => setTimeout(resolve, 500));
        }
        return ids.map(id => rowsById.get(id)).filter(Boolean);
      } catch (e) {
        console.warn('swipe batch hydration failed; falling back to single-row fetches', e);
        return Promise.all(ids.map(getSwipeWithRetry));
      }
    };
    const swipeRows = await getSwipesWithRetry(uniqueIds);
    const rows = await Promise.all(swipeRows.map(async (swipe) => {
      const id = swipe?.ad_id;
      try {
        if (!swipe) return null;
        let row = swipe;
        if (!row.transcript) {
          const detail = await RESILIA_API.fetchTranscript(id);
          if (detail && detail.transcript) {
            row = {
              ...row,
              transcript: detail.transcript,
              transcript_segments: detail.transcript_segments,
              transcript_status: detail.transcript_status,
              transcript_source: detail.transcript_source,
              transcript_provider: detail.transcript_provider,
              transcript_model: detail.transcript_model,
              transcript_word_count: detail.transcript_word_count,
              is_voice_transcript: detail.is_voice_transcript,
              raw: { ...(row.raw || {}), ...(detail.raw || {}) },
            };
          }
        }
        // ``adaptEvaluatedAd`` expects evaluated_ads-shaped rows; swipe rows
        // have the same column names for everything we need (ad_id, brand,
        // platform, transcript, score, reasoning, running_days, raw, …).
        const adapted = RESILIA_ADAPT.adaptEvaluatedAd(row);
        attachDirectionMeta(adapted, dirMap);
        return { ...adapted, source: 'auto_remodel' };
      } catch (e) {
        console.warn('hydratePickedAds failed for', id, e);
        return null;
      }
    }));
    return rows.filter(Boolean);
  };

  const adaptAutoRemodelSnapshotRows = (rows, directionSource = directionsRef.current, { current = false } = {}) => {
    const dirMap = directionMetaMap(directionSource);
    const ads = (rows || [])
      .filter(row => row?.ad_id && row.result_uuid)
      .map(row => {
        const adapted = RESILIA_ADAPT.adaptEvaluatedAd(row);
        attachDirectionMeta(adapted, dirMap);
        return {
          ...adapted,
          source: 'auto_remodel',
          _arRunId: row.run_id || adapted.runId || null,
          currentAutoRemodelRun: current,
        };
      });
    return uniqueAutoRemodelAds(ads, dedupeSettings);
  };

  const remodelEntryFromSnapshotRow = (row) => {
    const jobs = Array.isArray(row?.remodel_jobs) ? row.remodel_jobs : [];
    const doneJobs = jobs
      .filter(job => job && job.status === 'done' && job.response_text)
      .sort((a, b) => (a.variant_idx || 0) - (b.variant_idx || 0));
    if (doneJobs.length) {
      return {
        status: 'ready',
        variants: doneJobs.map((job, index) => ({
          jobId: job.id || null,
          variantIdx: job.variant_idx || index,
          raw: job.response_text,
          lines: parseRemodelScript(job.response_text),
          promotedAt: job.promoted_at || null,
        })),
      };
    }
    const scripts = Array.isArray(row?.remodeled_scripts) ? row.remodeled_scripts.filter(Boolean) : [];
    if (scripts.length) {
      return {
        status: 'ready',
        variants: scripts.map((text, index) => ({
          jobId: Array.isArray(row?.remodel_job_ids) ? row.remodel_job_ids[index] || null : null,
          variantIdx: index,
          raw: text,
          lines: parseRemodelScript(text),
          promotedAt: null,
        })),
      };
    }
    const failed = jobs.find(job => job && job.status === 'failed');
    if (failed) {
      const failedJobIds = jobs
        .filter(job => job && job.status === 'failed' && job.id)
        .map(job => job.id);
      return {
        status: 'error',
        variants: [],
        error: failed.error || 'Generation failed',
        failedJobIds,
      };
    }
    return null;
  };

  const seedAutoRemodelScriptsFromRows = (rows, ads) => {
    if (!Array.isArray(rows) || !Array.isArray(ads) || !rows.length || !ads.length) return;
    const rowsByKey = new Map();
    rows.forEach(row => {
      if (!row?.ad_id) return;
      const instanceKey = row.result_uuid || row.resultUuid || row.payload?.result_uuid || row.payload?.resultUuid;
      rowsByKey.set(String(instanceKey || row.ad_id), row);
      if (!instanceKey) rowsByKey.set(String(row.ad_id), row);
    });
    setRemodelScripts(prev => {
      const next = { ...prev };
      ads.forEach(ad => {
        const scriptKey = resultKey(ad);
        const row = rowsByKey.get(String(ad.resultUuid || ad.result_uuid || scriptKey))
          || rowsByKey.get(String(ad.id));
        const entry = remodelEntryFromSnapshotRow(row);
        if (entry) {
          next[scriptKey] = entry;
        } else if (!next[scriptKey] && !next[ad.id]) {
          next[scriptKey] = { status: 'pending', variants: [] };
        }
      });
      return next;
    });
  };

  const applyAutoRemodelCurrentPayload = async (
    payload,
    directionSource = directionsRef.current,
    { announce = false, focus = false } = {},
  ) => {
    if (!payload || payload.status !== 'ok' || !payload.state) return false;
    const rawState = payload.state;
    const terminalStages = new Set(['done', 'failed', 'cancelled']);
    const state = (!payload.running && !terminalStages.has(rawState.stage) && !terminalStages.has(rawState.status))
      ? {
          ...rawState,
          stage: 'cancelled',
          status: 'cancelled',
          cancel_reason: rawState.cancel_reason || 'No active Auto-Remodel run.',
        }
      : rawState;
    syncAutoRemodelState(state);
    const pickedIds = Array.isArray(state.picked_ad_ids)
      ? state.picked_ad_ids.slice(0, AUTO_REMODEL_COUNT_MAX)
      : [];
    const rows = Array.isArray(payload.ads) ? payload.ads : [];
    const historyRows = [];
    const currentRunId = state.run_id || null;
    const snapshotAds = adaptAutoRemodelSnapshotRows(rows, directionSource, { current: true })
      .map(ad => ({
        ...ad,
        _arRunId: currentRunId || ad._arRunId,
        currentAutoRemodelRun: true,
        autoRemodelProvisional: false,
      }));
    const historyAds = adaptAutoRemodelSnapshotRows(historyRows, directionSource, { current: false });
    if (!snapshotAds.length && !historyAds.length) return false;
    seedAutoRemodelScriptsFromRows(rows, snapshotAds);
    seedAutoRemodelScriptsFromRows(historyRows, historyAds);

    snapshotAds.forEach(ad => autoRemodelLoadedIdsRef.current.add(ad.id));
    setAutoRemodelAds(prev => {
      const currentKeys = autoRemodelDedupeKeySet(snapshotAds, dedupeSettings);
      const historyKeys = autoRemodelDedupeKeySet(historyAds, dedupeSettings);
      const retainedCurrent = prev
        .filter(ad => (
          currentRunId
          && ad.source === 'auto_remodel'
          && ad._arRunId === currentRunId
          && !ad.autoRemodelProvisional
          && !hasAutoRemodelDedupeOverlap(currentKeys, ad, dedupeSettings)
        ))
        .map(ad => ({ ...ad, currentAutoRemodelRun: true }));
      retainedCurrent.forEach(ad => addAutoRemodelDedupeKeys(currentKeys, ad, dedupeSettings));
      const previous = prev
        .filter(ad => (
          !hasAutoRemodelDedupeOverlap(currentKeys, ad, dedupeSettings)
          && !hasAutoRemodelDedupeOverlap(historyKeys, ad, dedupeSettings)
          && !(currentRunId && ad._arRunId === currentRunId && ad.autoRemodelProvisional)
        ))
        .map(ad => ({ ...ad, currentAutoRemodelRun: false }));
      return uniqueAutoRemodelAds([...snapshotAds, ...retainedCurrent, ...historyAds, ...previous], dedupeSettings);
    });
    setAutoRemodelHydration({
      total: pickedIds.length || snapshotAds.length,
      loaded: snapshotAds.length,
      loading: false,
    });
    const snapshotIds = snapshotAds.map(ad => ad.id);
    if (snapshotIds.length) pollRemodelJobs(snapshotAds, autoRemodelCurrentJobOptions());
    if (focus) setTab('autoRemodel');
    if (announce) {
      showToast(
        `Auto-Remodel: restored ${snapshotAds.length} saved ad${snapshotAds.length === 1 ? '' : 's'} from this run.`,
        'Auto-Remodel',
      );
    }
    return true;
  };

  const restorePreviousAutoRemodelHistory = async ({
    excludeRunId = arRunIdRef.current,
    directionSource = directionsRef.current,
    limit = 200,
  } = {}) => {
    if (!RESILIA_API.autoRemodelHistory) return [];
    const page = await RESILIA_API.autoRemodelHistory(limit, excludeRunId);
    const rows = Array.isArray(page) ? page : (Array.isArray(page?.ads) ? page.ads : []);
    const historyAds = adaptAutoRemodelSnapshotRows(rows, directionSource, { current: false });
    if (!historyAds.length) return [];
    seedAutoRemodelScriptsFromRows(rows, historyAds);
    setAutoRemodelAds(prev => {
      const existingKeys = autoRemodelDedupeKeySet(prev, dedupeSettings);
      const additions = historyAds.filter(ad => !hasAutoRemodelDedupeOverlap(existingKeys, ad, dedupeSettings));
      return additions.length ? uniqueAutoRemodelAds([...prev, ...additions], dedupeSettings) : prev;
    });
    pollAutoRemodelHistoryJobs(historyAds, { markMissingAsError: true });
    return historyAds;
  };

  const loadAutoRemodelPage = async ({
    ids = arPickedIdsRef.current,
    directionSource = directionsRef.current,
    reset = false,
    pageSize = AUTO_REMODEL_PAGE_SIZE,
  } = {}) => {
    const allIds = [...new Set((ids || []).filter(Boolean))];
    if (!allIds.length) return [];
    if (autoRemodelHydratingPageRef.current) {
      for (let attempt = 0; attempt < 20 && autoRemodelHydratingPageRef.current; attempt += 1) {
        await new Promise(resolve => setTimeout(resolve, 100));
      }
      if (autoRemodelHydratingPageRef.current) return [];
    }
    autoRemodelHydratingPageRef.current = true;
    const loadedIds = reset ? new Set() : new Set(autoRemodelLoadedIdsRef.current);
    if (reset) {
      autoRemodelLoadedIdsRef.current = loadedIds;
    }
    const pageIds = allIds.filter(id => !loadedIds.has(id)).slice(0, pageSize);
    if (!pageIds.length) {
      setAutoRemodelHydration({ total: allIds.length, loaded: loadedIds.size, loading: false });
      autoRemodelHydratingPageRef.current = false;
      return [];
    }
    setAutoRemodelHydration({ total: allIds.length, loaded: loadedIds.size, loading: true });
    try {
      const hydratedAds = await hydratePickedAds(pageIds, directionSource);
      const currentRunId = arRunIdRef.current;
      const hydratedById = new Map(hydratedAds.map(ad => [ad.id, ad]));
      const pageAds = pageIds.map(id => {
        const base = hydratedById.get(id) || autoRemodelFallbackAd(id);
        const runId = currentRunId || base._arRunId || 'current';
        return { ...base, _arRunId: runId, currentAutoRemodelRun: true, autoRemodelProvisional: false };
      });
      pageIds.forEach(id => loadedIds.add(id));
      autoRemodelLoadedIdsRef.current = loadedIds;
      setAutoRemodelAds(prev => {
        // Current-run ads stay current even when they streamed via ad_scored
        // before the picked-id list caught up.
        const currentRunAds = prev.filter(
          ad => ad.source === 'auto_remodel' && currentRunId && ad._arRunId === currentRunId,
        );
        const nextById = new Map(currentRunAds.map(ad => [ad.id, ad]));
        pageAds.forEach(ad => {
          const existing = nextById.get(ad.id);
          nextById.set(ad.id, mergeRichAdDetails(existing, { ...ad, cardKey: resultKey(ad) }));
        });
        const allIdSet = new Set(allIds);
        const currentIds = new Set(allIds);
        [...nextById.values()].forEach(ad => {
          if (currentRunId && ad._arRunId === currentRunId && !ad.autoRemodelProvisional) currentIds.add(ad.id);
        });
        const currentOrdered = allIds
          .filter(id => nextById.has(id))
          .map(id => ({ ...nextById.get(id), _arRunId: currentRunId || nextById.get(id)._arRunId || 'current', currentAutoRemodelRun: true }));
        const streamedCurrent = [...nextById.values()]
          .filter(ad => currentIds.has(ad.id) && !allIdSet.has(ad.id) && !ad.autoRemodelProvisional)
          .map(ad => ({ ...ad, _arRunId: currentRunId || ad._arRunId || 'current', currentAutoRemodelRun: true }));
        const remainder = prev
          .filter(ad => !(ad.source === 'auto_remodel' && currentRunId && ad._arRunId === currentRunId))
          .map(ad => ({ ...ad, currentAutoRemodelRun: false }));
        return uniqueAutoRemodelAds([...currentOrdered, ...streamedCurrent, ...remainder], dedupeSettings);
      });
      setRemodelScripts(prev => {
        const next = { ...prev };
        pageAds.forEach(a => { if (!next[a.id]) next[a.id] = { status: 'pending', variants: [] }; });
        return next;
      });
      setAutoRemodelHydration({ total: allIds.length, loaded: loadedIds.size, loading: false });
      const pageAdIds = pageAds.map(ad => ad.id);
      if (pageAdIds.length) pollRemodelJobs(pageAds, autoRemodelCurrentJobOptions());
      return pageAds;
    } finally {
      setAutoRemodelHydration(prev => ({ ...prev, loading: false }));
      autoRemodelHydratingPageRef.current = false;
    }
  };

  const hydrateAutoRemodelPickedState = async (
    state,
    directionSource = directionsRef.current,
    { announce = false, focus = false } = {},
  ) => {
    if (!state || !Array.isArray(state.picked_ad_ids) || state.picked_ad_ids.length === 0) return false;
    const pickedIds = state.picked_ad_ids.slice(0, AUTO_REMODEL_COUNT_MAX);
    if (!pickedIds.length) return false;
    arPickedIdsRef.current = pickedIds;
    setArPickedIds(pickedIds);
    if (typeof state.target_ads === 'number') setArTargetAds(state.target_ads);
    if (typeof state.ads_found === 'number') setArAdsFound(Math.min(state.ads_found, pickedIds.length));

    if (state.run_id && RESILIA_API.getAutoRemodelRunAds) {
      try {
        const snapshot = await RESILIA_API.getAutoRemodelRunAds(state.run_id);
        const snapshotRows = Array.isArray(snapshot) ? snapshot : (Array.isArray(snapshot?.ads) ? snapshot.ads : []);
        const currentRunId = state.run_id || null;
        const snapshotAds = adaptAutoRemodelSnapshotRows(snapshotRows, directionSource, { current: true })
          .map(ad => ({
            ...ad,
            _arRunId: currentRunId || ad._arRunId,
            currentAutoRemodelRun: true,
            autoRemodelProvisional: false,
          }));
        if (snapshotAds.length) {
          seedAutoRemodelScriptsFromRows(snapshotRows, snapshotAds);
          snapshotAds.forEach(ad => autoRemodelLoadedIdsRef.current.add(ad.id));
          setAutoRemodelAds(prev => {
            const currentKeys = autoRemodelDedupeKeySet(snapshotAds, dedupeSettings);
            const retainedCurrent = prev
              .filter(ad => (
                currentRunId
                && ad.source === 'auto_remodel'
                && ad._arRunId === currentRunId
                && !ad.autoRemodelProvisional
                && !hasAutoRemodelDedupeOverlap(currentKeys, ad, dedupeSettings)
              ))
              .map(ad => ({ ...ad, currentAutoRemodelRun: true }));
            retainedCurrent.forEach(ad => addAutoRemodelDedupeKeys(currentKeys, ad, dedupeSettings));
            const previous = prev
              .filter(ad => (
                !hasAutoRemodelDedupeOverlap(currentKeys, ad, dedupeSettings)
                && !(currentRunId && ad._arRunId === currentRunId && ad.autoRemodelProvisional)
              ))
              .map(ad => ({ ...ad, currentAutoRemodelRun: false }));
            return uniqueAutoRemodelAds([...snapshotAds, ...retainedCurrent, ...previous], dedupeSettings);
          });
          setAutoRemodelHydration({ total: pickedIds.length, loaded: snapshotAds.length, loading: false });
          const snapshotIds = snapshotAds.map(ad => ad.id);
          if (snapshotIds.length) pollRemodelJobs(snapshotAds, autoRemodelCurrentJobOptions());
          if (focus) setTab('autoRemodel');
          if (announce) {
            showToast(
              `Auto-Remodel: restored ${snapshotAds.length} saved ad${snapshotAds.length === 1 ? '' : 's'} from this run.`,
              'Auto-Remodel',
            );
          }
          return true;
        }
        // Snapshot returned 0 rows. For an active run, the early picks have
        // not been persisted yet — SSE will stream them in within seconds.
        // Falling through to ``loadAutoRemodelPage`` (which calls the heavy
        // ``/api/swipe/batch`` endpoint, currently 10–20 s under DB pool
        // contention) blocks the UI for no reason and is the main source of
        // the "Auto-Remodel ads appear 20 s late" symptom on refresh.
        if (state.status !== 'done') return false;
      } catch (e) {
        console.warn('auto-remodel run snapshot restore failed; falling back to swipe hydration', e);
      }
    }

    const ads = await loadAutoRemodelPage({ ids: pickedIds, directionSource, reset: true });
    if (!ads.length) return false;
    setRemodelScripts(prev => {
      const next = { ...prev };
      ads.forEach(a => { if (!next[a.id]) next[a.id] = { status: 'pending', variants: [] }; });
      return next;
    });
    if (focus) setTab('autoRemodel');
    if (announce) {
      showToast(
        `Auto-Remodel: showing ${ads.length} of ${pickedIds.length} saved ad${pickedIds.length === 1 ? '' : 's'} while variants generate.`,
        'Auto-Remodel',
      );
    }
    return true;
  };

  useEffect(() => {
    const ids = arPickedIds || [];
    if (!ids.length || !['picked', 'filtering', 'filter_done', 'remodeling', 'done'].includes(arStage)) return;
    if (autoRemodelLoadedIdsRef.current.size > 0) return;
    let cancelled = false;
    (async () => {
      const ads = await loadAutoRemodelPage({ ids, reset: true });
      if (cancelled || ads.length === 0) return;
      setRemodelScripts(prev => {
        const next = { ...prev };
        ads.forEach(a => { if (!next[a.id]) next[a.id] = { status: 'pending', variants: [] }; });
        return next;
      });
    })();
    return () => { cancelled = true; };
    // arPickedIds is an array from SSE/state restore; use its contents as the trigger.
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [arStage, arPickedIds.join('|')]);

  // SSE subscription. The same channel powers the existing autoresearch
  // panel's live progress; we layer Auto-Remodel on top with no new
  // transport. Returned cleanup closes the EventSource on unmount / HMR.
  const streamAutoresearchWinnerFromEvent = (ev) => {
    if (!ev || ev.type !== 'ad_scored' || !ev.ad_id) return false;
    if (searchPassesOnlyRef.current && ev.passes !== true) return false;
    const active = arRunningRef.current || autoresearchActiveRef.current;
    if (!active) return false;

    const dirMap = directionMetaMap(directionsRef.current);
    const synth = {
      ad_id: ev.ad_id,
      direction_id: ev.direction_id,
      score: ev.score,
      passes: ev.passes === true,
      reasoning: ev.reasoning,
      transcript: ev.transcript_preview,
      brand: ev.brand,
      platform: ev.platform,
      source_platform: ev.source_platform,
      running_days: ev.running_days,
      creative_url: ev.creative_url,
      thumbnail_url: ev.thumbnail_url,
      link_url: ev.link_url,
      metrics: ev.metrics || {},
      raw: { source_platform: ev.source_platform, metrics: ev.metrics || {} },
    };
    const adapted = RESILIA_ADAPT.adaptEvaluatedAd(synth);
    attachDirectionMeta(adapted, dirMap);
    const currentBatchId = autoresearchBatchIdRef.current || 'latest-autoresearch';
    const arrivedAtMs = Date.now();
    const ad = {
      ...adapted,
      source: 'autoresearch',
      _arBatchId: currentBatchId,
      currentAutoresearchRun: true,
      currentRunLabel: 'Current',
      autoresearchProvisional: true,
      arrivedAtMs,
    };
    const key = autoresearchDisplayKey(ad);
    if (autoresearchLoadedIdsRef.current.has(key)) return false;

    const loadedIds = new Set(autoresearchLoadedIdsRef.current);
    const currentIds = new Set(autoresearchCurrentIdsRef.current);
    loadedIds.add(key);
    currentIds.add(key);
    autoresearchLoadedIdsRef.current = loadedIds;
    autoresearchCurrentIdsRef.current = currentIds;

    setResearchAds(prev => {
      const searchAds = prev.filter(a => a.source !== 'autoresearch');
      const priorAutoAds = prev
        .filter(a => (
          a.source === 'autoresearch'
          && (!a.currentAutoresearchRun || (a._arBatchId && a._arBatchId !== currentBatchId))
        ))
        .map(a => ({ ...a, currentAutoresearchRun: false, currentRunLabel: null }));
      const currentAutoAds = prev.filter(a => (
        a.source === 'autoresearch'
        && a.currentAutoresearchRun
        && (a._arBatchId || currentBatchId) === currentBatchId
      ));
      const byKey = new Map(currentAutoAds.map(item => [autoresearchDisplayKey(item), item]));
      byKey.set(key, { ...ad, cardKey: resultKey(ad) });
      const nextAds = [...searchAds, ...Array.from(byKey.values()), ...priorAutoAds];
      return nextAds;
    });
    setAutoresearchPage(prev => ({ ...prev, loaded: Math.max(prev.loaded || 0, currentIds.size) }));
    setLatestAutoresearchRun(prev => prev ? ({ ...prev, status: 'running', count: currentIds.size }) : prev);
    setAutoresearchActive(true);
    return true;
  };

  useEffect(() => {
    if (authState.status !== 'authenticated') return undefined;

    const close = RESILIA_API.subscribe((ev) => {
      if (!ev || !ev.type) return;
      try {
        streamAutoresearchWinnerFromEvent(ev);
      } catch (streamErr) {
        throw streamErr;
      }

      if (ev.type === 'auto_remodel_event') {
        // Late subscribers latch onto whichever run is broadcasting — keep
        // the first run_id we see for this session unless dismissed.
        const previousRunId = arRunIdRef.current;
        setArRunId(prev => prev || ev.run_id);
        if (!arRunIdRef.current && ev.run_id) arRunIdRef.current = ev.run_id;
        if (arRunIdRef.current && ev.run_id && ev.run_id !== arRunIdRef.current) {
          return;
        }
        setArStage(ev.stage || null);
        arStageRef.current = ev.stage || null;
        setArLastEvent(ev);

        // Crawl-side progress counters from the orchestrator.
        if (typeof ev.directions_done === 'number') setArDirectionsDone(ev.directions_done);
        if (typeof ev.directions_total === 'number') setArDirectionsTotal(ev.directions_total);
        if (typeof ev.ads_found === 'number') {
          const target = arTargetAdsRef.current || ev.target_ads || 0;
          setArAdsFound(target > 0 ? Math.min(ev.ads_found, target) : ev.ads_found);
        }
        if (typeof ev.target_ads === 'number') {
          setArTargetAds(ev.target_ads);
          arTargetAdsRef.current = ev.target_ads;
        }

        if (Array.isArray(ev.picked_ad_ids) && ev.picked_ad_ids.length) {
          const target = ev.target_ads || arTargetAdsRef.current || ev.picked_ad_ids.length;
          const cappedIds = ev.picked_ad_ids.slice(0, Math.max(1, Math.min(AUTO_REMODEL_COUNT_MAX, target)));
          arPickedIdsRef.current = cappedIds;
          setArPickedIds(cappedIds);
        }

        // Pipeline now skips the Filter step entirely; the backend only picks
        // ads that meet the configured winning-score threshold. Stages we react to:
        //   * picked (with non-empty picks)
        //                → hydrate ads, switch to Remodel tab, push to
        //                   remodeledAds with pending variant slots, start
        //                   polling.
        //   * picked (empty picks)
        //                → orchestrator's fresh-run filter found nothing
        //                   new. Stay on the current tab; toast explains.
        //   * remodeling → no-op (just a status update).
        //   * done/failed → final poll + toast.
        (async () => {
          if (ev.stage === 'picked' && Array.isArray(ev.picked_ad_ids)) {
            if (ev.picked_ad_ids.length === 0) {
              // Critical: never switch tabs on empty picks. The user clicked
              // Run expecting ads — switching to a blank Remodel view feels
              // like the app broke. Stay put; the orchestrator will fire
              // ``done`` next anyway.
              showToast(
                'Auto-Remodel: no fresh ads found this run. Try different directions or wait for new winners to surface.',
                'Auto-Remodel',
              );
              return;
            }
            const target = ev.target_ads || arTargetAdsRef.current || ev.picked_ad_ids.length;
            const pickedIds = ev.picked_ad_ids.slice(0, Math.max(1, Math.min(AUTO_REMODEL_COUNT_MAX, target)));
            // Bug — the orchestrator emits ``picked`` once per batch as
            // ``_queue_fresh_remodels`` runs (not just at the very end), and
            // ``ev.picked_ad_ids`` is the running cumulative list. Calling
            // ``loadAutoRemodelPage({ reset: true })`` on every batch wiped
            // every ad of the current run from screen, flashed the
            // "Restoring Auto-Remodel results — Loading saved ads and
            // scripts" empty state (which is keyed on
            // ``ads.length === 0 && (restoring || loadingMore)``), then
            // re-hydrated from scratch — and any remodelled script that had
            // already streamed in disappeared until the next ``remodel_*``
            // event re-populated it. Hydrate only the genuinely new ids and
            // leave the existing rendered ads / scripts in place.
            const alreadyLoaded = autoRemodelLoadedIdsRef.current;
            const newIds = (Array.isArray(ev.new_picked_ad_ids) && ev.new_picked_ad_ids.length
              ? ev.new_picked_ad_ids
              : pickedIds
            ).filter(id => id && !alreadyLoaded.has(id));
            if (newIds.length === 0) {
              // Nothing new to render in this batch — still refresh the
              // remodel-job poll so the variant counter keeps ticking.
              const allLoadedIds = [...autoRemodelLoadedIdsRef.current];
              if (allLoadedIds.length) {
                startVisibilityAwarePoll('remodel',
                  () => pollRemodelJobs(autoRemodelCurrentPollTargetsForIds(allLoadedIds), autoRemodelCurrentJobOptions()),
                  REMODEL_POLL_INTERVAL_MS,
                );
              }
              return;
            }
            // Incremental hydration: keeps existing ads + scripts on screen
            // and just appends the new picks to the bottom.
            const ads = await loadAutoRemodelPage({ ids: pickedIds, reset: false });
            setRemodelScripts(prev => {
              const next = { ...prev };
              ads.forEach(a => {
                if (!next[a.id]) next[a.id] = { status: 'pending', variants: [] };
              });
              return next;
            });
            const allLoadedIds = [...autoRemodelLoadedIdsRef.current];
            if (allLoadedIds.length) {
              startVisibilityAwarePoll('remodel',
                () => pollRemodelJobs(autoRemodelCurrentPollTargetsForIds(allLoadedIds), autoRemodelCurrentJobOptions()),
                REMODEL_POLL_INTERVAL_MS,
              );
            }
            showToast(
              `Auto-Remodel: +${newIds.length} new ad${newIds.length === 1 ? '' : 's'} (${pickedIds.length}/${target}).`,
              'Auto-Remodel',
            );
          } else if (ev.stage === 'done') {
            // Suppress the redundant "done" toast when we already toasted
            // about an empty pick — same run, same news.
            if ((ev.picked ?? 0) > 0) {
              showToast(
                `Auto-Remodel done — ${ev.completed ?? 0}/${ev.jobs_created ?? 0} jobs ok`,
                ev.failed ? 'Warn' : 'Auto-Remodel',
              );
            }
            const loadedIds = [...autoRemodelLoadedIdsRef.current];
            if (loadedIds.length) pollRemodelJobs(autoRemodelCurrentPollTargetsForIds(loadedIds), autoRemodelCurrentJobOptions({ markMissingAsError: true }));
          } else if (ev.stage === 'failed') {
            showToast(`Auto-Remodel failed: ${ev.error || 'unknown'}`, 'Error');
          } else if (ev.stage === 'cancelled') {
            showToast('Auto-Remodel stopped.', 'Auto-Remodel');
          }
        })();
      } else if (ev.type === 'remodel_completed' || ev.type === 'remodel_failed' || ev.type === 'remodel_edited') {
        // Variant-level updates: refresh just the affected ad's scripts via
        // the existing poll helper (handles the parsing + status logic).
        const isAutoRemodelAd = ev.ad_id && (
          arPickedIdsRef.current.includes(ev.ad_id)
          || autoRemodelAdsRef.current.some(ad => ad.id === ev.ad_id)
        );
        if (isAutoRemodelAd) {
          pollRemodelJobs(autoRemodelCurrentPollTargetsForIds([ev.ad_id]), autoRemodelCurrentJobOptions());
          // Each remodel_completed/_failed represents one variant finishing
          // (in any state) — tick the panel's variant counter.
          if (ev.type === 'remodel_completed' || ev.type === 'remodel_failed') {
            setArVariantsDone(n => n + 1);
          }
        } else if (ev.ad_id) {
          refreshManualRemodelAd(ev.ad_id, { runId: ev.run_id || null });
        }
      } else if (ev.type === 'remodel_batch_completed') {
        const adIds = Array.isArray(ev.ad_ids) ? ev.ad_ids.filter(Boolean) : [];
        if ((ev.source || 'manual') === 'manual') {
          if (adIds.length) {
            adIds.forEach(adId => refreshManualRemodelAd(adId, { runId: ev.run_id || null }));
          }
          refreshRemodelRunState().catch(e => console.warn('remodel state refresh failed', e));
        }
      } else if (ev.type === 'remodel_batch_failed') {
        refreshRemodelRunState().catch(e => console.warn('remodel state refresh failed', e));
      } else if (ev.type === 'compliance_completed' || ev.type === 'compliance_failed' || ev.type === 'compliance_batch_completed') {
        loadCompliancePage({ reset: true }).catch(e => console.warn('compliance refresh failed', e));
        refreshComplianceRunState().catch(e => console.warn('compliance state refresh failed', e));
      } else if (ev.type === 'filter_ad_verdict' && ev.ad_id) {
        if (ev.source && ev.source !== 'manual') return;
        RESILIA_API.getSwipe(ev.ad_id)
          .then(row => {
            if (!row) return;
            const adapted = RESILIA_ADAPT.adaptEvaluatedAd(row);
            const pipelineAt = row.filter_verdict?.filtered_at || row.saved_at || null;
            const pipelineAtMs = pipelineAt ? Date.parse(pipelineAt) : NaN;
            const ad = {
              ...adapted,
              source: 'filter',
              filterVerdict: row.filter_verdict || null,
              pipelineAt,
              arrivedAtMs: Number.isFinite(pipelineAtMs)
                ? pipelineAtMs
                : (adapted.arrivedAtMs || Date.now()),
            };
            setFilteredAds(prev => mergeRecentAds([ad], prev));
          })
          .catch(e => console.warn('filter verdict swipe refresh failed', e));
      } else if (ev.type === 'filter_completed') {
        if (ev.source && ev.source !== 'manual') return;
        loadFilterPage({ reset: true }).catch(e => console.warn('filter restore after completion failed', e));
      } else if (ev.type === 'remodel_promoted') {
        if (ev.ad_id && arPickedIdsRef.current.includes(ev.ad_id)) {
          pollRemodelJobs(autoRemodelCurrentPollTargetsForIds([ev.ad_id]), autoRemodelCurrentJobOptions());
        }
        showToast(`Promoted to Video Queue (job ${ev.job_id})`, 'Promote');
      } else if (ev.type === 'filter_verdicts_deleted') {
        const deleted = new Set(ev.ad_ids || []);
        setFilteredAds(prev => prev.filter(a => !deleted.has(a.id)));
      } else if (ev.type === 'remodel_jobs_deleted') {
        const deleted = new Set(ev.ad_ids || []);
        let deletedKeys = [];
        setRemodeledAds(prev => {
          deletedKeys = prev
            .filter(a => deleted.has(a.id))
            .map(a => resultKey(a))
            .filter(Boolean);
          return prev.filter(a => !deleted.has(a.id));
        });
        setRemodelScripts(prev => {
          const next = { ...prev };
          deleted.forEach(id => { delete next[id]; });
          deletedKeys.forEach(key => { delete next[key]; });
          return next;
        });
        setSelRemodel(prev => new Set([...prev].filter(id => !deleted.has(id))));
      } else if (ev.type === 'evaluated_deleted') {
        showToast(`Bulk delete — ${ev.count} ad(s) removed`, 'Delete');
      } else if (ev.type === 'ad_scored' && ev.passes === true) {
        // The crawler fires ``ad_scored`` for every ad it evaluates — when
        // ``passes`` is true the ad just qualified. Stream qualifying ads
        // into Auto-Remodel results immediately so the user sees winners
        // landing while the all-source crawl is still running.
        const stageNow = arStageRef.current;
        const active = !!arRunIdRef.current && stageNow !== 'done' && stageNow !== 'failed';
        if (!active) return;
        const target = arTargetAdsRef.current || 5;
        if (arAcceptedAdIdsRef.current.has(ev.ad_id)) return;
        if (arAcceptedAdIdsRef.current.size >= target) return;

        const dirMap = directionMetaMap(directionsRef.current);
        const synth = {
          ad_id: ev.ad_id,
          direction_id: ev.direction_id,
          score: ev.score,
          passes: true,
          reasoning: ev.reasoning,
          transcript: ev.transcript_preview,
          brand: ev.brand,
          platform: ev.platform,
          source_platform: ev.source_platform,
          running_days: ev.running_days,
          creative_url: ev.creative_url,
          thumbnail_url: ev.thumbnail_url,
          link_url: ev.link_url,
          metrics: ev.metrics || {},
          raw: { source_platform: ev.source_platform, metrics: ev.metrics || {} },
        };
        const adapted = RESILIA_ADAPT.adaptEvaluatedAd(synth);
        attachDirectionMeta(adapted, dirMap);
        const arrivedAtMs = Date.now();
        const ad = {
          ...adapted,
          source: 'auto_remodel',
          _arRunId: arRunIdRef.current,
          currentAutoRemodelRun: true,
          autoRemodelProvisional: true,
          arrivedAtMs,
        };
        const dedupeSettingsNow = dedupeSettingsRef.current;
        const currentRunId = arRunIdRef.current;
        const currentRunAdsNow = autoRemodelAdsRef.current.filter(
          a => a.source === 'auto_remodel' && a._arRunId === currentRunId,
        );
        const currentRunKeys = autoRemodelDedupeKeySet(currentRunAdsNow, dedupeSettingsNow);
        if (hasAutoRemodelDedupeOverlap(currentRunKeys, ad, dedupeSettingsNow)) return;
        if (currentRunAdsNow.length >= target) return;
        arAcceptedAdIdsRef.current.add(ev.ad_id);
        setArAdsFound(Math.min(currentRunAdsNow.length + 1, target));
        RESILIA_API.fetchTranscript(ev.ad_id).then(detail => {
          if (!detail || !detail.transcript) return;
          const patchTranscript = (item) => {
            if (item.id !== ev.ad_id) return item;
            const patched = RESILIA_ADAPT.adaptEvaluatedAd({
              ...synth,
              transcript: detail.transcript,
              transcript_segments: detail.transcript_segments,
              transcript_status: detail.transcript_status,
              transcript_source: detail.transcript_source,
              transcript_provider: detail.transcript_provider,
              transcript_model: detail.transcript_model,
              transcript_word_count: detail.transcript_word_count,
              is_voice_transcript: detail.is_voice_transcript,
              raw: { ...(synth.raw || {}), ...(detail.raw || {}) },
            });
            patched.directionLabel = item.directionLabel;
            patched.directionSearchLabel = item.directionSearchLabel;
            return mergeRichAdDetails(item, {
              ...patched,
              source: item.source,
              _arRunId: item._arRunId,
              autoRemodelProvisional: item.autoRemodelProvisional,
              resultUuid: item.resultUuid,
              cardKey: item.cardKey,
              runId: item.runId,
              arrivedAtMs: item.arrivedAtMs || patched.arrivedAtMs || arrivedAtMs,
            });
          };
          setAutoRemodelAds(prev => uniqueAutoRemodelAds(prev.map(patchTranscript), dedupeSettingsRef.current));
        }).catch(e => console.warn('auto-remodel transcript hydration failed', e));
        setAutoRemodelAds(prev => {
          // Cap by current-run pick count, not by every auto_remodel ad on
          // screen — ads from prior runs are kept below the new ones for
          // history (Bug 8) and must not block new picks.
          const currentRunId = arRunIdRef.current;
          const currentRunAds = prev.filter(
            a => a.source === 'auto_remodel' && a._arRunId === currentRunId,
          );
          const dedupeSettingsCurrent = dedupeSettingsRef.current;
          const currentRunKeysForAdd = autoRemodelDedupeKeySet(currentRunAds, dedupeSettingsCurrent);
          if (hasAutoRemodelDedupeOverlap(currentRunKeysForAdd, ad, dedupeSettingsCurrent)) return prev;
          if (currentRunAds.length >= target) return prev;
          // Append to the end of the current-run section so within-run order
          // matches scoring order, while previous-run ads stay below.
          const otherAds = prev.filter(
            a => !(a.source === 'auto_remodel' && a._arRunId === currentRunId),
          );
          return uniqueAutoRemodelAds([...currentRunAds, { ...ad, cardKey: resultKey(ad) }, ...otherAds], dedupeSettingsCurrent);
        });
        setRemodelScripts(prev => {
          const scriptKey = resultKey(ad);
          if (prev[scriptKey] || prev[ad.id]) return prev;
          return { ...prev, [scriptKey]: { status: 'scored', variants: [] } };
        });
      }
    });
    return close;
    // We intentionally don't include arRunId/arPickedIds in deps — re-creating
    // the SSE connection on every state change would thrash the server. The
    // closure reads the latest values via the setState callbacks.
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [authState.status]);

  const manualRemodelStrategyForAd = (adId) => mergeRemodelInstructions(
    remodelPageInstruction,
    remodelAdInstructions[adId],
  );
  const autoRemodelStrategyForAd = (adId) => mergeRemodelInstructions(
    autoRemodelPageInstruction,
    autoRemodelAdInstructions[adId],
  );

  const startAutoRemodel = async ({
    mode,
    directionId,
    directionIds,
    count,
    nVariants,
    platforms,
    maxAdsPerDirection,
    maxAdsPerPlatform,
    strategy,
  }) => {
    const activeStage = arStageRef.current;
    const alreadyActive = arRunIdRef.current && !['done', 'failed', 'cancelled'].includes(activeStage);
    if (arStartPendingRef.current || alreadyActive) {
      showToast('Auto-Remodel is already starting or running. Use Stop Remodel if that run is stale.', 'Auto-Remodel');
      return;
    }
    arStartPendingRef.current = true;
    setArStartPending(true);
    try {
      const out = await RESILIA_API.runAutoRemodel({
        mode,
        directionId,
        directionIds,
        count,
        nVariants,
        platforms,
        maxAdsPerDirection,
        maxAdsPerPlatform,
        strategy: capRemodelInstruction(strategy || autoRemodelPageInstruction).trim(),
      });
      if (out.status === 'already_running') {
        if (out.state) syncAutoRemodelState(out.state);
        else if (out.run_id) {
          setArRunId(out.run_id);
          arRunIdRef.current = out.run_id;
        }
        showToast('Auto-Remodel is already running. Use Stop Remodel if that run is stale.', 'Auto-Remodel');
        return;
      }
      // Fresh run — clear stale state and keep the user on Auto-Remodel so
      // live scored ads and picked ads appear in the same panel. Stop the
      // manual-flow remodel poller too, otherwise it could overwrite the
      // orchestrator's state.
      if (remodelPollRef.current) {
        stopPoll('remodel');
      }
      setArRunId(out.run_id);
      arRunIdRef.current = out.run_id;
      setArStage('queued');
      arStageRef.current = 'queued';
      setArLastEvent(null);
      setArPickedIds([]);
      arPickedIdsRef.current = [];
      // New Auto-Remodel runs replace the live view; old runs are available
      // only through the explicit "load older" action.
      setAutoRemodelAds([]);
      setRemodelScripts(prev => {
        const autoKeys = new Set(autoRemodelAdsRef.current.flatMap(ad => [ad.id, resultKey(ad)].filter(Boolean)));
        if (!autoKeys.size) return prev;
        const next = { ...prev };
        autoKeys.forEach(key => { delete next[key]; });
        return next;
      });
      setAutoRemodelHistoryLoaded(false);
      // Loaded ids are scoped to the active run. Previous ads stay rendered,
      // but must not block a newly picked copy from hydrating as current.
      autoRemodelLoadedIdsRef.current = new Set();
      setAutoRemodelHydration({ total: 0, loaded: 0, loading: false });
      arAcceptedAdIdsRef.current = new Set();
      setArDirectionsDone(0);
      setArDirectionsTotal(0);
      setArAdsFound(0);
      const requestedTarget = Math.max(1, Math.min(AUTO_REMODEL_COUNT_MAX, parseInt(count, 10) || 5));
      setArTargetAds(requestedTarget);
      arTargetAdsRef.current = requestedTarget;
      setArVariantsDone(0);
      const requestedVariants = Math.max(1, parseInt(nVariants, 10) || 1);
      setArNVariants(requestedVariants);
      arNVariantsRef.current = requestedVariants;
      setArVariantsTotalOverride(null);
      // Bug 8 — do NOT clear autoRemodelAds or remodelScripts here. Previous-
      // run picks and their generated scripts stay visible underneath the new
      // run's results so the user keeps their history when they re-run.
      setTab('autoRemodel');
      // Note — we deliberately do NOT clear ``researchAds`` here. Auto-Remodel
      // results live in ``autoRemodelAds`` (rendered on the Auto-Remodel tab);
      // the Research tab's autoresearch winners are independent state that the
      // user expects to find intact when they switch back. A previous version
      // wiped ``researchAds.filter(a => a.source === 'autoresearch')`` here,
      // which made the Research tab look empty after every Auto-Remodel run.
      showToast(`Auto-Remodel started (${out.run_id}) — staying here until picks land.`, 'Auto-Remodel');
      (async () => {
        for (let attempt = 0; attempt < 8; attempt += 1) {
          const snapshot = RESILIA_API.getAutoRemodelCurrent
            ? await RESILIA_API.getAutoRemodelCurrent(out.run_id)
            : await RESILIA_API.getAutoRemodelState(out.run_id);
          if (snapshot && snapshot.status === 'ok' && snapshot.state) {
            const hydrated = await applyAutoRemodelCurrentPayload(
              snapshot,
              directionsRef.current,
              { announce: attempt > 0 },
            ) || await hydrateAutoRemodelPickedState(
                snapshot.state,
                directionsRef.current,
                { announce: attempt > 0 },
              );
            if (hydrated) return;
          }
          await new Promise(resolve => setTimeout(resolve, 250));
        }
      })().catch(e => console.warn('auto-remodel hydrate failed', e));
    } catch (e) {
      showToast('Failed to start Auto-Remodel: ' + e.message, 'Error');
    } finally {
      arStartPendingRef.current = false;
      setArStartPending(false);
    }
  };

  const loadMoreAutoRemodelAds = async () => {
    try {
      await loadAutoRemodelPage({ ids: arPickedIdsRef.current });
    } catch (e) {
      showToast('Failed to load more Auto-Remodel ads: ' + e.message, 'Error');
    }
  };

  const handleLoadAutoRemodelHistory = async () => {
    if (autoRemodelHistoryLoading) return;
    setAutoRemodelHistoryLoading(true);
    try {
      const ads = await restorePreviousAutoRemodelHistory({
        excludeRunId: arRunIdRef.current,
        directionSource: directionsRef.current,
      });
      setAutoRemodelHistoryLoaded(true);
      if (!ads || ads.length === 0) {
        showToast('No older Auto-Remodel runs to load.', 'Auto-Remodel');
      }
    } catch (e) {
      showToast('Failed to load older Auto-Remodel runs: ' + e.message, 'Error');
    } finally {
      setAutoRemodelHistoryLoading(false);
    }
  };

  const clearAutoRemodelRunState = () => {
    if (remodelPollRef.current) stopPoll('remodel');
    setArRunId(null);
    arRunIdRef.current = null;
    setArStage(null);
    arStageRef.current = null;
    setArLastEvent(null);
    setArPickedIds([]);
    arPickedIdsRef.current = [];
    autoRemodelLoadedIdsRef.current = new Set();
    arAcceptedAdIdsRef.current = new Set();
    setArDirectionsDone(0);
    setArDirectionsTotal(0);
    setArAdsFound(0);
    setArTargetAds(0);
    arTargetAdsRef.current = 0;
    setArVariantsDone(0);
    setArVariantsTotalOverride(null);
    setAutoRemodelHydration({ total: 0, loaded: 0, loading: false });
    setAutoRemodelAds(prev => prev.map(ad => ({ ...ad, currentAutoRemodelRun: false })));
  };

  const stopAutoRemodel = async () => {
    try {
      const out = await RESILIA_API.stopAutoRemodel();
      if (out.status === 'not_running') {
        clearAutoRemodelRunState();
        showToast('Auto-Remodel is not running.', 'Auto-Remodel');
        return;
      }
      clearAutoRemodelRunState();
      const failedJobs = Number(out.jobs_failed || 0);
      const cancelledRuns = Number(out.runs_cancelled || 0);
      showToast(
        failedJobs || cancelledRuns
          ? `Auto-Remodel stopped: cancelled ${cancelledRuns} run(s), failed ${failedJobs} script job(s).`
          : 'Auto-Remodel stopping...',
        'Auto-Remodel',
      );
    } catch (e) {
      showToast('Failed to stop Auto-Remodel: ' + e.message, 'Error');
    }
  };

  const retryFailedAutoRemodel = async () => {
    try {
      const loadedIds = [...autoRemodelLoadedIdsRef.current];
      const loadedIdSet = new Set(loadedIds);
      const loadedAds = loadedIds.length
        ? autoRemodelAds.filter(ad => loadedIdSet.has(ad.id))
        : autoRemodelAds.filter(ad => ad.source === 'auto_remodel');
      const out = await RESILIA_API.retryFailedAutoRemodel(25, {
        runId: arRunIdRef.current,
        nVariants: Math.max(1, parseInt(arNVariantsRef.current, 10) || 1),
        ads: loadedAds,
      });
      let jobsStarted = Number(out.jobs_started || 0);
      const repairIds = loadedIds.length ? loadedIds : loadedAds.map(ad => ad.id).filter(Boolean);
      const targets = repairIds.length ? autoRemodelCurrentPollTargetsForIds(repairIds) : [];
      if (jobsStarted === 0 && loadedIds.length) {
        const fallbackJobs = await RESILIA_API.listRemodelJobs({
          adIds: [...new Set(targets.map(item => (typeof item === 'object' ? item.id : item)).filter(Boolean))],
          resultUuids: [...new Set(targets
            .map(item => (typeof item === 'object' ? item.resultUuid || item.result_uuid : null))
            .filter(Boolean))],
          source: 'auto_remodel',
          limitPerAd: 10,
          limit: 1000,
        });
        const failedJobIds = [...new Set((fallbackJobs || [])
          .filter(job => job?.status === 'failed' && job.id)
          .map(job => job.id))];
        for (const jobId of failedJobIds.slice(0, 25)) {
          await RESILIA_API.retryRemodelJob(jobId);
          jobsStarted += 1;
        }
      }
      if (jobsStarted === 0 && targets.length) {
        const retryableCards = targets
          .filter(item => typeof item === 'object')
          .filter(ad => {
            const entry = scriptEntryForAd(remodelScripts, ad);
            return entry?.status === 'error';
          });
        for (const ad of retryableCards.slice(0, 25)) {
          const strategy = manualRemodelStrategyForAd(ad.id);
          await RESILIA_API.runRemodel({
            adIds: [ad.id],
            nVariants: 1,
            ads: [ad],
            batchStrategy: strategy,
            strategiesByAdId: strategy ? { [ad.id]: strategy } : null,
            source: 'auto_remodel',
          });
          jobsStarted += 1;
        }
      }
      if (repairIds.length) {
        setRemodelScripts(prev => {
          const next = { ...prev };
          loadedAds.forEach(ad => {
            const pending = { status: 'pending', variants: [] };
            next[resultKey(ad)] = pending;
          });
          repairIds.forEach(id => {
            if (!next[id]) next[id] = { status: 'pending', variants: [] };
          });
          return next;
        });
        startVisibilityAwarePoll('remodel', () => pollRemodelJobs(autoRemodelCurrentPollTargetsForIds(repairIds), autoRemodelCurrentJobOptions()), REMODEL_POLL_INTERVAL_MS);
        pollRemodelJobs(autoRemodelCurrentPollTargetsForIds(repairIds), autoRemodelCurrentJobOptions());
      }
      const created = Number(out.jobs_created || 0);
      const retried = Number(out.jobs_retried || 0);
      showToast(
        `Retry started for ${jobsStarted} Auto-Remodel job batch(es): ${retried} retried, ${created} created.`,
        'Auto-Remodel',
      );
    } catch (e) {
      showToast('Retry failed Auto-Remodel scripts failed: ' + e.message, 'Error');
    }
  };

  useEffect(() => {
    if (!arRunningOrch || !arRunId) return undefined;
    let cancelled = false;
    const reconcile = async () => {
      try {
        const snapshot = RESILIA_API.getAutoRemodelCurrent
          ? await RESILIA_API.getAutoRemodelCurrent(arRunIdRef.current)
          : await RESILIA_API.getAutoRemodelState(arRunIdRef.current);
        if (cancelled || !snapshot || snapshot.status !== 'ok' || !snapshot.state) return;
        const hydrated = await applyAutoRemodelCurrentPayload(
          snapshot,
          directionsRef.current,
        );
        if (!hydrated) {
          syncAutoRemodelState(snapshot.state);
        }
      } catch (e) {
        console.warn('auto-remodel reconcile failed', e);
      }
    };
    const timer = setInterval(reconcile, 3000);
    reconcile();
    return () => {
      cancelled = true;
      clearInterval(timer);
    };
  }, [arRunningOrch, arRunId]);

  const regenerateRemodelJob = async (jobId, options = {}) => {
    try {
      const locatedScriptKey = Object.keys(remodelScripts).find(id => {
        const e = remodelScripts[id];
        return e && (
          (Array.isArray(e.variants) && e.variants.some(v => v.jobId === jobId))
          || (Array.isArray(e.failedJobIds) && e.failedJobIds.includes(jobId))
        );
      });
      const scriptKey = options.scriptKey || locatedScriptKey || options.adId || null;
      const ad = [...remodeledAds, ...autoRemodelAds].find(item => (
        (scriptKey && resultKey(item) === scriptKey)
        || item.id === options.adId
        || item.id === scriptKey
      ));
      const adId = options.adId || ad?.id || scriptKey;
      const source = options.source || (ad?.source === 'auto_remodel' ? 'auto_remodel' : 'manual');
      const strategy = options.strategy || (adId ? manualRemodelStrategyForAd(adId) : null);
      const variantPlan = Array.isArray(options.variantPlan) && options.variantPlan.length
        ? options.variantPlan.slice(0, REMODEL_VARIANTS_MAX).map(row => remodelPlanRow(row.remodelMethod, row.temperature))
        : [remodelPlanRow()];
      let out;
      if (jobId && !options.variantPlan) {
        out = await RESILIA_API.retryRemodelJob(jobId, { strategy });
      } else if (ad) {
        // Regenerate is replace-not-append: clear old jobs for this card/source
        // before queueing the fresh variant plan.
        await RESILIA_API.bulkDeleteRemodelJobs(
          [ad.id],
          source,
          { reason: 'replace' },
        );
        out = await RESILIA_API.runRemodel({
          adIds: [ad.id],
          nVariants: variantPlan.length,
          variantPlan,
          ads: [ad],
          batchStrategy: strategy,
          strategiesByAdId: strategy ? { [ad.id]: strategy } : null,
          source,
        });
      } else {
        throw new Error('No failed job id or ad payload available to retry');
      }
      if (scriptKey || adId) {
        const pollTarget = ad || adId;
        setRemodelScripts(prev => {
          const next = { ...prev };
          const pending = remodelPendingEntryForPlan(variantPlan);
          if (scriptKey) next[scriptKey] = pending;
          if (!ad?.resultUuid && adId) next[adId] = pending;
          return next;
        });
        const pollOptions = source === 'auto_remodel'
          ? autoRemodelJobOptions({ expectedVariants: variantPlan.length, runId: out?.run_id || null })
          : { source: 'manual', limitPerAd: variantPlan.length, expectedVariants: variantPlan.length, runId: out?.run_id || null };
        startVisibilityAwarePoll('remodel', () => pollRemodelJobs([pollTarget], pollOptions), REMODEL_POLL_INTERVAL_MS);
        pollRemodelJobs([pollTarget], pollOptions);
      }
      showToast(`Regeneration started (${out.run_id || jobId || adId}).`, 'Remodel');
    } catch (e) {
      showToast('Regenerate failed: ' + e.message, 'Error');
    }
  };

  const discardRemodelJob = async (jobId) => {
    if (!confirm('Discard this remodel variant?')) return;
    try {
      await RESILIA_API.deleteRemodelJob(jobId);
      setRemodelScripts(prev => {
        const next = { ...prev };
        for (const adId of Object.keys(next)) {
          const entry = next[adId];
          if (!entry || !Array.isArray(entry.variants)) continue;
          const variants = entry.variants.filter(v => v.jobId !== jobId);
          next[adId] = { ...entry, variants, status: variants.length ? entry.status : 'error' };
        }
        return next;
      });
      showToast('Variant discarded.', 'Remodel');
    } catch (e) {
      showToast('Discard failed: ' + e.message, 'Error');
    }
  };

  const promoteRemodelJob = async (jobId) => {
    try {
      const out = await RESILIA_API.promoteRemodelJob(jobId);
      const job = out.job || out;
      const ad = remodeledAds.find(a => a.id === job.ad_id) || autoRemodelAds.find(a => a.id === job.ad_id);
      if (ad) {
        setConvertedAds(prev => prev.some(a => a.id === ad.id) ? prev : [...prev, ad]);
      }
      if (job.ad_id) pollRemodelJobs([job.ad_id]);
      showToast(`Promoted to Video Queue (job ${job.id || jobId}).`, 'Promote');
      return job;
    } catch (e) {
      showToast('Promote failed: ' + e.message, 'Error');
      return null;
    }
  };

  const promoteAllAutoRemodelJobs = async () => {
    const jobIds = [];
    autoRemodelAds.forEach(ad => {
      const entry = remodelScripts[resultKey(ad)] || remodelScripts[ad.id];
      if (!entry || !Array.isArray(entry.variants)) return;
      entry.variants.forEach(v => {
        if (v.jobId) jobIds.push(v.jobId);
      });
    });
    if (!jobIds.length) {
      showToast('No Auto-Remodel scripts are ready.', 'Promote');
      return;
    }
    try {
      const promoted = [];
      for (const jobId of jobIds) {
        const out = await RESILIA_API.promoteRemodelJob(jobId);
        const job = out.job || out;
        if (job) promoted.push(job);
      }
      const promotedAdIds = new Set(promoted.map(j => j.ad_id).filter(Boolean));
      setConvertedAds(prev => {
        const seen = new Set(prev.map(a => a.id));
        const next = [...prev];
        autoRemodelAds.forEach(ad => {
          if (promotedAdIds.has(ad.id) && !seen.has(ad.id)) next.push(ad);
        });
        return next;
      });
      if (promotedAdIds.size) pollRemodelJobs([...promotedAdIds]);
      showToast(`${promoted.length} Auto-Remodel script${promoted.length === 1 ? '' : 's'} promoted to Video Queue.`, 'Promote');
    } catch (e) {
      showToast('Promote all failed: ' + e.message, 'Error');
    }
  };

  const readyRemodelJobIdsForSelection = (ads, selected) => {
    const out = [];
    (ads || []).forEach(ad => {
      if (!selected.has(ad.id)) return;
      const entry = scriptEntryForAd(remodelScripts, ad);
      if (!entry || !Array.isArray(entry.variants)) return;
      entry.variants.forEach(variant => {
        if (variant.jobId && !variant.pending && variant.raw) out.push(variant.jobId);
      });
    });
    return [...new Set(out)];
  };

  const readyRemodelVariantIds = (ads) => {
    const ids = new Set();
    (ads || []).forEach(ad => {
      const entry = scriptEntryForAd(remodelScripts, ad);
      if (!entry || !Array.isArray(entry.variants)) return;
      entry.variants.forEach(variant => {
        if (variant.jobId && !variant.pending && variant.raw) ids.add(variant.jobId);
      });
    });
    return ids;
  };

  const fetchReadyRemodelJobIdsForSelection = async (ads, selected, source) => {
    const picked = (ads || []).filter(ad => selected.has(ad.id));
    if (!picked.length) return [];
    const adIds = [...new Set(picked.map(ad => ad.id).filter(Boolean))];
    const resultUuids = [...new Set(picked.map(ad => ad.resultUuid || ad.result_uuid).filter(Boolean))];
    try {
      const jobs = await RESILIA_API.listRemodelJobs({
        adIds,
        resultUuids,
        source,
        limitPerAd: REMODEL_VARIANTS_MAX,
        limit: Math.max(PIPELINE_PAGE_SIZE, adIds.length * REMODEL_VARIANTS_MAX),
      });
      const ready = (jobs || [])
        .filter(job => job.status === 'done' && job.response_text)
        .map(job => job.id)
        .filter(Boolean);
      if (ready.length) return [...new Set(ready)];
    } catch (e) {
      console.warn('fetch selected remodel variants failed, falling back to local state', e);
    }
    return readyRemodelJobIdsForSelection(ads, selected);
  };

  const sendSelectedToCompliance = async (source) => {
    const isAuto = source === 'auto_remodel';
    const ads = isAuto ? autoRemodelAds : remodeledAds;
    const selected = isAuto ? selAutoRemodel : selRemodel;
    const selectedVariants = isAuto ? selAutoRemodelVariants : selRemodelVariants;
    let jobIds = [...selectedVariants];
    if (selected.size) {
      const selectedAdJobIds = await fetchReadyRemodelJobIdsForSelection(ads, selected, source);
      jobIds = [...new Set([...jobIds, ...selectedAdJobIds])];
    }
    if (!jobIds.length) {
      showToast('No ready remodel variants selected for compliance.', 'Compliance');
      return;
    }
    setComplianceBusy(true);
    try {
      const out = await RESILIA_API.runCompliance({
        remodelJobIds: jobIds,
        strictness: complianceStrictness,
        source,
      });
      if (isAuto) setSelAutoRemodel(new Set());
      else setSelRemodel(new Set());
      if (isAuto) setSelAutoRemodelVariants(new Set());
      else setSelRemodelVariants(new Set());
      setSelCompliance(new Set(out.jobs || []));
      setTab('compliance');
      tabLoadedRef.current.delete('compliance');
      await loadCompliancePage({ reset: true });
      await refreshComplianceRunState();
      startCompliancePolling();
      showToast(`${jobIds.length} variant${jobIds.length === 1 ? '' : 's'} sent to Compliance.`, 'Compliance');
    } catch (e) {
      showToast('Compliance run failed: ' + e.message, 'Error');
    } finally {
      setComplianceBusy(false);
    }
  };

  const convertComplianceToVideo = () => {
    const picked = complianceJobs.filter(job => selCompliance.has(job.id) && job.status === 'done');
    if (!picked.length) {
      showToast('No completed compliance jobs selected.', 'Compliance');
      return;
    }
    const resolveFinalTranscript = (job) => (
      job.final_transcript || (job.compliant ? job.input_script : job.regenerated_transcript) || ''
    );
    const ads = picked.map(job => {
      const base = job.ad ? RESILIA_ADAPT.adaptEvaluatedAd(job.ad) : pipelineFallbackAd(job.ad_id, 'compliance');
      const finalTranscript = resolveFinalTranscript(job);
      return {
        ...base,
        source: 'compliance',
        complianceJobId: job.id,
        complianceFinalTranscript: finalTranscript,
        finalTranscript,
      };
    });
    setConvertedAds(prev => {
      const seen = new Set(prev.map(a => a.complianceJobId ? `c:${a.complianceJobId}` : `a:${a.id}`));
      const next = [...prev];
      ads.forEach(ad => {
        const key = ad.complianceJobId ? `c:${ad.complianceJobId}` : `a:${ad.id}`;
        if (!seen.has(key)) {
          seen.add(key);
          next.push(ad);
        }
      });
      return next;
    });
    setSelCompliance(new Set());
    setTab('video');
  };

  const editComplianceTranscript = async (jobId, newText) => {
    if (!jobId) {
      showToast('No compliance job to edit yet — wait for it to finish.', 'Compliance');
      return;
    }
    try {
      const out = await RESILIA_API.updateComplianceTranscript({ jobId, finalTranscript: newText });
      const job = out.job || out;
      setComplianceJobs(prev => prev.map(item => (item.id === jobId ? { ...item, ...job } : item)));
      showToast('Edit saved.', 'Compliance');
    } catch (e) {
      showToast('Save failed: ' + e.message, 'Error');
      throw e;
    }
  };

  const discardComplianceJob = async (jobId) => {
    if (!confirm('Discard this compliance result?')) return;
    try {
      await RESILIA_API.deleteComplianceJob(jobId);
      setComplianceJobs(prev => prev.filter(item => item.id !== jobId));
      setSelCompliance(prev => {
        const next = new Set(prev);
        next.delete(jobId);
        return next;
      });
      showToast('Compliance result discarded.', 'Compliance');
    } catch (e) {
      showToast('Discard failed: ' + e.message, 'Error');
    }
  };

  const scopedResearchAdsForTab = (targetTab = tab) => researchAds.filter(ad => (
    targetTab === 'autoSearch' ? ad.source === 'autoresearch' : ad.source !== 'autoresearch'
  ));

  // ---- Research-tab bulk delete -----------------------------------------
  const bulkDeleteResearch = async () => {
    const ids = scopedResearchAdsForTab().filter(ad => selResearch.has(ad.id)).map(ad => ad.id);
    if (!ids.length) return;
    if (!confirm(`Remove ${ids.length} selected card${ids.length === 1 ? '' : 's'}?\n\nSearch history, AutoSearch history, and evaluated rows are updated. Swipe-file rows stay available.`)) return;
    try {
      const idSet = new Set(ids);
      const out = await RESILIA_API.bulkDeleteEvaluated(ids);
      setResearchAds(prev => prev.filter(a => !idSet.has(a.id)));
      setSelResearch(new Set());
      showToast(`${out.removed} ad(s) removed.`, 'Delete');
    } catch (e) {
      showToast('Bulk delete failed: ' + e.message, 'Error');
    }
  };

  const removeAdFromLocalViews = (adId) => {
    setResearchAds(prev => prev.filter(a => a.id !== adId));
    setFilteredAds(prev => prev.filter(a => a.id !== adId));
    setRemodeledAds(prev => prev.filter(a => a.id !== adId));
    setAutoRemodelAds(prev => prev.filter(a => a.id !== adId));
    setConvertedAds(prev => prev.filter(a => a.id !== adId));
    const drop = (setter) => setter(prev => {
      if (!prev.has(adId)) return prev;
      const next = new Set(prev);
      next.delete(adId);
      return next;
    });
    drop(setSelResearch);
    drop(setSelFilter);
    drop(setSelRemodel);
    drop(setSelAutoRemodel);
    setSelCompliance(prev => new Set([...prev].filter(jobId => {
      const job = complianceJobs.find(row => row.id === jobId);
      return job && job.ad_id !== adId;
    })));
  };

  const dislikeAd = async (ad) => {
    const adId = ad?.id || ad?.ad_id;
    if (!adId) return;
    if (!confirm('Dislike this ad?\n\nIt will disappear now and the same creative will be blocked from future searches.')) return;
    try {
      const out = await RESILIA_API.dislikeAd(ad);
      removeAdFromLocalViews(adId);
      showToast('Ad disliked and hidden permanently.', 'Dislike');
      return out;
    } catch (e) {
      showToast('Dislike failed: ' + e.message, 'Error');
      return null;
    }
  };

  const loadMoreFilterAds = async () => {
    try {
      await loadFilterPage();
    } catch (e) {
      showToast('Failed to load more filtered ads: ' + e.message, 'Error');
    }
  };

  const loadMoreRemodelAds = async () => {
    try {
      await loadRemodelPage();
    } catch (e) {
      showToast('Failed to load more remodels: ' + e.message, 'Error');
    }
  };

  const loadMoreComplianceJobs = async () => {
    try {
      await loadCompliancePage();
    } catch (e) {
      showToast('Failed to load more compliance jobs: ' + e.message, 'Error');
    }
  };

  const bulkDeleteFilter = async () => {
    const ids = [...selFilter];
    if (!ids.length) return;
    if (!confirm(`Remove ${ids.length} filtered video${ids.length === 1 ? '' : 's'} from the Filter tab?\n\nSwipe-file rows stay available for search and future runs.`)) return;
    try {
      const out = await RESILIA_API.bulkDeleteFilterVerdicts(ids);
      setFilteredAds(prev => prev.filter(a => !selFilter.has(a.id)));
      setSelFilter(new Set());
      showToast(`${out.removed} filtered video${out.removed === 1 ? '' : 's'} removed.`, 'Delete');
    } catch (e) {
      showToast('Filter delete failed: ' + e.message, 'Error');
    }
  };

  const bulkDeleteRemodel = async () => {
    const ids = [...selRemodel];
    if (!ids.length) return;
    if (!confirm(`Delete remodel jobs for ${ids.length} video${ids.length === 1 ? '' : 's'}?\n\nThis removes the scripts from the Remodel tab.`)) return;
    const selectedAds = remodeledAds.filter(a => selRemodel.has(a.id));
    try {
      const out = await RESILIA_API.bulkDeleteRemodelJobs(ids);
      setRemodeledAds(prev => prev.filter(a => !selRemodel.has(a.id)));
      setRemodelScripts(prev => {
        const next = { ...prev };
        selectedAds.forEach(ad => {
          delete next[ad.id];
          const key = resultKey(ad);
          if (key) delete next[key];
        });
        return next;
      });
      setSelRemodel(new Set());
      setSelRemodelVariants(new Set());
      showToast(`${out.removed} remodel job${out.removed === 1 ? '' : 's'} deleted.`, 'Delete');
    } catch (e) {
      showToast('Remodel delete failed: ' + e.message, 'Error');
    }
  };

  const bulkDeleteAutoRemodel = async () => {
    const ids = [...selAutoRemodel];
    if (!ids.length) return;
    if (!confirm(`Delete ${ids.length} Auto-Remodel video${ids.length === 1 ? '' : 's'}?\n\nThis removes saved Auto-Remodel result cards and their generated scripts. Swipe-file rows stay available for future searches.`)) return;
    const selectedAds = autoRemodelAds.filter(ad => selAutoRemodel.has(ad.id));
    try {
      const out = await RESILIA_API.bulkDeleteAutoRemodelAds(ids);
      setAutoRemodelAds(prev => prev.filter(a => !selAutoRemodel.has(a.id)));
      const remainingPickedIds = arPickedIdsRef.current.filter(id => !selAutoRemodel.has(id));
      arPickedIdsRef.current = remainingPickedIds;
      setArPickedIds(remainingPickedIds);
      autoRemodelLoadedIdsRef.current = new Set(
        [...autoRemodelLoadedIdsRef.current].filter(id => !selAutoRemodel.has(id)),
      );
      setAutoRemodelHydration(prev => ({
        ...prev,
        total: Math.max(0, (prev.total || 0) - ids.length),
        loaded: Math.max(0, (prev.loaded || 0) - ids.length),
      }));
      setRemodelScripts(prev => {
        const next = { ...prev };
        selectedAds.forEach(ad => {
          delete next[ad.id];
          const key = resultKey(ad);
          if (key) delete next[key];
        });
        return next;
      });
      setAutoRemodelAdInstructions(prev => {
        const next = { ...prev };
        ids.forEach(id => { delete next[id]; });
        return next;
      });
      setSelAutoRemodel(new Set());
      showToast(`${out.removed} Auto-Remodel video${out.removed === 1 ? '' : 's'} deleted.`, 'Delete');
    } catch (e) {
      showToast('Auto-Remodel delete failed: ' + e.message, 'Error');
    }
  };

  // ---- Pipeline transitions ----------------------------------------------
  const toggleSel = (setter) => (id) => {
    setter(prev => {
      const n = new Set(prev);
      n.has(id) ? n.delete(id) : n.add(id);
      return n;
    });
  };

  useEffect(() => {
    const focus = pipelineFocusRef.current;
    if (!focus || focus.tab !== tab || !focus.ids?.length) return undefined;
    const timer = setTimeout(() => {
      const cards = [...document.querySelectorAll('[data-ad-id]')];
      const node = cards.find(el => focus.ids.includes(el.getAttribute('data-ad-id')));
      if (!node) return;
      node.scrollIntoView({ behavior: 'smooth', block: 'center' });
      if (typeof node.focus === 'function') node.focus({ preventScroll: true });
    }, 120);
    return () => clearTimeout(timer);
  }, [tab, pipelineFocusTick, filteredAds, remodeledAds]);

  const passToFilter = async () => {
    const pickedById = new Map();
    scopedResearchAdsForTab().forEach(ad => {
      if (selResearch.has(ad.id) && !pickedById.has(ad.id)) pickedById.set(ad.id, ad);
    });
    const picked = [...pickedById.values()];
    if (picked.length === 0) return;
    setFilterBusy(true);
    const arrivedAtMs = Date.now();
    const batchId = `filter-move-${arrivedAtMs}`;
    const movedIds = picked.map(a => a.id);
    const seededAds = picked.map(a => ({ ...a, source: 'filter', arrivedAtMs }));
    setFilteredAds(prev => mergeCurrentPipelineAds(seededAds, prev, batchId));
    setSelResearch(new Set());
    setSelFilter(new Set(movedIds));
    tabLoadedRef.current.add('filter');
    setTab('filter');
    focusPipelineAds('filter', movedIds);
    try {
      await RESILIA_API.runFilter({ adIds: picked.map(a => a.id), ads: picked });
      await refreshFilterRunState();
      startFilterStatePolling();
      showToast(`Filter started on ${picked.length} ads.`, 'Filter');
    } catch (e) {
      showToast('Filter failed: ' + e.message, 'Error');
    } finally {
      if (!filterPollRef.current) setFilterBusy(false);
    }
  };

  const passWithoutFilter = async () => {
    const picked = scopedResearchAdsForTab().filter(a => selResearch.has(a.id));
    if (picked.length === 0) return;
    if (remodelStartPendingRef.current) {
      showToast('Remodel is already starting. Wait a moment and try again.', 'Remodel');
      return;
    }
    remodelStartPendingRef.current = true;
    setRemodelBusy(true);
    const arrivedAtMs = Date.now();
    const batchId = `remodel-bypass-${arrivedAtMs}`;
    const movedIds = picked.map(a => a.id);
    const seededAds = picked.map(a => ({ ...a, source: 'remodel', arrivedAtMs }));
    setRemodeledAds(prev => mergeCurrentPipelineAds(seededAds, prev, batchId));
    setRemodelScripts(prev => {
      const next = { ...prev };
      picked.forEach(ad => { next[resultKey(ad)] = remodelPendingEntryForPlan(); });
      return next;
    });
    setSelResearch(new Set());
    setSelRemodel(new Set(movedIds));
    setTab('remodel');
    focusPipelineAds('remodel', movedIds);
    try {
      await startRemodelAndPoll(picked);
      showToast(`${picked.length} ads passed without filtering. Remodeling…`, 'Bypass');
    } catch (e) {
      setRemodelScripts(prev => {
        const next = { ...prev };
        picked.forEach(ad => { next[resultKey(ad)] = 'error'; });
        return next;
      });
      showToast('Pass failed: ' + e.message, 'Error');
    } finally {
      remodelStartPendingRef.current = false;
      if (!remodelPollRef.current) {
        setRemodelBusy(false);
      }
    }
  };

  // Extract just the REMODELED SCRIPT prose from the remodeler output — skip
  // the analysis header and any surrounding metadata. Each paragraph becomes
  // one untagged line so the UI renders it as a readable transcript rather
  // than a HOOK/PROBLEM/SOLUTION breakdown.
  const parseRemodelScript = (text) => {
    if (!text) return [];
    const scriptMatch = text.match(/###\s*REMODELED\s+SCRIPT\s*\n([\s\S]*?)(?=\n###\s|$)/i);
    const body = scriptMatch ? scriptMatch[1].trim() : text.trim();
    const paras = body.split(/\n\s*\n/).map(p => p.trim()).filter(Boolean);
    return paras.map(p => ({
      tag: '',
      body: p.replace(/^\*\*[^*]+\*\*\s*/, '').replace(/\n+/g, ' '),
    }));
  };

  // ``editRemodelScript`` now takes a job id directly (RemodelView passes
  // the jobId of the specific variant being edited). Each ad can have
  // multiple variants in flight, so an adId-only lookup would be ambiguous.
  const editRemodelScript = async (jobId, newText) => {
    if (!jobId) {
      showToast('No remodel job to edit yet — wait for it to finish.', 'Remodel');
      return;
    }
    try {
      await RESILIA_API.updateRemodelScript({ jobId, responseText: newText });
      // Refresh by re-polling the affected ad — keeps the variants array in
      // sync with the DB without a bespoke patch path.
      const adId = Object.keys(remodelScripts).find(id => {
        const e = remodelScripts[id];
        return e && Array.isArray(e.variants) && e.variants.some(v => v.jobId === jobId);
      });
      if (adId) pollRemodelJobs([adId]);
      showToast('Edit saved.', 'Remodel');
    } catch (e) {
      showToast('Save failed: ' + e.message, 'Error');
      throw e;
    }
  };

  // Each ad can spawn N variants (one Sonnet call per variant). We track
  // them as ``{ status, variants: [{ jobId, variantIdx, raw, lines }] }``
  // so the Remodel view can render every finished variant as its own
  // editable card. Manual polling stops when the backend run state reaches a
  // terminal status, so the progress banner cannot get stranded at 0/N.
  const autoRemodelJobOptions = (extra = {}) => ({
    source: 'auto_remodel',
    limitPerAd: REMODEL_VARIANTS_MAX,
    stopWhenSettled: true,
    ...extra,
  });

  const autoRemodelCurrentJobOptions = (extra = {}) => autoRemodelJobOptions(extra);

  const autoRemodelPollTargetsForIds = (ids) => {
    const idSet = new Set((ids || []).filter(Boolean));
    if (!idSet.size) return [];
    const sourceAds = autoRemodelAdsRef.current.length ? autoRemodelAdsRef.current : autoRemodelAds;
    const targets = sourceAds.filter(ad => idSet.has(ad.id));
    return targets.length ? targets : [...idSet];
  };

  const autoRemodelCurrentPollTargetsForIds = (ids) => {
    const currentRunId = arRunIdRef.current;
    const targets = autoRemodelPollTargetsForIds(ids);
    if (!currentRunId) return targets;
    const currentTargets = targets.filter(item => (
      typeof item !== 'object'
      || item._arRunId === currentRunId
      || item.currentAutoRemodelRun
    ));
    return currentTargets.length ? currentTargets : targets;
  };

  const pollAutoRemodelHistoryJobs = (ads, extra = {}) => {
    const grouped = new Map();
    (ads || []).forEach(ad => {
      if (!ad || typeof ad !== 'object') return;
      const runId = ad._arRunId || null;
      const key = runId || '__unknown__';
      if (!grouped.has(key)) grouped.set(key, []);
      grouped.get(key).push(ad);
    });
    grouped.forEach((group, key) => {
      pollRemodelJobs(
        group,
        autoRemodelJobOptions({
          ...extra,
        }),
      );
    });
  };

  const refreshManualRemodelAd = async (adId, options = {}) => {
    if (!adId) return;
    try {
      const row = await RESILIA_API.getSwipe(adId);
      const ad = row
        ? { ...RESILIA_ADAPT.adaptEvaluatedAd(row), source: 'remodel' }
        : pipelineFallbackAd(adId, 'remodel');
      setRemodeledAds(prev => mergeRecentAds([ad], prev));
      await pollRemodelJobs([ad], { source: 'manual', ...options });
    } catch (e) {
      console.warn('manual remodel refresh failed', e);
      await pollRemodelJobs([adId], { source: 'manual', ...options });
    }
  };

  const pollRemodelJobs = async (adIds, options = {}) => {
    const markMissingAsError = !!options.markMissingAsError;
    const stopWhenSettled = !!options.stopWhenSettled;
    try {
      const targets = (adIds || [])
        .map(item => {
          if (item && typeof item === 'object') {
            return {
              adId: item.id || item.ad_id,
              scriptKey: resultKey(item),
              resultUuid: item.resultUuid || item.result_uuid || null,
            };
          }
          const id = String(item || '').trim();
          const currentAutoRemodelAds = autoRemodelAdsRef.current.length
            ? autoRemodelAdsRef.current
            : autoRemodelAds;
          const existing = [...remodeledAds, ...currentAutoRemodelAds].find(ad => ad.id === id);
          return {
            adId: id,
            scriptKey: existing ? resultKey(existing) : id,
            resultUuid: existing?.resultUuid || existing?.result_uuid || null,
          };
        })
        .filter(t => t.adId && t.scriptKey);
      const cleanIds = [...new Set(targets.map(t => t.adId))];
      const resultUuids = [...new Set(targets.map(t => t.resultUuid).filter(Boolean))];
      if (!cleanIds.length) return;
      const jobs = await RESILIA_API.listRemodelJobs({
        adIds: cleanIds,
        resultUuids,
        runId: options.runId || null,
        source: options.source || null,
        limitPerAd: options.limitPerAd || null,
        limit: Math.min(1000, cleanIds.length * Math.max(1, options.limitPerAd || 10)),
      });
      const jobsByTarget = new Map();
      (jobs || []).forEach(job => {
        if (!job?.ad_id) return;
        const payloadUuid = job.payload?.result_uuid || job.payload?.resultUuid || null;
        [...new Set([payloadUuid, job.ad_id].filter(Boolean))].forEach(key => {
          if (!jobsByTarget.has(key)) jobsByTarget.set(key, []);
          jobsByTarget.get(key).push(job);
        });
      });
      setRemodelScripts(prev => {
        const next = { ...prev };
        let stillPending = 0;
        targets.forEach(({ adId, scriptKey, resultUuid }) => {
          const existingEntry = next[scriptKey] || next[adId];
          const targetJobs = (resultUuid && jobsByTarget.get(resultUuid))
            || jobsByTarget.get(adId)
            || [];
          const latestRunId = options.runId || targetJobs.find(job => job?.run_id)?.run_id || null;
          const jobs = latestRunId
            ? targetJobs.filter(job => job.run_id === latestRunId)
            : targetJobs;
          if (jobs.length === 0) {
            const hasExistingVariants = Array.isArray(existingEntry?.variants)
              && existingEntry.variants.length > 0;
            if (hasExistingVariants || existingEntry?.status === 'error') return;
            if (markMissingAsError) {
              next[scriptKey] = {
                status: 'error',
                variants: [],
                error: 'No remodel job was found for this ad yet. '
                  + 'Retry generation or check the Auto-Remodel run logs.',
              };
            } else {
              if (!existingEntry || existingEntry.status === 'pending' || existingEntry.status === 'scored') {
                next[scriptKey] = { status: 'pending', variants: [] };
              }
              stillPending++;
            }
            return;
          }
          const done = jobs.filter(j => j.status === 'done' && j.response_text);
          const failed = jobs.filter(j => j.status === 'failed');
          const inFlight = jobs.length - done.length - failed.length;
          const expectedCount = Math.max(
            1,
            options.expectedVariants || existingEntry?.expectedVariants || jobs.length,
          );
          const variantFromJob = (j, extra = {}) => ({
            jobId: j.id,
            variantIdx: j.variant_idx || 0,
            remodelMethod: normalizeRemodelMethod(j.remodel_method),
            temperature: j.temperature,
            promotedAt: j.promoted_at || null,
            ...extra,
          });
          if (jobs.length > 0) {
            const pendingJobs = jobs.filter(j => j.status !== 'done' && j.status !== 'failed');
            const mappedVariants = [
              ...done.map(j => variantFromJob(j, {
                raw: j.response_text,
                lines: parseRemodelScript(j.response_text),
              })),
              ...failed.map(j => variantFromJob(j, {
                raw: '',
                lines: [{ tag: 'ERROR', body: j.error || 'Generation failed' }],
              })),
              ...pendingJobs.map(j => variantFromJob(j, {
                pending: true,
                raw: '',
                lines: [{ tag: 'REMODELING', body: 'Claude Sonnet is generating this remodel output.' }],
              })),
            ].sort((a, b) => (a.variantIdx || 0) - (b.variantIdx || 0));
            const filledVariants = [...mappedVariants];
            while (filledVariants.length < expectedCount) {
              const seed = existingEntry?.variants?.[filledVariants.length] || {};
              filledVariants.push({
                jobId: null,
                variantIdx: filledVariants.length + 1,
                pending: true,
                raw: '',
                lines: [{ tag: 'REMODELING', body: 'Claude Sonnet is generating this remodel output.' }],
                remodelMethod: normalizeRemodelMethod(seed.remodelMethod),
                temperature: seed.temperature ?? 0.5,
                promotedAt: null,
              });
            }
            next[scriptKey] = {
              status: inFlight > 0 || filledVariants.some(v => v.pending) ? 'pending' : 'ready',
              expectedVariants: expectedCount,
              variants: filledVariants,
              failedJobIds: failed.filter(j => j.id).map(j => j.id),
            };
            if (scriptKey !== adId) next[adId] = next[scriptKey];
            if (inFlight > 0 || filledVariants.some(v => v.pending)) stillPending++;
          } else if (failed.length === jobs.length && jobs.length > 0) {
            next[scriptKey] = {
              status: 'error',
              variants: [],
              error: failed[0]?.error || 'Generation failed',
              failedJobIds: failed.filter(j => j.id).map(j => j.id),
            };
            if (scriptKey !== adId) next[adId] = next[scriptKey];
          } else {
            next[scriptKey] = { status: 'pending', variants: [] };
            if (scriptKey !== adId) next[adId] = next[scriptKey];
            stillPending++;
          }
        });
        if (stopWhenSettled && stillPending === 0 && remodelPollRef.current) {
          stopPoll('remodel');
        }
        return next;
      });
    } catch (e) {
      console.warn('poll remodel jobs failed', e);
    }
  };

  const startRemodelAndPoll = async (picked) => {
    const adIds = picked.map(a => a.id);
    const perAdStrategies = Object.fromEntries(
      picked
        .map(ad => [ad.id, capRemodelInstruction(remodelAdInstructions[ad.id] || '').trim()])
        .filter(([, strategy]) => !!strategy),
    );
    setRemodelScripts(prev => {
      const next = { ...prev };
      picked.forEach(ad => { next[resultKey(ad)] = remodelPendingEntryForPlan(); });
      return next;
    });
    const out = await RESILIA_API.runRemodel({
      adIds,
      nVariants: 1,
      variantPlan: [remodelPlanRow()],
      ads: picked,
      batchStrategy: capRemodelInstruction(remodelPageInstruction).trim(),
      strategiesByAdId: perAdStrategies,
    });
    const runId = out.run_id || null;
    await refreshRemodelRunState();
    startVisibilityAwarePoll('remodel', () => {
      pollRemodelJobs(picked, { source: 'manual', runId });
      refreshRemodelRunState();
    }, REMODEL_POLL_INTERVAL_MS);
    remodelStartPendingRef.current = false;
    pollRemodelJobs(picked, { source: 'manual', runId });
  };

  const runRemodel = async () => {
    const picked = filteredAds.filter(a => selFilter.has(a.id));
    if (picked.length === 0) return;
    if (remodelStartPendingRef.current) {
      showToast('Remodel is already starting. Wait a moment and try again.', 'Remodel');
      return;
    }
    remodelStartPendingRef.current = true;
    setRemodelBusy(true);
    const arrivedAtMs = Date.now();
    const batchId = `remodel-move-${arrivedAtMs}`;
    const movedIds = picked.map(a => a.id);
    const seededAds = picked.map(a => ({ ...a, source: 'remodel', arrivedAtMs }));
    setRemodeledAds(prev => mergeCurrentPipelineAds(seededAds, prev, batchId));
    setRemodelScripts(prev => {
      const next = { ...prev };
      picked.forEach(ad => { next[resultKey(ad)] = remodelPendingEntryForPlan(); });
      return next;
    });
    setSelFilter(new Set());
    setSelRemodel(new Set(movedIds));
    setTab('remodel');
    focusPipelineAds('remodel', movedIds);
    try {
      await startRemodelAndPoll(picked);
      showToast(`Remodel started on ${picked.length} ads (1 variant each).`, 'Remodel');
    } catch (e) {
      setRemodelScripts(prev => {
        const next = { ...prev };
        picked.forEach(ad => { next[resultKey(ad)] = 'error'; });
        return next;
      });
      showToast('Remodel failed: ' + e.message, 'Error');
    } finally {
      remodelStartPendingRef.current = false;
      if (!remodelPollRef.current) {
        setRemodelBusy(false);
      }
    }
  };

  const convert = () => {
    const picked = remodeledAds.filter(a => selRemodel.has(a.id));
    setConvertedAds(picked);
    setSelRemodel(new Set());
    setTab('video');
    showToast(`${picked.length} ads queued for video conversion.`, 'Video');
  };

  const signIn = () => {
    window.location.assign(RESILIA_API.loginUrl());
  };

  const signOut = async () => {
    await RESILIA_API.logout();
    setAuthState({ status: 'signed_out', user: null });
  };

  const pipelineStatusText = (pipelineState, label) => {
    const run = pipelineState?.run;
    if (!run) return null;
    const effectiveStatus = pipelineState?.state?.effective_status || run.status;
    const total = run.total || pipelineState?.state?.job_count || 0;
    const completed = run.completed || 0;
    const failed = run.failed || 0;
    if (effectiveStatus === 'running') {
      return `${label} running: ${completed}/${total || '?'} processed${failed ? `, ${failed} failed` : ''}.`;
    }
    if (effectiveStatus === 'interrupted') {
      return `${label} was interrupted after ${completed}/${total || '?'} processed. Saved results are shown.`;
    }
    if (effectiveStatus === 'failed') {
      return `${label} finished with failures: ${completed}/${total || '?'} processed, ${failed} failed.`;
    }
    if (effectiveStatus === 'done' && total > 0) {
      return `${label} restored: ${completed}/${total} processed.`;
    }
    return null;
  };

  const applyWinningCriteriaThresholds = React.useCallback((criteriaRows) => {
    const rows = Array.isArray(criteriaRows) ? criteriaRows : [criteriaRows].filter(Boolean);
    const thresholds = rows
      .map(row => [String(row?.platform || '').toLowerCase(), Number(row?.criteria?.pass_threshold)])
      .filter(([platform, threshold]) => platform && Number.isFinite(threshold));
    const sharedThreshold = thresholds.find(([platform]) => platform === 'foreplay')?.[1] ?? thresholds[0]?.[1];
    if (!Number.isFinite(sharedThreshold)) return;
    const thresholdsByPlatform = Object.fromEntries(
      ['foreplay', 'adspy', 'brandsearch'].map(platform => [platform, sharedThreshold]),
    );
    const applyThreshold = (ad) => {
      const threshold = thresholdsByPlatform[sourcePlatformForAd(ad)] ?? sharedThreshold;
      if (!Number.isFinite(threshold)) return ad;
      const score = Number(ad?.score);
      const raw = ad?.raw && typeof ad.raw === 'object'
        ? { ...ad.raw, pass_threshold: threshold, source_platform: sourcePlatformForAd(ad) }
        : ad?.raw;
      return {
        ...ad,
        sourcePlatform: sourcePlatformForAd(ad),
        source_platform: sourcePlatformForAd(ad),
        passThreshold: threshold,
        pass_threshold: threshold,
        passes: Number.isFinite(score) ? score >= threshold : ad.passes,
        raw,
      };
    };
    setResearchAds(prev => prev.map(applyThreshold));
    setFilteredAds(prev => prev.map(applyThreshold));
    setRemodeledAds(prev => prev.map(applyThreshold));
    setAutoRemodelAds(prev => prev.map(applyThreshold));
    setConvertedAds(prev => prev.map(applyThreshold));
    setExpandedAd(prev => (prev ? applyThreshold(prev) : prev));
  }, []);

  const manualSearchAdsForTab = scopedResearchAdsForTab('research');
  const autoSearchAdsForTab = scopedResearchAdsForTab('autoSearch');
  const researchSelectionAdsForTab = scopedResearchAdsForTab(tab);
  const researchSelectionIdsForTab = new Set(researchSelectionAdsForTab.map(a => a.id));
  const researchSelectionCount = [...selResearch].filter(id => researchSelectionIdsForTab.has(id)).length;
  const autoSearchServerTotal = Number(autoresearchPage?.total ?? latestAutoresearchRun?.total);
  // Static Ads brand dropdown reuses the loaded competitor catalog (no new
  // fetch); keyword autocomplete flattens symptom/mechanism keyword seeds.
  const staticAdsBrandOptions = (directions || [])
    .filter(d => d && d.type === 'competitor')
    .map(d => ({ id: d.id, label: d.display_name || d.brand_name || d.name || d.id }));
  const staticAdsKeywordSuggestions = [...new Set(
    (directions || []).flatMap(d => (Array.isArray(d?.keywords) ? d.keywords : [])),
  )].filter(Boolean).slice(0, 60);
  // In-flight badge count, deduped by run_id across the live snapshot + history.
  const staticAdsInFlight = (() => {
    const byId = new Map();
    (saRuns || []).forEach(r => { if (r?.run_id) byId.set(r.run_id, r.status); });
    if (saActiveRun?.run_id) byId.set(saActiveRun.run_id, saActiveRun.status);
    let n = 0;
    byId.forEach(s => { if (s === 'queued' || s === 'running') n += 1; });
    return n;
  })();
  const counts = {
    staticAds: staticAdsInFlight,
    research: manualSearchAdsForTab.length,
    autoSearch: Number.isFinite(autoSearchServerTotal)
      ? Math.max(autoSearchAdsForTab.length, autoSearchServerTotal)
      : autoSearchAdsForTab.length,
    autoRemodel: autoRemodelAds.length || arPickedIds.length,
    filter: filteredAds.length,
    remodel: remodeledAds.length,
    compliance: complianceJobs.length,
    video: convertedAds.length,
  };
  const remodeledAdIds = new Set(remodeledAds.map(ad => ad.id));
  const autoRemodelAdIds = new Set(autoRemodelAds.map(ad => ad.id));
  const remodelReadyVariantIds = readyRemodelVariantIds(remodeledAds);
  const autoRemodelReadyVariantIds = readyRemodelVariantIds(autoRemodelAds);
  const remodelSelectedAdCount = [...selRemodel].filter(id => remodeledAdIds.has(id)).length;
  const autoRemodelSelectedAdCount = [...selAutoRemodel].filter(id => autoRemodelAdIds.has(id)).length;
  const remodelSelectedVariantCount = [...selRemodelVariants].filter(id => remodelReadyVariantIds.has(id)).length;
  const autoRemodelSelectedVariantCount = [...selAutoRemodelVariants].filter(id => autoRemodelReadyVariantIds.has(id)).length;
  const remodelVariantOnlySelection = remodelSelectedAdCount === 0 && remodelSelectedVariantCount > 0;
  const autoRemodelVariantOnlySelection = autoRemodelSelectedAdCount === 0 && autoRemodelSelectedVariantCount > 0;
  const remodelSelectionCount = remodelVariantOnlySelection ? remodelSelectedVariantCount : remodelSelectedAdCount;
  const autoRemodelSelectionCount = autoRemodelVariantOnlySelection ? autoRemodelSelectedVariantCount : autoRemodelSelectedAdCount;
  const remodelSelectionTotal = remodelVariantOnlySelection ? remodelReadyVariantIds.size : remodeledAds.length;
  const autoRemodelSelectionTotal = autoRemodelVariantOnlySelection ? autoRemodelReadyVariantIds.size : autoRemodelAds.length;
  const isAdmin = !!authState.user?.is_admin || authState.user?.auth_method === 'disabled';

  useEffect(() => {
    const prune = (setter, allowed) => {
      setter(prev => {
        const next = new Set([...prev].filter(id => allowed.has(id)));
        return next.size === prev.size ? prev : next;
      });
    };
    prune(setSelRemodel, remodeledAdIds);
    prune(setSelAutoRemodel, autoRemodelAdIds);
    prune(setSelRemodelVariants, remodelReadyVariantIds);
    prune(setSelAutoRemodelVariants, autoRemodelReadyVariantIds);
    // Selection is local UI state; prune it whenever visible cards/scripts change.
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [remodeledAds, autoRemodelAds, remodelScripts]);

  // group by platform
  const groupByPlatform = (ads) => {
    const g = { foreplay: [], adspy: [], brandsearch: [], metaads: [] };
    ads.forEach(a => g[a.platform]?.push(a));
    return g;
  };

  if (authState.status !== 'authenticated') {
    return (
      <div className="auth-shell">
        <div className="auth-panel">
          <img src="../../assets/mark.svg" alt="" className="auth-mark" />
          <div className="auth-brand">Resilia</div>
          <button className="btn-primary auth-button" onClick={signIn}>Sign in</button>
        </div>
      </div>
    );
  }

  return (
    <div className="app">
      <TopBar
        credits={platformCredits}
        user={authState.user}
        pendingAccessCount={pendingAccessCount}
        onOpenAccessRequests={() => setTab('accessRequests')}
        onLogout={signOut}
      />
      <TabBar
        active={tab}
        counts={counts}
        onChange={setTab}
      />
      <div className="workspace-shell">
        {isAdmin && (
          <ManagementSidebar
            active={tab}
            collapsed={managementCollapsed}
            pendingAccessCount={pendingAccessCount}
            onToggle={() => setManagementCollapsed(v => !v)}
            onChange={setTab}
          />
        )}
        <main className="content">
          {tab === 'staticAds' && (
            <StaticAdsPanel
              running={saRunning}
              status={saStatus}
              state={saState}
              activeRun={saActiveRun}
              runs={saRuns}
              brandOptions={staticAdsBrandOptions}
              keywordSuggestions={staticAdsKeywordSuggestions}
              starting={saStarting}
              error={saError}
              onRun={handleRunStaticAds}
              onStop={handleStopStaticAds}
              onDeleteRun={handleDeleteStaticAdsRun}
              onSaveVariant={handleSaveStaticAdsVariant}
              onRegenerateVariant={handleRegenerateStaticAdsVariant}
              onDeleteVariant={handleDeleteStaticAdsVariant}
              onRefresh={refreshStaticAds}
            />
          )}
          {tab === 'adminDirections' && isAdmin && (
            <AdminDirectionManager onCatalogChanged={refreshDirectionCatalog} />
          )}
          {tab === 'adminWinningCriteria' && isAdmin && (
            <AdminWinningCriteriaManager onCriteriaSaved={applyWinningCriteriaThresholds} />
          )}
          {tab === 'adminGlobalCrawler' && isAdmin && (
            <AdminGlobalCrawlerStatus />
          )}
          {tab === 'adminCosts' && isAdmin && (
            <AdminCostManager />
          )}
          {tab === 'adminEvaluationQuotas' && isAdmin && (
            <AdminEvaluationGuardrailsView />
          )}
          {tab === 'adminWorkspaces' && isAdmin && (
            <AdminWorkspaceMirror />
          )}
          {tab === 'accessRequests' && isAdmin && (
            <AccessRequestsAdminView onPendingCountChange={setPendingAccessCount} />
          )}
          {tab === 'adminUsers' && isAdmin && (
            <AdminUsersView />
          )}
          {(tab === 'research' || tab === 'autoSearch') && (
            <ResearchView
              mode={tab === 'autoSearch' ? 'autoSearch' : 'manual'}
              rows={rows} setRows={setRows}
              competitors={competitors}
              ads={researchAds} searching={searching}
              searchCacheLoading={searchCacheLoading}
              searchingRowIds={searchingRowIds}
              searchWarnings={searchWarnings}
              latestSearchRun={latestSearchRun}
              latestAutoresearchRun={latestAutoresearchRun}
              searchPassesOnly={searchPassesOnly}
              setSearchPassesOnly={handleSearchPassesOnlyChange}
              dedupeSettings={dedupeSettings}
              dedupeSaving={dedupeSaving}
              onDedupeChange={updateDedupeSettings}
              sel={selResearch} onToggle={toggleSel(setSelResearch)}
              onSearch={runSearch}
              onSearchRow={runSearchRow}
              onStopSearch={stopSearch}
              onExpandAd={expandAd}
              onDislikeAd={dislikeAd}
              density={tweaks.density}
              minimal={tweaks.cardStyle === 'minimal'}
              groupByPlatform={groupByPlatform}
              arRunning={arRunning}
              arProgress={arProgress}
              arSources={arSources}
              arFilters={arFilters}
              setArFilters={setArFilters}
              onToggleArSource={toggleArSource}
              onSelectAllArSources={selectAllArSources}
              arTarget={arTarget}
              setArTarget={setBoundedArTarget}
              arMaxAdsPerDirection={arMaxAdsPerDirection}
              setArMaxAdsPerDirection={setBoundedArMaxAdsPerDirection}
              arMaxAdsPerPlatform={arMaxAdsPerPlatform}
              setArMaxAdsPerPlatform={setBoundedArMaxAdsPerPlatform}
              arNumDirections={arNumDirections}
              setArNumDirections={setArNumDirections}
              directions={directions}
              arPickedDirections={arPickedDirections}
              onToggleArPickedDirection={toggleArPickedDirection}
              onClearArPickedDirections={clearArPickedDirections}
              onSelectAllArPickedDirections={selectAllArPickedDirections}
              autoresearchPage={autoresearchPage}
              onLoadMoreAutoresearch={loadMoreAutoresearchWinners}
              onRunAutoresearch={runAutoresearch}
              onStopAutoresearch={stopAutoresearch}
              startupLoading={!phaseAComplete}
            />
          )}
          {tab === 'filter' && (
            <PipelineView
              title="Filter" subtitle="Ads running through the filtering layer. Select and pass to Remodel."
              ads={filteredAds} sel={selFilter} onToggle={toggleSel(setSelFilter)}
              onExpandAd={expandAd}
              onDislikeAd={dislikeAd}
              density={tweaks.density} minimal={tweaks.cardStyle === 'minimal'}
              groupByPlatform={groupByPlatform}
              emptyTitle="Nothing filtered yet"
              emptyBody="Select ads in Manual Search or AutoSearch and press Pass to Filter."
              pagination={filterPage}
              onLoadMore={loadMoreFilterAds}
              statusText={pipelineStatusText(filterRunState, 'Filter')}
              preserveOrder={true}
              showPassVerdict={false}
              loadingTitle="Loading Filter ads"
              loadingBody="Fetching filtered ads from the database."
              startupLoading={!phaseAComplete}
              busy={filterBusy}
            />
          )}
          {showAutoRemodelUI() && tab === 'autoRemodel' && (
            <AutoRemodelWorkspace
              ads={autoRemodelAds}
              scripts={remodelScripts}
              directionOptions={directions}
              running={arRunningOrch}
              starting={arStartPending}
              stage={arStage}
              lastEvent={arLastEvent}
              adsFound={arAdsFound}
              targetAds={arTargetAds}
              variantsDone={arVariantsDone}
              variantsTotal={arVariantsTotalOverride ?? arPickedIds.length * arNVariants}
              resultsTotal={autoRemodelHydration.total || arPickedIds.length}
              resultsLoaded={autoRemodelHydration.loaded}
              resultsLoading={autoRemodelHydration.loading}
              resultsRestoring={autoRemodelRestoring}
              dedupeSettings={dedupeSettings}
              sel={selAutoRemodel}
              onToggle={toggleAutoRemodelAdSelection}
              sources={arOrchSources}
              onToggleSource={toggleArOrchSource}
              onSelectAllSources={selectAllArOrchSources}
              onRun={startAutoRemodel}
              onStop={stopAutoRemodel}
              onRetryFailed={retryFailedAutoRemodel}
              pageInstruction={autoRemodelPageInstruction}
              onPageInstructionChange={setAutoRemodelPageInstruction}
              adInstructions={autoRemodelAdInstructions}
              onAdInstructionChange={setAutoAdInstruction}
              strategyForAd={autoRemodelStrategyForAd}
              selectedVariantIds={selAutoRemodelVariants}
              onToggleVariant={toggleAutoRemodelVariantSelection}
              onLoadMore={loadMoreAutoRemodelAds}
              onLoadHistory={handleLoadAutoRemodelHistory}
              historyLoaded={autoRemodelHistoryLoaded}
              historyLoading={autoRemodelHistoryLoading}
              onEditScript={editRemodelScript}
              onRegenerate={regenerateRemodelJob}
              onDiscard={discardRemodelJob}
              onPromote={promoteRemodelJob}
              onPromoteAll={promoteAllAutoRemodelJobs}
              onExpandAd={expandAd}
              startupLoading={!phaseAComplete}
            />
          )}
          {tab === 'remodel' && (
            <RemodelView
              ads={remodeledAds} sel={selRemodel} onToggle={toggleRemodelAdSelection}
              scripts={remodelScripts}
              onEditScript={editRemodelScript}
              onRegenerate={regenerateRemodelJob}
              onDiscard={discardRemodelJob}
              onPromote={promoteRemodelJob}
              pageInstruction={remodelPageInstruction}
              onPageInstructionChange={setRemodelPageInstruction}
              adInstructions={remodelAdInstructions}
              onAdInstructionChange={setManualAdInstruction}
              strategyForAd={manualRemodelStrategyForAd}
              selectedVariantIds={selRemodelVariants}
              onToggleVariant={toggleRemodelVariantSelection}
              pagination={remodelPage}
              onRefresh={() => Promise.all([
                loadRemodelPage({ reset: true }),
                refreshRemodelRunState(),
              ])}
              onLoadMore={loadMoreRemodelAds}
              statusText={pipelineStatusText(remodelRunState, 'Remodel')}
              busy={remodelBusy}
            />
          )}
          {tab === 'compliance' && (
            <ComplianceView
              jobs={complianceJobs}
              sel={selCompliance}
              onToggle={toggleSel(setSelCompliance)}
              strictness={complianceStrictness}
              onStrictnessChange={setComplianceStrictness}
              busy={complianceBusy}
              runState={complianceRunState}
              pagination={compliancePage}
              onRefresh={() => loadCompliancePage({ reset: true })}
              onLoadMore={loadMoreComplianceJobs}
              onEditTranscript={editComplianceTranscript}
              onDiscard={discardComplianceJob}
            />
          )}
          {tab === 'video' && (
            <VideoView ads={convertedAds} />
          )}
        </main>
      </div>

      {(tab === 'research' || tab === 'autoSearch') && (
        <SelectionBar
          count={researchSelectionCount} total={researchSelectionAdsForTab.length}
          ctaLabel={filterBusy ? 'Filtering…' : 'Pass to Filter'} icon="filter"
          onCTA={passToFilter}
          secondaryLabel="Pass without Filtering"
          secondaryIcon="arrowRight"
          onSecondary={passWithoutFilter}
          dangerLabel="Delete selected"
          dangerIcon="x"
          onDanger={bulkDeleteResearch}
          onClear={() => setSelResearch(new Set())}
          onSelectAll={() => setSelResearch(new Set(researchSelectionAdsForTab.map(a => a.id)))}
        />
      )}
      {tab === 'filter' && (
        <SelectionBar
          count={selFilter.size} total={filteredAds.length}
          ctaLabel="Run Remodel" icon="scroll"
          onCTA={runRemodel}
          dangerLabel="Delete selected"
          dangerIcon="trash"
          onDanger={bulkDeleteFilter}
          onClear={() => setSelFilter(new Set())}
          onSelectAll={() => setSelFilter(new Set(filteredAds.map(a => a.id)))}
        />
      )}
      {tab === 'remodel' && (
        <SelectionBar
          count={remodelSelectionCount} total={remodelSelectionTotal}
          itemLabel={remodelVariantOnlySelection ? 'variants' : 'ads'}
          ctaLabel={remodelVariantOnlySelection ? (complianceBusy ? 'Sending…' : 'Send to Compliance') : 'Convert to Video'}
          icon={remodelVariantOnlySelection ? 'shield' : 'film'}
          onCTA={remodelVariantOnlySelection ? () => sendSelectedToCompliance('manual') : convert}
          secondaryLabel={remodelVariantOnlySelection ? null : (complianceBusy ? 'Sending…' : 'Send to Compliance')}
          secondaryIcon="shield"
          onSecondary={remodelVariantOnlySelection ? null : () => sendSelectedToCompliance('manual')}
          dangerLabel={remodelVariantOnlySelection ? null : 'Delete selected'}
          dangerIcon="trash"
          onDanger={remodelVariantOnlySelection ? null : bulkDeleteRemodel}
          onClear={() => { setSelRemodel(new Set()); setSelRemodelVariants(new Set()); }}
          onSelectAll={() => {
            if (remodelVariantOnlySelection) {
              setSelRemodel(new Set());
              setSelRemodelVariants(new Set(remodelReadyVariantIds));
            } else {
              setSelRemodel(new Set(remodeledAds.map(a => a.id)));
              setSelRemodelVariants(new Set());
            }
          }}
        />
      )}
      {showAutoRemodelUI() && tab === 'autoRemodel' && (
        <SelectionBar
          count={autoRemodelSelectionCount} total={autoRemodelSelectionTotal}
          itemLabel={autoRemodelVariantOnlySelection ? 'variants' : 'ads'}
          ctaLabel={autoRemodelVariantOnlySelection ? (complianceBusy ? 'Sending…' : 'Send to Compliance') : 'Promote ready scripts'}
          icon={autoRemodelVariantOnlySelection ? 'shield' : 'film'}
          onCTA={autoRemodelVariantOnlySelection ? () => sendSelectedToCompliance('auto_remodel') : promoteAllAutoRemodelJobs}
          secondaryLabel={autoRemodelVariantOnlySelection ? null : (complianceBusy ? 'Sending…' : 'Send to Compliance')}
          secondaryIcon="shield"
          onSecondary={autoRemodelVariantOnlySelection ? null : () => sendSelectedToCompliance('auto_remodel')}
          dangerLabel={autoRemodelVariantOnlySelection ? null : 'Delete selected'}
          dangerIcon="trash"
          onDanger={autoRemodelVariantOnlySelection ? null : bulkDeleteAutoRemodel}
          onClear={() => { setSelAutoRemodel(new Set()); setSelAutoRemodelVariants(new Set()); }}
          onSelectAll={() => {
            if (autoRemodelVariantOnlySelection) {
              setSelAutoRemodel(new Set());
              setSelAutoRemodelVariants(new Set(autoRemodelReadyVariantIds));
            } else {
              setSelAutoRemodel(new Set(autoRemodelAds.map(a => a.id)));
              setSelAutoRemodelVariants(new Set());
            }
          }}
        />
      )}
      {tab === 'compliance' && (
        <SelectionBar
          count={selCompliance.size} total={complianceJobs.length}
          ctaLabel="Convert to Video" icon="film"
          onCTA={convertComplianceToVideo}
          onClear={() => setSelCompliance(new Set())}
          onSelectAll={() => setSelCompliance(new Set(complianceJobs.map(job => job.id)))}
        />
      )}

      <AdDrawer ad={expandedAd} onClose={() => setExpandedAd(null)} />

      {toast && (
        <div className="toast">
          <span className="toast-badge">{toast.badge}</span>
          <span>{toast.msg}</span>
        </div>
      )}

      {!tweaksOpen && (
        <button className="tweaks-fab" onClick={() => setTweaksOpen(true)} title="Tweaks">
          <Icon name="sliders" size={18} />
        </button>
      )}
      {tweaksOpen && (
        <Tweaks
          state={tweaks}
          setState={(next) => {
            setTweaks(next);
            window.parent.postMessage({ type: '__edit_mode_set_keys', edits: next }, '*');
          }}
          onClose={() => setTweaksOpen(false)}
        />
      )}
    </div>
  );
}

function SectionLoadingPanel({ title = 'Loading data', body = 'Fetching from internet and database.' }) {
  return (
    <div className="section-loading" role="status" aria-live="polite">
      <div className="section-loading-spinner" aria-hidden="true" />
      <div className="section-loading-copy">
        <span className="section-loading-title">{title}</span>
        <span className="section-loading-body">{body}</span>
      </div>
    </div>
  );
}

function ManagementSidebar({ active, collapsed, pendingAccessCount, onToggle, onChange }) {
  const items = [
    { id: 'adminDirections', label: 'Direction Manager', icon: 'target', meta: 'Directions' },
    { id: 'adminWinningCriteria', label: 'Winning Ads Criteria', icon: 'trend', meta: 'Scoring rules' },
    { id: 'adminGlobalCrawler', label: 'Global Crawler', icon: 'loader', meta: 'Shared winners' },
    { id: 'adminCosts', label: 'Cost Dashboard', icon: 'bolt', meta: 'Spend' },
    { id: 'adminEvaluationQuotas', label: 'Evaluation Guardrails', icon: 'shield', meta: 'Daily quotas' },
    { id: 'adminWorkspaces', label: 'User Work Viewer', icon: 'users', meta: 'User snapshots' },
    { id: 'adminUsers', label: 'User Manager', icon: 'users', meta: 'Grant or revoke admin' },
    { id: 'accessRequests', label: 'Access Manager', icon: 'shield', meta: `${pendingAccessCount || 0} pending`, count: pendingAccessCount || 0 },
  ];
  return (
    <aside className={`management-sidebar ${collapsed ? 'collapsed' : ''}`} aria-label="Management">
      <div className="management-sidebar-head">
        {!collapsed && (
          <div>
            <div className="management-title">Management</div>
            <div className="management-subtitle">Admin tools</div>
          </div>
        )}
        <button className="management-toggle" onClick={onToggle} title={collapsed ? 'Expand management' : 'Collapse management'}>
          <Icon name={collapsed ? 'arrowRight' : 'chevronDown'} size={15} />
        </button>
      </div>
      <nav className="management-nav">
        {items.map(item => (
          <button
            key={item.id}
            className={`management-nav-item ${active === item.id ? 'active' : ''}`}
            onClick={() => onChange(item.id)}
            aria-label={item.count > 0 ? `${item.label}, ${item.count} pending` : item.label}
            title={collapsed ? item.label : undefined}
          >
            <Icon name={item.icon} size={16} />
            {!collapsed && (
              <span className="management-nav-copy">
                <span>{item.label}</span>
                <small>{item.meta}</small>
              </span>
            )}
            {item.count > 0 && <span className="management-count">{item.count}</span>}
          </button>
        ))}
      </nav>
    </aside>
  );
}

const blankDirectionForm = () => ({
  display_name: '', search_method: 'keyword_discovery', keywords: '',
  platforms: { foreplay: true, adspy: true, brandsearch: true },
  target_n: '5', status: 'active',
  platform_filters: emptyDirectionPlatformFilters(),
});

function AdminDirectionManager({ onCatalogChanged }) {
  const [directions, setDirections] = useState([]);
  const [selectedId, setSelectedId] = useState(null);
  const [form, setForm] = useState(blankDirectionForm());
  const [loading, setLoading] = useState(false);
  const [busy, setBusy] = useState(false);
  const [error, setError] = useState('');
  const [importStatus, setImportStatus] = useState('');
  const importFileRef = useRef(null);

  const activeDirections = directions.filter(d => d.status !== 'archived');
  const archivedCount = directions.length - activeDirections.length;
  const selected = directions.find(d => d.id === selectedId) || null;

  const splitKeywords = (value) => String(value || '')
    .split(/[\n,]+/)
    .map(item => item.trim())
    .filter(Boolean);
  const keywordText = (direction) => {
    const keywords = Array.isArray(direction?.keywords) ? direction.keywords : [];
    return keywords.filter(Boolean).join('\n');
  };
  const platformsForDirection = (direction) => {
    const saved = Array.isArray(direction?.platforms)
      ? direction.platforms.map(item => String(item).toLowerCase()).filter(item => LIVE_SOURCES.includes(item))
      : [];
    if (saved.length) return saved;
    const platformKeywords = direction?.platform_keywords || {};
    const keyed = LIVE_SOURCES.filter(platform => Array.isArray(platformKeywords[platform]) && platformKeywords[platform].length);
    if (keyed.length) return keyed;
    return LIVE_SOURCES;
  };
  const platformStateForDirection = (direction) => {
    const selected = new Set(platformsForDirection(direction));
    return Object.fromEntries(LIVE_SOURCES.map(platform => [platform, selected.has(platform)]));
  };
  const selectedFormPlatforms = () => LIVE_SOURCES.filter(platform => form.platforms?.[platform]);
  const platformFiltersForDirection = (direction) => Object.fromEntries(
    LIVE_SOURCES.map(platform => {
      const config = direction?.platform_configs?.[platform] || {};
      const filters = config && typeof config === 'object' && !Array.isArray(config) && config.api_filters
        ? config.api_filters
        : config;
      return [platform, normalizeDirectionApiFilters(platform, filters || {})];
    }),
  );
  const methodLabel = (method) => method === 'brand_lookup' ? 'Brand lookup' : 'Keyword discovery';

  const load = async () => {
    setLoading(true);
    setError('');
    try {
      const directionData = await RESILIA_API.adminListDirections();
      const nextDirections = directionData.directions || [];
      setDirections(nextDirections);
      if (!selectedId && nextDirections.length) setSelectedId(nextDirections[0].id);
    } catch (e) {
      setError(e.message || 'Admin data failed to load');
    } finally {
      setLoading(false);
    }
  };

  useEffect(() => { load(); }, []);
  useEffect(() => {
    if (!selected) return;
    setForm({
      display_name: selected.display_name || selected.brand_name || selected.id || '',
      search_method: selected.search_method || (selected.type === 'competitor' ? 'brand_lookup' : 'keyword_discovery'),
      keywords: keywordText(selected),
      platforms: platformStateForDirection(selected),
      target_n: String(selected.target_n || 5),
      status: selected.status || 'active',
      platform_filters: platformFiltersForDirection(selected),
    });
  }, [selected]);

  const updateForm = (patch) => setForm(current => ({ ...current, ...patch }));
  const updatePlatform = (platform, value) => setForm(current => ({
    ...current,
    platforms: { ...current.platforms, [platform]: value },
  }));
  const updatePlatformFilterValue = (platform, key, value) => setForm(current => {
    const filters = { ...(current.platform_filters?.[platform] || {}) };
    if (value === null || value === undefined || value === '' || (Array.isArray(value) && value.length === 0)) delete filters[key];
    else filters[key] = value;
    return {
      ...current,
      platform_filters: { ...current.platform_filters, [platform]: filters },
    };
  });
  const updatePlatformFilterRange = (platform, key, index, value, numeric) => setForm(current => {
    const filters = { ...(current.platform_filters?.[platform] || {}) };
    const existing = asFilterList(filters[key]);
    const next = [existing[0] || '', existing[1] || ''];
    if (value === '') next[index] = '';
    else if (numeric) {
      const numberValue = Number(value);
      next[index] = Number.isFinite(numberValue) ? numberValue : value;
    } else {
      next[index] = value;
    }
    const compact = next.filter(item => item !== null && item !== undefined && item !== '');
    if (compact.length) filters[key] = compact;
    else delete filters[key];
    return {
      ...current,
      platform_filters: { ...current.platform_filters, [platform]: filters },
    };
  });

  const newDirection = () => {
    setSelectedId(null);
    setForm(blankDirectionForm());
    setError('');
    setImportStatus('');
  };

  const exportDirections = async () => {
    setBusy(true);
    setError('');
    setImportStatus('');
    try {
      const payload = await RESILIA_API.adminExportDirections();
      const blob = new Blob([`${JSON.stringify(payload, null, 2)}\n`], { type: 'application/json' });
      const url = URL.createObjectURL(blob);
      const a = document.createElement('a');
      const stamp = new Date().toISOString().slice(0, 10);
      a.href = url;
      a.download = `search_directions_${stamp}.json`;
      document.body.appendChild(a);
      a.click();
      a.remove();
      URL.revokeObjectURL(url);
      setImportStatus(`Exported ${Array.isArray(payload) ? payload.length : 0} directions.`);
    } catch (e) {
      setError(e.message || 'Export failed');
    } finally {
      setBusy(false);
    }
  };

  const importDirections = async (event) => {
    const file = event.target.files?.[0];
    if (!file) return;
    setBusy(true);
    setError('');
    setImportStatus('');
    try {
      const text = await file.text();
      let payload;
      try {
        payload = JSON.parse(text);
      } catch (parseError) {
        throw new Error(`Invalid JSON: ${parseError.message}`);
      }
      if (!Array.isArray(payload)) throw new Error('Import file must be a JSON array of directions');
      const result = await RESILIA_API.adminImportDirections(payload);
      await load();
      await onCatalogChanged?.();
      if (Array.isArray(result.ids) && result.ids.length) setSelectedId(result.ids[0]);
      setImportStatus(`Imported ${result.imported || 0} directions (${result.created || 0} created, ${result.updated || 0} updated).`);
    } catch (e) {
      setError(e.message || 'Import failed');
    } finally {
      setBusy(false);
      event.target.value = '';
    }
  };

  const syncDirectionKeywords = async (directionId, platforms, keywords) => {
    if (!selectedId || !directionId) return;
    const desired = new Set();
    platforms.forEach(platform => keywords.forEach(keyword => desired.add(`${platform}\u0000${keyword.toLowerCase()}`)));
    const existingItems = Array.isArray(selected?.keyword_items) ? selected.keyword_items : [];
    const existing = new Set(
      existingItems.map(item => `${String(item.platform || 'all').toLowerCase()}\u0000${String(item.keyword || '').trim().toLowerCase()}`),
    );
    for (const item of existingItems) {
      const key = `${String(item.platform || 'all').toLowerCase()}\u0000${String(item.keyword || '').trim().toLowerCase()}`;
      if (!desired.has(key)) await RESILIA_API.adminArchiveDirectionKeyword(directionId, item.id);
    }
    for (const platform of platforms) {
      for (const keyword of keywords) {
        const key = `${platform}\u0000${keyword.toLowerCase()}`;
        if (!existing.has(key)) await RESILIA_API.adminAddDirectionKeyword(directionId, { platform, keyword });
      }
    }
  };

  const renderPlatformFilterField = (platform, field, disabled) => {
    const filters = form.platform_filters?.[platform] || {};
    const value = filters[field.key];
    const title = `${PLATFORMS[platform]?.label || platform} ${field.key}`;
    if (field.type === 'select') {
      const currentValue = Array.isArray(value) ? (value[0] || '') : (value || '');
      return (
        <label key={field.key} className="admin-filter-field" title={title}>
          <span>{field.label}</span>
          <select
            value={currentValue}
            disabled={disabled}
            onChange={e => updatePlatformFilterValue(platform, field.key, field.array ? (e.target.value ? [e.target.value] : []) : e.target.value)}
          >
            {field.options.map(([optionValue, label]) => <option key={optionValue} value={optionValue}>{label}</option>)}
          </select>
        </label>
      );
    }
    if (field.type === 'boolean') {
      const currentValue = value === true ? 'true' : value === false ? 'false' : '';
      return (
        <label key={field.key} className="admin-filter-field" title={title}>
          <span>{field.label}</span>
          <select
            value={currentValue}
            disabled={disabled}
            onChange={e => updatePlatformFilterValue(platform, field.key, e.target.value === '' ? '' : e.target.value === 'true')}
          >
            <option value="">{field.emptyLabel || 'Any'}</option>
            <option value="true">{field.trueLabel || 'True'}</option>
            <option value="false">{field.falseLabel || 'False'}</option>
          </select>
        </label>
      );
    }
    if (field.type === 'list') {
      return (
        <label key={field.key} className="admin-filter-field wide" title={title}>
          <span>{field.label}</span>
          <input
            value={listText(value)}
            disabled={disabled}
            placeholder={field.placeholder || ''}
            onChange={e => updatePlatformFilterValue(platform, field.key, asFilterList(e.target.value))}
          />
        </label>
      );
    }
    if (field.type === 'range' || field.type === 'dateRange') {
      const range = asFilterList(value);
      const numeric = field.type === 'range';
      return (
        <label key={field.key} className="admin-filter-field range" title={title}>
          <span>{field.label}</span>
          <div className="admin-filter-range">
            <input
              type={field.type === 'dateRange' ? 'text' : 'number'}
              min={field.min}
              max={field.max}
              value={range[0] || ''}
              disabled={disabled}
              placeholder={field.startPlaceholder || (field.type === 'dateRange' ? 'YYYY-MM-DD' : 'From')}
              onChange={e => updatePlatformFilterRange(platform, field.key, 0, e.target.value, numeric)}
            />
            <input
              type={field.type === 'dateRange' ? 'text' : 'number'}
              min={field.min}
              max={field.max}
              value={range[1] || ''}
              disabled={disabled}
              placeholder={field.endPlaceholder || (field.type === 'dateRange' ? 'YYYY-MM-DD' : 'To')}
              onChange={e => updatePlatformFilterRange(platform, field.key, 1, e.target.value, numeric)}
            />
          </div>
        </label>
      );
    }
    return (
      <label key={field.key} className="admin-filter-field" title={title}>
        <span>{field.label}</span>
        <input
          type={field.type === 'number' ? 'number' : 'text'}
          min={field.min}
          step={field.step}
          value={value ?? ''}
          disabled={disabled}
          placeholder={field.placeholder || ''}
          onChange={e => {
            if (field.type !== 'number') updatePlatformFilterValue(platform, field.key, e.target.value);
            else if (e.target.value === '') updatePlatformFilterValue(platform, field.key, '');
            else {
              const numberValue = Number(e.target.value);
              updatePlatformFilterValue(platform, field.key, Number.isFinite(numberValue) ? numberValue : e.target.value);
            }
          }}
        />
      </label>
    );
  };

  const saveDirection = async () => {
    setBusy(true);
    setError('');
    try {
      const name = form.display_name.trim();
      if (!name) throw new Error('Name is required');
      const selectedPlatforms = selectedFormPlatforms();
      if (!selectedPlatforms.length) throw new Error('Select at least one platform');
      const keywords = splitKeywords(form.keywords);
      const firstLookup = keywords[0] || '';
      const platformFilters = Object.fromEntries(
        LIVE_SOURCES.map(platform => [platform, { api_filters: cleanApiFilters(form.platform_filters?.[platform] || {}) }]),
      );
      const body = {
        id: selectedId || null,
        type: form.search_method === 'brand_lookup' ? 'competitor' : 'keyword',
        display_name: name,
        description: null,
        search_method: form.search_method,
        brand_name: form.search_method === 'brand_lookup' ? name : null,
        brand_domain: form.search_method === 'brand_lookup' && firstLookup.includes('.') ? firstLookup : null,
        target_n: Math.max(1, Math.min(1000, parseInt(form.target_n, 10) || 5)),
        status: form.status || 'active',
        api_filters: {},
        keywords: [],
        platforms: selectedPlatforms,
        platform_keywords: Object.fromEntries(selectedPlatforms.map(platform => [platform, keywords])),
        platform_configs: platformFilters,
      };
      const out = await RESILIA_API.adminSaveDirection(body);
      const savedId = out.direction?.id || selectedId || null;
      if (savedId) await syncDirectionKeywords(savedId, selectedPlatforms, keywords);
      await load();
      await onCatalogChanged?.();
      setSelectedId(savedId || out.direction?.id || body.id);
    } catch (e) {
      setError(e.message || 'Save failed');
    } finally {
      setBusy(false);
    }
  };

  const archiveDirection = async () => {
    if (!selectedId || !confirm(`Archive ${selectedId}?`)) return;
    setBusy(true);
    setError('');
    try {
      await RESILIA_API.adminArchiveDirection(selectedId);
      setSelectedId(null);
      setForm(blankDirectionForm());
      await load();
      await onCatalogChanged?.();
    } catch (e) {
      setError(e.message || 'Archive failed');
    } finally {
      setBusy(false);
    }
  };

  return (
    <section className="admin-grid-view">
      <div className="content-hdr">
        <div>
          <div className="content-title">Direction Manager</div>
          <div className="content-sub">{activeDirections.length} active · {archivedCount} archived</div>
        </div>
        <div className="spacer" />
        <input
          ref={importFileRef}
          type="file"
          accept="application/json,.json"
          className="admin-import-input"
          onChange={importDirections}
        />
        <button className="btn-secondary" onClick={exportDirections} disabled={busy}>Export JSON</button>
        <button className="btn-secondary" onClick={() => importFileRef.current?.click()} disabled={busy}>Import JSON</button>
        <button className="btn-secondary" onClick={newDirection} disabled={busy}><Icon name="plus" size={14} /> New</button>
        <button className="btn-primary" onClick={saveDirection} disabled={busy}><Icon name="check" size={14} /> Save</button>
      </div>
      {error && <div className="access-error">{error}</div>}
      {importStatus && <div className="admin-import-status">{importStatus}</div>}
      <div className="admin-grid directions">
        <aside className="admin-list">
          {loading && <div className="access-loading">Loading...</div>}
          {directions.map(direction => {
            const directionPlatforms = platformsForDirection(direction);
            return (
              <button
                key={direction.id}
                className={`admin-list-row ${selectedId === direction.id ? 'active' : ''} ${direction.status === 'archived' ? 'muted' : ''}`}
                onClick={() => setSelectedId(direction.id)}
              >
                <span>{direction.display_name || direction.brand_name || direction.id}</span>
                <small>{methodLabel(direction.search_method)} · {directionPlatforms.map(platform => PLATFORMS[platform]?.label || platform).join(', ')}</small>
                <small>{(direction.keywords || []).length || 0} keywords</small>
              </button>
            );
          })}
        </aside>
        <section className="admin-editor">
          <div className="admin-form-grid simple">
            <label>Name<input value={form.display_name} onChange={e => updateForm({ display_name: e.target.value })} /></label>
            <label>Method<select value={form.search_method} onChange={e => updateForm({ search_method: e.target.value })}><option value="keyword_discovery">Keyword discovery</option><option value="brand_lookup">Brand lookup</option></select></label>
          </div>
          <label className="admin-field-full direction-keywords">Keywords<textarea value={form.keywords} onChange={e => updateForm({ keywords: e.target.value })} spellCheck="true" /></label>
          <div className="admin-platform-select-row">
            {LIVE_SOURCES.map(platform => (
              <label key={platform} className={`admin-platform-toggle ${form.platforms?.[platform] ? 'active' : ''}`}>
                <input type="checkbox" checked={!!form.platforms?.[platform]} onChange={e => updatePlatform(platform, e.target.checked)} />
                <span>{PLATFORMS[platform]?.label || platform}</span>
              </label>
            ))}
          </div>

          <div className="admin-platform-grid">
            {LIVE_SOURCES.map(platform => {
              const disabled = !form.platforms?.[platform];
              return (
                <div key={platform} className={`admin-platform-box ${disabled ? 'disabled' : ''}`}>
                  <div className="admin-platform-box-head">
                    <span>{PLATFORMS[platform].label} Filters</span>
                    <small>api_filters</small>
                  </div>
                  <div className="admin-filter-fields">
                    {(DIRECTION_FILTER_FIELDS[platform] || []).map(field => renderPlatformFilterField(platform, field, disabled))}
                  </div>
                </div>
              );
            })}
          </div>
          <div className="admin-danger-row">
            <button className="btn-secondary danger" onClick={archiveDirection} disabled={busy || !selectedId}><Icon name="trash" size={14} /> Archive Direction</button>
          </div>
        </section>
      </div>
    </section>
  );
}

const CRITERIA_PLATFORM_ORDER = ['foreplay', 'adspy', 'brandsearch'];
const blankCriteriaForm = () => ({ pass_threshold: '', video_scoring_criteria: '', rubric: '', source_specific_signals: '', allow_shared_pool_prefill: true });

function CostStackedBar({ segments, total }) {
  const safeTotal = Math.max(Number(total || 0), 0);
  return (
    <div className="cost-stacked-bar" aria-label={`Total ${costMoney(safeTotal)}`}>
      {segments.map(segment => {
        const value = Number(segment.value || 0);
        const width = safeTotal > 0 ? Math.max((value / safeTotal) * 100, value > 0 ? 2 : 0) : 0;
        return (
          <span
            key={segment.key}
            className={`cost-stack-segment ${segment.key}`}
            style={{
              width: `${width}%`,
              ...(segment.color ? { background: segment.color } : {}),
            }}
            title={`${segment.label}: ${costMoney(value)}`}
          />
        );
      })}
    </div>
  );
}

const formatCostDuration = (seconds) => {
  const total = Math.max(0, Math.round(Number(seconds || 0)));
  if (total < 60) return `${total}s`;
  const minutes = Math.floor(total / 60);
  const remainder = total % 60;
  return remainder ? `${minutes}m ${remainder}s` : `${minutes}m`;
};

function DailyQuotaBar({ used, max, label }) {
  const safeMax = Math.max(0, Number(max || 0));
  const safeUsed = Math.max(0, Number(used || 0));
  const pct = safeMax > 0 ? Math.min(100, (safeUsed / safeMax) * 100) : 0;
  const tone = pct >= 100 ? 'danger' : pct >= 90 ? 'warn' : '';
  return (
    <div className={`daily-quota-bar ${tone}`}>
      <div className="daily-quota-bar-meta">
        <span>{label}</span>
        <span>{safeUsed.toLocaleString()} / {safeMax.toLocaleString()}</span>
      </div>
      <div className="daily-quota-bar-track" aria-hidden="true">
        <div className="daily-quota-bar-fill" style={{ width: `${pct}%` }} />
      </div>
    </div>
  );
}

function AdminEvaluationGuardrailsView() {
  const [summary, setSummary] = useState(null);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState('');
  const [filter, setFilter] = useState('');
  const [busyUserId, setBusyUserId] = useState(null);
  const [drafts, setDrafts] = useState({});
  const loadSeqRef = useRef(0);

  const load = async () => {
    const loadSeq = loadSeqRef.current + 1;
    loadSeqRef.current = loadSeq;
    setLoading(true);
    setError('');
    try {
      const data = await RESILIA_API.adminDailyQuotas();
      if (loadSeq !== loadSeqRef.current) return;
      setSummary(data);
      const nextDrafts = {};
      (data.users || []).forEach((row) => {
        const settings = row.settings || {};
        nextDrafts[row.user_id] = {
          max_live_evaluations_per_day: settings.max_live_evaluations_per_day ?? '',
          max_remodels_per_day: settings.max_remodels_per_day ?? '',
        };
      });
      setDrafts(nextDrafts);
    } catch (e) {
      if (loadSeq !== loadSeqRef.current) return;
      setError(e.message || 'Failed to load daily quotas');
    } finally {
      if (loadSeq === loadSeqRef.current) setLoading(false);
    }
  };

  useEffect(() => {
    load();
    const timer = setInterval(load, 30000);
    return () => clearInterval(timer);
  }, []);

  const defaults = summary?.defaults || {};
  const users = (summary?.users || []).filter((row) => {
    if (!filter) return true;
    const needle = filter.toLowerCase();
    return (row.email || '').toLowerCase().includes(needle)
      || (row.display_name || '').toLowerCase().includes(needle)
      || (row.user_id || '').toLowerCase().includes(needle);
  });

  const formatDate = (value) => {
    if (!value) return '—';
    const date = new Date(value);
    return Number.isNaN(date.getTime()) ? value : date.toLocaleString();
  };

  const saveRow = async (row) => {
    const userId = row.user_id;
    const draft = drafts[userId] || {};
    setBusyUserId(userId);
    setError('');
    try {
      const parseCap = (value) => {
        if (value === '' || value === null || value === undefined) return null;
        const parsed = Number(value);
        return Number.isFinite(parsed) ? parsed : null;
      };
      await RESILIA_API.adminSetDailyQuota(userId, {
        max_live_evaluations_per_day: parseCap(draft.max_live_evaluations_per_day),
        max_remodels_per_day: parseCap(draft.max_remodels_per_day),
      });
      await load();
    } catch (e) {
      setError(e.message || 'Failed to save quota settings');
    } finally {
      setBusyUserId(null);
    }
  };

  return (
    <section className="access-admin quota-guardrails">
      <div className="content-hdr">
        <div>
          <div className="content-title">Evaluation Guardrails</div>
          <div className="content-sub">
            Daily live-evaluation and remodel quotas per user. Shared pool prefill does not count toward evaluations.
          </div>
        </div>
        <div className="spacer" />
        <button className="btn-secondary" onClick={load} disabled={loading}>
          <Icon name="loader" size={14} /> Refresh
        </button>
      </div>

      {summary && (
        <div className="quota-guardrails-summary">
          <div className="quota-guardrails-defaults">
            <strong>Defaults</strong>
            <span>{Number(defaults.max_live_evaluations_per_day || 0).toLocaleString()} evaluations/day</span>
            <span>{Number(defaults.max_remodels_per_day || 0).toLocaleString()} remodels/day</span>
          </div>
          <div className="quota-guardrails-totals">
            <strong>Org today</strong>
            <span>{Number(summary.totals?.evaluations_used || 0).toLocaleString()} evaluations</span>
            <span>{Number(summary.totals?.remodels_used || 0).toLocaleString()} remodels</span>
          </div>
          <div className="quota-guardrails-reset">
            <span>Resets {formatDate(summary.resets_at)}</span>
          </div>
        </div>
      )}

      {error && <div className="access-error">{error}</div>}
      {loading && !summary && <div className="access-loading">Loading quotas...</div>}

      <div className="quota-guardrails-toolbar">
        <input
          className="input"
          placeholder="Filter by email, name, or user id"
          value={filter}
          onChange={(e) => setFilter(e.target.value)}
        />
      </div>

      {summary && (
        <div className="quota-guardrails-table-wrap">
          <table className="quota-guardrails-table">
            <thead>
              <tr>
                <th>User</th>
                <th>Evaluations</th>
                <th>Remodels</th>
                <th>Cap overrides</th>
                <th>Audit</th>
                <th />
              </tr>
            </thead>
            <tbody>
              {users.map((row) => {
                const draft = drafts[row.user_id] || {};
                const settings = row.settings || {};
                return (
                  <tr key={row.user_id}>
                    <td>
                      <div className="quota-user-cell">
                        <strong>{row.display_name || row.email || row.user_id}</strong>
                        <small>{row.email || row.user_id}</small>
                      </div>
                    </td>
                    <td>
                      <DailyQuotaBar used={row.evaluations?.used} max={row.evaluations?.max} label="Evaluations" />
                      <small>{Number(row.evaluations?.remaining || 0).toLocaleString()} remaining</small>
                    </td>
                    <td>
                      <DailyQuotaBar used={row.remodels?.used} max={row.remodels?.max} label="Remodels" />
                      <small>{Number(row.remodels?.remaining || 0).toLocaleString()} remaining</small>
                    </td>
                    <td>
                      <div className="quota-cap-inputs">
                        <label>
                          Eval cap
                          <input
                            type="number"
                            min="0"
                            placeholder={`Default ${defaults.max_live_evaluations_per_day}`}
                            value={draft.max_live_evaluations_per_day ?? ''}
                            onChange={(e) => setDrafts((prev) => ({
                              ...prev,
                              [row.user_id]: {
                                ...draft,
                                max_live_evaluations_per_day: e.target.value,
                              },
                            }))}
                          />
                        </label>
                        <label>
                          Remodel cap
                          <input
                            type="number"
                            min="0"
                            placeholder={`Default ${defaults.max_remodels_per_day}`}
                            value={draft.max_remodels_per_day ?? ''}
                            onChange={(e) => setDrafts((prev) => ({
                              ...prev,
                              [row.user_id]: {
                                ...draft,
                                max_remodels_per_day: e.target.value,
                              },
                            }))}
                          />
                        </label>
                      </div>
                    </td>
                    <td>
                      <small>{settings.updated_by_email || settings.updated_by || '—'}</small>
                      <small>{formatDate(settings.updated_at)}</small>
                    </td>
                    <td>
                      <button
                        className="btn-secondary compact"
                        disabled={busyUserId === row.user_id}
                        onClick={() => saveRow(row)}
                      >
                        Save
                      </button>
                    </td>
                  </tr>
                );
              })}
            </tbody>
          </table>
          {users.length === 0 && <div className="access-empty">No users match this filter.</div>}
        </div>
      )}
    </section>
  );
}

function AdminCostManager() {
  const [summary, setSummary] = useState(null);
  const [selectedUserIds, setSelectedUserIds] = useState([]);
  const [userOptions, setUserOptions] = useState([]);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState('');
  const loadSeqRef = useRef(0);

  const load = async (userIds = selectedUserIds) => {
    const requestedUserIds = [...(userIds || [])];
    const loadSeq = loadSeqRef.current + 1;
    loadSeqRef.current = loadSeq;
    setLoading(true);
    setError('');
    try {
      const data = await RESILIA_API.adminCostSummary({ userIds: requestedUserIds });
      if (loadSeq !== loadSeqRef.current) return;
      setSummary(data);
      const options = data.available_users?.length ? data.available_users : data.users || [];
      if (options.length) setUserOptions(options);
    } catch (e) {
      if (loadSeq !== loadSeqRef.current) return;
      setError(e.message || 'Cost summary failed to load');
    } finally {
      if (loadSeq === loadSeqRef.current) setLoading(false);
    }
  };

  useEffect(() => { load([]); }, []);

  const org = summary?.organization || {};
  const users = summary?.users || [];
  const filterActive = selectedUserIds.length > 0;
  const selectedUserValue = selectedUserIds[0] || '__all__';
  const selectedUserNames = selectedUserIds
    .map(userId => userOptions.find(user => user.user_id === userId)?.email || userId)
    .join(', ');
  const daily = (summary?.daily || []).slice(-14);
  const platformConfig = summary?.config?.platform_api || {};
  const modelConfig = summary?.config?.model_api || {};
  const maxUserCost = Math.max(1, ...users.map(user => Number(user.total_cost_usd || 0)));
  const maxDailyCost = Math.max(1, ...daily.map(day => Number(day.total_cost_usd || 0)));
  const flowSegments = (row) => COST_FLOW_ORDER.map(flow => ({
    key: flow,
    label: COST_FLOW_LABELS[flow],
    value: row?.flows?.[flow]?.total_cost_usd || 0,
  }));
  const platformRows = [...LIVE_SOURCES, 'unknown'].map(platform => ({
    key: platform,
    label: platformConfig?.[platform]?.label || PLATFORMS?.[platform]?.label || platform,
    total: summary?.platforms?.[platform] || {},
    config: platformConfig?.[platform] || {},
  }));
  const platformFlowRows = platformRows.map(row => ({
    ...row,
    flows: summary?.platform_flows?.[row.key] || {},
  }));
  const modelRows = Object.entries(modelConfig).map(([key, item]) => ({ key, ...(item || {}) }));
  const stageBreakdown = summary?.stage_breakdown || {};
  const sprintVsFallback = summary?.sprint_vs_fallback || {};
  const poolDedup = summary?.pool_dedup || {};
  const recentSprints = Array.isArray(summary?.sprints) ? summary.sprints : [];
  const stageSegments = [
    { key: "search_api", label: "Search APIs", color: "#2563eb", value: stageBreakdown.search_api_cost_usd },
    { key: "whisper", label: "Whisper / ASR", color: "#0891b2", value: stageBreakdown.whisper_cost_usd },
    { key: "scoring", label: "Scoring (LLM)", color: "#7c3aed", value: stageBreakdown.scoring_cost_usd },
    { key: "filtering", label: "Filtering", color: "#ea580c", value: stageBreakdown.filtering_cost_usd },
    { key: "remodeling", label: "Remodel", color: "#16a34a", value: stageBreakdown.remodeling_cost_usd },
  ];
  const stageTotal = stageSegments.reduce((sum, seg) => sum + Number(seg.value || 0), 0);
  const handleUserFilterChange = (event) => {
    const userId = event.target.value;
    const nextUserIds = userId === '__all__' ? [] : [userId];
    setSelectedUserIds(nextUserIds);
    load(nextUserIds);
  };

  return (
    <section className="cost-dashboard admin-grid-view">
      <div className="content-hdr">
        <div>
          <div className="content-title">Cost Dashboard</div>
          <div className="content-sub">Manual Search, AutoSearch, and Auto-Remodel spend — per-stage breakdown, sprint vs fallback, pool dedup savings</div>
        </div>
        <div className="spacer" />
        <button className="btn-secondary" onClick={() => load(selectedUserIds)} disabled={loading}><Icon name="loader" size={14} /> Refresh</button>
      </div>
      {error && <div className="access-error">{error}</div>}
      {loading && !summary && <div className="access-loading">Loading costs...</div>}
      {summary && (
        <>
          <section className="cost-panel cost-user-filter-panel">
            <div className="cost-panel-head">
              <div><strong>User Filter</strong><span>{filterActive ? `Showing ${selectedUserNames}` : 'Showing all users'}</span></div>
            </div>
            <div className="cost-user-filter-control">
              <select className="cost-user-filter-select" value={selectedUserValue} onChange={handleUserFilterChange} disabled={loading || userOptions.length === 0}>
                <option value="__all__">Select All</option>
                {userOptions.map(user => (
                  <option key={user.user_id} value={user.user_id}>{user.email ? `${user.email} - ${user.user_id}` : user.user_id}</option>
                ))}
              </select>
              {filterActive && <button className="btn-secondary compact" onClick={() => { setSelectedUserIds([]); load([]); }} disabled={loading}>Clear</button>}
            </div>
            {userOptions.length === 0 && <div className="access-empty">No users with cost activity yet.</div>}
          </section>

          <div className="cost-kpi-grid">
            <div className="cost-kpi primary"><span>{filterActive ? 'Selected User Cost' : 'Total Organization Cost'}</span><strong>{costMoney(org.total_cost_usd)}</strong><small>{compactNumber(org.users)} users in view</small></div>
            <div className="cost-kpi"><span>Tracked LLM Cost</span><strong>{costMoney(org.tracked_cost_usd)}</strong><small>Filter + remodel ledger</small></div>
            <div className="cost-kpi"><span>Estimated Search Cost</span><strong>{costMoney(org.estimated_cost_usd)}</strong><small>Platform + scoring config</small></div>
            <div className="cost-kpi"><span>Winning Ads Found</span><strong>{compactNumber(org.winning_ads_found)}</strong><small>{compactNumber(org.videos_searched)} videos searched</small></div>
            <div className="cost-kpi savings"><span>Whisper / ASR</span><strong>{costMoney(stageBreakdown.whisper_cost_usd)}</strong><small>Transcription stage</small></div>
            <div className="cost-kpi savings"><span>Pool Dedup Saved</span><strong>{costMoney(poolDedup.savings_usd)}</strong><small>{compactNumber(poolDedup.hits)} skipped re-evaluations</small></div>
          </div>

          <div className="cost-stage-grid">
            <section className="cost-panel cost-stage-panel">
              <div className="cost-panel-head">
                <div><strong>Per-Stage Breakdown</strong><span>Whisper vs scoring vs search APIs</span></div>
                <strong>{costMoney(stageTotal)}</strong>
              </div>
              <CostStackedBar segments={stageSegments} total={stageTotal} />
              <div className="cost-stage-rows">
                {stageSegments.map(segment => {
                  const value = Number(segment.value || 0);
                  const pct = stageTotal > 0 ? (value / stageTotal) * 100 : 0;
                  return (
                    <div className="cost-stage-row" key={segment.key}>
                      <span className="cost-stage-label"><i style={{ background: segment.color }} />{segment.label}</span>
                      <span className="cost-stage-pct">{pct.toFixed(pct > 0 && pct < 10 ? 1 : 0)}%</span>
                      <strong>{costMoney(value)}</strong>
                    </div>
                  );
                })}
              </div>
            </section>

            <section className="cost-panel cost-sprint-panel">
              <div className="cost-panel-head"><div><strong>Sprint vs Fallback</strong><span>Global crawler sprints vs autoresearch fallback</span></div></div>
              <div className="cost-sprint-metrics">
                <div className="cost-sprint-metric">
                  <span>Sprint cost</span>
                  <strong>{costMoney(sprintVsFallback.sprint_cost_usd)}</strong>
                  <small>Shared-pool global crawler</small>
                </div>
                <div className="cost-sprint-metric">
                  <span>Fallback cost</span>
                  <strong>{costMoney(sprintVsFallback.fallback_cost_usd)}</strong>
                  <small>Autoresearch search + whisper + scoring</small>
                </div>
                <div className="cost-sprint-metric savings">
                  <span>Prefill savings</span>
                  <strong>{costMoney(sprintVsFallback.prefill_savings_usd)}</strong>
                  <small>Pool winners copied without re-processing</small>
                </div>
                <div className="cost-sprint-metric">
                  <span>Net fallback</span>
                  <strong>{costMoney(sprintVsFallback.net_fallback_cost_usd)}</strong>
                  <small>Fallback minus prefill savings</small>
                </div>
              </div>
              <div className="cost-pool-dedup-banner">
                <div>
                  <strong>Pool dedup hits</strong>
                  <span>{compactNumber(poolDedup.hits)} ads skipped re-whisper / re-score</span>
                </div>
                <strong className="cost-pool-dedup-value">{costMoney(poolDedup.savings_usd)} saved</strong>
              </div>
            </section>
          </div>

          <section className="cost-panel cost-sprint-history-panel">
            <div className="cost-panel-head">
              <div><strong>Recent Sprints</strong><span>Last {recentSprints.length} global crawler runs</span></div>
            </div>
            <div className="cost-table-scroll">
              {recentSprints.length === 0 && <div className="access-empty">No sprint history yet — runs appear after the next global crawler completion.</div>}
              {recentSprints.length > 0 && (
                <table className="cost-platform-flow-table cost-sprint-table">
                  <thead>
                    <tr>
                      <th>Sprint</th>
                      <th>Trigger</th>
                      <th>Status</th>
                      <th>Search</th>
                      <th>Whisper</th>
                      <th>Scoring</th>
                      <th>Total</th>
                      <th>Dedup</th>
                      <th>Saved</th>
                      <th>Winners</th>
                      <th>Duration</th>
                    </tr>
                  </thead>
                  <tbody>
                    {recentSprints.map((sprint, index) => {
                      const breakdown = sprint.cost_breakdown || {};
                      const dedup = sprint.pool_dedup || {};
                      const sprintKey = sprint.sprint_id || sprint.finished_at || `sprint-${index}`;
                      return (
                        <tr key={sprintKey}>
                          <td>
                            <div className="cost-flow-cell">
                              <strong>{sprint.sprint_id || '—'}</strong>
                              <span>{sprint.finished_at ? sprint.finished_at.slice(0, 19).replace('T', ' ') : '—'}</span>
                            </div>
                          </td>
                          <td>{sprint.trigger_source || '—'}</td>
                          <td>{sprint.final_status || '—'}</td>
                          <td>{costMoney(breakdown.search_api_cost_usd ?? 0)}</td>
                          <td>{costMoney(breakdown.whisper_cost_usd ?? 0)}</td>
                          <td>{costMoney(breakdown.scoring_cost_usd ?? 0)}</td>
                          <td><strong>{costMoney(breakdown.total_cost_usd ?? sprint.cost_usd ?? 0)}</strong></td>
                          <td>{compactNumber(dedup.hits ?? sprint.pool_dedup_hits ?? 0)}</td>
                          <td>{costMoney(dedup.savings_usd ?? 0)}</td>
                          <td>{compactNumber(sprint.inserts_added ?? 0)}</td>
                          <td>{formatCostDuration(sprint.duration_seconds)}</td>
                        </tr>
                      );
                    })}
                  </tbody>
                </table>
              )}
            </div>
          </section>

          <div className="cost-main-grid">
            <section className="cost-panel cost-user-panel">
              <div className="cost-panel-head">
                <div><strong>User Cost</strong><span>Stacked by flow</span></div>
                <div className="cost-legend">
                  {COST_FLOW_ORDER.map(flow => <span key={flow}><i className={flow} />{COST_FLOW_LABELS[flow]}</span>)}
                </div>
              </div>
              <div className="cost-user-list">
                {users.length === 0 && <div className="access-empty">No cost rows yet.</div>}
                {users.map(user => (
                  <div className="cost-user-row" key={user.user_id}>
                    <div className="cost-user-id">
                      <strong>{user.email || user.user_id}</strong>
                      <span>{compactNumber(user.videos_searched)} videos - {compactNumber(user.winning_ads_found)} winners</span>
                    </div>
                    <div className="cost-user-bar-wrap">
                      <CostStackedBar segments={flowSegments(user)} total={maxUserCost} />
                    </div>
                    <div className="cost-user-total">
                      <strong>{costMoney(user.total_cost_usd)}</strong>
                      <span>{costMoney(user.tracked_cost_usd)} tracked</span>
                    </div>
                  </div>
                ))}
              </div>
            </section>

            <section className="cost-panel cost-daily-panel">
              <div className="cost-panel-head"><div><strong>Daily Cost</strong><span>Last {daily.length || 0} days</span></div></div>
              <div className="cost-daily-bars">
                {daily.length === 0 && <div className="access-empty">No daily activity yet.</div>}
                {daily.map(day => (
                  <div className="cost-daily-row" key={day.date}>
                    <span>{day.date}</span>
                    <div className="cost-daily-track"><CostStackedBar segments={flowSegments(day)} total={maxDailyCost} /></div>
                    <strong>{costMoney(day.total_cost_usd)}</strong>
                  </div>
                ))}
              </div>
            </section>
          </div>

          <div className="cost-platform-grid">
            {platformRows.map(row => (
              <section className="cost-platform-card" key={row.key}>
                <div className="cost-platform-title"><span>{row.label}</span><strong>{costMoney(row.total.total_cost_usd)}</strong></div>
                <div className="cost-platform-metrics">
                  <span>{compactNumber(row.total.videos_searched)} videos</span>
                  <span>{compactNumber(row.total.winning_ads_found)} winners</span>
                  <span>{costMoney(row.total.tracked_cost_usd)} tracked</span>
                </div>
              </section>
            ))}
          </div>

          <section className="cost-panel cost-platform-flow-panel">
            <div className="cost-panel-head"><div><strong>Platform Split</strong><span>Cost by platform and flow</span></div></div>
            <div className="cost-table-scroll">
              <table className="cost-platform-flow-table">
                <thead>
                  <tr>
                    <th>Platform</th>
                    {COST_FLOW_ORDER.map(flow => <th key={flow}>{COST_FLOW_LABELS[flow]}</th>)}
                    <th>Videos</th>
                    <th>Winners</th>
                    <th>Total</th>
                  </tr>
                </thead>
                <tbody>
                  {platformFlowRows.map(row => (
                    <tr key={row.key}>
                      <td>{row.label}</td>
                      {COST_FLOW_ORDER.map(flow => {
                        const flowMetrics = row.flows?.[flow] || {};
                        return (
                          <td key={flow}>
                            <div className="cost-flow-cell">
                              <strong>{costMoney(flowMetrics.total_cost_usd || 0)}</strong>
                              <span>{compactNumber(flowMetrics.videos_searched)} videos</span>
                            </div>
                          </td>
                        );
                      })}
                      <td>{compactNumber(row.total.videos_searched)}</td>
                      <td>{compactNumber(row.total.winning_ads_found)}</td>
                      <td><strong>{costMoney(row.total.total_cost_usd)}</strong></td>
                    </tr>
                  ))}
                </tbody>
              </table>
            </div>
          </section>

          <section className="cost-panel cost-config-panel">
            <div className="cost-panel-head"><div><strong>Cost Config</strong><span>Update values in settings when rates change</span></div></div>
            <div className="cost-config-grid">
              <table className="cost-config-table">
                <thead><tr><th>Platform</th><th>Video Search Cost</th><th>API Key</th><th>Status</th></tr></thead>
                <tbody>
                  {LIVE_SOURCES.map(platform => {
                    const item = platformConfig?.[platform] || {};
                    return (
                      <tr key={platform}>
                        <td>{item.label || PLATFORMS?.[platform]?.label || platform}</td>
                        <td>{costMoney(item.cost_per_video_searched_usd || 0)} / video</td>
                        <td><code>{item.api_key_env_var || 'Not set'}</code></td>
                        <td><span className={`cost-key-state ${item.api_key_configured ? 'ok' : 'missing'}`}>{item.api_key_configured ? 'Configured' : 'Missing'}</span></td>
                      </tr>
                    );
                  })}
                </tbody>
              </table>
              <table className="cost-config-table">
                <thead><tr><th>Model Stage</th><th>Model</th><th>Cost</th><th>API Key</th></tr></thead>
                <tbody>
                  {modelRows.map(row => (
                    <tr key={row.key}>
                      <td>{row.label || row.key}</td>
                      <td>{row.model || row.provider || 'Configured model'}</td>
                      <td>{row.cost_per_video_usd !== undefined ? `${costMoney(row.cost_per_video_usd)} / video` : `${costMoney(row.input_per_mtok_usd)} in / ${costMoney(row.output_per_mtok_usd)} out MTok`}</td>
                      <td><code>{row.api_key_env_var || 'Not set'}</code> <span className={`cost-key-state ${row.api_key_configured ? 'ok' : 'missing'}`}>{row.api_key_configured ? 'Configured' : 'Missing'}</span></td>
                    </tr>
                  ))}
                </tbody>
              </table>
            </div>
          </section>
        </>
      )}
    </section>
  );
}

function AdminWinningCriteriaManager({ onCriteriaSaved }) {
  const [rows, setRows] = useState([]);
  const [selectedPlatform, setSelectedPlatform] = useState('foreplay');
  const [form, setForm] = useState(blankCriteriaForm());
  const [loading, setLoading] = useState(false);
  const [busy, setBusy] = useState(false);
  const [error, setError] = useState('');

  const orderedRows = CRITERIA_PLATFORM_ORDER
    .map(platform => rows.find(row => row.platform === platform))
    .filter(Boolean);
  const selected = rows.find(row => row.platform === selectedPlatform) || orderedRows[0] || null;
  const signalsText = (signals) => Array.isArray(signals) ? signals.join('\n') : (signals || '');

  const load = async () => {
    setLoading(true);
    setError('');
    try {
      const data = await RESILIA_API.adminListWinningAdCriteria();
      const nextRows = data.criteria || [];
      setRows(nextRows);
      if (!nextRows.some(row => row.platform === selectedPlatform) && nextRows[0]) {
        setSelectedPlatform(nextRows[0].platform);
      }
    } catch (e) {
      setError(e.message || 'Winning ad criteria failed to load');
    } finally {
      setLoading(false);
    }
  };

  useEffect(() => { load(); }, []);
  useEffect(() => {
    if (!selected) return;
    const criteria = selected.criteria || {};
    setForm({
      pass_threshold: String(criteria.pass_threshold ?? ''),
      video_scoring_criteria: criteria.video_scoring_criteria || criteria.score_rubric || '',
      rubric: criteria.rubric || '',
      source_specific_signals: signalsText(criteria.source_specific_signals),
      allow_shared_pool_prefill: criteria.allow_shared_pool_prefill !== false,
    });
  }, [selected]);

  const updateForm = (patch) => setForm(current => ({ ...current, ...patch }));
  const splitSignals = (value) => String(value || '')
    .split('\n')
    .map(line => line.trim())
    .filter(Boolean);

  const save = async () => {
    if (!selectedPlatform) return;
    const threshold = Number(form.pass_threshold);
    if (!Number.isFinite(threshold)) {
      setError('Pass threshold must be a number');
      return;
    }
    setBusy(true);
    setError('');
    try {
      const criteria = {
        ...(selected?.criteria || {}),
        pass_threshold: Math.max(0, Math.min(10, Math.round(threshold * 10) / 10)),
        video_scoring_criteria: form.video_scoring_criteria.trim(),
        rubric: form.rubric.trim(),
        source_specific_signals: splitSignals(form.source_specific_signals),
        allow_shared_pool_prefill: form.allow_shared_pool_prefill !== false,
      };
      const out = await RESILIA_API.adminSaveWinningAdCriteria(selectedPlatform, criteria);
      const saved = out.criteria;
      const savedThreshold = Number(saved?.criteria?.pass_threshold);
      const nextRows = rows.map(row => {
        if (row.platform === saved.platform) return saved;
        if (!Number.isFinite(savedThreshold)) return row;
        return {
          ...row,
          criteria: { ...(row.criteria || {}), pass_threshold: savedThreshold },
        };
      });
      const refreshedRows = nextRows.length ? nextRows : [saved];
      setRows(refreshedRows);
      onCriteriaSaved?.(refreshedRows);
    } catch (e) {
      setError(e.message || 'Save failed');
    } finally {
      setBusy(false);
    }
  };

  return (
    <section className="admin-grid-view">
      <div className="content-hdr">
        <div>
          <div className="content-title">Winning Ads Criteria</div>
          <div className="content-sub">{orderedRows.length || 0} platforms · scoring and winner selection</div>
        </div>
        <div className="spacer" />
        <button className="btn-secondary" onClick={load} disabled={loading || busy}><Icon name="loader" size={14} /> Refresh</button>
        <button className="btn-primary" onClick={save} disabled={busy || !selected}><Icon name="check" size={14} /> Save</button>
      </div>
      {error && <div className="access-error">{error}</div>}
      <div className="admin-grid criteria">
        <aside className="admin-list">
          {loading && <div className="access-loading">Loading...</div>}
          {orderedRows.map(row => {
            const platformMeta = PLATFORMS[row.platform] || {};
            return (
              <button
                key={row.platform}
                className={`admin-list-row ${selectedPlatform === row.platform ? 'active' : ''}`}
                onClick={() => setSelectedPlatform(row.platform)}
              >
                <span className="criteria-platform-name"><i style={{ background: platformMeta.color || 'var(--primary-500)' }} />{row.label || platformMeta.label || row.platform}</span>
                <small>Shared threshold {row.criteria?.pass_threshold ?? '—'}</small>
                <small>{row.updated_at ? `Updated ${row.updated_at.slice(0, 19)}` : 'Default criteria'}</small>
              </button>
            );
          })}
        </aside>
        <section className="admin-editor criteria-editor">
          <div className="criteria-top-row">
            <div>
              <div className="admin-section-title">{selected?.label || 'Platform'}</div>
              <div className="content-sub">{selected?.platform || '—'}</div>
            </div>
            <label className="criteria-threshold">Shared Pass Threshold<input type="number" min="0" max="10" step="0.1" value={form.pass_threshold} onChange={e => updateForm({ pass_threshold: e.target.value })} /></label>
          </div>
          <label className="admin-field-full criteria-toggle">
            <input type="checkbox" checked={form.allow_shared_pool_prefill !== false} onChange={e => updateForm({ allow_shared_pool_prefill: e.target.checked })} />
            Allow user Search and AutoSearch to prefill from the global crawler pool for this platform.
          </label>
          <label className="admin-field-full criteria-score">Video Scoring Criteria<textarea value={form.video_scoring_criteria} onChange={e => updateForm({ video_scoring_criteria: e.target.value })} spellCheck="true" /></label>
          <label className="admin-field-full criteria-rubric">Platform Winning Criteria<textarea value={form.rubric} onChange={e => updateForm({ rubric: e.target.value })} spellCheck="true" /></label>
          <label className="admin-field-full criteria-signals">Source Signals<textarea value={form.source_specific_signals} onChange={e => updateForm({ source_specific_signals: e.target.value })} spellCheck="true" /></label>
          <div className="criteria-meta-grid">
            <div><span>Created</span><strong>{selected?.created_at ? selected.created_at.slice(0, 19) : '—'}</strong></div>
            <div><span>Updated</span><strong>{selected?.updated_at ? selected.updated_at.slice(0, 19) : '—'}</strong></div>
            <div><span>Updated By</span><strong>{selected?.updated_by_email || selected?.updated_by || 'system'}</strong></div>
          </div>
        </section>
      </div>
    </section>
  );
}

function inferGlobalCrawlerDisplayState(liveProgress, crawler, starting, crawlActive) {
  if (starting) return 'starting';
  if (crawler?.user_paused) return crawlActive ? 'stopping' : 'stopped';
  if (crawler?.stop_requested) return crawlActive ? 'stopping' : 'stopped';
  const base = liveProgress?.crawlState || crawler?.crawl_status || crawler?.status || 'unknown';
  if (['stopping', 'cancelled', 'stopped'].includes(String(base).toLowerCase())) return base.toLowerCase();
  const platformProgress = liveProgress?.platformProgress || liveProgress?.platform_progress || {};
  const rows = Object.values(platformProgress).filter(row => row && typeof row === 'object');
  if (rows.some(row => Number(row.queued) > 0 || Number(row.scoring) > 0 || Number(row.transcribing) > 0)) {
    return 'running';
  }
  if (rows.some(row => String(row.status || '').toLowerCase() === 'running')) return 'running';
  const terminal = new Set(['exhausted', 'target_met', 'budget_reached', 'stopped', 'failed']);
  if (
    rows.length
    && rows.every(row => terminal.has(String(row.status || '').toLowerCase()))
  ) {
    const touched = Number(liveProgress?.coverageTouched || 0);
    const total = Number(liveProgress?.coverageTotal || 0);
    if (['running', 'started', 'queued'].includes(String(base).toLowerCase()) || (total > 0 && touched >= total)) {
      return 'done';
    }
  }
  return base;
}

function AdminGlobalCrawlerStatus() {
  const [status, setStatus] = useState(null);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState('');
  const [pollMs, setPollMs] = useState(30000);
  const [maxAdsPerDirection, setMaxAdsPerDirection] = useState('5000');
  const [perDirectionTarget, setPerDirectionTarget] = useState('50');
  const [schedulerEnabled, setSchedulerEnabled] = useState(true);
  const [schedulerHour, setSchedulerHour] = useState('6');
  const [schedulerMinute, setSchedulerMinute] = useState('0');
  const [schedulerTimezone, setSchedulerTimezone] = useState('Asia/Karachi');
  const [schedulerMinWinners, setSchedulerMinWinners] = useState('20');
  const [savingSettings, setSavingSettings] = useState(false);
  const [starting, setStarting] = useState(false);
  const [stopping, setStopping] = useState(false);
  const [clearing, setClearing] = useState(false);
  const [settingsDirty, setSettingsDirty] = useState(false);
  const [lastStartedPlatforms, setLastStartedPlatforms] = useState([]);
  const [notice, setNotice] = useState('');
  const [versions, setVersions] = useState([]);
  const [versionsLoading, setVersionsLoading] = useState(false);
  const [versionActionId, setVersionActionId] = useState(null);
  const [newVersionName, setNewVersionName] = useState('');
  const [newVersionNotes, setNewVersionNotes] = useState('');
  const [creatingVersion, setCreatingVersion] = useState(false);
  const [selectedVersionId, setSelectedVersionId] = useState(null);
  const [poolAds, setPoolAds] = useState([]);
  const [poolAdsTotal, setPoolAdsTotal] = useState(0);
  const [poolAdsLoading, setPoolAdsLoading] = useState(false);
  const [poolAdsWinnersOnly, setPoolAdsWinnersOnly] = useState(false);
  const [poolAdsPlatform, setPoolAdsPlatform] = useState('');
  const [poolAdsPage, setPoolAdsPage] = useState(0);
  const POOL_ADS_PAGE_SIZE = 25;
  const [editingVersionId, setEditingVersionId] = useState(null);
  const [editingVersionName, setEditingVersionName] = useState('');
  const emptyPlatformSelection = () => Object.fromEntries(LIVE_SOURCES.map(platform => [platform, false]));
  const [selectedPlatforms, setSelectedPlatforms] = useState(emptyPlatformSelection);

  const applySettings = (settings) => {
    if (!settings || settingsDirty) return;
    if (settings.max_ads_per_direction != null) {
      setMaxAdsPerDirection(String(settings.max_ads_per_direction));
    }
    if (settings.per_direction_target != null) {
      setPerDirectionTarget(String(settings.per_direction_target));
    }
    if (Array.isArray(settings.platforms)) {
      const next = emptyPlatformSelection();
      settings.platforms.forEach(platform => {
        const key = String(platform || '').toLowerCase();
        if (LIVE_SOURCES.includes(key)) next[key] = true;
      });
      setSelectedPlatforms(next);
    }
    if (settings.scheduler_enabled != null) {
      setSchedulerEnabled(Boolean(settings.scheduler_enabled));
    }
    if (settings.scheduler_hour != null) {
      setSchedulerHour(String(settings.scheduler_hour));
    }
    if (settings.scheduler_minute != null) {
      setSchedulerMinute(String(settings.scheduler_minute));
    }
    if (settings.scheduler_timezone) {
      setSchedulerTimezone(String(settings.scheduler_timezone));
    }
    if (settings.scheduler_min_winners_per_direction != null) {
      setSchedulerMinWinners(String(settings.scheduler_min_winners_per_direction));
    }
  };

  const activePlatformList = () => LIVE_SOURCES.filter(platform => selectedPlatforms[platform]);
  const updatePlatformSelection = (platform, enabled) => {
    setSettingsDirty(true);
    setSelectedPlatforms(prev => ({ ...prev, [platform]: enabled }));
  };

  const loadVersions = async () => {
    setVersionsLoading(true);
    try {
      const data = await RESILIA_API.adminListSharedPoolVersions();
      const rows = Array.isArray(data?.versions) ? data.versions : [];
      setVersions(rows);
      const active = rows.find(v => v.status === 'active');
      setSelectedVersionId(prev => prev ?? active?.id ?? rows[0]?.id ?? null);
      return rows;
    } catch (e) {
      setError(e.message || 'Failed to load pool versions');
      return [];
    } finally {
      setVersionsLoading(false);
    }
  };

  const loadPoolAds = async (versionId = selectedVersionId, page = poolAdsPage) => {
    if (!versionId) {
      setPoolAds([]);
      setPoolAdsTotal(0);
      return;
    }
    setPoolAdsLoading(true);
    try {
      const safePage = Math.max(0, Number(page) || 0);
      const data = await RESILIA_API.adminListSharedPoolAds({
        poolVersionId: versionId,
        winnersOnly: poolAdsWinnersOnly,
        platform: poolAdsPlatform || undefined,
        limit: POOL_ADS_PAGE_SIZE,
        offset: safePage * POOL_ADS_PAGE_SIZE,
      });
      setPoolAds(Array.isArray(data?.ads) ? data.ads : []);
      setPoolAdsTotal(Number(data?.total || 0));
      setPoolAdsPage(safePage);
    } catch (e) {
      setError(e.message || 'Failed to load pool ads');
    } finally {
      setPoolAdsLoading(false);
    }
  };

  const load = async () => {
    setLoading(true);
    setError('');
    try {
      const data = await RESILIA_API.adminGlobalCrawlerStatus();
      setStatus(data);
      if (Array.isArray(data?.pool_versions)) {
        setVersions(data.pool_versions);
        const active = data.pool_versions.find(v => v.status === 'active');
        setSelectedVersionId(prev => prev ?? active?.id ?? data.pool_versions[0]?.id ?? null);
      } else {
        await loadVersions();
      }
      applySettings(data.settings);
      if (Array.isArray(data?.crawler?.platforms) && data.crawler.platforms.length) {
        setLastStartedPlatforms(data.crawler.platforms);
      }
      const crawlState = data?.live_progress?.crawl_state || data?.crawler?.status || data?.crawler?.crawl_status;
      setPollMs(['running', 'stopping'].includes(crawlState) ? 3000 : 30000);
      return data;
    } catch (e) {
      setError(e.message || 'Global crawler status failed to load');
      return null;
    } finally {
      setLoading(false);
    }
  };

  const saveSettings = async () => {
    setSavingSettings(true);
    setError('');
    try {
      const maxAds = boundedPositiveIntOrNull(maxAdsPerDirection, SEARCH_ADS_PER_DIRECTION_MAX);
      const winners = boundedPositiveIntOrNull(perDirectionTarget, AR_TARGET_MAX);
      if (!maxAds || !winners) {
        throw new Error('Enter valid ads-per-direction and winner caps');
      }
      const platforms = activePlatformList();
      if (!platforms.length) throw new Error('Select at least one platform');
      const data = await RESILIA_API.adminUpdateGlobalCrawlerSettings({
        maxAdsPerDirection: maxAds,
        perDirectionTarget: winners,
        platforms,
        schedulerEnabled,
        schedulerHour: schedulerClockIntOrNull(schedulerHour, 23),
        schedulerMinute: schedulerClockIntOrNull(schedulerMinute, 59),
        schedulerTimezone: schedulerTimezone || settings?.scheduler_timezone || 'Asia/Karachi',
        schedulerMinWinnersPerDirection: boundedPositiveIntOrNull(schedulerMinWinners, AR_TARGET_MAX),
      });
      applySettings(data.settings);
      setSettingsDirty(false);
      setMaxAdsPerDirection(String(data.settings?.max_ads_per_direction ?? maxAds));
      setPerDirectionTarget(String(data.settings?.per_direction_target ?? winners));
      await load();
      return data.settings;
    } catch (e) {
      setError(e.message || 'Failed to save global crawler settings');
      return null;
    } finally {
      setSavingSettings(false);
    }
  };

  const startCrawl = async () => {
    setStarting(true);
    setError('');
    setNotice('');
    try {
      // Persist platform + scheduler settings so manual runs and the daily scheduler stay aligned.
      const saved = await saveSettings();
      if (!saved) return;
      const platformsToRun = Array.isArray(saved.platforms) && saved.platforms.length
        ? saved.platforms
        : activePlatformList();
      if (!platformsToRun.length) throw new Error('Select at least one platform');
      setLastStartedPlatforms(platformsToRun);
      const result = await RESILIA_API.adminStartGlobalCrawler({ platforms: platformsToRun });
      if (result?.status === 'disabled') {
        setError(result.message || 'Global crawler is disabled in config');
        return;
      }
      if (result?.status === 'invalid') {
        setError(result.message || 'Select at least one platform');
        return;
      }
      if (result?.status === 'already_running') {
        setError('Global crawler is already running');
        await load();
        return;
      }
      if (result?.status === 'started') {
        const startedPlatforms = Array.isArray(result.platforms) && result.platforms.length
          ? result.platforms
          : platformsToRun;
        setLastStartedPlatforms(startedPlatforms);
        const backend = result.job_backend === 'rabbitmq' ? 'RabbitMQ worker' : 'API process';
        const queued = result.queued ? 'Queued on' : 'Running in';
        setNotice(`${queued} ${backend}. Live progress will update below.`);
        setPollMs(5000);
        setStatus(prev => ({
          ...(prev || {}),
          crawler: {
            ...(prev?.crawler || {}),
            status: 'running',
            crawl_status: 'running',
            platforms: startedPlatforms,
            trigger_source: 'manual',
            platform_progress: {},
            direction_progress: {},
          },
          settings: {
            ...(prev?.settings || {}),
            platforms: startedPlatforms,
          },
          live_progress: {
            ...(prev?.live_progress || {}),
            crawl_state: 'running',
            status: 'running',
            platformProgress: {},
            directionProgress: {},
          },
        }));
      }
      const refreshed = await load();
      if (result?.status === 'started' && refreshed?.crawler?.message) {
        const finalState = refreshed?.live_progress?.crawl_state
          || refreshed?.crawler?.crawl_status
          || refreshed?.crawler?.status;
        if (finalState && finalState !== 'running') {
          setNotice('');
          setError(refreshed.crawler.message);
        }
      }
    } catch (e) {
      setError(e.message || 'Failed to start global crawler');
    } finally {
      setStarting(false);
    }
  };

  const clearSelectedPlatforms = async () => {
    const platforms = activePlatformList();
    if (!platforms.length) {
      setError('Select at least one platform to clear');
      return;
    }
    const labels = platforms.map(p => PLATFORMS[p]?.label || p).join(', ');
    const confirmed = confirm(
      `Clear all global shared-pool data for ${labels}?\n\n`
      + 'This permanently removes for the active shared pool only:\n'
      + '• Evaluated ads and winners\n'
      + '• Keyword crawl progress (cursors)\n'
      + '• Dedupe “already seen” claims\n'
      + '• Cached voice transcripts (disk)\n\n'
      + 'The next crawl will re-fetch, re-transcribe, and re-score from scratch.\n\n'
      + 'Foreplay/AdSpy are untouched if not selected. This cannot be undone.',
    );
    if (!confirmed) return;

    setClearing(true);
    setError('');
    setNotice('');
    try {
      const result = await RESILIA_API.adminClearGlobalPoolPlatforms({ platforms });
      if (result?.status === 'running') {
        setError(result.message || 'Stop the global crawler before clearing platform data');
        return;
      }
      if (result?.status === 'invalid') {
        setError(result.message || 'Select at least one platform');
        return;
      }
      setNotice(
        `Cleared ${labels}: `
        + `${compactNumber(result?.evaluated_removed || 0)} evaluated, `
        + `${compactNumber(result?.qualified_removed || 0)} winners, `
        + `${compactNumber(result?.keywords_removed || 0)} keywords, `
        + `${compactNumber(result?.dedupe_claims_removed || 0)} dedupe claims, `
        + `${compactNumber(result?.transcripts_removed || 0)} transcript files.`,
      );
      await load();
    } catch (e) {
      setError(e.message || 'Failed to clear platform pool data');
    } finally {
      setClearing(false);
    }
  };

  const stopCrawl = async () => {
    setStopping(true);
    setError('');
    setNotice('');
    try {
      const result = await RESILIA_API.adminStopGlobalCrawler();
      if (result?.status === 'not_running') {
        setError('No active global crawler run found to stop.');
        await load();
        return;
      }
      if (result?.status === 'forbidden') {
        setError('Stop is not allowed for the current crawl owner.');
        await load();
        return;
      }
      setNotice(
        result?.message
          || 'Stop accepted. In-flight ads may take a few minutes to finish transcription/scoring.',
      );
      setPollMs(5000);
      setStatus(prev => ({
        ...(prev || {}),
        crawler: {
          ...(prev?.crawler || {}),
          status: 'stopping',
          crawl_status: 'stopping',
          stop_requested: true,
        },
        live_progress: {
          ...(prev?.live_progress || {}),
          crawl_state: 'stopping',
          status: 'stopping',
        },
      }));
      await load();
    } catch (e) {
      setError(e.message || 'Failed to stop global crawler');
    } finally {
      setStopping(false);
    }
  };

  const createPoolVersion = async () => {
    const name = String(newVersionName || '').trim();
    if (!name) {
      setError('Enter a name for the new pool version');
      return;
    }
    setCreatingVersion(true);
    setError('');
    setNotice('');
    try {
      const result = await RESILIA_API.adminResetSharedPool({
        name,
        notes: newVersionNotes.trim() || null,
      });
      if (result?.status === 'running') {
        setError(result.message || 'Stop the global crawler before creating a new pool version');
        return;
      }
      setNewVersionName('');
      setNewVersionNotes('');
      setNotice(`Created pool version “${result?.version?.name || name}” (id ${result?.version?.id})`);
      await load();
      if (result?.version?.id) {
        setSelectedVersionId(result.version.id);
      }
    } catch (e) {
      setError(e.message || 'Failed to create pool version');
    } finally {
      setCreatingVersion(false);
    }
  };

  const activatePoolVersion = async (versionId) => {
    const version = versions.find(v => v.id === versionId);
    const label = version?.name || versionId;
    if (version?.status === 'active') {
      setSelectedVersionId(versionId);
      return;
    }
    const confirmed = confirm(
      `Switch active pool to “${label}” (id ${versionId})?\n\n`
      + 'Global Crawl and prefill will use this version’s ads and keyword progress.',
    );
    if (!confirmed) return;
    setVersionActionId(versionId);
    setError('');
    setNotice('');
    try {
      const result = await RESILIA_API.adminActivateSharedPoolVersion(versionId);
      if (result?.status === 'running') {
        setError(result.message || 'Stop the global crawler before switching versions');
        return;
      }
      setNotice(`Activated pool version “${result?.version?.name || label}”`);
      setSelectedVersionId(versionId);
      await load();
    } catch (e) {
      setError(e.message || 'Failed to activate pool version');
    } finally {
      setVersionActionId(null);
    }
  };

  const saveVersionName = async (versionId) => {
    const name = String(editingVersionName || '').trim();
    if (!name) {
      setError('Version name cannot be empty');
      return;
    }
    setVersionActionId(versionId);
    setError('');
    try {
      await RESILIA_API.adminUpdateSharedPoolVersion(versionId, { name });
      setEditingVersionId(null);
      setEditingVersionName('');
      await loadVersions();
    } catch (e) {
      setError(e.message || 'Failed to rename pool version');
    } finally {
      setVersionActionId(null);
    }
  };

  useEffect(() => {
    load();
  }, []);

  useEffect(() => {
    setPoolAdsPage(0);
  }, [selectedVersionId, poolAdsWinnersOnly, poolAdsPlatform]);

  useEffect(() => {
    if (selectedVersionId) loadPoolAds(selectedVersionId, poolAdsPage);
  }, [selectedVersionId, poolAdsWinnersOnly, poolAdsPlatform, poolAdsPage]);

  useEffect(() => {
    const timer = setInterval(load, pollMs);
    return () => clearInterval(timer);
  }, [pollMs]);

  const crawler = status?.crawler || {};
  const settings = status?.settings || {};
  const scheduler = status?.scheduler || {};
  const directions = status?.directions || [];
  const totals = status?.totals || {};
  const activePool = status?.active_pool || {};
  const schedulerFloor = Number(
    scheduler.min_winners_per_direction
    ?? settings?.scheduler_min_winners_per_direction
    ?? schedulerMinWinners
    ?? 20
  );
  const platformTotals = status?.platform_totals || {};
  const poolPlatformSummary = React.useMemo(() => {
    let evaluated = 0;
    let winners = 0;
    LIVE_SOURCES.forEach((platform) => {
      const row = platformTotals[platform] || {};
      evaluated += Number(row.evaluated || 0);
      winners += Number(row.winners || 0);
    });
    return { evaluated, winners };
  }, [platformTotals]);
  const directionWinnerTotals = React.useMemo(() => {
    const byPlatform = Object.fromEntries(LIVE_SOURCES.map(platform => [platform, 0]));
    let grand = 0;
    directions.forEach((direction) => {
      LIVE_SOURCES.forEach((platform) => {
        const count = Number(direction.platforms?.[platform] || 0);
        byPlatform[platform] += count;
        grand += count;
      });
    });
    return { byPlatform, grand };
  }, [directions]);
  const liveDirectionWinnerTotals = React.useMemo(() => {
    const byPlatform = Object.fromEntries(LIVE_SOURCES.map(platform => [platform, 0]));
    let grand = 0;
    directions.forEach((direction) => {
      LIVE_SOURCES.forEach((platform) => {
        const progress = direction.progress?.[platform] || {};
        const count = Number(progress.visible_winners ?? progress.winners ?? 0);
        byPlatform[platform] += count;
        grand += count;
      });
    });
    return { byPlatform, grand };
  }, [directions]);
  const poolAdsPageCount = Math.max(1, Math.ceil(poolAdsTotal / POOL_ADS_PAGE_SIZE));
  const poolAdsPageStart = poolAdsTotal ? poolAdsPage * POOL_ADS_PAGE_SIZE + 1 : 0;
  const poolAdsPageEnd = poolAdsTotal
    ? Math.min(poolAdsTotal, (poolAdsPage + 1) * POOL_ADS_PAGE_SIZE)
    : 0;
  const poolAdVideoUrl = (ad) => {
    const url = String(ad?.creative_url || ad?.creativeUrl || '').trim();
    return url || null;
  };
  const poolAdLandingUrl = (ad) => {
    const url = String(ad?.link_url || ad?.linkUrl || '').trim();
    return url || null;
  };
  const poolAdReasonSnippet = (ad, maxLen = 140) => {
    let text = String(ad?.reasoning || '').replace(/\s+/g, ' ').trim();
    if (!text && ad?.score != null) {
      const verdict = ad.passes ? 'Pass' : 'Fail';
      text = `${verdict} · score ${Number(ad.score).toFixed(1)} (no reasoning stored — re-crawl to refresh)`;
    }
    if (!text) return '';
    return text.length <= maxLen ? text : `${text.slice(0, maxLen - 1)}…`;
  };
  const concurrencyLimit = status?.concurrency_limit || settings?.concurrency_limit || 4;
  const fetchCap = settings?.max_ads_per_direction ?? crawler?.settings?.max_ads_per_direction ?? maxAdsPerDirection;
  const winnerCap = crawler?.settings?.per_direction_target ?? settings?.per_direction_target ?? perDirectionTarget;
  const currentRunWinnerFloor = Number(winnerCap ?? schedulerFloor ?? 20);
  const liveProgress = mapCrawlerStateToProgress(status?.live_progress || crawler);
  const enrichedLiveProgress = React.useMemo(() => {
    if (!liveProgress) return liveProgress;
    const runWinners = Number(
      liveProgress.totalWinners
      ?? liveProgress.winners
      ?? totals.live_winners
      ?? 0,
    );
    return {
      ...liveProgress,
      totalWinners: runWinners,
      winners: runWinners,
      globalWinners: Number(totals.winners || 0),
      globalScored: Number(totals.evaluated || 0),
    };
  }, [liveProgress, totals.evaluated, totals.live_winners, totals.winners]);
  const displayWinners = Number(
    enrichedLiveProgress?.totalWinners
    || enrichedLiveProgress?.winners
    || totals.live_winners
    || 0,
  );
  const crawlActive = Boolean(status?.crawl_active);
  const crawlState = inferGlobalCrawlerDisplayState(liveProgress, crawler, starting, crawlActive);
  const isWindingDown = crawlState === 'stopping' && crawlActive;
  const isStopping = isWindingDown || stopping;
  const isRunning = !isStopping && ['running', 'starting', 'queued'].includes(crawlState);
  const isComplete = crawlState === 'done' || crawlState === 'stopped';
  const hasLiveProgress = !!enrichedLiveProgress && (
    isRunning
    || isComplete
    || Object.keys(enrichedLiveProgress.directionProgress || {}).length > 0
    || Object.keys(enrichedLiveProgress.platformProgress || {}).length > 0
  );
  const resolveRunPlatforms = () => {
    const normalize = (values) => (
      Array.isArray(values)
        ? values
          .map(value => String(value || '').toLowerCase())
          .filter(value => LIVE_SOURCES.includes(value))
        : []
    );
    const unique = (values) => LIVE_SOURCES.filter(platform => values.includes(platform));
    const fromStart = unique(normalize(lastStartedPlatforms));
    if (fromStart.length) return fromStart;
    const fromCrawler = unique(normalize(crawler?.platforms));
    if (fromCrawler.length) return fromCrawler;
    const fromUi = activePlatformList();
    if (fromUi.length) return fromUi;
    const fromSettings = unique(normalize(settings?.platforms));
    if (fromSettings.length) return fromSettings;
    return [];
  };
  const runPlatforms = resolveRunPlatforms();
  const globalSourceLabel = runPlatforms.length
    ? runPlatforms.map(p => PLATFORMS[p]?.label || p).join(' + ')
    : 'no platform selected';
  const triggerSource = crawler?.trigger_source || status?.crawler?.trigger_source;
  const isScheduledRun = triggerSource === 'scheduler';
  const userPaused = Boolean(crawler?.user_paused || status?.user_paused || scheduler?.user_paused);
  const pauseMessage = crawler?.pause_message || status?.pause_message || scheduler?.pause_message || 'Automatic starts are paused until Run Global Crawl is clicked.';
  const runSourceLabel = triggerSource === 'scheduler'
    ? 'Scheduler'
    : triggerSource === 'daemon'
      ? 'Daemon'
      : triggerSource === 'manual'
        ? 'Manual'
        : 'Unknown';
  const backendLabel = crawler?.job_backend === 'rabbitmq'
    ? 'RabbitMQ worker'
    : crawler?.job_backend === 'in_process'
      ? 'API process'
      : (status?.autostart_enabled ? 'Daemon capable' : 'Manual only');
  const statusErrors = Array.isArray(status?.errors) ? status.errors : [];
  const schedulerTz = schedulerTimezone || settings?.scheduler_timezone || 'Asia/Karachi';
  const schedulerTimeLabel = `${String(schedulerHour ?? settings?.scheduler_hour ?? 6).padStart(2, '0')}:${String(schedulerMinute ?? settings?.scheduler_minute ?? 0).padStart(2, '0')} (${schedulerTz})`;
  const schedulerPlatformLabel = (Array.isArray(scheduler?.platforms) && scheduler.platforms.length
    ? scheduler.platforms
    : (Array.isArray(settings?.platforms) ? settings.platforms : []))
    .map(p => PLATFORMS[p]?.label || p)
    .join(' + ') || 'none';
  const schedulerTimezoneOptions = SCHEDULER_TIMEZONE_OPTIONS.includes(schedulerTz)
    ? SCHEDULER_TIMEZONE_OPTIONS
    : [schedulerTz, ...SCHEDULER_TIMEZONE_OPTIONS];
  const progressStatusText = isComplete
    ? (crawlState === 'stopped'
      ? (userPaused ? 'Global crawler paused' : (isScheduledRun ? 'Scheduled crawl stopped' : 'Global crawl stopped'))
      : (isScheduledRun ? 'Scheduled crawl complete' : 'Global crawl complete'))
    : isWindingDown
      ? (isScheduledRun ? 'Scheduled crawl — finishing in-flight ads…' : 'Finishing in-flight ads…')
      : isStopping
        ? (isScheduledRun ? 'Stopping scheduled crawl…' : 'Stopping global crawl…')
        : isRunning
        ? (isScheduledRun
          ? `Scheduled crawl — scanning ${globalSourceLabel}…`
          : `Scanning ${globalSourceLabel}…`)
        : crawlState === 'failed'
          ? (isScheduledRun ? 'Scheduled crawl failed' : 'Global crawl failed')
          : `Global crawler ${crawlState}`;

  return (
    <section className="admin-grid-view">
      <div className="content-hdr">
        <div>
          <div className="content-title">Global Crawler</div>
          <div className="content-sub">Live crawl progress and shared pool winners by direction</div>
        </div>
        <div className="spacer" />
        <button className="btn-secondary" onClick={load} disabled={loading}><Icon name="loader" size={14} /> Refresh</button>
        {!isRunning && (
          <button className="btn-primary" onClick={startCrawl} disabled={starting || savingSettings}>
            {starting ? 'Starting…' : 'Run Global Crawl'}
          </button>
        )}
        {(isRunning || isWindingDown) && (
          <button className="btn-secondary" onClick={stopCrawl} disabled={stopping || isWindingDown}>
            {isWindingDown ? 'Finishing…' : 'Stop'}
          </button>
        )}
      </div>
      <div className="ar-sources admin-crawler-settings">
        <div className="admin-crawler-platforms">
          <span className="admin-crawler-platforms-label">Platforms</span>
          <div className="admin-platform-select-row">
            {LIVE_SOURCES.map(platform => (
              <label
                key={platform}
                className={`admin-platform-toggle ${selectedPlatforms[platform] ? 'active' : ''}`}
              >
                <input
                  type="checkbox"
                  checked={!!selectedPlatforms[platform]}
                  disabled={isRunning || isStopping}
                  onChange={e => updatePlatformSelection(platform, e.target.checked)}
                />
                <span className="admin-platform-toggle-body">
                  <span>{PLATFORMS[platform]?.label || platform}</span>
                  <span className="admin-platform-toggle-stats">
                    {compactNumber(platformTotals[platform]?.winners || 0)} winners
                    · {compactNumber(platformTotals[platform]?.evaluated || 0)} ads
                  </span>
                </span>
              </label>
            ))}
          </div>
          <div className="admin-platform-pool-summary">
            Active pool total: {compactNumber(poolPlatformSummary.winners)} winners
            · {compactNumber(poolPlatformSummary.evaluated)} ads
          </div>
        </div>
        <label className="ar-target">
          <span>Ads per direction cap</span>
          <input
            type="number"
            min={1}
            max={SEARCH_ADS_PER_DIRECTION_MAX}
            step={1}
            value={maxAdsPerDirection}
            title={`Maximum ads to fetch/score per direction and platform. Max ${SEARCH_ADS_PER_DIRECTION_MAX}.`}
            onChange={(e) => {
              setSettingsDirty(true);
              setMaxAdsPerDirection(sanitizeArTargetInput(e.target.value, SEARCH_ADS_PER_DIRECTION_MAX));
            }}
          />
        </label>
        <label className="ar-target">
          <span>Winners per direction cap</span>
          <input
            type="number"
            min={1}
            max={AR_TARGET_MAX}
            step={1}
            value={perDirectionTarget}
            title={`Maximum qualified ads per direction and platform per pass. Max ${AR_TARGET_MAX}.`}
            onChange={(e) => {
              setSettingsDirty(true);
              setPerDirectionTarget(sanitizeArTargetInput(e.target.value, AR_TARGET_MAX));
            }}
          />
        </label>
        <button className="btn-secondary" onClick={saveSettings} disabled={savingSettings || !settingsDirty}>
          {savingSettings ? 'Saving…' : 'Save settings'}
        </button>
        <button
          className="btn-secondary danger"
          onClick={clearSelectedPlatforms}
          disabled={clearing || isRunning || isStopping || !activePlatformList().length}
          title="Remove evaluated ads and winners for the checked platforms from the active shared pool"
        >
          {clearing ? 'Clearing…' : 'Clear selected platforms'}
        </button>
        <div className="admin-crawler-settings-note">
          Platforms {globalSourceLabel || 'none selected'} · Fetch cap {compactNumber(fetchCap)} · Winner cap {compactNumber(winnerCap)} · Concurrency {concurrencyLimit}
          {status?.autostart_enabled ? ' · Autostart on' : ' · Manual run only'}
        </div>
        <div className={`admin-crawler-state-note ${userPaused ? 'paused' : ''}`}>
          Run source: {runSourceLabel} · Backend: {backendLabel}
          {crawler?.queued ? ' · Queued' : ''}
          {crawler?.start_request_id ? ` · Request ${String(crawler.start_request_id).slice(-8)}` : ''}
          {userPaused ? ` · Paused: ${pauseMessage}` : ''}
        </div>
        <div className="admin-crawler-scheduler">
          <span className="admin-crawler-platforms-label">Daily scheduler</span>
          <label className="admin-platform-toggle">
            <input
              type="checkbox"
              checked={!!schedulerEnabled}
              disabled={isRunning || isStopping}
              onChange={e => {
                setSettingsDirty(true);
                setSchedulerEnabled(e.target.checked);
              }}
            />
            <span>Enabled</span>
          </label>
          <label className="ar-target">
            <span>Run at (hour)</span>
            <input
              type="number"
              min={0}
              max={23}
              step={1}
              value={schedulerHour}
              disabled={isRunning || isStopping}
              title="Hour in the selected timezone (0–23)."
              onChange={(e) => {
                setSettingsDirty(true);
                setSchedulerHour(sanitizeSchedulerClockInput(e.target.value, 23));
              }}
            />
          </label>
          <label className="ar-target">
            <span>Minute</span>
            <input
              type="number"
              min={0}
              max={59}
              step={1}
              value={schedulerMinute}
              disabled={isRunning || isStopping}
              title="Minute in the selected timezone (0–59)."
              onChange={(e) => {
                setSettingsDirty(true);
                setSchedulerMinute(sanitizeSchedulerClockInput(e.target.value, 59));
              }}
            />
          </label>
          <label className="ar-target">
            <span>Timezone</span>
            <select
              value={schedulerTimezone}
              disabled={isRunning || isStopping}
              title="IANA timezone for the daily run time."
              onChange={(e) => {
                setSettingsDirty(true);
                setSchedulerTimezone(e.target.value);
              }}
            >
              {schedulerTimezoneOptions.map(tz => (
                <option key={tz} value={tz}>{tz}</option>
              ))}
            </select>
          </label>
          <label className="ar-target">
            <span>Min winners / direction</span>
            <input
              type="number"
              min={1}
              max={AR_TARGET_MAX}
              step={1}
              value={schedulerMinWinners}
              disabled={isRunning || isStopping}
              title="Scheduler crawls directions below this winner count until they reach it."
              onChange={(e) => {
                setSettingsDirty(true);
                setSchedulerMinWinners(sanitizeArTargetInput(e.target.value, AR_TARGET_MAX));
              }}
            />
          </label>
          <div className="admin-crawler-settings-note">
            {userPaused
              ? `Scheduler paused by Stop · ${pauseMessage}`
              : schedulerEnabled
              ? `Runs daily at ${schedulerTimeLabel} · platforms: ${schedulerPlatformLabel} · ${compactNumber(scheduler.directions_needing_crawl ?? 0)} active directions in fresh pass`
              : 'Scheduler off — use Run Global Crawl manually'}
            {scheduler.run_in ? ` · Owner: ${scheduler.run_in}` : ''}
            {scheduler.next_run_at ? ` · Next: ${String(scheduler.next_run_at).slice(0, 19)}` : ''}
            {scheduler.last_status ? ` · Last: ${scheduler.last_status}` : ''}
          </div>
        </div>
      </div>
      {error && <div className="access-error">{error}</div>}
      {notice && !error && <div className="access-notice">{notice}</div>}
      {statusErrors.length > 0 && (
        <div className="access-error">
          Status partially loaded: {statusErrors.map(err => err.stage || err.error_type || 'section').join(', ')}
        </div>
      )}
      {loading && !status && <div className="access-loading">Loading global crawler status...</div>}
      {status && (
        <>
          <div className="cost-kpi-grid">
            <div className="cost-kpi primary"><span>Status</span><strong>{crawlState}</strong><small>{crawler.enabled === false ? 'Disabled' : 'Enabled'}</small></div>
            <div className="cost-kpi"><span>Pool Winners</span><strong>{compactNumber(totals.winners || 0)}</strong><small>{compactNumber(totals.directions_with_winners || 0)} of {compactNumber(totals.directions || directions.length)} directions</small></div>
            <div className="cost-kpi"><span>Live Run</span><strong>{compactNumber(displayWinners)}</strong><small>{compactNumber(totals.live_scored || 0)} scored · {compactNumber(totals.live_ads_checked || 0)} checked</small></div>
            <div className="cost-kpi"><span>Active Pool</span><strong>{activePool.name || `v${activePool.id}` || '—'}</strong><small>{activePool.id ? `ID ${activePool.id}` : 'Current pool'}{activePool.created_at ? ` · ${activePool.created_at.slice(0, 19)}` : ''}</small></div>
          </div>
          <section className="cost-panel">
            <div className="cost-panel-head">
              <div>
                <strong>Pool versions</strong>
                <span>Shared catalog snapshots — create fresh or switch back to an older version</span>
              </div>
              <button className="btn-secondary" type="button" onClick={loadVersions} disabled={versionsLoading}>
                {versionsLoading ? 'Refreshing…' : 'Refresh versions'}
              </button>
            </div>
            <div className="admin-pool-version-create">
              <label>
                <span>New version name</span>
                <input
                  type="text"
                  value={newVersionName}
                  placeholder="e.g. Q2 competitor refresh"
                  onChange={e => setNewVersionName(e.target.value)}
                />
              </label>
              <label>
                <span>Notes (optional)</span>
                <input
                  type="text"
                  value={newVersionNotes}
                  placeholder="Why start fresh?"
                  onChange={e => setNewVersionNotes(e.target.value)}
                />
              </label>
              <button
                className="btn-primary"
                type="button"
                onClick={createPoolVersion}
                disabled={creatingVersion || isRunning || isStopping}
              >
                {creatingVersion ? 'Creating…' : 'New pool version'}
              </button>
            </div>
            <div className="cost-table-scroll">
              <table className="cost-platform-flow-table">
                <thead>
                  <tr>
                    <th>ID</th>
                    <th>Name</th>
                    <th>Status</th>
                    <th>Evaluated</th>
                    <th>Winners</th>
                    <th>Created</th>
                    <th>Actions</th>
                  </tr>
                </thead>
                <tbody>
                  {versions.length === 0 && (
                    <tr><td colSpan={7}>{versionsLoading ? 'Loading versions…' : 'No pool versions yet.'}</td></tr>
                  )}
                  {versions.map(version => (
                    <tr
                      key={version.id}
                      className={version.status === 'active' ? 'pool-version-active' : ''}
                    >
                      <td>
                        <button
                          type="button"
                          className="linkish"
                          onClick={() => {
                            setSelectedVersionId(version.id);
                            if (version.status !== 'active') activatePoolVersion(version.id);
                          }}
                          title={version.status === 'active' ? 'Browse this version' : 'Activate and browse'}
                        >
                          {version.id}
                        </button>
                      </td>
                      <td>
                        {editingVersionId === version.id ? (
                          <span className="inline-edit-row">
                            <input
                              type="text"
                              value={editingVersionName}
                              onChange={e => setEditingVersionName(e.target.value)}
                            />
                            <button type="button" className="btn-secondary" onClick={() => saveVersionName(version.id)} disabled={versionActionId === version.id}>Save</button>
                            <button type="button" className="btn-secondary" onClick={() => { setEditingVersionId(null); setEditingVersionName(''); }}>Cancel</button>
                          </span>
                        ) : (
                          <>
                            {version.name || '—'}
                            <button
                              type="button"
                              className="linkish admin-inline-action"
                              onClick={() => {
                                setEditingVersionId(version.id);
                                setEditingVersionName(version.name || '');
                              }}
                            >
                              rename
                            </button>
                          </>
                        )}
                      </td>
                      <td>{version.status}</td>
                      <td>{compactNumber(version.evaluated_count || 0)}</td>
                      <td>{compactNumber(version.winner_count || 0)}</td>
                      <td>{version.created_at ? version.created_at.slice(0, 19) : '—'}</td>
                      <td>
                        {version.status === 'active' ? (
                          <button type="button" className="btn-secondary" onClick={() => setSelectedVersionId(version.id)}>Browse</button>
                        ) : (
                          <button
                            type="button"
                            className="btn-secondary"
                            onClick={() => activatePoolVersion(version.id)}
                            disabled={versionActionId === version.id || isRunning || isStopping}
                          >
                            {versionActionId === version.id ? 'Switching…' : 'Activate'}
                          </button>
                        )}
                      </td>
                    </tr>
                  ))}
                </tbody>
              </table>
            </div>
          </section>
          {hasLiveProgress && (
            <CrawlProgressView
              progress={enrichedLiveProgress}
              running={isRunning || isStopping}
              statusText={progressStatusText}
              runKind={isScheduledRun ? 'scheduler' : 'manual'}
              className="admin-crawler-live"
            />
          )}
          <section className="cost-panel">
            <div className="cost-panel-head">
              <div>
                <strong>Shared pool winners (persisted)</strong>
                <span>{directions.length} active directions · live columns are current run, pool columns are global totals</span>
              </div>
            </div>
            <div className="cost-table-scroll">
              <table className="cost-platform-flow-table">
                <thead>
                  <tr>
                    <th>Direction</th>
                    <th>Live status</th>
                    <th>Checked</th>
                    <th>Scored</th>
                    <th>Live winners</th>
                    {LIVE_SOURCES.map(platform => <th key={`live-${platform}`}>{PLATFORMS[platform]?.label || platform} (live)</th>)}
                    {LIVE_SOURCES.map(platform => <th key={platform}>{PLATFORMS[platform]?.label || platform} (pool)</th>)}
                    <th>Pool total</th>
                    <th>Last scored</th>
                  </tr>
                </thead>
                <tbody>
                  {directions.length === 0 && (
                    <tr><td colSpan={(LIVE_SOURCES.length * 2) + 7}>No global crawler directions yet.</td></tr>
                  )}
                  {directions.map(direction => {
                    const floorBasisWinners = Number(direction.live_winners || 0);
                    const belowFloor = Number.isFinite(currentRunWinnerFloor)
                      && floorBasisWinners < currentRunWinnerFloor;
                    const floorBadgeTitle = `Below current-run floor (${currentRunWinnerFloor} winners); global totals are shown separately`;
                    return (
                    <tr
                      key={direction.direction_id}
                      className={belowFloor ? 'pool-direction-below-floor' : ''}
                    >
                      <td>
                        {direction.direction_name || direction.direction_id}
                        {belowFloor ? (
                          <span className="pool-floor-badge" title={floorBadgeTitle}>
                            Below floor
                          </span>
                        ) : null}
                      </td>
                      <td>{direction.live_status || 'pending'}</td>
                      <td>{compactNumber(direction.ads_checked || 0)}</td>
                      <td>{compactNumber(direction.scored || 0)}</td>
                      <td>{compactNumber(direction.live_winners || 0)}</td>
                      {LIVE_SOURCES.map(platform => (
                        <td key={`live-${platform}`}>
                          {compactNumber(
                            direction.progress?.[platform]?.visible_winners
                            ?? direction.progress?.[platform]?.winners
                            ?? 0,
                          )}
                        </td>
                      ))}
                      {LIVE_SOURCES.map(platform => (
                        <td key={`pool-${platform}`}>{compactNumber(direction.platforms?.[platform] || 0)}</td>
                      ))}
                      <td><strong>{compactNumber(direction.total_winners || 0)}</strong></td>
                      <td>{direction.last_scored_at ? direction.last_scored_at.slice(0, 19) : '—'}</td>
                    </tr>
                    );
                  })}
                  {directions.length > 0 && (
                    <tr className="pool-direction-totals-row">
                      <td><strong>Total</strong></td>
                      <td>—</td>
                      <td>—</td>
                      <td>—</td>
                      <td><strong>{compactNumber(liveDirectionWinnerTotals.grand || totals.live_winners || 0)}</strong></td>
                      {LIVE_SOURCES.map(platform => (
                        <td key={`live-total-${platform}`}>
                          <strong>{compactNumber(liveDirectionWinnerTotals.byPlatform[platform] || 0)}</strong>
                        </td>
                      ))}
                      {LIVE_SOURCES.map(platform => (
                        <td key={`pool-total-${platform}`}>
                          <strong>{compactNumber(directionWinnerTotals.byPlatform[platform] || 0)}</strong>
                          <div className="pool-platform-subtotal">
                            {compactNumber(platformTotals[platform]?.evaluated || 0)} ads
                          </div>
                        </td>
                      ))}
                      <td>
                        <strong>{compactNumber(directionWinnerTotals.grand || totals.winners || 0)}</strong>
                        <div className="pool-platform-subtotal">
                          {compactNumber(poolPlatformSummary.evaluated || totals.evaluated || 0)} ads
                        </div>
                      </td>
                      <td>—</td>
                    </tr>
                  )}
                </tbody>
              </table>
            </div>
          </section>
          <section className="cost-panel">
            <div className="cost-panel-head">
              <div>
                <strong>Pool ads browser</strong>
                <span>
                  {selectedVersionId
                    ? `Version ${selectedVersionId} · ${compactNumber(poolAdsTotal)} total`
                    : 'Select a version to browse'}
                  {selectedVersionId && poolAdsTotal
                    ? ` · showing ${poolAdsPageStart}–${poolAdsPageEnd}`
                    : ''}
                </span>
              </div>
              <label className="admin-pool-ads-filter">
                <span>Platform</span>
                <select
                  value={poolAdsPlatform}
                  onChange={e => {
                    setPoolAdsPlatform(e.target.value);
                    setPoolAdsPage(0);
                  }}
                  disabled={poolAdsLoading || !selectedVersionId}
                >
                  <option value="">All platforms</option>
                  {LIVE_SOURCES.map(platform => (
                    <option key={platform} value={platform}>
                      {PLATFORMS[platform]?.label || platform}
                    </option>
                  ))}
                </select>
              </label>
              <label className="admin-pool-ads-filter">
                <input
                  type="checkbox"
                  checked={poolAdsWinnersOnly}
                  onChange={e => setPoolAdsWinnersOnly(e.target.checked)}
                />
                Winners only
              </label>
              <button
                type="button"
                className="btn-secondary"
                onClick={() => loadPoolAds(selectedVersionId, poolAdsPage)}
                disabled={poolAdsLoading || !selectedVersionId}
              >
                {poolAdsLoading ? 'Loading…' : 'Refresh ads'}
              </button>
            </div>
            <div className="cost-table-scroll">
              <table className="cost-platform-flow-table">
                <thead>
                  <tr>
                    <th>Ad</th>
                    <th>Direction</th>
                    <th>Platform</th>
                    <th>Score</th>
                    <th>Reason</th>
                    <th>Pass</th>
                    <th>Winner</th>
                    <th>Evaluated</th>
                    <th>Video</th>
                  </tr>
                </thead>
                <tbody>
                  {!selectedVersionId && (
                    <tr><td colSpan={9}>Select a pool version above.</td></tr>
                  )}
                  {selectedVersionId && poolAdsLoading && poolAds.length === 0 && (
                    <tr><td colSpan={9}>Loading pool ads…</td></tr>
                  )}
                  {selectedVersionId && !poolAdsLoading && poolAds.length === 0 && (
                    <tr><td colSpan={9}>No ads in this view yet.</td></tr>
                  )}
                  {poolAds.map(ad => {
                    const videoUrl = poolAdVideoUrl(ad);
                    const landingUrl = poolAdLandingUrl(ad);
                    const reasonSnippet = poolAdReasonSnippet(ad);
                    return (
                    <tr key={`${ad.ad_id}-${ad.direction_id}`}>
                      <td className="mono">{ad.ad_id}</td>
                      <td>{ad.direction_id}</td>
                      <td>{PLATFORMS[ad.source_platform]?.label || ad.source_platform}</td>
                      <td>{ad.score != null ? Number(ad.score).toFixed(1) : '—'}</td>
                      <td className="admin-pool-ad-reason" title={ad.reasoning || undefined}>
                        {reasonSnippet || '—'}
                      </td>
                      <td>{ad.passes ? 'yes' : 'no'}</td>
                      <td>{ad.is_winner ? 'yes' : '—'}</td>
                      <td>{ad.evaluated_at ? ad.evaluated_at.slice(0, 19) : '—'}</td>
                      <td className="admin-pool-ad-links">
                        {videoUrl ? (
                          <a
                            href={videoUrl}
                            target="_blank"
                            rel="noopener noreferrer"
                            className="linkish admin-pool-ad-link"
                          >
                            Watch video
                          </a>
                        ) : '—'}
                        {landingUrl && landingUrl !== videoUrl ? (
                          <a
                            href={landingUrl}
                            target="_blank"
                            rel="noopener noreferrer"
                            className="linkish admin-pool-ad-link"
                          >
                            Landing
                          </a>
                        ) : null}
                      </td>
                    </tr>
                    );
                  })}
                </tbody>
              </table>
            </div>
            {selectedVersionId && poolAdsTotal > POOL_ADS_PAGE_SIZE && (
              <div className="admin-pool-ads-pagination">
                <button
                  type="button"
                  className="btn-secondary"
                  disabled={poolAdsLoading || poolAdsPage <= 0}
                  onClick={() => setPoolAdsPage(page => Math.max(0, page - 1))}
                >
                  Previous
                </button>
                <span className="admin-pool-ads-page-label">
                  Page {poolAdsPage + 1} of {poolAdsPageCount}
                </span>
                <button
                  type="button"
                  className="btn-secondary"
                  disabled={poolAdsLoading || poolAdsPage + 1 >= poolAdsPageCount}
                  onClick={() => setPoolAdsPage(page => page + 1)}
                >
                  Next
                </button>
              </div>
            )}
          </section>
        </>
      )}
    </section>
  );
}

function AdminWorkspaceMirror() {
  const [users, setUsers] = useState([]);
  const [selectedId, setSelectedId] = useState(null);
  const [workspace, setWorkspace] = useState(null);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState('');

  const loadUsers = async () => {
    setLoading(true);
    setError('');
    try {
      const data = await RESILIA_API.adminListWorkspaces();
      const rows = data.workspaces || [];
      setUsers(rows);
      if (!selectedId && rows.length) setSelectedId(rows[0].user_id);
    } catch (e) {
      setError(e.message || 'User work failed to load');
    } finally {
      setLoading(false);
    }
  };

  useEffect(() => { loadUsers(); }, []);
  useEffect(() => {
    if (!selectedId) return;
    let cancelled = false;
    (async () => {
      setError('');
      try {
        const data = await RESILIA_API.adminGetWorkspace(selectedId);
        if (!cancelled) setWorkspace(data);
      } catch (e) {
        if (!cancelled) setError(e.message || 'Workspace failed to load');
      }
    })();
    return () => { cancelled = true; };
  }, [selectedId]);

  const renderAds = (title, ads) => (
    <section className="admin-work-card">
      <header><strong>{title}</strong><span>{(ads || []).length}</span></header>
      {(ads || []).length === 0 ? <div className="admin-work-empty">No rows</div> : (
        <div className="admin-work-ads">
          {(ads || []).slice(0, 12).map(ad => (
            <div className="admin-work-ad" key={`${title}-${ad.ad_id || ad.id}`}>
              <span>{ad.brand || ad.ad_id || ad.id}</span>
              <small>{ad.direction_id || 'manual'} · {ad.source_platform || ad.platform || 'source'} · {typeof ad.score === 'number' ? ad.score.toFixed(1) : '—'}</small>
            </div>
          ))}
        </div>
      )}
    </section>
  );

  return (
    <section className="admin-grid-view">
      <div className="content-hdr">
        <div>
          <div className="content-title">User Work Viewer</div>
          <div className="content-sub">{users.length} saved workspace{users.length === 1 ? '' : 's'} · latest Manual Search, AutoSearch, and Auto-Remodel snapshots</div>
        </div>
        <div className="spacer" />
        <button className="btn-secondary" onClick={loadUsers}><Icon name="loader" size={14} /> Refresh</button>
      </div>
      {error && <div className="access-error">{error}</div>}
      <div className="admin-grid workspaces">
        <aside className="admin-list">
          {loading && <div className="access-loading">Loading...</div>}
          {users.map(user => (
            <button key={user.user_id} className={`admin-list-row ${selectedId === user.user_id ? 'active' : ''}`} onClick={() => setSelectedId(user.user_id)}>
              <span>{user.email || user.user_id}</span>
              <small>{user.search_count} search · {user.autoresearch_count} autoresearch · {user.auto_remodel_count} auto</small>
            </button>
          ))}
        </aside>
        <section className="admin-workspace-detail">
          {!workspace ? <div className="empty"><span className="empty-display">No user selected</span></div> : (
            <>
              <div className="admin-work-summary">
                <div><span>Manual Search</span><strong>{workspace.search_run?.result_count || 0}</strong></div>
                <div><span>AutoSearch</span><strong>{workspace.autoresearch_run?.result_count || 0}</strong></div>
                <div><span>Auto-Remodel</span><strong>{workspace.auto_remodel_run?.result_count || 0}</strong></div>
                <div><span>Filter</span><strong>{workspace.filter_count || 0}</strong></div>
                <div><span>Remodel</span><strong>{workspace.remodel_count || 0}</strong></div>
              </div>
              <div className="admin-work-sections">
                {renderAds('Manual Search', workspace.search_ads)}
                {renderAds('AutoSearch', workspace.autoresearch_ads)}
                {renderAds('Auto-Remodel', workspace.auto_remodel_ads)}
              </div>
            </>
          )}
        </section>
      </div>
    </section>
  );
}

function AccessRequestsAdminView({ onPendingCountChange }) {
  const filters = ['pending', 'approved', 'rejected', 'all'];
  const [status, setStatus] = useState('pending');
  const [rows, setRows] = useState([]);
  const [counts, setCounts] = useState({ pending: 0, approved: 0, rejected: 0, all: 0 });
  const [notes, setNotes] = useState({});
  const [grantAdminMap, setGrantAdminMap] = useState({});
  const [busyId, setBusyId] = useState(null);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState('');

  const load = async () => {
    setLoading(true);
    setError('');
    try {
      const data = await RESILIA_API.listAccessRequests(status);
      const nextCounts = data.counts || { pending: 0, approved: 0, rejected: 0, all: 0 };
      setRows(Array.isArray(data.requests) ? data.requests : []);
      setCounts(nextCounts);
      onPendingCountChange(nextCounts.pending || 0);
    } catch (e) {
      setError(e.message || 'Failed to load access requests');
    } finally {
      setLoading(false);
    }
  };

  useEffect(() => { load(); }, [status]);

  const formatDate = (value) => {
    if (!value) return 'Unknown';
    const date = new Date(value);
    return Number.isNaN(date.getTime()) ? value : date.toLocaleString();
  };

  const review = async (row, action) => {
    setBusyId(row.id);
    setError('');
    try {
      const note = notes[row.id] || '';
      if (action === 'approve') {
        await RESILIA_API.approveAccessRequest(row.id, note, { grantAdmin: !!grantAdminMap[row.id] });
      } else {
        await RESILIA_API.rejectAccessRequest(row.id, note);
      }
      setNotes(current => ({ ...current, [row.id]: '' }));
      setGrantAdminMap(current => ({ ...current, [row.id]: false }));
      await load();
    } catch (e) {
      setError(e.message || `Failed to ${action} request`);
    } finally {
      setBusyId(null);
    }
  };

  return (
    <section className="access-admin">
      <div className="content-hdr">
        <div>
          <div className="content-title">Access Requests</div>
          <div className="content-sub">{counts.pending || 0} pending</div>
        </div>
        <div className="spacer" />
        <div className="access-filters" role="tablist" aria-label="Access request filters">
          {filters.map(key => (
            <button
              key={key}
              className={`access-filter ${status === key ? 'active' : ''}`}
              onClick={() => setStatus(key)}
            >
              {key}
              <span>{counts[key] || 0}</span>
            </button>
          ))}
        </div>
      </div>

      {error && <div className="access-error">{error}</div>}
      {loading && <div className="access-loading">Loading requests...</div>}
      {!loading && rows.length === 0 && (
        <div className="empty">
          <div className="empty-display">No requests</div>
          <p className="empty-body">The selected queue is clear.</p>
        </div>
      )}
      {!loading && rows.length > 0 && (
        <div className="access-table">
          {rows.map(row => (
            <article className="access-row" key={row.id}>
              <div className="access-main">
                <div className="access-name-line">
                  <strong>{row.name}</strong>
                  <span className={`status-pill ${row.status}`}>{row.status}</span>
                </div>
                <div className="access-meta">
                  <span>{row.email}</span>
                  <span>{row.role}</span>
                  <span>{formatDate(row.requested_at)}</span>
                  {row.company_team && <span>{row.company_team}</span>}
                </div>
                <p className="access-reason">{row.reason}</p>
                {row.notes && <p className="access-notes">Notes: {row.notes}</p>}
              </div>
              {row.status === 'pending' && (
                <div className="access-actions">
                  <textarea
                    value={notes[row.id] || ''}
                    onChange={(event) => setNotes(current => ({ ...current, [row.id]: event.target.value }))}
                    placeholder="Optional notes"
                  />
                  <label className="access-grant-admin" style={{ display: 'flex', alignItems: 'center', gap: 6, fontSize: 12 }}>
                    <input
                      type="checkbox"
                      checked={!!grantAdminMap[row.id]}
                      onChange={(event) => setGrantAdminMap(current => ({ ...current, [row.id]: event.target.checked }))}
                    />
                    Grant admin access on approval
                  </label>
                  <div className="access-action-buttons">
                    <button
                      className="btn-primary"
                      disabled={busyId === row.id}
                      onClick={() => review(row, 'approve')}
                    >
                      {grantAdminMap[row.id] ? 'Approve as admin' : 'Approve'}
                    </button>
                    <button
                      className="btn-secondary danger"
                      disabled={busyId === row.id}
                      onClick={() => review(row, 'reject')}
                    >
                      Reject
                    </button>
                  </div>
                </div>
              )}
            </article>
          ))}
        </div>
      )}
    </section>
  );
}

function AdminUsersView() {
  const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
  const displayName = (user) => {
    const name = (user.username || '').trim();
    if (!name || UUID_RE.test(name)) {
      return (user.email || '').split('@')[0] || user.email || 'Unknown user';
    }
    return name;
  };
  const [users, setUsers] = useState([]);
  const [loading, setLoading] = useState(false);
  const [busyEmail, setBusyEmail] = useState(null);
  const [error, setError] = useState('');
  const [filter, setFilter] = useState('');

  const load = async () => {
    setLoading(true);
    setError('');
    try {
      const data = await RESILIA_API.listAdminUsers();
      setUsers(Array.isArray(data.users) ? data.users : []);
    } catch (e) {
      setError(e.message || 'Failed to load users');
    } finally {
      setLoading(false);
    }
  };

  useEffect(() => { load(); }, []);

  const formatDate = (value) => {
    if (!value) return '—';
    const date = new Date(value);
    return Number.isNaN(date.getTime()) ? value : date.toLocaleString();
  };

  const toggleAdmin = async (user, nextValue) => {
    if (user.admin_locked) return;
    setBusyEmail(user.email);
    setError('');
    try {
      await RESILIA_API.setAdminStatus(user.email, nextValue);
      await load();
    } catch (e) {
      setError(e.message || 'Failed to update admin status');
    } finally {
      setBusyEmail(null);
    }
  };

  const visible = filter
    ? users.filter(u => {
        const needle = filter.toLowerCase();
        return (u.email || '').toLowerCase().includes(needle)
          || displayName(u).toLowerCase().includes(needle);
      })
    : users;
  const adminCount = users.filter(u => u.is_admin).length;

  return (
    <section className="access-admin">
      <div className="content-hdr">
        <div>
          <div className="content-title">User Manager</div>
          <div className="content-sub">{users.length} known users · {adminCount} admin</div>
        </div>
        <div className="spacer" />
        <input
          className="input"
          placeholder="Filter by email or name"
          value={filter}
          onChange={(e) => setFilter(e.target.value)}
          style={{ minWidth: 220 }}
        />
        <button className="btn-secondary" onClick={load} disabled={loading} style={{ marginLeft: 8 }}>
          {loading ? 'Refreshing…' : 'Refresh'}
        </button>
      </div>

      {error && <div className="access-error">{error}</div>}
      {loading && users.length === 0 && <div className="access-loading">Loading users…</div>}
      {!loading && visible.length === 0 && (
        <div className="empty">
          <div className="empty-display">No users</div>
          <p className="empty-body">No users match the current filter.</p>
        </div>
      )}
      {visible.length > 0 && (
        <div className="access-table">
          {visible.map(user => {
            const busy = busyEmail === user.email;
            return (
              <article className="access-row" key={user.email}>
                <div className="access-main">
                  <div className="access-name-line">
                    <strong>{displayName(user)}</strong>
                    {user.is_admin && (
                      <span className={`status-pill approved`} title={user.admin_source === 'config' ? 'Configured in settings — cannot be revoked here' : 'Granted from this dashboard'}>
                        admin{user.admin_source === 'config' ? ' · locked' : ''}
                      </span>
                    )}
                    {!user.is_admin && <span className="status-pill pending">user</span>}
                  </div>
                  <div className="access-meta">
                    <span>{user.email}</span>
                    <span>Last seen: {formatDate(user.last_seen_at)}</span>
                    {user.granted_by && <span>Granted by: {user.granted_by}</span>}
                  </div>
                </div>
                <div className="access-actions">
                  <div className="access-action-buttons">
                    {user.is_admin ? (
                      <button
                        className="btn-secondary danger"
                        disabled={busy || user.admin_locked}
                        title={user.admin_locked ? 'Configured in settings — edit settings.yaml to revoke' : 'Revoke admin access'}
                        onClick={() => toggleAdmin(user, false)}
                      >
                        {user.admin_locked ? 'Locked' : (busy ? 'Revoking…' : 'Revoke admin')}
                      </button>
                    ) : (
                      <button
                        className="btn-primary"
                        disabled={busy}
                        onClick={() => toggleAdmin(user, true)}
                      >
                        {busy ? 'Granting…' : 'Make admin'}
                      </button>
                    )}
                  </div>
                </div>
              </article>
            );
          })}
        </div>
      )}
    </section>
  );
}

function ResearchView({
  mode = 'manual',
  rows, setRows, competitors,
  ads, searching, searchCacheLoading, searchingRowIds, sel, onToggle, onSearch, onSearchRow, onStopSearch, onExpandAd, onDislikeAd, density, minimal, groupByPlatform,
  searchWarnings, latestSearchRun, latestAutoresearchRun,
  searchPassesOnly = true, setSearchPassesOnly = () => {},
  dedupeSettings = DEDUPE_DEFAULTS, dedupeSaving = false, onDedupeChange,
  arRunning, arProgress, arSources, arFilters, setArFilters,
  onToggleArSource, onSelectAllArSources,
  arTarget, setArTarget, onRunAutoresearch, onStopAutoresearch,
  arMaxAdsPerDirection, setArMaxAdsPerDirection,
  arMaxAdsPerPlatform, setArMaxAdsPerPlatform,
  arNumDirections, setArNumDirections,
  directions, arPickedDirections, onToggleArPickedDirection, onClearArPickedDirections,
  onSelectAllArPickedDirections,
  autoresearchPage, onLoadMoreAutoresearch,
  startupLoading = false,
}) {
  const updateRow = (id, patch) => setRows(rows.map(r => r.id === id ? patch : r));
  const removeRow = (id) => setRows(rows.filter(r => r.id !== id));
  const addRow = () => setRows([...rows, {
    id: Date.now(), query: '', competitorId: null, count: 20,
    filters: defaultSearchFilters(),
    platforms: sourceState(),
  }]);

  const searchAds = ads.filter(a => a.source !== 'autoresearch');
  const autoAds = ads.filter(a => a.source === 'autoresearch');
  const currentSearchAds = uniqueSearchAds(searchAds.filter(a => a.currentSearchRun));
  const previousSearchAds = uniqueSearchAds(searchAds.filter(a => !a.currentSearchRun));
  const currentAutoAds = uniqueAutoresearchAds(autoAds.filter(a => a.currentAutoresearchRun));
  const previousAutoAds = uniqueAutoresearchAds(autoAds.filter(a => !a.currentAutoresearchRun));
  // Bug 15 — Manual Search defaults to "passing only" (configured score threshold / passes
  // flag from the server) so low-score ads do not clutter the list. Users
  // can flip the toggle off for prompt calibration / scorer sanity-checks.
  const autoFailSampleSeedRef = React.useRef(`${Date.now()}:${Math.random()}`);
  const isPassing = React.useCallback((a) => {
    return adMeetsPassThreshold(a, false);
  }, []);
  const visibleAutoFailKeys = React.useMemo(() => {
    if (searchPassesOnly) return new Set();
    const failing = autoAds.filter(ad => !isPassing(ad));
    if (failing.length <= AUTORESEARCH_FAIL_DISPLAY_LIMIT) {
      return new Set(failing.map(ad => resultKey(ad)));
    }
    const seed = autoFailSampleSeedRef.current;
    return new Set(
      [...failing]
        .sort((a, b) => (
          stableRandomRank(resultKey(a), seed) - stableRandomRank(resultKey(b), seed)
        ))
        .slice(0, AUTORESEARCH_FAIL_DISPLAY_LIMIT)
        .map(ad => resultKey(ad)),
    );
  }, [autoAds, searchPassesOnly, isPassing]);
  const passingSearchAds = React.useMemo(() => {
    if (!searchPassesOnly) return searchAds;
    return searchAds.filter(isPassing);
  }, [searchAds, searchPassesOnly, isPassing]);
  // Same passes-only filter applied to Autoresearch winners so the toggle
  // affects every result panel on the Research tab in one click.
  const passingAutoAds = React.useMemo(() => {
    if (!searchPassesOnly) {
      return autoAds.filter(ad => isPassing(ad) || visibleAutoFailKeys.has(resultKey(ad)));
    }
    return autoAds.filter(isPassing);
  }, [autoAds, searchPassesOnly, isPassing, visibleAutoFailKeys]);
  const passingCurrentAutoAds = React.useMemo(() => {
    if (!searchPassesOnly) {
      return currentAutoAds.filter(ad => isPassing(ad) || visibleAutoFailKeys.has(resultKey(ad)));
    }
    return currentAutoAds.filter(isPassing);
  }, [currentAutoAds, searchPassesOnly, isPassing, visibleAutoFailKeys]);
  const passingPreviousAutoAds = React.useMemo(() => {
    if (!searchPassesOnly) {
      return previousAutoAds.filter(ad => isPassing(ad) || visibleAutoFailKeys.has(resultKey(ad)));
    }
    return previousAutoAds.filter(isPassing);
  }, [previousAutoAds, searchPassesOnly, isPassing, visibleAutoFailKeys]);
  const passingCurrentSearchAds = React.useMemo(() => {
    if (!searchPassesOnly) return currentSearchAds;
    return currentSearchAds.filter(isPassing);
  }, [currentSearchAds, searchPassesOnly, isPassing]);
  const passingPreviousSearchAds = React.useMemo(() => {
    if (!searchPassesOnly) return previousSearchAds;
    return previousSearchAds.filter(isPassing);
  }, [previousSearchAds, searchPassesOnly, isPassing]);
  const currentSearchHiddenCount = Math.max(0, currentSearchAds.length - passingCurrentSearchAds.length);
  const previousSearchHiddenCount = Math.max(0, previousSearchAds.length - passingPreviousSearchAds.length);
  const currentAutoHiddenCount = Math.max(0, currentAutoAds.length - passingCurrentAutoAds.length);
  const previousAutoHiddenCount = Math.max(0, previousAutoAds.length - passingPreviousAutoAds.length);
  const autoFailDisplayHiddenCount = !searchPassesOnly
    ? Math.max(0, autoAds.filter(ad => !isPassing(ad)).length - visibleAutoFailKeys.size)
    : 0;
  const hiddenLowScoreCount =
    currentSearchHiddenCount +
    previousSearchHiddenCount +
    currentAutoHiddenCount +
    previousAutoHiddenCount;
  const currentSearchGrouped = groupByPlatform(passingCurrentSearchAds);
  const previousSearchGrouped = groupByPlatform(passingPreviousSearchAds);
  const currentAutoGrouped = groupByPlatform(passingCurrentAutoAds);
  const previousAutoGrouped = groupByPlatform(passingPreviousAutoAds);
  const currentSearchPlatforms = Object.keys(currentSearchGrouped).filter(p => currentSearchGrouped[p].length > 0);
  const previousSearchPlatforms = Object.keys(previousSearchGrouped).filter(p => previousSearchGrouped[p].length > 0);
  const currentAutoPlatforms = Object.keys(currentAutoGrouped).filter(p => currentAutoGrouped[p].length > 0);
  const previousAutoPlatforms = Object.keys(previousAutoGrouped).filter(p => previousAutoGrouped[p].length > 0);
  const visibleSearchPlatformCount = currentSearchPlatforms.length + previousSearchPlatforms.length;
  const visibleAutoPlatformCount = currentAutoPlatforms.length + previousAutoPlatforms.length;
  const showAutoLoadMore = searchPassesOnly
    && autoresearchPage?.hasMore
    && (currentAutoPlatforms.length > 0 || previousAutoPlatforms.length > 0);
  const searchProgressEntries = React.useMemo(() => {
    const progress = latestSearchRun?.progress;
    if (!progress || typeof progress !== 'object') return [];
    const order = ['foreplay', 'adspy', 'brandsearch'];
    const keys = order.filter(key => progress[key]);
    const extras = Object.keys(progress).filter(key => !order.includes(key));
    return [...keys, ...extras].map(key => progress[key]).filter(Boolean);
  }, [latestSearchRun]);
  const searchProgressTerminal = searchProgressEntries.length > 0
    && searchProgressEntries.every(entry => (
      ['target_met', 'exhausted', 'depth_reached', 'done', 'no_new_winners'].includes(entry.status)
    ));
  const searchRunActive = Boolean(searching || (searchingRowIds?.size || 0) > 0);
  const searchPrefillReady = latestSearchRun?.searchPhase === 'shared_pool_ready' && searchProgressTerminal;
  const searchLiveProgressActive = searchProgressEntries.some(entry => {
    const stage = String(entry?.stage || '');
    const status = String(entry?.status || '');
    if (status !== 'running') return false;
    return ['fetching', 'processing', 'scoring', 'deduping_results', 'scored'].includes(stage)
      || (entry?.unit !== 'shared_pool' && stage !== 'shared_pool_prefill');
  });
  const searchPhase = latestSearchRun?.searchPhase || null;
  const searchSharedPoolActive = searchRunActive
    && !searchPrefillReady
    && !searchLiveProgressActive
    && searchPhase !== 'live_search'
    && (
      searchPhase === 'prefilling_shared_pool'
      || searchProgressEntries.some(entry => (
        entry.stage === 'shared_pool_prefill' && entry.status === 'running'
      ))
    );
  const searchVisuallyComplete = !searchRunActive;
  const searchProgressStatusLabel = !searchRunActive
    ? 'Manual search complete'
    : searchSharedPoolActive
      ? 'Fetching from shared pool…'
      : searchPrefillReady
        ? 'Finalizing manual search…'
      : 'Manual search running…';
  const progressText = (entry) => {
    const label = String(entry.source || 'platform').toUpperCase();
    const kept = Number(entry.kept || 0);
    const target = Number(entry.target || 0);
    const totalKept = Number(entry.total_kept || 0);
    const totalSuffix = totalKept > 0 && target > 0 ? ` (${totalKept}/${target} total)` : '';
    const targetLabel = entry.target_kind === 'winning' ? 'winning' : 'unique';
    const stageLabel = entry.stage
      ? String(entry.stage).replace(/_/g, ' ')
      : '';
    const statusLabel = entry.status === 'queued'
      ? 'waiting'
      : entry.status === 'exhausted'
      ? 'source exhausted'
      : entry.status === 'target_met'
          ? 'target met'
          : '';
    if (entry.unit === 'shared_pool') {
      const poolLabel = entry.status === 'target_met'
        ? 'filled from shared pool'
        : entry.status === 'running'
          ? 'fetching from shared pool'
          : 'shared pool';
      return `${label}: ${kept}/${target} ${targetLabel}${totalSuffix} · ${poolLabel}${statusLabel ? ` · ${statusLabel}` : ''}`;
    }
    if (entry.unit === 'ads') {
      const checked = Number(entry.ads_checked || 0);
      return `${label}: ${checked} ads checked · ${kept}/${target} ${targetLabel}${totalSuffix}${stageLabel ? ` · ${stageLabel}` : ''}${statusLabel ? ` · ${statusLabel}` : ''}`;
    }
    const pages = Number(entry.pages_fetched || 0);
    const rawSeen = Number(entry.raw_ads_seen || 0);
    return `${label}: ${pages} pages · ${rawSeen} ads checked · ${kept}/${target} ${targetLabel}${totalSuffix}${stageLabel ? ` · ${stageLabel}` : ''}${statusLabel ? ` · ${statusLabel}` : ''}`;
  };
  const searchProgressRatio = (entry) => {
    if (!entry || typeof entry !== 'object') return null;
    if (entry.status === 'queued') return 0;
    if (entry.unit === 'shared_pool' && entry.status === 'running') {
      const kept = Number(entry.kept || 0);
      const target = Number(entry.target || 0);
      if (target > 0 && kept > 0) return Math.min(1, kept / target);
      return 0.12;
    }
    if (['done', 'target_met', 'exhausted', 'depth_reached'].includes(entry.status)) return 1;
    const kept = Number(entry.kept || 0);
    const target = Number(entry.target || 0);
    if (target > 0 && kept > 0) return Math.min(1, kept / target);
    return null;
  };
  const searchProgressPct = (() => {
    if (!searchProgressEntries.length) return searchRunActive && latestSearchRun ? 8 : null;
    if (!searchRunActive) return 100;
    if (searchPrefillReady) return 95;
    const ratios = searchProgressEntries
      .map(searchProgressRatio)
      .filter(value => Number.isFinite(value));
    if (!ratios.length) return 8;
    const average = ratios.reduce((sum, value) => sum + value, 0) / ratios.length;
    return Math.max(8, Math.min(95, Math.round(average * 100)));
  })();
  const showSearchLoading = (startupLoading || searchRunActive || searchCacheLoading) && visibleSearchPlatformCount === 0;
  const showAutoresearchLoading = (
    startupLoading ||
    autoresearchPage?.loading ||
    arRunning
  ) && visibleAutoPlatformCount === 0;
  const showManual = mode !== 'autoSearch';
  const showAuto = mode === 'autoSearch';

  return (
    <>
      {showManual && (
        <>
      <div className="search-card">
        <div className="sc-hdr">
          <span className="sc-title">Manual Search</span>
          <span className="sc-sub">Multi-row keyword or competitor-scoped search · Foreplay default, AdSpy and BrandSearch selectable</span>
          <div className="sc-actions">
            <label
              title="Controls whether a transcript can repeat inside the same search area, platform, or anywhere for your user."
              style={{ display: 'inline-flex', alignItems: 'center', gap: 6, fontSize: 12, color: 'var(--fg-2)' }}
            >
              Dedupe
              <select
                value={dedupeSettings.scope_mode || DEDUPE_DEFAULTS.scope_mode}
                disabled={dedupeSaving}
                onChange={(e) => onDedupeChange && onDedupeChange({ scope_mode: e.target.value })}
              >
                {Object.entries(DEDUPE_SCOPE_LABELS).map(([value, label]) => (
                  <option key={value} value={value}>{label}</option>
                ))}
              </select>
            </label>
            <label
              className="sc-passes-toggle"
              title="Hide ads scored below the configured winner threshold. Toggle off to see the full Haiku score distribution, including dedupe history."
              onClick={(e) => e.stopPropagation()}
              style={{
                display: 'inline-flex',
                alignItems: 'center',
                gap: 6,
                fontSize: 12,
                color: 'var(--fg-2)',
                userSelect: 'none',
                cursor: 'pointer',
              }}
            >
              <input
                type="checkbox"
                checked={searchPassesOnly}
                onClick={(e) => e.stopPropagation()}
                onChange={(e) => {
                  e.stopPropagation();
                  setSearchPassesOnly(e.target.checked);
                }}
              />
              Show only passing ads
              {searchPassesOnly && hiddenLowScoreCount > 0 && (
                <span
                  style={{ color: 'var(--fg-3)' }}
                  title={`${hiddenLowScoreCount} below-threshold ad(s) hidden across Manual Search and AutoSearch`}
                >
                  · {hiddenLowScoreCount} hidden
                </span>
              )}
            </label>
            {searching ? (
              <button type="button" className="btn-secondary" onClick={onStopSearch}>
                <Icon name="x" size={14} /> Stop Search
              </button>
            ) : (
              <button type="button" className="btn-primary" onClick={onSearch}>
                <Icon name="search" size={14} />Search
              </button>
            )}
          </div>
        </div>
        <div className="search-rows">
          {rows.map(r => (
            <SearchRow
              key={r.id}
              row={r}
              competitors={competitors}
              onChange={(patch) => updateRow(r.id, patch)}
              onRemove={() => removeRow(r.id)}
              onSearch={() => onSearchRow && onSearchRow(r.id)}
              searching={!!searchingRowIds?.has(r.id)}
              canRemove={rows.length > 1}
            />
          ))}
        </div>
        <button className="sr-add" onClick={addRow}>
          <Icon name="plus" size={14} /> Add row
        </button>
      </div>

      {Array.isArray(searchWarnings) && searchWarnings.filter(w => w && w.severity === 'high').length > 0 && (
        <div
          role="alert"
          style={{
            margin: '12px 14px 0',
            padding: '10px 14px',
            borderRadius: 8,
            background: '#fff1f1',
            border: '1px solid #f5c2c2',
            color: '#a01c1c',
            display: 'flex',
            flexDirection: 'column',
            gap: 4,
            fontSize: 13,
          }}
        >
          <div style={{ display: 'inline-flex', alignItems: 'center', gap: 6, fontWeight: 600 }}>
            <span aria-hidden="true">⚠</span>
            Some platforms returned no matching ads
          </div>
          {searchWarnings
            .filter(w => w && w.severity === 'high')
            .map((w, i) => (
              <div key={i} style={{ paddingLeft: 20 }}>
                <strong>{(w.source || 'platform').toUpperCase()}</strong>
                {w.query ? <> · query <em>“{w.query}”</em></> : null}
                {' — '}
                {w.detail || 'no ads matched the criteria.'}
              </div>
            ))}
        </div>
      )}

      {latestSearchRun && (
        <div className="search-run-summary current" role="status" aria-live="polite">
          <div className="search-run-main">
            <span className="search-run-title"><Icon name="search" size={13} /> Current search output</span>
            <span className="search-run-count">{Math.max(latestSearchRun.count || 0, currentSearchAds.length)} ads output</span>
            {searchPassesOnly && currentSearchHiddenCount > 0 && (
              <span className="search-run-muted">{currentSearchHiddenCount} hidden below passing score</span>
            )}
            {latestSearchRun.duplicatesHidden > 0 && (
              <span className="search-run-muted">{latestSearchRun.duplicatesHidden} duplicates hidden</span>
            )}
          </div>
          {Array.isArray(latestSearchRun.labels) && latestSearchRun.labels.length > 0 && (
            <span className="search-run-labels" title={latestSearchRun.labels.join(', ')}>
              {latestSearchRun.labels.slice(0, 3).join(' + ')}{latestSearchRun.labels.length > 3 ? ` +${latestSearchRun.labels.length - 3}` : ''}
            </span>
          )}
          {searchSharedPoolActive && (
            <div className="search-run-shared-pool" role="status">
              <span className="ar-spinner spin" aria-hidden="true" />
              <span>
                Fetching from shared pool first
                {Number(latestSearchRun?.sharedPoolPrefill?.selected || 0) > 0
                  ? ` — ${latestSearchRun.sharedPoolPrefill.selected} ad(s) loaded so far`
                  : '…'}
              </span>
            </div>
          )}
          {searchProgressPct !== null && (
            <div className="search-run-progress-bar" title={searchVisuallyComplete ? 'Search complete' : searchProgressStatusLabel}>
              <div className="search-run-progress-head">
                {searchVisuallyComplete ? (
                  <span className="ar-complete-indicator" aria-hidden="true"><Icon name="check" size={10} /></span>
                ) : (
                  <span className="ar-spinner spin" aria-hidden="true" />
                )}
                <span className="search-run-progress-status">
                  {searchProgressStatusLabel}
                </span>
              </div>
              <div className="ar-progress-bar">
                <div className="ar-progress-fill" style={{ width: `${searchProgressPct}%` }} />
              </div>
            </div>
          )}
          {searchProgressEntries.length > 0 && (
            <div className="search-run-progress">
              {searchProgressEntries.map((entry, index) => (
                <span
                  key={`${entry.source || 'platform'}-${index}`}
                  className="search-run-muted"
                  title={entry.message || ''}
                >
                  {progressText(entry)}
                  {entry.duplicates_hidden > 0 ? ` · ${entry.duplicates_hidden} duplicates hidden` : ''}
                </span>
              ))}
            </div>
          )}
        </div>
      )}

      {currentSearchPlatforms.length > 0 && currentSearchPlatforms.map(p => (
        <PlatformPanel
          key={`current-s-${p}`}
          platform={p}
          ads={currentSearchGrouped[p]}
          selected={sel}
          onToggleAd={onToggle}
          onExpandAd={onExpandAd}
          onDislikeAd={onDislikeAd}
          minimal={minimal}
          density={density}
          showWinnersBadge={false}
        />
      ))}

      {latestSearchRun && latestSearchRun.count === 0 && !searching && (
        <div className="search-run-empty">Current search returned 0 ads.</div>
      )}

      {previousSearchPlatforms.length > 0 && (
        <div className="search-run-summary previous">
          <div className="search-run-main">
            <span className="search-run-title">Previous search ads</span>
            <span className="search-run-count">{previousSearchAds.length} retained</span>
            {searchPassesOnly && previousSearchHiddenCount > 0 && (
              <span className="search-run-muted">{previousSearchHiddenCount} hidden below passing score</span>
            )}
          </div>
        </div>
      )}

      {previousSearchPlatforms.length > 0 && previousSearchPlatforms.map(p => (
        <PlatformPanel
          key={`previous-s-${p}`}
          platform={p}
          ads={previousSearchGrouped[p]}
          selected={sel}
          onToggleAd={onToggle}
          onExpandAd={onExpandAd}
          onDislikeAd={onDislikeAd}
          minimal={minimal}
          density={density}
          showWinnersBadge={false}
        />
      ))}

      {showSearchLoading && (
        <SectionLoadingPanel
          title={searching ? 'Searching ads' : 'Loading Search ads'}
          body={searching
            ? 'Fetching ads from the selected platforms. Results will appear here as soon as the first batch returns.'
            : 'Fetching saved Manual Search data from the database.'}
        />
      )}
        </>
      )}

      {showAuto && (
        <>
      <AutoresearchPanel
        sources={arSources}
        filters={arFilters}
        onFilterChange={(key, value) => setArFilters(prev => ({ ...(prev || defaultSearchFilters()), [key]: value }))}
        onFiltersChange={(patch) => setArFilters(prev => ({ ...(prev || defaultSearchFilters()), ...(patch || {}) }))}
        onToggleSource={onToggleArSource}
        onSelectAllSources={onSelectAllArSources}
        perDirectionTarget={arTarget}
        onPerDirectionTarget={setArTarget}
        maxAdsPerDirection={arMaxAdsPerDirection}
        onMaxAdsPerDirection={setArMaxAdsPerDirection}
        maxAdsPerPlatform={arMaxAdsPerPlatform}
        onMaxAdsPerPlatform={setArMaxAdsPerPlatform}
        numDirections={arNumDirections}
        onNumDirections={setArNumDirections}
        directions={directions}
        pickedDirections={arPickedDirections}
        onTogglePickedDirection={onToggleArPickedDirection}
        onClearPickedDirections={onClearArPickedDirections}
        onSelectAllPickedDirections={onSelectAllArPickedDirections}
        onRun={onRunAutoresearch}
        onStop={onStopAutoresearch}
        running={arRunning}
        progress={arProgress}
        disabled={false}
      />

      <div className="search-run-summary current" role="group" aria-label="AutoSearch display filters">
        <div className="search-run-main">
          <span className="search-run-title"><Icon name="filter" size={13} /> AutoSearch display</span>
          <label
            className="sc-passes-toggle"
            title="Checked shows only winning AutoSearch ads. Uncheck to load a random sample of non-winning ads across directions, capped at 500."
            onClick={(e) => e.stopPropagation()}
            style={{
              display: 'inline-flex',
              alignItems: 'center',
              gap: 6,
              fontSize: 12,
              color: 'var(--fg-2)',
              userSelect: 'none',
              cursor: 'pointer',
            }}
          >
            <input
              type="checkbox"
              checked={searchPassesOnly}
              onClick={(e) => e.stopPropagation()}
              onChange={(e) => {
                e.stopPropagation();
                setSearchPassesOnly(e.target.checked);
              }}
            />
            Show only passing ads
            {searchPassesOnly && currentAutoHiddenCount + previousAutoHiddenCount > 0 && (
              <span
                style={{ color: 'var(--fg-3)' }}
                title={`${currentAutoHiddenCount + previousAutoHiddenCount} below-threshold AutoSearch ad(s) hidden`}
              >
                · {currentAutoHiddenCount + previousAutoHiddenCount} hidden
              </span>
            )}
            {!searchPassesOnly && autoFailDisplayHiddenCount > 0 && (
              <span
                style={{ color: 'var(--fg-3)' }}
                title="Only 500 non-winning AutoSearch ads are shown at once."
              >
                · random {AUTORESEARCH_FAIL_DISPLAY_LIMIT} non-winning ads
              </span>
            )}
          </label>
        </div>
      </div>

      {showAutoresearchLoading && (
        <SectionLoadingPanel
          title={arRunning ? 'AutoSearch is fetching ads' : 'Loading AutoSearch ads'}
          body={arRunning
            ? 'Fetching and scoring ads from the internet. Passing ads appear by default; toggle off the passing filter to inspect failures too.'
            : 'Fetching saved AutoSearch ads from the database.'}
        />
      )}

      {latestAutoresearchRun && (
        <div className="search-run-summary current" role="status" aria-live="polite">
          <div className="search-run-main">
            <span className="search-run-title"><Icon name="bolt" size={13} /> Current AutoSearch output</span>
            <span className="search-run-count">{latestAutoresearchRun.count || currentAutoAds.length} ads output</span>
            {latestAutoresearchRun.status && latestAutoresearchRun.status !== 'restored' && (
              <span className="search-run-muted">{latestAutoresearchRun.status}</span>
            )}
            {searchPassesOnly && currentAutoHiddenCount > 0 && (
              <span className="search-run-muted">{currentAutoHiddenCount} hidden below passing score</span>
            )}
            {!searchPassesOnly && autoFailDisplayHiddenCount > 0 && (
              <span className="search-run-muted">
                showing random {AUTORESEARCH_FAIL_DISPLAY_LIMIT} failing ads
              </span>
            )}
          </div>
          {Array.isArray(latestAutoresearchRun.labels) && latestAutoresearchRun.labels.length > 0 && (
            <span className="search-run-labels" title={latestAutoresearchRun.labels.join(', ')}>
              {latestAutoresearchRun.labels.join(' + ')}
            </span>
          )}
        </div>
      )}

      {currentAutoPlatforms.length > 0 && currentAutoPlatforms.map(p => (
        <PlatformPanel
          key={`current-a-${p}`}
          platform={p}
          ads={currentAutoGrouped[p]}
          selected={sel}
          onToggleAd={onToggle}
          onExpandAd={onExpandAd}
          onDislikeAd={onDislikeAd}
          minimal={minimal}
          density={density}
          showWinnersBadge={true}
        />
      ))}

      {showAutoLoadMore && (
        <div style={{ display: 'flex', justifyContent: 'center', gap: 12, padding: '0 14px 14px' }}>
          <button
            className="btn-secondary"
            onClick={onLoadMoreAutoresearch}
            disabled={autoresearchPage?.loading}
            title="Load the next AutoSearch page, including older retained runs"
          >
            {autoresearchPage?.loading
              ? <><Icon name="loader" size={14} className="icon-spin" /> Loading</>
              : <><Icon name="chevronDown" size={14} /> Load more ads</>}
          </button>
        </div>
      )}

      {previousAutoPlatforms.length > 0 && (
        <div className="search-run-summary previous">
          <div className="search-run-main">
            <span className="search-run-title">Previous AutoSearch ads</span>
            <span className="search-run-count">{previousAutoAds.length} retained</span>
            {searchPassesOnly && previousAutoHiddenCount > 0 && (
              <span className="search-run-muted">{previousAutoHiddenCount} hidden below passing score</span>
            )}
          </div>
        </div>
      )}

      {previousAutoPlatforms.length > 0 && previousAutoPlatforms.map(p => (
        <PlatformPanel
          key={`previous-a-${p}`}
          platform={p}
          ads={previousAutoGrouped[p]}
          selected={sel}
          onToggleAd={onToggle}
          onExpandAd={onExpandAd}
          onDislikeAd={onDislikeAd}
          minimal={minimal}
          density={density}
          showWinnersBadge={true}
        />
      ))}
        </>
      )}

      {showManual && visibleSearchPlatformCount === 0 && !searching && !showSearchLoading && !latestSearchRun && (
        <div className="empty">
          <span className="empty-display">No manual search results yet.</span>
          <p className="empty-body">Run a manual search above to collect ads.</p>
        </div>
      )}

      {showAuto && visibleAutoPlatformCount === 0 && !showAutoresearchLoading && !latestAutoresearchRun && (
        <div className="empty">
          <span className="empty-display">No AutoSearch results yet.</span>
          <p className="empty-body">Pick AutoSearch directions and press Run AutoSearch.</p>
        </div>
      )}
    </>
  );
}

function PipelineView({
  title,
  subtitle,
  ads,
  sel,
  onToggle,
  onExpandAd,
  onDislikeAd,
  density,
  minimal,
  groupByPlatform,
  emptyTitle,
  emptyBody,
  pagination,
  onLoadMore,
  preserveOrder,
  statusText,
  loadingTitle,
  loadingBody,
  startupLoading = false,
  busy = false,
  showPassVerdict = true,
}) {
  const grouped = groupByPlatform(ads);
  const activePlatforms = Object.keys(grouped).filter(p => grouped[p].length > 0);
  // Cycle: score → createdAt → arrival (raw insertion order) → score …
  // Defaults to 'score' so winners stay on top by default — mirrors the
  // PlatformPanel default used by Search and Autoresearch.
  const PIPELINE_SORTS = ['score', 'createdAt', 'arrival'];
  const [pipelineSort, setPipelineSort] = React.useState('score');
  const cyclePipelineSort = () => setPipelineSort(prev => {
    const idx = PIPELINE_SORTS.indexOf(prev);
    return PIPELINE_SORTS[(idx + 1) % PIPELINE_SORTS.length];
  });
  const pipelineSortLabel = pipelineSort === 'score'
    ? 'Score ↓'
    : pipelineSort === 'createdAt'
      ? 'Created at ↓'
      : 'Recent first';
  const orderedAds = React.useMemo(() => {
    const sortWithinMode = (items) => {
      if (pipelineSort === 'score') {
        return [...items].sort((a, b) => (b.score ?? -1) - (a.score ?? -1));
      }
      if (pipelineSort === 'createdAt') {
        return [...items].sort((a, b) => (b.createdAtMs || 0) - (a.createdAtMs || 0));
      }
      return items;
    };
    const current = ads.filter(ad => ad.currentPipelineMove);
    const older = ads.filter(ad => !ad.currentPipelineMove);
    if (current.length) {
      return [...sortWithinMode(current), ...sortWithinMode(older)];
    }
    return sortWithinMode(ads);
  }, [ads, pipelineSort]);
  const loadMoreButton = pagination?.hasMore ? (
    <button className="btn-secondary" onClick={onLoadMore} disabled={pagination.loading} style={{ alignSelf: 'center' }}>
      {pagination.loading ? <><Icon name="loader" size={14} className="icon-spin" /> Loading</> : <><Icon name="chevronDown" size={14} /> Load more</>}
    </button>
  ) : null;
  return (
    <>
      <div className="content-hdr">
        <span className="content-title">{title}</span>
        <span className="content-sub">{subtitle}</span>
      </div>
      {statusText && <div className="inline-note">{statusText}</div>}
      {!statusText && busy && <div className="inline-note">{title} is running. Results will update here as they finish.</div>}
      {activePlatforms.length === 0 && (startupLoading || pagination?.loading || busy) ? (
        <SectionLoadingPanel
          title={busy ? `${title} is running` : (loadingTitle || `Loading ${title}`)}
          body={busy ? 'Processing selected ads. Results will appear here as soon as they are ready.' : (loadingBody || 'Fetching saved ads from the database.')}
        />
      ) : activePlatforms.length === 0 ? (
        <div className="empty">
          <span className="empty-display">{emptyTitle}</span>
          <p className="empty-body">{emptyBody}</p>
        </div>
      ) : preserveOrder ? (
        <>
          <section className="platform-panel">
            <header className="pp-hdr">
              <span className="pp-chip" style={{ background: '#e6fbf7', color: '#00796b' }}>{pipelineSortLabel}</span>
              <span className="pp-count">{ads.length} ads</span>
              <div className="pp-spacer" />
              {sel.size > 0 && <span className="pp-selected">{sel.size} selected</span>}
              <button className="ghost-btn" onClick={cyclePipelineSort} title="Cycle sort: Recent → Score → Created at">
                Sort: {pipelineSortLabel}
              </button>
            </header>
            <div className={`pp-grid density-${density}`}>
              {orderedAds.map(ad => (
                <AdCard
                  key={resultKey(ad)}
                  ad={ad}
                  selected={sel.has(ad.id)}
                  onToggle={() => onToggle(ad.id)}
                  onExpand={onExpandAd}
                  onDislike={onDislikeAd}
                  minimal={minimal}
                  showPassVerdict={showPassVerdict}
                />
              ))}
            </div>
          </section>
          {loadMoreButton}
        </>
      ) : (
        <>
          {activePlatforms.map(p => (
            <PlatformPanel
              key={p}
              platform={p}
              ads={grouped[p]}
              selected={sel}
              onToggleAd={onToggle}
              onExpandAd={onExpandAd}
              onDislikeAd={onDislikeAd}
              minimal={minimal}
              density={density}
              showPassVerdict={showPassVerdict}
            />
          ))}
          {loadMoreButton}
        </>
      )}
    </>
  );
}

function AutoRemodelWorkspace({
  ads,
  scripts,
  directionOptions,
  running,
  starting,
  stage,
  lastEvent,
  adsFound,
  targetAds,
  variantsDone,
  variantsTotal,
  resultsTotal,
  resultsLoaded,
  resultsLoading,
  resultsRestoring,
  dedupeSettings = DEDUPE_DEFAULTS,
  sel,
  onToggle,
  sources,
  onToggleSource,
  onSelectAllSources,
  onRun,
  onStop,
  pageInstruction = '',
  onPageInstructionChange,
  adInstructions = {},
  onAdInstructionChange,
  strategyForAd,
  selectedVariantIds = new Set(),
  onToggleVariant,
  onLoadMore,
  onLoadHistory,
  historyLoaded,
  historyLoading,
  onEditScript,
  onRegenerate,
  onDiscard,
  onPromote,
  onPromoteAll,
  onExpandAd,
  startupLoading = false,
}) {
  const readyJobs = ads.flatMap(ad => {
    const entry = scriptEntryForAd(scripts, ad);
    if (!entry || !Array.isArray(entry.variants)) return [];
    return entry.variants.filter(v => v.jobId).map(v => v.jobId);
  });
  return (
    <>
      <div className="content-hdr">
        <span className="content-title">Auto-Remodel</span>
        <span className="content-sub">Autonomous search, scoring, filter audit, and script generation.</span>
      </div>
      <AutoRemodelPanel
        open={true}
        directionOptions={directionOptions || []}
        running={running}
        starting={starting}
        stage={stage}
        lastEvent={lastEvent}
        adsFound={adsFound}
        targetAds={targetAds}
        variantsDone={variantsDone}
        variantsTotal={variantsTotal}
        sources={sources}
        onToggleSource={onToggleSource}
        onSelectAllSources={onSelectAllSources}
        onRun={onRun}
        onStop={onStop}
        strategy={pageInstruction}
        onStrategyChange={onPageInstructionChange}
      />
      <AutoRemodelResultsView
        ads={ads}
        scripts={scripts}
        readyJobs={readyJobs}
        total={resultsTotal}
        loaded={resultsLoaded}
        loadingMore={resultsLoading}
        restoring={resultsRestoring}
        dedupeSettings={dedupeSettings}
        sel={sel}
        onToggle={onToggle}
        running={running}
        stage={stage}
        onLoadMore={onLoadMore}
        onLoadHistory={onLoadHistory}
        historyLoaded={historyLoaded}
        historyLoading={historyLoading}
        onEditScript={onEditScript}
        onRegenerate={onRegenerate}
        onDiscard={onDiscard}
        onPromote={onPromote}
        onPromoteAll={onPromoteAll}
        onExpandAd={onExpandAd}
        adInstructions={adInstructions}
        onAdInstructionChange={onAdInstructionChange}
        strategyForAd={strategyForAd}
        selectedVariantIds={selectedVariantIds}
        onToggleVariant={onToggleVariant}
        startupLoading={startupLoading}
      />
    </>
  );
}

function AutoRemodelResultsView({
  ads,
  scripts,
  readyJobs,
  total,
  loaded,
  loadingMore,
  restoring,
  dedupeSettings = DEDUPE_DEFAULTS,
  sel = new Set(),
  onToggle,
  running,
  stage,
  onLoadMore,
  onLoadHistory,
  historyLoaded,
  historyLoading,
  onEditScript,
  onRegenerate,
  onDiscard,
  onPromote,
  onPromoteAll,
  onExpandAd,
  adInstructions = {},
  onAdInstructionChange,
  strategyForAd,
  selectedVariantIds = new Set(),
  onToggleVariant,
  startupLoading = false,
}) {
  const canPromoteAll = readyJobs.length > 0;
  const uniqueAds = React.useMemo(() => uniqueAutoRemodelAds(ads, dedupeSettings), [ads, dedupeSettings]);
  // Bug 15 (Auto-Remodel) — same passing-only filter as Manual Search /
  // Autoresearch. Default ON: only ads meeting the configured passing threshold
  // true) appear. Toggle off to inspect the full Sonnet distribution.
  const [arPassesOnly, setArPassesOnly] = React.useState(true);
  const isPassingAr = React.useCallback((a) => {
    return adMeetsPassThreshold(a, true);
  }, []);
  const visibleAds = React.useMemo(() => {
    if (!arPassesOnly) return uniqueAds;
    return uniqueAds.filter(isPassingAr);
  }, [uniqueAds, arPassesOnly, isPassingAr]);
  const arHiddenCount = uniqueAds.length - visibleAds.length;
  const currentRunAds = React.useMemo(() => uniqueAds.filter(a => a.currentAutoRemodelRun), [uniqueAds]);
  const previousRunAds = React.useMemo(() => uniqueAds.filter(a => !a.currentAutoRemodelRun), [uniqueAds]);
  const visibleCurrentRunAds = React.useMemo(() => visibleAds.filter(a => a.currentAutoRemodelRun), [visibleAds]);
  const visiblePreviousRunAds = React.useMemo(() => visibleAds.filter(a => !a.currentAutoRemodelRun), [visibleAds]);
  const currentRunHiddenCount = Math.max(0, currentRunAds.length - visibleCurrentRunAds.length);
  const previousRunHiddenCount = Math.max(0, previousRunAds.length - visiblePreviousRunAds.length);
  const totalCount = Math.max(total || 0, uniqueAds.length);
  const loadedCount = loaded || 0;
  const hasMore = (total || 0) > loadedCount;
  const [previousPageSize, setPreviousPageSize] = React.useState(AUTO_REMODEL_PREVIOUS_PAGE_SIZE);
  // Bug 16 — preserve scroll position when new ads append while a run is
  // streaming. Without this, every new card appended at the top (sorted by
  // score) shifts existing cards down, which the user perceives as an
  // unwanted auto-scroll-to-top jump. Strategy: before each render where
  // the ads array grew, capture the wrapper's bounding-rect offset; after
  // the new DOM is committed, if the user is not near the top of the page
  // (>200px scrolled), add the height delta to window.scrollY so the cards
  // they were inspecting stay visually fixed in the viewport.
  const wrapperRef = React.useRef(null);
  const prevHeightRef = React.useRef(0);
  const prevAdsLenRef = React.useRef(ads.length);
  React.useLayoutEffect(() => {
    const node = wrapperRef.current;
    if (!node) return;
    const newHeight = node.getBoundingClientRect().height;
    const grew = ads.length > prevAdsLenRef.current;
    const heightDelta = newHeight - prevHeightRef.current;
    if (grew && heightDelta > 0 && running) {
      const scrolledY = window.scrollY || document.documentElement.scrollTop || 0;
      // Only adjust when the user has scrolled into the results — anyone
      // still near the top wants to watch new ads stream in normally.
      if (scrolledY > 200) {
        window.scrollTo({ top: scrolledY + heightDelta, left: 0, behavior: 'instant' in window ? 'instant' : 'auto' });
      }
    }
    prevHeightRef.current = newHeight;
    prevAdsLenRef.current = ads.length;
  }, [ads.length, running]);
  // Sort cycle: score (default) -> createdAt -> ready (script generation
  // status, original behavior). Mirrors PipelineView so users can flip
  // between "best ads" and "newest ads" with one click.
  const AR_RESULT_SORTS = ['score', 'createdAt', 'ready'];
  const [arResultSort, setArResultSort] = React.useState('score');
  const cycleArResultSort = () => setArResultSort(prev => {
    const idx = AR_RESULT_SORTS.indexOf(prev);
    return AR_RESULT_SORTS[(idx + 1) % AR_RESULT_SORTS.length];
  });
  const arResultSortLabel = arResultSort === 'score'
    ? 'Score ↓'
    : arResultSort === 'createdAt'
      ? 'Created at ↓'
      : 'Ready first';
  const sortedAds = React.useMemo(() => {
    const copy = visibleAds.slice();
    if (arResultSort === 'score') {
      copy.sort((a, b) => (b.score ?? -1) - (a.score ?? -1));
    } else if (arResultSort === 'createdAt') {
      copy.sort((a, b) => (b.createdAtMs || 0) - (a.createdAtMs || 0));
    } else {
      copy.sort((a, b) => {
        const entryA = scriptEntryForAd(scripts, a);
        const entryB = scriptEntryForAd(scripts, b);
        const readyA = entryA && Array.isArray(entryA.variants) && entryA.variants.length > 0;
        const readyB = entryB && Array.isArray(entryB.variants) && entryB.variants.length > 0;
        if (readyA !== readyB) return readyA ? -1 : 1;
        const hasTranscriptA = !!(a.transcriptRaw || (Array.isArray(a.transcript) && a.transcript.length));
        const hasTranscriptB = !!(b.transcriptRaw || (Array.isArray(b.transcript) && b.transcript.length));
        if (hasTranscriptA !== hasTranscriptB) return hasTranscriptA ? -1 : 1;
        return 0;
      });
    }
    return copy;
  }, [visibleAds, scripts, arResultSort]);
  const sortedCurrentAds = React.useMemo(
    () => sortedAds.filter(a => a.currentAutoRemodelRun),
    [sortedAds],
  );
  const sortedPreviousAds = React.useMemo(
    () => sortedAds.filter(a => !a.currentAutoRemodelRun),
    [sortedAds],
  );
  const pagedPreviousAds = React.useMemo(
    () => sortedPreviousAds.slice(0, previousPageSize),
    [sortedPreviousAds, previousPageSize],
  );
  const hasMorePreviousAds = sortedPreviousAds.length > pagedPreviousAds.length;
  // Transcribing-progress indicator across all visible Auto-Remodel ads
  // (AdSpy + BrandSearch). Keeps the spinner up until every video has its
  // transcript generated server-side.
  const arTranscriptStats = React.useMemo(() => {
    let pending = 0;
    let done = 0;
    let total = 0;
    for (const a of ads) {
      if (a.platform !== 'adspy' && a.platform !== 'brandsearch') continue;
      if (!a.creativeUrl) continue;
      total += 1;
      const status = a.transcriptStatus || null;
      const has = a.transcript && a.transcript.length > 0;
      if (has) { done += 1; continue; }
      if (status === 'fallback' || status === 'unavailable' || status === 'empty') {
        done += 1;
        continue;
      }
      pending += 1;
    }
    return { pending, done, total };
  }, [ads]);
  return (
    <div className="platform-panel" ref={wrapperRef}>
      <header className="pp-hdr">
        <span className="pp-chip" style={{ background: '#e6fbf7', color: '#00796b' }}>Auto-Remodel Results</span>
        <span className="pp-title">
          {totalCount > visibleAds.length
            ? `Showing ${visibleAds.length} of ${totalCount} ads`
            : `${visibleAds.length} ad${visibleAds.length === 1 ? '' : 's'}`}
          {arPassesOnly && arHiddenCount > 0 && (
            <span style={{ color: 'var(--fg-3)', fontWeight: 400, marginLeft: 6 }} title={`${arHiddenCount} below-threshold ad(s) hidden`}>
              · {arHiddenCount} hidden
            </span>
          )}
        </span>
        {arTranscriptStats.pending > 0 && (
          <span
            className="pp-transcribing"
            title={`Generating transcripts for ${arTranscriptStats.pending} of ${arTranscriptStats.total} video ads (Whisper/ElevenLabs). Typically 5–20s per ad.`}
            style={{
              display: 'inline-flex',
              alignItems: 'center',
              gap: 4,
              fontSize: 11,
              color: 'var(--fg-3)',
              padding: '2px 8px',
              borderRadius: 999,
              background: 'rgba(0,0,0,0.04)',
            }}
          >
            <Icon name="loader" size={11} />
            Transcribing {arTranscriptStats.done}/{arTranscriptStats.total}
          </span>
        )}
        <div className="pp-spacer" />
        <label
          className="sc-passes-toggle"
          title="Hide Auto-Remodel ads scored below the configured winner threshold."
          onClick={(e) => e.stopPropagation()}
          style={{
            display: 'inline-flex',
            alignItems: 'center',
            gap: 6,
            fontSize: 12,
            color: 'var(--fg-2)',
            userSelect: 'none',
            cursor: 'pointer',
          }}
        >
          <input
            type="checkbox"
            checked={arPassesOnly}
            onClick={(e) => e.stopPropagation()}
            onChange={(e) => { e.stopPropagation(); setArPassesOnly(e.target.checked); }}
          />
          Passing only
        </label>
        <button type="button" className="ghost-btn" onClick={cycleArResultSort} title="Cycle sort">
          Sort: {arResultSortLabel}
        </button>
        {hasMore && (
          <button type="button" className="btn-secondary" onClick={onLoadMore} disabled={loadingMore} title="Load the next Auto-Remodel result page">
            {loadingMore ? <><Icon name="loader" size={14} className="icon-spin" /> Loading</> : <><Icon name="chevronDown" size={14} /> Load more</>}
          </button>
        )}
        <button type="button" className="btn-primary" onClick={onPromoteAll} disabled={!canPromoteAll} title="Promote every ready Auto-Remodel script">
          <Icon name="film" size={14} /> Promote All to Video Queue
        </button>
      </header>
      {ads.length === 0 && (startupLoading || restoring || loadingMore || running) ? (
        <SectionLoadingPanel
          title={running ? 'Auto-Remodel is fetching ads' : restoring ? 'Restoring Auto-Remodel results' : 'Loading Auto-Remodel results'}
          body={running
            ? 'Fetching from the internet, scoring ads, and preparing remodel scripts. Results will appear here as soon as top ads are picked.'
            : 'Fetching saved Auto-Remodel ads and scripts from the database.'}
        />
      ) : ads.length === 0 ? (
        <div className="empty" style={{ margin: 0 }}>
          <span className="empty-display">No Auto-Remodel results yet</span>
          <p className="empty-body">
            {running
              ? 'The run is working. Results will appear here as soon as top ads are picked.'
              : stage === 'cancelled'
                ? 'The last run was stopped.'
                : stage === 'failed'
                  ? 'The last run failed.'
                  : 'Configure Auto-Remodel above and run it.'}
          </p>
          {onLoadHistory && !historyLoaded && (
            <button
              type="button"
              className="btn-secondary"
              onClick={onLoadHistory}
              disabled={!!historyLoading}
              title="Fetch ads from previous Auto-Remodel runs"
            >
              {historyLoading
                ? <><Icon name="loader" size={14} className="icon-spin" /> Loading older runs</>
                : <><Icon name="clock" size={14} /> Load older Auto-Remodel runs</>}
            </button>
          )}
        </div>
      ) : visibleAds.length === 0 ? (
        <div className="empty" style={{ margin: 0 }}>
          <span className="empty-display">All {ads.length} ads are below the configured winner threshold</span>
          <p className="empty-body">Toggle off “Passing only” in the header to see them.</p>
        </div>
      ) : (
        <div style={{ display: 'grid', gap: 14, padding: 14 }}>
          <div className="search-run-summary current" style={{ margin: 0 }}>
            <div className="search-run-main">
              <span className="search-run-title"><Icon name="bolt" size={13} /> Current Auto-Remodel output</span>
              <span className="search-run-count">{currentRunAds.length} ads output</span>
              {running && <span className="search-run-muted">running</span>}
              {arPassesOnly && currentRunHiddenCount > 0 && (
                <span className="search-run-muted">{currentRunHiddenCount} hidden below passing score</span>
              )}
            </div>
          </div>
          {sortedCurrentAds.map(ad => (
            <AutoRemodelResultCard
              key={resultKey(ad)}
              ad={ad}
              entry={scriptEntryForAd(scripts, ad)}
              selected={sel.has(ad.id)}
              onToggle={onToggle ? () => onToggle(ad.id) : null}
              onExpandAd={onExpandAd}
              onEditScript={onEditScript}
              onRegenerate={onRegenerate}
              onDiscard={onDiscard}
              onPromote={onPromote}
              instruction={adInstructions[ad.id] || ''}
              onInstructionChange={onAdInstructionChange}
              strategyForAd={strategyForAd}
              selectedVariantIds={selectedVariantIds}
              onToggleVariant={onToggleVariant}
            />
          ))}
          {onLoadHistory && !historyLoaded && previousRunAds.length === 0 && (
            <button
              type="button"
              className="btn-secondary"
              onClick={onLoadHistory}
              disabled={!!historyLoading}
              style={{ justifySelf: 'center' }}
              title="Fetch ads from previous Auto-Remodel runs"
            >
              {historyLoading
                ? <><Icon name="loader" size={14} className="icon-spin" /> Loading older runs</>
                : <><Icon name="clock" size={14} /> Load older Auto-Remodel runs</>}
            </button>
          )}
          {previousRunAds.length > 0 && (
            <div className="search-run-summary previous" style={{ margin: 0 }}>
              <div className="search-run-main">
                <span className="search-run-title">Previous Auto-Remodel ads</span>
                <span className="search-run-count">
                  {pagedPreviousAds.length} of {sortedPreviousAds.length} shown
                </span>
                {sortedPreviousAds.length < previousRunAds.length && (
                  <span className="search-run-muted">{previousRunAds.length} retained</span>
                )}
                {arPassesOnly && previousRunHiddenCount > 0 && (
                  <span className="search-run-muted">{previousRunHiddenCount} hidden below passing score</span>
                )}
              </div>
            </div>
          )}
          {pagedPreviousAds.map(ad => (
            <AutoRemodelResultCard
              key={resultKey(ad)}
              ad={ad}
              entry={scriptEntryForAd(scripts, ad)}
              selected={sel.has(ad.id)}
              onToggle={onToggle ? () => onToggle(ad.id) : null}
              onExpandAd={onExpandAd}
              onEditScript={onEditScript}
              onRegenerate={onRegenerate}
              onDiscard={onDiscard}
              onPromote={onPromote}
              instruction={adInstructions[ad.id] || ''}
              onInstructionChange={onAdInstructionChange}
              strategyForAd={strategyForAd}
              selectedVariantIds={selectedVariantIds}
              onToggleVariant={onToggleVariant}
            />
          ))}
          {hasMorePreviousAds && (
            <button
              className="btn-secondary"
              onClick={() => setPreviousPageSize(n => n + AUTO_REMODEL_PREVIOUS_PAGE_SIZE)}
              style={{ justifySelf: 'center' }}
              title="Load the next page of previous Auto-Remodel ads"
            >
              <Icon name="chevronDown" size={14} /> Load more previous ads
            </button>
          )}
          {hasMore && (
            <button
              className="btn-secondary"
              onClick={onLoadMore}
              disabled={loadingMore}
              style={{ justifySelf: 'center' }}
              title="Load the next Auto-Remodel result page"
            >
              {loadingMore ? <><Icon name="loader" size={14} className="icon-spin" /> Loading</> : <><Icon name="chevronDown" size={14} /> Load more</>}
            </button>
          )}
        </div>
      )}
    </div>
  );
}

function AutoRemodelResultCard({
  ad,
  entry,
  selected = false,
  onToggle,
  onExpandAd,
  onEditScript,
  onRegenerate,
  onDiscard,
  onPromote,
  instruction = '',
  onInstructionChange,
  strategyForAd,
  selectedVariantIds = new Set(),
  onToggleVariant,
}) {
  const variants = entry && Array.isArray(entry.variants) ? entry.variants : [];
  const [variantPlan, setVariantPlan] = React.useState(() => remodelPlanFromVariants(variants));
  const [regenerating, setRegenerating] = React.useState(false);
  React.useEffect(() => {
    if (variants.length > 0) setVariantPlan(remodelPlanFromVariants(variants));
  }, [variants.map(v => `${v.remodelMethod || ''}:${v.temperature ?? ''}:${v.jobId || ''}`).join('|')]);
  const status = entry && entry.status ? entry.status : (entry === 'error' ? 'error' : 'pending');
  const errorText = entry && entry.error ? entry.error : 'Regenerate from the job action when a failed variant is available.';
  const failedJobIds = entry && Array.isArray(entry.failedJobIds) ? entry.failedJobIds.filter(Boolean) : [];
  const transcriptText = ad.transcriptRaw
    || (Array.isArray(ad.transcript)
      ? ad.transcript.map(seg => seg.body || seg.t || '').filter(Boolean).join('\n')
      : (ad.transcript || ''));
  const directionWords = ad.directionSearchLabel || ad.directionLabel;
  const statusLabel = variants.length > 0
    ? `${variants.length} remodeled script${variants.length === 1 ? '' : 's'}`
    : status === 'error'
      ? 'Generation failed'
      : status === 'scored'
        ? 'Found while searching'
        : 'Generating scripts';
  const updatePlanRow = (index, patch) => {
    setVariantPlan(prev => prev.map((row, i) => (
      i === index ? remodelPlanRow(patch.remodelMethod ?? row.remodelMethod, patch.temperature ?? row.temperature) : row
    )));
  };
  const addPlanRow = () => {
    setVariantPlan(prev => {
      const seed = prev[prev.length - 1] || remodelPlanRow();
      return prev.length >= REMODEL_VARIANTS_MAX ? prev : [...prev, remodelPlanRow(seed.remodelMethod, seed.temperature)];
    });
  };
  const removePlanRow = (index) => {
    setVariantPlan(prev => (prev.length <= 1 ? prev : prev.filter((_, i) => i !== index)));
  };
  const regenerateAd = async (jobId = null) => {
    if (!onRegenerate) return;
    setRegenerating(true);
    try {
      await onRegenerate(jobId, {
        adId: ad.id,
        scriptKey: resultKey(ad),
        source: ad.source === 'auto_remodel' ? 'auto_remodel' : 'manual',
        strategy: strategyForAd ? strategyForAd(ad.id) : null,
        variantPlan,
      });
    } finally {
      setRegenerating(false);
    }
  };
  return (
    <article className={`transcript ar-result-card ${ad.currentAutoRemodelRun ? 'current-run' : ''} ${selected ? 'sel' : ''}`}>
      <header className="t-hdr ar-result-hdr">
        {onToggle && (
          <button
            type="button"
            className={`t-edit-btn ${selected ? 'primary' : ''}`}
            onClick={(e) => { e.stopPropagation(); onToggle(); }}
            title={selected ? 'Deselect Auto-Remodel video' : 'Select Auto-Remodel video'}
          >
            <Icon name={selected ? 'check' : 'plus'} size={12} />
            {selected ? 'Selected' : 'Select'}
          </button>
        )}
        <span className="t-brand">{ad.brand || ad.id}</span>
        <span className="t-meta">{ad.id}</span>
        <div className="t-hdr-spacer" />
        {onExpandAd && (
          <button
            className="t-edit-btn"
            onClick={() => onExpandAd(ad)}
            title="Open original ad details"
          >
            <Icon name="expand" size={12} /> Details
          </button>
        )}
        {ad.currentAutoRemodelRun && <span className="ar-result-status ready">Current</span>}
        <span className={`ar-result-status ${status === 'error' ? 'error' : variants.length > 0 ? 'ready' : 'pending'}`}>{statusLabel}</span>
        {typeof ad.score === 'number' && <span className="ad-score high">{ad.score.toFixed(1)}</span>}
      </header>
      <div className="ar-result-meta">
        {ad.daysRunning ? <span>{ad.daysRunning}d running</span> : null}
        {directionWords ? (
          <span className="ad-direction" title={`Search direction: ${directionWords}`}>
            <Icon name="target" size={10} />
            <span>{directionWords}</span>
          </span>
        ) : null}
        {ad.platform ? <span>{ad.platform}</span> : null}
        {ad.arrivedAtMs ? (
          <TimeAgo
            ms={ad.arrivedAtMs}
            className="ad-arrived-at"
            title={`Arrived in Auto-Remodel ${new Date(ad.arrivedAtMs).toLocaleString()}`}
          />
        ) : null}
      </div>
      <div style={{ display: 'grid', gridTemplateColumns: 'minmax(260px, 0.9fr) minmax(340px, 1.1fr)', gap: 12, alignItems: 'start', margin: '0 14px' }}>
        <AutoRemodelMediaPreview ad={ad} transcriptText={transcriptText} onExpandAd={onExpandAd} />
        <div style={{ display: 'grid', gap: 10 }}>
          <div className="inline-note" style={{ display: 'grid', gap: 6, margin: 0 }}>
            <strong>This ad instructions</strong>
            <textarea
              className="t-edit-area"
              value={instruction}
              maxLength={REMODEL_STRATEGY_MAX}
              onChange={(e) => onInstructionChange && onInstructionChange(ad.id, e.target.value)}
              placeholder="Optional instructions for regenerating this Auto-Remodel script."
              style={{ minHeight: 62 }}
            />
          </div>
          {onRegenerate && (
            <div className="inline-note" style={{ display: 'grid', gap: 10, margin: 0 }}>
              <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
                <strong>Remodel outputs</strong>
                <span style={{ fontSize: 12, color: 'var(--fg-3)' }}>
                  {variantPlan.length} output{variantPlan.length === 1 ? '' : 's'} for this ad
                </span>
                <div className="t-hdr-spacer" />
                <button
                  type="button"
                  className="t-edit-btn"
                  onClick={addPlanRow}
                  disabled={regenerating || variantPlan.length >= REMODEL_VARIANTS_MAX}
                  title="Copy the previous row"
                >
                  + Add
                </button>
              </div>
              <div style={{ display: 'grid', gap: 8 }}>
                {variantPlan.map((row, index) => {
                  const meta = remodelMethodMeta(row.remodelMethod);
                  return (
                    <div
                      key={`${index}-${row.remodelMethod}`}
                      style={{
                        display: 'grid',
                        gridTemplateColumns: '42px minmax(118px, 160px) minmax(140px, 1fr) 68px 34px',
                        gap: 8,
                        alignItems: 'center',
                      }}
                    >
                      <span className="ar-result-status ready" title={meta.label}>{meta.short}</span>
                      <select
                        className="t-edit-area"
                        value={row.remodelMethod}
                        onChange={(e) => updatePlanRow(index, { remodelMethod: e.target.value })}
                        disabled={regenerating}
                        style={{ minHeight: 34, padding: '6px 8px' }}
                      >
                        {REMODEL_METHODS.map(method => (
                          <option key={method.id} value={method.id}>{method.label} ({method.short})</option>
                        ))}
                      </select>
                      <input
                        type="range"
                        min="0"
                        max="1"
                        step="0.01"
                        value={clampRemodelTemperature(row.temperature)}
                        onChange={(e) => updatePlanRow(index, { temperature: e.target.value })}
                        disabled={regenerating}
                        title={`Temperature: ${formatRemodelTemperature(row.temperature)}`}
                      />
                      <input
                        type="number"
                        min="0"
                        max="1"
                        step="0.01"
                        value={formatRemodelTemperature(row.temperature)}
                        onChange={(e) => updatePlanRow(index, { temperature: e.target.value })}
                        disabled={regenerating}
                        style={{ width: 68 }}
                      />
                      <button
                        type="button"
                        className="t-edit-btn ghost"
                        onClick={() => removePlanRow(index)}
                        disabled={regenerating || variantPlan.length <= 1}
                        title="Remove row"
                      >
                        ×
                      </button>
                    </div>
                  );
                })}
              </div>
              <button
                className="btn-secondary"
                onClick={() => regenerateAd(variants.find(v => v.jobId)?.jobId || failedJobIds[0] || null)}
                disabled={regenerating}
                style={{ justifySelf: 'start' }}
              >
                <Icon name={regenerating ? 'loader' : 'bolt'} size={14} className={regenerating ? 'icon-spin' : ''} />
                {regenerating ? 'Regenerating...' : `Regenerate ad (${variantPlan.length} output${variantPlan.length === 1 ? '' : 's'})`}
              </button>
            </div>
          )}
        </div>
      </div>
      {(transcriptText || ad.reasoning) && (
        <details style={{ margin: '10px 14px 0', border: '1px solid var(--border-subtle)', borderRadius: 8, padding: '8px 10px', background: 'var(--bg-sunken)' }}>
          <summary style={{ cursor: 'pointer', fontSize: 12, color: 'var(--fg-2)' }}>Original transcript, Haiku score, and reasoning</summary>
          {typeof ad.score === 'number' && <p style={{ margin: '8px 0', fontSize: 12 }}>Haiku score: {ad.score.toFixed(1)}</p>}
          {ad.reasoning && <p style={{ margin: '8px 0', fontSize: 12, color: 'var(--fg-2)' }}>{ad.reasoning}</p>}
          {transcriptText && <pre style={{ whiteSpace: 'pre-wrap', maxHeight: 220, overflow: 'auto', fontSize: 12, lineHeight: 1.45 }}>{transcriptText}</pre>}
        </details>
      )}
      <div style={{ padding: 14, display: 'grid', gap: 10 }}>
        {status === 'scored' && variants.length === 0 && (
          <div className="empty" style={{ margin: 0, padding: 18 }}>
            <span className="empty-display">Found while searching</span>
            <p className="empty-body">Auto-Remodel will generate this automatically if it is selected. Use the fallback only if this card stays here without a script.</p>
            <button
              className="btn-secondary"
              onClick={() => regenerateAd(null)}
              disabled={regenerating}
            >
              <Icon name={regenerating ? 'loader' : 'bolt'} size={14} className={regenerating ? 'icon-spin' : ''} /> Fallback: generate script
            </button>
          </div>
        )}
        {status === 'pending' && variants.length === 0 && (
          <div className="empty" style={{ margin: 0, padding: 18 }}>
            <span className="empty-display">Generating scripts</span>
            <p className="empty-body">Claude Sonnet is still working on this ad.</p>
          </div>
        )}
        {status === 'error' && variants.length === 0 && (
          <div className="empty" style={{ margin: 0, padding: 18 }}>
            <span className="empty-display">Generation failed</span>
            <p className="empty-body">{errorText}</p>
            <button
              className="btn-secondary"
              onClick={() => regenerateAd(failedJobIds[0] || null)}
              disabled={regenerating}
            >
              <Icon name={regenerating ? 'loader' : 'bolt'} size={14} className={regenerating ? 'icon-spin' : ''} /> Retry this script
            </button>
          </div>
        )}
        {variants.map((v, index) => (
          <AutoRemodelScriptEditor
            key={`${ad.id}-${v.variantIdx}-${v.jobId || 'pending'}`}
            variant={v}
            label={remodelVariantLabel(v, index)}
            selectedForCompliance={!!v.jobId && selectedVariantIds.has(v.jobId)}
            onToggleCompliance={v.jobId && onToggleVariant ? () => onToggleVariant(v.jobId) : null}
            onSave={(text) => onEditScript(v.jobId, text)}
            onRegenerate={v.jobId ? () => regenerateAd(v.jobId) : null}
            onDiscard={v.jobId ? () => onDiscard(v.jobId) : null}
            onPromote={v.jobId ? () => onPromote(v.jobId) : null}
          />
        ))}
      </div>
    </article>
  );
}

function AutoRemodelMediaPreview({ ad, transcriptText, onExpandAd }) {
  const [playing, setPlaying] = React.useState(false);
  const [thumbnailBroken, setThumbnailBroken] = React.useState(false);
  const [measuredDuration, setMeasuredDuration] = React.useState('');
  const p = PLATFORMS[ad.platform] || PLATFORMS.foreplay;
  const metricChips = RESILIA_ADAPT.metricChips(ad);
  const raw = ad.raw && typeof ad.raw === 'object' ? ad.raw : {};
  const creativeUrl = ad.creativeUrl
    || raw.creative_url
    || raw.creativeUrl
    || raw.video
    || raw.video_url
    || raw.video_hd_url
    || raw.video_sd_url
    || raw.videoMediaPath
    || raw.video_media_path
    || null;
  const thumbnailUrl = ad.thumbnailUrl
    || raw.thumbnail_url
    || raw.thumbnail
    || raw.thumb_url
    || raw.thumb
    || raw.image_url
    || raw.image
    || null;
  const hasVideo = !!creativeUrl;
  const hasThumbnail = !!thumbnailUrl && !thumbnailBroken;
  const showVideoPreview = hasVideo && !hasThumbnail && !playing;
  const thumbBackground = (hasVideo || thumbnailUrl)
    ? '#111827'
    : (ad.thumb || RESILIA_ADAPT.gradFor(ad.id));
  const scoreText = typeof ad.score === 'number'
    ? (ad.score > 10 ? Math.round(ad.score) : ad.score.toFixed(1))
    : null;
  const displayDuration = ad.duration || measuredDuration;
  const formatDuration = (seconds) => {
    if (!Number.isFinite(seconds) || seconds <= 0) return '';
    const total = Math.round(seconds);
    const h = Math.floor(total / 3600);
    const m = Math.floor((total % 3600) / 60);
    const s = total % 60;
    if (h > 0) return `${h}:${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}`;
    return `${m}:${String(s).padStart(2, '0')}`;
  };
  const captureDuration = (video) => {
    const formatted = formatDuration(video.duration);
    if (formatted) setMeasuredDuration(formatted);
  };
  const sourceCopy = ad.copy || (transcriptText ? transcriptText.slice(0, 220) : '');
  const directionWords = ad.directionSearchLabel || ad.directionLabel;
  return (
    <div className="ar-source-preview">
      <div className="ar-source-media" style={{ background: thumbBackground }}>
        {playing && hasVideo ? (
          <video
            src={creativeUrl}
            poster={thumbnailUrl || undefined}
            controls
            autoPlay
            playsInline
            className="ar-source-video"
          />
        ) : (
          <>
            {hasThumbnail && (
              <img
                className="ar-source-thumb-img"
                src={thumbnailUrl}
                alt={`${ad.brand || 'Auto-Remodel'} ad thumbnail`}
                loading="lazy"
                onError={() => setThumbnailBroken(true)}
              />
            )}
            {showVideoPreview && (
              <video
                className="ar-source-video-preview"
                src={creativeUrl}
                muted
                autoPlay
                loop
                playsInline
                preload="auto"
                onLoadedData={(e) => {
                  const video = e.currentTarget;
                  captureDuration(video);
                  try { e.currentTarget.currentTime = 0.15; } catch { /* preview seek can fail on some streams */ }
                  try { video.play(); } catch { /* muted autoplay can still be blocked in rare cases */ }
                }}
                onLoadedMetadata={(e) => captureDuration(e.currentTarget)}
                onError={(e) => { e.currentTarget.style.display = 'none'; }}
              />
            )}
            {hasVideo && hasThumbnail && !displayDuration && (
              <video
                src={creativeUrl}
                preload="metadata"
                muted
                playsInline
                style={{ position: 'absolute', width: 1, height: 1, opacity: 0, pointerEvents: 'none' }}
                onLoadedMetadata={(e) => captureDuration(e.currentTarget)}
              />
            )}
            <span className="ad-plat" style={{ background: p.bg, color: p.color }}>
              <span className="ad-plat-dot" style={{ background: p.color }} />
              {p.label}
            </span>
            <button
              className="ad-play ar-source-play"
              onClick={() => {
                if (!hasVideo) return;
                if (onExpandAd) onExpandAd(ad, { autoPlay: true });
                else setPlaying(true);
              }}
              title={hasVideo ? 'Play original video in details drawer' : 'No video creative available'}
              disabled={!hasVideo}
            >
              <Icon name="play" size={16} />
            </button>
            {displayDuration && <span className="ad-duration">{displayDuration}</span>}
            {scoreText !== null && (
              <span className="ar-source-score" title="Evaluator score">
                <Icon name="trend" size={10} />
                {scoreText}
              </span>
            )}
          </>
        )}
      </div>
      <div className="ar-source-body">
        <div className="ar-source-title-row">
          <span className="ar-source-title">Original ad</span>
          {hasVideo ? <span className="ar-source-pill">video</span> : <span className="ar-source-pill muted">thumbnail</span>}
        </div>
        {directionWords && (
          <div className="ad-direction" title={`Search direction: ${directionWords}`}>
            <Icon name="target" size={10} />
            <span>{directionWords}</span>
          </div>
        )}
        {sourceCopy && <p className="ar-source-copy">{sourceCopy}</p>}
        <div className="ar-source-meta">
          {scoreText !== null && <span><Icon name="trend" size={10} /> {scoreText}</span>}
          {metricChips.map(({ label, value, title, className }) => (
            <span key={label} className={`ad-metric-chip ${className}`.trim()} title={title}>
              <span className="ad-metric-label">{label}</span>{value}
            </span>
          ))}
          {ad.daysRunning ? <span>{ad.daysRunning}d running</span> : null}
          {ad.directionLabel && ad.directionLabel !== directionWords ? <span>{ad.directionLabel}</span> : null}
        </div>
      </div>
    </div>
  );
}

function AutoRemodelScriptEditor({
  variant,
  label = 'Remodeled',
  selectedForCompliance = false,
  onToggleCompliance,
  onSave,
  onRegenerate,
  onDiscard,
  onPromote,
}) {
  const [draft, setDraft] = React.useState(variant.raw || '');
  const [expanded, setExpanded] = React.useState(false);
  const [editing, setEditing] = React.useState(false);
  const [saving, setSaving] = React.useState(false);
  React.useEffect(() => { setDraft(variant.raw || ''); }, [variant.raw, variant.jobId]);
  const startEdit = () => {
    setExpanded(true);
    setEditing(true);
  };
  const save = async () => {
    setSaving(true);
    try {
      await onSave(draft);
      setEditing(false);
    } finally {
      setSaving(false);
    }
  };
  const cancelEdit = () => {
    setDraft(variant.raw || '');
    setEditing(false);
  };
  const isPending = !!variant.pending || !variant.jobId;
  const isError = Array.isArray(variant.lines) && variant.lines.some(line => line?.tag === 'ERROR');
  return (
    <section style={{ border: '1px solid var(--border-subtle)', borderRadius: 8, overflow: 'hidden', background: 'var(--bg-sunken)' }}>
      <div style={{ display: 'flex', alignItems: 'center', gap: 8, padding: '8px 10px', borderBottom: '1px solid var(--border-subtle)' }}>
        <button
          className={`ar-script-toggle ${expanded ? 'open' : ''}`}
          onClick={() => setExpanded(v => !v)}
          title={expanded ? 'Hide remodeled script' : 'Show remodeled script'}
          aria-expanded={expanded}
        >
          <Icon name="chevronDown" size={14} />
        </button>
        {isPending && <Icon name="loader" size={13} className="icon-spin" />}
        <strong style={{ fontSize: 12 }}>{label}</strong>
        {isPending && <span style={{ fontSize: 12, color: 'var(--fg-3)' }}>Generating</span>}
        {isError && <span style={{ fontSize: 12, color: 'var(--danger)' }}>Failed</span>}
        {variant.promotedAt && <span style={{ fontSize: 12, color: 'var(--fg-3)' }}>Promoted {String(variant.promotedAt).slice(0, 16)}</span>}
        <div className="t-hdr-spacer" />
        {onToggleCompliance && !isPending && !isError && (
          <button
            type="button"
            className={`t-edit-btn ${selectedForCompliance ? 'primary' : ''}`}
            onClick={onToggleCompliance}
            title={selectedForCompliance ? 'Deselect this variant for Compliance' : 'Select this variant for Compliance'}
          >
            <Icon name={selectedForCompliance ? 'check' : 'shield'} size={12} />
            {selectedForCompliance ? 'Compliance selected' : 'Select for Compliance'}
          </button>
        )}
        {!editing && !isPending && !isError && (
          <button className="t-edit-btn" onClick={startEdit} title="Edit generated script">
            <Icon name="edit" size={12} /> Edit
          </button>
        )}
        {!editing && onRegenerate && (
          <button className="t-edit-btn" onClick={onRegenerate}>
            <Icon name="bolt" size={12} /> Regenerate
          </button>
        )}
        {!editing && onDiscard && (
          <button className="t-edit-btn ghost" onClick={onDiscard}>
            <Icon name="trash" size={12} /> Discard
          </button>
        )}
        {!editing && onPromote && !isPending && !isError && (
          <button className="t-edit-btn primary" onClick={onPromote}>
            <Icon name="film" size={12} /> {variant.promotedAt ? 'Promoted' : 'Promote to Video Queue'}
          </button>
        )}
        {editing && (
          <>
            <button className="t-edit-btn ghost" onClick={cancelEdit} disabled={saving}>
              Cancel
            </button>
            <button className="t-edit-btn primary" onClick={save} disabled={saving || !draft.trim()}>
              <Icon name="check" size={12} /> {saving ? 'Saving…' : 'Save'}
            </button>
          </>
        )}
      </div>
      {(expanded || isPending || isError) && (editing ? (
        <textarea
          className="t-edit-area"
          value={draft}
          onChange={(e) => setDraft(e.target.value)}
          spellCheck
          autoFocus
          style={{ minHeight: 180, border: 0, borderRadius: 0 }}
        />
      ) : (
        <pre className="ar-script-readonly">
          {variant.raw || (Array.isArray(variant.lines) ? variant.lines.map(line => line.body).join('\n') : '')}
        </pre>
      ))}
    </section>
  );
}

function RemodelView({
  ads,
  sel,
  onToggle,
  scripts,
  onEditScript,
  onRegenerate,
  onDiscard,
  onPromote,
  pageInstruction = '',
  onPageInstructionChange,
  adInstructions = {},
  onAdInstructionChange,
  strategyForAd,
  selectedVariantIds = new Set(),
  onToggleVariant,
  pagination,
  onRefresh,
  onLoadMore,
  statusText,
  busy = false,
}) {
  const entryForAd = (ad) => {
    const entry = scriptEntryForAd(scripts, ad);
    if (entry && Array.isArray(entry.variants)) {
      return entry;
    }
    if (entry && typeof entry === 'object' && entry.jobId) {
      return {
        status: 'ready',
        variants: [{ jobId: entry.jobId, variantIdx: 0, raw: entry.raw, lines: entry.lines }],
      };
    }
    if (entry === 'error') return { status: 'error', variants: [] };
    if (Array.isArray(entry) && entry.length) {
      return { status: 'ready', variants: [{ jobId: null, variantIdx: 0, raw: '', lines: entry }] };
    }
    return entry || { status: 'pending', variants: [] };
  };
  const isAutoRemodel = ads.some(a => a.source === 'auto_remodel');
  const orderedAds = React.useMemo(() => {
    const current = ads.filter(ad => ad.currentPipelineMove);
    const older = ads.filter(ad => !ad.currentPipelineMove);
    return current.length ? [...current, ...older] : ads;
  }, [ads]);
  return (
    <>
      <div className="content-hdr">
        <span className="content-title">{isAutoRemodel ? 'Auto-Remodel Results' : 'Remodel'}</span>
        <span className="content-sub">Review final scripts, edit inline, regenerate, discard, or promote to the video queue.</span>
        <div className="spacer" />
        <button className="btn-secondary" onClick={onRefresh} disabled={busy || pagination?.loading}>
          <Icon name="loader" size={14} className={pagination?.loading ? 'icon-spin' : ''} />
          Refresh
        </button>
      </div>
      <div className="inline-note" style={{ display: 'grid', gap: 6 }}>
        <strong>Page remodel instructions</strong>
        <textarea
          className="t-edit-area"
          value={pageInstruction}
          maxLength={REMODEL_STRATEGY_MAX}
          onChange={(e) => onPageInstructionChange && onPageInstructionChange(capRemodelInstruction(e.target.value))}
          placeholder="Optional instructions for every ad in this Remodel batch, e.g. make the hook more direct, keep claims conservative, or shift tone."
          style={{ minHeight: 72 }}
        />
      </div>
      {statusText && <div className="inline-note">{statusText}</div>}
      {!statusText && busy && <div className="inline-note">Remodel is running. Pending cards are shown here while scripts generate.</div>}
      {ads.length === 0 ? (
        <div className="empty">
          <span className="empty-display">Nothing remodeled yet</span>
          <p className="empty-body">Select ads in Filter and press Run Remodel, Pass without Filtering from Research, or kick off Auto-Remodel.</p>
        </div>
      ) : (
        <>
          {orderedAds.map(ad => (
            <AutoRemodelResultCard
              key={resultKey(ad)}
              ad={ad}
              entry={entryForAd(ad)}
              selected={sel.has(ad.id)}
              onToggle={onToggle ? () => onToggle(ad.id) : null}
              onExpandAd={null}
              onEditScript={onEditScript}
              onRegenerate={onRegenerate}
              onDiscard={onDiscard}
              onPromote={onPromote}
              instruction={adInstructions[ad.id] || ''}
              onInstructionChange={onAdInstructionChange}
              strategyForAd={strategyForAd}
              selectedVariantIds={selectedVariantIds}
              onToggleVariant={onToggleVariant}
            />
          ))}
          {pagination?.hasMore && (
            <button className="btn-secondary" onClick={onLoadMore} disabled={pagination.loading} style={{ alignSelf: 'center' }}>
              {pagination.loading ? <><Icon name="loader" size={14} className="icon-spin" /> Loading</> : <><Icon name="chevronDown" size={14} /> Load more</>}
            </button>
          )}
        </>
      )}
    </>
  );
}

function ComplianceView({
  jobs,
  sel,
  onToggle,
  strictness,
  onStrictnessChange,
  busy = false,
  runState,
  pagination,
  onRefresh,
  onLoadMore,
  onEditTranscript,
  onDiscard,
}) {
  const orderedAdGroups = React.useMemo(() => {
    const grouped = new Map();
    (jobs || []).forEach(job => {
      const key = job.ad_id || `job-${job.id}`;
      if (!grouped.has(key)) grouped.set(key, []);
      grouped.get(key).push(job);
    });
    return [...grouped.values()]
      .map(group => group.sort((a, b) => {
        const createdCmp = String(b.created_at || '').localeCompare(String(a.created_at || ''));
        if (createdCmp !== 0) return createdCmp;
        const av = Number(a.remodel_job?.variant_idx ?? a.payload?.variant_idx ?? a.id);
        const bv = Number(b.remodel_job?.variant_idx ?? b.payload?.variant_idx ?? b.id);
        return av - bv;
      }))
      .sort((a, b) => {
        const aRunning = a.some(job => ['pending', 'running'].includes(job.status));
        const bRunning = b.some(job => ['pending', 'running'].includes(job.status));
        if (aRunning !== bRunning) return aRunning ? -1 : 1;
        const aLatest = a.reduce((latest, job) => String(job.created_at || '') > latest ? String(job.created_at || '') : latest, '');
        const bLatest = b.reduce((latest, job) => String(job.created_at || '') > latest ? String(job.created_at || '') : latest, '');
        return bLatest.localeCompare(aLatest);
      });
  }, [jobs]);
  const statusText = runState?.running
    ? `Compliance is running: ${runState?.state?.completed || 0}/${runState?.state?.job_count || runState?.run?.total || 0} checked.`
    : null;
  const adCount = new Set((jobs || []).map(job => job.ad_id).filter(Boolean)).size;
  return (
    <>
      <div className="content-hdr">
        <span className="content-title">Compliance</span>
        <span className="content-sub">
          {jobs.length
            ? `${jobs.length} separate compliance decision${jobs.length === 1 ? '' : 's'} across ${adCount} ad${adCount === 1 ? '' : 's'}.`
            : 'Check selected remodel variants against the video compliance framework. Only non-compliant scripts are regenerated.'}
        </span>
        <div className="spacer" />
        <label className="compliance-select-wrap">
          <span>Strictness</span>
          <select
            className="compliance-select"
            value={strictness}
            onChange={(e) => onStrictnessChange(e.target.value)}
          >
            <option value="strict">Strict</option>
            <option value="standard">Standard</option>
            <option value="lenient">Lenient</option>
          </select>
        </label>
        <button className="btn-secondary" onClick={onRefresh} disabled={busy || pagination?.loading}>
          <Icon name="loader" size={14} className={pagination?.loading ? 'icon-spin' : ''} />
          Refresh
        </button>
      </div>
      <div className="inline-note">
        Select ads in Remodel or Auto-Remodel, then choose Send to Compliance. Platform is derived from the saved ad data; strictness is selected here.
      </div>
      {statusText && <div className="inline-note">{statusText}</div>}
      {jobs.length === 0 ? (
        <div className="empty">
          <span className="empty-display">No compliance checks yet</span>
          <p className="empty-body">Send selected remodel variants here to store the ad, original script, and compliant final transcript.</p>
        </div>
      ) : (
        <>
          {orderedAdGroups.map(group => (
            <ComplianceAdCard
              key={group[0]?.ad_id || group[0]?.id}
              jobs={group}
              sel={sel}
              onToggle={onToggle}
              onEditTranscript={onEditTranscript}
              onDiscard={onDiscard}
            />
          ))}
          {pagination?.hasMore && (
            <button className="btn-secondary" onClick={onLoadMore} disabled={pagination.loading} style={{ alignSelf: 'center' }}>
              {pagination.loading ? <><Icon name="loader" size={14} className="icon-spin" /> Loading</> : <><Icon name="chevronDown" size={14} /> Load more</>}
            </button>
          )}
        </>
      )}
    </>
  );
}

function ComplianceAdCard({ jobs, sel, onToggle, onEditTranscript, onDiscard }) {
  const sortedJobs = React.useMemo(() => [...(jobs || [])].sort((a, b) => {
    const createdCmp = String(b.created_at || '').localeCompare(String(a.created_at || ''));
    if (createdCmp !== 0) return createdCmp;
    const av = Number(a.remodel_job?.variant_idx ?? a.payload?.variant_idx ?? a.id);
    const bv = Number(b.remodel_job?.variant_idx ?? b.payload?.variant_idx ?? b.id);
    return av - bv;
  }), [jobs]);
  const preferredJobId = (
    sortedJobs.find(job => sel.has(job.id))
    || sortedJobs.find(job => job.status === 'done')
    || sortedJobs[0]
  )?.id;
  const [activeJobId, setActiveJobId] = React.useState(preferredJobId);
  React.useEffect(() => {
    if (!sortedJobs.some(job => job.id === activeJobId)) {
      setActiveJobId(preferredJobId);
    } else if (preferredJobId && sel.has(preferredJobId) && !sel.has(activeJobId)) {
      setActiveJobId(preferredJobId);
    }
  }, [activeJobId, preferredJobId, sel, sortedJobs]);
  const activeJob = sortedJobs.find(job => job.id === activeJobId) || sortedJobs[0];
  if (!activeJob) return null;
  return (
    <ComplianceJobCard
      job={activeJob}
      variantJobs={sortedJobs}
      activeJobId={activeJob.id}
      onActiveJobChange={setActiveJobId}
      selected={sel.has(activeJob.id)}
      onToggle={() => onToggle(activeJob.id)}
      onEditTranscript={onEditTranscript}
      onDiscard={onDiscard}
    />
  );
}

function ComplianceJobCard({
  job,
  variantJobs = [],
  activeJobId,
  onActiveJobChange,
  selected,
  onToggle,
  onEditTranscript,
  onDiscard,
}) {
  const ad = job.ad ? { ...RESILIA_ADAPT.adaptEvaluatedAd(job.ad), source: 'compliance' } : pipelineFallbackAd(job.ad_id, 'compliance');
  const finalText = job.final_transcript || (job.compliant ? job.input_script : job.regenerated_transcript) || '';
  const inputText = job.input_script || '';
  const status = job.status || 'pending';
  const statusClass = status === 'failed' ? 'error' : status === 'done' ? 'ready' : 'pending';
  const statusLabel = status === 'done'
    ? (job.compliant ? 'Compliant' : 'Regenerated')
    : status === 'failed'
      ? 'Failed'
      : status === 'running'
        ? 'Checking'
        : 'Pending';
  const transcriptText = ad.transcriptRaw
    || (Array.isArray(ad.transcript)
      ? ad.transcript.map(seg => seg.body || seg.t || '').filter(Boolean).join('\n')
      : (ad.transcript || ''));
  const variant = job.remodel_job || {};
  const fullOutputPreview = remodelOutputWithComplianceScript(
    variant.response_text,
    finalText || inputText,
  );
  const variantLabel = remodelVariantLabel({
    variantIdx: variant.variant_idx,
    remodelMethod: variant.remodel_method,
    temperature: variant.temperature,
  }, Math.max(0, Number(variant.variant_idx || 1) - 1));
  const variantOptionLabel = (item, index) => {
    const itemVariant = item.remodel_job || {};
    return remodelVariantLabel({
      remodelMethod: itemVariant.remodel_method,
      temperature: itemVariant.temperature,
    }, Math.max(0, Number(itemVariant.variant_idx || index + 1) - 1));
  };
  return (
    <article className={`transcript compliance-card ${selected ? 'sel' : ''}`} data-ad-id={ad.id}>
      <header className="t-hdr ar-result-hdr">
        <button
          type="button"
          className={`t-edit-btn ${selected ? 'primary' : ''}`}
          onClick={(e) => { e.stopPropagation(); onToggle(); }}
          title={selected ? 'Deselect compliance job' : 'Select compliance job'}
        >
          <Icon name={selected ? 'check' : 'plus'} size={12} />
          {selected ? 'Selected' : 'Select'}
        </button>
        <span className="t-brand">{ad.brand || ad.id}</span>
        <span className="t-meta">{ad.id}</span>
        {variantJobs.length > 1 ? (
          <label className="compliance-variant-select-wrap">
            <span>Remodel variant</span>
            <select
              className="compliance-select"
              value={activeJobId || job.id}
              onChange={(e) => onActiveJobChange(Number(e.target.value))}
            >
              {variantJobs.map((item, index) => (
                <option key={item.id} value={item.id}>
                  {variantOptionLabel(item, index)}
                </option>
              ))}
            </select>
          </label>
        ) : null}
        <span className="ar-result-status ready">{variantLabel}</span>
        {variantJobs.length > 1 ? <span className="ar-result-status ready">{variantJobs.length} variants</span> : null}
        <span className={`ar-result-status ${statusClass}`}>{statusLabel}</span>
        <span className="ar-result-status ready">{job.strictness || 'strict'}</span>
        {job.platform ? <span className="t-meta">{job.platform}</span> : null}
      </header>
      <div className="compliance-grid">
        <div className="compliance-left-stack">
          <AutoRemodelMediaPreview ad={ad} transcriptText={transcriptText} onExpandAd={null} />
          <details className="compliance-full-output">
            <summary className="compliance-full-output-head">
              <strong>Full remodel output</strong>
              <span>with compliant `### REMODELED SCRIPT` inserted</span>
            </summary>
            <pre>{fullOutputPreview || transcriptText || 'Waiting for remodel output.'}</pre>
          </details>
        </div>
        <div className="compliance-script-stack">
          <div className="inline-note" style={{ margin: 0 }}>
            <strong>Separate compliance decision for this remodel</strong>
            <div style={{ marginTop: 6, color: 'var(--fg-3)', fontSize: 12 }}>
              {variantLabel} · Remodel job {job.remodel_job_id} · Compliance job {job.id}
              {variantJobs.length > 1 ? ' · Use the dropdown above to review the other compliant remodels for this ad.' : ''}
            </div>
          </div>
          <details className="compliance-details">
            <summary>Input script sent to compliance</summary>
            <pre>{inputText || 'No extracted script saved.'}</pre>
          </details>
          <div className="compliance-decision">
            <strong>Decision reason</strong>
            <p>{job.decision_reason || (status === 'done' ? 'No decision reason was returned.' : 'Waiting for compliance decision.')}</p>
          </div>
          <ComplianceFinalTranscript
            job={job}
            finalText={finalText}
            status={status}
            onSave={onEditTranscript ? (text) => onEditTranscript(job.id, text) : null}
            onDiscard={onDiscard ? () => onDiscard(job.id) : null}
          />
        </div>
      </div>
    </article>
  );
}

function ComplianceFinalTranscript({ job, finalText, status, onSave, onDiscard }) {
  const [editing, setEditing] = React.useState(false);
  const [draft, setDraft] = React.useState(finalText || '');
  const [saving, setSaving] = React.useState(false);
  React.useEffect(() => {
    setDraft(finalText || '');
    setEditing(false);
  }, [finalText, job.id]);
  const canEdit = status === 'done' && !!onSave;
  const save = async () => {
    if (!onSave) return;
    setSaving(true);
    try {
      await onSave(draft);
      setEditing(false);
    } finally {
      setSaving(false);
    }
  };
  const cancelEdit = () => {
    setDraft(finalText || '');
    setEditing(false);
  };
  return (
    <div className="compliance-final">
      <div className="compliance-final-head">
        <strong>Final compliant transcript</strong>
        {job.compliant === true ? <span className="ar-result-status ready">No rewrite</span> : null}
        {job.compliant === false ? <span className="ar-result-status pending">Edited</span> : null}
        <div className="t-hdr-spacer" />
        {canEdit && !editing && (
          <button className="t-edit-btn" onClick={() => setEditing(true)} title="Edit compliant transcript">
            <Icon name="edit" size={12} /> Edit
          </button>
        )}
        {canEdit && !editing && onDiscard && (
          <button className="t-edit-btn ghost" onClick={onDiscard} title="Discard this compliance result">
            <Icon name="trash" size={12} /> Discard
          </button>
        )}
        {editing && (
          <>
            <button className="t-edit-btn ghost" onClick={cancelEdit} disabled={saving}>
              Cancel
            </button>
            <button className="t-edit-btn primary" onClick={save} disabled={saving || !draft.trim()}>
              <Icon name="check" size={12} /> {saving ? 'Saving…' : 'Save'}
            </button>
          </>
        )}
      </div>
      {status === 'failed' ? (
        <p className="empty-body">{job.error || 'Compliance check failed.'}</p>
      ) : editing ? (
        <textarea
          className="t-edit-area"
          value={draft}
          onChange={(e) => setDraft(e.target.value)}
          spellCheck
          autoFocus
          style={{ minHeight: 180 }}
        />
      ) : finalText ? (
        <div className="t-script">
          {scriptLinesFromText(finalText).map((line, i) => (
            <div key={i} className="t-line">
              <span>{line.body}</span>
            </div>
          ))}
        </div>
      ) : (
        <p className="empty-body">Waiting for compliance output.</p>
      )}
    </div>
  );
}

function VideoView({ ads }) {
  return (
    <>
      <div className="content-hdr">
        <span className="content-title">Video-Conversion</span>
        <span className="content-sub">Queued for the video-editing pipeline (backend in development).</span>
      </div>
      {ads.length === 0 ? (
        <div className="empty">
          <span className="empty-display">Queue is empty</span>
          <p className="empty-body">Select remodeled scripts in Remodel and press Convert to Video.</p>
        </div>
      ) : (
        <div className="platform-panel">
          <header className="pp-hdr">
            <span className="pp-chip" style={{ background: '#fdf1da', color: '#a56c00' }}>
              <span className="pp-dot" style={{ background: '#d97706' }} />
              Queued
            </span>
            <span className="pp-count">{ads.length} jobs</span>
            <div className="pp-spacer" />
            <span className="pp-dedupe">ETA ~{ads.length * 90}s</span>
          </header>
          <div style={{ padding: '14px 16px', display: 'flex', flexDirection: 'column', gap: 10 }}>
            {ads.map(ad => (
              <div key={ad.complianceJobId ? `c-${ad.complianceJobId}` : ad.id} style={{ display: 'flex', gap: 12, alignItems: 'center', padding: 10, background: 'var(--bg-sunken)', borderRadius: 8, border: '1px solid var(--border-subtle)' }}>
                <div style={{ width: 40, height: 50, background: ad.thumb, borderRadius: 6, flexShrink: 0 }} />
                <div style={{ flex: 1, display: 'flex', flexDirection: 'column', gap: 3 }}>
                  <span style={{ fontSize: 13, fontWeight: 600 }}>{ad.brand}</span>
                  <span style={{ fontSize: 11, color: 'var(--fg-3)', fontFamily: 'var(--font-mono)' }}>
                    {ad.id} · {ad.complianceJobId ? 'compliance-approved' : 'remodeled'} · awaiting editor
                  </span>
                  {ad.finalTranscript ? (
                    <span style={{ fontSize: 12, color: 'var(--fg-2)' }}>
                      {String(ad.finalTranscript).slice(0, 180)}{String(ad.finalTranscript).length > 180 ? '…' : ''}
                    </span>
                  ) : null}
                </div>
                <span className="pill">Pending</span>
              </div>
            ))}
          </div>
        </div>
      )}
    </>
  );
}

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